Skip to content

Objects: specify get() sync-wait failure (RTO23c1), malformed sync serial handling (RTO5a6) and related clauses, with UTS unit cases - #514

Open
sacOO7 wants to merge 2 commits into
mainfrom
objects/get-sync-wait-failure-and-sync-fixes
Open

Objects: specify get() sync-wait failure (RTO23c1), malformed sync serial handling (RTO5a6) and related clauses, with UTS unit cases#514
sacOO7 wants to merge 2 commits into
mainfrom
objects/get-sync-wait-failure-and-sync-fixes

Conversation

@sacOO7

@sacOO7 sacOO7 commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

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:

  1. RealtimeObject.get() could hang forever. RTO23c says get() waits for the sync state to reach SYNCED, but did not say what happens if the channel leaves a usable state during that wait. A get() parked while the channel enters FAILED (including via a connection failure while SUSPENDED), or while the user calls detach(), waits on a sync that can never complete — the objects data has been cleared per RTO27a and no recovery is possible without user intervention. publishAndApply already fails deterministically in the identical wait (RTO20e1); get() had no equivalent clause.
  2. Malformed OBJECT_SYNC channelSerial handling 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.
  3. publishAndApply with an empty synthetic-message list was unspecified. When every serial in the PublishResult is null (RTO20d1), there is nothing to apply, but the spec still required the RTO20e sync wait.
  4. No-op update events. RTLC14b/RTLM22b as written produce spurious amount: 0 counter updates and empty map updates when a diff computes no change.
  5. RTO27a1 described the cleared state loosely ("zero value") rather than in terms of the existing new-object definitions.

Changes

specifications/objects-features.md

Clause Change
RTO23c1 (new) get() parked in the RTO23c wait must fail with ErrorInfo 92008 / statusCode 400 / cause = channel errorReason when the channel enters DETACHED, SUSPENDED or FAILED — regardless of the state transitioned from (e.g. FAILED entered from SUSPENDED). Mirrors RTO20e1, publishAndApply's equivalent failure.
RTO5a6 (new) A present-but-malformed 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 client SYNCING forever.
RTO20d4 (new) An empty synthetic-message list completes publishAndApply successfully without performing the RTO20e wait.
RTLC14c / RTLM22c (new) When a counter diff computes a delta of 0, or a map diff computes no changed keys, return a no-op update per RTLO4b4b instead of a spurious update event.
RTO27a1 (edited) Cleared object data is now defined as that of a new empty object of its type (RTLM4c / RTLC4b).
RTO27 preamble (edited) Cross-references both in-progress-operation failures: RTO20e1 (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; the SUSPENDED case is driven through the SDK's internal channel-state handler (as the RTO27 cases do) since a channel-level mock cannot produce SUSPENDED; the FAILED case additionally asserts the cause.

SDK status

SDK RTO23c1 RTO5a6 UTS tests
ably-js Implemented in the companion PR: ably/ably-js#2284 Already conformant (treat-as-absent is the js behaviour this clause adopts) In the companion PR
ably-cocoa Implemented (feature/liveobjects-implementation) Implemented Native twins exist; UTS ports to follow
ably-java Implemented (branch refactor/uts-objects-unit-into-liveobjects, PR to follow) Implemented Implemented

Notes for reviewers

  • RTO23c1 deliberately makes get()'s mid-wait behaviour stricter than entry (RTO23e/RTL33 tolerates a channel already SUSPENDED at 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 (for DETACHED/FAILED the data was just cleared); the rejection is recoverable — a retried get() passes entry and parks anew. This matches the choice RTO20e1 already made, including for SUSPENDED.
  • On cause: at the moment the waiter fails, the state-change reason and RealtimeChannel.errorReason are the same error; on a reason-less transition (e.g. a clean solicited detach) the cause is absent.

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 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 enters DETACHED/SUSPENDED/FAILED during the RTO23c sync wait (RTO23c1), mirroring existing publishAndApply behavior (RTO20e1).
  • Specify malformed OBJECT_SYNC.channelSerial handling (RTO5a6) and an early-success path for publishAndApply when there’s nothing to apply locally (RTO20d4).
  • Add UTS unit-tier cases for the new spec branches (get() mid-wait failures; absent/malformed channelSerial single-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.noop to true (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)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

sacOO7 added a commit to ably/ably-cocoa that referenced this pull request Aug 9, 2026
…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
sacOO7 added a commit to ably/ably-java that referenced this pull request Aug 9, 2026
…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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Seems, this is what we do in case of ably-js

@sacOO7
sacOO7 requested a review from VeskeR August 10, 2026 08:30
sacOO7 added a commit to ably/ably-cocoa that referenced this pull request Aug 11, 2026
…(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
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