Skip to content

LiveObjects: suppress no-op diff updates (RTLC14c/RTLM22c) and skip the sync wait for an empty synthetic list (RTO20d4) - #2288

Open
sacOO7 wants to merge 1 commit into
liveobjects/rto23c1-get-sync-wait-failurefrom
liveobjects/noop-diffs-and-empty-synthetic-list
Open

LiveObjects: suppress no-op diff updates (RTLC14c/RTLM22c) and skip the sync wait for an empty synthetic list (RTO20d4)#2288
sacOO7 wants to merge 1 commit into
liveobjects/rto23c1-get-sync-wait-failurefrom
liveobjects/noop-diffs-and-empty-synthetic-list

Conversation

@sacOO7

@sacOO7 sacOO7 commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Note

Stacked on #2284 (liveobjects/rto23c1-get-sync-wait-failure) — this branch bases there because both touch the publishAndApply sync-wait region; review after that PR. Spec companion: ably/specification#515 (stacked on ably/specification#514).

Problem

Three related behaviours diverged from the Objects spec's no-op update clauses, each delivering a spurious or wasteful result:

  1. Spurious update events on re-sync of unchanged data (RTLC14c / RTLM22c). When an OBJECT_SYNC re-delivers state identical to what an object already holds (reconnect, re-attach with HAS_OBJECTS), _updateFromDataDiff produced { update: { amount: 0 } } / { update: {} } and _applySync delivered them to subscribers — change events for no change. The spec requires such diffs to be no-op updates (noop: true), which notifyUpdated already suppresses.
  2. Spurious empty update on reset of an already-empty root (RTO4b2a). The ATTACHED-without-HAS_OBJECTS reset routes through the same diff (resetToInitialPoolclearObjectsDatanotifyUpdated), so an already-empty root emitted { update: {} }. The clarified RTO4b2a specifies suppression (ably-cocoa and ably-java already suppress).
  3. publishAndApply parked on a wait with nothing to apply (RTO20d4). When every serial in the PublishResult is null (each skipped per RTO20d1), the synthetic message list is empty, yet the code still entered the RTO20e sync wait — an unnecessary park that would surface a spurious 92008 if the channel dropped meanwhile. (Defensive: all-null serials are not an expected server behaviour today.)

Solution

  • The no-op collapse lives in _updateFromDataDiff itself (both LiveCounter and LiveMap), so every caller — the overrideWithObjectState re-sync path, the RTO4b reset, and any future call site — is covered by one change. The overrideWithObjectState sites gain a no-op passthrough (mirroring their existing RTLC6e/RTLM6e terminal-noop returns) so objectMessage is only stamped on real updates.
  • The tombstone path is the one mandated exception (the RTLC14c/RTLM22c advisory: the exception "must not be applied when the diff is computed for a tombstone per RTLO4e5"). A tombstone update must never be no-op-marked — notifyUpdated suppresses no-ops before its tombstone branch, so the RTLO4b4c3c listener teardown would silently be skipped for an already-zero counter / already-empty map. tombstone() therefore synthesizes the typed no-change update ({ update: { amount: 0 } } / { update: {} }) via a new protected abstract _createNoChangeUpdate() hook when the diff is a no-op, then stamps tombstone = true. The hook follows the class's existing template-method design (it joins seven existing abstract members) and matches ably-java's synthesis semantics (BaseRealtimeLiveObject.tombstone()); a base-class type-switch was rejected because LiveObject instances carry no type discriminator and a concrete literal is not assignable to the generic TUpdate without casts.
  • RTO20d4 guard: publishAndApply returns successfully before the RTO20e wait when the synthetic list is empty.

Tests

