Align path-based LiveObjects API spec with ably-js implementation#477
Align path-based LiveObjects API spec with ably-js implementation#477sacOO7 wants to merge 4 commits into
Conversation
Cross-validation against ably-js commit 3deeee8e surfaced multiple gaps between the path-based API spec and the implementation. This commit applies all non-PR-delegated spec changes identified in the actionable review plan. Major additions: - Path event semantics (RTO24c-e): bubbling vs non-bubbling event dispatch rules - Multi-parent reference graph (RTLO3f/g, RTO5c10): parentReferences maintenance and getFullPaths for multi-path subscription dispatch; post-sync rebuild - Tombstone auto-deregistration (RTLO4b8-b10): instance listeners deregistered after the final tombstone update; path subscriptions unaffected - Raw LiveMap/Counter rejection (RTLM20e8): value-type-only consumption in MAP_SET; replaces RTLM20e1 - PathObject/Instance unsubscribe(listener) removed (RTPO20, RTINS17): deregistration only via Subscription.unsubscribe() Validation tightening: - Channel-mode/state/echo checks on PathObject/Instance write methods (RTPO15e-18e, RTINS12d-15d) and subscribe wrappers (RTPO19i, RTINS16i, RTPO19h) - at() path string check (RTPO6e) - Instance#get conditional key-type check (RTINS5d) - subscribeIterator cancellation (RTPO21d, RTINS18d) - amount validation on increment/decrement wrappers (RTPO17f/18f, RTINS14e/15e) - LiveMap value-type entries null rejection (RTLMV4c) - LiveCounter value-type defaulting simplified (RTLCV4a/b1) Editorial / non-normative: - Compact memoization broadened (RTPO13b5, RTPO14a2): cyclic OR shared reference dedup - LiveMap value-type immutability shallow-only (RTLMV3d) - Brand-equivalent return types on LiveMap.create / LiveCounter.create (RTLMV3e, RTLCV3e) - Listener ordering and duplicate-subscription independence (RTO24f/g) - Subscription idempotency (SUB2b) - RTPO6f path-parsing edge cases (empty segment, trailing backslash, at(p.path()) round-trip) - RTLMV4l/m/n: validation-error order, parallelisable server-time fetches, Number type scope - RTO23e: typed channel.object.get<T>() usage hint - Listener-error isolation (RTLO4b10): aligned with ably-js EventEmitter callListener pattern Spec hygiene: - RTO11/RTO12 deprecation cross-references remapped to specific RTLMV3/4 and RTLCV3/4 sub-clauses; post-publish-lookup sub-clauses marked deleted - RFC 2119: lowercase should -> MUST on new-in-branch validation clauses (RTLMV4a/b, RTLCV4a, RTLMV4c) - IDL: named type aliases (Primitive, LiveObjectType, Value, ObjectIdReference); internal value-type fields; unsubscribe lines removed from PathObject/Instance Out of scope (delegated to upstream PRs): - BatchContext spec + batch() on PathObject/Instance (PR #471) - Implicit attach on RealtimeObject#get (PR #472) - REST objects API (PR #476) - ably-js code changes (separate PRs in ably-js repo) Verified: build/npm-run-lint passes; cross-checked every applied clause against ably-js src/plugins/liveobjects at commit 3deeee8e. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| - `(RTPO17b)` Resolves the path using the path resolution procedure ([RTPO3](#RTPO3)). On failure, throws per [RTPO3c2](#RTPO3c2) | ||
| - `(RTPO17c)` If the resolved value is a `LiveCounter`, delegates to `LiveCounter#increment` ([RTLC12](#RTLC12)) with the provided `amount` | ||
| - `(RTPO17d)` If the resolved value is not a `LiveCounter`, the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 92007 | ||
| - `(RTPO17e)` Before resolving the path, the wrapper MUST perform the `OBJECT_PUBLISH` channel-mode check ([RTO2](#RTO2)), the channel-state check of [RTLC12c](#RTLC12c), and the `echoMessages` check of [RTLC12d](#RTLC12d) |
There was a problem hiding this comment.
I'm a bit confused by this (and similar ones here); if we're already delegating to LiveCounter#increment then why would we need to re-perform its checks?
There was a problem hiding this comment.
Okay, I checked the ably-js codebase, it seems we have moved those checks from LiveMap/LiveCounter to PathObject level as you can see at ably-js code ->
Seems we need to extract those checks into separate spec point group and then re-use the same at PathObject level. Accordingly we need to remove references from LiveCounter/LiveMap
There was a problem hiding this comment.
Similarly, checks for Instance#increment, Instance#decrement etc have been moved at top level in ably-js code. Though it feels like a duplication of checks since ideally, those should exists at LiveMap/LiveCounter level? But ably-js is our source of truth to perform the operation.
| - `(RTPO6c)` Returns a new `PathObject` with the same `root` and with the parsed segments appended to the current `path` segments | ||
| - `(RTPO6d)` This is a convenience for chaining multiple `PathObject#get` calls. For example, `pathObject.at("a.b.c")` is equivalent to `pathObject.get("a").get("b").get("c")` | ||
| - `(RTPO6e)` If `path` is not of type `String`, the library MUST throw an `ErrorInfo` error with `statusCode` 400 and `code` 40003 | ||
| - `(RTPO6f)` (non-normative) Path parsing edge cases: |
There was a problem hiding this comment.
Would this not be better off in the UTS?
There was a problem hiding this comment.
Removed spec item and other similar items -> cc84fdf
|
I'm holding off on reviewing this until you've reviewed it yourself but at a glance it seems like it would definitely benefit from you looking through it and deciding what actually needs changing |
|
The stuff to do with |
|
Would you mind taking a look at my comments on #427 to see whether there's any overlap with your changes here please? |
Sure, going through liveobject spec PRs to better understand existing context and avoid relevant duplications |
| - `(RTO24b3)` If the event path matches, apply depth filtering: the event is dispatched to the subscription if the number of path segments from the subscription path to the event path plus 1 does not exceed the subscription's `depth` option (or if `depth` is undefined). Formally, the event is dispatched if `eventPath.length - subscriptionPath.length + 1 <= depth` | ||
| - `(RTO24b4)` Create a `PathObjectSubscriptionEvent` with a `PathObject` pointing to the event path and the `ObjectMessage` that caused the change, and call the subscription's listener | ||
| - `(RTO24b5)` If a listener throws an error, the error must be caught and logged without affecting the dispatch to other subscriptions | ||
| - `(RTO24c)` Path events have a boolean `bubbles` attribute (default `true`): |
There was a problem hiding this comment.
@sacOO7 I've replaced the bubbling logic in ably/ably-js#2223; I'm happy to write the spec for the new logic
There was a problem hiding this comment.
Great, seems PR is merged, you can go ahead with the change 👍
There was a problem hiding this comment.
Okay, you should create separate PR for bubbles, I will remove the bubbles related spec from here
There was a problem hiding this comment.
Removed bubbling specific spec here -> 34bf506, since it's outdated with respect to new implementation at ably/ably-js#2223, you can add it as a part of new PR if needed
| - `(RTO11f14c1e)` This clause has been replaced by [RTLMV3](#RTLMV3). | ||
| - `(RTO11f14c1f)` This clause has been replaced by [RTLMV3](#RTLMV3). | ||
| - `(RTO11f14c2)` This clause has been replaced by [RTLMV3](#RTLMV3). | ||
| - `(RTO11f14)` This clause has been replaced by [RTLMV4e](#RTLMV4e). |
There was a problem hiding this comment.
I think that in the interests of keeping this PR as small as possible let's drop this stuff; if the references are genuinely incorrect we can sort it out later
| - `(RTPO15b)` Resolves the path using the path resolution procedure ([RTPO3](#RTPO3)). On failure, throws per [RTPO3c2](#RTPO3c2) | ||
| - `(RTPO15c)` If the resolved value is a `LiveMap`, delegates to `LiveMap#set` ([RTLM20](#RTLM20)) with the provided `key` and `value` | ||
| - `(RTPO15d)` If the resolved value is not a `LiveMap`, the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 92007, indicating that the operation is not supported for the resolved object type | ||
| - `(RTPO15e)` Before resolving the path, the wrapper MUST perform the `OBJECT_PUBLISH` channel-mode check ([RTO2](#RTO2)), the channel-state check of [RTLM20c](#RTLM20c), and the `echoMessages` check of [RTLM20d](#RTLM20d). If any of these checks fail, the library MUST throw before attempting path resolution |
There was a problem hiding this comment.
I think that even if ably-js technically does these checks before the resolution, it doesn't really matter and it inflates the spec — we can introduce these later if error preference is a real issue. let's drop these
| - `(RTPO17c)` If the resolved value is a `LiveCounter`, delegates to `LiveCounter#increment` ([RTLC12](#RTLC12)) with the provided `amount` | ||
| - `(RTPO17d)` If the resolved value is not a `LiveCounter`, the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 92007 | ||
| - `(RTPO17e)` Before resolving the path, the wrapper MUST perform the `OBJECT_PUBLISH` channel-mode check ([RTO2](#RTO2)), the channel-state check of [RTLC12c](#RTLC12c), and the `echoMessages` check of [RTLC12d](#RTLC12d) | ||
| - `(RTPO17f)` If `amount` is provided but not a valid finite `Number`, the underlying [RTLC12e1](#RTLC12e1) validation MUST apply and the library MUST throw an `ErrorInfo` error with `statusCode` 400 and `code` 40003. If `amount` is omitted or null, it defaults to 1 per [RTPO17a1](#RTPO17a1) |
There was a problem hiding this comment.
I suggest that we remove these validation points, see #427 (comment)
| - `(RTINS3)` `Instance#id` property: | ||
| - `(RTINS3a)` If the wrapped value is a `LiveObject`, returns the `objectId` of that object | ||
| - `(RTINS3b)` If the wrapped value is a primitive, returns undefined/null | ||
| - `(RTINS3c)` `id` is the Object ID assigned to the wrapped `LiveObject` at creation time and is immutable: it does not change when the object is tombstoned. For primitives, `id` remains undefined regardless of source |
There was a problem hiding this comment.
we can lose this
| - `(RTINS18a)` If the wrapped value is not a `LiveObject`, the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 92007 | ||
| - `(RTINS18b)` Returns a stream or iterable that yields `InstanceSubscriptionEvent` objects, using the idiomatic construct for the language (e.g. async iterators, channels, flows, or async sequences) | ||
| - `(RTINS18c)` Internally wraps `Instance#subscribe` ([RTINS16](#RTINS16)), converting the callback-based subscription into the appropriate streaming or iterable pattern | ||
| - `(RTINS18d)` When the returned stream/iterable is closed (e.g. early termination of a `for await … of` loop, or explicit close), the underlying subscription created via [RTINS16](#RTINS16) MUST be deregistered |
There was a problem hiding this comment.
this is gone in the upstream
| - `(RTLM20e7d)` If the `value` is of type `Number`, set `ObjectMessage.operation.mapSet.value.number` to that value | ||
| - `(RTLM20e7e)` If the `value` is of type `Boolean`, set `ObjectMessage.operation.mapSet.value.boolean` to that value | ||
| - `(RTLM20e7f)` If the `value` is of type `Binary`, set `ObjectMessage.operation.mapSet.value.bytes` to that value | ||
| - `(RTLM20e8)` Validation procedure (supersedes [RTLM20e1](#RTLM20e1)). MUST be performed before any I/O or value-type consumption: |
There was a problem hiding this comment.
none of these needed
| - `(RTLCV3b)` Returns a new `LiveCounterValueType` instance with the internal `count` set to the provided `initialCount` (or 0 if omitted) | ||
| - `(RTLCV3c)` No input validation is performed at creation time. Validation is deferred to the consumption procedure ([RTLCV4](#RTLCV4)) | ||
| - `(RTLCV3d)` The returned `LiveCounterValueType` is immutable and must not be modified after creation | ||
| - `(RTLCV3e)` Implementations MAY return the `LiveCounterValueType` instance from `LiveCounter.create` typed as the underlying `LiveCounter` interface (i.e. brand-equivalent) for ergonomic compatibility with mutation methods like [LiveMap#set](#RTLM20). SDKs are free to expose a distinct `LiveCounterValueType` type if their host language type system makes that more idiomatic. |
There was a problem hiding this comment.
I think we can lose this
| - `(RTLMV3b)` Returns a new `LiveMapValueType` instance with the internal `entries` set to the provided `entries` (or undefined if omitted) | ||
| - `(RTLMV3c)` No input validation is performed at creation time. Validation is deferred to the consumption procedure ([RTLMV4](#RTLMV4)) | ||
| - `(RTLMV3d)` The returned `LiveMapValueType` is immutable and must not be modified after creation | ||
| - `(RTLMV3d)` The returned `LiveMapValueType` instance is shallowly immutable: its own properties (e.g. `entries`) MUST NOT be reassigned after creation. SDKs MAY shallow-freeze the value type instance. SDKs are NOT required to deep-freeze or deep-copy the user-provided `entries` object. Users SHOULD NOT mutate the `entries` object after passing it to [`LiveMap.create`](#RTLMV3); the behaviour is unspecified if they do. |
There was a problem hiding this comment.
i don't think this is necessary
|
Tons of conflicts in current PR, addressed all review comments in #480 |
Move the OBJECT_SUBSCRIBE mode + channel-state check (access API preconditions) and the OBJECT_PUBLISH mode + channel-state + echoMessages check (write API preconditions) out of the LiveMap/LiveCounter/LiveObject public methods and into two new common clauses (RTO25 and RTO26). Each PathObject and Instance public method that accesses or mutates data now references the applicable preconditions and renumbers its sub-clauses so the check sits in a logical position (after Expects, before any data work). External cross-references to the renumbered sub-clauses, including the IDL section, are updated. Two motivations: 1. Previously the spec placed these checks on LiveMap/LiveCounter, which delegating PathObject/Instance methods triggered only after path resolution and type checks. A call against a stale or detached channel could then yield a "wrong type" result (empty array etc.) instead of a state error. ably-js already moved the checks to the public entry points for this reason (commit a7462b14, "Handle channel configuration checks on PathObject/Instance level instead of LiveMap/LiveCounter"). 2. With the checks lifted out, the underlying LiveMap/LiveCounter methods become non-throwing for channel-state reasons. This matters for internal callers that invoke them in a non-throwing context, e.g. RTO5c10b iterating LiveMap#entries during the post-sync parentReferences rebuild. See [1]. The displaced LiveMap/LiveCounter/LiveObject sub-clauses are kept as "replaced by RTO25/RTO26" markers rather than deleted. [1] #477 (comment) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Cross-validation against ably-js commit
3deeee8esurfaced multiple gaps between the path-based LiveObjects API spec and the implementation. This PR applies every spec change that doesn't already live in an in-flight PR on this repo.Scope is deliberately limited to non-PR-delegated items. Three upstream PRs already cover the BatchContext, implicit-attach, and REST API surfaces, and review comments have been posted there with the corresponding spec gaps; this PR doesn't duplicate that work.
Major additions
RTO24c–RTO24e: bubbling vs non-bubbling dispatch rules; identity-change non-bubbling event when a parentMAP_SEToverwrites a subscribed path.RTLO3f,RTLO3g,RTO5c10:parentReferencesmap maintained on every map mutation,getFullPathsreturns all distinct root-to-object paths (cycle-safe), pool rebuild after a sync sequence.RTLO4b8–RTLO4b10: instance listeners receive the tombstone update, then are deregistered; path subscriptions unaffected; listener errors caught and logged per ably-jsEventEmitter.callListener.LiveMap/LiveCounterrejection —RTLM20e8: validation rejects raw live-object references in MAP_SET;RTLM20e1marked replaced.PathObject#unsubscribe/Instance#unsubscriberemoved —RTPO20,RTINS17marked deleted; deregistration is exclusively viaSubscription.unsubscribe()(matches impl).Validation tightening
RTPO15e–RTPO18e,RTINS12d–RTINS15d) and subscribe wrapper (RTPO19i,RTINS16i,RTPO19h).at()path-string type check (RTPO6e).Instance#getconditional key-type check (RTINS5d).subscribeIteratorcancellation contract (RTPO21d,RTINS18d).amountvalidation on increment/decrement wrappers (RTPO17f/18f,RTINS14e/15e).LiveMapvalue-typeentriesnull rejection (RTLMV4crewritten).LiveCountervalue-type defaulting simplified (RTLCV4a/b1).Editorial / non-normative additions
RTPO13b5,RTPO14a2): cyclic OR shared reference dedup.LiveMapvalue-type immutability scope clarified (RTLMV3d): shallow only.LiveMap.create/LiveCounter.create(RTLMV3e,RTLCV3e).RTO24f/g).Subscriptionidempotency (SUB2bin features.md).RTPO6fpath edge cases (empty segment, empty middle segment, trailing backslash,at(p.path())round-trip).RTLMV4l/m/n: validation-error order, parallelisable server-time fetches,Numbertype scope (BigInt-equivalent optional).RTO23e: typedchannel.object.get<T>()usage hint.Spec hygiene
RTO11/RTO12deprecation cross-references remapped from umbrellaRTLMV3/RTLCV3to specific sub-clauses (e.g.RTO11f14a→RTLMV4e1,RTO12f7→RTLCV4g1); post-publish-lookup sub-clauses (RTO11g/h/i,RTO12g/h/i) marked deleted because the wrap-result-as-LiveObject semantics no longer apply.should→MUSTon new-in-branch validation clauses (RTLMV4a/b,RTLCV4a).Primitive,LiveObjectType,Value,ObjectIdReference); internalcount/entriesfields on value types;unsubscribelines removed from PathObject/Instance.Out of scope (delegated to upstream PRs)
These items were intentionally NOT applied locally because in-flight PRs already cover them. Review comments with detailed cross-references to ably-js are posted on each:
batch()on PathObject/InstanceRealtimeObject#getAlso out of scope:
LiveMap/Counterrejection invalidateKeyValue,cheapRandStr→randomStringfor nonces, depth-validation tightening). Will be raised separately in ably-js.api-docstrings.mdrewrites — deferred to a follow-up PR (large editorial task).Verification
(cd build && npm run lint)→ ✓ no duplicate IDs across all three spec files.src/plugins/liveobjects/at commit3deeee8e.Test plan
🤖 Generated with Claude Code