Objects: specify get() sync-wait failure (RTO23c1), malformed sync serial handling (RTO5a6) and related clauses, with UTS unit cases - #514
Conversation
…a1 and RTO27 cross-refs
There was a problem hiding this comment.
Pull request overview
This PR updates the LiveObjects specification to cover several previously unspecified/underspecified edge cases (notably RealtimeObject.get() sync-wait failure modes and malformed OBJECT_SYNC serial handling), and adds corresponding UTS unit-tier coverage to lock in the new behaviors for SDK implementations.
Changes:
- Specify deterministic failure behavior for
get()when the channel entersDETACHED/SUSPENDED/FAILEDduring the RTO23c sync wait (RTO23c1), mirroring existingpublishAndApplybehavior (RTO20e1). - Specify malformed
OBJECT_SYNC.channelSerialhandling (RTO5a6) and an early-success path forpublishAndApplywhen there’s nothing to apply locally (RTO20d4). - Add UTS unit-tier cases for the new spec branches (
get()mid-wait failures; absent/malformedchannelSerialsingle-message sync).
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
specifications/objects-features.md |
Adds/adjusts spec clauses for get() mid-wait failure, malformed channelSerial handling, empty synthetic-apply behavior, no-op diff updates, and clarified cleared-state definition. |
uts/objects/unit/realtime_object.md |
Adds unit-tier UTS cases asserting get() fails with 92008 when the channel enters DETACHED/SUSPENDED/FAILED during the sync wait (including cause assertion for FAILED). |
uts/objects/unit/objects_pool.md |
Adds unit-tier UTS cases for single-message sync behavior when channelSerial is absent (RTO5a5) and when it is present-but-malformed and treated as absent (RTO5a6). |
Suppressed comments (1)
specifications/objects-features.md:808
- RTLM22c says to return a no-op update but doesn’t explicitly state how it must be represented. Other no-op cases in this spec require setting
LiveMapUpdate.nooptotrue(e.g. RTLM6e1, RTLM7h, RTLM8g). Please make the no-op marker explicit here too.
- `(RTLM22c)` As an exception to [RTLM22b](#RTLM22b): if the `LiveMapUpdate.update` computed in [RTLM22b](#RTLM22b) contains no changed keys (it is empty), no map key actually changed, so instead of returning an update return a `LiveMapUpdate` marked as a no-op per [RTLO4b4b](#RTLO4b4b)
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| - `(RTLC14a1)` `previousData` `Number` - the previous `data` value | ||
| - `(RTLC14a2)` `newData` `Number` - the new `data` value | ||
| - `(RTLC14b)` Return a `LiveCounterUpdate` object with `LiveCounterUpdate.update.amount` set to `newData - previousData` | ||
| - `(RTLC14c)` As an exception to [RTLC14b](#RTLC14b): if `newData` equals `previousData` (that is, the computed delta is `0`), the counter data did not change, so instead of returning an update return a `LiveCounterUpdate` marked as a no-op per [RTLO4b4b](#RTLO4b4b) |
There was a problem hiding this comment.
Good catch — fixed in the stacked follow-up PR #515: RTLC14c/RTLM22c now use the spec-wide explicit form ("return a Live*Update object with *.noop set to true ([RTLO4b4b])"), citing RTLC9h / RTLM16b as the phrasing exemplars.
…and port their UTS unit cases
- RTO23c1: a get() parked waiting for objects sync now fails with its own
error case (getFailedChannelStateChanged — same 92008/400/cause as
publishAndApply's RTO20e1, message stating the object could not be
retrieved), and the stale pending-spec comment cites RTO23c1 directly.
- Port the five new UTS unit cases (RTO23c1 fails-on-channel-{detached,
suspended,failed}, RTO5a5 absent-channel-serial, RTO5a6 malformed-channel-
serial-treated-as-absent) into Test/UTS/unit/objects/, retiring the
superseded native twins.
- Annotate the implementation sites of the newly specified points (RTO20d4,
RTLC14c, RTLM22c) and align the RTO27a1/RTO27 comments with the updated
spec wording.
Spec changes: ably/specification#514
Companion ably-js fix: ably/ably-js#2284
…nd add their UTS unit tests - RTO23c1: a get() parked waiting for objects sync now fails when the channel enters DETACHED/SUSPENDED/FAILED — ensureSynced routes through the shared pendingSyncWaiters, each waiter carrying a caller-specific failure description (the object could not be retrieved vs RTO20e1's operation could not be applied locally), built into the 92008/400/cause error at the failure site. - RTO5a6: a malformed OBJECT_SYNC channelSerial (no ':' separator) is normalized to null so it takes the same branch as an absent serial (RTO5a5), with a warning logged. - Add the five UTS unit tests derived from the new spec cases (3x RTO23c1 per channel state, RTO5a5, RTO5a6). - Annotate the implementation sites of the newly specified points (RTO20d4, RTLC14c, RTLM22c). Spec changes: ably/specification#514 Companion ably-js fix: ably/ably-js#2284
| - `(RTO5a3)` If the sequence id matches the previously received sequence id, the client library should continue the sync process | ||
| - `(RTO5a4)` The objects sync sequence for that sequence identifier is considered complete once the cursor is empty; that is when the `channelSerial` looks like `<sequence id>:` | ||
| - `(RTO5a5)` An `OBJECT_SYNC` may also be sent with no `channelSerial` attribute. In this case, the sync data is entirely contained within the `ProtocolMessage` | ||
| - `(RTO5a6)` If the `channelSerial` is present but malformed --- that is, it does not contain the `:` separator required by [RTO5a1](#RTO5a1) and so cannot be split into a `<sequence id>` and a `<cursor value>` --- the client library must handle the `OBJECT_SYNC` as if the `channelSerial` were absent per [RTO5a5](#RTO5a5), and should log a warning |
There was a problem hiding this comment.
Seems, this is what we do in case of ably-js
…(OD2g), path segments (RTPO2a), explicit channel-state guards - Atomic blueprint writes: a recursive build-without-publish evaluator (ObjectCreationHelpers) composes all *_CREATE ObjectMessages depth-first with the self-create last (RTLMV4k), points each entry and the MAP_SET at the final create's objectId (RTLMV4d1/d2, RTLM20e7g2), validates eagerly (RTLCV4a finiteness, RTLMV4a-c1) and fetches the RTO16 server time per object; InternalDefaultLiveMap.set publishes creates + MAP_SET in ONE publishAndApply (RTLM20h1; primitives unchanged per RTLM20h2). Deletes the objectCreator narrowing cast; createMap/createCounter reuse the shared composition path. Enables the previously-excluded blueprint UTS cases and removes their deviations entries; adds native atomicity tests over the captured publish array. Retags the replaced RTO11*/RTO12* citations to RTLMV3/RTLCV3-family clauses and cites RTTS11/RTTS11a on LiveMapValue. - Public ObjectData.json is now the decoded JSONValue (OD2g; decoded per OD5) instead of a re-serialized string, matching ably-js and ably-java. - PathObject stores its path as ordered [String] segments (RTPO2a, root empty per RTO23d), with escaping confined to the path() render (RTPO4b, dots only, matching ably-js) and at() parsing (RTPO6b); removes the internal string-storage escaping deviation. - Explicit channel-state guards: nosync_validateChannelStateForAccessAPI (RTO25b) / ForWriteAPI (RTO26b) replace raw state lists at all call sites (no behaviour change; audit found no mismatches). - compactJson routes through instance()'s single RTO25 guard; RTPO7c-f citations added on the primitive/counter value() views. Spec: ably/specification#514 + /pull/515
Problem
While aligning the LiveObjects implementations in ably-cocoa, ably-java and ably-js against the Objects feature spec, we found several behaviours that the SDKs need but the spec either did not define or defined incompletely:
RealtimeObject.get()could hang forever. RTO23c saysget()waits for the sync state to reachSYNCED, but did not say what happens if the channel leaves a usable state during that wait. Aget()parked while the channel entersFAILED(including via a connection failure whileSUSPENDED), or while the user callsdetach(), waits on a sync that can never complete — the objects data has been cleared per RTO27a and no recovery is possible without user intervention.publishAndApplyalready fails deterministically in the identical wait (RTO20e1);get()had no equivalent clause.OBJECT_SYNCchannelSerialhandling was unspecified. RTO5a1 defines the<sequence id>:<cursor value>format, but no clause said what to do when the serial cannot be parsed. SDK behaviour had diverged.publishAndApplywith an empty synthetic-message list was unspecified. When every serial in thePublishResultisnull(RTO20d1), there is nothing to apply, but the spec still required the RTO20e sync wait.amount: 0counter updates and empty map updates when a diff computes no change.RTO27a1described the cleared state loosely ("zero value") rather than in terms of the existing new-object definitions.Changes
specifications/objects-features.mdRTO23c1(new)get()parked in the RTO23c wait must fail withErrorInfo92008 /statusCode400 /cause= channelerrorReasonwhen the channel entersDETACHED,SUSPENDEDorFAILED— regardless of the state transitioned from (e.g.FAILEDentered fromSUSPENDED). Mirrors RTO20e1,publishAndApply's equivalent failure.RTO5a6(new)channelSerial(no:separator) must be handled as if the serial were absent per RTO5a5 (data applied, sync completes), and a warning should be logged. Treat-as-absent self-heals on the next sync, whereas discarding could leave a clientSYNCINGforever.RTO20d4(new)publishAndApplysuccessfully without performing the RTO20e wait.RTLC14c/RTLM22c(new)0, or a map diff computes no changed keys, return a no-op update per RTLO4b4b instead of a spurious update event.RTO27a1(edited)RTO27preamble (edited)publishAndApply) and RTO23c1 (get).uts/objects/unit/Five new unit-tier test cases, one per behaviour branch:
objects_pool.md:RTO5a5/absent-channel-serial-0(the previously untested baseline the malformed case defers to),RTO5a6/malformed-channel-serial-treated-as-absent-0.realtime_object.md:RTO23c1/fails-on-channel-{detached,suspended,failed}-0— per-state cases following the RTO20e1 precedent; theSUSPENDEDcase is driven through the SDK's internal channel-state handler (as the RTO27 cases do) since a channel-level mock cannot produceSUSPENDED; theFAILEDcase additionally asserts thecause.SDK status
feature/liveobjects-implementation)refactor/uts-objects-unit-into-liveobjects, PR to follow)Notes for reviewers
get()'s mid-wait behaviour stricter than entry (RTO23e/RTL33 tolerates a channel alreadySUSPENDEDat entry, serving retained best-effort data per RTO27b/RTO25b). Rationale: a transition into a non-usable state while parked means the sync being waited on cannot complete as observed (forDETACHED/FAILEDthe data was just cleared); the rejection is recoverable — a retriedget()passes entry and parks anew. This matches the choice RTO20e1 already made, including forSUSPENDED.cause: at the moment the waiter fails, the state-change reason andRealtimeChannel.errorReasonare the same error; on a reason-less transition (e.g. a clean solicited detach) thecauseis absent.