Seven UTS unit test ports (spec cases added in ably/specification#514 + #515):

Test ID Asserts
RTLC14c/zero-delta-diff-is-noop-0 identical-state override → noop: true, no event
RTLM22c/empty-diff-is-noop-0 identical non-tombstoned entries (timeserial ignored per RTLM22b3) → noop: true
RTO20d4/empty-synthetic-list-skips-sync-wait-0 all-null ACK serials while SYNCING → operation resolves without the sync completing
RTLO5/tombstone-zero-value-counter-emits-update-0 tombstoning a zero counter → non-no-op update, tombstone: true, amount: 0
RTLO5/tombstone-empty-map-emits-update-0 tombstoning an all-tombstoned map → non-no-op, tombstone: true, empty payload
RTLO4b4c3c/tombstone-zero-value-counter-tears-down-0 subscriber receives the zero-amount tombstone update and listeners are deregistered
RTO4b2a/reset-of-empty-root-emits-no-update-0 reset of an empty root emits nothing (second-channel liveness control)

Verification

  • Full test/uts/objects/unit tier: 332 passing, 0 failing, 0 pending (325 baseline + 7).
  • The RTLC14c/RTLM22c tests fail without the production change (noop is undefined pre-fix); the RTO20d4 test times out parked in the RTO20e wait pre-fix.
  • Existing tombstone, RTO4b/RTO27, RTLC14/RTLM22 and RTO20d1/RTO20e neighbours all green (the tombstone suite passing proves the teardown exception holds).
  • tsc --noEmit and eslint clean on the touched sources.

Summary by CodeRabbit

  • Bug Fixes
    • Corrected zero-value counter and empty-map updates during deletion and state replacement.
    • Tombstone events now reach listeners reliably, include deletion details, and properly stop future notifications.
    • Resetting an already-empty root no longer sends unnecessary subscriber updates.
    • Publish operations with no applicable acknowledgements now complete without waiting for synchronization.
  • Tests
    • Added coverage for zero-delta updates, tombstones, listener behavior, root resets, and acknowledgement handling.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 096118d4-a581-4407-bbbb-37e28344c527

📥 Commits

Reviewing files that changed from the base of the PR and between f36b64b and 9490a18.

📒 Files selected for processing (1)
  • test/uts/objects/unit/live_map.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/uts/objects/unit/live_map.test.ts

Walkthrough

LiveObject updates now distinguish unchanged-state no-ops from typed zero-change tombstone updates. LiveCounter and LiveMap implement this behavior. RealtimeObject skips sync waiting when acknowledgements contain only null synthetic serials. Tests cover tombstones, subscriptions, resets, and acknowledgements.

Changes

LiveObject update and tombstone flow

Layer / File(s) Summary
No-op and tombstone update contracts
src/plugins/liveobjects/liveobject.ts, src/plugins/liveobjects/livecounter.ts, src/plugins/liveobjects/livemap.ts
Unchanged counter and map state now returns noop updates. Tombstoning converts noop clears into typed non-noop updates.
Tombstone, subscription, and reset validation
test/uts/objects/unit/live_counter.test.ts, test/uts/objects/unit/live_map.test.ts, test/uts/objects/unit/live_object_subscribe.test.ts, test/uts/objects/unit/objects_pool.test.ts
Tests cover zero-delta tombstones, unchanged replacements, listener removal, and empty-root resets.

Realtime publish handling

Layer / File(s) Summary
Empty synthetic publish handling
src/plugins/liveobjects/realtimeobject.ts, test/uts/objects/unit/realtime_object.test.ts
publishAndApply returns without local application or sync waiting when all synthetic serials are null. The integration test verifies that the local value remains unchanged.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Publisher
  participant RealtimeObject
  participant SyncState
  Publisher->>RealtimeObject: publishAndApply()
  RealtimeObject-->>Publisher: ACK with null synthetic serials
  RealtimeObject->>RealtimeObject: skip synthetic message application
  RealtimeObject->>SyncState: skip sync wait
  RealtimeObject-->>Publisher: resolve without SYNCED
Loading

Possibly related PRs

  • ably/ably-js#2219: Covers overlapping LiveObject, LiveCounter, and LiveMap no-op and tombstone behavior.
  • ably/ably-js#2257: Modifies related LiveObject update paths and tests.
  • ably/ably-js#2263: Changes LiveObjectUpdateNoop handling in overlapping update flows.

Poem

I’m a rabbit guarding updates tight,
No-op hops vanish out of sight.
Zero tombstones still pass through,
Maps and counters know what to do.
Null serials rest; sync waits no more.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the two main changes: suppressing no-op LiveObject diffs and skipping the sync wait for empty synthetic messages.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch liveobjects/noop-diffs-and-empty-synthetic-list

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

… an empty synthetic list

Data diffs now collapse to a no-op update when nothing changed (RTLC14c/
RTLM22c): a zero-delta counter diff or an empty map key-diff returns
{ noop: true } from _updateFromDataDiff itself, so every caller is covered -
an OBJECT_SYNC re-delivering identical state no longer emits spurious
{ amount: 0 } / empty-update events to subscribers, and the RTO4b reset of an
already-empty root no longer emits an empty update (RTO4b2a).

The one mandated exception is the tombstone path (RTLO4e5): a tombstone
update must never be no-op-marked, since notifyUpdated suppresses no-ops
before its tombstone branch and the RTLO4b4c3c listener teardown would be
skipped. tombstone() therefore synthesizes the typed no-change update via a
new per-subclass _createNoChangeUpdate() hook when the diff is a no-op,
matching ably-java's synthesis semantics.

publishAndApply now completes without the RTO20e sync wait when the
synthetic message list is empty (every serial null per RTO20d1), per
RTO20d4, instead of parking on a wait with nothing to apply.

Adds the seven UTS unit test ports covering these behaviours.

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

Aligns the LiveObjects implementation with the Objects spec’s no-op update semantics by suppressing “update” events when state/data is unchanged, while preserving the mandated tombstone (delete) teardown behavior and avoiding an unnecessary sync-wait in publishAndApply when there’s nothing to apply locally.

Changes:

  • Collapse empty diffs to { noop: true } for LiveCounter (zero delta) and LiveMap (no changed keys), so re-sync/reset paths do not emit spurious updates.
  • Ensure tombstoning still produces a deliverable (non-noop) update even when the diff would otherwise be a no-op, via a new _createNoChangeUpdate() hook.
  • Short-circuit publishAndApply to skip the RTO20e sync wait when the synthetic message list is empty (e.g., all-null ACK serials).

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated no comments.

Show a summary per file
File Description
src/plugins/liveobjects/realtimeobject.ts Skips the RTO20e sync wait when publishAndApply has no synthetic messages to apply.
src/plugins/liveobjects/liveobject.ts Adds tombstone no-op carve-out handling and introduces _createNoChangeUpdate() to synthesize a deliverable tombstone update.
src/plugins/liveobjects/livemap.ts Returns noop for empty key diffs and provides map-specific _createNoChangeUpdate().
src/plugins/liveobjects/livecounter.ts Returns noop for zero-delta diffs and provides counter-specific _createNoChangeUpdate().
test/uts/objects/unit/realtime_object.test.ts Adds coverage for RTO20d4 (empty synthetic list skips sync wait).
test/uts/objects/unit/objects_pool.test.ts Adds coverage for RTO4b2a (reset of already-empty root emits no update).
test/uts/objects/unit/live_object_subscribe.test.ts Adds coverage ensuring tombstone teardown still happens even with a zero-delta tombstone update.
test/uts/objects/unit/live_map.test.ts Adds coverage for RTLM22c no-op diffs and tombstone carve-out on all-tombstoned maps.
test/uts/objects/unit/live_counter.test.ts Adds coverage for RTLC14c no-op diffs and tombstone carve-out on already-zero counters.

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

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
@sacOO7 sacOO7 reopened this Aug 20, 2026
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