Skip to content

Objects: explicit no-op marking, enforceable tombstone exception, RTO4b2a reset clarifications + UTS cases - #515

Open
sacOO7 wants to merge 3 commits into
objects/get-sync-wait-failure-and-sync-fixesfrom
objects/noop-marking-and-tombstone-reset-clarifications
Open

Objects: explicit no-op marking, enforceable tombstone exception, RTO4b2a reset clarifications + UTS cases#515
sacOO7 wants to merge 3 commits into
objects/get-sync-wait-failure-and-sync-fixesfrom
objects/noop-marking-and-tombstone-reset-clarifications

Conversation

@sacOO7

@sacOO7 sacOO7 commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Note

Stacked on #514 (objects/get-sync-wait-failure-and-sync-fixes) — review after that PR; only the two commits of this branch are new.

Problem

Follow-up to #514, addressing its review feedback and the gaps found while bringing ably-js into conformance with the no-op update clauses:

  1. Inconsistent no-op marking (review feedback on Objects: specify get() sync-wait failure (RTO23c1), malformed sync serial handling (RTO5a6) and related clauses, with UTS unit cases #514). The new RTLC14c/RTLM22c said "return a … update marked as a no-op per RTLO4b4b", while every other no-op-producing clause in the spec (RTLC6e1, RTLC9h, RTLC16d, RTLM6e1, RTLM7h, RTLM16b, …) uses the explicit form "return a Live*Update object with *.noop set to true". Divergent SDK interpretations were possible. (Raised by Copilot in Objects: specify get() sync-wait failure (RTO23c1), malformed sync serial handling (RTO5a6) and related clauses, with UTS unit cases #514 (comment).)
  2. The tombstone exception was unenforceable prose. A literal implementation follows RTLO4e5 (compute the tombstone update per RTLC14/RTLM22), hits the new no-op exception for an already-zero counter / already-empty map, and RTLO4b4c1 then suppresses the update — so the RTLO4b4c3c listener teardown never fires. All three SDKs independently work around exactly this in code (ably-js guards the caller, ably-cocoa bypasses the diff on the tombstone path, ably-java synthesizes a typed update when the diff is a no-op) — a strong signal the boundary was under-specified.
  3. RTO4b2a (the ATTACHED/no-objects reset update) was silent on two edges: whether already-tombstoned entries are reported as removed, and what happens when no keys were removed (an already-empty root). ably-cocoa filters tombstoned entries and suppresses the empty update; ably-java suppresses via its diff; ably-js emitted a spurious {update: {}}.
  4. No UTS coverage existed for the no-op diff clauses, RTO20d4, tombstoning an already-zero/empty object (the branch every SDK special-cases — untested in all three), or the empty-root reset.

Changes

specifications/objects-features.md

Clause Change
RTLC14c / RTLM22c (reworded) No-op marking now explicit — "return a Live*Update object with *.noop set to true ([RTLO4b4b]), as in [RTLC9h] / [RTLM16b]" — matching the spec-wide convention. The tombstone exception is now normative and mechanical: the exception "must not be applied when the diff is computed for a tombstone per [RTLO4e5]: the resulting tombstone update ([RTLO4b4e]) must not be marked as a no-op", so it is delivered and drives the [RTLO4b4c3c] teardown even when the data was already zero/empty.
RTO4b2a (clarified) Two sentences: only non-tombstoned entries are reported as removed (consistent with [RTLM22b]'s non-tombstoned-visibility rule), and if no keys were removed the update has no changed keys and is a no-op per [RTLM22c]/[RTLO4b4b], so nothing is emitted.

uts/objects/unit/ — 7 new unit cases

  • internal_live_counter.md: RTLC14c/zero-delta-diff-is-noop-0 (identical-state override → no-op, not amount: 0); RTLO5/tombstone-zero-value-counter-emits-update-0 (the exception's flip side: tombstoning a zero counter still yields a non-no-op update with tombstone == true, amount == 0).
  • internal_live_map.md: RTLM22c/empty-diff-is-noop-0 (identical non-tombstoned entries — only the timeserial differs, which RTLM22b3 does not compare); RTLO5/tombstone-empty-map-emits-update-0 (all-entries-tombstoned map).
  • realtime_object.md: RTO20d4/empty-synthetic-list-skips-sync-wait-0 (all-null ACK serials while SYNCING → the operation resolves without the sync ever completing, proving the RTO20e wait was skipped).
  • live_object_subscribe.md: RTLO4b4c3c/tombstone-zero-value-counter-tears-down-0 (subscriber receives the zero-amount tombstone update and listeners are deregistered).
  • objects_pool.md: RTO4b2a/reset-of-empty-root-emits-no-update-0 (with a second-pool liveness control).

SDK status

SDK Production Tests
ably-js Fixed in the companion PR: ably/ably-js#2288 (no-op collapse in the diff, tombstone synthesis, RTO20d4 guard) 7 UTS ports in the same PR
ably-cocoa Already conformant at every site (diff collapse, tombstone bypass, empty-reset suppression + tombstone filtering) 7 UTS ports on feature/liveobjects-implementation
ably-java Already conformant (in-diff collapse + tombstone() NoOp-synthesis) 7 UTS ports on its branch; the tombstone-synthesis branch gains its first-ever coverage

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refines the LiveObjects/Objects specification around no-op updates and tombstone behavior (making tombstone teardown reliably enforceable), clarifies RTO4b2a reset edge cases, and adds UTS unit cases to cover previously untested branches.

Changes:

  • Makes no-op marking explicit (*.noop = true) for counter/map diffs and adds a normative carve-out so tombstone diffs are not collapsed to no-op.
  • Clarifies RTO4b2a behavior for tombstoned entries and the “empty root reset” case.
  • Adds 7 UTS unit test cases covering empty-diff no-ops, tombstone-on-zero/empty behavior, empty synthetic list publishAndApply behavior, listener teardown, and empty-root reset behavior.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
specifications/objects-features.md Updates spec wording for explicit no-op marking, tombstone carve-outs, and RTO4b2a reset clarifications
uts/objects/unit/realtime_object.md Adds UTS case for RTO20d4 (empty synthetic list skipping sync wait)
uts/objects/unit/objects_pool.md Adds UTS case for RTO4b2a (empty-root reset emits no update)
uts/objects/unit/live_object_subscribe.md Adds UTS case asserting tombstone update delivery + listener deregistration on already-zero counter
uts/objects/unit/internal_live_map.md Adds UTS cases for tombstone-empty-map update and RTLM22c empty-diff no-op
uts/objects/unit/internal_live_counter.md Adds UTS cases for tombstone-zero-counter update and RTLC14c zero-delta no-op

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

- `(RTO4b1)` All objects except the one with id `root` must be removed from the internal `ObjectsPool`
- `(RTO4b2)` The data for the `InternalLiveMap` with id `root` must be set to the value described in [RTLM4c](#RTLM4c). Note that the client SDK must not create a new `InternalLiveMap` instance with id `root`; it must only clear the internal data of the existing `InternalLiveMap` with id `root`
- `(RTO4b2a)` Emit a `LiveMapUpdate` object for the `InternalLiveMap` with ID `root`, with `LiveMapUpdate.update` consisting of entries for the keys that were removed, each set to `removed`, and without populating `LiveMapUpdate.objectMessage`
- `(RTO4b2a)` Emit a `LiveMapUpdate` object for the `InternalLiveMap` with ID `root`, with `LiveMapUpdate.update` consisting of entries for the keys that were removed, each set to `removed`, and without populating `LiveMapUpdate.objectMessage`. Only the keys of non-tombstoned entries are reported as `removed`, consistent with the non-tombstoned-visibility rule in [RTLM22b](#RTLM22b): entries that were already tombstoned were not part of the user-visible map data, so their removal is not reported. If no keys were removed (that is, the `root` map was already empty), the computed `LiveMapUpdate.update` contains no changed keys and is therefore a no-op per [RTLM22c](#RTLM22c) ([RTLO4b4b](#RTLO4b4b)), so no update is emitted.
sacOO7 added a commit to ably/ably-cocoa that referenced this pull request Aug 11, 2026
…on tail (RTO4b4/RTO5c)

- Port the seven no-op-package UTS unit cases: RTLC14c/RTLM22c (zero-delta/
  empty diffs are no-op updates), RTO20d4 (empty synthetic list skips the
  RTO20e wait), RTLO5 tombstone-of-zero/empty-object cases (the tombstone
  update must not be no-op-marked), RTLO4b4c3c teardown for a zero-valued
  counter, and RTO4b2a (reset of an already-empty root emits no update).
  Production already conforms at every site; the RTO20d4 port drives the
  real publishAndApply pipeline via a new optional ObjectsUTSCoreSDK
  publishHandler.
- Extract the shared nosync_completeSync() completion tail used by both the
  ATTACHED(HAS_OBJECTS=0) path (RTO4b4) and the OBJECT_SYNC completion path
  (RTO5c), so the two cannot drift; add a native test proving the ATTACHED
  path resolves parked publishAndApply sync waiters.
- Retire the native RTO5a5 twin superseded by the UTS port (bumped to two
  objects to keep its multi-object coverage); cross-link the counter
  tombstone-bypass comments to the RTLC14c zero-delta exception.

Spec changes: ably/specification#515
Companion ably-js fix: ably/ably-js#2288
sacOO7 added a commit to ably/ably-java that referenced this pull request Aug 11, 2026
Port the seven no-op-package UTS cases: RTLC14c/RTLM22c (zero-delta/empty
diffs are no-op updates, never delivered), RTO20d4 (empty synthetic list
skips the RTO20e sync wait), the RTLO5 tombstone-of-zero/empty-object cases
and the RTLO4b4c3c zero-valued-counter teardown case (covering
BaseRealtimeLiveObject.tombstone()'s NoOp-synthesis branch for the first
time), and RTO4b2a (reset of an already-empty root emits no update; verified
with a second-pool liveness control via a backward-compatible optional
target parameter on the ObjectsPoolTest processAttached helper).

Production already conforms at every site; test-only change.

Spec changes: ably/specification#515
Companion ably-js fix: ably/ably-js#2288
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants