LiveObjects: suppress no-op diff updates (RTLC14c/RTLM22c) and skip the sync wait for an empty synthetic list (RTO20d4) - #2288
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
WalkthroughLiveObject 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. ChangesLiveObject update and tombstone flow
Realtime publish handling
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
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
… 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.
f36b64b to
9490a18
Compare
There was a problem hiding this comment.
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
publishAndApplyto 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.
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
Note
Stacked on #2284 (
liveobjects/rto23c1-get-sync-wait-failure) — this branch bases there because both touch thepublishAndApplysync-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:
OBJECT_SYNCre-delivers state identical to what an object already holds (reconnect, re-attach withHAS_OBJECTS),_updateFromDataDiffproduced{ update: { amount: 0 } }/{ update: {} }and_applySyncdelivered them to subscribers — change events for no change. The spec requires such diffs to be no-op updates (noop: true), whichnotifyUpdatedalready suppresses.ATTACHED-without-HAS_OBJECTSreset routes through the same diff (resetToInitialPool→clearObjectsData→notifyUpdated), so an already-empty root emitted{ update: {} }. The clarified RTO4b2a specifies suppression (ably-cocoa and ably-java already suppress).publishAndApplyparked on a wait with nothing to apply (RTO20d4). When every serial in thePublishResultisnull(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
_updateFromDataDiffitself (bothLiveCounterandLiveMap), so every caller — theoverrideWithObjectStatere-sync path, the RTO4b reset, and any future call site — is covered by one change. TheoverrideWithObjectStatesites gain a no-op passthrough (mirroring their existing RTLC6e/RTLM6e terminal-noop returns) soobjectMessageis only stamped on real updates.notifyUpdatedsuppresses 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 newprotected abstract _createNoChangeUpdate()hook when the diff is a no-op, then stampstombstone = 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 becauseLiveObjectinstances carry no type discriminator and a concrete literal is not assignable to the genericTUpdatewithout casts.publishAndApplyreturns 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):
RTLC14c/zero-delta-diff-is-noop-0noop: true, no eventRTLM22c/empty-diff-is-noop-0noop: trueRTO20d4/empty-synthetic-list-skips-sync-wait-0RTLO5/tombstone-zero-value-counter-emits-update-0tombstone: true,amount: 0RTLO5/tombstone-empty-map-emits-update-0tombstone: true, empty payloadRTLO4b4c3c/tombstone-zero-value-counter-tears-down-0RTO4b2a/reset-of-empty-root-emits-no-update-0Verification
test/uts/objects/unittier: 332 passing, 0 failing, 0 pending (325 baseline + 7).noopisundefinedpre-fix); the RTO20d4 test times out parked in the RTO20e wait pre-fix.tsc --noEmitandeslintclean on the touched sources.Summary by CodeRabbit