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 |
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