[AIT-30] LiveObjects Path-based API spec#427
Conversation
13dee45 to
1e518c6
Compare
1fa3eeb to
46261f4
Compare
49f0364 to
47a9d51
Compare
46261f4 to
3608895
Compare
3608895 to
b4ad764
Compare
7738c92 to
fa2a54e
Compare
b4ad764 to
1eb4dd8
Compare
1eb4dd8 to
035aef9
Compare
`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>
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>
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>
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>
The term "outermost" was unclear in RTLM20e7g2 and RTLMV4d2. Replace it with "final element in the list/array", leveraging RTLMV4k's ordering guarantee that the value type's own MAP_CREATE comes last in the returned array. RTLM20e7g1 is also tweaked to explicitly normalise both branches (RTLCV4 returns a single ObjectMessage; RTLMV4 returns an array) into an ordered list, so that RTLM20e7g2's "final element" wording applies uniformly for both LiveCounterValueType and LiveMapValueType. Addresses [1] and [2]. [1] #427 (comment) [2] #427 (comment) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was a transcription error in be2e752, which intended to base the `PublicAPI::ObjectMessage` (PAOM2) field types on ably-js's public `ObjectMessage` type in `liveobjects.d.ts`. That type has `connectionId?: string` (optional), but PAOM2c was written as required. Fix both the prose and the IDL to mark `connectionId` as optional, matching ably-js. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PAOM3 constructs a `PublicAPI::ObjectMessage` from a source `ObjectMessage`, and references the source's `operation` field (both directly in PAOM3d and transitively via PAOOP3, which expects an `ObjectOperation`). All three call sites (RTO24b2b2, RTPO19d2, RTINS16d2) already gate the call on `operation` being populated, and PAOM1 frames the type as the user-facing representation of an `ObjectMessage` "that carried an operation", but the procedure itself didn't state the precondition. Add a PAOM3a "Preconditions" subclause stating that callers must ensure the source has its `operation` field populated, and shift the existing steps to PAOM3b-d. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
These values are not populated for `ObjectMessage`s created by apply-on-ACK (RTO20d2). Matches the corresponding change in ably-js#2230. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…eobjects-path-based-api-spec
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>
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>
The RTLO4f getFullPaths clause was added to the prose spec but missed from the IDL. Add it as `getFullPaths() -> String[][]`, positioned between tombstone (RTLO4e) and addParentReference (RTLO4g) to preserve clause-letter ordering. The return type reflects RTLO4f, which describes the result as a list of distinct paths, each being an ordered sequence of string keys. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Make the objectId-keyed lookup convention explicit at the point of use, rather than relying on the reader to infer it from RTLO3f. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop the explicit ordering language (it's implied by the surrounding RTO5c sequence), merge the entries-iteration and addParentReference sub-clauses into one, and defer to LiveMap#entries to determine when a value is a LiveObject. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fix the key argument (Sachin's version passed entry.value, not the entry's key), align terminology with ObjectsMapEntry naming used elsewhere in the file, flatten the nesting, and re-position relative to RTLO4e4 by referencing the previous value of LiveMap.data instead of imposing a "before RTLO4e4" ordering. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fold RTLM7i's parent-reference recording into RTLM7g as RTLM7g2, removing the duplicated MapSet.value.objectId presence check. Also replace "the operation's key" with "the specified key" in RTLM7a3b, RTLM7g2 and RTLM8a3b, matching the wording used by the surrounding RTLM7a/b/b4 and RTLM8a/b/b1 clauses. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
RTLM7a3, RTLM8a3, RTLM24e1c and RTLO4e9 now all share the same
"Before [target] is applied: { fetch from ObjectsPool; if found
call removeParentReference }" shape, dropping the imprecise
"ObjectsMapEntry is of type LiveObject" / "parent reference
recorded on existing ObjectsMapEntry" wording.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Nest RTLM7a3 and RTLM8a3 inside RTLM7a2 / RTLM8a2 (the "Otherwise, apply" branches) so their "Otherwise" pair with the noop check isn't obscured, and reword all four parent clauses (RTLM7a3, RTLM8a3, RTLM24e1c, RTLO4e9) to name the data modification each one precedes (set / cleared / removed / reset). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the seven-clause MUST-style spec with four: define a directed graph G over parentReferences, return the *key-paths* corresponding to G's simple paths from root to this LiveObject. The new term *key-path* (matching PathObject's "path" concept) is used here to distinguish from the graph-theoretical "simple path". Edge cases (root, orphan, multi-key, multi-ancestor, cycles) fall out of the definition. There's a tension here: the most universal contract would just say "returns the key-paths from root to this LiveObject" and leave the mechanism to SDK implementers. But any SDK implementing `getFullPaths` will probably want a `parentReferences`-equivalent data structure, and keeping that structure consistent across the many places where `LiveMap.data` is mutated (`MAP_SET`, `MAP_REMOVE`, `MAP_CLEAR`, tombstone, sync rebuild) is the part SDKs are likely to get wrong. The prescriptive `parentReferences`-based formulation pays for itself by making those bookkeeping responsibilities explicit at each mutation site. If we hadn't already specified `parentReferences` and its maintenance, we might not have bothered — but we have, so let's use it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
|
OK, I think this spec is in a good state now; I've addressed my high-priority feedback from #427 (comment), plus a bunch of my other review comments, and incorporated @sacOO7's ably-js alignment work from #480. My intention is to squash the commits before merge (or to open a new squashed PR and leave the history here). But @VeskeR (or anyone else) are you interested in eyeballing the changes here before that? cc @paddybyers |
| @@ -319,26 +333,37 @@ Objects feature enables clients to store shared data as "objects" on a channel. | |||
| - `(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` `Dict<String, Set<String>>` - tracks which `LiveMap`s in the local `ObjectsPool` currently reference this `LiveObject`, and at which keys they do so. The mapping is keyed by the parent `LiveMap`'s `objectId`, with each value being the set of keys at which that `LiveMap` references this `LiveObject`. Used by `getFullPaths` ([RTLO4f](#RTLO4f)) to determine every path the object currently occupies in the LiveObjects tree | |||
There was a problem hiding this comment.
| - `(RTLO3f)` protected `parentReferences` `Dict<String, Set<String>>` - tracks which `LiveMap`s in the local `ObjectsPool` currently reference this `LiveObject`, and at which keys they do so. The mapping is keyed by the parent `LiveMap`'s `objectId`, with each value being the set of keys at which that `LiveMap` references this `LiveObject`. Used by `getFullPaths` ([RTLO4f](#RTLO4f)) to determine every path the object currently occupies in the LiveObjects tree | |
| - `(RTLO3f)` protected `parentReferences` `Dict<LiveMap, Set<String>>` - tracks which `LiveMap`s in the local `ObjectsPool` currently reference this `LiveObject`, and at which keys they do so. The mapping is keyed by the parent `LiveMap`'s `objectId`, with each value being the set of keys at which that `LiveMap` references this `LiveObject`. Used by `getFullPaths` ([RTLO4f](#RTLO4f)) to determine every path the object currently occupies in the LiveObjects tree |
Currently it holds LiveMap instance as a key in ably-js
There was a problem hiding this comment.
Also, I don't think the "mapping is keyed by the parent LiveMap's objectId", I will double check with the original.
There was a problem hiding this comment.
Okay, seems some rephrasing has been done to the spec, I will have to double check all migrated changes to make sure they are consistent with spec points in align js PR or meaning is not totally changed
There was a problem hiding this comment.
Currently it holds LiveMap instance as a key in ably-js
Please see RTLO3f1
There was a problem hiding this comment.
getFullPaths have M * N (n^2) complexity, do you think it would be better if we suggest to keep Dict<LiveMap, Set<String>> as default and making Dict<String, Set<String>> as optional
There was a problem hiding this comment.
Currently, we agree that Dict<String, Set<String>> needs explicit resolving objectId from ObjectsPool right?
| - `(RTLO4e8)` Return the `LiveObjectUpdate` object computed in [RTLO4e5](#RTLO4e5) | ||
| - `(RTLO4f)` internal `getFullPaths` function - returns the list of all key-paths from the root `LiveMap` (objectId `root`) to this `LiveObject`. A *key-path* is a list of zero or more keys (the same concept as "path" elsewhere in this spec, e.g. on `PathObject`); we use the term key-path in this clause specifically to distinguish it from the graph-theoretical "simple path" used in [RTLO4f2](#RTLO4f2) | ||
| - `(RTLO4f1)` Which key-paths are returned is determined via a directed graph G defined as follows. The nodes of G are the `LiveObject`s in the `ObjectsPool`. For each `(parent, key)` pair recorded in `child`'s `parentReferences` ([RTLO3f](#RTLO3f)), G has a directed edge from `parent` to `child` labelled `key` | ||
| - `(RTLO4f2)` A *simple path* in G is a sequence of edges visiting each node at most once. Each such path in G from `root` to this `LiveObject` contributes one key-path to the returned list: the list of its edge labels. The empty simple path (which exists only when this `LiveObject` is itself `root`) contributes the empty key-path `[]` |
There was a problem hiding this comment.
Somehow this feels a bit confusing : (
Not sure about others, they can post their thoughts
Note: This PR is based on #470; please review that one first.
Resolves AIT-30.