Align path-based LiveObjects API spec with ably-js implementation#480
Align path-based LiveObjects API spec with ably-js implementation#480sacOO7 wants to merge 13 commits into
Conversation
… Subscription-only deregistration
…i-spec' into fix/liveobjects-path-spec-based-on-ably-js-v2 # Conflicts: # specifications/objects-features.md
`Subscription` (returned by `subscribe`) is now the sole deregistration mechanism, matching the ably-js public API. RTLO4c is retained as a "This clause has been deleted" stub since it existed on main; RTPO20 and RTINS17 are removed outright as they were introduced earlier in this PR branch. The corresponding `unsubscribe` declarations are also removed from the IDL. Lifted from Sachin's spec-alignment PR [1]. [1] #480 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| - `(RTPO20)` `PathObject#unsubscribe` function: | ||
| - `(RTPO20a)` Accepts a `listener` argument and deregisters it from receiving further events for this `PathObject`'s path | ||
| - `(RTPO20b)` This operation must not have any side effects on `RealtimeObject`, the underlying channel, or their status | ||
| - `(RTPO20)` This clause has been deleted |
| - `(RTINS17)` `Instance#unsubscribe` function: | ||
| - `(RTINS17a)` Accepts a `listener` argument and deregisters it from receiving further events using `LiveObject#unsubscribe` ([RTLO4c](#RTLO4c)) | ||
| - `(RTINS17b)` This operation must not have any side effects on `RealtimeObject`, the underlying channel, or their status | ||
| - `(RTINS17)` This clause has been deleted |
| - `(RTLO4b8c)` Subsequent updates on this `LiveObject` MUST NOT invoke the deregistered listeners | ||
| - `(RTLO4b9)` Path-based subscriptions ([RTPO19](#RTPO19)) are NOT affected by [RTLO4b8](#RTLO4b8): they remain registered after the underlying object is tombstoned, because path subscriptions follow a path, not an object identity. A subsequent `MAP_SET` on the parent that creates a new object at the same path will deliver further events to the existing path subscription | ||
| - `(RTLO4b10)` If a registered listener throws when invoked with a `LiveObjectUpdate` per [RTLO4b4c2](#RTLO4b4c2), the error MUST be caught and logged. The error MUST NOT affect the dispatch to other listeners registered on the same `LiveObject`, nor abort the iteration over the listener list | ||
| - `(RTLO4c)` This clause has been deleted |
…point, removed unnecessary `RTLO4b8`, `RTLO4b9` and `RTLO4b10`
| Describes types for RealtimeObject.\ | ||
| Types and their properties/methods are public and exposed to users by default. An `internal` label may be used to indicate that a type or its property/method must not be exposed to users and is intended for internal SDK use only. | ||
|
|
||
| // Primitive is an alias used throughout the IDL |
There was a problem hiding this comment.
Primitive does not appear anywhere else in this PR, what purpose is this serving?
| - `(RTLO3e1)` Set to undefined/null when the `LiveObject` is initialized | ||
| - `(RTLO3f)` protected `parentReferences` - a mapping from each parent `LiveMap` to the set of keys at which that `LiveMap` currently references this `LiveObject`. The map MUST be maintained as map operations are applied so that path-based subscribers ([RTO24](#RTO24)) can determine every path the object currently occupies in the LiveObjects tree | ||
| - `(RTLO3f1)` Set to an empty map when the `LiveObject` is initialized | ||
| - `(RTLO3f2)` When a `MAP_SET` operation adds an entry whose `ObjectData.objectId` references another `LiveObject` in the local `ObjectsPool`, the referenced object's `parentReferences` MUST be updated to include the entry `(parent LiveMap, key)` |
There was a problem hiding this comment.
these things that should be done when an operation is applied belong in the respective parts of the spec, not here
Adds SUB2b clarifying that repeated calls to `Subscription#unsubscribe` are a no-op, matching the ably-js implementation across all three subscription factories (LiveObject EventEmitter.off, the PathObjectSubscriptionRegister Map.delete, and Instance which delegates to LiveObject). Lifted from Sachin's spec-alignment PR [1]. [1] #480 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| - `(SUB1)` A `Subscription` represents a registration for receiving events from a subscribe operation | ||
| - `(SUB2)` The `Subscription` object has the following method: | ||
| - `(SUB2a)` `unsubscribe` - deregisters the listener that was registered by the corresponding `subscribe` call. Once `unsubscribe` is called, the listener must not be called for any subsequent events | ||
| - `(SUB2b)` Calling `unsubscribe` more than once is a no-op |
The internal `count` (RTLCV2a) and `entries` (RTLMV2a) properties were already specified in prose but missing from the IDL block. Add them, matching the private `_count` and `_entries` fields on ably-js's `LiveCounterValueType` and `LiveMapValueType`. Lifted from Sachin's spec-alignment PR [1]. [1] #480 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| update: Dict<String, 'updated' | 'removed'> // RTLM18b | ||
|
|
||
| class LiveCounterValueType: // RTLCV* | ||
| count: Number // RTLCV2a, internal |
| static create(Number initialCount?) -> LiveCounterValueType // RTLCV3 | ||
|
|
||
| class LiveMapValueType: // RTLMV* | ||
| entries: Dict<String, (Boolean | Binary | Number | String | JsonArray | JsonObject | LiveCounterValueType | LiveMapValueType)>? // RTLMV2a, internal |
| - `(RTLO3d1)` Set to `false` when the `LiveObject` is initialized | ||
| - `(RTLO3e)` protected `tombstonedAt` (optional) Time - a timestamp indicating when this object was tombstoned. This property is nullable, and specification points that manipulate this value maintain the invariant that it is non-null if and only if `isTombstone` is `true` | ||
| - `(RTLO3e1)` Set to undefined/null when the `LiveObject` is initialized | ||
| - `(RTLO3f)` protected `parentReferences` - a mapping from each parent `LiveMap` to the set of keys at which that `LiveMap` currently references this `LiveObject`. The map MUST be maintained as map operations are applied so that path-based subscribers ([RTO24](#RTO24)) can determine every path the object currently occupies in the LiveObjects tree |
There was a problem hiding this comment.
Please can you add to the IDL
There was a problem hiding this comment.
I've added a placeholder for parentReferences in #427 (8ce7584) and added it to the IDL. Note that the parent references are now stored as object ID strings, not direct references; I've added a spec point that explains why (commit message expands on this) but provides flexibility for implementing like ably-js. When I pull in getFullPaths() spec I'll update accordingly to reflect this
Stubs out the new `parentReferences` internal property on `LiveObject`, needed by `getFullPaths` (RTLO4f). The detailed maintenance rules (across `MAP_SET`, `MAP_REMOVE`, `MAP_CLEAR`, `LiveMap` tombstoning, and post-sync rebuild) are deferred to a follow-up by Sachin; the in-progress draft is at [1]. ably-js stores `parentReferences` as a map keyed by a direct `LiveMap` reference; the placeholder instead keys by `objectId`, for consistency with how the rest of the LiveObjects spec models inter-object references (forward references in `LiveMap` entries are already objectIds resolved via the `ObjectsPool` on demand). This is also load-bearing for languages without automatic cycle collection. The protocol allows cyclic `LiveMap` graphs (e.g. `A.x = B`, `B.y = A`), and `getFullPaths` is being specified to handle them; under ARC in Swift, direct parent references in such a cycle would form an unbreakable retain cycle on the two `LiveMap`s. Keying by `objectId` lets the `ObjectsPool` remain the single owner and sidesteps the issue. Implementations remain explicitly permitted to store a direct `LiveMap` reference if more idiomatic in their language -- e.g. to avoid an `ObjectsPool` lookup at each `getFullPaths` traversal step -- as ably-js does today, provided they handle the cycle concern. [1] #480 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lifts the `getFullPaths` definition verbatim from commit ecf85df of Sachin's spec-alignment PR [1]. The only departure from the source is renumbering: Sachin places `getFullPaths` under `RTLO3 LiveObject properties` as `RTLO3g`; this commit places it under `RTLO4 LiveObject methods` as `RTLO4f` (with sub-clauses `RTLO4f1`-`RTLO4f7`) since `getFullPaths` is a function, not a property. Cross-references in RTO24b1 and RTLO3f are updated to match. Lawrence has not reviewed the lifted content yet; the imported clauses retain Sachin's capitalised RFC 2119 keywords and the NetworkX references, both of which may be tightened in follow-up commits. [1] #480 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| - `(RTLO3f4)` When a `MAP_REMOVE` operation tombstones an entry that previously referenced an object, the previously-referenced object's `parentReferences` entry for `(parent LiveMap, key)` MUST be removed | ||
| - `(RTLO3f5)` When a `MAP_CLEAR` operation removes an entry that previously referenced an object, the previously-referenced object's `parentReferences` entry for `(parent LiveMap, key)` MUST be removed | ||
| - `(RTLO3f6)` When a `LiveMap` is tombstoned per [RTLO4e](#RTLO4e), all references it holds to other `LiveObjects` MUST be removed from those objects' `parentReferences` maps | ||
| - `(RTLO3g)` internal `getFullPaths` function - returns the list of distinct paths from the root `LiveMap` (objectId `root`) to this `LiveObject`, computed by traversing `parentReferences` upward. Each returned path is an ordered sequence of keys from `root` to this `LiveObject`. |
| - `(RTLO3f4)` When a `MAP_REMOVE` operation tombstones an entry that previously referenced an object, the previously-referenced object's `parentReferences` entry for `(parent LiveMap, key)` MUST be removed | ||
| - `(RTLO3f5)` When a `MAP_CLEAR` operation removes an entry that previously referenced an object, the previously-referenced object's `parentReferences` entry for `(parent LiveMap, key)` MUST be removed | ||
| - `(RTLO3f6)` When a `LiveMap` is tombstoned per [RTLO4e](#RTLO4e), all references it holds to other `LiveObjects` MUST be removed from those objects' `parentReferences` maps | ||
| - `(RTLO3f2)` internal `addParentReference(parent, key)` method - records that the `LiveMap` `parent` references this `LiveObject` at `key` |
There was a problem hiding this comment.
just a note that methods belong under RTLO4, not RTLO3
There was a problem hiding this comment.
and also in the IDL
| - `(RTLO4e3b)` This clause has been replaced by [RTLO6b](#RTLO6b) | ||
| - `(RTLO4e3b1)` This clause has been replaced by [RTLO6b1](#RTLO6b1) | ||
| - `(RTLO4e5)` If this `LiveObject` is a `LiveMap`, before [RTLO4e4](#RTLO4e4) is applied, for each `ObjectsMapEntry` in this `LiveMap`'s current `data`: | ||
| - `(RTLO4e5a)` If `ObjectsMapEntry.data` is not an `ObjectIdObjectData`, no action is required for that entry |
There was a problem hiding this comment.
ObjectIdObjectData is not a concept that exists in the spec; this will need to be worded in some alternative fashion
| - `(RTLM24d)` Set the private `clearTimeserial` to the provided `serial` | ||
| - `(RTLM24e)` For each `ObjectsMapEntry` in the internal `data`: | ||
| - `(RTLM24e1)` If `ObjectsMapEntry.timeserial` is null or omitted, or the `serial` is lexicographically greater than `ObjectsMapEntry.timeserial`: | ||
| - `(RTLM24e1c)` Before [RTLM24e1a](#RTLM24e1a) is applied, the parent reference recorded on the `LiveObject` referenced by this entry (if any) MUST be removed: |
There was a problem hiding this comment.
Do we need a "before"? the order doesn't seem to matter? everything here is atomic
There was a problem hiding this comment.
Yeah, we also need to rephrase it
…dated IDL and references accordingly
…i-spec' into fix/liveobjects-path-spec-based-on-ably-js-v2 I've deliberately left one conflict in to resolve, need to understand the parentReferences stuff first.
…jects-path-spec-based-on-ably-js-v2
PR #480 [1] proposed specifying that ably-js deregisters all LiveObject#subscribe listeners on tombstone. Adopt that proposal with refined wording and a new LiveObjectUpdate.tombstone field that makes the trigger condition explicit. Also add the related ably-js refactor (commit 1d98cc3 [2]) that has tombstone() return the cleared LiveObjectUpdate rather than dispatching it inline. [1] #480 [2] ably/ably-js@1d98cc3 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| - `(RTLO4b4c3)` Otherwise: | ||
| - `(RTLO4b4c3a)` The registered listener of each subscription created via `LiveObject#subscribe` ([RTLO4b](#RTLO4b)) on this `LiveObject` is called with the `LiveObjectUpdate` | ||
| - `(RTLO4b4c3b)` Perform path-based subscription dispatch as described in [RTO24b](#RTO24b), passing this `LiveObject` and the `LiveObjectUpdate` | ||
| - `(RTLO4b4c3c)` When a `LiveObjectUpdate` is emitted as a result of a tombstone - i.e. an `OBJECT_DELETE` operation or a sync state with `tombstone: true`, after all listeners have been invoked from [RTLO4b4c3a](#RTLO4b4c3a) and [RTLO4b4c3b](#RTLO4b4c3b), the library MUST deregister all listeners on this `LiveObject`. Path-based subscriptions ([RTPO19](#RTPO19)) are NOT affected by `tombstone` update. |
There was a problem hiding this comment.
Thanks for noticing this — I've tried to tighten it up a bit in 2fa9a5e, so will remove from this PR
…jects-path-spec-based-on-ably-js-v2
| - `(RTLO3d1)` Set to `false` when the `LiveObject` is initialized | ||
| - `(RTLO3e)` protected `tombstonedAt` (optional) Time - a timestamp indicating when this object was tombstoned. This property is nullable, and specification points that manipulate this value maintain the invariant that it is non-null if and only if `isTombstone` is `true` | ||
| - `(RTLO3e1)` Set to undefined/null when the `LiveObject` is initialized | ||
| <<<<<<< HEAD |
There was a problem hiding this comment.
I've pulled all the parentReferences stuff into the main PR in 860e479
Imports the parentReferences bookkeeping spec from PR #480 [1] onto this integration branch, resolving the committed conflict marker at RTLO3f and the duplicate clause IDs introduced by the import. Imported from #480 verbatim: - RTO5c10: post-sync rebuild of every parentReferences map. - addParentReference and removeParentReference internal methods, with set-merge / set-remove / empty-set-delete semantics. - Tombstone-time children walk for LiveMap, stripping parent references from each referenced child before the data is cleared. - MAP_SET, MAP_REMOVE and MAP_CLEAR parent-reference maintenance (RTLM7a3, RTLM7i, RTLM8a3, RTLM24e1c). - IDL declarations for the two new internal methods. The Primitive type alias added in #480 was deliberately not imported, as it is unrelated to the parentReferences work. Conflicts reconciled: - The committed <<<<<<< / >>>>>>> block at RTLO3f. Kept the objectId-keyed Dict<String, Set<String>> description from this branch (consistent with #480's own IDL line and its set-style manipulation contracts; the alternative half mandated a specific in-memory representation that ably-js does not match literally). The "set to an empty map on initialisation" clause from #480 was moved to RTLO3f2; the prior RTLO3f2 TODO is deleted, since the imported maintenance rules now resolve it. RTO5c10a's back-reference was updated to point at the new RTLO3f2. - Duplicate clause IDs introduced by #480 were renamed per the "rename the later addition" convention in CONTRIBUTING.md: - addParentReference: RTLO4f -> RTLO4g - removeParentReference: RTLO4g -> RTLO4h - tombstone children walk: RTLO4e5* -> RTLO4e9* All cross-references to the renamed clauses were updated accordingly. The pre-existing RTLO4f (getFullPaths) and RTLO4e5-e8 (Compute LiveObjectUpdate through Return) are untouched. Linter passes. Still needs human review. [1] #480 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Follow-up to 860e479. The clauses pulled in from PR #480 use the uppercase RFC 2119 convention (MUST etc.); lowercase them for consistency with the prose style preferred on this branch. Touches the 10 occurrences of MUST in RTO5c10, RTLO4g1-g2, RTLO4h1-h3, RTLM7a3, RTLM7i, RTLM8a3 and RTLM24e1c. The pre-existing uppercase keywords in RTLO4f1-f7 (getFullPaths) are intentionally left alone. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The IDL entries imported from PR #480 declared these two methods without argument types. Annotate them as (LiveMap parent, String key), matching the conventional style used for multi-arg methods elsewhere in the IDL and the parent/key descriptions in the RTLO4g/RTLO4h prose. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
LiveObjects spec changes covering path-based subscriptions, identity-based subscription lifecycle behavior, and deregistration APIs:
• Multi-parent reference tracking (RTLO3f, RTLO3g, RTO5c10, RTO24b1)
LiveObjectnow maintains aparentReferencesmap keyed by parentLiveMap.LiveMapis tombstoned.getFullPathshelper traverses the parent graph upward to the root with cycle protection, returning all distinct paths currently referencing the object (or an empty set if the object is orphaned).PathObjectSubscriptionRegister(RTO24b1) now usesgetFullPathsto determine dispatch targets, allowing a single object update to fan out across every path referencing that object.parentReferencesfrom the finalizedLiveMapstate.• Tombstone-driven auto-deregistration (RTLO4b8, RTLO4b9, RTLO4b10)
LiveObject#subscribelisteners receiving a tombstone update (OBJECT_DELETE or sync tombstone) are invoked once and then automatically deregistered.•
Subscriptionas the sole deregistration mechanism (SUB2b, RTLO4c, RTPO20, RTINS17)unsubscribe(listener)APIs fromLiveObject,PathObject, andInstance(corresponding clauses deleted).Subscriptionreturned bysubscribe.Subscription#unsubscribeis explicitly documented as idempotent; repeated calls are treated as no-ops.• IDL updates
unsubscribedeclarations.Primitivetype alias.countandentriesbacking fields onLiveCounterValueTypeandLiveMapValueType.