From d2be8a1c15fc7ac9206c6b536c6f80784d2046ac Mon Sep 17 00:00:00 2001 From: Andrew Bulat Date: Tue, 24 Feb 2026 11:59:02 +0000 Subject: [PATCH 01/40] PathObject and Instance API - access and mutation methods (no subscription/batching) --- specifications/features.md | 2 +- specifications/objects-features.md | 193 ++++++++++++++++++++++++++++- 2 files changed, 192 insertions(+), 3 deletions(-) diff --git a/specifications/features.md b/specifications/features.md index 0a7d3693..32f5a0ab 100644 --- a/specifications/features.md +++ b/specifications/features.md @@ -939,7 +939,7 @@ The threading and/or asynchronous model for each realtime library will vary by l ### RealtimeObject {#realtime-objects} -Reserved for `RealtimeObject` feature specification, see [objects-features](../objects-features). Reserved spec points: `RTO`, `RTLO`, `RTLC`, `RTLM` +Reserved for `RealtimeObject` feature specification, see [objects-features](../objects-features). Reserved spec points: `RTO`, `RTLO`, `RTLC`, `RTLM`, `RTPO`, `RTINS` ### RealtimeAnnotations {#realtime-annotations} diff --git a/specifications/objects-features.md b/specifications/objects-features.md index bd0040b4..e8b3eda8 100644 --- a/specifications/objects-features.md +++ b/specifications/objects-features.md @@ -19,7 +19,7 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTO23a)` Requires the `OBJECT_SUBSCRIBE` channel mode to be granted per [RTO2](#RTO2) - `(RTO23b)` If the channel is in the `DETACHED` or `FAILED` state, the library should throw an `ErrorInfo` error with `statusCode` 400 and `code` 90001 - `(RTO23c)` If the [RTO17](#RTO17) sync state is not `SYNCED`, waits for the sync state to transition to `SYNCED` - - `(RTO23d)` Returns the object with id `root` from the internal `ObjectsPool` as a `LiveMap` + - `(RTO23d)` Returns a `PathObject` ([RTPO1](#RTPO1)) wrapping the `LiveMap` with id `root` from the internal `ObjectsPool`. The `PathObject` is created with an empty path, rooted at the `root` `LiveMap` - `(RTO11)` `RealtimeObject#createMap` function: - `(RTO11a)` Expects the following arguments: - `(RTO11a1)` `entries` `Dict` (optional) - the initial entries for the new `LiveMap` object @@ -715,13 +715,170 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLM22b2)` For each key that exists in the non-tombstoned entries of `newData` but does not exist in the non-tombstoned entries of `previousData`, add the key to `LiveMapUpdate.update` with the value `updated` - `(RTLM22b3)` For each key that exists in the non-tombstoned entries of both `previousData` and `newData`, perform a deep comparison of the `data` attributes from `previousData` and `newData`. If the data values differ, add the key to `LiveMapUpdate.update` with the value `updated` +### PathObject + +A `PathObject` is a lazy, path-based reference into the LiveObjects tree. It stores a path (as an ordered list of string segments) from the root `LiveMap` and resolves it at the time each method is called. This means a `PathObject` survives object replacements: if the object at a given path changes (e.g. via a `MAP_SET` operation), the same `PathObject` will resolve to the new object on subsequent calls. + +A `PathObject` is obtained from `RealtimeObject#get` ([RTO23](#RTO23)), which returns a `PathObject` rooted at the channel's root `LiveMap` with an empty path. Further `PathObjects` are obtained by navigating with `PathObject#get` or `PathObject#at`. + +- `(RTPO1)` The `PathObject` class provides a path-based view over the LiveObjects tree + - `(RTPO1a)` A specific SDK implementation may choose to expose a subset of the methods available on the `PathObject` class based on the expected type at the path. For example, when the user provides a type structure as a generic type parameter to `RealtimeObject#get`, the SDK may use type-specific class names (e.g. `LiveMapPathObject`, `LiveCounterPathObject`, `PrimitivePathObject`) that only expose the methods applicable to that type. The specification describes the general `PathObject` class with the full set of methods +- `(RTPO2)` `PathObject` has the following internal properties: + - `(RTPO2a)` `path` - an ordered list of string segments representing the path from the root `LiveMap` to this position in the tree + - `(RTPO2b)` `root` - a reference to the root `LiveMap` instance from the internal `ObjectsPool` +- `(RTPO3)` Internal path resolution procedure - resolves the stored `path` against the LiveObjects tree: + - `(RTPO3a)` Starting from `root`, walk through the path segments in order. For each segment: + - `(RTPO3a1)` The current object must be a `LiveMap`. If it is not, the resolution has failed + - `(RTPO3a2)` Look up the segment as a key in the current `LiveMap` using `LiveMap#get` ([RTLM5](#RTLM5)). If the result is undefined/null, the resolution has failed + - `(RTPO3a3)` The result becomes the current object for the next segment + - `(RTPO3b)` If the path is empty, the result is the `root` `LiveMap` itself + - `(RTPO3c)` On resolution failure: + - `(RTPO3c1)` For read operations (`value`, `instance`, `entries`, `keys`, `values`, `size`, `compact`, `compactJson`), return undefined/null. The client library may log a debug or trace message + - `(RTPO3c2)` For write operations (`set`, `remove`, `increment`, `decrement`), the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 92005, indicating that the path could not be resolved +- `(RTPO4)` `PathObject#path` function: + - `(RTPO4a)` Returns a dot-delimited string representation of the stored path segments + - `(RTPO4b)` Any dot characters (`.`) occurring within individual path segments must be escaped with a backslash (`\`) in the returned string. For example, a path with segments `["a", "b.c", "d"]` is represented as `a.b\.c.d` + - `(RTPO4c)` An empty path (root `PathObject`) returns an empty string +- `(RTPO5)` `PathObject#get` function: + - `(RTPO5a)` Expects the following arguments: + - `(RTPO5a1)` `key` `String` - the key to navigate to + - `(RTPO5b)` If `key` is not of type `String`, the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 40003, indicating that the key must be a `String` + - `(RTPO5c)` Returns a new `PathObject` with the same `root` and with `key` appended to the current `path` segments + - `(RTPO5d)` This is purely navigational and does not resolve the path or access any `LiveObject` data +- `(RTPO6)` `PathObject#at` function: + - `(RTPO6a)` Expects the following arguments: + - `(RTPO6a1)` `path` `String` - a dot-delimited path string + - `(RTPO6b)` Parses the dot-delimited `path` string into individual segments, respecting backslash-escaped dots (a `\.` sequence is treated as a literal dot within a segment, not a separator) + - `(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")` +- `(RTPO7)` `PathObject#value` function: + - `(RTPO7a)` Resolves the path using the path resolution procedure ([RTPO3](#RTPO3)) + - `(RTPO7b)` If the resolved value is a `LiveCounter`, returns its current numeric value (equivalent to `LiveCounter#value`, see [RTLC5](#RTLC5)) + - `(RTPO7c)` If the resolved value is a primitive (`Boolean`, `Binary`, `Number`, `String`, `JsonArray`, `JsonObject`), returns the value directly + - `(RTPO7d)` If the resolved value is a `LiveMap`, returns undefined/null + - `(RTPO7e)` If path resolution fails, returns undefined/null per [RTPO3c1](#RTPO3c1) +- `(RTPO8)` `PathObject#instance` function: + - `(RTPO8a)` Resolves the path using the path resolution procedure ([RTPO3](#RTPO3)) + - `(RTPO8b)` If the resolved value is a `LiveObject` (i.e. a `LiveMap` or `LiveCounter`), returns a new `Instance` ([RTINS1](#RTINS1)) wrapping that `LiveObject` + - `(RTPO8c)` If the resolved value is a primitive, returns undefined/null + - `(RTPO8d)` If path resolution fails, returns undefined/null per [RTPO3c1](#RTPO3c1) +- `(RTPO9)` `PathObject#entries` function: + - `(RTPO9a)` Resolves the path using the path resolution procedure ([RTPO3](#RTPO3)) + - `(RTPO9b)` If the resolved value is a `LiveMap`, returns an iterator yielding `[key, PathObject]` pairs, where each `PathObject` is created as if by calling `PathObject#get` with the corresponding key on this `PathObject` + - `(RTPO9c)` Only non-tombstoned entries are included, following the same rules as `LiveMap#entries` ([RTLM11](#RTLM11)) + - `(RTPO9d)` If the resolved value is not a `LiveMap`, or if path resolution fails, returns an empty iterator +- `(RTPO10)` `PathObject#keys` function: + - `(RTPO10a)` Behaves identically to `PathObject#entries` ([RTPO9](#RTPO9)) except that it yields only the keys +- `(RTPO11)` `PathObject#values` function: + - `(RTPO11a)` Behaves identically to `PathObject#entries` ([RTPO9](#RTPO9)) except that it yields only the `PathObject` values +- `(RTPO12)` `PathObject#size` function: + - `(RTPO12a)` Resolves the path using the path resolution procedure ([RTPO3](#RTPO3)) + - `(RTPO12b)` If the resolved value is a `LiveMap`, returns the number of non-tombstoned entries (equivalent to `LiveMap#size`, see [RTLM10](#RTLM10)) + - `(RTPO12c)` If the resolved value is not a `LiveMap`, or if path resolution fails, returns undefined/null +- `(RTPO13)` `PathObject#compact` function: + - `(RTPO13a)` Resolves the path using the path resolution procedure ([RTPO3](#RTPO3)) + - `(RTPO13b)` If the resolved value is a `LiveMap`, returns a recursively compacted representation as a plain key-value object: + - `(RTPO13b1)` Each entry in the `LiveMap` is included in the result. Tombstoned entries are excluded + - `(RTPO13b2)` Nested `LiveMap` values are recursively compacted into nested plain key-value objects + - `(RTPO13b3)` Nested `LiveCounter` values are resolved to their numeric value + - `(RTPO13b4)` Primitive values (`Boolean`, `Binary`, `Number`, `String`, `JsonArray`, `JsonObject`) are included as-is + - `(RTPO13b5)` Cyclic references (a `LiveMap` that has already been visited during this compaction) are represented by reusing the same in-memory object reference to the already-compacted result for that `LiveMap` + - `(RTPO13c)` If the resolved value is a `LiveCounter`, returns its current numeric value (equivalent to `PathObject#value`) + - `(RTPO13d)` If the resolved value is a primitive, returns the value directly (equivalent to `PathObject#value`) + - `(RTPO13e)` If path resolution fails, returns undefined/null per [RTPO3c1](#RTPO3c1) +- `(RTPO14)` `PathObject#compactJson` function: + - `(RTPO14a)` Behaves identically to `PathObject#compact` ([RTPO13](#RTPO13)) except for the following differences, which ensure the result is JSON-serializable: + - `(RTPO14a1)` `Binary` values are encoded as base64 strings instead of being included as-is + - `(RTPO14a2)` Cyclic references are represented as an object with a single `objectId` property containing the Object ID of the referenced `LiveMap`, instead of reusing the in-memory object reference +- `(RTPO15)` `PathObject#set` function: + - `(RTPO15a)` Expects the following arguments: + - `(RTPO15a1)` `key` `String` - the key to set the value for + - `(RTPO15a2)` `value` - the value to assign to the key. Accepted types are the same as for `LiveMap#set` ([RTLM20](#RTLM20)) + - `(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 +- `(RTPO16)` `PathObject#remove` function: + - `(RTPO16a)` Expects the following arguments: + - `(RTPO16a1)` `key` `String` - the key to remove the value for + - `(RTPO16b)` Resolves the path using the path resolution procedure ([RTPO3](#RTPO3)). On failure, throws per [RTPO3c2](#RTPO3c2) + - `(RTPO16c)` If the resolved value is a `LiveMap`, delegates to `LiveMap#remove` ([RTLM21](#RTLM21)) with the provided `key` + - `(RTPO16d)` If the resolved value is not a `LiveMap`, the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 92007 +- `(RTPO17)` `PathObject#increment` function: + - `(RTPO17a)` Expects the following arguments: + - `(RTPO17a1)` `amount` `Number` (optional) - the amount by which to increment the counter value. Defaults to 1 + - `(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 +- `(RTPO18)` `PathObject#decrement` function: + - `(RTPO18a)` Expects the following arguments: + - `(RTPO18a1)` `amount` `Number` (optional) - the amount by which to decrement the counter value. Defaults to 1 + - `(RTPO18b)` Resolves the path using the path resolution procedure ([RTPO3](#RTPO3)). On failure, throws per [RTPO3c2](#RTPO3c2) + - `(RTPO18c)` If the resolved value is a `LiveCounter`, delegates to `LiveCounter#decrement` ([RTLC13](#RTLC13)) with the provided `amount` + - `(RTPO18d)` If the resolved value is not a `LiveCounter`, the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 92007 + +### Instance + +An `Instance` holds a direct reference to a specific resolved `LiveObject` or primitive value. Unlike `PathObject` which is path-addressed and re-resolves on each call, `Instance` is identity-addressed: it follows the specific object it was created with, regardless of where that object sits in the tree. + +- `(RTINS1)` The `Instance` class provides a direct-reference view of a `LiveObject` or primitive value + - `(RTINS1a)` A specific SDK implementation may choose to expose a subset of the methods available on the `Instance` class based on the known underlying type. For example, the SDK may use type-specific class names (e.g. `LiveMapInstance`, `LiveCounterInstance`, `PrimitiveInstance`) that only expose the methods applicable to the wrapped type. The specification describes the general `Instance` class with the full set of methods +- `(RTINS2)` `Instance` has the following internal properties: + - `(RTINS2a)` `value` - a reference to the wrapped `LiveObject` or primitive value +- `(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 +- `(RTINS4)` `Instance#value` function: + - `(RTINS4a)` If the wrapped value is a `LiveCounter`, returns its current numeric value (equivalent to `LiveCounter#value`, see [RTLC5](#RTLC5)) + - `(RTINS4b)` If the wrapped value is a primitive (`Boolean`, `Binary`, `Number`, `String`, `JsonArray`, `JsonObject`), returns the value directly + - `(RTINS4c)` If the wrapped value is a `LiveMap`, returns undefined/null +- `(RTINS5)` `Instance#get` function: + - `(RTINS5a)` Expects the following arguments: + - `(RTINS5a1)` `key` `String` - the key to look up + - `(RTINS5b)` If the wrapped value is a `LiveMap`, looks up the value at `key` using `LiveMap#get` ([RTLM5](#RTLM5)) and returns a new `Instance` wrapping the result. If the result is undefined/null, returns undefined/null + - `(RTINS5c)` If the wrapped value is not a `LiveMap`, returns undefined/null +- `(RTINS6)` `Instance#entries` function: + - `(RTINS6a)` If the wrapped value is a `LiveMap`, returns an iterator yielding `[key, Instance]` pairs, where each `Instance` wraps the corresponding entry value from `LiveMap#entries` ([RTLM11](#RTLM11)) + - `(RTINS6b)` If the wrapped value is not a `LiveMap`, returns an empty iterator +- `(RTINS7)` `Instance#keys` function: + - `(RTINS7a)` Behaves identically to `Instance#entries` ([RTINS6](#RTINS6)) except that it yields only the keys +- `(RTINS8)` `Instance#values` function: + - `(RTINS8a)` Behaves identically to `Instance#entries` ([RTINS6](#RTINS6)) except that it yields only the `Instance` values +- `(RTINS9)` `Instance#size` function: + - `(RTINS9a)` If the wrapped value is a `LiveMap`, returns the number of non-tombstoned entries (equivalent to `LiveMap#size`, see [RTLM10](#RTLM10)) + - `(RTINS9b)` If the wrapped value is not a `LiveMap`, returns undefined/null +- `(RTINS10)` `Instance#compact` function: + - `(RTINS10a)` Behaves identically to `PathObject#compact` ([RTPO13](#RTPO13)), but operates on the wrapped value directly instead of resolving a path +- `(RTINS11)` `Instance#compactJson` function: + - `(RTINS11a)` Behaves identically to `PathObject#compactJson` ([RTPO14](#RTPO14)), but operates on the wrapped value directly instead of resolving a path +- `(RTINS12)` `Instance#set` function: + - `(RTINS12a)` Expects the following arguments: + - `(RTINS12a1)` `key` `String` - the key to set the value for + - `(RTINS12a2)` `value` - the value to assign to the key. Accepted types are the same as for `LiveMap#set` ([RTLM20](#RTLM20)) + - `(RTINS12b)` If the wrapped value is a `LiveMap`, delegates to `LiveMap#set` ([RTLM20](#RTLM20)) with the provided `key` and `value` + - `(RTINS12c)` If the wrapped value is not a `LiveMap`, the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 92007 +- `(RTINS13)` `Instance#remove` function: + - `(RTINS13a)` Expects the following arguments: + - `(RTINS13a1)` `key` `String` - the key to remove the value for + - `(RTINS13b)` If the wrapped value is a `LiveMap`, delegates to `LiveMap#remove` ([RTLM21](#RTLM21)) with the provided `key` + - `(RTINS13c)` If the wrapped value is not a `LiveMap`, the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 92007 +- `(RTINS14)` `Instance#increment` function: + - `(RTINS14a)` Expects the following arguments: + - `(RTINS14a1)` `amount` `Number` (optional) - the amount by which to increment the counter value. Defaults to 1 + - `(RTINS14b)` If the wrapped value is a `LiveCounter`, delegates to `LiveCounter#increment` ([RTLC12](#RTLC12)) with the provided `amount` + - `(RTINS14c)` If the wrapped value is not a `LiveCounter`, the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 92007 +- `(RTINS15)` `Instance#decrement` function: + - `(RTINS15a)` Expects the following arguments: + - `(RTINS15a1)` `amount` `Number` (optional) - the amount by which to decrement the counter value. Defaults to 1 + - `(RTINS15b)` If the wrapped value is a `LiveCounter`, delegates to `LiveCounter#decrement` ([RTLC13](#RTLC13)) with the provided `amount` + - `(RTINS15c)` If the wrapped value is not a `LiveCounter`, the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 92007 + ## Interface Definition {#idl} 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. class RealtimeObject: // RTO* - get() => io LiveMap // RTO23 + get() => io PathObject // RTO23 createMap(Dict entries?) => io LiveMap // RTO11 createCounter(Number count?) => io LiveCounter // RTO12 on(ObjectsEvent event, (() ->) callback) -> StatusSubscription // RTO18 @@ -783,3 +940,35 @@ Types and their properties/methods are public and exposed to users by default. A interface LiveMapUpdate extends LiveObjectUpdate: // RTLM18, RTLM18a update: Dict // RTLM18b + + class PathObject: // RTPO* + path() -> String // RTPO4 + get(String key) -> PathObject // RTPO5 + at(String path) -> PathObject // RTPO6 + value() -> (Boolean | Binary | Number | String | JsonArray | JsonObject)? // RTPO7 + instance() -> Instance? // RTPO8 + entries() -> Iterator<[String, PathObject]> // RTPO9 + keys() -> Iterator // RTPO10 + values() -> Iterator // RTPO11 + size() -> Number? // RTPO12 + compact() -> Object? // RTPO13 + compactJson() -> Object? // RTPO14 + set(String key, (Boolean | Binary | Number | String | JsonArray | JsonObject | LiveCounter | LiveMap) value) => io // RTPO15 + remove(String key) => io // RTPO16 + increment(Number amount?) => io // RTPO17 + decrement(Number amount?) => io // RTPO18 + + class Instance: // RTINS* + id: String? // RTINS3 + value() -> (Boolean | Binary | Number | String | JsonArray | JsonObject)? // RTINS4 + get(String key) -> Instance? // RTINS5 + entries() -> Iterator<[String, Instance]> // RTINS6 + keys() -> Iterator // RTINS7 + values() -> Iterator // RTINS8 + size() -> Number? // RTINS9 + compact() -> Object? // RTINS10 + compactJson() -> Object? // RTINS11 + set(String key, (Boolean | Binary | Number | String | JsonArray | JsonObject | LiveCounter | LiveMap) value) => io // RTINS12 + remove(String key) => io // RTINS13 + increment(Number amount?) => io // RTINS14 + decrement(Number amount?) => io // RTINS15 From 8661d3d55d03221cfd54e0a67b6c3ede73790920 Mon Sep 17 00:00:00 2001 From: Andrew Bulat Date: Tue, 24 Feb 2026 12:12:31 +0000 Subject: [PATCH 02/40] LiveMap and LiveCounter creation via value types --- specifications/features.md | 6 +- specifications/objects-features.md | 313 ++++++++++++++++++----------- 2 files changed, 194 insertions(+), 125 deletions(-) diff --git a/specifications/features.md b/specifications/features.md index 32f5a0ab..11323233 100644 --- a/specifications/features.md +++ b/specifications/features.md @@ -939,7 +939,7 @@ The threading and/or asynchronous model for each realtime library will vary by l ### RealtimeObject {#realtime-objects} -Reserved for `RealtimeObject` feature specification, see [objects-features](../objects-features). Reserved spec points: `RTO`, `RTLO`, `RTLC`, `RTLM`, `RTPO`, `RTINS` +Reserved for `RealtimeObject` feature specification, see [objects-features](../objects-features). Reserved spec points: `RTO`, `RTLO`, `RTLC`, `RTLM`, `RTPO`, `RTINS`, `RTLCV`, `RTLMV` ### RealtimeAnnotations {#realtime-annotations} @@ -1331,13 +1331,13 @@ The core SDK provides an API for wrapper SDKs to supply Ably with analytics info - `(OOP4g)` The size is the sum of the sizes of the map create, `mapSet`, `mapRemove`, counter create, and `counterInc` components - `(OOP4h)` The size of the map create component is: - `(OOP4h1)` If `mapCreate` is present, it is equal to the size of `mapCreate` calculated per [MCR3](#MCR3) - - `(OOP4h2)` Else if `mapCreateWithObjectId` is present, it is equal to the size of the `MapCreate` retained in [RTO11f18](objects-features#RTO11f18), calculated per [MCR3](#MCR3) + - `(OOP4h2)` Else if `mapCreateWithObjectId` is present, it is equal to the size of the `MapCreate` retained in [RTLMV4j5](objects-features#RTLMV4j5), calculated per [MCR3](#MCR3) - `(OOP4h3)` Otherwise it is zero - `(OOP4i)` The size of the `mapSet` property is calculated per [MST3](#MST3) - `(OOP4j)` The size of the `mapRemove` property is calculated per [MRM3](#MRM3) - `(OOP4k)` The size of the counter create component is: - `(OOP4k1)` If `counterCreate` is present, it is equal to the size of `counterCreate` calculated per [CCR3](#CCR3) - - `(OOP4k2)` Else if `counterCreateWithObjectId` is present, it is equal to the size of the `CounterCreate` retained in [RTO12f16](objects-features#RTO12f16), calculated per [CCR3](#CCR3) + - `(OOP4k2)` Else if `counterCreateWithObjectId` is present, it is equal to the size of the `CounterCreate` retained in [RTLCV4g5](objects-features#RTLCV4g5), calculated per [CCR3](#CCR3) - `(OOP4k3)` Otherwise it is zero - `(OOP4l)` The size of the `counterInc` property is calculated per [CIN3](#CIN3) - `(OOP4f)` The size of a `null` or omitted property is zero diff --git a/specifications/objects-features.md b/specifications/objects-features.md index e8b3eda8..22a6d825 100644 --- a/specifications/objects-features.md +++ b/specifications/objects-features.md @@ -20,17 +20,17 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTO23b)` If the channel is in the `DETACHED` or `FAILED` state, the library should throw an `ErrorInfo` error with `statusCode` 400 and `code` 90001 - `(RTO23c)` If the [RTO17](#RTO17) sync state is not `SYNCED`, waits for the sync state to transition to `SYNCED` - `(RTO23d)` Returns a `PathObject` ([RTPO1](#RTPO1)) wrapping the `LiveMap` with id `root` from the internal `ObjectsPool`. The `PathObject` is created with an empty path, rooted at the `root` `LiveMap` -- `(RTO11)` `RealtimeObject#createMap` function: - - `(RTO11a)` Expects the following arguments: - - `(RTO11a1)` `entries` `Dict` (optional) - the initial entries for the new `LiveMap` object - - `(RTO11b)` The return type is a `LiveMap`, which is returned once the required I/O has successfully completed - - `(RTO11c)` Requires the `OBJECT_PUBLISH` channel mode to be granted per [RTO2](#RTO2) - - `(RTO11d)` If the channel is in the `DETACHED`, `FAILED` or `SUSPENDED` state, the library should throw an `ErrorInfo` error with `statusCode` 400 and `code` 90001 - - `(RTO11e)` If [`echoMessages`](../features#TO3h) client option is `false`, the library should throw an `ErrorInfo` error with `statusCode` 400 and `code` 40000, indicating that `echoMessages` must be enabled for this operation - - `(RTO11f)` Creates an `ObjectMessage` for a `MAP_CREATE` action in the following way: - - `(RTO11f1)` If `entries` is null or not of type `Dict`, the library should throw an `ErrorInfo` error with `statusCode` 400 and `code` 40003, indicating that `entries` must be a `Dict`. Note that `entries` is an optional argument, and if omitted, this error must not be thrown - - `(RTO11f2)` If any of the keys provided in `entries` are not of type `String`, the library should throw an `ErrorInfo` error with `statusCode` 400 and `code` 40003, indicating that keys must be `String` - - `(RTO11f3)` If any of the values provided in `entries` are not of an expected type, the library should throw an `ErrorInfo` error with `statusCode` 400 and `code` 40013, indicating that such data type is unsupported +- `(RTO11)` This clause has been replaced by [RTLMV3](#RTLMV3). + - `(RTO11a)` This clause has been replaced by [RTLMV3](#RTLMV3). + - `(RTO11a1)` This clause has been replaced by [RTLMV3](#RTLMV3). + - `(RTO11b)` This clause has been replaced by [RTLMV3](#RTLMV3). + - `(RTO11c)` This clause has been replaced by [RTLMV3](#RTLMV3). + - `(RTO11d)` This clause has been replaced by [RTLMV3](#RTLMV3). + - `(RTO11e)` This clause has been replaced by [RTLMV3](#RTLMV3). + - `(RTO11f)` This clause has been replaced by [RTLMV3](#RTLMV3). + - `(RTO11f1)` This clause has been replaced by [RTLMV3](#RTLMV3). + - `(RTO11f2)` This clause has been replaced by [RTLMV3](#RTLMV3). + - `(RTO11f3)` This clause has been replaced by [RTLMV3](#RTLMV3). - `(RTO11f4)` This clause has been replaced by [RTO11f14](#RTO11f14) as of specification version 6.0.0. - `(RTO11f4a)` This clause has been replaced by [RTO11f14a](#RTO11f14a) as of specification version 6.0.0. - `(RTO11f4b)` This clause has been replaced by [RTO11f14b](#RTO11f14b) as of specification version 6.0.0. @@ -43,98 +43,83 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTO11f4c1e)` This clause has been replaced by [RTO11f14c1e](#RTO11f14c1e) as of specification version 6.0.0. - `(RTO11f4c1f)` This clause has been replaced by [RTO11f14c1f](#RTO11f14c1f) as of specification version 6.0.0. - `(RTO11f4c2)` This clause has been replaced by [RTO11f14c2](#RTO11f14c2) as of specification version 6.0.0. - - `(RTO11f14)` Create a `MapCreate` object with the initial value for the new `LiveMap`: - - `(RTO11f14a)` Set `MapCreate.semantics` to `ObjectsMapSemantics.LWW` - - `(RTO11f14b)` Set `MapCreate.entries` to an empty map if `entries` is omitted - - `(RTO11f14c)` Otherwise, set `MapCreate.entries` based on the provided `entries`. For each key-value pair in `entries`: - - `(RTO11f14c1)` Create an `ObjectsMapEntry` for the current value: - - `(RTO11f14c1a)` If the value is of type `LiveCounter` or `LiveMap`, set `ObjectsMapEntry.data.objectId` to the `objectId` of that object - - `(RTO11f14c1b)` If the value is of type `JsonArray` or `JsonObject`, set `ObjectsMapEntry.data.json` to that value - - `(RTO11f14c1c)` If the value is of type `String`, set `ObjectsMapEntry.data.string` to that value - - `(RTO11f14c1d)` If the value is of type `Number`, set `ObjectsMapEntry.data.number` to that value - - `(RTO11f14c1e)` If the value is of type `Boolean`, set `ObjectsMapEntry.data.boolean` to that value - - `(RTO11f14c1f)` If the value is of type `Binary`, set `ObjectsMapEntry.data.bytes` to that value - - `(RTO11f14c2)` Add a new entry to `MapCreate.entries` with the current key and the created `ObjectsMapEntry` as the value + - `(RTO11f14)` This clause has been replaced by [RTLMV3](#RTLMV3). + - `(RTO11f14a)` This clause has been replaced by [RTLMV3](#RTLMV3). + - `(RTO11f14b)` This clause has been replaced by [RTLMV3](#RTLMV3). + - `(RTO11f14c)` This clause has been replaced by [RTLMV3](#RTLMV3). + - `(RTO11f14c1)` This clause has been replaced by [RTLMV3](#RTLMV3). + - `(RTO11f14c1a)` This clause has been replaced by [RTLMV3](#RTLMV3). + - `(RTO11f14c1b)` This clause has been replaced by [RTLMV3](#RTLMV3). + - `(RTO11f14c1c)` This clause has been replaced by [RTLMV3](#RTLMV3). + - `(RTO11f14c1d)` This clause has been replaced by [RTLMV3](#RTLMV3). + - `(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). - `(RTO11f5)` This clause has been replaced by [RTO11f15](#RTO11f15) as of specification version 6.0.0. - - `(RTO11f15)` Create an initial value JSON string based on `MapCreate` object from [RTO11f14](#RTO11f14) as follows: - - `(RTO11f15a)` The `MapCreate` object may contain user-provided `ObjectData` that requires encoding. Encode the `ObjectData` values using the procedure described in [OD4](../features#OD4) - - `(RTO11f15b)` Return a JSON string representation of the encoded `MapCreate` object - - `(RTO11f6)` Create a unique string nonce with 16+ characters; the nonce is used to ensure object ID uniqueness across clients - - `(RTO11f7)` Get the current server time as described in [RTO16](#RTO16) - - `(RTO11f8)` Create an `objectId` for the new `LiveMap` object as described in [RTO14](#RTO14), passing in `map` string as the `type`, the initial value JSON string from [RTO11f15](#RTO11f15), the nonce from [RTO11f6](#RTO11f6), and the server time from [RTO11f7](#RTO11f7) - - `(RTO11f9)` Set `ObjectMessage.operation.action` to `ObjectOperationAction.MAP_CREATE` - - `(RTO11f10)` Set `ObjectMessage.operation.objectId` to the `objectId` created in [RTO11f8](#RTO11f8) + - `(RTO11f15)` This clause has been replaced by [RTLMV3](#RTLMV3). + - `(RTO11f15a)` This clause has been replaced by [RTLMV3](#RTLMV3). + - `(RTO11f15b)` This clause has been replaced by [RTLMV3](#RTLMV3). + - `(RTO11f6)` This clause has been replaced by [RTLMV3](#RTLMV3). + - `(RTO11f7)` This clause has been replaced by [RTLMV3](#RTLMV3). + - `(RTO11f8)` This clause has been replaced by [RTLMV3](#RTLMV3). + - `(RTO11f9)` This clause has been replaced by [RTLMV3](#RTLMV3). + - `(RTO11f10)` This clause has been replaced by [RTLMV3](#RTLMV3). - `(RTO11f11)` This clause has been replaced by [RTO11f16](#RTO11f16) as of specification version 6.0.0. - `(RTO11f12)` This clause has been replaced by [RTO11f17](#RTO11f17) as of specification version 6.0.0. - `(RTO11f13)` This clause has been deleted as of specification version 6.0.0. - - `(RTO11f16)` Set `ObjectMessage.operation.mapCreateWithObjectId.nonce` to the nonce value created in [RTO11f6](#RTO11f6) - - `(RTO11f17)` Set `ObjectMessage.operation.mapCreateWithObjectId.initialValue` to the JSON string created in [RTO11f15](#RTO11f15) - - `(RTO11f18)` The client library must retain the `MapCreate` object from [RTO11f14](#RTO11f14) alongside the `MapCreateWithObjectId`. It is the operation from which the `MapCreateWithObjectId` was derived, and is needed for message size calculation ([OOP4h2](../features#OOP4h2)) and local application of the operation ([RTLM23](#RTLM23)). This `MapCreate` is for local use only and must not be sent over the wire. + - `(RTO11f16)` This clause has been replaced by [RTLMV3](#RTLMV3). + - `(RTO11f17)` This clause has been replaced by [RTLMV3](#RTLMV3). + - `(RTO11f18)` This clause has been replaced by [RTLMV4j5](#RTLMV4j5). - `(RTO11g)` This clause has been replaced by [RTO11i](#RTO11i) - - `(RTO11i)` Publishes the `ObjectMessage` from [RTO11f](#RTO11f) using `RealtimeObject#publishAndApply` ([RTO20](#RTO20)), passing the `ObjectMessage` as a single element in the array - - `(RTO11i1)` The client library waits for the publish operation I/O to complete. On failure, an error is returned to the caller; on success, the `createMap` operation continues - - `(RTO11h)` Returns a `LiveMap` instance: + - `(RTO11i)` This clause has been replaced by [RTLMV3](#RTLMV3). + - `(RTO11i1)` This clause has been replaced by [RTLMV3](#RTLMV3). + - `(RTO11h)` This clause has been replaced by [RTLMV3](#RTLMV3). - `(RTO11h1)` This clause has been deleted. - - `(RTO11h2)` If an object with the `ObjectMessage.operation.objectId` exists in the internal `ObjectsPool`, return it - - `(RTO11h3)` Otherwise, if the object does not exist in the internal `ObjectsPool`: + - `(RTO11h2)` This clause has been replaced by [RTLMV3](#RTLMV3). + - `(RTO11h3)` This clause has been replaced by [RTLMV3](#RTLMV3). - `(RTO11h3a)` This clause has been deleted. - `(RTO11h3b)` This clause has been deleted. - `(RTO11h3c)` This clause has been deleted. - - `(RTO11h3d)` The library should throw an `ErrorInfo` error with `statusCode` 500 and `code` 50000 (Note: this is not expected to happen since the object should have been created as part of applying the `MAP_CREATE` operation via `publishAndApply` in [RTO11i](#RTO11i)) -- `(RTO12)` `RealtimeObject#createCounter` function: - - `(RTO12a)` Expects the following arguments: - - `(RTO12a1)` `count` `Number` (optional) - the initial count for the new `LiveCounter` object - - `(RTO12b)` The return type is a `LiveCounter`, which is returned once the required I/O has successfully completed - - `(RTO12c)` Requires the `OBJECT_PUBLISH` channel mode to be granted per [RTO2](#RTO2) - - `(RTO12d)` If the channel is in the `DETACHED`, `FAILED` or `SUSPENDED` state, the library should throw an `ErrorInfo` error with `statusCode` 400 and `code` 90001 - - `(RTO12e)` If [`echoMessages`](../features#TO3h) client option is `false`, the library should throw an `ErrorInfo` error with `statusCode` 400 and `code` 40000, indicating that `echoMessages` must be enabled for this operation - - `(RTO12f)` Creates an `ObjectMessage` for a `COUNTER_CREATE` action in the following way: - - `(RTO12f1)` If `count` is null, not of type `Number`, or not a finite number, the library should throw an `ErrorInfo` error with `statusCode` 400 and `code` 40003, indicating that `count` must be a valid number. Note that `count` is an optional argument, and if omitted, this error must not be thrown + - `(RTO11h3d)` This clause has been replaced by [RTLMV3](#RTLMV3). +- `(RTO12)` This clause has been replaced by [RTLCV3](#RTLCV3). + - `(RTO12a)` This clause has been replaced by [RTLCV3](#RTLCV3). + - `(RTO12a1)` This clause has been replaced by [RTLCV3](#RTLCV3). + - `(RTO12b)` This clause has been replaced by [RTLCV3](#RTLCV3). + - `(RTO12c)` This clause has been replaced by [RTLCV3](#RTLCV3). + - `(RTO12d)` This clause has been replaced by [RTLCV3](#RTLCV3). + - `(RTO12e)` This clause has been replaced by [RTLCV3](#RTLCV3). + - `(RTO12f)` This clause has been replaced by [RTLCV3](#RTLCV3). + - `(RTO12f1)` This clause has been replaced by [RTLCV3](#RTLCV3). - `(RTO12f2)` This clause has been replaced by [RTO12f12](#RTO12f12) as of specification version 6.0.0. - `(RTO12f2a)` This clause has been replaced by [RTO12f12a](#RTO12f12a) as of specification version 6.0.0. - `(RTO12f2b)` This clause has been replaced by [RTO12f12b](#RTO12f12b) as of specification version 6.0.0. - - `(RTO12f12)` Create a `CounterCreate` object with the initial value for the new `LiveCounter`: - - `(RTO12f12a)` Set `CounterCreate.count` to 0 if `count` is omitted - - `(RTO12f12b)` Otherwise, set `CounterCreate.count` to the provided `count` value + - `(RTO12f12)` This clause has been replaced by [RTLCV3](#RTLCV3). + - `(RTO12f12a)` This clause has been replaced by [RTLCV3](#RTLCV3). + - `(RTO12f12b)` This clause has been replaced by [RTLCV3](#RTLCV3). - `(RTO12f3)` This clause has been replaced by [RTO12f13](#RTO12f13) as of specification version 6.0.0. - - `(RTO12f13)` Create an initial value JSON string by generating a JSON string representation of the `CounterCreate` object from [RTO12f12](#RTO12f12) - - `(RTO12f4)` Create a unique string nonce with 16+ characters; the nonce is used to ensure object ID uniqueness across clients - - `(RTO12f5)` Get the current server time as described in [RTO16](#RTO16) - - `(RTO12f6)` Create an `objectId` for the new `LiveCounter` object as described in [RTO14](#RTO14), passing in `counter` string as the `type`, the initial value JSON string from [RTO12f13](#RTO12f13), the nonce from [RTO12f4](#RTO12f4), and the server time from [RTO12f5](#RTO12f5) - - `(RTO12f7)` Set `ObjectMessage.operation.action` to `ObjectOperationAction.COUNTER_CREATE` - - `(RTO12f8)` Set `ObjectMessage.operation.objectId` to the `objectId` created in [RTO12f6](#RTO12f6) + - `(RTO12f13)` This clause has been replaced by [RTLCV3](#RTLCV3). + - `(RTO12f4)` This clause has been replaced by [RTLCV3](#RTLCV3). + - `(RTO12f5)` This clause has been replaced by [RTLCV3](#RTLCV3). + - `(RTO12f6)` This clause has been replaced by [RTLCV3](#RTLCV3). + - `(RTO12f7)` This clause has been replaced by [RTLCV3](#RTLCV3). + - `(RTO12f8)` This clause has been replaced by [RTLCV3](#RTLCV3). - `(RTO12f9)` This clause has been replaced by [RTO12f14](#RTO12f14) as of specification version 6.0.0. - `(RTO12f10)` This clause has been replaced by [RTO12f15](#RTO12f15) as of specification version 6.0.0. - `(RTO12f11)` This clause has been deleted as of specification version 6.0.0. - -\* `(RTO12f14)` Set `ObjectMessage.operation.counterCreateWithObjectId.nonce` to the nonce value created in [RTO12f4](#RTO12f4) - -\* `(RTO12f15)` Set `ObjectMessage.operation.counterCreateWithObjectId.initialValue` to the JSON string created in [RTO12f13](#RTO12f13) - -\* `(RTO12f16)` The client library must retain the `CounterCreate` object from [RTO12f12](#RTO12f12) alongside the `CounterCreateWithObjectId`. It is the operation from which the `CounterCreateWithObjectId` was derived, and is needed for message size calculation ([OOP4k2](../features#OOP4k2)) and local application of the operation ([RTLC16](#RTLC16)). This `CounterCreate` is for local use only and must not be sent over the wire. - -`(RTO12g)` This clause has been replaced by [RTO12i](#RTO12i) - -`(RTO12i)` Publishes the `ObjectMessage` from [RTO12f](#RTO12f) using `RealtimeObject#publishAndApply` ([RTO20](#RTO20)), passing the `ObjectMessage` as a single element in the array - -\* `(RTO12i1)` The client library waits for the publish operation I/O to complete. On failure, an error is returned to the caller; on success, the `createCounter` operation continues - -`(RTO12h)` Returns a `LiveCounter` instance: - -\* `(RTO12h1)` This clause has been deleted. - -\* `(RTO12h2)` If an object with the `ObjectMessage.operation.objectId` exists in the internal `ObjectsPool`, return it - -\* `(RTO12h3)` Otherwise, if the object does not exist in the internal `ObjectsPool`: - -`(RTO12h3a)` This clause has been deleted. - -`(RTO12h3b)` This clause has been deleted. - -`(RTO12h3c)` This clause has been deleted. - -`(RTO12h3d)` The library should throw an `ErrorInfo` error with `statusCode` 500 and `code` 50000 (Note: this is not expected to happen since the object should have been created as part of applying the `COUNTER_CREATE` operation via `publishAndApply` in [RTO12i](#RTO12i)) - + - `(RTO12f14)` This clause has been replaced by [RTLCV3](#RTLCV3). + - `(RTO12f15)` This clause has been replaced by [RTLCV3](#RTLCV3). + - `(RTO12f16)` This clause has been replaced by [RTLCV4g5](#RTLCV4g5). + - `(RTO12g)` This clause has been replaced by [RTO12i](#RTO12i) + - `(RTO12i)` This clause has been replaced by [RTLCV3](#RTLCV3). + - `(RTO12i1)` This clause has been replaced by [RTLCV3](#RTLCV3). + - `(RTO12h)` This clause has been replaced by [RTLCV3](#RTLCV3). + - `(RTO12h1)` This clause has been deleted. + - `(RTO12h2)` This clause has been replaced by [RTLCV3](#RTLCV3). + - `(RTO12h3)` This clause has been replaced by [RTLCV3](#RTLCV3). + - `(RTO12h3a)` This clause has been deleted. + - `(RTO12h3b)` This clause has been deleted. + - `(RTO12h3c)` This clause has been deleted. + - `(RTO12h3d)` This clause has been replaced by [RTLCV3](#RTLCV3). - `(RTO2)` Certain object operations may require a specific channel mode to be set on a channel in order to be performed. If a specific channel mode is required by an operation, then: - `(RTO2a)` If the channel is in the `ATTACHED` state, the presence of the required channel mode is checked against the set of channel modes granted by the server per [RTL4m](../features#RTL4m) : - `(RTO2a1)` If the channel mode is in the set, the operation is allowed @@ -457,7 +442,7 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLC10b)` This clause has been replaced by [RTLC16b](#RTLC16b) as of specification version 6.0.0. - `(RTLC10c)` This clause has been replaced by [RTLC16c](#RTLC16c) as of specification version 6.0.0. - `(RTLC10d)` This clause has been replaced by [RTLC16d](#RTLC16d) as of specification version 6.0.0. -- `(RTLC16)` The initial value from an `ObjectOperation` can be merged into this `LiveCounter` in the following way. Let `counterCreate` be `ObjectOperation.counterCreate` if present, else the `CounterCreate` from which `ObjectOperation.counterCreateWithObjectId` was derived (see [RTO12f16](#RTO12f16)): +- `(RTLC16)` The initial value from an `ObjectOperation` can be merged into this `LiveCounter` in the following way. Let `counterCreate` be `ObjectOperation.counterCreate` if present, else the `CounterCreate` from which `ObjectOperation.counterCreateWithObjectId` was derived (see [RTLCV4g5](#RTLCV4g5)): - `(RTLC16a)` Add `counterCreate.count` to `data`, if it exists - `(RTLC16b)` Set the private flag `createOperationIsMerged` to `true` - `(RTLC16c)` If `counterCreate.count` exists, return a `LiveCounterUpdate` object with `LiveCounterUpdate.update.amount` set to `counterCreate.count` @@ -521,12 +506,13 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLM20)` `LiveMap#set` function: - `(RTLM20a)` Expects the following arguments: - `(RTLM20a1)` `key` `String` - the key to set the value for - - `(RTLM20a2)` `value` `Boolean | Binary | Number | String | JsonArray | JsonObject | LiveCounter | LiveMap` - the value to assign to the key + - `(RTLM20a2)` This clause has been replaced by [RTLM20a3](#RTLM20a3). + - `(RTLM20a3)` `value` `Boolean | Binary | Number | String | JsonArray | JsonObject | LiveCounterValueType | LiveMapValueType` - the value to assign to the key - `(RTLM20b)` Requires the `OBJECT_PUBLISH` channel mode to be granted per [RTO2](#RTO2) - `(RTLM20c)` If the channel is in the `DETACHED`, `FAILED` or `SUSPENDED` state, the library should throw an `ErrorInfo` error with `statusCode` 400 and `code` 90001 - `(RTLM20d)` If [`echoMessages`](../features#TO3h) client option is `false`, the library should throw an `ErrorInfo` error with `statusCode` 400 and `code` 40000, indicating that `echoMessages` must be enabled for this operation - `(RTLM20e)` Creates an `ObjectMessage` for a `MAP_SET` action in the following way: - - `(RTLM20e1)` Validates the provided `key` and `value` in a similar way as described in [RTO11f2](#RTO11f2) and [RTO11f3](#RTO11f3) + - `(RTLM20e1)` Validates the provided `key` and `value` in a similar way as described in [RTLMV4b](#RTLMV4b) and [RTLMV4c](#RTLMV4c) - `(RTLM20e2)` Set `ObjectMessage.operation.action` to `ObjectOperationAction.MAP_SET` - `(RTLM20e3)` Set `ObjectMessage.operation.objectId` to the Object ID of this `LiveMap` - `(RTLM20e4)` This clause has been replaced by [RTLM20e6](#RTLM20e6) as of specification version 6.0.0. @@ -539,14 +525,20 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLM20e5f)` This clause has been replaced by [RTLM20e7f](#RTLM20e7f) as of specification version 6.0.0. - `(RTLM20e6)` Set `ObjectMessage.operation.mapSet.key` to the provided `key` value - `(RTLM20e7)` Set `ObjectMessage.operation.mapSet.value` depending on the type of the provided `value`: - - `(RTLM20e7a)` If the `value` is of type `LiveCounter` or `LiveMap`, set `ObjectMessage.operation.mapSet.value.objectId` to the `objectId` of that object + - `(RTLM20e7a)` This clause has been replaced by [RTLM20e7g](#RTLM20e7g). + - `(RTLM20e7g)` If the `value` is of type `LiveCounterValueType` or `LiveMapValueType`: + - `(RTLM20e7g1)` Consume the value type per [RTLCV4](#RTLCV4) or [RTLMV4](#RTLMV4) respectively to generate `*_CREATE` `ObjectMessages`. Collect all generated `ObjectMessages` + - `(RTLM20e7g2)` Set `ObjectMessage.operation.mapSet.value.objectId` to the `objectId` from the outermost `*_CREATE` `ObjectMessage` - `(RTLM20e7b)` If the `value` is of type `JsonArray` or `JsonObject`, set `ObjectMessage.operation.mapSet.value.json` to that value - `(RTLM20e7c)` If the `value` is of type `String`, set `ObjectMessage.operation.mapSet.value.string` to that value - `(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 - `(RTLM20f)` This clause has been replaced by [RTLM20g](#RTLM20g) - - `(RTLM20g)` Publishes the `ObjectMessage` from [RTLM20e](#RTLM20e) using `RealtimeObject#publishAndApply` ([RTO20](#RTO20)), passing the `ObjectMessage` as a single element in the array + - `(RTLM20g)` This clause has been replaced by [RTLM20h](#RTLM20h). + - `(RTLM20h)` Publishes all `ObjectMessages` using `RealtimeObject#publishAndApply` ([RTO20](#RTO20)): + - `(RTLM20h1)` If the `value` is of type `LiveCounterValueType` or `LiveMapValueType`, the array contains the `*_CREATE` `ObjectMessages` collected in [RTLM20e7g1](#RTLM20e7g1) followed by the `MAP_SET` `ObjectMessage` from [RTLM20e](#RTLM20e) + - `(RTLM20h2)` Otherwise, the `MAP_SET` `ObjectMessage` from [RTLM20e](#RTLM20e) is passed as a single element in the array - `(RTLM21)` `LiveMap#remove` function: - `(RTLM21a)` Expects the following arguments: - `(RTLM21a1)` `key` `String` - the key to remove the value for @@ -554,7 +546,7 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLM21c)` If the channel is in the `DETACHED`, `FAILED` or `SUSPENDED` state, the library should throw an `ErrorInfo` error with `statusCode` 400 and `code` 90001 - `(RTLM21d)` If [`echoMessages`](../features#TO3h) client option is `false`, the library should throw an `ErrorInfo` error with `statusCode` 400 and `code` 40000, indicating that `echoMessages` must be enabled for this operation - `(RTLM21e)` Creates an `ObjectMessage` for a `MAP_REMOVE` action in the following way: - - `(RTLM21e1)` Validates the provided `key` in a similar way as described in [RTO11f2](#RTO11f2) + - `(RTLM21e1)` Validates the provided `key` in a similar way as described in [RTLMV4b](#RTLMV4b) - `(RTLM21e2)` Set `ObjectMessage.operation.action` to `ObjectOperationAction.MAP_REMOVE` - `(RTLM21e3)` Set `ObjectMessage.operation.objectId` to the Object ID of this `LiveMap` - `(RTLM21e4)` This clause has been replaced by [RTLM21e5](#RTLM21e5) as of specification version 6.0.0. @@ -697,7 +689,7 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLM17a2)` This clause has been replaced by [RTLM23a2](#RTLM23a2) as of specification version 6.0.0. - `(RTLM17b)` This clause has been replaced by [RTLM23b](#RTLM23b) as of specification version 6.0.0. - `(RTLM17c)` This clause has been replaced by [RTLM23c](#RTLM23c) as of specification version 6.0.0. -- `(RTLM23)` The initial value from an `ObjectOperation` can be merged into this `LiveMap` in the following way. Let `mapCreate` be `ObjectOperation.mapCreate` if present, else the `MapCreate` from which `ObjectOperation.mapCreateWithObjectId` was derived (see [RTO11f18](#RTO11f18)): +- `(RTLM23)` The initial value from an `ObjectOperation` can be merged into this `LiveMap` in the following way. Let `mapCreate` be `ObjectOperation.mapCreate` if present, else the `MapCreate` from which `ObjectOperation.mapCreateWithObjectId` was derived (see [RTLMV4j5](#RTLMV4j5)): - `(RTLM23a)` For each key-`ObjectsMapEntry` pair in `mapCreate.entries`: - `(RTLM23a1)` If `ObjectsMapEntry.tombstone` is `false` or omitted, apply the `MAP_SET` operation to the current key as described in [RTLM7](#RTLM7), passing in `ObjectsMapEntry.data` and the current key as `MapSet`, and `ObjectsMapEntry.timeserial` as `serial`. Store the returned `LiveMapUpdate` object for use in [RTLM23c](#RTLM23c) - `(RTLM23a2)` If `ObjectsMapEntry.tombstone` is `true`, apply the `MAP_REMOVE` operation to the current key as described in [RTLM8](#RTLM8), passing in the current key as `MapRemove`, `ObjectsMapEntry.timeserial` as `serial`, and `ObjectsMapEntry.serialTimestamp` as `serialTimestamp`. Store the returned `LiveMapUpdate` object for use in [RTLM23c](#RTLM23c) @@ -715,6 +707,77 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLM22b2)` For each key that exists in the non-tombstoned entries of `newData` but does not exist in the non-tombstoned entries of `previousData`, add the key to `LiveMapUpdate.update` with the value `updated` - `(RTLM22b3)` For each key that exists in the non-tombstoned entries of both `previousData` and `newData`, perform a deep comparison of the `data` attributes from `previousData` and `newData`. If the data values differ, add the key to `LiveMapUpdate.update` with the value `updated` +### LiveCounterValueType + +A `LiveCounterValueType` is an immutable blueprint for creating a new `LiveCounter` object. It stores the desired initial count value and is consumed when passed to a mutation method such as `LiveMap#set` ([RTLM20](#RTLM20)) or as an entry value in `LiveMap.create` ([RTLMV3](#RTLMV3)). + +- `(RTLCV1)` `LiveCounterValueType` is an immutable value type representing the intent to create a new `LiveCounter` with a specific initial count +- `(RTLCV2)` `LiveCounterValueType` has the following internal properties: + - `(RTLCV2a)` `count` `Number` - the initial count value for the `LiveCounter` to be created +- `(RTLCV3)` `LiveCounter.create` static factory function: + - `(RTLCV3a)` Expects the following arguments: + - `(RTLCV3a1)` `initialCount` `Number` (optional) - the initial count for the new `LiveCounter` object. Defaults to 0 + - `(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 +- `(RTLCV4)` Internal consumption procedure - when a `LiveCounterValueType` is consumed by a mutation method (e.g. `LiveMap#set` or as an entry value during `LiveMapValueType` consumption per [RTLMV4](#RTLMV4)), a `COUNTER_CREATE` `ObjectMessage` is generated as follows: + - `(RTLCV4a)` If the internal `count` is not undefined and (is not of type `Number` or is not a finite number), the library should throw an `ErrorInfo` error with `statusCode` 400 and `code` 40003, indicating that the counter value must be a valid number + - `(RTLCV4b)` Create a `CounterCreate` object: + - `(RTLCV4b1)` Set `CounterCreate.count` to the internal `count` value, or to 0 if undefined + - `(RTLCV4c)` Create an initial value JSON string by generating a JSON string representation of the `CounterCreate` object + - `(RTLCV4d)` Create a unique string nonce with 16+ characters + - `(RTLCV4e)` Get the current server time as described in [RTO16](#RTO16) + - `(RTLCV4f)` Create an `objectId` for the new `LiveCounter` as described in [RTO14](#RTO14), passing in `counter` as the `type`, the initial value JSON string from [RTLCV4c](#RTLCV4c), the nonce from [RTLCV4d](#RTLCV4d), and the server time from [RTLCV4e](#RTLCV4e) + - `(RTLCV4g)` Create an `ObjectMessage` with: + - `(RTLCV4g1)` `ObjectMessage.operation.action` set to `ObjectOperationAction.COUNTER_CREATE` + - `(RTLCV4g2)` `ObjectMessage.operation.objectId` set to the `objectId` from [RTLCV4f](#RTLCV4f) + - `(RTLCV4g3)` `ObjectMessage.operation.counterCreateWithObjectId.nonce` set to the nonce from [RTLCV4d](#RTLCV4d) + - `(RTLCV4g4)` `ObjectMessage.operation.counterCreateWithObjectId.initialValue` set to the JSON string from [RTLCV4c](#RTLCV4c) + - `(RTLCV4g5)` The client library must retain the `CounterCreate` object from [RTLCV4b](#RTLCV4b) alongside the `CounterCreateWithObjectId`. It is the operation from which the `CounterCreateWithObjectId` was derived, and is needed for message size calculation ([OOP4k2](../features#OOP4k2)) and local application of the operation ([RTLC16](#RTLC16)). This `CounterCreate` is for local use only and must not be sent over the wire. + - `(RTLCV4h)` Return the `ObjectMessage` + +### LiveMapValueType + +A `LiveMapValueType` is an immutable blueprint for creating a new `LiveMap` object. It stores the desired initial entries and is consumed when passed to a mutation method such as `LiveMap#set` ([RTLM20](#RTLM20)) or as an entry value in another `LiveMap.create` ([RTLMV3](#RTLMV3)) call. Supports arbitrarily deep nesting of `LiveMapValueType` and `LiveCounterValueType` values within entries. + +- `(RTLMV1)` `LiveMapValueType` is an immutable value type representing the intent to create a new `LiveMap` with specific initial entries +- `(RTLMV2)` `LiveMapValueType` has the following internal properties: + - `(RTLMV2a)` `entries` `Dict` (optional) - the initial entries for the `LiveMap` to be created +- `(RTLMV3)` `LiveMap.create` static factory function: + - `(RTLMV3a)` Expects the following arguments: + - `(RTLMV3a1)` `entries` `Dict` (optional) - the initial entries for the new `LiveMap` object + - `(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 +- `(RTLMV4)` Internal consumption procedure - when a `LiveMapValueType` is consumed by a mutation method (e.g. `LiveMap#set` or as an entry value during another `LiveMapValueType` consumption), `ObjectMessages` are generated as follows: + - `(RTLMV4a)` If the internal `entries` is not undefined and (is null or is not of type `Dict`), the library should throw an `ErrorInfo` error with `statusCode` 400 and `code` 40003, indicating that entries must be a `Dict` + - `(RTLMV4b)` If any of the keys in the internal `entries` are not of type `String`, the library should throw an `ErrorInfo` error with `statusCode` 400 and `code` 40003, indicating that keys must be `String` + - `(RTLMV4c)` If any of the values in the internal `entries` are not of an expected type, the library should throw an `ErrorInfo` error with `statusCode` 400 and `code` 40013, indicating that such data type is unsupported + - `(RTLMV4d)` Build entries for the `MapCreate` object. For each key-value pair in the internal `entries` (if present), create an `ObjectsMapEntry` for the value: + - `(RTLMV4d1)` If the value is of type `LiveCounterValueType`, consume it per [RTLCV4](#RTLCV4) to generate a `COUNTER_CREATE` `ObjectMessage`. Collect the generated `ObjectMessage` and set `ObjectsMapEntry.data.objectId` to the `objectId` from the `ObjectMessage` + - `(RTLMV4d2)` If the value is of type `LiveMapValueType`, recursively consume it per [RTLMV4](#RTLMV4) to generate `ObjectMessages`. Collect all generated `ObjectMessages` and set `ObjectsMapEntry.data.objectId` to the `objectId` from the outermost `MAP_CREATE` `ObjectMessage` + - `(RTLMV4d3)` If the value is of type `JsonArray` or `JsonObject`, set `ObjectsMapEntry.data.json` to that value + - `(RTLMV4d4)` If the value is of type `String`, set `ObjectsMapEntry.data.string` to that value + - `(RTLMV4d5)` If the value is of type `Number`, set `ObjectsMapEntry.data.number` to that value + - `(RTLMV4d6)` If the value is of type `Boolean`, set `ObjectsMapEntry.data.boolean` to that value + - `(RTLMV4d7)` If the value is of type `Binary`, set `ObjectsMapEntry.data.bytes` to that value + - `(RTLMV4e)` Create a `MapCreate` object: + - `(RTLMV4e1)` Set `MapCreate.semantics` to `ObjectsMapSemantics.LWW` + - `(RTLMV4e2)` Set `MapCreate.entries` to an empty map if the internal `entries` is undefined, otherwise to the entries built in [RTLMV4d](#RTLMV4d) + - `(RTLMV4f)` Create an initial value JSON string based on the `MapCreate` object: + - `(RTLMV4f1)` The `MapCreate` object may contain user-provided `ObjectData` that requires encoding. Encode the `ObjectData` values using the procedure described in [OD4](../features#OD4) + - `(RTLMV4f2)` Return a JSON string representation of the encoded `MapCreate` object + - `(RTLMV4g)` Create a unique string nonce with 16+ characters + - `(RTLMV4h)` Get the current server time as described in [RTO16](#RTO16) + - `(RTLMV4i)` Create an `objectId` for the new `LiveMap` as described in [RTO14](#RTO14), passing in `map` as the `type`, the initial value JSON string from [RTLMV4f](#RTLMV4f), the nonce from [RTLMV4g](#RTLMV4g), and the server time from [RTLMV4h](#RTLMV4h) + - `(RTLMV4j)` Create an `ObjectMessage` with: + - `(RTLMV4j1)` `ObjectMessage.operation.action` set to `ObjectOperationAction.MAP_CREATE` + - `(RTLMV4j2)` `ObjectMessage.operation.objectId` set to the `objectId` from [RTLMV4i](#RTLMV4i) + - `(RTLMV4j3)` `ObjectMessage.operation.mapCreateWithObjectId.nonce` set to the nonce from [RTLMV4g](#RTLMV4g) + - `(RTLMV4j4)` `ObjectMessage.operation.mapCreateWithObjectId.initialValue` set to the JSON string from [RTLMV4f](#RTLMV4f) + - `(RTLMV4j5)` The client library must retain the `MapCreate` object from [RTLMV4e](#RTLMV4e) alongside the `MapCreateWithObjectId`. It is the operation from which the `MapCreateWithObjectId` was derived, and is needed for message size calculation ([OOP4h2](../features#OOP4h2)) and local application of the operation ([RTLM23](#RTLM23)). This `MapCreate` is for local use only and must not be sent over the wire. + - `(RTLMV4k)` Return an ordered array containing all `ObjectMessages` collected from nested value type consumptions in [RTLMV4d](#RTLMV4d) (in depth-first order), followed by the `MAP_CREATE` `ObjectMessage` from [RTLMV4j](#RTLMV4j) + ### PathObject A `PathObject` is a lazy, path-based reference into the LiveObjects tree. It stores a path (as an ordered list of string segments) from the root `LiveMap` and resolves it at the time each method is called. This means a `PathObject` survives object replacements: if the object at a given path changes (e.g. via a `MAP_SET` operation), the same `PathObject` will resolve to the new object on subsequent calls. @@ -879,8 +942,6 @@ Types and their properties/methods are public and exposed to users by default. A class RealtimeObject: // RTO* get() => io PathObject // RTO23 - createMap(Dict entries?) => io LiveMap // RTO11 - createCounter(Number count?) => io LiveCounter // RTO12 on(ObjectsEvent event, (() ->) callback) -> StatusSubscription // RTO18 off(() ->) // RTO19 publish(ObjectMessage[]) => io PublishResult // RTO15, internal @@ -902,45 +963,53 @@ Types and their properties/methods are public and exposed to users by default. A interface StatusSubscription: // RTO18f off() // RTO18f1 - class LiveObject: // RTLO* - objectId: String // RTLO3a, internal - siteTimeserials: Dict // RTLO3b, internal - createOperationIsMerged: Boolean // RTLO3c, internal - isTombstone: Boolean // RTLO3d, internal - tombstonedAt: Time? // RTLO3e, internal - canApplyOperation(ObjectMessage) -> Boolean // RTLO4a, internal - tombstone(ObjectMessage) // RTLO4e, internal + class LiveObject: // RTLO*, internal + objectId: String // RTLO3a + siteTimeserials: Dict // RTLO3b + createOperationIsMerged: Boolean // RTLO3c + isTombstone: Boolean // RTLO3d + tombstonedAt: Time? // RTLO3e + canApplyOperation(ObjectMessage) -> Boolean // RTLO4a + tombstone(ObjectMessage) // RTLO4e subscribe((LiveObjectUpdate) ->) -> LiveObjectSubscription // RTLO4b unsubscribe((LiveObjectUpdate) ->) // RTLO4c interface LiveObjectSubscription: // RTLO4b5 unsubscribe() // RTLO4b5a - interface LiveObjectUpdate: // RTLO4b4 + interface LiveObjectUpdate: // RTLO4b4, internal update: Object // RTLO4b4a - noop: Boolean // RTLO4b4b, internal + noop: Boolean // RTLO4b4b class LiveCounter extends LiveObject: // RTLC*, RTLC1 - value() -> Number // RTLC5 - increment(Number amount) => io // RTLC12 - decrement(Number amount) => io // RTLC13 + value() -> Number // RTLC5, internal + increment(Number amount) => io // RTLC12, internal + decrement(Number amount) => io // RTLC13, internal + static create(Number initialCount?) -> LiveCounterValueType // RTLCV3 - interface LiveCounterUpdate extends LiveObjectUpdate: // RTLC11, RTLC11a + interface LiveCounterUpdate extends LiveObjectUpdate: // RTLC11, RTLC11a, internal update: { amount: Number } // RTLC11b, RTLC11b1 - class LiveMap extends LiveObject: // RTLM*, RTLM1 + class LiveMap extends LiveObject: // RTLM*, RTLM1, internal clearTimeserial: String? // RTLM25, internal - get(key: String) -> (Boolean | Binary | Number | String | JsonArray | JsonObject | LiveCounter | LiveMap)? // RTLM5 - size() -> Number // RTLM10 - entries() -> [String, (Boolean | Binary | Number | String | JsonArray | JsonObject | LiveCounter | LiveMap)?][] // RTLM11 - keys() -> String[] // RTLM12 - values() -> (Boolean | Binary | Number | String | JsonArray | JsonObject | LiveCounter | LiveMap)?[] // RTLM13 - set(String key, (Boolean | Binary | Number | String | JsonArray | JsonObject | LiveCounter | LiveMap) value) => io // RTLM20 - remove(String key) => io // RTLM21 - - interface LiveMapUpdate extends LiveObjectUpdate: // RTLM18, RTLM18a + get(key: String) -> (Boolean | Binary | Number | String | JsonArray | JsonObject | LiveCounter | LiveMap)? // RTLM5, internal + size() -> Number // RTLM10, internal + entries() -> [String, (Boolean | Binary | Number | String | JsonArray | JsonObject | LiveCounter | LiveMap)?][] // RTLM11, internal + keys() -> String[] // RTLM12, internal + values() -> (Boolean | Binary | Number | String | JsonArray | JsonObject | LiveCounter | LiveMap)?[] // RTLM13, internal + set(String key, (Boolean | Binary | Number | String | JsonArray | JsonObject | LiveCounterValueType | LiveMapValueType) value) => io // RTLM20, internal + remove(String key) => io // RTLM21, internal + static create(Dict entries?) -> LiveMapValueType // RTLMV3 + + interface LiveMapUpdate extends LiveObjectUpdate: // RTLM18, RTLM18a, internal update: Dict // RTLM18b + class LiveCounterValueType: // RTLCV* + // created via LiveCounter.create(), RTLCV3 + + class LiveMapValueType: // RTLMV* + // created via LiveMap.create(), RTLMV3 + class PathObject: // RTPO* path() -> String // RTPO4 get(String key) -> PathObject // RTPO5 @@ -953,7 +1022,7 @@ Types and their properties/methods are public and exposed to users by default. A size() -> Number? // RTPO12 compact() -> Object? // RTPO13 compactJson() -> Object? // RTPO14 - set(String key, (Boolean | Binary | Number | String | JsonArray | JsonObject | LiveCounter | LiveMap) value) => io // RTPO15 + set(String key, (Boolean | Binary | Number | String | JsonArray | JsonObject | LiveCounterValueType | LiveMapValueType) value) => io // RTPO15 remove(String key) => io // RTPO16 increment(Number amount?) => io // RTPO17 decrement(Number amount?) => io // RTPO18 @@ -968,7 +1037,7 @@ Types and their properties/methods are public and exposed to users by default. A size() -> Number? // RTINS9 compact() -> Object? // RTINS10 compactJson() -> Object? // RTINS11 - set(String key, (Boolean | Binary | Number | String | JsonArray | JsonObject | LiveCounter | LiveMap) value) => io // RTINS12 + set(String key, (Boolean | Binary | Number | String | JsonArray | JsonObject | LiveCounterValueType | LiveMapValueType) value) => io // RTINS12 remove(String key) => io // RTINS13 increment(Number amount?) => io // RTINS14 decrement(Number amount?) => io // RTINS15 From babc296457c8b575e76676e748a171375eccdab3 Mon Sep 17 00:00:00 2001 From: Andrew Bulat Date: Tue, 24 Feb 2026 13:04:55 +0000 Subject: [PATCH 03/40] Subscriptions for PathObject and Instance --- specifications/features.md | 9 ++++ specifications/objects-features.md | 82 +++++++++++++++++++++++++++--- 2 files changed, 83 insertions(+), 8 deletions(-) diff --git a/specifications/features.md b/specifications/features.md index 11323233..9752832b 100644 --- a/specifications/features.md +++ b/specifications/features.md @@ -1870,6 +1870,12 @@ The core SDK provides an API for wrapper SDKs to supply Ably with analytics info - `(REX2b1)` Should be written in reverse domain name notation - `(REX2b2)` Types beginning with `com.ably.` are reserved +#### Subscription + +- `(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` called, the listener must not be called for any subsequent events + ### Option types {#options} #### ClientOptions @@ -2924,6 +2930,9 @@ Each type, method, and attribute is labelled with the name of one or more clause description: string? // TM2s4 metadata: Dict? //TM2s5 + interface Subscription: // SUB* + unsubscribe() // SUB2a + ## Old specs Use the version navigation to view older versions. References to diffs for each version are maintained below: diff --git a/specifications/objects-features.md b/specifications/objects-features.md index 22a6d825..ed77529e 100644 --- a/specifications/objects-features.md +++ b/specifications/objects-features.md @@ -288,6 +288,14 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTO22)` `ObjectsOperationSource` is an internal enum describing the source of an operation being applied: - `(RTO22a)` `LOCAL` - an operation that originated locally, being applied upon receipt of the `ACK` from Realtime - `(RTO22b)` `CHANNEL` - an operation received over a Realtime channel +- `(RTO24)` Internal `PathObjectSubscriptionRegister` - manages path-based subscriptions for `PathObject#subscribe` ([RTPO19](#RTPO19)) + - `(RTO24a)` The `RealtimeObject` instance maintains a single `PathObjectSubscriptionRegister` that manages all path-based subscriptions for the channel + - `(RTO24b)` When a `LiveObject` in the `ObjectsPool` emits a `LiveObjectUpdate` (per [RTLO4b4](#RTLO4b4)), the `PathObjectSubscriptionRegister` must determine which subscriptions should be notified: + - `(RTO24b1)` Determine the paths in the LiveObjects tree at which the updated `LiveObject` is located + - `(RTO24b2)` For each registered subscription, check whether the event path starts with (or equals) the subscription's path + - `(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 ### LiveObject @@ -315,9 +323,10 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLO4b4c)` When a `LiveObjectUpdate` is emitted: - `(RTLO4b4c1)` If `LiveObjectUpdate` is indicated to be a no-op, do nothing - `(RTLO4b4c2)` Otherwise, the registered listener is called with the `LiveObjectUpdate` object - - `(RTLO4b5)` The client library may return a subscription object (or the idiomatic equivalent for the language) as a result of this operation: - - `(RTLO4b5a)` The subscription object includes an `unsubscribe` function - - `(RTLO4b5b)` Calling `unsubscribe` deregisters the listener previously registered by the user via the corresponding `subscribe` call + - `(RTLO4b5)` This clause has been replaced by [RTLO4b7](#RTLO4b7) + - `(RTLO4b5a)` This clause has been replaced by [RTLO4b7](#RTLO4b7) + - `(RTLO4b5b)` This clause has been replaced by [RTLO4b7](#RTLO4b7) + - `(RTLO4b7)` Returns a [`Subscription`](../features#SUB1) object - `(RTLO4b6)` This operation must not have any side effects on `RealtimeObject`, the underlying channel, or their status - `(RTLO4c)` public `unsubscribe` - unsubscribes a previously registered listener - `(RTLO4c1)` This operation does not require any specific channel modes to be granted, nor does it require the channel to be in a specific state @@ -878,6 +887,31 @@ A `PathObject` is obtained from `RealtimeObject#get` ([RTO23](#RTO23)), which re - `(RTPO18b)` Resolves the path using the path resolution procedure ([RTPO3](#RTPO3)). On failure, throws per [RTPO3c2](#RTPO3c2) - `(RTPO18c)` If the resolved value is a `LiveCounter`, delegates to `LiveCounter#decrement` ([RTLC13](#RTLC13)) with the provided `amount` - `(RTPO18d)` If the resolved value is not a `LiveCounter`, the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 92007 +- `(RTPO19)` `PathObject#subscribe` function: + - `(RTPO19a)` Expects the following arguments: + - `(RTPO19a1)` `listener` - a callback function that receives a `PathObjectSubscriptionEvent` ([RTPO19d](#RTPO19d)) when a change occurs at or below this path + - `(RTPO19a2)` `options` `PathObjectSubscriptionOptions` (optional) - subscription options + - `(RTPO19b)` `PathObjectSubscriptionOptions` has the following properties: + - `(RTPO19b1)` `depth` `Number` (optional) - controls how many levels deep in the subtree changes trigger the listener: + - `(RTPO19b1a)` If undefined (default), the subscription receives events for changes at any depth below the subscribed path + - `(RTPO19b1b)` If `depth` is 1, only changes to the object at the exact subscribed path trigger the listener + - `(RTPO19b1c)` If `depth` is `n`, changes up to `n - 1` levels of children below the subscribed path trigger the listener + - `(RTPO19b1d)` If `depth` is provided and is not a positive integer, the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 40003 + - `(RTPO19c)` Returns a [`Subscription`](../features#SUB1) object + - `(RTPO19d)` The listener receives a `PathObjectSubscriptionEvent` object with: + - `(RTPO19d1)` `object` - a `PathObject` pointing to the path where the change occurred + - `(RTPO19d2)` `message` `ObjectMessage` (optional) - the `ObjectMessage` that caused the change + - `(RTPO19e)` The subscription is path-based: it follows the path, not a specific object. If the object at the path changes identity (e.g. via a `MAP_SET` operation replacing it), the subscription continues to deliver events for the new object at that path + - `(RTPO19f)` Events at child paths bubble up to the subscription, subject to depth filtering. For example, a subscription at path `a.b` receives events for changes at `a.b`, `a.b.c`, `a.b.c.d`, etc., depending on the configured depth. The dispatch rules are described in [RTO24b](#RTO24b) + - `(RTPO19g)` This operation must not have any side effects on `RealtimeObject`, the underlying channel, or their status +- `(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 +- `(RTPO21)` The client library should provide a method that allows consuming subscription events as a stream or iterable, rather than via a callback. A suggested name for this method is `subscribeIterator`: + - `(RTPO21a)` Expects the following arguments: + - `(RTPO21a1)` `options` `PathObjectSubscriptionOptions` (optional) - same options as `PathObject#subscribe` ([RTPO19b](#RTPO19b)) + - `(RTPO21b)` Returns a stream or iterable that yields `PathObjectSubscriptionEvent` objects, using the idiomatic construct for the language (e.g. async iterators, channels, flows, or async sequences) + - `(RTPO21c)` Internally wraps `PathObject#subscribe` ([RTPO19](#RTPO19)), converting the callback-based subscription into the appropriate streaming or iterable pattern ### Instance @@ -934,6 +968,24 @@ An `Instance` holds a direct reference to a specific resolved `LiveObject` or pr - `(RTINS15a1)` `amount` `Number` (optional) - the amount by which to decrement the counter value. Defaults to 1 - `(RTINS15b)` If the wrapped value is a `LiveCounter`, delegates to `LiveCounter#decrement` ([RTLC13](#RTLC13)) with the provided `amount` - `(RTINS15c)` If the wrapped value is not a `LiveCounter`, the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 92007 +- `(RTINS16)` `Instance#subscribe` function: + - `(RTINS16a)` Expects the following arguments: + - `(RTINS16a1)` `listener` - a callback function that receives an `InstanceSubscriptionEvent` ([RTINS16d](#RTINS16d)) when the wrapped object is updated + - `(RTINS16b)` If the wrapped value is not a `LiveObject` (i.e. it is a primitive), the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 92007, indicating that subscribe is not supported for primitive values + - `(RTINS16c)` Subscribes to data updates on the underlying `LiveObject` using `LiveObject#subscribe` ([RTLO4b](#RTLO4b)) + - `(RTINS16d)` The listener receives an `InstanceSubscriptionEvent` object with: + - `(RTINS16d1)` `object` - the `Instance` representing the updated object + - `(RTINS16d2)` `message` `ObjectMessage` (optional) - the `ObjectMessage` that caused the change + - `(RTINS16e)` Returns a [`Subscription`](../features#SUB1) object + - `(RTINS16f)` The subscription is identity-based: it follows the specific `LiveObject` instance, regardless of where it sits in the tree + - `(RTINS16g)` This operation must not have any side effects on `RealtimeObject`, the underlying channel, or their status +- `(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 +- `(RTINS18)` The client library should provide a method that allows consuming subscription events as a stream or iterable, rather than via a callback. A suggested name for this method is `subscribeIterator`: + - `(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 ## Interface Definition {#idl} @@ -971,13 +1023,10 @@ Types and their properties/methods are public and exposed to users by default. A tombstonedAt: Time? // RTLO3e canApplyOperation(ObjectMessage) -> Boolean // RTLO4a tombstone(ObjectMessage) // RTLO4e - subscribe((LiveObjectUpdate) ->) -> LiveObjectSubscription // RTLO4b + subscribe((LiveObjectUpdate) ->) -> Subscription // RTLO4b unsubscribe((LiveObjectUpdate) ->) // RTLO4c - interface LiveObjectSubscription: // RTLO4b5 - unsubscribe() // RTLO4b5a - - interface LiveObjectUpdate: // RTLO4b4, internal + interface LiveObjectUpdate: // RTLO4b4 update: Object // RTLO4b4a noop: Boolean // RTLO4b4b @@ -1010,6 +1059,17 @@ Types and their properties/methods are public and exposed to users by default. A class LiveMapValueType: // RTLMV* // created via LiveMap.create(), RTLMV3 + interface PathObjectSubscriptionEvent: // RTPO19d + object: PathObject // RTPO19d1 + message: ObjectMessage? // RTPO19d2 + + interface PathObjectSubscriptionOptions: // RTPO19b + depth: Number? // RTPO19b1 + + interface InstanceSubscriptionEvent: // RTINS16d + object: Instance // RTINS16d1 + message: ObjectMessage? // RTINS16d2 + class PathObject: // RTPO* path() -> String // RTPO4 get(String key) -> PathObject // RTPO5 @@ -1026,6 +1086,9 @@ Types and their properties/methods are public and exposed to users by default. A remove(String key) => io // RTPO16 increment(Number amount?) => io // RTPO17 decrement(Number amount?) => io // RTPO18 + subscribe((PathObjectSubscriptionEvent) -> listener, PathObjectSubscriptionOptions? options) -> Subscription // RTPO19 + unsubscribe((PathObjectSubscriptionEvent) -> listener) // RTPO20 + subscribeIterator(PathObjectSubscriptionOptions? options) -> Stream // RTPO21 class Instance: // RTINS* id: String? // RTINS3 @@ -1041,3 +1104,6 @@ Types and their properties/methods are public and exposed to users by default. A remove(String key) => io // RTINS13 increment(Number amount?) => io // RTINS14 decrement(Number amount?) => io // RTINS15 + subscribe((InstanceSubscriptionEvent) -> listener) -> Subscription // RTINS16 + unsubscribe((InstanceSubscriptionEvent) -> listener) // RTINS17 + subscribeIterator() -> Stream // RTINS18 From 06e15f1be09adafb50e28e0c17ac60136fa6c15b Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Wed, 20 May 2026 15:14:46 -0300 Subject: [PATCH 04/40] Mark `LiveObjectUpdate` as internal in IDL The `LiveCounterUpdate` and `LiveMapUpdate` subtypes already have the `internal` marker; the parent `LiveObjectUpdate` interface was missing it. Add it for consistency, reflecting that `LiveObject#subscribe` and its emitted update objects are not part of the public API (confirmed against ably-js, where these types live only in the plugin internals and are not exported from `liveobjects.d.ts`). Addresses [1]. [1] https://github.com/ably/specification/pull/427#discussion_r3259453973 Co-Authored-By: Claude Opus 4.7 (1M context) --- specifications/objects-features.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifications/objects-features.md b/specifications/objects-features.md index ed77529e..7fd0e5d7 100644 --- a/specifications/objects-features.md +++ b/specifications/objects-features.md @@ -1026,7 +1026,7 @@ Types and their properties/methods are public and exposed to users by default. A subscribe((LiveObjectUpdate) ->) -> Subscription // RTLO4b unsubscribe((LiveObjectUpdate) ->) // RTLO4c - interface LiveObjectUpdate: // RTLO4b4 + interface LiveObjectUpdate: // RTLO4b4, internal update: Object // RTLO4b4a noop: Boolean // RTLO4b4b From dcdb0b9d40ae78d517285e2c1a46b5f16753dd8b Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Wed, 20 May 2026 15:19:14 -0300 Subject: [PATCH 05/40] Use arrays, not iterators, for PathObject and Instance getters The `entries`, `keys`, and `values` methods on `PathObject` and `Instance` were previously described as returning iterators (`Iterator<...>` in the IDL, "iterator yielding..." in the prose). "Iterator" is not a term we use elsewhere in the spec, and the array form is easier to reason about and consistent with how `LiveMap#entries` ([RTLM11](#RTLM11)) etc. are already specified. SDKs remain free to use a platform-idiomatic equivalent in their public surface (e.g. a JS iterator). Addresses [1]. [1] https://github.com/ably/specification/pull/427#discussion_r3261238756 Co-Authored-By: Claude Opus 4.7 (1M context) --- specifications/objects-features.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/specifications/objects-features.md b/specifications/objects-features.md index 7fd0e5d7..53059c6f 100644 --- a/specifications/objects-features.md +++ b/specifications/objects-features.md @@ -836,13 +836,13 @@ A `PathObject` is obtained from `RealtimeObject#get` ([RTO23](#RTO23)), which re - `(RTPO8d)` If path resolution fails, returns undefined/null per [RTPO3c1](#RTPO3c1) - `(RTPO9)` `PathObject#entries` function: - `(RTPO9a)` Resolves the path using the path resolution procedure ([RTPO3](#RTPO3)) - - `(RTPO9b)` If the resolved value is a `LiveMap`, returns an iterator yielding `[key, PathObject]` pairs, where each `PathObject` is created as if by calling `PathObject#get` with the corresponding key on this `PathObject` + - `(RTPO9b)` If the resolved value is a `LiveMap`, returns an array of `[key, PathObject]` pairs, where each `PathObject` is created as if by calling `PathObject#get` with the corresponding key on this `PathObject` - `(RTPO9c)` Only non-tombstoned entries are included, following the same rules as `LiveMap#entries` ([RTLM11](#RTLM11)) - - `(RTPO9d)` If the resolved value is not a `LiveMap`, or if path resolution fails, returns an empty iterator + - `(RTPO9d)` If the resolved value is not a `LiveMap`, or if path resolution fails, returns an empty array - `(RTPO10)` `PathObject#keys` function: - - `(RTPO10a)` Behaves identically to `PathObject#entries` ([RTPO9](#RTPO9)) except that it yields only the keys + - `(RTPO10a)` Behaves identically to `PathObject#entries` ([RTPO9](#RTPO9)) except that the array contains only the keys - `(RTPO11)` `PathObject#values` function: - - `(RTPO11a)` Behaves identically to `PathObject#entries` ([RTPO9](#RTPO9)) except that it yields only the `PathObject` values + - `(RTPO11a)` Behaves identically to `PathObject#entries` ([RTPO9](#RTPO9)) except that the array contains only the `PathObject` values - `(RTPO12)` `PathObject#size` function: - `(RTPO12a)` Resolves the path using the path resolution procedure ([RTPO3](#RTPO3)) - `(RTPO12b)` If the resolved value is a `LiveMap`, returns the number of non-tombstoned entries (equivalent to `LiveMap#size`, see [RTLM10](#RTLM10)) @@ -934,12 +934,12 @@ An `Instance` holds a direct reference to a specific resolved `LiveObject` or pr - `(RTINS5b)` If the wrapped value is a `LiveMap`, looks up the value at `key` using `LiveMap#get` ([RTLM5](#RTLM5)) and returns a new `Instance` wrapping the result. If the result is undefined/null, returns undefined/null - `(RTINS5c)` If the wrapped value is not a `LiveMap`, returns undefined/null - `(RTINS6)` `Instance#entries` function: - - `(RTINS6a)` If the wrapped value is a `LiveMap`, returns an iterator yielding `[key, Instance]` pairs, where each `Instance` wraps the corresponding entry value from `LiveMap#entries` ([RTLM11](#RTLM11)) - - `(RTINS6b)` If the wrapped value is not a `LiveMap`, returns an empty iterator + - `(RTINS6a)` If the wrapped value is a `LiveMap`, returns an array of `[key, Instance]` pairs, where each `Instance` wraps the corresponding entry value from `LiveMap#entries` ([RTLM11](#RTLM11)) + - `(RTINS6b)` If the wrapped value is not a `LiveMap`, returns an empty array - `(RTINS7)` `Instance#keys` function: - - `(RTINS7a)` Behaves identically to `Instance#entries` ([RTINS6](#RTINS6)) except that it yields only the keys + - `(RTINS7a)` Behaves identically to `Instance#entries` ([RTINS6](#RTINS6)) except that the array contains only the keys - `(RTINS8)` `Instance#values` function: - - `(RTINS8a)` Behaves identically to `Instance#entries` ([RTINS6](#RTINS6)) except that it yields only the `Instance` values + - `(RTINS8a)` Behaves identically to `Instance#entries` ([RTINS6](#RTINS6)) except that the array contains only the `Instance` values - `(RTINS9)` `Instance#size` function: - `(RTINS9a)` If the wrapped value is a `LiveMap`, returns the number of non-tombstoned entries (equivalent to `LiveMap#size`, see [RTLM10](#RTLM10)) - `(RTINS9b)` If the wrapped value is not a `LiveMap`, returns undefined/null @@ -1076,9 +1076,9 @@ Types and their properties/methods are public and exposed to users by default. A at(String path) -> PathObject // RTPO6 value() -> (Boolean | Binary | Number | String | JsonArray | JsonObject)? // RTPO7 instance() -> Instance? // RTPO8 - entries() -> Iterator<[String, PathObject]> // RTPO9 - keys() -> Iterator // RTPO10 - values() -> Iterator // RTPO11 + entries() -> [String, PathObject][] // RTPO9 + keys() -> String[] // RTPO10 + values() -> PathObject[] // RTPO11 size() -> Number? // RTPO12 compact() -> Object? // RTPO13 compactJson() -> Object? // RTPO14 @@ -1094,9 +1094,9 @@ Types and their properties/methods are public and exposed to users by default. A id: String? // RTINS3 value() -> (Boolean | Binary | Number | String | JsonArray | JsonObject)? // RTINS4 get(String key) -> Instance? // RTINS5 - entries() -> Iterator<[String, Instance]> // RTINS6 - keys() -> Iterator // RTINS7 - values() -> Iterator // RTINS8 + entries() -> [String, Instance][] // RTINS6 + keys() -> String[] // RTINS7 + values() -> Instance[] // RTINS8 size() -> Number? // RTINS9 compact() -> Object? // RTINS10 compactJson() -> Object? // RTINS11 From a5736bc4f603053ccdadd1977e6f2bdf5482571e Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Wed, 20 May 2026 15:41:03 -0300 Subject: [PATCH 06/40] Use explicit delegation for PathObject and Instance methods Several PathObject and Instance methods previously described their behaviour by restating the underlying LiveMap or LiveCounter semantics ("returns the number of non-tombstoned entries, equivalent to LiveMap#size") or by chaining through a sibling method ("behaves identically to PathObject#entries except the array contains only the keys"). Both forms duplicate or obscure the fact that the SDK is just delegating to the LiveMap/LiveCounter spec point. Rewrite each as an explicit delegation, so the LiveMap/LiveCounter spec point remains the single source of truth for the semantics (tombstone handling, ordering, etc.): - RTPO7b, RTINS4a: PathObject#value / Instance#value for LiveCounter delegate to LiveCounter#value (consistency fold-in, no specific comment). - RTPO9 (PathObject#entries): collapse the previous b/c/d into b (delegate to LiveMap#keys and build [key, PathObject] pairs) plus c (empty array on failure). LiveMap#keys rather than #entries is correct because the PathObject is lazy and does not need resolved values. - RTPO10, RTPO11 (PathObject#keys / #values): resolve and delegate directly to LiveMap#keys instead of chaining through PathObject#entries. RTPO11 still wraps each key in a PathObject. - RTPO12b, RTINS9a (#size): delegate to LiveMap#size. - RTINS6a (Instance#entries): rephrase to lead with "delegates to LiveMap#entries" and wrap each value in an Instance. - RTINS7, RTINS8 (Instance#keys / #values): delegate directly to LiveMap#keys / #values; RTINS8 wraps each value in an Instance. compact / compactJson (RTPO13/14, RTINS10/11) are not touched here; they will be addressed separately because they require a new LiveMap#compact spec point. Addresses [1], [2], [3] (and reply [4]), [5]. [1] https://github.com/ably/specification/pull/427#discussion_r3260769728 [2] https://github.com/ably/specification/pull/427#discussion_r3260776863 [3] https://github.com/ably/specification/pull/427#discussion_r3260486938 [4] https://github.com/ably/specification/pull/427#discussion_r3260743558 [5] https://github.com/ably/specification/pull/427#discussion_r3260480987 Co-Authored-By: Claude Opus 4.7 (1M context) --- specifications/objects-features.md | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/specifications/objects-features.md b/specifications/objects-features.md index 53059c6f..e2b149bc 100644 --- a/specifications/objects-features.md +++ b/specifications/objects-features.md @@ -825,7 +825,7 @@ A `PathObject` is obtained from `RealtimeObject#get` ([RTO23](#RTO23)), which re - `(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")` - `(RTPO7)` `PathObject#value` function: - `(RTPO7a)` Resolves the path using the path resolution procedure ([RTPO3](#RTPO3)) - - `(RTPO7b)` If the resolved value is a `LiveCounter`, returns its current numeric value (equivalent to `LiveCounter#value`, see [RTLC5](#RTLC5)) + - `(RTPO7b)` If the resolved value is a `LiveCounter`, delegates to `LiveCounter#value` ([RTLC5](#RTLC5)) - `(RTPO7c)` If the resolved value is a primitive (`Boolean`, `Binary`, `Number`, `String`, `JsonArray`, `JsonObject`), returns the value directly - `(RTPO7d)` If the resolved value is a `LiveMap`, returns undefined/null - `(RTPO7e)` If path resolution fails, returns undefined/null per [RTPO3c1](#RTPO3c1) @@ -836,16 +836,19 @@ A `PathObject` is obtained from `RealtimeObject#get` ([RTO23](#RTO23)), which re - `(RTPO8d)` If path resolution fails, returns undefined/null per [RTPO3c1](#RTPO3c1) - `(RTPO9)` `PathObject#entries` function: - `(RTPO9a)` Resolves the path using the path resolution procedure ([RTPO3](#RTPO3)) - - `(RTPO9b)` If the resolved value is a `LiveMap`, returns an array of `[key, PathObject]` pairs, where each `PathObject` is created as if by calling `PathObject#get` with the corresponding key on this `PathObject` - - `(RTPO9c)` Only non-tombstoned entries are included, following the same rules as `LiveMap#entries` ([RTLM11](#RTLM11)) - - `(RTPO9d)` If the resolved value is not a `LiveMap`, or if path resolution fails, returns an empty array + - `(RTPO9b)` If the resolved value is a `LiveMap`, delegates to `LiveMap#keys` ([RTLM12](#RTLM12)) and returns an array of `[key, PathObject]` pairs, where each `PathObject` is created as if by calling `PathObject#get` with the corresponding key on this `PathObject` + - `(RTPO9c)` If the resolved value is not a `LiveMap`, or if path resolution fails, returns an empty array - `(RTPO10)` `PathObject#keys` function: - - `(RTPO10a)` Behaves identically to `PathObject#entries` ([RTPO9](#RTPO9)) except that the array contains only the keys + - `(RTPO10a)` Resolves the path using the path resolution procedure ([RTPO3](#RTPO3)) + - `(RTPO10b)` If the resolved value is a `LiveMap`, delegates to `LiveMap#keys` ([RTLM12](#RTLM12)) + - `(RTPO10c)` If the resolved value is not a `LiveMap`, or if path resolution fails, returns an empty array - `(RTPO11)` `PathObject#values` function: - - `(RTPO11a)` Behaves identically to `PathObject#entries` ([RTPO9](#RTPO9)) except that the array contains only the `PathObject` values + - `(RTPO11a)` Resolves the path using the path resolution procedure ([RTPO3](#RTPO3)) + - `(RTPO11b)` If the resolved value is a `LiveMap`, delegates to `LiveMap#keys` ([RTLM12](#RTLM12)) and returns an array of `PathObject`s, where each `PathObject` is created as if by calling `PathObject#get` with the corresponding key on this `PathObject` + - `(RTPO11c)` If the resolved value is not a `LiveMap`, or if path resolution fails, returns an empty array - `(RTPO12)` `PathObject#size` function: - `(RTPO12a)` Resolves the path using the path resolution procedure ([RTPO3](#RTPO3)) - - `(RTPO12b)` If the resolved value is a `LiveMap`, returns the number of non-tombstoned entries (equivalent to `LiveMap#size`, see [RTLM10](#RTLM10)) + - `(RTPO12b)` If the resolved value is a `LiveMap`, delegates to `LiveMap#size` ([RTLM10](#RTLM10)) - `(RTPO12c)` If the resolved value is not a `LiveMap`, or if path resolution fails, returns undefined/null - `(RTPO13)` `PathObject#compact` function: - `(RTPO13a)` Resolves the path using the path resolution procedure ([RTPO3](#RTPO3)) @@ -925,7 +928,7 @@ An `Instance` holds a direct reference to a specific resolved `LiveObject` or pr - `(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 - `(RTINS4)` `Instance#value` function: - - `(RTINS4a)` If the wrapped value is a `LiveCounter`, returns its current numeric value (equivalent to `LiveCounter#value`, see [RTLC5](#RTLC5)) + - `(RTINS4a)` If the wrapped value is a `LiveCounter`, delegates to `LiveCounter#value` ([RTLC5](#RTLC5)) - `(RTINS4b)` If the wrapped value is a primitive (`Boolean`, `Binary`, `Number`, `String`, `JsonArray`, `JsonObject`), returns the value directly - `(RTINS4c)` If the wrapped value is a `LiveMap`, returns undefined/null - `(RTINS5)` `Instance#get` function: @@ -934,14 +937,16 @@ An `Instance` holds a direct reference to a specific resolved `LiveObject` or pr - `(RTINS5b)` If the wrapped value is a `LiveMap`, looks up the value at `key` using `LiveMap#get` ([RTLM5](#RTLM5)) and returns a new `Instance` wrapping the result. If the result is undefined/null, returns undefined/null - `(RTINS5c)` If the wrapped value is not a `LiveMap`, returns undefined/null - `(RTINS6)` `Instance#entries` function: - - `(RTINS6a)` If the wrapped value is a `LiveMap`, returns an array of `[key, Instance]` pairs, where each `Instance` wraps the corresponding entry value from `LiveMap#entries` ([RTLM11](#RTLM11)) + - `(RTINS6a)` If the wrapped value is a `LiveMap`, delegates to `LiveMap#entries` ([RTLM11](#RTLM11)) and returns an array of `[key, Instance]` pairs, where each `Instance` wraps the corresponding value - `(RTINS6b)` If the wrapped value is not a `LiveMap`, returns an empty array - `(RTINS7)` `Instance#keys` function: - - `(RTINS7a)` Behaves identically to `Instance#entries` ([RTINS6](#RTINS6)) except that the array contains only the keys + - `(RTINS7a)` If the wrapped value is a `LiveMap`, delegates to `LiveMap#keys` ([RTLM12](#RTLM12)) + - `(RTINS7b)` If the wrapped value is not a `LiveMap`, returns an empty array - `(RTINS8)` `Instance#values` function: - - `(RTINS8a)` Behaves identically to `Instance#entries` ([RTINS6](#RTINS6)) except that the array contains only the `Instance` values + - `(RTINS8a)` If the wrapped value is a `LiveMap`, delegates to `LiveMap#values` ([RTLM13](#RTLM13)) and returns an array of `Instance`s, where each `Instance` wraps the corresponding value + - `(RTINS8b)` If the wrapped value is not a `LiveMap`, returns an empty array - `(RTINS9)` `Instance#size` function: - - `(RTINS9a)` If the wrapped value is a `LiveMap`, returns the number of non-tombstoned entries (equivalent to `LiveMap#size`, see [RTLM10](#RTLM10)) + - `(RTINS9a)` If the wrapped value is a `LiveMap`, delegates to `LiveMap#size` ([RTLM10](#RTLM10)) - `(RTINS9b)` If the wrapped value is not a `LiveMap`, returns undefined/null - `(RTINS10)` `Instance#compact` function: - `(RTINS10a)` Behaves identically to `PathObject#compact` ([RTPO13](#RTPO13)), but operates on the wrapped value directly instead of resolving a path From 5f78d21edaf7ca341ffa37623174e80735c2a428 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Wed, 20 May 2026 16:22:17 -0300 Subject: [PATCH 07/40] Use "evaluate"/"evaluation" instead of "consume"/"consumption" for value types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "Consume" suggested a one-shot procedure, which is misleading for an immutable value type that could in principle be evaluated multiple times (e.g. passed to several mutation calls). "Evaluate" captures the deferred-validation semantics of the value type (per RTLCV3c / RTLMV3c, validation is deferred to this procedure, much like evaluating a lazy expression) without the one-shot implication. The unrelated "consuming subscription events as a stream" usages in RTPO21 and RTINS18 are left alone — that is the standard consumer-of-stream sense, which doesn't have the same problem. Addresses [1]. [1] https://github.com/ably/specification/pull/427#discussion_r3259001228 Co-Authored-By: Claude Opus 4.7 (1M context) --- specifications/objects-features.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/specifications/objects-features.md b/specifications/objects-features.md index e2b149bc..848df9cf 100644 --- a/specifications/objects-features.md +++ b/specifications/objects-features.md @@ -536,7 +536,7 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLM20e7)` Set `ObjectMessage.operation.mapSet.value` depending on the type of the provided `value`: - `(RTLM20e7a)` This clause has been replaced by [RTLM20e7g](#RTLM20e7g). - `(RTLM20e7g)` If the `value` is of type `LiveCounterValueType` or `LiveMapValueType`: - - `(RTLM20e7g1)` Consume the value type per [RTLCV4](#RTLCV4) or [RTLMV4](#RTLMV4) respectively to generate `*_CREATE` `ObjectMessages`. Collect all generated `ObjectMessages` + - `(RTLM20e7g1)` Evaluate the value type per [RTLCV4](#RTLCV4) or [RTLMV4](#RTLMV4) respectively to generate `*_CREATE` `ObjectMessages`. Collect all generated `ObjectMessages` - `(RTLM20e7g2)` Set `ObjectMessage.operation.mapSet.value.objectId` to the `objectId` from the outermost `*_CREATE` `ObjectMessage` - `(RTLM20e7b)` If the `value` is of type `JsonArray` or `JsonObject`, set `ObjectMessage.operation.mapSet.value.json` to that value - `(RTLM20e7c)` If the `value` is of type `String`, set `ObjectMessage.operation.mapSet.value.string` to that value @@ -718,7 +718,7 @@ Objects feature enables clients to store shared data as "objects" on a channel. ### LiveCounterValueType -A `LiveCounterValueType` is an immutable blueprint for creating a new `LiveCounter` object. It stores the desired initial count value and is consumed when passed to a mutation method such as `LiveMap#set` ([RTLM20](#RTLM20)) or as an entry value in `LiveMap.create` ([RTLMV3](#RTLMV3)). +A `LiveCounterValueType` is an immutable blueprint for creating a new `LiveCounter` object. It stores the desired initial count value and is evaluated when passed to a mutation method such as `LiveMap#set` ([RTLM20](#RTLM20)) or as an entry value in `LiveMap.create` ([RTLMV3](#RTLMV3)). - `(RTLCV1)` `LiveCounterValueType` is an immutable value type representing the intent to create a new `LiveCounter` with a specific initial count - `(RTLCV2)` `LiveCounterValueType` has the following internal properties: @@ -727,9 +727,9 @@ A `LiveCounterValueType` is an immutable blueprint for creating a new `LiveCount - `(RTLCV3a)` Expects the following arguments: - `(RTLCV3a1)` `initialCount` `Number` (optional) - the initial count for the new `LiveCounter` object. Defaults to 0 - `(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)) + - `(RTLCV3c)` No input validation is performed at creation time. Validation is deferred to the evaluation procedure ([RTLCV4](#RTLCV4)) - `(RTLCV3d)` The returned `LiveCounterValueType` is immutable and must not be modified after creation -- `(RTLCV4)` Internal consumption procedure - when a `LiveCounterValueType` is consumed by a mutation method (e.g. `LiveMap#set` or as an entry value during `LiveMapValueType` consumption per [RTLMV4](#RTLMV4)), a `COUNTER_CREATE` `ObjectMessage` is generated as follows: +- `(RTLCV4)` Internal evaluation procedure - when a `LiveCounterValueType` is evaluated by a mutation method (e.g. `LiveMap#set` or as an entry value during `LiveMapValueType` evaluation per [RTLMV4](#RTLMV4)), a `COUNTER_CREATE` `ObjectMessage` is generated as follows: - `(RTLCV4a)` If the internal `count` is not undefined and (is not of type `Number` or is not a finite number), the library should throw an `ErrorInfo` error with `statusCode` 400 and `code` 40003, indicating that the counter value must be a valid number - `(RTLCV4b)` Create a `CounterCreate` object: - `(RTLCV4b1)` Set `CounterCreate.count` to the internal `count` value, or to 0 if undefined @@ -747,7 +747,7 @@ A `LiveCounterValueType` is an immutable blueprint for creating a new `LiveCount ### LiveMapValueType -A `LiveMapValueType` is an immutable blueprint for creating a new `LiveMap` object. It stores the desired initial entries and is consumed when passed to a mutation method such as `LiveMap#set` ([RTLM20](#RTLM20)) or as an entry value in another `LiveMap.create` ([RTLMV3](#RTLMV3)) call. Supports arbitrarily deep nesting of `LiveMapValueType` and `LiveCounterValueType` values within entries. +A `LiveMapValueType` is an immutable blueprint for creating a new `LiveMap` object. It stores the desired initial entries and is evaluated when passed to a mutation method such as `LiveMap#set` ([RTLM20](#RTLM20)) or as an entry value in another `LiveMap.create` ([RTLMV3](#RTLMV3)) call. Supports arbitrarily deep nesting of `LiveMapValueType` and `LiveCounterValueType` values within entries. - `(RTLMV1)` `LiveMapValueType` is an immutable value type representing the intent to create a new `LiveMap` with specific initial entries - `(RTLMV2)` `LiveMapValueType` has the following internal properties: @@ -756,15 +756,15 @@ A `LiveMapValueType` is an immutable blueprint for creating a new `LiveMap` obje - `(RTLMV3a)` Expects the following arguments: - `(RTLMV3a1)` `entries` `Dict` (optional) - the initial entries for the new `LiveMap` object - `(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)) + - `(RTLMV3c)` No input validation is performed at creation time. Validation is deferred to the evaluation procedure ([RTLMV4](#RTLMV4)) - `(RTLMV3d)` The returned `LiveMapValueType` is immutable and must not be modified after creation -- `(RTLMV4)` Internal consumption procedure - when a `LiveMapValueType` is consumed by a mutation method (e.g. `LiveMap#set` or as an entry value during another `LiveMapValueType` consumption), `ObjectMessages` are generated as follows: +- `(RTLMV4)` Internal evaluation procedure - when a `LiveMapValueType` is evaluated by a mutation method (e.g. `LiveMap#set` or as an entry value during another `LiveMapValueType` evaluation), `ObjectMessages` are generated as follows: - `(RTLMV4a)` If the internal `entries` is not undefined and (is null or is not of type `Dict`), the library should throw an `ErrorInfo` error with `statusCode` 400 and `code` 40003, indicating that entries must be a `Dict` - `(RTLMV4b)` If any of the keys in the internal `entries` are not of type `String`, the library should throw an `ErrorInfo` error with `statusCode` 400 and `code` 40003, indicating that keys must be `String` - `(RTLMV4c)` If any of the values in the internal `entries` are not of an expected type, the library should throw an `ErrorInfo` error with `statusCode` 400 and `code` 40013, indicating that such data type is unsupported - `(RTLMV4d)` Build entries for the `MapCreate` object. For each key-value pair in the internal `entries` (if present), create an `ObjectsMapEntry` for the value: - - `(RTLMV4d1)` If the value is of type `LiveCounterValueType`, consume it per [RTLCV4](#RTLCV4) to generate a `COUNTER_CREATE` `ObjectMessage`. Collect the generated `ObjectMessage` and set `ObjectsMapEntry.data.objectId` to the `objectId` from the `ObjectMessage` - - `(RTLMV4d2)` If the value is of type `LiveMapValueType`, recursively consume it per [RTLMV4](#RTLMV4) to generate `ObjectMessages`. Collect all generated `ObjectMessages` and set `ObjectsMapEntry.data.objectId` to the `objectId` from the outermost `MAP_CREATE` `ObjectMessage` + - `(RTLMV4d1)` If the value is of type `LiveCounterValueType`, evaluate it per [RTLCV4](#RTLCV4) to generate a `COUNTER_CREATE` `ObjectMessage`. Collect the generated `ObjectMessage` and set `ObjectsMapEntry.data.objectId` to the `objectId` from the `ObjectMessage` + - `(RTLMV4d2)` If the value is of type `LiveMapValueType`, recursively evaluate it per [RTLMV4](#RTLMV4) to generate `ObjectMessages`. Collect all generated `ObjectMessages` and set `ObjectsMapEntry.data.objectId` to the `objectId` from the outermost `MAP_CREATE` `ObjectMessage` - `(RTLMV4d3)` If the value is of type `JsonArray` or `JsonObject`, set `ObjectsMapEntry.data.json` to that value - `(RTLMV4d4)` If the value is of type `String`, set `ObjectsMapEntry.data.string` to that value - `(RTLMV4d5)` If the value is of type `Number`, set `ObjectsMapEntry.data.number` to that value @@ -785,7 +785,7 @@ A `LiveMapValueType` is an immutable blueprint for creating a new `LiveMap` obje - `(RTLMV4j3)` `ObjectMessage.operation.mapCreateWithObjectId.nonce` set to the nonce from [RTLMV4g](#RTLMV4g) - `(RTLMV4j4)` `ObjectMessage.operation.mapCreateWithObjectId.initialValue` set to the JSON string from [RTLMV4f](#RTLMV4f) - `(RTLMV4j5)` The client library must retain the `MapCreate` object from [RTLMV4e](#RTLMV4e) alongside the `MapCreateWithObjectId`. It is the operation from which the `MapCreateWithObjectId` was derived, and is needed for message size calculation ([OOP4h2](../features#OOP4h2)) and local application of the operation ([RTLM23](#RTLM23)). This `MapCreate` is for local use only and must not be sent over the wire. - - `(RTLMV4k)` Return an ordered array containing all `ObjectMessages` collected from nested value type consumptions in [RTLMV4d](#RTLMV4d) (in depth-first order), followed by the `MAP_CREATE` `ObjectMessage` from [RTLMV4j](#RTLMV4j) + - `(RTLMV4k)` Return an ordered array containing all `ObjectMessages` collected from nested value type evaluations in [RTLMV4d](#RTLMV4d) (in depth-first order), followed by the `MAP_CREATE` `ObjectMessage` from [RTLMV4j](#RTLMV4j) ### PathObject From c33e05ac5d2b2f0192421a6dd636237f96e26cba Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Wed, 20 May 2026 16:48:09 -0300 Subject: [PATCH 08/40] Fix missing "is" in SUB2a "Once `unsubscribe` called" was missing the verb. Reads as "Once `unsubscribe` is called" now. Addresses [1]. [1] https://github.com/ably/specification/pull/427#discussion_r3234006822 Co-Authored-By: Claude Opus 4.7 (1M context) --- specifications/features.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifications/features.md b/specifications/features.md index 9752832b..9ccb41f0 100644 --- a/specifications/features.md +++ b/specifications/features.md @@ -1874,7 +1874,7 @@ The core SDK provides an API for wrapper SDKs to supply Ably with analytics info - `(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` called, the listener must not be called for any subsequent events + - `(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 ### Option types {#options} From 78a7a77a7a858ff02258183d1a60fea1f2277a1b Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Wed, 20 May 2026 17:07:56 -0300 Subject: [PATCH 09/40] Clarify InstanceSubscriptionEvent.object wording RTINS16d1 previously read "the `Instance` representing the updated object", which was vague. Reword to "an `Instance` wrapping the underlying `LiveObject`", reusing the "underlying `LiveObject`" terminology already used in RTINS16c. The new wording deliberately uses "an `Instance`" rather than "the `Instance` on which `#subscribe` was called", so that the spec does not mandate strict reference identity between the subscribed `Instance` and the one delivered in the event. ably-js currently reuses the subscriber's own `Instance` (`instance.ts:218-223` passes `object: this`), which has the side effect of pinning that `Instance` for the lifetime of the subscription. Implementations are free to make that trade-off either way: pin for identity, or allocate a fresh `Instance` per event so the subscriber's reference can be collected independently. Addresses [1] and its follow-up [2]. [1] https://github.com/ably/specification/pull/427#discussion_r3260535507 [2] https://github.com/ably/specification/pull/427#discussion_r3261075871 Co-Authored-By: Claude Opus 4.7 (1M context) --- specifications/objects-features.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifications/objects-features.md b/specifications/objects-features.md index 848df9cf..d46dc234 100644 --- a/specifications/objects-features.md +++ b/specifications/objects-features.md @@ -979,7 +979,7 @@ An `Instance` holds a direct reference to a specific resolved `LiveObject` or pr - `(RTINS16b)` If the wrapped value is not a `LiveObject` (i.e. it is a primitive), the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 92007, indicating that subscribe is not supported for primitive values - `(RTINS16c)` Subscribes to data updates on the underlying `LiveObject` using `LiveObject#subscribe` ([RTLO4b](#RTLO4b)) - `(RTINS16d)` The listener receives an `InstanceSubscriptionEvent` object with: - - `(RTINS16d1)` `object` - the `Instance` representing the updated object + - `(RTINS16d1)` `object` - an `Instance` wrapping the underlying `LiveObject` - `(RTINS16d2)` `message` `ObjectMessage` (optional) - the `ObjectMessage` that caused the change - `(RTINS16e)` Returns a [`Subscription`](../features#SUB1) object - `(RTINS16f)` The subscription is identity-based: it follows the specific `LiveObject` instance, regardless of where it sits in the tree From ecf83de94ca66fec91c5b93c07cba8cd18a7a381 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Thu, 21 May 2026 08:44:09 -0300 Subject: [PATCH 10/40] Make LiveMap/LiveCounter/LiveObject fully internal in IDL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, `LiveCounter` was not marked `internal` in the IDL even though it extends the internal `LiveObject` — flagged by Copilot as non-implementable in some languages (e.g. C# requires a base type to be at least as accessible as the derived public type). Mark `LiveCounter` `internal` to match `LiveObject` and `LiveMap`. Drop the redundant per-member `, internal` markers from `LiveCounter` and `LiveMap` IDL bodies now that the enclosing class carries the marker (matching `LiveObject`'s existing convention). Move `static create(...)` off `LiveCounter` and `LiveMap` and onto `LiveCounterValueType` and `LiveMapValueType`. The public-facing surface is now `PathObject`, `Instance`, and the two value types; `LiveMap`/`LiveCounter`/`LiveObject` are purely internal implementation classes. This honestly reflects what ably-js does — its public `LiveMap`/`LiveCounter` (in `liveobjects.d.ts`) are branded interfaces that happen to share their name with the internal implementation classes, a JS-specific trick the spec doesn't need to replicate. Prose updated to reference `LiveCounterValueType.create` / `LiveMapValueType.create` (previously `LiveCounter.create` / `LiveMap.create`). Spec IDs (RTLCV3, RTLMV3) unchanged. The `ValueType` naming itself is not addressed here; we want to merge this PR rather than get lost in bikeshedding names, and will revisit before stabilising Swift/Kotlin. Addresses [1], [2], [3]. [1] https://github.com/ably/specification/pull/427#discussion_r3234006786 [2] https://github.com/ably/specification/pull/427#discussion_r3258929102 [3] https://github.com/ably/specification/pull/427#discussion_r3261405076 Co-Authored-By: Claude Opus 4.7 (1M context) --- specifications/objects-features.md | 38 ++++++++++++++---------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/specifications/objects-features.md b/specifications/objects-features.md index d46dc234..1dad924d 100644 --- a/specifications/objects-features.md +++ b/specifications/objects-features.md @@ -718,12 +718,12 @@ Objects feature enables clients to store shared data as "objects" on a channel. ### LiveCounterValueType -A `LiveCounterValueType` is an immutable blueprint for creating a new `LiveCounter` object. It stores the desired initial count value and is evaluated when passed to a mutation method such as `LiveMap#set` ([RTLM20](#RTLM20)) or as an entry value in `LiveMap.create` ([RTLMV3](#RTLMV3)). +A `LiveCounterValueType` is an immutable blueprint for creating a new `LiveCounter` object. It stores the desired initial count value and is evaluated when passed to a mutation method such as `LiveMap#set` ([RTLM20](#RTLM20)) or as an entry value in `LiveMapValueType.create` ([RTLMV3](#RTLMV3)). - `(RTLCV1)` `LiveCounterValueType` is an immutable value type representing the intent to create a new `LiveCounter` with a specific initial count - `(RTLCV2)` `LiveCounterValueType` has the following internal properties: - `(RTLCV2a)` `count` `Number` - the initial count value for the `LiveCounter` to be created -- `(RTLCV3)` `LiveCounter.create` static factory function: +- `(RTLCV3)` `LiveCounterValueType.create` static factory function: - `(RTLCV3a)` Expects the following arguments: - `(RTLCV3a1)` `initialCount` `Number` (optional) - the initial count for the new `LiveCounter` object. Defaults to 0 - `(RTLCV3b)` Returns a new `LiveCounterValueType` instance with the internal `count` set to the provided `initialCount` (or 0 if omitted) @@ -747,12 +747,12 @@ A `LiveCounterValueType` is an immutable blueprint for creating a new `LiveCount ### LiveMapValueType -A `LiveMapValueType` is an immutable blueprint for creating a new `LiveMap` object. It stores the desired initial entries and is evaluated when passed to a mutation method such as `LiveMap#set` ([RTLM20](#RTLM20)) or as an entry value in another `LiveMap.create` ([RTLMV3](#RTLMV3)) call. Supports arbitrarily deep nesting of `LiveMapValueType` and `LiveCounterValueType` values within entries. +A `LiveMapValueType` is an immutable blueprint for creating a new `LiveMap` object. It stores the desired initial entries and is evaluated when passed to a mutation method such as `LiveMap#set` ([RTLM20](#RTLM20)) or as an entry value in another `LiveMapValueType.create` ([RTLMV3](#RTLMV3)) call. Supports arbitrarily deep nesting of `LiveMapValueType` and `LiveCounterValueType` values within entries. - `(RTLMV1)` `LiveMapValueType` is an immutable value type representing the intent to create a new `LiveMap` with specific initial entries - `(RTLMV2)` `LiveMapValueType` has the following internal properties: - `(RTLMV2a)` `entries` `Dict` (optional) - the initial entries for the `LiveMap` to be created -- `(RTLMV3)` `LiveMap.create` static factory function: +- `(RTLMV3)` `LiveMapValueType.create` static factory function: - `(RTLMV3a)` Expects the following arguments: - `(RTLMV3a1)` `entries` `Dict` (optional) - the initial entries for the new `LiveMap` object - `(RTLMV3b)` Returns a new `LiveMapValueType` instance with the internal `entries` set to the provided `entries` (or undefined if omitted) @@ -1035,34 +1035,32 @@ Types and their properties/methods are public and exposed to users by default. A update: Object // RTLO4b4a noop: Boolean // RTLO4b4b - class LiveCounter extends LiveObject: // RTLC*, RTLC1 - value() -> Number // RTLC5, internal - increment(Number amount) => io // RTLC12, internal - decrement(Number amount) => io // RTLC13, internal - static create(Number initialCount?) -> LiveCounterValueType // RTLCV3 + class LiveCounter extends LiveObject: // RTLC*, RTLC1, internal + value() -> Number // RTLC5 + increment(Number amount) => io // RTLC12 + decrement(Number amount) => io // RTLC13 interface LiveCounterUpdate extends LiveObjectUpdate: // RTLC11, RTLC11a, internal update: { amount: Number } // RTLC11b, RTLC11b1 class LiveMap extends LiveObject: // RTLM*, RTLM1, internal - clearTimeserial: String? // RTLM25, internal - get(key: String) -> (Boolean | Binary | Number | String | JsonArray | JsonObject | LiveCounter | LiveMap)? // RTLM5, internal - size() -> Number // RTLM10, internal - entries() -> [String, (Boolean | Binary | Number | String | JsonArray | JsonObject | LiveCounter | LiveMap)?][] // RTLM11, internal - keys() -> String[] // RTLM12, internal - values() -> (Boolean | Binary | Number | String | JsonArray | JsonObject | LiveCounter | LiveMap)?[] // RTLM13, internal - set(String key, (Boolean | Binary | Number | String | JsonArray | JsonObject | LiveCounterValueType | LiveMapValueType) value) => io // RTLM20, internal - remove(String key) => io // RTLM21, internal - static create(Dict entries?) -> LiveMapValueType // RTLMV3 + clearTimeserial: String? // RTLM25 + get(key: String) -> (Boolean | Binary | Number | String | JsonArray | JsonObject | LiveCounter | LiveMap)? // RTLM5 + size() -> Number // RTLM10 + entries() -> [String, (Boolean | Binary | Number | String | JsonArray | JsonObject | LiveCounter | LiveMap)?][] // RTLM11 + keys() -> String[] // RTLM12 + values() -> (Boolean | Binary | Number | String | JsonArray | JsonObject | LiveCounter | LiveMap)?[] // RTLM13 + set(String key, (Boolean | Binary | Number | String | JsonArray | JsonObject | LiveCounterValueType | LiveMapValueType) value) => io // RTLM20 + remove(String key) => io // RTLM21 interface LiveMapUpdate extends LiveObjectUpdate: // RTLM18, RTLM18a, internal update: Dict // RTLM18b class LiveCounterValueType: // RTLCV* - // created via LiveCounter.create(), RTLCV3 + static create(Number initialCount?) -> LiveCounterValueType // RTLCV3 class LiveMapValueType: // RTLMV* - // created via LiveMap.create(), RTLMV3 + static create(Dict entries?) -> LiveMapValueType // RTLMV3 interface PathObjectSubscriptionEvent: // RTPO19d object: PathObject // RTPO19d1 From d08178eee430f0a14c976b8aa323508bc03cf3d3 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Thu, 21 May 2026 08:57:45 -0300 Subject: [PATCH 11/40] Reword RTO23d to use PathObject's path/root property names Previously RTO23d described the returned PathObject as "wrapping the `LiveMap` with id `root`", which is misleading: `PathObject` doesn't wrap a single resolved value, it has a `path` ([RTPO2a](#RTPO2)) and a separate `root` ([RTPO2b](#RTPO2)) property. The follow-on sentence also restated this in a slightly different way, leaving redundancy. Reword to a single sentence that names the two properties directly and assigns each. Addresses [1]. [1] https://github.com/ably/specification/pull/427#discussion_r3260885689 Co-Authored-By: Claude Opus 4.7 (1M context) --- specifications/objects-features.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifications/objects-features.md b/specifications/objects-features.md index 1dad924d..d10b15ac 100644 --- a/specifications/objects-features.md +++ b/specifications/objects-features.md @@ -19,7 +19,7 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTO23a)` Requires the `OBJECT_SUBSCRIBE` channel mode to be granted per [RTO2](#RTO2) - `(RTO23b)` If the channel is in the `DETACHED` or `FAILED` state, the library should throw an `ErrorInfo` error with `statusCode` 400 and `code` 90001 - `(RTO23c)` If the [RTO17](#RTO17) sync state is not `SYNCED`, waits for the sync state to transition to `SYNCED` - - `(RTO23d)` Returns a `PathObject` ([RTPO1](#RTPO1)) wrapping the `LiveMap` with id `root` from the internal `ObjectsPool`. The `PathObject` is created with an empty path, rooted at the `root` `LiveMap` + - `(RTO23d)` Returns a new `PathObject` ([RTPO1](#RTPO1)) with `path` ([RTPO2a](#RTPO2)) set to an empty list and `root` ([RTPO2b](#RTPO2)) set to the `LiveMap` with id `root` from the internal `ObjectsPool` - `(RTO11)` This clause has been replaced by [RTLMV3](#RTLMV3). - `(RTO11a)` This clause has been replaced by [RTLMV3](#RTLMV3). - `(RTO11a1)` This clause has been replaced by [RTLMV3](#RTLMV3). From f85ad7afa3adafe6879b76787e34f33b2548835e Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Thu, 21 May 2026 08:58:10 -0300 Subject: [PATCH 12/40] Drop "public" modifier from RTLO4b and RTLO4c MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `LiveObject` is now marked `internal`, so its `subscribe` and `unsubscribe` methods can no longer meaningfully be described as public — they are callable only from within the internal class hierarchy (specifically from `Instance#subscribe`/`unsubscribe`). Drop the "public" prefix from both spec points to avoid implying a user-facing access modifier. The "user may provide a listener" sub-clauses (RTLO4b3, RTLO4c2) are left as-is — strictly the listener comes via `Instance#subscribe` now, but that's a more sweeping rewording outside the scope of this comment. Addresses [1]. [1] https://github.com/ably/specification/pull/427#discussion_r3259389816 Co-Authored-By: Claude Opus 4.7 (1M context) --- specifications/objects-features.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specifications/objects-features.md b/specifications/objects-features.md index d10b15ac..c480d191 100644 --- a/specifications/objects-features.md +++ b/specifications/objects-features.md @@ -313,7 +313,7 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(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 - `(RTLO4)` `LiveObject` methods: - - `(RTLO4b)` public `subscribe` - subscribes a user to data updates on this `LiveObject` instance + - `(RTLO4b)` `subscribe` - subscribes a user to data updates on this `LiveObject` instance - `(RTLO4b1)` Requires the `OBJECT_SUBSCRIBE` channel mode to be granted per [RTO2](#RTO2) - `(RTLO4b2)` If the channel is in the `DETACHED` or `FAILED` state, the library should throw an `ErrorInfo` error with `statusCode` 400 and `code` 90001 - `(RTLO4b3)` A user may provide a listener to subscribe to data updates on this `LiveObject` instance @@ -328,7 +328,7 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLO4b5b)` This clause has been replaced by [RTLO4b7](#RTLO4b7) - `(RTLO4b7)` Returns a [`Subscription`](../features#SUB1) object - `(RTLO4b6)` This operation must not have any side effects on `RealtimeObject`, the underlying channel, or their status - - `(RTLO4c)` public `unsubscribe` - unsubscribes a previously registered listener + - `(RTLO4c)` `unsubscribe` - unsubscribes a previously registered listener - `(RTLO4c1)` This operation does not require any specific channel modes to be granted, nor does it require the channel to be in a specific state - `(RTLO4c2)` A user may provide a listener they wish to deregister from receiving data updates for this `LiveObject` - `(RTLO4c3)` Once deregistered, subsequent data updates for this `LiveObject` must not result in the listener being called From 416e988dfbdd0e11dedd9b67041132aba0743197 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Thu, 21 May 2026 09:22:19 -0300 Subject: [PATCH 13/40] Drop RTPO21 and RTINS18 (`subscribeIterator` spec points) These spec points required SDKs to provide a stream/iterable variant of `PathObject#subscribe` and `Instance#subscribe`. But the principle they encode -- "if there's a callback API, expose a platform-idiomatic stream variant too" -- is not LiveObjects-specific. None of the other callback subscriptions in the Ably specs (Channel#subscribe, Connection.on, Presence#subscribe, etc.) carry an equivalent clause, and encoding it per-API would not scale. A reader also might assume the normative `should` implied something LiveObjects-specific. Drop both prose blocks and the corresponding IDL lines. SDKs that want to expose a Swift `AsyncSequence`, Kotlin `Flow`, etc. variant are free to do so; the existing `subscribeIterator` methods in ably-js (`src/plugins/liveobjects/pathobject.ts:411`, `src/plugins/liveobjects/instance.ts:226`) are unaffected. If the broader principle is worth surfacing, it can be added as a single non-normative note alongside the existing `Subscription` / `SUB1` material in features.md -- separate change, separate discussion. Addresses [1]. [1] https://github.com/ably/specification/pull/427#discussion_r3259912384 Co-Authored-By: Claude Opus 4.7 (1M context) --- specifications/objects-features.md | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/specifications/objects-features.md b/specifications/objects-features.md index c480d191..fb65b299 100644 --- a/specifications/objects-features.md +++ b/specifications/objects-features.md @@ -910,11 +910,6 @@ A `PathObject` is obtained from `RealtimeObject#get` ([RTO23](#RTO23)), which re - `(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 -- `(RTPO21)` The client library should provide a method that allows consuming subscription events as a stream or iterable, rather than via a callback. A suggested name for this method is `subscribeIterator`: - - `(RTPO21a)` Expects the following arguments: - - `(RTPO21a1)` `options` `PathObjectSubscriptionOptions` (optional) - same options as `PathObject#subscribe` ([RTPO19b](#RTPO19b)) - - `(RTPO21b)` Returns a stream or iterable that yields `PathObjectSubscriptionEvent` objects, using the idiomatic construct for the language (e.g. async iterators, channels, flows, or async sequences) - - `(RTPO21c)` Internally wraps `PathObject#subscribe` ([RTPO19](#RTPO19)), converting the callback-based subscription into the appropriate streaming or iterable pattern ### Instance @@ -987,10 +982,6 @@ An `Instance` holds a direct reference to a specific resolved `LiveObject` or pr - `(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 -- `(RTINS18)` The client library should provide a method that allows consuming subscription events as a stream or iterable, rather than via a callback. A suggested name for this method is `subscribeIterator`: - - `(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 ## Interface Definition {#idl} @@ -1091,7 +1082,6 @@ Types and their properties/methods are public and exposed to users by default. A decrement(Number amount?) => io // RTPO18 subscribe((PathObjectSubscriptionEvent) -> listener, PathObjectSubscriptionOptions? options) -> Subscription // RTPO19 unsubscribe((PathObjectSubscriptionEvent) -> listener) // RTPO20 - subscribeIterator(PathObjectSubscriptionOptions? options) -> Stream // RTPO21 class Instance: // RTINS* id: String? // RTINS3 @@ -1109,4 +1099,3 @@ Types and their properties/methods are public and exposed to users by default. A decrement(Number amount?) => io // RTINS15 subscribe((InstanceSubscriptionEvent) -> listener) -> Subscription // RTINS16 unsubscribe((InstanceSubscriptionEvent) -> listener) // RTINS17 - subscribeIterator() -> Stream // RTINS18 From be2e752d710ed9b95e969edbde599b2688f4ccb0 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Thu, 21 May 2026 13:31:58 -0300 Subject: [PATCH 14/40] Specify public-facing types for subscription event message field Previously `PathObjectSubscriptionEvent.message` and `InstanceSubscriptionEvent.message` were typed as `ObjectMessage`, the canonical wire type. ably-js (the reference implementation) does not expose the raw wire `ObjectMessage` to users; it maps to a separate public type via `toUserFacingMessage`, which also recursively converts the embedded `ObjectOperation` -- most notably resolving `mapCreateWithObjectId` / `counterCreateWithObjectId` back to the original `mapCreate` / `counterCreate`. Introduce a `PublicAPI::` namespace-prefix convention in CONTRIBUTING for spec-side disambiguation of public-API types whose natural names clash with canonical wire/internal concepts. Apply the convention by introducing `PublicAPI::ObjectMessage` (PAOM*) and `PublicAPI::ObjectOperation` (PAOOP*) in objects-features.md. Retype the `message` field on both subscription event interfaces, update RTO24b4 to construct via PAOM3, and add a PAOOP3 procedure that resolves the `*CreateWithObjectId` variants back to their derived `*Create` forms (retained per RTLCV4g5 / RTLMV4j5). Co-Authored-By: Claude Opus 4.7 (1M context) --- CONTRIBUTING.md | 8 ++++ specifications/objects-features.md | 76 ++++++++++++++++++++++++++++-- 2 files changed, 79 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9c96bd1a..4ad42710 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -44,6 +44,14 @@ Historically, before the above guidance was established - in particular around _ This left us open to the problem that client library references to spec items could end up semantically invalid if that spec point was re-used later. For example, if `XXX1a` and `XXX1c` exist but `XXX1b` doesn’t because it was removed in the past (prior to this guidance being established), then we should introduce `XXX1d` for the new spec item rather than re-using `XXX1b`. +## Public-API namespacing for name clashes + +Most spec types are public API by default (the IDL marks the exceptions with `internal`). When a public-API type would have the same natural name as an existing internal/wire concept, the first preference is to rename the internal concept so the public type can take the unqualified name. Where no good rename exists for the internal concept, or where renaming it would cause excessive churn or inconsistency across the spec, the spec instead qualifies the public type with a `PublicAPI::` namespace prefix (e.g. `PublicAPI::ObjectMessage`). + +This is purely a spec-side disambiguation: SDKs should expose the type to users under its unqualified name (here, `ObjectMessage`). Where an SDK's language uses a single flat namespace and cannot have two types with that name, the canonical/wire concept may be renamed internally (e.g. `WireObjectMessage`) to free up the public name. + +The `PublicAPI::` prefix is only introduced when there is an actual clash; the bare name remains the canonical reference everywhere else. + ## SDK API docstrings The `api-docstrings.md` file is a set of language-agnostic reference API commentaries for SDK developers to use when adding docstring comments to Ably SDKs. For new fields, this file should be modified in the same PR that makes the spec changes for those fields. diff --git a/specifications/objects-features.md b/specifications/objects-features.md index fb65b299..289b199b 100644 --- a/specifications/objects-features.md +++ b/specifications/objects-features.md @@ -294,7 +294,7 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTO24b1)` Determine the paths in the LiveObjects tree at which the updated `LiveObject` is located - `(RTO24b2)` For each registered subscription, check whether the event path starts with (or equals) the subscription's path - `(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 + - `(RTO24b4)` Create a `PathObjectSubscriptionEvent` whose `object` is a `PathObject` pointing to the event path and whose `message` is a `PublicAPI::ObjectMessage` derived from the source `ObjectMessage` per [PAOM3](#PAOM3), 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 ### LiveObject @@ -903,7 +903,7 @@ A `PathObject` is obtained from `RealtimeObject#get` ([RTO23](#RTO23)), which re - `(RTPO19c)` Returns a [`Subscription`](../features#SUB1) object - `(RTPO19d)` The listener receives a `PathObjectSubscriptionEvent` object with: - `(RTPO19d1)` `object` - a `PathObject` pointing to the path where the change occurred - - `(RTPO19d2)` `message` `ObjectMessage` (optional) - the `ObjectMessage` that caused the change + - `(RTPO19d2)` `message` `PublicAPI::ObjectMessage` (optional) - if the event was caused by an `ObjectMessage` received on the channel, a `PublicAPI::ObjectMessage` ([PAOM1](#PAOM1)) derived from that source `ObjectMessage` per [PAOM3](#PAOM3); otherwise omitted - `(RTPO19e)` The subscription is path-based: it follows the path, not a specific object. If the object at the path changes identity (e.g. via a `MAP_SET` operation replacing it), the subscription continues to deliver events for the new object at that path - `(RTPO19f)` Events at child paths bubble up to the subscription, subject to depth filtering. For example, a subscription at path `a.b` receives events for changes at `a.b`, `a.b.c`, `a.b.c.d`, etc., depending on the configured depth. The dispatch rules are described in [RTO24b](#RTO24b) - `(RTPO19g)` This operation must not have any side effects on `RealtimeObject`, the underlying channel, or their status @@ -975,7 +975,7 @@ An `Instance` holds a direct reference to a specific resolved `LiveObject` or pr - `(RTINS16c)` Subscribes to data updates on the underlying `LiveObject` using `LiveObject#subscribe` ([RTLO4b](#RTLO4b)) - `(RTINS16d)` The listener receives an `InstanceSubscriptionEvent` object with: - `(RTINS16d1)` `object` - an `Instance` wrapping the underlying `LiveObject` - - `(RTINS16d2)` `message` `ObjectMessage` (optional) - the `ObjectMessage` that caused the change + - `(RTINS16d2)` `message` `PublicAPI::ObjectMessage` (optional) - if the event was caused by an `ObjectMessage` received on the channel, a `PublicAPI::ObjectMessage` ([PAOM1](#PAOM1)) derived from that source `ObjectMessage` per [PAOM3](#PAOM3); otherwise omitted - `(RTINS16e)` Returns a [`Subscription`](../features#SUB1) object - `(RTINS16f)` The subscription is identity-based: it follows the specific `LiveObject` instance, regardless of where it sits in the tree - `(RTINS16g)` This operation must not have any side effects on `RealtimeObject`, the underlying channel, or their status @@ -983,6 +983,49 @@ An `Instance` holds a direct reference to a specific resolved `LiveObject` or pr - `(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 +### PublicAPI::ObjectMessage + +- `(PAOM1)` A `PublicAPI::ObjectMessage` is the user-facing representation of an inbound `ObjectMessage` ([OM1](../features#OM1)) that carried an operation. It is delivered to user subscription listeners (see [RTPO19d2](#RTPO19d2), [RTINS16d2](#RTINS16d2)) so that user code can inspect the metadata of the message that triggered an object change. The `PublicAPI::` prefix is used to avoid a name clash with `ObjectMessage`; SDKs expose this type to users as `ObjectMessage`. +- `(PAOM2)` The attributes available in a `PublicAPI::ObjectMessage` are: + - `(PAOM2a)` `id` string - the `id` ([OM2a](../features#OM2a)) of the source `ObjectMessage` + - `(PAOM2b)` `clientId` string (optional) - the `clientId` ([OM2b](../features#OM2b)) of the source `ObjectMessage` + - `(PAOM2c)` `connectionId` string - the `connectionId` ([OM2c](../features#OM2c)) of the source `ObjectMessage` + - `(PAOM2d)` `timestamp` Time - the `timestamp` ([OM2e](../features#OM2e)) of the source `ObjectMessage` + - `(PAOM2e)` `channel` string - the name of the channel on which the source `ObjectMessage` was received + - `(PAOM2f)` `operation` `PublicAPI::ObjectOperation` ([PAOOP1](#PAOOP1)) - a `PublicAPI::ObjectOperation` derived per [PAOOP3](#PAOOP3) from the `operation` ([OM2f](../features#OM2f)) of the source `ObjectMessage` + - `(PAOM2g)` `serial` string (optional) - the `serial` ([OM2h](../features#OM2h)) of the source `ObjectMessage` + - `(PAOM2h)` `serialTimestamp` Time (optional) - the `serialTimestamp` ([OM2j](../features#OM2j)) of the source `ObjectMessage` + - `(PAOM2i)` `siteCode` string (optional) - the `siteCode` ([OM2i](../features#OM2i)) of the source `ObjectMessage` + - `(PAOM2j)` `extras` JSON-encodable object (optional) - the `extras` ([OM2d](../features#OM2d)) of the source `ObjectMessage` +- `(PAOM3)` To construct a `PublicAPI::ObjectMessage` from a source `ObjectMessage` received on a channel `channel`: + - `(PAOM3a)` Set the `channel` attribute to `channel.name` + - `(PAOM3b)` Copy `id`, `clientId`, `connectionId`, `timestamp`, `serial`, `serialTimestamp`, `siteCode`, and `extras` from the source `ObjectMessage` to the corresponding attributes of the `PublicAPI::ObjectMessage` + - `(PAOM3c)` Set `operation` to a `PublicAPI::ObjectOperation` derived per [PAOOP3](#PAOOP3) from the `operation` ([OM2f](../features#OM2f)) of the source `ObjectMessage` + +### PublicAPI::ObjectOperation + +- `(PAOOP1)` A `PublicAPI::ObjectOperation` is the user-facing representation of an `ObjectOperation` ([OOP1](../features#OOP1)). It is the type of the `operation` attribute of a `PublicAPI::ObjectMessage` ([PAOM2f](#PAOM2f)). The `PublicAPI::` prefix is used to avoid a name clash with `ObjectOperation`; SDKs expose this type to users as `ObjectOperation`. It differs from `ObjectOperation` in that it does not carry the `mapCreateWithObjectId` ([OOP3p](../features#OOP3p)) or `counterCreateWithObjectId` ([OOP3q](../features#OOP3q)) variants: these are outbound-only representations that are resolved back to their derived `MapCreate` / `CounterCreate` forms when constructing a `PublicAPI::ObjectOperation`. +- `(PAOOP2)` The attributes available in a `PublicAPI::ObjectOperation` are: + - `(PAOOP2a)` `action` `ObjectOperationAction` ([OOP2](../features#OOP2)) - the `action` ([OOP3a](../features#OOP3a)) of the source `ObjectOperation` + - `(PAOOP2b)` `objectId` string - the `objectId` ([OOP3b](../features#OOP3b)) of the source `ObjectOperation` + - `(PAOOP2c)` `mapCreate` `MapCreate` (optional) - the `MapCreate` payload, if applicable (see [PAOOP3b](#PAOOP3b)) + - `(PAOOP2d)` `mapSet` `MapSet` (optional) - the `mapSet` ([OOP3k](../features#OOP3k)) of the source `ObjectOperation` + - `(PAOOP2e)` `mapRemove` `MapRemove` (optional) - the `mapRemove` ([OOP3l](../features#OOP3l)) of the source `ObjectOperation` + - `(PAOOP2f)` `counterCreate` `CounterCreate` (optional) - the `CounterCreate` payload, if applicable (see [PAOOP3c](#PAOOP3c)) + - `(PAOOP2g)` `counterInc` `CounterInc` (optional) - the `counterInc` ([OOP3n](../features#OOP3n)) of the source `ObjectOperation` + - `(PAOOP2h)` `objectDelete` `ObjectDelete` (optional) - the `objectDelete` ([OOP3o](../features#OOP3o)) of the source `ObjectOperation` + - `(PAOOP2i)` `mapClear` `MapClear` (optional) - the `mapClear` ([OOP3r](../features#OOP3r)) of the source `ObjectOperation` +- `(PAOOP3)` To construct a `PublicAPI::ObjectOperation` from a source `ObjectOperation`: + - `(PAOOP3a)` Copy `action`, `objectId`, `mapSet`, `mapRemove`, `counterInc`, `objectDelete`, and `mapClear` from the source `ObjectOperation` to the corresponding attributes of the `PublicAPI::ObjectOperation` + - `(PAOOP3b)` Set `mapCreate` as follows: + - `(PAOOP3b1)` If `mapCreate` ([OOP3j](../features#OOP3j)) is present on the source, set `mapCreate` to that value + - `(PAOOP3b2)` Else if `mapCreateWithObjectId` ([OOP3p](../features#OOP3p)) is present on the source, set `mapCreate` to the `MapCreate` from which it was derived (retained per [RTLMV4j5](#RTLMV4j5)) + - `(PAOOP3b3)` Otherwise omit `mapCreate` + - `(PAOOP3c)` Set `counterCreate` as follows: + - `(PAOOP3c1)` If `counterCreate` ([OOP3m](../features#OOP3m)) is present on the source, set `counterCreate` to that value + - `(PAOOP3c2)` Else if `counterCreateWithObjectId` ([OOP3q](../features#OOP3q)) is present on the source, set `counterCreate` to the `CounterCreate` from which it was derived (retained per [RTLCV4g5](#RTLCV4g5)) + - `(PAOOP3c3)` Otherwise omit `counterCreate` + ## Interface Definition {#idl} Describes types for RealtimeObject.\ @@ -1055,14 +1098,37 @@ Types and their properties/methods are public and exposed to users by default. A interface PathObjectSubscriptionEvent: // RTPO19d object: PathObject // RTPO19d1 - message: ObjectMessage? // RTPO19d2 + message: PublicAPI::ObjectMessage? // RTPO19d2 interface PathObjectSubscriptionOptions: // RTPO19b depth: Number? // RTPO19b1 interface InstanceSubscriptionEvent: // RTINS16d object: Instance // RTINS16d1 - message: ObjectMessage? // RTINS16d2 + message: PublicAPI::ObjectMessage? // RTINS16d2 + + class PublicAPI::ObjectMessage: // PAOM* + id: String // PAOM2a + clientId: String? // PAOM2b + connectionId: String // PAOM2c + timestamp: Time // PAOM2d + channel: String // PAOM2e + operation: PublicAPI::ObjectOperation // PAOM2f + serial: String? // PAOM2g + serialTimestamp: Time? // PAOM2h + siteCode: String? // PAOM2i + extras: JsonObject? // PAOM2j + + class PublicAPI::ObjectOperation: // PAOOP* + action: ObjectOperationAction // PAOOP2a + objectId: String // PAOOP2b + mapCreate: MapCreate? // PAOOP2c + mapSet: MapSet? // PAOOP2d + mapRemove: MapRemove? // PAOOP2e + counterCreate: CounterCreate? // PAOOP2f + counterInc: CounterInc? // PAOOP2g + objectDelete: ObjectDelete? // PAOOP2h + mapClear: MapClear? // PAOOP2i class PathObject: // RTPO* path() -> String // RTPO4 From 5340bae21f01e3aba4f8bd9502288300cc546eb1 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Thu, 21 May 2026 15:20:24 -0300 Subject: [PATCH 15/40] Propagate source ObjectMessage on LiveObjectUpdate Introduces an optional `objectMessage` field on `LiveObjectUpdate` (RTLO4b4d) -- the source `ObjectMessage` that caused the update, if any -- and threads it through the apply procedures so that every non-noop `LiveObjectUpdate` carries a reference to the source `ObjectMessage`. Mirrors the structure ably-js uses: each apply procedure takes the source `ObjectMessage` as an additional argument and sets `objectMessage` on the returned update; pure data-diff helpers (RTLC14, RTLM22) are left untouched, with their callers (RTLC6, RTLM6) setting `objectMessage` on the resulting update before returning. Apply procedures updated to accept `ObjectMessage`: - LiveCounter: RTLC6, RTLC8, RTLC9, RTLC16 - LiveMap: RTLM6, RTLM7, RTLM8, RTLM16, RTLM23, RTLM24 The two sync override procedures (RTLC6, RTLM6) previously accepted `ObjectState` as input; they now accept the wrapping `ObjectMessage` and extract `ObjectState` from `ObjectMessage.object`. A parenthetical caveat makes it clear that the provided `ObjectMessage` is guaranteed to have its `object` field populated. Other apply procedures do not read fields off the `ObjectMessage` parameter -- it is purely threaded onto the returned update -- so no analogous precondition is needed for them. Caller sites updated to pass the source `ObjectMessage`: - Operation dispatchers: RTLC7d1, RTLC7d5, RTLM15d1, RTLM15d6, RTLM15d7, RTLM15d8 - Sync caller: RTO5c1a1, RTO5c1b1a, RTO5c1b1b OBJECT_DELETE emit clauses (RTLC7d4a, RTLM15d5a) construct their `LiveObjectUpdate` inline rather than calling an apply procedure; the `objectMessage` field is set directly in those clauses. This commit lays the groundwork for addressing [1] (which asked for the `ObjectMessage` to be added to the `LiveObjectUpdate` so that all emit sites can specify the value to use). A follow-up commit will surface this via the user-facing subscription events. [1] https://github.com/ably/specification/pull/427#discussion_r3259541256 Co-Authored-By: Claude Opus 4.7 (1M context) --- specifications/objects-features.md | 76 +++++++++++++++++------------- 1 file changed, 42 insertions(+), 34 deletions(-) diff --git a/specifications/objects-features.md b/specifications/objects-features.md index 289b199b..2daecd4d 100644 --- a/specifications/objects-features.md +++ b/specifications/objects-features.md @@ -138,7 +138,7 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTO4b)` If the `HAS_OBJECTS` flag is 0 or there is no `flags` field, the sync sequence must be considered complete immediately, and the client library must perform the following actions in order: - `(RTO4b1)` All objects except the one with id `root` must be removed from the internal `ObjectsPool` - `(RTO4b2)` The data for the `LiveMap` with id `root` must be cleared by setting it to a zero-value per [RTLM4](#RTLM4). Note that the client SDK must not create a new `LiveMap` instance with id `root`; it must only clear the internal data of the existing `LiveMap` with id `root` - - `(RTO4b2a)` Emit a `LiveMapUpdate` object for the `LiveMap` with ID `root`, with `LiveMapUpdate.update` consisting of entries for the keys that were removed, each set to `removed` + - `(RTO4b2a)` Emit a `LiveMapUpdate` object for the `LiveMap` with ID `root`, with `LiveMapUpdate.update` consisting of entries for the keys that were removed, each set to `removed`, and without populating `LiveMapUpdate.objectMessage` - `(RTO4b3)` The `SyncObjectsPool` must be cleared - `(RTO4b5)` This clause has been replaced by [RTO4d](#RTO4d) - `(RTO4b4)` Perform the actions for objects sync completion as described in [RTO5c](#RTO5c) @@ -165,12 +165,12 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTO5c)` When the objects sync has completed, the client library must perform the following actions in order: - `(RTO5c1)` For each `ObjectMessage` in the `SyncObjectsPool`, let `ObjectState` be `ObjectMessage.object`: - `(RTO5c1a)` If an object with `ObjectState.objectId` exists in the internal `ObjectsPool`: - - `(RTO5c1a1)` Replace the internal data for the object as described in [RTLC6](#RTLC6) or [RTLM6](#RTLM6) depending on the object type, passing in current `ObjectState` + - `(RTO5c1a1)` Replace the internal data for the object as described in [RTLC6](#RTLC6) or [RTLM6](#RTLM6) depending on the object type, passing in the current `ObjectMessage` - `(RTO5c1a2)` Store the `LiveObjectUpdate` object returned by the operation, along with a reference to the updated object - `(RTO5c1b)` If an object with `ObjectState.objectId` does not exist in the internal `ObjectsPool`: - `(RTO5c1b1)` Create a new `LiveObject` using the data from `ObjectState` and add it to the internal `ObjectsPool`: - - `(RTO5c1b1a)` If `ObjectState.counter` is present, create a zero-value `LiveCounter` (per [RTLC4](#RTLC4)), set its private `objectId` equal to `ObjectState.objectId` and replace its internal data using the current `ObjectState` per [RTLC6](#RTLC6) - - `(RTO5c1b1b)` If `ObjectState.map` is present, create a zero-value `LiveMap` (per [RTLM4](#RTLM4)), set its private `objectId` equal to `ObjectState.objectId`, set its private `semantics` equal to `ObjectState.map.semantics` and replace its internal data using the current `ObjectState` per [RTLM6](#RTLM6) + - `(RTO5c1b1a)` If `ObjectState.counter` is present, create a zero-value `LiveCounter` (per [RTLC4](#RTLC4)), set its private `objectId` equal to `ObjectState.objectId` and replace its internal data using the current `ObjectMessage` per [RTLC6](#RTLC6) + - `(RTO5c1b1b)` If `ObjectState.map` is present, create a zero-value `LiveMap` (per [RTLM4](#RTLM4)), set its private `objectId` equal to `ObjectState.objectId`, set its private `semantics` equal to `ObjectState.map.semantics` and replace its internal data using the current `ObjectMessage` per [RTLM6](#RTLM6) - `(RTO5c1b1c)` This clause has been deleted (redundant to [RTO5f3](#RTO5f3)). - `(RTO5c2)` Remove any objects from the internal `ObjectsPool` for which `objectId`s were not received during the sync sequence - `(RTO5c2a)` The object with ID `root` must not be removed from `ObjectsPool`, as per [RTO3b](#RTO3b) @@ -320,6 +320,7 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLO4b4)` An update to `LiveObject` data is communicated by internally emitting a `LiveObjectUpdate` object for this `LiveObject`, or in any other platform-appropriate manner: - `(RTLO4b4a)` `LiveObjectUpdate.update` contains the specific information about what was changed on the object. The exact type depends on the object type - `(RTLO4b4b)` The `LiveObjectUpdate.noop` internal property can be used to indicate that the update was a no-op + - `(RTLO4b4d)` `LiveObjectUpdate.objectMessage` is an optional `ObjectMessage` - the source `ObjectMessage` that caused this update, if any - `(RTLO4b4c)` When a `LiveObjectUpdate` is emitted: - `(RTLO4b4c1)` If `LiveObjectUpdate` is indicated to be a no-op, do nothing - `(RTLO4b4c2)` Otherwise, the registered listener is called with the `LiveObjectUpdate` object @@ -392,19 +393,19 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLC13a1)` `amount` `Number` - the amount by which to decrement the counter value - `(RTLC13b)` This is an alias for calling [`LiveCounter#increment`](#RTLC12) with a negative `amount` and must be implemented with the same behavior - `(RTLC13c)` If the client library chooses to delegate to `LiveCounter#increment` with a negated `amount`, then in languages where negating a non-number may result in implicit type coercion, the `amount` argument must first be validated as described in [RTLC12e1](#RTLC12e1) before proceeding -- `(RTLC6)` `LiveCounter`'s internal `data` can be replaced with the provided `ObjectState` in the following way: +- `(RTLC6)` `LiveCounter`'s internal `data` can be replaced with the `ObjectState` from a provided `ObjectMessage` (which the caller must ensure has its `object` field populated; let `ObjectState` refer to `ObjectMessage.object`) in the following way: - `(RTLC6a)` Replace the private `siteTimeserials` of the `LiveCounter` with the value from `ObjectState.siteTimeserials` - `(RTLC6e)` If `LiveCounter.isTombstone` is `true`, finish processing the `ObjectState` - `(RTLC6e1)` Return a `LiveCounterUpdate` object with `LiveCounterUpdate.noop` set to `true`, indicating that no update was made to the object - - `(RTLC6f)` If `ObjectState.tombstone` is `true`, tombstone the current `LiveCounter` using [`LiveObject.tombstone`](#RTLO4e), passing in the outer `ObjectMessage` for the `ObjectState`. Finish processing the `ObjectState` - - `(RTLC6f1)` Return a `LiveCounterUpdate` object with `LiveCounterUpdate.update.amount` set to the negative `data` value that this `LiveCounter` had before being tombstoned + - `(RTLC6f)` If `ObjectState.tombstone` is `true`, tombstone the current `LiveCounter` using [`LiveObject.tombstone`](#RTLO4e), passing in the `ObjectMessage`. Finish processing the `ObjectState` + - `(RTLC6f1)` Return a `LiveCounterUpdate` object with `LiveCounterUpdate.update.amount` set to the negative `data` value that this `LiveCounter` had before being tombstoned, and `LiveCounterUpdate.objectMessage` set to the provided `ObjectMessage` - `(RTLC6g)` Store the current `data` value as `previousData` for use in [RTLC6h](#RTLC6h) - `(RTLC6b)` Set the private flag `createOperationIsMerged` to `false` - `(RTLC6c)` Set `data` to the value of `ObjectState.counter.count`, or to 0 if it does not exist - - `(RTLC6d)` If `ObjectState.createOp` is present, merge the initial value into the `LiveCounter` as described in [RTLC16](#RTLC16), passing in the `ObjectState.createOp` instance. Discard the `LiveCounterUpdate` object returned by the merge operation + - `(RTLC6d)` If `ObjectState.createOp` is present, merge the initial value into the `LiveCounter` as described in [RTLC16](#RTLC16), passing in the `ObjectState.createOp` instance and the `ObjectMessage`. Discard the `LiveCounterUpdate` object returned by the merge operation - `(RTLC6d1)` This clause has been replaced by [RTLC10a](#RTLC10a) - `(RTLC6d2)` This clause has been replaced by [RTLC10b](#RTLC10b) - - `(RTLC6h)` Calculate the diff between `previousData` from [RTLC6g](#RTLC6g) and the current `data` per [RTLC14](#RTLC14), and return the resulting `LiveCounterUpdate` object + - `(RTLC6h)` Calculate the diff between `previousData` from [RTLC6g](#RTLC6g) and the current `data` per [RTLC14](#RTLC14), set `LiveCounterUpdate.objectMessage` on the resulting update to the provided `ObjectMessage`, and return the resulting `LiveCounterUpdate` object - `(RTLC7)` An `ObjectOperation` from `ObjectMessage.operation` can be applied to a `LiveCounter` by performing the following actions in order: - `(RTLC7f)` Expects the following arguments: - `(RTLC7f1)` `ObjectMessage` - an `ObjectMessage` instance with an existing `ObjectMessage.operation` object, with `ObjectMessage.operation.objectId` matching the Object ID of this `LiveCounter`. This `ObjectMessage` represents the operation to be applied to this `LiveCounter` @@ -415,46 +416,48 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLC7c)` If `source` is `CHANNEL`, set the entry in the private `siteTimeserials` map at the key `ObjectMessage.siteCode` to equal `ObjectMessage.serial` - `(RTLC7e)` If `LiveCounter.isTombstone` is `true`, the operation cannot be applied to the object. Finish processing the `ObjectMessage` without taking any further action. No data update event is emitted. Return `false` - `(RTLC7d)` The `ObjectMessage.operation.action` field (see [`ObjectOperationAction`](../features#OOP2)) determines the type of operation to apply: - - `(RTLC7d1)` If `ObjectMessage.operation.action` is set to `COUNTER_CREATE`, apply the operation as described in [RTLC8](#RTLC8), passing in `ObjectMessage.operation` + - `(RTLC7d1)` If `ObjectMessage.operation.action` is set to `COUNTER_CREATE`, apply the operation as described in [RTLC8](#RTLC8), passing in `ObjectMessage.operation` and `ObjectMessage` - `(RTLC7d1a)` Emit the `LiveCounterUpdate` object returned as a result of applying the operation - `(RTLC7d1b)` Return `true` - `(RTLC7d2)` This clause has been replaced by [RTLC7d5](#RTLC7d5) as of specification version 6.0.0. - `(RTLC7d2a)` This clause has been replaced by [RTLC7d5a](#RTLC7d5a) as of specification version 6.0.0. - `(RTLC7d2b)` This clause has been replaced by [RTLC7d5b](#RTLC7d5b) as of specification version 6.0.0. - - `(RTLC7d5)` If `ObjectMessage.operation.action` is set to `COUNTER_INC`, apply the operation as described in [RTLC9](#RTLC9), passing in `ObjectMessage.operation.counterInc` + - `(RTLC7d5)` If `ObjectMessage.operation.action` is set to `COUNTER_INC`, apply the operation as described in [RTLC9](#RTLC9), passing in `ObjectMessage.operation.counterInc` and `ObjectMessage` - `(RTLC7d5a)` Emit the `LiveCounterUpdate` object returned as a result of applying the operation - `(RTLC7d5b)` Return `true` - `(RTLC7d4)` If `ObjectMessage.operation.action` is set to `OBJECT_DELETE`, apply the operation as described in [RTLO5](#RTLO5), passing in `ObjectMessage` - - `(RTLC7d4a)` Emit a `LiveCounterUpdate` object after applying the `OBJECT_DELETE` operation, with `LiveCounterUpdate.update.amount` set to the negated value that this `LiveCounter` held before the operation was applied + - `(RTLC7d4a)` Emit a `LiveCounterUpdate` object after applying the `OBJECT_DELETE` operation, with `LiveCounterUpdate.update.amount` set to the negated value that this `LiveCounter` held before the operation was applied and `LiveCounterUpdate.objectMessage` set to `ObjectMessage` - `(RTLC7d4b)` Return `true` - `(RTLC7d3)` Otherwise, log a warning that an object operation message with an unsupported action has been received, and discard the current `ObjectMessage` without taking any further action. No data update event is emitted. Return `false` - `(RTLC8)` A `COUNTER_CREATE` operation can be applied to a `LiveCounter` in the following way: - `(RTLC8a)` Expects the following arguments: - `(RTLC8a1)` `ObjectOperation` + - `(RTLC8a2)` `ObjectMessage` - the source `ObjectMessage` that contains the operation - `(RTLC8d)` The return type is a `LiveCounterUpdate` object, which indicates the data update for this `LiveCounter` - `(RTLC8b)` If the private flag `createOperationIsMerged` is `true`, log a debug or trace message indicating that the operation will not be applied because a `COUNTER_CREATE` operation has already been applied to this `LiveCounter`. Discard the operation without taking any further action, and return a `LiveCounterUpdate` object with `LiveCounterUpdate.noop` set to `true`, indicating that no update was made to the object - - `(RTLC8c)` Otherwise merge the initial value into the `LiveCounter` as described in [RTLC16](#RTLC16), passing in the `ObjectOperation` instance + - `(RTLC8c)` Otherwise merge the initial value into the `LiveCounter` as described in [RTLC16](#RTLC16), passing in the `ObjectOperation` instance and the `ObjectMessage` - `(RTLC8e)` Return the `LiveCounterUpdate` object returned by [RTLC16](#RTLC16) - `(RTLC9)` A `COUNTER_INC` operation can be applied to a `LiveCounter` in the following way: - `(RTLC9a)` Expects the following arguments: - `(RTLC9a1)` This clause has been replaced by [RTLC9a2](#RTLC9a2) as of specification version 6.0.0. - `(RTLC9a2)` `CounterInc` + - `(RTLC9a3)` `ObjectMessage` - the source `ObjectMessage` that contains the operation - `(RTLC9c)` The return type is a `LiveCounterUpdate` object, which indicates the data update for this `LiveCounter` - `(RTLC9b)` This clause has been replaced by [RTLC9f](#RTLC9f) as of specification version 6.0.0. - `(RTLC9d)` This clause has been replaced by [RTLC9g](#RTLC9g) as of specification version 6.0.0. - `(RTLC9e)` This clause has been replaced by [RTLC9h](#RTLC9h) as of specification version 6.0.0. - `(RTLC9f)` Add `CounterInc.number` to `data`, if it exists - - `(RTLC9g)` If `CounterInc.number` exists, return a `LiveCounterUpdate` object with `LiveCounterUpdate.update.amount` set to `CounterInc.number` + - `(RTLC9g)` If `CounterInc.number` exists, return a `LiveCounterUpdate` object with `LiveCounterUpdate.update.amount` set to `CounterInc.number` and `LiveCounterUpdate.objectMessage` set to the provided `ObjectMessage` - `(RTLC9h)` If `CounterInc.number` does not exist, return a `LiveCounterUpdate` object with `LiveCounterUpdate.noop` set to `true` - `(RTLC10)` This clause has been replaced by [RTLC16](#RTLC16) as of specification version 6.0.0. - `(RTLC10a)` This clause has been replaced by [RTLC16a](#RTLC16a) as of specification version 6.0.0. - `(RTLC10b)` This clause has been replaced by [RTLC16b](#RTLC16b) as of specification version 6.0.0. - `(RTLC10c)` This clause has been replaced by [RTLC16c](#RTLC16c) as of specification version 6.0.0. - `(RTLC10d)` This clause has been replaced by [RTLC16d](#RTLC16d) as of specification version 6.0.0. -- `(RTLC16)` The initial value from an `ObjectOperation` can be merged into this `LiveCounter` in the following way. Let `counterCreate` be `ObjectOperation.counterCreate` if present, else the `CounterCreate` from which `ObjectOperation.counterCreateWithObjectId` was derived (see [RTLCV4g5](#RTLCV4g5)): +- `(RTLC16)` The initial value from an `ObjectOperation` can be merged into this `LiveCounter` in the following way. Expects an `ObjectOperation` and an `ObjectMessage` (the source `ObjectMessage` that contains the operation) as arguments. Let `counterCreate` be `ObjectOperation.counterCreate` if present, else the `CounterCreate` from which `ObjectOperation.counterCreateWithObjectId` was derived (see [RTLCV4g5](#RTLCV4g5)): - `(RTLC16a)` Add `counterCreate.count` to `data`, if it exists - `(RTLC16b)` Set the private flag `createOperationIsMerged` to `true` - - `(RTLC16c)` If `counterCreate.count` exists, return a `LiveCounterUpdate` object with `LiveCounterUpdate.update.amount` set to `counterCreate.count` + - `(RTLC16c)` If `counterCreate.count` exists, return a `LiveCounterUpdate` object with `LiveCounterUpdate.update.amount` set to `counterCreate.count` and `LiveCounterUpdate.objectMessage` set to the provided `ObjectMessage` - `(RTLC16d)` If `counterCreate.count` does not exist, return a `LiveCounterUpdate` object with `LiveCounterUpdate.noop` set to `true` - `(RTLC14)` The diff between two `LiveCounter` data values can be calculated in the following way: - `(RTLC14a)` Expects the following arguments: @@ -566,12 +569,12 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLM14a)` The method returns true if `ObjectsMapEntry.tombstone` is true - `(RTLM14c)` The method returns true if `ObjectsMapEntry.data.objectId` exists, there is an object in the local `ObjectsPool` with that id, and that `LiveObject.isTombstone` property is `true` - `(RTLM14b)` Otherwise, it returns false -- `(RTLM6)` `LiveMap` internal `data` can be replaced with the provided `ObjectState` in the following way: +- `(RTLM6)` `LiveMap` internal `data` can be replaced with the `ObjectState` from a provided `ObjectMessage` (which the caller must ensure has its `object` field populated; let `ObjectState` refer to `ObjectMessage.object`) in the following way: - `(RTLM6a)` Replace the private `siteTimeserials` of the `LiveMap` with the value from `ObjectState.siteTimeserials` - `(RTLM6e)` If `LiveMap.isTombstone` is `true`, finish processing the `ObjectState` - `(RTLM6e1)` Return a `LiveMapUpdate` object with `LiveMapUpdate.noop` set to `true`, indicating that no update was made to the object - - `(RTLM6f)` If `ObjectState.tombstone` is `true`, tombstone the current `LiveMap` using [`LiveObject.tombstone`](#RTLO4e), passing in the outer `ObjectMessage` for the `ObjectState`. Finish processing the `ObjectState` - - `(RTLM6f1)` Return a `LiveMapUpdate` object with `LiveMapUpdate.update` consisting of entries for the keys that were removed as a result of the object being tombstoned, each set to `removed` + - `(RTLM6f)` If `ObjectState.tombstone` is `true`, tombstone the current `LiveMap` using [`LiveObject.tombstone`](#RTLO4e), passing in the `ObjectMessage`. Finish processing the `ObjectState` + - `(RTLM6f1)` Return a `LiveMapUpdate` object with `LiveMapUpdate.update` consisting of entries for the keys that were removed as a result of the object being tombstoned, each set to `removed`, and `LiveMapUpdate.objectMessage` set to the provided `ObjectMessage` - `(RTLM6g)` Store the current `data` value as `previousData` for use in [RTLM6h](#RTLM6h) - `(RTLM6b)` Set the private flag `createOperationIsMerged` to `false` - `(RTLM6i)` Set the private `clearTimeserial` to `ObjectState.map.clearTimeserial`, or to `null` if not provided @@ -580,12 +583,12 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLM6c1a)` This clause has been replaced by [RTLO6a](#RTLO6a) - `(RTLM6c1b)` This clause has been replaced by [RTLO6b](#RTLO6b) - `(RTLM6c1b1)` This clause has been replaced by [RTLO6b1](#RTLO6b1) - - `(RTLM6d)` If `ObjectState.createOp` is present, merge the initial value into the `LiveMap` as described in [RTLM23](#RTLM23), passing in the `ObjectState.createOp` instance. Discard the `LiveMapUpdate` object returned by the merge operation + - `(RTLM6d)` If `ObjectState.createOp` is present, merge the initial value into the `LiveMap` as described in [RTLM23](#RTLM23), passing in the `ObjectState.createOp` instance and the `ObjectMessage`. Discard the `LiveMapUpdate` object returned by the merge operation - `(RTLM6d1)` This clause has been replaced by [RTLM17a](#RTLM17a) - `(RTLM6d1a)` This clause has been replaced by [RTLM17a1](#RTLM17a1) - `(RTLM6d1b)` This clause has been replaced by [RTLM17a2](#RTLM17a2) - `(RTLM6d2)` This clause has been replaced by [RTLM17b](#RTLM17b) - - `(RTLM6h)` Calculate the diff between `previousData` from [RTLM6g](#RTLM6g) and the current `data` per [RTLM22](#RTLM22), and return the resulting `LiveMapUpdate` object + - `(RTLM6h)` Calculate the diff between `previousData` from [RTLM6g](#RTLM6g) and the current `data` per [RTLM22](#RTLM22), set `LiveMapUpdate.objectMessage` on the resulting update to the provided `ObjectMessage`, and return the resulting `LiveMapUpdate` object - `(RTLM15)` An `ObjectOperation` from `ObjectMessage.operation` can be applied to a `LiveMap` by performing the following actions in order: - `(RTLM15f)` Expects the following arguments: - `(RTLM15f1)` `ObjectMessage` - an `ObjectMessage` instance with an existing `ObjectMessage.operation` object, with `ObjectMessage.operation.objectId` matching the Object ID of this `LiveMap`. This `ObjectMessage` represents the operation to be applied to this `LiveMap` @@ -596,41 +599,43 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLM15c)` If `source` is `CHANNEL`, set the entry in the private `siteTimeserials` map at the key `ObjectMessage.siteCode` to equal `ObjectMessage.serial` - `(RTLM15e)` If `LiveMap.isTombstone` is `true`, the operation cannot be applied to the object. Finish processing the `ObjectMessage` without taking any further action. No data update event is emitted. Return `false` - `(RTLM15d)` The `ObjectMessage.operation.action` field (see [`ObjectOperationAction`](../features#OOP2)) determines the type of operation to apply: - - `(RTLM15d1)` If `ObjectMessage.operation.action` is set to `MAP_CREATE`, apply the operation as described in [RTLM16](#RTLM16), passing in `ObjectMessage.operation` + - `(RTLM15d1)` If `ObjectMessage.operation.action` is set to `MAP_CREATE`, apply the operation as described in [RTLM16](#RTLM16), passing in `ObjectMessage.operation` and `ObjectMessage` - `(RTLM15d1a)` Emit the `LiveMapUpdate` object returned as a result of applying the operation - `(RTLM15d1b)` Return `true` - `(RTLM15d2)` This clause has been replaced by [RTLM15d6](#RTLM15d6) as of specification version 6.0.0. - `(RTLM15d2a)` This clause has been replaced by [RTLM15d6a](#RTLM15d6a) as of specification version 6.0.0. - `(RTLM15d2b)` This clause has been replaced by [RTLM15d6b](#RTLM15d6b) as of specification version 6.0.0. - - `(RTLM15d6)` If `ObjectMessage.operation.action` is set to `MAP_SET`, apply the operation as described in [RTLM7](#RTLM7), passing in `ObjectMessage.operation.mapSet` and `ObjectMessage.serial` + - `(RTLM15d6)` If `ObjectMessage.operation.action` is set to `MAP_SET`, apply the operation as described in [RTLM7](#RTLM7), passing in `ObjectMessage.operation.mapSet`, `ObjectMessage.serial`, and `ObjectMessage` - `(RTLM15d6a)` Emit the `LiveMapUpdate` object returned as a result of applying the operation - `(RTLM15d6b)` Return `true` - `(RTLM15d3)` This clause has been replaced by [RTLM15d7](#RTLM15d7) as of specification version 6.0.0. - `(RTLM15d3a)` This clause has been replaced by [RTLM15d7a](#RTLM15d7a) as of specification version 6.0.0. - `(RTLM15d3b)` This clause has been replaced by [RTLM15d7b](#RTLM15d7b) as of specification version 6.0.0. - - `(RTLM15d7)` If `ObjectMessage.operation.action` is set to `MAP_REMOVE`, apply the operation as described in [RTLM8](#RTLM8), passing in `ObjectMessage.operation.mapRemove`, `ObjectMessage.serial` and `ObjectMessage.serialTimestamp` + - `(RTLM15d7)` If `ObjectMessage.operation.action` is set to `MAP_REMOVE`, apply the operation as described in [RTLM8](#RTLM8), passing in `ObjectMessage.operation.mapRemove`, `ObjectMessage.serial`, `ObjectMessage.serialTimestamp`, and `ObjectMessage` - `(RTLM15d7a)` Emit the `LiveMapUpdate` object returned as a result of applying the operation - `(RTLM15d7b)` Return `true` - `(RTLM15d5)` If `ObjectMessage.operation.action` is set to `OBJECT_DELETE`, apply the operation as described in [RTLO5](#RTLO5), passing in `ObjectMessage` - - `(RTLM15d5a)` Emit a `LiveMapUpdate` object with `LiveMapUpdate.update` consisting of entries for the keys that were removed as a result of applying the `OBJECT_DELETE` operation, each set to `removed` + - `(RTLM15d5a)` Emit a `LiveMapUpdate` object with `LiveMapUpdate.update` consisting of entries for the keys that were removed as a result of applying the `OBJECT_DELETE` operation, each set to `removed`, and `LiveMapUpdate.objectMessage` set to `ObjectMessage` - `(RTLM15d5b)` Return `true` - - `(RTLM15d8)` If `ObjectMessage.operation.action` is set to `MAP_CLEAR`, apply the operation as described in [RTLM24](#RTLM24), passing in `ObjectMessage.serial` + - `(RTLM15d8)` If `ObjectMessage.operation.action` is set to `MAP_CLEAR`, apply the operation as described in [RTLM24](#RTLM24), passing in `ObjectMessage.serial` and `ObjectMessage` - `(RTLM15d8a)` Emit the `LiveMapUpdate` object returned as a result of applying the operation - `(RTLM15d8b)` Return `true` - `(RTLM15d4)` Otherwise, log a warning that an object operation message with an unsupported action has been received, and discard the current `ObjectMessage` without taking any further action. No data update event is emitted. Return `false` - `(RTLM16)` A `MAP_CREATE` operation can be applied to a `LiveMap` in the following way: - `(RTLM16a)` Expects the following arguments: - `(RTLM16a1)` `ObjectOperation` + - `(RTLM16a2)` `ObjectMessage` - the source `ObjectMessage` that contains the operation - `(RTLM16e)` The return type is a `LiveMapUpdate` object, which indicates the data update for this `LiveMap` - `(RTLM16b)` If the private flag `createOperationIsMerged` is `true`, log a debug or trace message indicating that the operation will not be applied because a `MAP_CREATE` operation has already been applied to this `LiveMap`. Discard the operation without taking any further action, and return a `LiveMapUpdate` object with `LiveMapUpdate.noop` set to `true`, indicating that no update was made to the object - `(RTLM16c)` This clause has been deleted. - - `(RTLM16d)` Otherwise merge the initial value into the `LiveMap` as described in [RTLM23](#RTLM23), passing in the `ObjectOperation` instance + - `(RTLM16d)` Otherwise merge the initial value into the `LiveMap` as described in [RTLM23](#RTLM23), passing in the `ObjectOperation` instance and the `ObjectMessage` - `(RTLM16f)` Return the `LiveMapUpdate` object returned by [RTLM23](#RTLM23) - `(RTLM7)` A `MAP_SET` operation for a key can be applied to a `LiveMap` in the following way: - `(RTLM7d)` Expects the following arguments: - `(RTLM7d1)` This clause has been replaced by [RTLM7d3](#RTLM7d3) as of specification version 6.0.0. - `(RTLM7d3)` `MapSet` - `(RTLM7d2)` `serial` string - operation's serial value + - `(RTLM7d4)` `ObjectMessage` - the source `ObjectMessage` that contains the operation - `(RTLM7e)` The return type is a `LiveMapUpdate` object, which indicates the data update for this `LiveMap` - `(RTLM7h)` If the private `clearTimeserial` is non-null, and the provided `serial` is null or the `clearTimeserial` is lexicographically greater than or equal to `serial`, discard the operation without taking any action. Return a `LiveMapUpdate` object with `LiveMapUpdate.noop` set to `true`, indicating that no update was made to the object - `(RTLM7a)` If an `ObjectsMapEntry` exists in the private `data` for the specified key: @@ -650,13 +655,14 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLM7c1)` This clause has been replaced by [RTLM7g1](#RTLM7g1) as of specification version 6.0.0. - `(RTLM7g)` If `MapSet.value.objectId` is non-empty: - `(RTLM7g1)` Create a zero-value `LiveObject` for this `objectId` in the internal `ObjectsPool` per [RTO6](#RTO6) - - `(RTLM7f)` Return a `LiveMapUpdate` object with a `LiveMapUpdate.update` map containing the key used in this operation set to `updated` + - `(RTLM7f)` Return a `LiveMapUpdate` object with a `LiveMapUpdate.update` map containing the key used in this operation set to `updated`, and `LiveMapUpdate.objectMessage` set to the provided `ObjectMessage` - `(RTLM8)` A `MAP_REMOVE` operation for a key can be applied to a `LiveMap` in the following way: - `(RTLM8c)` Expects the following arguments: - `(RTLM8c1)` This clause has been replaced by [RTLM8c4](#RTLM8c4) as of specification version 6.0.0. - `(RTLM8c4)` `MapRemove` - `(RTLM8c2)` `serial` string - operation's serial value - `(RTLM8c3)` `serialTimestamp` Time - operation's serial timestamp value + - `(RTLM8c5)` `ObjectMessage` - the source `ObjectMessage` that contains the operation - `(RTLM8d)` The return type is a `LiveMapUpdate` object, which indicates the data update for this `LiveMap` - `(RTLM8g)` If the private `clearTimeserial` is non-null, and the provided `serial` is null or the `clearTimeserial` is lexicographically greater than or equal to `serial`, discard the operation without taking any action. Return a `LiveMapUpdate` object with `LiveMapUpdate.noop` set to `true`, indicating that no update was made to the object - `(RTLM8a)` If an `ObjectsMapEntry` exists in the private `data` for the specified key: @@ -674,10 +680,11 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLM8f1)` This clause has been replaced by [RTLO6a](#RTLO6a) - `(RTLM8f2)` This clause has been replaced by [RTLO6b](#RTLO6b) - `(RTLM8f2a)` This clause has been replaced by [RTLO6b1](#RTLO6b1) - - `(RTLM8e)` Return a `LiveMapUpdate` object with a `LiveMapUpdate.update` map containing the key used in this operation set to `removed` + - `(RTLM8e)` Return a `LiveMapUpdate` object with a `LiveMapUpdate.update` map containing the key used in this operation set to `removed`, and `LiveMapUpdate.objectMessage` set to the provided `ObjectMessage` - `(RTLM24)` A `MAP_CLEAR` operation can be applied to a `LiveMap` in the following way: - `(RTLM24a)` Expects the following arguments: - `(RTLM24a1)` `serial` string - the operation's serial value + - `(RTLM24a2)` `ObjectMessage` - the source `ObjectMessage` that contains the operation - `(RTLM24b)` The return type is a `LiveMapUpdate` object, which indicates the data update for this `LiveMap` - `(RTLM24c)` If the private `clearTimeserial` is non-null and is lexicographically greater than the provided `serial`, discard the operation without taking any action. Return a `LiveMapUpdate` object with `LiveMapUpdate.noop` set to `true`, indicating that no update was made to the object - `(RTLM24d)` Set the private `clearTimeserial` to the provided `serial` @@ -685,7 +692,7 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLM24e1)` If `ObjectsMapEntry.timeserial` is null or omitted, or the `serial` is lexicographically greater than `ObjectsMapEntry.timeserial`: - `(RTLM24e1a)` Remove the entry from the internal `data` map. The entry is not retained as a tombstone. - `(RTLM24e1b)` Record the key for the `LiveMapUpdate` as `removed` - - `(RTLM24f)` Return a `LiveMapUpdate` object with `LiveMapUpdate.update` containing each key recorded in [RTLM24e1b](#RTLM24e1b) set to `removed` + - `(RTLM24f)` Return a `LiveMapUpdate` object with `LiveMapUpdate.update` containing each key recorded in [RTLM24e1b](#RTLM24e1b) set to `removed`, and `LiveMapUpdate.objectMessage` set to the provided `ObjectMessage` - `(RTLM9)` Whether a map operation can be applied to a map entry is determined as follows: - `(RTLM9a)` For a `LiveMap` with `semantics` set to `ObjectsMapSemantics.LWW` (Last-Write-Wins CRDT semantics), the operation must only be applied if its serial is strictly greater ("after") than the entry's serial when compared lexicographically - `(RTLM9b)` If both the entry serial and the operation serial are null or empty strings, they are treated as the "earliest possible" serials and considered "equal", so the operation must not be applied @@ -698,12 +705,12 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLM17a2)` This clause has been replaced by [RTLM23a2](#RTLM23a2) as of specification version 6.0.0. - `(RTLM17b)` This clause has been replaced by [RTLM23b](#RTLM23b) as of specification version 6.0.0. - `(RTLM17c)` This clause has been replaced by [RTLM23c](#RTLM23c) as of specification version 6.0.0. -- `(RTLM23)` The initial value from an `ObjectOperation` can be merged into this `LiveMap` in the following way. Let `mapCreate` be `ObjectOperation.mapCreate` if present, else the `MapCreate` from which `ObjectOperation.mapCreateWithObjectId` was derived (see [RTLMV4j5](#RTLMV4j5)): +- `(RTLM23)` The initial value from an `ObjectOperation` can be merged into this `LiveMap` in the following way. Expects an `ObjectOperation` and an `ObjectMessage` (the source `ObjectMessage` that contains the operation) as arguments. Let `mapCreate` be `ObjectOperation.mapCreate` if present, else the `MapCreate` from which `ObjectOperation.mapCreateWithObjectId` was derived (see [RTLMV4j5](#RTLMV4j5)): - `(RTLM23a)` For each key-`ObjectsMapEntry` pair in `mapCreate.entries`: - - `(RTLM23a1)` If `ObjectsMapEntry.tombstone` is `false` or omitted, apply the `MAP_SET` operation to the current key as described in [RTLM7](#RTLM7), passing in `ObjectsMapEntry.data` and the current key as `MapSet`, and `ObjectsMapEntry.timeserial` as `serial`. Store the returned `LiveMapUpdate` object for use in [RTLM23c](#RTLM23c) - - `(RTLM23a2)` If `ObjectsMapEntry.tombstone` is `true`, apply the `MAP_REMOVE` operation to the current key as described in [RTLM8](#RTLM8), passing in the current key as `MapRemove`, `ObjectsMapEntry.timeserial` as `serial`, and `ObjectsMapEntry.serialTimestamp` as `serialTimestamp`. Store the returned `LiveMapUpdate` object for use in [RTLM23c](#RTLM23c) + - `(RTLM23a1)` If `ObjectsMapEntry.tombstone` is `false` or omitted, apply the `MAP_SET` operation to the current key as described in [RTLM7](#RTLM7), passing in `ObjectsMapEntry.data` and the current key as `MapSet`, `ObjectsMapEntry.timeserial` as `serial`, and the `ObjectMessage`. Store the returned `LiveMapUpdate` object for use in [RTLM23c](#RTLM23c) + - `(RTLM23a2)` If `ObjectsMapEntry.tombstone` is `true`, apply the `MAP_REMOVE` operation to the current key as described in [RTLM8](#RTLM8), passing in the current key as `MapRemove`, `ObjectsMapEntry.timeserial` as `serial`, `ObjectsMapEntry.serialTimestamp` as `serialTimestamp`, and the `ObjectMessage`. Store the returned `LiveMapUpdate` object for use in [RTLM23c](#RTLM23c) - `(RTLM23b)` Set the private flag `createOperationIsMerged` to `true` - - `(RTLM23c)` Return a single `LiveMapUpdate` object, where `LiveMapUpdate.update` is a merged map containing all key-value pairs from the `LiveMapUpdate.update` maps of the stored `LiveMapUpdate` objects. Skip any stored `LiveMapUpdate` objects marked as no-op + - `(RTLM23c)` Return a single `LiveMapUpdate` object, where `LiveMapUpdate.update` is a merged map containing all key-value pairs from the `LiveMapUpdate.update` maps of the stored `LiveMapUpdate` objects (skipping any stored `LiveMapUpdate` objects marked as no-op), and `LiveMapUpdate.objectMessage` is set to the provided `ObjectMessage` - `(RTLM19)` The `LiveMap` can be checked to determine whether it should release resources for its tombstoned `ObjectsMapEntry` entries as follows: - `(RTLM19a)` For each `ObjectsMapEntry` in the internal `data`: - `(RTLM19a1)` If `ObjectsMapEntry.tombstone` is `true`, and the difference between the current time and `ObjectsMapEntry.tombstonedAt` is greater than or equal to the [grace period](#RTO10b), remove the entry from the internal `data` map and release resources for the corresponding `ObjectsMapEntry` entity to allow it to be garbage collected @@ -1068,6 +1075,7 @@ Types and their properties/methods are public and exposed to users by default. A interface LiveObjectUpdate: // RTLO4b4, internal update: Object // RTLO4b4a noop: Boolean // RTLO4b4b + objectMessage: ObjectMessage? // RTLO4b4d class LiveCounter extends LiveObject: // RTLC*, RTLC1, internal value() -> Number // RTLC5 From 2e96ba2474d9e04805985aff9ea5dac6eb5867b2 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Thu, 21 May 2026 16:04:17 -0300 Subject: [PATCH 16/40] Unify dispatch and surface ObjectMessage to subscribers Restructures the dispatch step for an emitted `LiveObjectUpdate` so that it is a single point that fans out to both the `LiveObject#subscribe` listeners and the path-based subscription dispatch. This means the single no-op check at RTLO4b4c1 covers both consumers; previously path-based dispatch (RTO24b) was described as independently triggered "when a LiveObject emits a LiveObjectUpdate" with no no-op check of its own. Names the path-based dispatch step ("Path-based subscription dispatch") so RTLO4b4c can refer to it by name. RTO24b is reframed as a procedure that accepts a `LiveObject` and a `LiveObjectUpdate` as arguments rather than being self-triggered. RTLO4b4c2 is properly replaced (per CONTRIBUTING) by a new RTLO4b4c3 since the scope changed from "call the LiveObject#subscribe listener" to "fan out to both consumers". The user-facing `message` field on `PathObjectSubscriptionEvent` (RTPO19d2) and `InstanceSubscriptionEvent` (RTINS16d2), and the construction logic at RTO24b4, are tightened to derive `PublicAPI::ObjectMessage` from `LiveObjectUpdate.objectMessage` only when that field is populated AND its `operation` field is populated. This filter at the user-facing boundary preserves the descriptive meaning of `LiveObjectUpdate.objectMessage` (any source ObjectMessage, including sync) while ensuring users only see operation-carrying messages -- matching ably-js's behaviour. RTO24b4 is also split into per-field sub-clauses (RTO24b4a / RTO24b4b) to mirror the structure of RTINS16d. Addresses [1] (no-op handling in path subscription dispatch) and continues [2] (mechanism for surfacing the source `ObjectMessage` via PublicAPI::ObjectMessage). [1] https://github.com/ably/specification/pull/427#discussion_r3262637746 [2] https://github.com/ably/specification/pull/427#discussion_r3259449857 Co-Authored-By: Claude Opus 4.7 (1M context) --- specifications/objects-features.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/specifications/objects-features.md b/specifications/objects-features.md index 2daecd4d..9f3d60a4 100644 --- a/specifications/objects-features.md +++ b/specifications/objects-features.md @@ -290,11 +290,13 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTO22b)` `CHANNEL` - an operation received over a Realtime channel - `(RTO24)` Internal `PathObjectSubscriptionRegister` - manages path-based subscriptions for `PathObject#subscribe` ([RTPO19](#RTPO19)) - `(RTO24a)` The `RealtimeObject` instance maintains a single `PathObjectSubscriptionRegister` that manages all path-based subscriptions for the channel - - `(RTO24b)` When a `LiveObject` in the `ObjectsPool` emits a `LiveObjectUpdate` (per [RTLO4b4](#RTLO4b4)), the `PathObjectSubscriptionRegister` must determine which subscriptions should be notified: - - `(RTO24b1)` Determine the paths in the LiveObjects tree at which the updated `LiveObject` is located + - `(RTO24b)` Path-based subscription dispatch: given a `LiveObject` and a `LiveObjectUpdate`, the `PathObjectSubscriptionRegister` must determine which subscriptions should be notified by performing the following actions in order: + - `(RTO24b1)` Determine the paths in the LiveObjects tree at which the `LiveObject` is located - `(RTO24b2)` For each registered subscription, check whether the event path starts with (or equals) the subscription's path - `(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` whose `object` is a `PathObject` pointing to the event path and whose `message` is a `PublicAPI::ObjectMessage` derived from the source `ObjectMessage` per [PAOM3](#PAOM3), and call the subscription's listener + - `(RTO24b4)` Call the subscription's listener with a `PathObjectSubscriptionEvent` that has: + - `(RTO24b4a)` `object` - a `PathObject` pointing to the event path + - `(RTO24b4b)` `message` - if `LiveObjectUpdate.objectMessage` is populated and its `operation` field is populated, a `PublicAPI::ObjectMessage` derived from `LiveObjectUpdate.objectMessage` per [PAOM3](#PAOM3); otherwise omitted - `(RTO24b5)` If a listener throws an error, the error must be caught and logged without affecting the dispatch to other subscriptions ### LiveObject @@ -323,7 +325,10 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLO4b4d)` `LiveObjectUpdate.objectMessage` is an optional `ObjectMessage` - the source `ObjectMessage` that caused this update, if any - `(RTLO4b4c)` When a `LiveObjectUpdate` is emitted: - `(RTLO4b4c1)` If `LiveObjectUpdate` is indicated to be a no-op, do nothing - - `(RTLO4b4c2)` Otherwise, the registered listener is called with the `LiveObjectUpdate` object + - `(RTLO4b4c2)` This clause has been replaced by [RTLO4b4c3](#RTLO4b4c3) as of specification version 6.0.0. + - `(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` - `(RTLO4b5)` This clause has been replaced by [RTLO4b7](#RTLO4b7) - `(RTLO4b5a)` This clause has been replaced by [RTLO4b7](#RTLO4b7) - `(RTLO4b5b)` This clause has been replaced by [RTLO4b7](#RTLO4b7) @@ -910,7 +915,7 @@ A `PathObject` is obtained from `RealtimeObject#get` ([RTO23](#RTO23)), which re - `(RTPO19c)` Returns a [`Subscription`](../features#SUB1) object - `(RTPO19d)` The listener receives a `PathObjectSubscriptionEvent` object with: - `(RTPO19d1)` `object` - a `PathObject` pointing to the path where the change occurred - - `(RTPO19d2)` `message` `PublicAPI::ObjectMessage` (optional) - if the event was caused by an `ObjectMessage` received on the channel, a `PublicAPI::ObjectMessage` ([PAOM1](#PAOM1)) derived from that source `ObjectMessage` per [PAOM3](#PAOM3); otherwise omitted + - `(RTPO19d2)` `message` `PublicAPI::ObjectMessage` (optional) - if `LiveObjectUpdate.objectMessage` from the [RTLO4b4](#RTLO4b4) emission that triggered this event is populated and its `operation` field is populated, a `PublicAPI::ObjectMessage` ([PAOM1](#PAOM1)) derived from it per [PAOM3](#PAOM3); otherwise omitted - `(RTPO19e)` The subscription is path-based: it follows the path, not a specific object. If the object at the path changes identity (e.g. via a `MAP_SET` operation replacing it), the subscription continues to deliver events for the new object at that path - `(RTPO19f)` Events at child paths bubble up to the subscription, subject to depth filtering. For example, a subscription at path `a.b` receives events for changes at `a.b`, `a.b.c`, `a.b.c.d`, etc., depending on the configured depth. The dispatch rules are described in [RTO24b](#RTO24b) - `(RTPO19g)` This operation must not have any side effects on `RealtimeObject`, the underlying channel, or their status @@ -982,7 +987,7 @@ An `Instance` holds a direct reference to a specific resolved `LiveObject` or pr - `(RTINS16c)` Subscribes to data updates on the underlying `LiveObject` using `LiveObject#subscribe` ([RTLO4b](#RTLO4b)) - `(RTINS16d)` The listener receives an `InstanceSubscriptionEvent` object with: - `(RTINS16d1)` `object` - an `Instance` wrapping the underlying `LiveObject` - - `(RTINS16d2)` `message` `PublicAPI::ObjectMessage` (optional) - if the event was caused by an `ObjectMessage` received on the channel, a `PublicAPI::ObjectMessage` ([PAOM1](#PAOM1)) derived from that source `ObjectMessage` per [PAOM3](#PAOM3); otherwise omitted + - `(RTINS16d2)` `message` `PublicAPI::ObjectMessage` (optional) - if `LiveObjectUpdate.objectMessage` from the underlying `LiveObject#subscribe` notification is populated and its `operation` field is populated, a `PublicAPI::ObjectMessage` ([PAOM1](#PAOM1)) derived from it per [PAOM3](#PAOM3); otherwise omitted - `(RTINS16e)` Returns a [`Subscription`](../features#SUB1) object - `(RTINS16f)` The subscription is identity-based: it follows the specific `LiveObject` instance, regardless of where it sits in the tree - `(RTINS16g)` This operation must not have any side effects on `RealtimeObject`, the underlying channel, or their status From 07ca016d13f6cd10aa998b5e65440c4490873988 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Thu, 21 May 2026 17:25:09 -0300 Subject: [PATCH 17/40] Restructure path-based subscription dispatch Addresses the remaining unticked items in the "Event dispatch: must fix" section of the PR 427 prioritisation comment [1]: - RTO24b now iterates per `pathToThis` and dispatches against a preference-ordered list of candidate paths, matching the post-2223 ably-js implementation [2]. For a `LiveMapUpdate`, the keys whose entries changed contribute further candidates beyond `pathToThis` itself, so a subscription targeting `parentMap.someKey` is now notified when the parent map emits an update mentioning `someKey`. - RTO24c hoists out the subscription-coverage predicate (prefix + depth) as a reusable definition with worked examples. - RTO24d captures the two non-normative consequences of the dispatch model (at-most-one notification per `pathToThis`; the parent path wins when both are covered). - RTLO4f introduces a placeholder for `LiveObject.getFullPaths`; the procedure itself will be specified in a follow-up. - RTPO19 is slimmed down: dispatch-describing prose moves to RTO24, the worked depth examples move under the coverage rule, and the new RTPO19e states the actual normative behaviour of `subscribe` ("adds a subscription to the `PathObjectSubscriptionRegister`"). [1] https://github.com/ably/specification/pull/427#issuecomment-4508418686 [2] https://github.com/ably/ably-js/pull/2223 Co-Authored-By: Claude Opus 4.7 (1M context) --- specifications/objects-features.md | 42 ++++++++++++++++++------------ 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/specifications/objects-features.md b/specifications/objects-features.md index 9f3d60a4..13a547ee 100644 --- a/specifications/objects-features.md +++ b/specifications/objects-features.md @@ -291,13 +291,25 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTO24)` Internal `PathObjectSubscriptionRegister` - manages path-based subscriptions for `PathObject#subscribe` ([RTPO19](#RTPO19)) - `(RTO24a)` The `RealtimeObject` instance maintains a single `PathObjectSubscriptionRegister` that manages all path-based subscriptions for the channel - `(RTO24b)` Path-based subscription dispatch: given a `LiveObject` and a `LiveObjectUpdate`, the `PathObjectSubscriptionRegister` must determine which subscriptions should be notified by performing the following actions in order: - - `(RTO24b1)` Determine the paths in the LiveObjects tree at which the `LiveObject` is located - - `(RTO24b2)` For each registered subscription, check whether the event path starts with (or equals) the subscription's path - - `(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)` Call the subscription's listener with a `PathObjectSubscriptionEvent` that has: - - `(RTO24b4a)` `object` - a `PathObject` pointing to the event path - - `(RTO24b4b)` `message` - if `LiveObjectUpdate.objectMessage` is populated and its `operation` field is populated, a `PublicAPI::ObjectMessage` derived from `LiveObjectUpdate.objectMessage` per [PAOM3](#PAOM3); otherwise omitted - - `(RTO24b5)` If a listener throws an error, the error must be caught and logged without affecting the dispatch to other subscriptions + - `(RTO24b1)` Let `pathsToThis` be the set of paths returned by calling `getFullPaths` ([RTLO4f](#RTLO4f)) on the `LiveObject` + - `(RTO24b2)` For each `pathToThis` in `pathsToThis`: + - `(RTO24b2a)` Construct an ordered list of candidate paths `candidatePaths`, in order of decreasing preference: + - `(RTO24b2a1)` The first (most-preferred) candidate is `pathToThis` itself + - `(RTO24b2a2)` If the `LiveObjectUpdate` is a `LiveMapUpdate`, then for each key in `LiveMapUpdate.update`, append a further candidate consisting of `pathToThis` extended by that key + - `(RTO24b2b)` For each registered subscription, find the first `eventPath` in `candidatePaths` that the subscription covers per [RTO24c1](#RTO24c1). If no such `eventPath` exists, do nothing for this subscription. Otherwise, call the subscription's listener exactly once with a `PathObjectSubscriptionEvent` that has: + - `(RTO24b2b1)` `object` - a new `PathObject` ([RTPO1](#RTPO1)) with `path` ([RTPO2a](#RTPO2)) set to `eventPath` and `root` ([RTPO2b](#RTPO2)) set to the `LiveMap` with id `root` from the internal `ObjectsPool` + - `(RTO24b2b2)` `message` - if `LiveObjectUpdate.objectMessage` is populated and its `operation` field is populated, a `PublicAPI::ObjectMessage` derived from `LiveObjectUpdate.objectMessage` per [PAOM3](#PAOM3); otherwise omitted + - `(RTO24b2c)` If a listener throws an error, the error must be caught and logged without affecting the dispatch to other subscriptions, nor to other `pathToThis` iterations + - `(RTO24c)` Subscription coverage: + - `(RTO24c1)` A subscription with subscribed path `subPath` and `depth` option is said to *cover* a path `eventPath` if and only if `subPath` is a prefix of `eventPath` (treating `subPath` as a prefix of itself, so that an exact path match also satisfies this condition), and either `depth` is undefined/null or `eventPath.length - subPath.length + 1 <= depth` + - `(RTO24c2)` (non-normative) Coverage examples, for a subscription at path `["users"]`: + - `(RTO24c2a)` With `depth` undefined/null: covers `["users"]`, `["users", "emma"]`, `["users", "emma", "visits"]`, and so on at any depth + - `(RTO24c2b)` With `depth = 1`: covers `["users"]`; does not cover `["users", "emma"]` or any deeper path + - `(RTO24c2c)` With `depth = 2`: covers `["users"]` and `["users", "emma"]`; does not cover `["users", "emma", "visits"]` or any deeper path + - `(RTO24c2d)` With any `depth`: does not cover `["admins"]` or `["userPosts"]`, since the subscription path is not a prefix of either + - `(RTO24d)` (non-normative) Consequences of the dispatch model in [RTO24b](#RTO24b): + - `(RTO24d1)` Each subscription receives at most one notification per `pathToThis`. When the same subscription's subscribed path covers both `pathToThis` and one of the longer candidate paths derived from it, the shorter `pathToThis` is the one reported to the listener + - `(RTO24d2)` A subscription whose subscribed path is `parentMap.someKey` still receives notifications when the parent map emits a `LiveMapUpdate` mentioning `someKey`, even though the underlying `LiveObjectUpdate` is emitted by the parent map, not by the entry's object ### LiveObject @@ -356,6 +368,8 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLO4e3b)` This clause has been replaced by [RTLO6b](#RTLO6b) - `(RTLO4e3b1)` This clause has been replaced by [RTLO6b1](#RTLO6b1) - `(RTLO4e4)` Set the data for the `LiveObject` to a zero-value, as described in [RTLC4](#RTLC4) or [RTLM4](#RTLM4) depending on the object type + - `(RTLO4f)` protected `getFullPaths` - returns the set of paths in the LiveObjects tree at which this `LiveObject` is currently located. Each path is an ordered list of string segments from the root `LiveMap`. The same `LiveObject` may be reachable from multiple paths (e.g. when it is referenced by more than one map entry) or from zero paths (e.g. when it is not a descendant of the root). Used by path-based subscription dispatch ([RTO24b](#RTO24b)) + - `(RTLO4f1)` TODO: The procedure for computing the set of paths is to be specified separately - `(RTLO5)` An `OBJECT_DELETE` operation can be applied to a `LiveObject` in the following way: - `(RTLO5a)` Expects the following arguments: - `(RTLO5a1)` `ObjectMessage` @@ -904,21 +918,17 @@ A `PathObject` is obtained from `RealtimeObject#get` ([RTO23](#RTO23)), which re - `(RTPO18d)` If the resolved value is not a `LiveCounter`, the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 92007 - `(RTPO19)` `PathObject#subscribe` function: - `(RTPO19a)` Expects the following arguments: - - `(RTPO19a1)` `listener` - a callback function that receives a `PathObjectSubscriptionEvent` ([RTPO19d](#RTPO19d)) when a change occurs at or below this path + - `(RTPO19a1)` `listener` - a callback function that receives a `PathObjectSubscriptionEvent` ([RTPO19d](#RTPO19d)) - `(RTPO19a2)` `options` `PathObjectSubscriptionOptions` (optional) - subscription options - `(RTPO19b)` `PathObjectSubscriptionOptions` has the following properties: - - `(RTPO19b1)` `depth` `Number` (optional) - controls how many levels deep in the subtree changes trigger the listener: - - `(RTPO19b1a)` If undefined (default), the subscription receives events for changes at any depth below the subscribed path - - `(RTPO19b1b)` If `depth` is 1, only changes to the object at the exact subscribed path trigger the listener - - `(RTPO19b1c)` If `depth` is `n`, changes up to `n - 1` levels of children below the subscribed path trigger the listener - - `(RTPO19b1d)` If `depth` is provided and is not a positive integer, the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 40003 + - `(RTPO19b1)` `depth` `Number` (optional) - controls how many levels deep in the subtree changes trigger the listener. Defaults to undefined/null. The `depth` value is interpreted by the subscription coverage rule in [RTO24c1](#RTO24c1); see [RTO24c2](#RTO24c2) for worked examples + - `(RTPO19b1a)` If `depth` is provided and is not a positive integer, the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 40003 - `(RTPO19c)` Returns a [`Subscription`](../features#SUB1) object - `(RTPO19d)` The listener receives a `PathObjectSubscriptionEvent` object with: - `(RTPO19d1)` `object` - a `PathObject` pointing to the path where the change occurred - `(RTPO19d2)` `message` `PublicAPI::ObjectMessage` (optional) - if `LiveObjectUpdate.objectMessage` from the [RTLO4b4](#RTLO4b4) emission that triggered this event is populated and its `operation` field is populated, a `PublicAPI::ObjectMessage` ([PAOM1](#PAOM1)) derived from it per [PAOM3](#PAOM3); otherwise omitted - - `(RTPO19e)` The subscription is path-based: it follows the path, not a specific object. If the object at the path changes identity (e.g. via a `MAP_SET` operation replacing it), the subscription continues to deliver events for the new object at that path - - `(RTPO19f)` Events at child paths bubble up to the subscription, subject to depth filtering. For example, a subscription at path `a.b` receives events for changes at `a.b`, `a.b.c`, `a.b.c.d`, etc., depending on the configured depth. The dispatch rules are described in [RTO24b](#RTO24b) - - `(RTPO19g)` This operation must not have any side effects on `RealtimeObject`, the underlying channel, or their status + - `(RTPO19e)` Adds a subscription to the `RealtimeObject`'s `PathObjectSubscriptionRegister` ([RTO24](#RTO24)) with subscribed path equal to this `PathObject`'s `path` (per [RTPO2a](#RTPO2a)), the provided `listener`, and the provided `options.depth` + - `(RTPO19f)` This operation must not have any side effects on `RealtimeObject`, the underlying channel, or their status - `(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 From 6a5177fd2e389cbe71744afe0665417a4d9d32c1 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Fri, 22 May 2026 08:57:06 -0300 Subject: [PATCH 18/40] remove RTO24d added in 07ca016d13f6cd10aa998b5e65440c4490873988 but actually would be better off in the UTS --- specifications/objects-features.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/specifications/objects-features.md b/specifications/objects-features.md index 13a547ee..9c5151e1 100644 --- a/specifications/objects-features.md +++ b/specifications/objects-features.md @@ -307,9 +307,6 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTO24c2b)` With `depth = 1`: covers `["users"]`; does not cover `["users", "emma"]` or any deeper path - `(RTO24c2c)` With `depth = 2`: covers `["users"]` and `["users", "emma"]`; does not cover `["users", "emma", "visits"]` or any deeper path - `(RTO24c2d)` With any `depth`: does not cover `["admins"]` or `["userPosts"]`, since the subscription path is not a prefix of either - - `(RTO24d)` (non-normative) Consequences of the dispatch model in [RTO24b](#RTO24b): - - `(RTO24d1)` Each subscription receives at most one notification per `pathToThis`. When the same subscription's subscribed path covers both `pathToThis` and one of the longer candidate paths derived from it, the shorter `pathToThis` is the one reported to the listener - - `(RTO24d2)` A subscription whose subscribed path is `parentMap.someKey` still receives notifications when the parent map emits a `LiveMapUpdate` mentioning `someKey`, even though the underlying `LiveObjectUpdate` is emitted by the parent map, not by the entry's object ### LiveObject From 9858d2f316841156add74c71fbc2f442fdf2d540 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Fri, 22 May 2026 09:13:12 -0300 Subject: [PATCH 19/40] Remove unsubscribe(listener) from LiveObject/PathObject/Instance `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] https://github.com/ably/specification/pull/480 Co-Authored-By: Claude Opus 4.7 (1M context) --- specifications/objects-features.md | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/specifications/objects-features.md b/specifications/objects-features.md index 9c5151e1..4b23a07c 100644 --- a/specifications/objects-features.md +++ b/specifications/objects-features.md @@ -343,11 +343,11 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLO4b5b)` This clause has been replaced by [RTLO4b7](#RTLO4b7) - `(RTLO4b7)` Returns a [`Subscription`](../features#SUB1) object - `(RTLO4b6)` This operation must not have any side effects on `RealtimeObject`, the underlying channel, or their status - - `(RTLO4c)` `unsubscribe` - unsubscribes a previously registered listener - - `(RTLO4c1)` This operation does not require any specific channel modes to be granted, nor does it require the channel to be in a specific state - - `(RTLO4c2)` A user may provide a listener they wish to deregister from receiving data updates for this `LiveObject` - - `(RTLO4c3)` Once deregistered, subsequent data updates for this `LiveObject` must not result in the listener being called - - `(RTLO4c4)` This operation must not have any side effects on `RealtimeObject`, the underlying channel, or their status + - `(RTLO4c)` This clause has been deleted + - `(RTLO4c1)` This clause has been deleted + - `(RTLO4c2)` This clause has been deleted + - `(RTLO4c3)` This clause has been deleted + - `(RTLO4c4)` This clause has been deleted - `(RTLO4a)` protected `canApplyOperation` - a convenience method used to determine whether the `ObjectMessage.operation` should be applied to this object based on a serial value - `(RTLO4a1)` Expects the following arguments: - `(RTLO4a1a)` `ObjectMessage` @@ -926,9 +926,6 @@ A `PathObject` is obtained from `RealtimeObject#get` ([RTO23](#RTO23)), which re - `(RTPO19d2)` `message` `PublicAPI::ObjectMessage` (optional) - if `LiveObjectUpdate.objectMessage` from the [RTLO4b4](#RTLO4b4) emission that triggered this event is populated and its `operation` field is populated, a `PublicAPI::ObjectMessage` ([PAOM1](#PAOM1)) derived from it per [PAOM3](#PAOM3); otherwise omitted - `(RTPO19e)` Adds a subscription to the `RealtimeObject`'s `PathObjectSubscriptionRegister` ([RTO24](#RTO24)) with subscribed path equal to this `PathObject`'s `path` (per [RTPO2a](#RTPO2a)), the provided `listener`, and the provided `options.depth` - `(RTPO19f)` This operation must not have any side effects on `RealtimeObject`, the underlying channel, or their status -- `(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 ### Instance @@ -998,9 +995,6 @@ An `Instance` holds a direct reference to a specific resolved `LiveObject` or pr - `(RTINS16e)` Returns a [`Subscription`](../features#SUB1) object - `(RTINS16f)` The subscription is identity-based: it follows the specific `LiveObject` instance, regardless of where it sits in the tree - `(RTINS16g)` This operation must not have any side effects on `RealtimeObject`, the underlying channel, or their status -- `(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 ### PublicAPI::ObjectMessage @@ -1082,7 +1076,6 @@ Types and their properties/methods are public and exposed to users by default. A canApplyOperation(ObjectMessage) -> Boolean // RTLO4a tombstone(ObjectMessage) // RTLO4e subscribe((LiveObjectUpdate) ->) -> Subscription // RTLO4b - unsubscribe((LiveObjectUpdate) ->) // RTLO4c interface LiveObjectUpdate: // RTLO4b4, internal update: Object // RTLO4b4a @@ -1167,7 +1160,6 @@ Types and their properties/methods are public and exposed to users by default. A increment(Number amount?) => io // RTPO17 decrement(Number amount?) => io // RTPO18 subscribe((PathObjectSubscriptionEvent) -> listener, PathObjectSubscriptionOptions? options) -> Subscription // RTPO19 - unsubscribe((PathObjectSubscriptionEvent) -> listener) // RTPO20 class Instance: // RTINS* id: String? // RTINS3 @@ -1184,4 +1176,3 @@ Types and their properties/methods are public and exposed to users by default. A increment(Number amount?) => io // RTINS14 decrement(Number amount?) => io // RTINS15 subscribe((InstanceSubscriptionEvent) -> listener) -> Subscription // RTINS16 - unsubscribe((InstanceSubscriptionEvent) -> listener) // RTINS17 From e1adbc0dcb3a4c6a6c7e748c0aec3b9460c3dba4 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Fri, 22 May 2026 09:27:00 -0300 Subject: [PATCH 20/40] Specify that Subscription#unsubscribe is idempotent 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] https://github.com/ably/specification/pull/480 Co-Authored-By: Claude Opus 4.7 (1M context) --- specifications/features.md | 1 + 1 file changed, 1 insertion(+) diff --git a/specifications/features.md b/specifications/features.md index 9ccb41f0..0282218b 100644 --- a/specifications/features.md +++ b/specifications/features.md @@ -1875,6 +1875,7 @@ The core SDK provides an API for wrapper SDKs to supply Ably with analytics info - `(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 ### Option types {#options} From 1e80b4271f268a2d4b4db8c6828330511e0dc465 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Fri, 22 May 2026 09:29:51 -0300 Subject: [PATCH 21/40] Add internal *ValueType backing fields to the IDL 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] https://github.com/ably/specification/pull/480 Co-Authored-By: Claude Opus 4.7 (1M context) --- specifications/objects-features.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/specifications/objects-features.md b/specifications/objects-features.md index 4b23a07c..6ebf260c 100644 --- a/specifications/objects-features.md +++ b/specifications/objects-features.md @@ -1104,9 +1104,11 @@ Types and their properties/methods are public and exposed to users by default. A update: Dict // RTLM18b class LiveCounterValueType: // RTLCV* + count: Number // RTLCV2a, internal static create(Number initialCount?) -> LiveCounterValueType // RTLCV3 class LiveMapValueType: // RTLMV* + entries: Dict? // RTLMV2a, internal static create(Dict entries?) -> LiveMapValueType // RTLMV3 interface PathObjectSubscriptionEvent: // RTPO19d From 8ce758401748adbd3a99fe8f8d435b921bf1302e Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Fri, 22 May 2026 10:50:17 -0300 Subject: [PATCH 22/40] Add placeholder RTLO3f for parentReferences 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] https://github.com/ably/specification/pull/480 Co-Authored-By: Claude Opus 4.7 (1M context) --- specifications/objects-features.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/specifications/objects-features.md b/specifications/objects-features.md index 6ebf260c..7aa0e2f1 100644 --- a/specifications/objects-features.md +++ b/specifications/objects-features.md @@ -323,6 +323,9 @@ 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>` - 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 + - `(RTLO3f1)` This mapping is keyed by `objectId` for consistency with the rest of the LiveObjects spec, where references between objects are stored as `objectId`s and resolved via the `ObjectsPool` on demand. Implementations may store a direct reference to the parent `LiveMap` instead — for example to avoid an `ObjectsPool` lookup at each step of `getFullPaths` ([RTLO4f](#RTLO4f)) traversal — provided the observable behaviour is unchanged. Such implementations should be aware that this may introduce reference cycles between `LiveMap`s, and must ensure this does not cause memory leaks + - `(RTLO3f2)` TODO: The detailed maintenance rules for `parentReferences` (across `MAP_SET`, `MAP_REMOVE`, `MAP_CLEAR`, `LiveMap` tombstoning, and post-sync rebuild) are to be specified by Sachin in a follow-up; see https://github.com/ably/specification/pull/480 for the in-progress draft - `(RTLO4)` `LiveObject` methods: - `(RTLO4b)` `subscribe` - subscribes a user to data updates on this `LiveObject` instance - `(RTLO4b1)` Requires the `OBJECT_SUBSCRIBE` channel mode to be granted per [RTO2](#RTO2) @@ -1073,6 +1076,7 @@ Types and their properties/methods are public and exposed to users by default. A createOperationIsMerged: Boolean // RTLO3c isTombstone: Boolean // RTLO3d tombstonedAt: Time? // RTLO3e + parentReferences: Dict> // RTLO3f canApplyOperation(ObjectMessage) -> Boolean // RTLO4a tombstone(ObjectMessage) // RTLO4e subscribe((LiveObjectUpdate) ->) -> Subscription // RTLO4b From ec61d11c63d25cba120b14dbda4b03e397d23b80 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Fri, 22 May 2026 11:02:47 -0300 Subject: [PATCH 23/40] Add `getFullPaths` from PR #480 (unreviewed) 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] https://github.com/ably/specification/pull/480 Co-Authored-By: Claude Opus 4.7 (1M context) --- specifications/objects-features.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/specifications/objects-features.md b/specifications/objects-features.md index 7aa0e2f1..c0f95e8f 100644 --- a/specifications/objects-features.md +++ b/specifications/objects-features.md @@ -368,8 +368,14 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLO4e3b)` This clause has been replaced by [RTLO6b](#RTLO6b) - `(RTLO4e3b1)` This clause has been replaced by [RTLO6b1](#RTLO6b1) - `(RTLO4e4)` Set the data for the `LiveObject` to a zero-value, as described in [RTLC4](#RTLC4) or [RTLM4](#RTLM4) depending on the object type - - `(RTLO4f)` protected `getFullPaths` - returns the set of paths in the LiveObjects tree at which this `LiveObject` is currently located. Each path is an ordered list of string segments from the root `LiveMap`. The same `LiveObject` may be reachable from multiple paths (e.g. when it is referenced by more than one map entry) or from zero paths (e.g. when it is not a descendant of the root). Used by path-based subscription dispatch ([RTO24b](#RTO24b)) - - `(RTLO4f1)` TODO: The procedure for computing the set of paths is to be specified separately + - `(RTLO4f)` 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`. + - `(RTLO4f1)` `getFullPaths` MUST be implemented as an enumeration of all *simple paths* from this `LiveObject` to the root `LiveMap` over the inverse of the `parentReferences` graph (i.e. walking child → parent). A *simple path* is a path along which no `LiveObject` appears more than once. This is the standard graph problem, typically solved by a depth-first traversal with path-local backtracking equivalent to NetworkX's `all_simple_paths`. Implementation should choose iterative DFS with explicit stack (easier to read and debugging). + - `(RTLO4f2)` If this `LiveObject` is the root `LiveMap` (objectId `root`), the returned list MUST contain exactly one path, and that path MUST be empty (zero key segments). This makes the root reachable from itself via the empty key sequence + - `(RTLO4f3)` If this `LiveObject` is not the root `LiveMap` and has no entries in its `parentReferences` at the time of the call (e.g. orphaned, or not yet reachable from root), the returned list MUST be empty + - `(RTLO4f4)` While traversing paths, suppress cyclic paths whenever a sibling branch had already revisited the same node. Reference behaviour on cyclic graphs is given by NetworkX's `all_simple_paths`, which implementations MAY consult for worked examples + - `(RTLO4f5)` When a single parent `LiveMap` references this `LiveObject` at multiple keys, the returned list MUST contain one distinct path per such key, each ending at the corresponding key + - `(RTLO4f6)` When this `LiveObject` is reachable via multiple distinct ancestor paths (either because it has multiple parents in `parentReferences`, or because any ancestor on the way to root itself has multiple paths to root), the returned list MUST contain one path per distinct ancestor path + - `(RTLO4f7)` The order of paths in the returned list is not mandatory. Implementations MAY return paths in any order; callers requiring a stable order MUST sort the result themselves - `(RTLO5)` An `OBJECT_DELETE` operation can be applied to a `LiveObject` in the following way: - `(RTLO5a)` Expects the following arguments: - `(RTLO5a1)` `ObjectMessage` From 1755df318c1bccdc969e53ec37999f0e72df9814 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Fri, 22 May 2026 13:52:57 -0300 Subject: [PATCH 24/40] Replace "outermost" with "final element" in *_CREATE references 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] https://github.com/ably/specification/pull/427#discussion_r3259042315 [2] https://github.com/ably/specification/pull/427#discussion_r3261200723 Co-Authored-By: Claude Opus 4.7 (1M context) --- specifications/objects-features.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/specifications/objects-features.md b/specifications/objects-features.md index c0f95e8f..60cca479 100644 --- a/specifications/objects-features.md +++ b/specifications/objects-features.md @@ -564,8 +564,8 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLM20e7)` Set `ObjectMessage.operation.mapSet.value` depending on the type of the provided `value`: - `(RTLM20e7a)` This clause has been replaced by [RTLM20e7g](#RTLM20e7g). - `(RTLM20e7g)` If the `value` is of type `LiveCounterValueType` or `LiveMapValueType`: - - `(RTLM20e7g1)` Evaluate the value type per [RTLCV4](#RTLCV4) or [RTLMV4](#RTLMV4) respectively to generate `*_CREATE` `ObjectMessages`. Collect all generated `ObjectMessages` - - `(RTLM20e7g2)` Set `ObjectMessage.operation.mapSet.value.objectId` to the `objectId` from the outermost `*_CREATE` `ObjectMessage` + - `(RTLM20e7g1)` Evaluate the value type per [RTLCV4](#RTLCV4) or [RTLMV4](#RTLMV4) respectively. Collect all generated `ObjectMessages` into an ordered list — for [RTLCV4](#RTLCV4) the list contains the single returned `ObjectMessage`; for [RTLMV4](#RTLMV4) the list is the returned array + - `(RTLM20e7g2)` Set `ObjectMessage.operation.mapSet.value.objectId` to the `objectId` from the final `ObjectMessage` in the list gathered in [`RTLM20e7g1`](#RTLM20e7g1) - `(RTLM20e7b)` If the `value` is of type `JsonArray` or `JsonObject`, set `ObjectMessage.operation.mapSet.value.json` to that value - `(RTLM20e7c)` If the `value` is of type `String`, set `ObjectMessage.operation.mapSet.value.string` to that value - `(RTLM20e7d)` If the `value` is of type `Number`, set `ObjectMessage.operation.mapSet.value.number` to that value @@ -796,7 +796,7 @@ A `LiveMapValueType` is an immutable blueprint for creating a new `LiveMap` obje - `(RTLMV4c)` If any of the values in the internal `entries` are not of an expected type, the library should throw an `ErrorInfo` error with `statusCode` 400 and `code` 40013, indicating that such data type is unsupported - `(RTLMV4d)` Build entries for the `MapCreate` object. For each key-value pair in the internal `entries` (if present), create an `ObjectsMapEntry` for the value: - `(RTLMV4d1)` If the value is of type `LiveCounterValueType`, evaluate it per [RTLCV4](#RTLCV4) to generate a `COUNTER_CREATE` `ObjectMessage`. Collect the generated `ObjectMessage` and set `ObjectsMapEntry.data.objectId` to the `objectId` from the `ObjectMessage` - - `(RTLMV4d2)` If the value is of type `LiveMapValueType`, recursively evaluate it per [RTLMV4](#RTLMV4) to generate `ObjectMessages`. Collect all generated `ObjectMessages` and set `ObjectsMapEntry.data.objectId` to the `objectId` from the outermost `MAP_CREATE` `ObjectMessage` + - `(RTLMV4d2)` If the value is of type `LiveMapValueType`, recursively evaluate it per [RTLMV4](#RTLMV4) to generate an ordered array of `ObjectMessages`. Collect all generated `ObjectMessages` and set `ObjectsMapEntry.data.objectId` to the `objectId` from the final `ObjectMessage` in the array - `(RTLMV4d3)` If the value is of type `JsonArray` or `JsonObject`, set `ObjectsMapEntry.data.json` to that value - `(RTLMV4d4)` If the value is of type `String`, set `ObjectsMapEntry.data.string` to that value - `(RTLMV4d5)` If the value is of type `Number`, set `ObjectsMapEntry.data.number` to that value From d16e23ed877013548c1cf3a9d7b6b1392ba19125 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Fri, 22 May 2026 14:22:48 -0300 Subject: [PATCH 25/40] Mark PAOM2c `connectionId` as optional This was a transcription error in be2e752d, 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) --- specifications/objects-features.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specifications/objects-features.md b/specifications/objects-features.md index 60cca479..64f54a37 100644 --- a/specifications/objects-features.md +++ b/specifications/objects-features.md @@ -1011,7 +1011,7 @@ An `Instance` holds a direct reference to a specific resolved `LiveObject` or pr - `(PAOM2)` The attributes available in a `PublicAPI::ObjectMessage` are: - `(PAOM2a)` `id` string - the `id` ([OM2a](../features#OM2a)) of the source `ObjectMessage` - `(PAOM2b)` `clientId` string (optional) - the `clientId` ([OM2b](../features#OM2b)) of the source `ObjectMessage` - - `(PAOM2c)` `connectionId` string - the `connectionId` ([OM2c](../features#OM2c)) of the source `ObjectMessage` + - `(PAOM2c)` `connectionId` string (optional) - the `connectionId` ([OM2c](../features#OM2c)) of the source `ObjectMessage` - `(PAOM2d)` `timestamp` Time - the `timestamp` ([OM2e](../features#OM2e)) of the source `ObjectMessage` - `(PAOM2e)` `channel` string - the name of the channel on which the source `ObjectMessage` was received - `(PAOM2f)` `operation` `PublicAPI::ObjectOperation` ([PAOOP1](#PAOOP1)) - a `PublicAPI::ObjectOperation` derived per [PAOOP3](#PAOOP3) from the `operation` ([OM2f](../features#OM2f)) of the source `ObjectMessage` @@ -1135,7 +1135,7 @@ Types and their properties/methods are public and exposed to users by default. A class PublicAPI::ObjectMessage: // PAOM* id: String // PAOM2a clientId: String? // PAOM2b - connectionId: String // PAOM2c + connectionId: String? // PAOM2c timestamp: Time // PAOM2d channel: String // PAOM2e operation: PublicAPI::ObjectOperation // PAOM2f From 7d8333cae866b651bc1ea54addd4cdc26d240799 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Fri, 22 May 2026 14:26:51 -0300 Subject: [PATCH 26/40] Make PAOM3's operation precondition explicit 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) --- specifications/objects-features.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/specifications/objects-features.md b/specifications/objects-features.md index 64f54a37..51db3060 100644 --- a/specifications/objects-features.md +++ b/specifications/objects-features.md @@ -1020,9 +1020,11 @@ An `Instance` holds a direct reference to a specific resolved `LiveObject` or pr - `(PAOM2i)` `siteCode` string (optional) - the `siteCode` ([OM2i](../features#OM2i)) of the source `ObjectMessage` - `(PAOM2j)` `extras` JSON-encodable object (optional) - the `extras` ([OM2d](../features#OM2d)) of the source `ObjectMessage` - `(PAOM3)` To construct a `PublicAPI::ObjectMessage` from a source `ObjectMessage` received on a channel `channel`: - - `(PAOM3a)` Set the `channel` attribute to `channel.name` - - `(PAOM3b)` Copy `id`, `clientId`, `connectionId`, `timestamp`, `serial`, `serialTimestamp`, `siteCode`, and `extras` from the source `ObjectMessage` to the corresponding attributes of the `PublicAPI::ObjectMessage` - - `(PAOM3c)` Set `operation` to a `PublicAPI::ObjectOperation` derived per [PAOOP3](#PAOOP3) from the `operation` ([OM2f](../features#OM2f)) of the source `ObjectMessage` + - `(PAOM3a)` Preconditions (callers are responsible for ensuring these): + - `(PAOM3a1)` The source `ObjectMessage` has its `operation` ([OM2f](../features#OM2f)) field populated + - `(PAOM3b)` Set the `channel` attribute to `channel.name` + - `(PAOM3c)` Copy `id`, `clientId`, `connectionId`, `timestamp`, `serial`, `serialTimestamp`, `siteCode`, and `extras` from the source `ObjectMessage` to the corresponding attributes of the `PublicAPI::ObjectMessage` + - `(PAOM3d)` Set `operation` to a `PublicAPI::ObjectOperation` derived per [PAOOP3](#PAOOP3) from the `operation` ([OM2f](../features#OM2f)) of the source `ObjectMessage` ### PublicAPI::ObjectOperation From 5a61965cee3a67e8784f487ee65120c38864a224 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Fri, 22 May 2026 15:15:59 -0300 Subject: [PATCH 27/40] Mark PAOM2a `id` and PAOM2d `timestamp` as optional 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) --- specifications/objects-features.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/specifications/objects-features.md b/specifications/objects-features.md index 51db3060..ea8d098c 100644 --- a/specifications/objects-features.md +++ b/specifications/objects-features.md @@ -1009,10 +1009,10 @@ An `Instance` holds a direct reference to a specific resolved `LiveObject` or pr - `(PAOM1)` A `PublicAPI::ObjectMessage` is the user-facing representation of an inbound `ObjectMessage` ([OM1](../features#OM1)) that carried an operation. It is delivered to user subscription listeners (see [RTPO19d2](#RTPO19d2), [RTINS16d2](#RTINS16d2)) so that user code can inspect the metadata of the message that triggered an object change. The `PublicAPI::` prefix is used to avoid a name clash with `ObjectMessage`; SDKs expose this type to users as `ObjectMessage`. - `(PAOM2)` The attributes available in a `PublicAPI::ObjectMessage` are: - - `(PAOM2a)` `id` string - the `id` ([OM2a](../features#OM2a)) of the source `ObjectMessage` + - `(PAOM2a)` `id` string (optional) - the `id` ([OM2a](../features#OM2a)) of the source `ObjectMessage` - `(PAOM2b)` `clientId` string (optional) - the `clientId` ([OM2b](../features#OM2b)) of the source `ObjectMessage` - `(PAOM2c)` `connectionId` string (optional) - the `connectionId` ([OM2c](../features#OM2c)) of the source `ObjectMessage` - - `(PAOM2d)` `timestamp` Time - the `timestamp` ([OM2e](../features#OM2e)) of the source `ObjectMessage` + - `(PAOM2d)` `timestamp` Time (optional) - the `timestamp` ([OM2e](../features#OM2e)) of the source `ObjectMessage` - `(PAOM2e)` `channel` string - the name of the channel on which the source `ObjectMessage` was received - `(PAOM2f)` `operation` `PublicAPI::ObjectOperation` ([PAOOP1](#PAOOP1)) - a `PublicAPI::ObjectOperation` derived per [PAOOP3](#PAOOP3) from the `operation` ([OM2f](../features#OM2f)) of the source `ObjectMessage` - `(PAOM2g)` `serial` string (optional) - the `serial` ([OM2h](../features#OM2h)) of the source `ObjectMessage` @@ -1135,10 +1135,10 @@ Types and their properties/methods are public and exposed to users by default. A message: PublicAPI::ObjectMessage? // RTINS16d2 class PublicAPI::ObjectMessage: // PAOM* - id: String // PAOM2a + id: String? // PAOM2a clientId: String? // PAOM2b connectionId: String? // PAOM2c - timestamp: Time // PAOM2d + timestamp: Time? // PAOM2d channel: String // PAOM2e operation: PublicAPI::ObjectOperation // PAOM2f serial: String? // PAOM2g From 2fa9a5e0268597e0af5228711cfb3836d7638bc1 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Sun, 24 May 2026 15:04:58 -0300 Subject: [PATCH 28/40] Align LiveObject tombstone behaviour with ably-js 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] https://github.com/ably/specification/pull/480 [2] https://github.com/ably/ably-js/commit/1d98cc3 Co-Authored-By: Claude Opus 4.7 (1M context) --- specifications/objects-features.md | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/specifications/objects-features.md b/specifications/objects-features.md index 7c0b8888..3ccc4f99 100644 --- a/specifications/objects-features.md +++ b/specifications/objects-features.md @@ -335,12 +335,15 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLO4b4a)` `LiveObjectUpdate.update` contains the specific information about what was changed on the object. The exact type depends on the object type - `(RTLO4b4b)` The `LiveObjectUpdate.noop` internal property can be used to indicate that the update was a no-op - `(RTLO4b4d)` `LiveObjectUpdate.objectMessage` is an optional `ObjectMessage` - the source `ObjectMessage` that caused this update, if any + - `(RTLO4b4e)` The `LiveObjectUpdate.tombstone` internal Boolean property indicates that this update was emitted as a result of this `LiveObject` being tombstoned. It defaults to `false` if not explicitly set - `(RTLO4b4c)` When a `LiveObjectUpdate` is emitted: - `(RTLO4b4c1)` If `LiveObjectUpdate` is indicated to be a no-op, do nothing - `(RTLO4b4c2)` This clause has been replaced by [RTLO4b4c3](#RTLO4b4c3) as of specification version 6.0.0. - `(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)` If `LiveObjectUpdate.tombstone` is `true`, after [RTLO4b4c3a](#RTLO4b4c3a) has completed, the library must deregister all listeners on this `LiveObject` that were registered via `LiveObject#subscribe` ([RTLO4b](#RTLO4b)) + - `(RTLO4b4c3c1)` (non-normative) Path-based subscriptions ([RTPO19](#RTPO19)) are unaffected, because their lifetime is tied to the path rather than to this `LiveObject` instance - `(RTLO4b5)` This clause has been replaced by [RTLO4b7](#RTLO4b7) - `(RTLO4b5a)` This clause has been replaced by [RTLO4b7](#RTLO4b7) - `(RTLO4b5b)` This clause has been replaced by [RTLO4b7](#RTLO4b7) @@ -368,6 +371,10 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLO4e3b)` This clause has been replaced by [RTLO6b](#RTLO6b) - `(RTLO4e3b1)` This clause has been replaced by [RTLO6b1](#RTLO6b1) - `(RTLO4e4)` Set the `data` attribute of the `LiveObject` to the value described in [RTLC4b](#RTLC4b) or [RTLM4c](#RTLM4c), depending on the object type + - `(RTLO4e5)` Compute a `LiveObjectUpdate` representing the data change resulting from this `LiveObject` being tombstoned, by calculating the diff between the `data` value from before [RTLO4e4](#RTLO4e4) was applied (as `previousData`) and the current `data` value (as `newData`), per [RTLC14](#RTLC14) or [RTLM22](#RTLM22), depending on the object type + - `(RTLO4e6)` Set `LiveObjectUpdate.tombstone` to `true` on the object computed in [RTLO4e5](#RTLO4e5) + - `(RTLO4e7)` Set `LiveObjectUpdate.objectMessage` on the object computed in [RTLO4e5](#RTLO4e5) to the `ObjectMessage` argument + - `(RTLO4e8)` Return the `LiveObjectUpdate` object computed in [RTLO4e5](#RTLO4e5) - `(RTLO4f)` 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`. - `(RTLO4f1)` `getFullPaths` MUST be implemented as an enumeration of all *simple paths* from this `LiveObject` to the root `LiveMap` over the inverse of the `parentReferences` graph (i.e. walking child → parent). A *simple path* is a path along which no `LiveObject` appears more than once. This is the standard graph problem, typically solved by a depth-first traversal with path-local backtracking equivalent to NetworkX's `all_simple_paths`. Implementation should choose iterative DFS with explicit stack (easier to read and debugging). - `(RTLO4f2)` If this `LiveObject` is the root `LiveMap` (objectId `root`), the returned list MUST contain exactly one path, and that path MUST be empty (zero key segments). This makes the root reachable from itself via the empty key sequence @@ -380,6 +387,7 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLO5a)` Expects the following arguments: - `(RTLO5a1)` `ObjectMessage` - `(RTLO5b)` Tombstone the current `LiveObject` using [`LiveObject.tombstone`](#RTLO4e), passing in the `ObjectMessage` + - `(RTLO5c)` Return the `LiveObjectUpdate` returned by the `LiveObject.tombstone` call performed in [RTLO5b](#RTLO5b) - `(RTLO6)` A `tombstonedAt` value can be calculated from a provided `serialTimestamp` as follows: - `(RTLO6a)` It is equal to `serialTimestamp` if it exists - `(RTLO6b)` Otherwise, it is equal to the current time using the local clock @@ -425,7 +433,8 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLC6e)` If `LiveCounter.isTombstone` is `true`, finish processing the `ObjectState` - `(RTLC6e1)` Return a `LiveCounterUpdate` object with `LiveCounterUpdate.noop` set to `true`, indicating that no update was made to the object - `(RTLC6f)` If `ObjectState.tombstone` is `true`, tombstone the current `LiveCounter` using [`LiveObject.tombstone`](#RTLO4e), passing in the `ObjectMessage`. Finish processing the `ObjectState` - - `(RTLC6f1)` Return a `LiveCounterUpdate` object with `LiveCounterUpdate.update.amount` set to the negative `data` value that this `LiveCounter` had before being tombstoned, and `LiveCounterUpdate.objectMessage` set to the provided `ObjectMessage` + - `(RTLC6f1)` This clause has been replaced by [RTLC6f2](#RTLC6f2) as of specification version 6.0.0. + - `(RTLC6f2)` Return the `LiveCounterUpdate` returned by the `LiveObject.tombstone` call performed in [RTLC6f](#RTLC6f) - `(RTLC6g)` Store the current `data` value as `previousData` for use in [RTLC6h](#RTLC6h) - `(RTLC6b)` Set the private flag `createOperationIsMerged` to `false` - `(RTLC6c)` Set `data` to the value of `ObjectState.counter.count`, or to 0 if it does not exist @@ -453,7 +462,8 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLC7d5a)` Emit the `LiveCounterUpdate` object returned as a result of applying the operation - `(RTLC7d5b)` Return `true` - `(RTLC7d4)` If `ObjectMessage.operation.action` is set to `OBJECT_DELETE`, apply the operation as described in [RTLO5](#RTLO5), passing in `ObjectMessage` - - `(RTLC7d4a)` Emit a `LiveCounterUpdate` object after applying the `OBJECT_DELETE` operation, with `LiveCounterUpdate.update.amount` set to the negated value that this `LiveCounter` held before the operation was applied and `LiveCounterUpdate.objectMessage` set to `ObjectMessage` + - `(RTLC7d4a)` This clause has been replaced by [RTLC7d4c](#RTLC7d4c) as of specification version 6.0.0. + - `(RTLC7d4c)` Emit the `LiveCounterUpdate` object returned as a result of applying the operation - `(RTLC7d4b)` Return `true` - `(RTLC7d3)` Otherwise, log a warning that an object operation message with an unsupported action has been received, and discard the current `ObjectMessage` without taking any further action. No data update event is emitted. Return `false` - `(RTLC8)` A `COUNTER_CREATE` operation can be applied to a `LiveCounter` in the following way: @@ -606,7 +616,8 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLM6e)` If `LiveMap.isTombstone` is `true`, finish processing the `ObjectState` - `(RTLM6e1)` Return a `LiveMapUpdate` object with `LiveMapUpdate.noop` set to `true`, indicating that no update was made to the object - `(RTLM6f)` If `ObjectState.tombstone` is `true`, tombstone the current `LiveMap` using [`LiveObject.tombstone`](#RTLO4e), passing in the `ObjectMessage`. Finish processing the `ObjectState` - - `(RTLM6f1)` Return a `LiveMapUpdate` object with `LiveMapUpdate.update` consisting of entries for the keys that were removed as a result of the object being tombstoned, each set to `removed`, and `LiveMapUpdate.objectMessage` set to the provided `ObjectMessage` + - `(RTLM6f1)` This clause has been replaced by [RTLM6f2](#RTLM6f2) as of specification version 6.0.0. + - `(RTLM6f2)` Return the `LiveMapUpdate` returned by the `LiveObject.tombstone` call performed in [RTLM6f](#RTLM6f) - `(RTLM6g)` Store the current `data` value as `previousData` for use in [RTLM6h](#RTLM6h) - `(RTLM6b)` Set the private flag `createOperationIsMerged` to `false` - `(RTLM6i)` Set the private `clearTimeserial` to `ObjectState.map.clearTimeserial`, or to `null` if not provided @@ -647,7 +658,8 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLM15d7a)` Emit the `LiveMapUpdate` object returned as a result of applying the operation - `(RTLM15d7b)` Return `true` - `(RTLM15d5)` If `ObjectMessage.operation.action` is set to `OBJECT_DELETE`, apply the operation as described in [RTLO5](#RTLO5), passing in `ObjectMessage` - - `(RTLM15d5a)` Emit a `LiveMapUpdate` object with `LiveMapUpdate.update` consisting of entries for the keys that were removed as a result of applying the `OBJECT_DELETE` operation, each set to `removed`, and `LiveMapUpdate.objectMessage` set to `ObjectMessage` + - `(RTLM15d5a)` This clause has been replaced by [RTLM15d5c](#RTLM15d5c) as of specification version 6.0.0. + - `(RTLM15d5c)` Emit the `LiveMapUpdate` object returned as a result of applying the operation - `(RTLM15d5b)` Return `true` - `(RTLM15d8)` If `ObjectMessage.operation.action` is set to `MAP_CLEAR`, apply the operation as described in [RTLM24](#RTLM24), passing in `ObjectMessage.serial` and `ObjectMessage` - `(RTLM15d8a)` Emit the `LiveMapUpdate` object returned as a result of applying the operation @@ -1093,13 +1105,14 @@ Types and their properties/methods are public and exposed to users by default. A tombstonedAt: Time? // RTLO3e parentReferences: Dict> // RTLO3f canApplyOperation(ObjectMessage) -> Boolean // RTLO4a - tombstone(ObjectMessage) // RTLO4e + tombstone(ObjectMessage) -> LiveObjectUpdate // RTLO4e subscribe((LiveObjectUpdate) ->) -> Subscription // RTLO4b interface LiveObjectUpdate: // RTLO4b4, internal update: Object // RTLO4b4a noop: Boolean // RTLO4b4b objectMessage: ObjectMessage? // RTLO4b4d + tombstone: Boolean // RTLO4b4e class LiveCounter extends LiveObject: // RTLC*, RTLC1, internal value() -> Number // RTLC5 From 860e4797790a99509411236466864316f735ec8b Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Sun, 24 May 2026 16:00:29 -0300 Subject: [PATCH 29/40] Pull parentReferences maintenance rules from PR #480 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> 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] https://github.com/ably/specification/pull/480 Co-Authored-By: Claude Opus 4.7 (1M context) --- specifications/objects-features.md | 31 +++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/specifications/objects-features.md b/specifications/objects-features.md index 3ccc4f99..75f1c47e 100644 --- a/specifications/objects-features.md +++ b/specifications/objects-features.md @@ -174,6 +174,10 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTO5c1b1c)` This clause has been deleted (redundant to [RTO5f3](#RTO5f3)). - `(RTO5c2)` Remove any objects from the internal `ObjectsPool` for which `objectId`s were not received during the sync sequence - `(RTO5c2a)` The object with ID `root` must not be removed from `ObjectsPool`, as per [RTO3b](#RTO3b) + - `(RTO5c10)` After re-establishing the `ObjectsPool` per [RTO5c1](#RTO5c1) and [RTO5c2](#RTO5c2), the client MUST rebuild every `parentReferences` map ([RTLO3f](#RTLO3f)). Specifically: + - `(RTO5c10a)` For each `LiveObject` in the internal `ObjectsPool` ([RTO3](#RTO3)), reset its `parentReferences` to an empty map as defined in [RTLO3f2](#RTLO3f2) + - `(RTO5c10b)` After [RTO5c10a](#RTO5c10a) has completed, for each `LiveMap` in the internal `ObjectsPool`, iterate its `LiveMap#entries` as per [RTLM11](#RTLM11) + - `(RTO5c10b1)` For each iterated entry whose value type is `LiveObject`, call `addParentReference(parent, key)` on the `LiveObject` (per [RTLO4g](#RTLO4g)), passing the iterated `LiveMap` as `parent` and the iterated entry key as `key` - `(RTO5c7)` For each previously existing object that was updated as a result of [RTO5c1a](#RTO5c1a), emit the corresponding stored `LiveObjectUpdate` object from [RTO5c1a2](#RTO5c1a2) - `(RTO5c6)` `ObjectMessages` stored in the `bufferedObjectOperations` list are applied as described in [RTO9](#RTO9), passing `source` as `CHANNEL` - `(RTO5c3)` Clear any stored sync sequence identifiers and cursor values @@ -325,7 +329,7 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLO3e1)` Set to undefined/null when the `LiveObject` is initialized - `(RTLO3f)` protected `parentReferences` `Dict>` - 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 - `(RTLO3f1)` This mapping is keyed by `objectId` for consistency with the rest of the LiveObjects spec, where references between objects are stored as `objectId`s and resolved via the `ObjectsPool` on demand. Implementations may store a direct reference to the parent `LiveMap` instead — for example to avoid an `ObjectsPool` lookup at each step of `getFullPaths` ([RTLO4f](#RTLO4f)) traversal — provided the observable behaviour is unchanged. Such implementations should be aware that this may introduce reference cycles between `LiveMap`s, and must ensure this does not cause memory leaks - - `(RTLO3f2)` TODO: The detailed maintenance rules for `parentReferences` (across `MAP_SET`, `MAP_REMOVE`, `MAP_CLEAR`, `LiveMap` tombstoning, and post-sync rebuild) are to be specified by Sachin in a follow-up; see https://github.com/ably/specification/pull/480 for the in-progress draft + - `(RTLO3f2)` Set to an empty map when the `LiveObject` is initialized - `(RTLO4)` `LiveObject` methods: - `(RTLO4b)` `subscribe` - subscribes a user to data updates on this `LiveObject` instance - `(RTLO4b1)` Requires the `OBJECT_SUBSCRIBE` channel mode to be granted per [RTO2](#RTO2) @@ -362,6 +366,13 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLO4a4)` Get the `siteSerial` value stored for this `LiveObject` in the `siteTimeserials` map using the key `ObjectMessage.siteCode` - `(RTLO4a5)` If the `siteSerial` for this `LiveObject` is null or an empty string, return true - `(RTLO4a6)` If the `siteSerial` for this `LiveObject` is not an empty string, return true if `ObjectMessage.serial` is greater than `siteSerial` when compared lexicographically + - `(RTLO4g)` internal `addParentReference(parent, key)` method - records that the `LiveMap` `parent` references this `LiveObject` at `key` + - `(RTLO4g1)` If `parent` is already present in `parentReferences`, `key` MUST be added to the existing set associated with `parent` + - `(RTLO4g2)` Otherwise, a new entry MUST be inserted into `parentReferences` for `parent` with a set containing only `key` + - `(RTLO4h)` internal `removeParentReference(parent, key)` method - removes the recorded reference from `parent` at `key` + - `(RTLO4h1)` If `parent` is not present in `parentReferences`, the call MUST be a no-op + - `(RTLO4h2)` Otherwise, `key` MUST be removed from the set associated with `parent` + - `(RTLO4h3)` If, as a result of [RTLO4h2](#RTLO4h2), the set associated with `parent` is empty, the `parent` entry MUST be removed from `parentReferences` - `(RTLO4e)` protected `tombstone` - a convenience method used to tombstone this `LiveObject`. The realtime system reserves the right to tombstone an object (i.e. mark it for deletion from the objects pool) by publishing an `OBJECT_DELETE` operation at any time if the object is orphaned (not a descendant of the root object) or remains uninitialized (no `*_CREATE` operation has been received) for an extended period. Only the realtime system may publish an `OBJECT_DELETE` operation; clients must never send it. This method describes the steps the client library must take when it needs to tombstone an object locally. Eventually, tombstoned objects will be garbage collected following the procedure described in [RTO10](#RTO10) - `(RTLO4e1)` Expects the following arguments: - `(RTLO4e1a)` `ObjectMessage` @@ -370,6 +381,10 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLO4e3a)` This clause has been replaced by [RTLO6a](#RTLO6a) - `(RTLO4e3b)` This clause has been replaced by [RTLO6b](#RTLO6b) - `(RTLO4e3b1)` This clause has been replaced by [RTLO6b1](#RTLO6b1) + - `(RTLO4e9)` If the current `LiveObject` is of type `LiveMap`, then before [RTLO4e4](#RTLO4e4) is applied, do following: + - `(RTLO4e9a)` For each iterated `entry` in current `LiveMap`'s private `data`: + - `(RTLO4e9a1)` If `entry.value.data` have `objectId` as a field, retrieve corresponding child `LiveObject` from `ObjectsPool` using given `objectId` + - `(RTLO4e9a2)` If child `LiveObject` exists, call its `removeParentReference(parent, key)` method per [RTLO4h](#RTLO4h), passing current `LiveMap` as `parent` and the iterated `entry.value` as `key` - `(RTLO4e4)` Set the `data` attribute of the `LiveObject` to the value described in [RTLC4b](#RTLC4b) or [RTLM4c](#RTLM4c), depending on the object type - `(RTLO4e5)` Compute a `LiveObjectUpdate` representing the data change resulting from this `LiveObject` being tombstoned, by calculating the diff between the `data` value from before [RTLO4e4](#RTLO4e4) was applied (as `previousData`) and the current `data` value (as `newData`), per [RTLC14](#RTLC14) or [RTLM22](#RTLM22), depending on the object type - `(RTLO4e6)` Set `LiveObjectUpdate.tombstone` to `true` on the object computed in [RTLO4e5](#RTLO4e5) @@ -684,6 +699,9 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLM7h)` If the private `clearTimeserial` is non-null, and the provided `serial` is null or the `clearTimeserial` is lexicographically greater than or equal to `serial`, discard the operation without taking any action. Return a `LiveMapUpdate` object with `LiveMapUpdate.noop` set to `true`, indicating that no update was made to the object - `(RTLM7a)` If an `ObjectsMapEntry` exists in the private `data` for the specified key: - `(RTLM7a1)` If the operation cannot be applied to the existing entry as per [RTLM9](#RTLM9), discard the operation without taking any action. Return a `LiveMapUpdate` object with `LiveMapUpdate.noop` set to `true`, indicating that no update was made to the object + - `(RTLM7a3)` If the current `ObjectsMapEntry` is of type `LiveObject`, before [RTLM7a2](#RTLM7a2e) is applied, the parent reference recorded on existing `ObjectsMapEntry` MUST be removed: + - `(RTLM7a3a)` To check `ObjectsMapEntry` is of type `LiveObject`, validate `ObjectsMapEntry.data` has a `objectId` field, retrieve corresponding `LiveObject` from `ObjectsPool` using given `objectId` + - `(RTLM7a3b)` If `LiveObject` exists, call its `removeParentReference(parent, key)` method per [RTLO4h](#RTLO4h), passing this `LiveMap` as `parent` and the operation's key as `key` - `(RTLM7a2)` Otherwise, apply the operation to the existing entry: - `(RTLM7a2a)` This clause has been replaced by [RTLM7a2e](#RTLM7a2e) as of specification version 6.0.0. - `(RTLM7a2e)` Set `ObjectsMapEntry.data` to the `MapSet.value` @@ -699,6 +717,9 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLM7c1)` This clause has been replaced by [RTLM7g1](#RTLM7g1) as of specification version 6.0.0. - `(RTLM7g)` If `MapSet.value.objectId` is non-empty: - `(RTLM7g1)` Create a new `LiveObject` for this `objectId` in the internal `ObjectsPool` per [RTO6](#RTO6) + - `(RTLM7i)` A parent reference MUST be recorded on the `LiveObject` newly referenced by this entry (if any): + - `(RTLM7i1)` If `MapSet.value.objectId` is not present, no action is required + - `(RTLM7i2)` Otherwise, call `addParentReference(parent, key)` per [RTLO4g](#RTLO4g) on the `LiveObject` in the local `ObjectsPool` with `objectId` equal to `MapSet.value.objectId` (guaranteed to exist per [RTLM7g](#RTLM7g)), passing this `LiveMap` as `parent` and the operation's key as `key` - `(RTLM7f)` Return a `LiveMapUpdate` object with a `LiveMapUpdate.update` map containing the key used in this operation set to `updated`, and `LiveMapUpdate.objectMessage` set to the provided `ObjectMessage` - `(RTLM8)` A `MAP_REMOVE` operation for a key can be applied to a `LiveMap` in the following way: - `(RTLM8c)` Expects the following arguments: @@ -711,6 +732,9 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLM8g)` If the private `clearTimeserial` is non-null, and the provided `serial` is null or the `clearTimeserial` is lexicographically greater than or equal to `serial`, discard the operation without taking any action. Return a `LiveMapUpdate` object with `LiveMapUpdate.noop` set to `true`, indicating that no update was made to the object - `(RTLM8a)` If an `ObjectsMapEntry` exists in the private `data` for the specified key: - `(RTLM8a1)` If the operation cannot be applied to the existing entry as per [RTLM9](#RTLM9), discard the operation without taking any action. Return a `LiveMapUpdate` object with `LiveMapUpdate.noop` set to `true`, indicating that no update was made to the object + - `(RTLM8a3)` If the current `ObjectsMapEntry` is of type `LiveObject`, before [RTLM8a2](#RTLM8a2) is applied, the parent reference recorded on existing `ObjectsMapEntry` MUST be removed: + - `(RTLM8a3a)` To check `ObjectsMapEntry` is of type `LiveObject`, validate `ObjectsMapEntry.data` has a `objectId` field, retrieve corresponding `LiveObject` from `ObjectsPool` using given `objectId` + - `(RTLM8a3b)` If `LiveObject` exists, call its `removeParentReference(parent, key)` method per [RTLO4h](#RTLO4h), passing this `LiveMap` as `parent` and the operation's key as `key` - `(RTLM8a2)` Otherwise, apply the operation to the existing entry: - `(RTLM8a2a)` Set `ObjectsMapEntry.data` to undefined/null - `(RTLM8a2b)` Set `ObjectsMapEntry.timeserial` to the provided `serial` @@ -734,6 +758,9 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(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)` If the current `ObjectsMapEntry` is of type `LiveObject`, the parent reference recorded on existing `ObjectsMapEntry` MUST be removed: + - `(RTLM24e1c1)` To check `ObjectsMapEntry` is of type `LiveObject`, validate `ObjectsMapEntry.data` has a `objectId` field, retrieve corresponding `LiveObject` from `ObjectsPool` using given `objectId` + - `(RTLM24e1c2)` If `LiveObject` exists, call its `removeParentReference(parent, key)` method per [RTLO4h](#RTLO4h), passing this `LiveMap` as `parent` and the iterated entry key as `key` - `(RTLM24e1a)` Remove the entry from the internal `data` map. The entry is not retained as a tombstone. - `(RTLM24e1b)` Record the key for the `LiveMapUpdate` as `removed` - `(RTLM24f)` Return a `LiveMapUpdate` object with `LiveMapUpdate.update` containing each key recorded in [RTLM24e1b](#RTLM24e1b) set to `removed`, and `LiveMapUpdate.objectMessage` set to the provided `ObjectMessage` @@ -1106,6 +1133,8 @@ Types and their properties/methods are public and exposed to users by default. A parentReferences: Dict> // RTLO3f canApplyOperation(ObjectMessage) -> Boolean // RTLO4a tombstone(ObjectMessage) -> LiveObjectUpdate // RTLO4e + addParentReference(parent, key) // RTLO4g + removeParentReference(parent, key) // RTLO4h subscribe((LiveObjectUpdate) ->) -> Subscription // RTLO4b interface LiveObjectUpdate: // RTLO4b4, internal From 4879b50915b947427baec13db76050f867c4e548 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Sun, 24 May 2026 16:05:33 -0300 Subject: [PATCH 30/40] Lowercase RFC 2119 keywords in clauses imported from PR #480 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) --- specifications/objects-features.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/specifications/objects-features.md b/specifications/objects-features.md index 75f1c47e..bd585083 100644 --- a/specifications/objects-features.md +++ b/specifications/objects-features.md @@ -174,7 +174,7 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTO5c1b1c)` This clause has been deleted (redundant to [RTO5f3](#RTO5f3)). - `(RTO5c2)` Remove any objects from the internal `ObjectsPool` for which `objectId`s were not received during the sync sequence - `(RTO5c2a)` The object with ID `root` must not be removed from `ObjectsPool`, as per [RTO3b](#RTO3b) - - `(RTO5c10)` After re-establishing the `ObjectsPool` per [RTO5c1](#RTO5c1) and [RTO5c2](#RTO5c2), the client MUST rebuild every `parentReferences` map ([RTLO3f](#RTLO3f)). Specifically: + - `(RTO5c10)` After re-establishing the `ObjectsPool` per [RTO5c1](#RTO5c1) and [RTO5c2](#RTO5c2), the client must rebuild every `parentReferences` map ([RTLO3f](#RTLO3f)). Specifically: - `(RTO5c10a)` For each `LiveObject` in the internal `ObjectsPool` ([RTO3](#RTO3)), reset its `parentReferences` to an empty map as defined in [RTLO3f2](#RTLO3f2) - `(RTO5c10b)` After [RTO5c10a](#RTO5c10a) has completed, for each `LiveMap` in the internal `ObjectsPool`, iterate its `LiveMap#entries` as per [RTLM11](#RTLM11) - `(RTO5c10b1)` For each iterated entry whose value type is `LiveObject`, call `addParentReference(parent, key)` on the `LiveObject` (per [RTLO4g](#RTLO4g)), passing the iterated `LiveMap` as `parent` and the iterated entry key as `key` @@ -367,12 +367,12 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLO4a5)` If the `siteSerial` for this `LiveObject` is null or an empty string, return true - `(RTLO4a6)` If the `siteSerial` for this `LiveObject` is not an empty string, return true if `ObjectMessage.serial` is greater than `siteSerial` when compared lexicographically - `(RTLO4g)` internal `addParentReference(parent, key)` method - records that the `LiveMap` `parent` references this `LiveObject` at `key` - - `(RTLO4g1)` If `parent` is already present in `parentReferences`, `key` MUST be added to the existing set associated with `parent` - - `(RTLO4g2)` Otherwise, a new entry MUST be inserted into `parentReferences` for `parent` with a set containing only `key` + - `(RTLO4g1)` If `parent` is already present in `parentReferences`, `key` must be added to the existing set associated with `parent` + - `(RTLO4g2)` Otherwise, a new entry must be inserted into `parentReferences` for `parent` with a set containing only `key` - `(RTLO4h)` internal `removeParentReference(parent, key)` method - removes the recorded reference from `parent` at `key` - - `(RTLO4h1)` If `parent` is not present in `parentReferences`, the call MUST be a no-op - - `(RTLO4h2)` Otherwise, `key` MUST be removed from the set associated with `parent` - - `(RTLO4h3)` If, as a result of [RTLO4h2](#RTLO4h2), the set associated with `parent` is empty, the `parent` entry MUST be removed from `parentReferences` + - `(RTLO4h1)` If `parent` is not present in `parentReferences`, the call must be a no-op + - `(RTLO4h2)` Otherwise, `key` must be removed from the set associated with `parent` + - `(RTLO4h3)` If, as a result of [RTLO4h2](#RTLO4h2), the set associated with `parent` is empty, the `parent` entry must be removed from `parentReferences` - `(RTLO4e)` protected `tombstone` - a convenience method used to tombstone this `LiveObject`. The realtime system reserves the right to tombstone an object (i.e. mark it for deletion from the objects pool) by publishing an `OBJECT_DELETE` operation at any time if the object is orphaned (not a descendant of the root object) or remains uninitialized (no `*_CREATE` operation has been received) for an extended period. Only the realtime system may publish an `OBJECT_DELETE` operation; clients must never send it. This method describes the steps the client library must take when it needs to tombstone an object locally. Eventually, tombstoned objects will be garbage collected following the procedure described in [RTO10](#RTO10) - `(RTLO4e1)` Expects the following arguments: - `(RTLO4e1a)` `ObjectMessage` @@ -699,7 +699,7 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLM7h)` If the private `clearTimeserial` is non-null, and the provided `serial` is null or the `clearTimeserial` is lexicographically greater than or equal to `serial`, discard the operation without taking any action. Return a `LiveMapUpdate` object with `LiveMapUpdate.noop` set to `true`, indicating that no update was made to the object - `(RTLM7a)` If an `ObjectsMapEntry` exists in the private `data` for the specified key: - `(RTLM7a1)` If the operation cannot be applied to the existing entry as per [RTLM9](#RTLM9), discard the operation without taking any action. Return a `LiveMapUpdate` object with `LiveMapUpdate.noop` set to `true`, indicating that no update was made to the object - - `(RTLM7a3)` If the current `ObjectsMapEntry` is of type `LiveObject`, before [RTLM7a2](#RTLM7a2e) is applied, the parent reference recorded on existing `ObjectsMapEntry` MUST be removed: + - `(RTLM7a3)` If the current `ObjectsMapEntry` is of type `LiveObject`, before [RTLM7a2](#RTLM7a2e) is applied, the parent reference recorded on existing `ObjectsMapEntry` must be removed: - `(RTLM7a3a)` To check `ObjectsMapEntry` is of type `LiveObject`, validate `ObjectsMapEntry.data` has a `objectId` field, retrieve corresponding `LiveObject` from `ObjectsPool` using given `objectId` - `(RTLM7a3b)` If `LiveObject` exists, call its `removeParentReference(parent, key)` method per [RTLO4h](#RTLO4h), passing this `LiveMap` as `parent` and the operation's key as `key` - `(RTLM7a2)` Otherwise, apply the operation to the existing entry: @@ -717,7 +717,7 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLM7c1)` This clause has been replaced by [RTLM7g1](#RTLM7g1) as of specification version 6.0.0. - `(RTLM7g)` If `MapSet.value.objectId` is non-empty: - `(RTLM7g1)` Create a new `LiveObject` for this `objectId` in the internal `ObjectsPool` per [RTO6](#RTO6) - - `(RTLM7i)` A parent reference MUST be recorded on the `LiveObject` newly referenced by this entry (if any): + - `(RTLM7i)` A parent reference must be recorded on the `LiveObject` newly referenced by this entry (if any): - `(RTLM7i1)` If `MapSet.value.objectId` is not present, no action is required - `(RTLM7i2)` Otherwise, call `addParentReference(parent, key)` per [RTLO4g](#RTLO4g) on the `LiveObject` in the local `ObjectsPool` with `objectId` equal to `MapSet.value.objectId` (guaranteed to exist per [RTLM7g](#RTLM7g)), passing this `LiveMap` as `parent` and the operation's key as `key` - `(RTLM7f)` Return a `LiveMapUpdate` object with a `LiveMapUpdate.update` map containing the key used in this operation set to `updated`, and `LiveMapUpdate.objectMessage` set to the provided `ObjectMessage` @@ -732,7 +732,7 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLM8g)` If the private `clearTimeserial` is non-null, and the provided `serial` is null or the `clearTimeserial` is lexicographically greater than or equal to `serial`, discard the operation without taking any action. Return a `LiveMapUpdate` object with `LiveMapUpdate.noop` set to `true`, indicating that no update was made to the object - `(RTLM8a)` If an `ObjectsMapEntry` exists in the private `data` for the specified key: - `(RTLM8a1)` If the operation cannot be applied to the existing entry as per [RTLM9](#RTLM9), discard the operation without taking any action. Return a `LiveMapUpdate` object with `LiveMapUpdate.noop` set to `true`, indicating that no update was made to the object - - `(RTLM8a3)` If the current `ObjectsMapEntry` is of type `LiveObject`, before [RTLM8a2](#RTLM8a2) is applied, the parent reference recorded on existing `ObjectsMapEntry` MUST be removed: + - `(RTLM8a3)` If the current `ObjectsMapEntry` is of type `LiveObject`, before [RTLM8a2](#RTLM8a2) is applied, the parent reference recorded on existing `ObjectsMapEntry` must be removed: - `(RTLM8a3a)` To check `ObjectsMapEntry` is of type `LiveObject`, validate `ObjectsMapEntry.data` has a `objectId` field, retrieve corresponding `LiveObject` from `ObjectsPool` using given `objectId` - `(RTLM8a3b)` If `LiveObject` exists, call its `removeParentReference(parent, key)` method per [RTLO4h](#RTLO4h), passing this `LiveMap` as `parent` and the operation's key as `key` - `(RTLM8a2)` Otherwise, apply the operation to the existing entry: @@ -758,7 +758,7 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(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)` If the current `ObjectsMapEntry` is of type `LiveObject`, the parent reference recorded on existing `ObjectsMapEntry` MUST be removed: + - `(RTLM24e1c)` If the current `ObjectsMapEntry` is of type `LiveObject`, the parent reference recorded on existing `ObjectsMapEntry` must be removed: - `(RTLM24e1c1)` To check `ObjectsMapEntry` is of type `LiveObject`, validate `ObjectsMapEntry.data` has a `objectId` field, retrieve corresponding `LiveObject` from `ObjectsPool` using given `objectId` - `(RTLM24e1c2)` If `LiveObject` exists, call its `removeParentReference(parent, key)` method per [RTLO4h](#RTLO4h), passing this `LiveMap` as `parent` and the iterated entry key as `key` - `(RTLM24e1a)` Remove the entry from the internal `data` map. The entry is not retained as a tombstone. From 220a9266260dd03ec92709c0207f610b7df2550a Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Sun, 24 May 2026 16:07:16 -0300 Subject: [PATCH 31/40] Add argument types to addParentReference/removeParentReference IDL 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) --- specifications/objects-features.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specifications/objects-features.md b/specifications/objects-features.md index bd585083..28ef8002 100644 --- a/specifications/objects-features.md +++ b/specifications/objects-features.md @@ -1133,8 +1133,8 @@ Types and their properties/methods are public and exposed to users by default. A parentReferences: Dict> // RTLO3f canApplyOperation(ObjectMessage) -> Boolean // RTLO4a tombstone(ObjectMessage) -> LiveObjectUpdate // RTLO4e - addParentReference(parent, key) // RTLO4g - removeParentReference(parent, key) // RTLO4h + addParentReference(LiveMap parent, String key) // RTLO4g + removeParentReference(LiveMap parent, String key) // RTLO4h subscribe((LiveObjectUpdate) ->) -> Subscription // RTLO4b interface LiveObjectUpdate: // RTLO4b4, internal From e6ab048ffbb1b43001b8fe4f952ef0f42acd44a3 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Sun, 24 May 2026 16:08:22 -0300 Subject: [PATCH 32/40] Add getFullPaths to the LiveObject IDL 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) --- specifications/objects-features.md | 1 + 1 file changed, 1 insertion(+) diff --git a/specifications/objects-features.md b/specifications/objects-features.md index 28ef8002..b45d87e2 100644 --- a/specifications/objects-features.md +++ b/specifications/objects-features.md @@ -1133,6 +1133,7 @@ Types and their properties/methods are public and exposed to users by default. A parentReferences: Dict> // RTLO3f canApplyOperation(ObjectMessage) -> Boolean // RTLO4a tombstone(ObjectMessage) -> LiveObjectUpdate // RTLO4e + getFullPaths() -> String[][] // RTLO4f addParentReference(LiveMap parent, String key) // RTLO4g removeParentReference(LiveMap parent, String key) // RTLO4h subscribe((LiveObjectUpdate) ->) -> Subscription // RTLO4b From da7078bfaeb47f05c54f6c6f277d7f27ba767cf7 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Sun, 24 May 2026 16:31:34 -0300 Subject: [PATCH 33/40] Tighten parent-presence wording in RTLO4g/RTLO4h 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) --- specifications/objects-features.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/specifications/objects-features.md b/specifications/objects-features.md index b45d87e2..fd9826e8 100644 --- a/specifications/objects-features.md +++ b/specifications/objects-features.md @@ -367,12 +367,12 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLO4a5)` If the `siteSerial` for this `LiveObject` is null or an empty string, return true - `(RTLO4a6)` If the `siteSerial` for this `LiveObject` is not an empty string, return true if `ObjectMessage.serial` is greater than `siteSerial` when compared lexicographically - `(RTLO4g)` internal `addParentReference(parent, key)` method - records that the `LiveMap` `parent` references this `LiveObject` at `key` - - `(RTLO4g1)` If `parent` is already present in `parentReferences`, `key` must be added to the existing set associated with `parent` - - `(RTLO4g2)` Otherwise, a new entry must be inserted into `parentReferences` for `parent` with a set containing only `key` + - `(RTLO4g1)` If `parentReferences` already contains an entry whose key is `parent.objectId`, add `key` to that entry's set + - `(RTLO4g2)` Otherwise, insert into `parentReferences` a new entry whose key is `parent.objectId` and whose value is a set containing only `key` - `(RTLO4h)` internal `removeParentReference(parent, key)` method - removes the recorded reference from `parent` at `key` - - `(RTLO4h1)` If `parent` is not present in `parentReferences`, the call must be a no-op - - `(RTLO4h2)` Otherwise, `key` must be removed from the set associated with `parent` - - `(RTLO4h3)` If, as a result of [RTLO4h2](#RTLO4h2), the set associated with `parent` is empty, the `parent` entry must be removed from `parentReferences` + - `(RTLO4h1)` If `parentReferences` does not contain an entry whose key is `parent.objectId`, do nothing + - `(RTLO4h2)` Otherwise, remove `key` from that entry's set + - `(RTLO4h3)` If, as a result of [RTLO4h2](#RTLO4h2), that entry's set is empty, remove the entry from `parentReferences` - `(RTLO4e)` protected `tombstone` - a convenience method used to tombstone this `LiveObject`. The realtime system reserves the right to tombstone an object (i.e. mark it for deletion from the objects pool) by publishing an `OBJECT_DELETE` operation at any time if the object is orphaned (not a descendant of the root object) or remains uninitialized (no `*_CREATE` operation has been received) for an extended period. Only the realtime system may publish an `OBJECT_DELETE` operation; clients must never send it. This method describes the steps the client library must take when it needs to tombstone an object locally. Eventually, tombstoned objects will be garbage collected following the procedure described in [RTO10](#RTO10) - `(RTLO4e1)` Expects the following arguments: - `(RTLO4e1a)` `ObjectMessage` From 10388e2b5949734a1fe86fc31dcfebfd22100681 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Sun, 24 May 2026 16:50:14 -0300 Subject: [PATCH 34/40] Simplify RTO5c10 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) --- specifications/objects-features.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/specifications/objects-features.md b/specifications/objects-features.md index fd9826e8..bee9fb0f 100644 --- a/specifications/objects-features.md +++ b/specifications/objects-features.md @@ -174,10 +174,9 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTO5c1b1c)` This clause has been deleted (redundant to [RTO5f3](#RTO5f3)). - `(RTO5c2)` Remove any objects from the internal `ObjectsPool` for which `objectId`s were not received during the sync sequence - `(RTO5c2a)` The object with ID `root` must not be removed from `ObjectsPool`, as per [RTO3b](#RTO3b) - - `(RTO5c10)` After re-establishing the `ObjectsPool` per [RTO5c1](#RTO5c1) and [RTO5c2](#RTO5c2), the client must rebuild every `parentReferences` map ([RTLO3f](#RTLO3f)). Specifically: - - `(RTO5c10a)` For each `LiveObject` in the internal `ObjectsPool` ([RTO3](#RTO3)), reset its `parentReferences` to an empty map as defined in [RTLO3f2](#RTLO3f2) - - `(RTO5c10b)` After [RTO5c10a](#RTO5c10a) has completed, for each `LiveMap` in the internal `ObjectsPool`, iterate its `LiveMap#entries` as per [RTLM11](#RTLM11) - - `(RTO5c10b1)` For each iterated entry whose value type is `LiveObject`, call `addParentReference(parent, key)` on the `LiveObject` (per [RTLO4g](#RTLO4g)), passing the iterated `LiveMap` as `parent` and the iterated entry key as `key` + - `(RTO5c10)` Rebuild every `parentReferences` map ([RTLO3f](#RTLO3f)): + - `(RTO5c10a)` For each `LiveObject` in the internal `ObjectsPool`, reset its `parentReferences` to the initial value defined in [RTLO3f2](#RTLO3f2) + - `(RTO5c10b)` For each `LiveMap` in the internal `ObjectsPool`, iterate its `LiveMap#entries` ([RTLM11](#RTLM11)); for each entry whose value is a `LiveObject`, call `addParentReference(parent, key)` on that `LiveObject` per [RTLO4g](#RTLO4g), passing the `LiveMap` as `parent` and the entry's key as `key` - `(RTO5c7)` For each previously existing object that was updated as a result of [RTO5c1a](#RTO5c1a), emit the corresponding stored `LiveObjectUpdate` object from [RTO5c1a2](#RTO5c1a2) - `(RTO5c6)` `ObjectMessages` stored in the `bufferedObjectOperations` list are applied as described in [RTO9](#RTO9), passing `source` as `CHANNEL` - `(RTO5c3)` Clear any stored sync sequence identifiers and cursor values From ff16eedde642019a17b109f242257593d4f05e9c Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Sun, 24 May 2026 17:02:00 -0300 Subject: [PATCH 35/40] Rework RTLO4e9 tombstone children walk 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) --- specifications/objects-features.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/specifications/objects-features.md b/specifications/objects-features.md index bee9fb0f..329de239 100644 --- a/specifications/objects-features.md +++ b/specifications/objects-features.md @@ -380,11 +380,10 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLO4e3a)` This clause has been replaced by [RTLO6a](#RTLO6a) - `(RTLO4e3b)` This clause has been replaced by [RTLO6b](#RTLO6b) - `(RTLO4e3b1)` This clause has been replaced by [RTLO6b1](#RTLO6b1) - - `(RTLO4e9)` If the current `LiveObject` is of type `LiveMap`, then before [RTLO4e4](#RTLO4e4) is applied, do following: - - `(RTLO4e9a)` For each iterated `entry` in current `LiveMap`'s private `data`: - - `(RTLO4e9a1)` If `entry.value.data` have `objectId` as a field, retrieve corresponding child `LiveObject` from `ObjectsPool` using given `objectId` - - `(RTLO4e9a2)` If child `LiveObject` exists, call its `removeParentReference(parent, key)` method per [RTLO4h](#RTLO4h), passing current `LiveMap` as `parent` and the iterated `entry.value` as `key` - `(RTLO4e4)` Set the `data` attribute of the `LiveObject` to the value described in [RTLC4b](#RTLC4b) or [RTLM4c](#RTLM4c), depending on the object type + - `(RTLO4e9)` If the `LiveObject` is a `LiveMap`, then for each `ObjectsMapEntry` in the previous value of `LiveMap.data` (that is, the value before resetting it in [`RTLO4e4`](#RTLO4e4)): + - `(RTLO4e9a)` If `ObjectsMapEntry.data.objectId` is populated, fetch the object with this `objectId` from the `ObjectsPool` + - `(RTLO4e9b)` If the [`RTLO4e9a`](#RTLO4e9a) fetch returned an object, then call its [`RTLO4h`](#RTLO4h) `removeParentReference(parent, key)` method, passing the `LiveMap` as `parent` and the iterated entry's key as `key` - `(RTLO4e5)` Compute a `LiveObjectUpdate` representing the data change resulting from this `LiveObject` being tombstoned, by calculating the diff between the `data` value from before [RTLO4e4](#RTLO4e4) was applied (as `previousData`) and the current `data` value (as `newData`), per [RTLC14](#RTLC14) or [RTLM22](#RTLM22), depending on the object type - `(RTLO4e6)` Set `LiveObjectUpdate.tombstone` to `true` on the object computed in [RTLO4e5](#RTLO4e5) - `(RTLO4e7)` Set `LiveObjectUpdate.objectMessage` on the object computed in [RTLO4e5](#RTLO4e5) to the `ObjectMessage` argument From 42e45ba394b557e29b57bc2f348b444c7363bbae Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Sun, 24 May 2026 17:09:57 -0300 Subject: [PATCH 36/40] Tidy RTLM7 parent-reference clauses 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) --- specifications/objects-features.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/specifications/objects-features.md b/specifications/objects-features.md index 329de239..30a41944 100644 --- a/specifications/objects-features.md +++ b/specifications/objects-features.md @@ -699,7 +699,7 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLM7a1)` If the operation cannot be applied to the existing entry as per [RTLM9](#RTLM9), discard the operation without taking any action. Return a `LiveMapUpdate` object with `LiveMapUpdate.noop` set to `true`, indicating that no update was made to the object - `(RTLM7a3)` If the current `ObjectsMapEntry` is of type `LiveObject`, before [RTLM7a2](#RTLM7a2e) is applied, the parent reference recorded on existing `ObjectsMapEntry` must be removed: - `(RTLM7a3a)` To check `ObjectsMapEntry` is of type `LiveObject`, validate `ObjectsMapEntry.data` has a `objectId` field, retrieve corresponding `LiveObject` from `ObjectsPool` using given `objectId` - - `(RTLM7a3b)` If `LiveObject` exists, call its `removeParentReference(parent, key)` method per [RTLO4h](#RTLO4h), passing this `LiveMap` as `parent` and the operation's key as `key` + - `(RTLM7a3b)` If `LiveObject` exists, call its `removeParentReference(parent, key)` method per [RTLO4h](#RTLO4h), passing this `LiveMap` as `parent` and the specified key as `key` - `(RTLM7a2)` Otherwise, apply the operation to the existing entry: - `(RTLM7a2a)` This clause has been replaced by [RTLM7a2e](#RTLM7a2e) as of specification version 6.0.0. - `(RTLM7a2e)` Set `ObjectsMapEntry.data` to the `MapSet.value` @@ -715,9 +715,7 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLM7c1)` This clause has been replaced by [RTLM7g1](#RTLM7g1) as of specification version 6.0.0. - `(RTLM7g)` If `MapSet.value.objectId` is non-empty: - `(RTLM7g1)` Create a new `LiveObject` for this `objectId` in the internal `ObjectsPool` per [RTO6](#RTO6) - - `(RTLM7i)` A parent reference must be recorded on the `LiveObject` newly referenced by this entry (if any): - - `(RTLM7i1)` If `MapSet.value.objectId` is not present, no action is required - - `(RTLM7i2)` Otherwise, call `addParentReference(parent, key)` per [RTLO4g](#RTLO4g) on the `LiveObject` in the local `ObjectsPool` with `objectId` equal to `MapSet.value.objectId` (guaranteed to exist per [RTLM7g](#RTLM7g)), passing this `LiveMap` as `parent` and the operation's key as `key` + - `(RTLM7g2)` Call `addParentReference(parent, key)` per [RTLO4g](#RTLO4g) on the `LiveObject` from [RTLM7g1](#RTLM7g1), passing this `LiveMap` as `parent` and the specified key as `key` - `(RTLM7f)` Return a `LiveMapUpdate` object with a `LiveMapUpdate.update` map containing the key used in this operation set to `updated`, and `LiveMapUpdate.objectMessage` set to the provided `ObjectMessage` - `(RTLM8)` A `MAP_REMOVE` operation for a key can be applied to a `LiveMap` in the following way: - `(RTLM8c)` Expects the following arguments: @@ -732,7 +730,7 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLM8a1)` If the operation cannot be applied to the existing entry as per [RTLM9](#RTLM9), discard the operation without taking any action. Return a `LiveMapUpdate` object with `LiveMapUpdate.noop` set to `true`, indicating that no update was made to the object - `(RTLM8a3)` If the current `ObjectsMapEntry` is of type `LiveObject`, before [RTLM8a2](#RTLM8a2) is applied, the parent reference recorded on existing `ObjectsMapEntry` must be removed: - `(RTLM8a3a)` To check `ObjectsMapEntry` is of type `LiveObject`, validate `ObjectsMapEntry.data` has a `objectId` field, retrieve corresponding `LiveObject` from `ObjectsPool` using given `objectId` - - `(RTLM8a3b)` If `LiveObject` exists, call its `removeParentReference(parent, key)` method per [RTLO4h](#RTLO4h), passing this `LiveMap` as `parent` and the operation's key as `key` + - `(RTLM8a3b)` If `LiveObject` exists, call its `removeParentReference(parent, key)` method per [RTLO4h](#RTLO4h), passing this `LiveMap` as `parent` and the specified key as `key` - `(RTLM8a2)` Otherwise, apply the operation to the existing entry: - `(RTLM8a2a)` Set `ObjectsMapEntry.data` to undefined/null - `(RTLM8a2b)` Set `ObjectsMapEntry.timeserial` to the provided `serial` From 49a56be39a66031a77284ccf74fe5c69e17850eb Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Sun, 24 May 2026 17:21:20 -0300 Subject: [PATCH 37/40] Tighten parent-ref cleanup clauses to a single parallel form 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) --- specifications/objects-features.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/specifications/objects-features.md b/specifications/objects-features.md index 30a41944..4d945980 100644 --- a/specifications/objects-features.md +++ b/specifications/objects-features.md @@ -380,10 +380,10 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLO4e3a)` This clause has been replaced by [RTLO6a](#RTLO6a) - `(RTLO4e3b)` This clause has been replaced by [RTLO6b](#RTLO6b) - `(RTLO4e3b1)` This clause has been replaced by [RTLO6b1](#RTLO6b1) - - `(RTLO4e4)` Set the `data` attribute of the `LiveObject` to the value described in [RTLC4b](#RTLC4b) or [RTLM4c](#RTLM4c), depending on the object type - - `(RTLO4e9)` If the `LiveObject` is a `LiveMap`, then for each `ObjectsMapEntry` in the previous value of `LiveMap.data` (that is, the value before resetting it in [`RTLO4e4`](#RTLO4e4)): + - `(RTLO4e9)` If the `LiveObject` is a `LiveMap`, then before [RTLO4e4](#RTLO4e4) is applied, for each `ObjectsMapEntry` in `LiveMap.data`: - `(RTLO4e9a)` If `ObjectsMapEntry.data.objectId` is populated, fetch the object with this `objectId` from the `ObjectsPool` - - `(RTLO4e9b)` If the [`RTLO4e9a`](#RTLO4e9a) fetch returned an object, then call its [`RTLO4h`](#RTLO4h) `removeParentReference(parent, key)` method, passing the `LiveMap` as `parent` and the iterated entry's key as `key` + - `(RTLO4e9b)` If the [`RTLO4e9a`](#RTLO4e9a) fetch returned an object, call its [`RTLO4h`](#RTLO4h) `removeParentReference(parent, key)` method, passing this `LiveMap` as `parent` and the iterated entry's key as `key` + - `(RTLO4e4)` Set the `data` attribute of the `LiveObject` to the value described in [RTLC4b](#RTLC4b) or [RTLM4c](#RTLM4c), depending on the object type - `(RTLO4e5)` Compute a `LiveObjectUpdate` representing the data change resulting from this `LiveObject` being tombstoned, by calculating the diff between the `data` value from before [RTLO4e4](#RTLO4e4) was applied (as `previousData`) and the current `data` value (as `newData`), per [RTLC14](#RTLC14) or [RTLM22](#RTLM22), depending on the object type - `(RTLO4e6)` Set `LiveObjectUpdate.tombstone` to `true` on the object computed in [RTLO4e5](#RTLO4e5) - `(RTLO4e7)` Set `LiveObjectUpdate.objectMessage` on the object computed in [RTLO4e5](#RTLO4e5) to the `ObjectMessage` argument @@ -697,9 +697,9 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLM7h)` If the private `clearTimeserial` is non-null, and the provided `serial` is null or the `clearTimeserial` is lexicographically greater than or equal to `serial`, discard the operation without taking any action. Return a `LiveMapUpdate` object with `LiveMapUpdate.noop` set to `true`, indicating that no update was made to the object - `(RTLM7a)` If an `ObjectsMapEntry` exists in the private `data` for the specified key: - `(RTLM7a1)` If the operation cannot be applied to the existing entry as per [RTLM9](#RTLM9), discard the operation without taking any action. Return a `LiveMapUpdate` object with `LiveMapUpdate.noop` set to `true`, indicating that no update was made to the object - - `(RTLM7a3)` If the current `ObjectsMapEntry` is of type `LiveObject`, before [RTLM7a2](#RTLM7a2e) is applied, the parent reference recorded on existing `ObjectsMapEntry` must be removed: - - `(RTLM7a3a)` To check `ObjectsMapEntry` is of type `LiveObject`, validate `ObjectsMapEntry.data` has a `objectId` field, retrieve corresponding `LiveObject` from `ObjectsPool` using given `objectId` - - `(RTLM7a3b)` If `LiveObject` exists, call its `removeParentReference(parent, key)` method per [RTLO4h](#RTLO4h), passing this `LiveMap` as `parent` and the specified key as `key` + - `(RTLM7a3)` Before [RTLM7a2](#RTLM7a2e) is applied: + - `(RTLM7a3a)` If `ObjectsMapEntry.data.objectId` is populated, fetch the object with this `objectId` from the `ObjectsPool` + - `(RTLM7a3b)` If the [`RTLM7a3a`](#RTLM7a3a) fetch returned an object, call its [`RTLO4h`](#RTLO4h) `removeParentReference(parent, key)` method, passing this `LiveMap` as `parent` and the specified key as `key` - `(RTLM7a2)` Otherwise, apply the operation to the existing entry: - `(RTLM7a2a)` This clause has been replaced by [RTLM7a2e](#RTLM7a2e) as of specification version 6.0.0. - `(RTLM7a2e)` Set `ObjectsMapEntry.data` to the `MapSet.value` @@ -728,9 +728,9 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLM8g)` If the private `clearTimeserial` is non-null, and the provided `serial` is null or the `clearTimeserial` is lexicographically greater than or equal to `serial`, discard the operation without taking any action. Return a `LiveMapUpdate` object with `LiveMapUpdate.noop` set to `true`, indicating that no update was made to the object - `(RTLM8a)` If an `ObjectsMapEntry` exists in the private `data` for the specified key: - `(RTLM8a1)` If the operation cannot be applied to the existing entry as per [RTLM9](#RTLM9), discard the operation without taking any action. Return a `LiveMapUpdate` object with `LiveMapUpdate.noop` set to `true`, indicating that no update was made to the object - - `(RTLM8a3)` If the current `ObjectsMapEntry` is of type `LiveObject`, before [RTLM8a2](#RTLM8a2) is applied, the parent reference recorded on existing `ObjectsMapEntry` must be removed: - - `(RTLM8a3a)` To check `ObjectsMapEntry` is of type `LiveObject`, validate `ObjectsMapEntry.data` has a `objectId` field, retrieve corresponding `LiveObject` from `ObjectsPool` using given `objectId` - - `(RTLM8a3b)` If `LiveObject` exists, call its `removeParentReference(parent, key)` method per [RTLO4h](#RTLO4h), passing this `LiveMap` as `parent` and the specified key as `key` + - `(RTLM8a3)` Before [RTLM8a2](#RTLM8a2) is applied: + - `(RTLM8a3a)` If `ObjectsMapEntry.data.objectId` is populated, fetch the object with this `objectId` from the `ObjectsPool` + - `(RTLM8a3b)` If the [`RTLM8a3a`](#RTLM8a3a) fetch returned an object, call its [`RTLO4h`](#RTLO4h) `removeParentReference(parent, key)` method, passing this `LiveMap` as `parent` and the specified key as `key` - `(RTLM8a2)` Otherwise, apply the operation to the existing entry: - `(RTLM8a2a)` Set `ObjectsMapEntry.data` to undefined/null - `(RTLM8a2b)` Set `ObjectsMapEntry.timeserial` to the provided `serial` @@ -754,9 +754,9 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(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)` If the current `ObjectsMapEntry` is of type `LiveObject`, the parent reference recorded on existing `ObjectsMapEntry` must be removed: - - `(RTLM24e1c1)` To check `ObjectsMapEntry` is of type `LiveObject`, validate `ObjectsMapEntry.data` has a `objectId` field, retrieve corresponding `LiveObject` from `ObjectsPool` using given `objectId` - - `(RTLM24e1c2)` If `LiveObject` exists, call its `removeParentReference(parent, key)` method per [RTLO4h](#RTLO4h), passing this `LiveMap` as `parent` and the iterated entry key as `key` + - `(RTLM24e1c)` Before [RTLM24e1a](#RTLM24e1a) is applied: + - `(RTLM24e1c1)` If `ObjectsMapEntry.data.objectId` is populated, fetch the object with this `objectId` from the `ObjectsPool` + - `(RTLM24e1c2)` If the [`RTLM24e1c1`](#RTLM24e1c1) fetch returned an object, call its [`RTLO4h`](#RTLO4h) `removeParentReference(parent, key)` method, passing this `LiveMap` as `parent` and the iterated entry's key as `key` - `(RTLM24e1a)` Remove the entry from the internal `data` map. The entry is not retained as a tombstone. - `(RTLM24e1b)` Record the key for the `LiveMapUpdate` as `removed` - `(RTLM24f)` Return a `LiveMapUpdate` object with `LiveMapUpdate.update` containing each key recorded in [RTLM24e1b](#RTLM24e1b) set to `removed`, and `LiveMapUpdate.objectMessage` set to the provided `ObjectMessage` From 72e2377c228121c49e012830f0b56b0a33ffa2e9 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Sun, 24 May 2026 17:29:12 -0300 Subject: [PATCH 38/40] Restructure parent-ref cleanup clauses around the data modification 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) --- specifications/objects-features.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/specifications/objects-features.md b/specifications/objects-features.md index 4d945980..4d4c6102 100644 --- a/specifications/objects-features.md +++ b/specifications/objects-features.md @@ -380,7 +380,7 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLO4e3a)` This clause has been replaced by [RTLO6a](#RTLO6a) - `(RTLO4e3b)` This clause has been replaced by [RTLO6b](#RTLO6b) - `(RTLO4e3b1)` This clause has been replaced by [RTLO6b1](#RTLO6b1) - - `(RTLO4e9)` If the `LiveObject` is a `LiveMap`, then before [RTLO4e4](#RTLO4e4) is applied, for each `ObjectsMapEntry` in `LiveMap.data`: + - `(RTLO4e9)` If the `LiveObject` is a `LiveMap`, then before `LiveMap.data` is reset per [RTLO4e4](#RTLO4e4), for each `ObjectsMapEntry` in `LiveMap.data`: - `(RTLO4e9a)` If `ObjectsMapEntry.data.objectId` is populated, fetch the object with this `objectId` from the `ObjectsPool` - `(RTLO4e9b)` If the [`RTLO4e9a`](#RTLO4e9a) fetch returned an object, call its [`RTLO4h`](#RTLO4h) `removeParentReference(parent, key)` method, passing this `LiveMap` as `parent` and the iterated entry's key as `key` - `(RTLO4e4)` Set the `data` attribute of the `LiveObject` to the value described in [RTLC4b](#RTLC4b) or [RTLM4c](#RTLM4c), depending on the object type @@ -697,10 +697,10 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLM7h)` If the private `clearTimeserial` is non-null, and the provided `serial` is null or the `clearTimeserial` is lexicographically greater than or equal to `serial`, discard the operation without taking any action. Return a `LiveMapUpdate` object with `LiveMapUpdate.noop` set to `true`, indicating that no update was made to the object - `(RTLM7a)` If an `ObjectsMapEntry` exists in the private `data` for the specified key: - `(RTLM7a1)` If the operation cannot be applied to the existing entry as per [RTLM9](#RTLM9), discard the operation without taking any action. Return a `LiveMapUpdate` object with `LiveMapUpdate.noop` set to `true`, indicating that no update was made to the object - - `(RTLM7a3)` Before [RTLM7a2](#RTLM7a2e) is applied: - - `(RTLM7a3a)` If `ObjectsMapEntry.data.objectId` is populated, fetch the object with this `objectId` from the `ObjectsPool` - - `(RTLM7a3b)` If the [`RTLM7a3a`](#RTLM7a3a) fetch returned an object, call its [`RTLO4h`](#RTLO4h) `removeParentReference(parent, key)` method, passing this `LiveMap` as `parent` and the specified key as `key` - `(RTLM7a2)` Otherwise, apply the operation to the existing entry: + - `(RTLM7a3)` Before `ObjectsMapEntry.data` is set per [RTLM7a2e](#RTLM7a2e): + - `(RTLM7a3a)` If `ObjectsMapEntry.data.objectId` is populated, fetch the object with this `objectId` from the `ObjectsPool` + - `(RTLM7a3b)` If the [`RTLM7a3a`](#RTLM7a3a) fetch returned an object, call its [`RTLO4h`](#RTLO4h) `removeParentReference(parent, key)` method, passing this `LiveMap` as `parent` and the specified key as `key` - `(RTLM7a2a)` This clause has been replaced by [RTLM7a2e](#RTLM7a2e) as of specification version 6.0.0. - `(RTLM7a2e)` Set `ObjectsMapEntry.data` to the `MapSet.value` - `(RTLM7a2b)` Set `ObjectsMapEntry.timeserial` to the provided `serial` @@ -728,10 +728,10 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLM8g)` If the private `clearTimeserial` is non-null, and the provided `serial` is null or the `clearTimeserial` is lexicographically greater than or equal to `serial`, discard the operation without taking any action. Return a `LiveMapUpdate` object with `LiveMapUpdate.noop` set to `true`, indicating that no update was made to the object - `(RTLM8a)` If an `ObjectsMapEntry` exists in the private `data` for the specified key: - `(RTLM8a1)` If the operation cannot be applied to the existing entry as per [RTLM9](#RTLM9), discard the operation without taking any action. Return a `LiveMapUpdate` object with `LiveMapUpdate.noop` set to `true`, indicating that no update was made to the object - - `(RTLM8a3)` Before [RTLM8a2](#RTLM8a2) is applied: - - `(RTLM8a3a)` If `ObjectsMapEntry.data.objectId` is populated, fetch the object with this `objectId` from the `ObjectsPool` - - `(RTLM8a3b)` If the [`RTLM8a3a`](#RTLM8a3a) fetch returned an object, call its [`RTLO4h`](#RTLO4h) `removeParentReference(parent, key)` method, passing this `LiveMap` as `parent` and the specified key as `key` - `(RTLM8a2)` Otherwise, apply the operation to the existing entry: + - `(RTLM8a3)` Before `ObjectsMapEntry.data` is cleared per [RTLM8a2a](#RTLM8a2a): + - `(RTLM8a3a)` If `ObjectsMapEntry.data.objectId` is populated, fetch the object with this `objectId` from the `ObjectsPool` + - `(RTLM8a3b)` If the [`RTLM8a3a`](#RTLM8a3a) fetch returned an object, call its [`RTLO4h`](#RTLO4h) `removeParentReference(parent, key)` method, passing this `LiveMap` as `parent` and the specified key as `key` - `(RTLM8a2a)` Set `ObjectsMapEntry.data` to undefined/null - `(RTLM8a2b)` Set `ObjectsMapEntry.timeserial` to the provided `serial` - `(RTLM8a2c)` Set `ObjectsMapEntry.tombstone` to `true` @@ -754,7 +754,7 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(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: + - `(RTLM24e1c)` Before the `ObjectsMapEntry` is removed per [RTLM24e1a](#RTLM24e1a): - `(RTLM24e1c1)` If `ObjectsMapEntry.data.objectId` is populated, fetch the object with this `objectId` from the `ObjectsPool` - `(RTLM24e1c2)` If the [`RTLM24e1c1`](#RTLM24e1c1) fetch returned an object, call its [`RTLO4h`](#RTLO4h) `removeParentReference(parent, key)` method, passing this `LiveMap` as `parent` and the iterated entry's key as `key` - `(RTLM24e1a)` Remove the entry from the internal `data` map. The entry is not retained as a tombstone. From c0938f3d290acd42d4c1a5fb862ee12c36c33d10 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Sun, 24 May 2026 18:11:37 -0300 Subject: [PATCH 39/40] Rework RTLO4f around an explicit graph-theoretic definition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- specifications/objects-features.md | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/specifications/objects-features.md b/specifications/objects-features.md index 4d4c6102..dad8552a 100644 --- a/specifications/objects-features.md +++ b/specifications/objects-features.md @@ -388,14 +388,11 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLO4e6)` Set `LiveObjectUpdate.tombstone` to `true` on the object computed in [RTLO4e5](#RTLO4e5) - `(RTLO4e7)` Set `LiveObjectUpdate.objectMessage` on the object computed in [RTLO4e5](#RTLO4e5) to the `ObjectMessage` argument - `(RTLO4e8)` Return the `LiveObjectUpdate` object computed in [RTLO4e5](#RTLO4e5) - - `(RTLO4f)` 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`. - - `(RTLO4f1)` `getFullPaths` MUST be implemented as an enumeration of all *simple paths* from this `LiveObject` to the root `LiveMap` over the inverse of the `parentReferences` graph (i.e. walking child → parent). A *simple path* is a path along which no `LiveObject` appears more than once. This is the standard graph problem, typically solved by a depth-first traversal with path-local backtracking equivalent to NetworkX's `all_simple_paths`. Implementation should choose iterative DFS with explicit stack (easier to read and debugging). - - `(RTLO4f2)` If this `LiveObject` is the root `LiveMap` (objectId `root`), the returned list MUST contain exactly one path, and that path MUST be empty (zero key segments). This makes the root reachable from itself via the empty key sequence - - `(RTLO4f3)` If this `LiveObject` is not the root `LiveMap` and has no entries in its `parentReferences` at the time of the call (e.g. orphaned, or not yet reachable from root), the returned list MUST be empty - - `(RTLO4f4)` While traversing paths, suppress cyclic paths whenever a sibling branch had already revisited the same node. Reference behaviour on cyclic graphs is given by NetworkX's `all_simple_paths`, which implementations MAY consult for worked examples - - `(RTLO4f5)` When a single parent `LiveMap` references this `LiveObject` at multiple keys, the returned list MUST contain one distinct path per such key, each ending at the corresponding key - - `(RTLO4f6)` When this `LiveObject` is reachable via multiple distinct ancestor paths (either because it has multiple parents in `parentReferences`, or because any ancestor on the way to root itself has multiple paths to root), the returned list MUST contain one path per distinct ancestor path - - `(RTLO4f7)` The order of paths in the returned list is not mandatory. Implementations MAY return paths in any order; callers requiring a stable order MUST sort the result themselves + - `(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 `[]` + - `(RTLO4f3)` Each such key-path appears in the returned list exactly once. The order is unspecified + - `(RTLO4f4)` (non-normative) A typical approach is iterative DFS with an explicit stack: walk upward from this `LiveObject` toward `root` via `parentReferences`, collecting keys along the way and skipping branches that would revisit a node - `(RTLO5)` An `OBJECT_DELETE` operation can be applied to a `LiveObject` in the following way: - `(RTLO5a)` Expects the following arguments: - `(RTLO5a1)` `ObjectMessage` From 37b336c3bcc6f876a15d15c4ada15cd7c73a940c Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Sun, 24 May 2026 22:06:30 -0300 Subject: [PATCH 40/40] Centralise PathObject/Instance access checks in RTO25/RTO26 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] https://github.com/ably/specification/pull/477#discussion_r3281612167 Co-Authored-By: Claude Opus 4.7 (1M context) --- specifications/objects-features.md | 265 ++++++++++++++++------------- 1 file changed, 149 insertions(+), 116 deletions(-) diff --git a/specifications/objects-features.md b/specifications/objects-features.md index dad8552a..1de79f00 100644 --- a/specifications/objects-features.md +++ b/specifications/objects-features.md @@ -127,6 +127,13 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTO2b)` Otherwise, a best-effort attempt is made, and the channel mode is checked against the set of channel modes requested by the user per [TB2d](../features#TB2d) : - `(RTO2b1)` If the channel mode is in the set, the operation is allowed - `(RTO2b2)` If the channel mode is missing, unless otherwise specified by the operation, the library should throw an `ErrorInfo` error with `statusCode` 400 and `code` 40024, indicating that the operation cannot be performed without the required channel mode +- `(RTO25)` Certain object operations may require the *access API preconditions* to be satisfied in order to be performed. If the access API preconditions are required by an operation, then before doing anything else: + - `(RTO25a)` Require the `OBJECT_SUBSCRIBE` channel mode to be granted per [RTO2](#RTO2) + - `(RTO25b)` If the channel is in the `DETACHED` or `FAILED` state, throw an `ErrorInfo` error with `statusCode` 400 and `code` 90001 +- `(RTO26)` Certain object operations may require the *write API preconditions* to be satisfied in order to be performed. If the write API preconditions are required by an operation, then before doing anything else: + - `(RTO26a)` Require the `OBJECT_PUBLISH` channel mode to be granted per [RTO2](#RTO2) + - `(RTO26b)` If the channel is in the `DETACHED`, `FAILED`, or `SUSPENDED` state, throw an `ErrorInfo` error with `statusCode` 400 and `code` 90001 + - `(RTO26c)` If [`echoMessages`](../features#TO3h) client option is `false`, throw an `ErrorInfo` error with `statusCode` 400 and `code` 40000, indicating that `echoMessages` must be enabled for this operation - `(RTO3)` An internal `ObjectsPool` should be used to maintain the list of objects present on a channel - `(RTO3a)` `ObjectsPool` is a `Dict` - a map of `LiveObject`s keyed by [`objectId`](../features#OST2a) string - `(RTO3b)` It must always contain a `LiveMap` object with id `root` @@ -331,8 +338,8 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLO3f2)` Set to an empty map when the `LiveObject` is initialized - `(RTLO4)` `LiveObject` methods: - `(RTLO4b)` `subscribe` - subscribes a user to data updates on this `LiveObject` instance - - `(RTLO4b1)` Requires the `OBJECT_SUBSCRIBE` channel mode to be granted per [RTO2](#RTO2) - - `(RTLO4b2)` If the channel is in the `DETACHED` or `FAILED` state, the library should throw an `ErrorInfo` error with `statusCode` 400 and `code` 90001 + - `(RTLO4b1)` This clause has been replaced by [RTO25](#RTO25); the access API preconditions are now checked by callers + - `(RTLO4b2)` This clause has been replaced by [RTO25](#RTO25); the access API preconditions are now checked by callers - `(RTLO4b3)` A user may provide a listener to subscribe to data updates on this `LiveObject` instance - `(RTLO4b4)` An update to `LiveObject` data is communicated by internally emitting a `LiveObjectUpdate` object for this `LiveObject`, or in any other platform-appropriate manner: - `(RTLO4b4a)` `LiveObjectUpdate.update` contains the specific information about what was changed on the object. The exact type depends on the object type @@ -416,15 +423,15 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLC11b)` `LiveCounterUpdate.update` has the following properties: - `(RTLC11b1)` `amount` number - the value by which the counter was incremented or decremented - `(RTLC5)` `LiveCounter#value` function: - - `(RTLC5a)` Requires the `OBJECT_SUBSCRIBE` channel mode to be granted per [RTO2](#RTO2) - - `(RTLC5b)` If the channel is in the `DETACHED` or `FAILED` state, the library should throw an `ErrorInfo` error with `statusCode` 400 and `code` 90001 + - `(RTLC5a)` This clause has been replaced by [RTO25](#RTO25); the access API preconditions are now checked by callers + - `(RTLC5b)` This clause has been replaced by [RTO25](#RTO25); the access API preconditions are now checked by callers - `(RTLC5c)` Returns the current `data` value - `(RTLC12)` `LiveCounter#increment` function: - `(RTLC12a)` Expects the following arguments: - `(RTLC12a1)` `amount` `Number` - the amount by which to increment the counter value - - `(RTLC12b)` Requires the `OBJECT_PUBLISH` channel mode to be granted per [RTO2](#RTO2) - - `(RTLC12c)` If the channel is in the `DETACHED`, `FAILED` or `SUSPENDED` state, the library should throw an `ErrorInfo` error with `statusCode` 400 and `code` 90001 - - `(RTLC12d)` If [`echoMessages`](../features#TO3h) client option is `false`, the library should throw an `ErrorInfo` error with `statusCode` 400 and `code` 40000, indicating that `echoMessages` must be enabled for this operation + - `(RTLC12b)` This clause has been replaced by [RTO26](#RTO26); the write API preconditions are now checked by callers + - `(RTLC12c)` This clause has been replaced by [RTO26](#RTO26); the write API preconditions are now checked by callers + - `(RTLC12d)` This clause has been replaced by [RTO26](#RTO26); the write API preconditions are now checked by callers - `(RTLC12e)` Creates an `ObjectMessage` for a `COUNTER_INC` action in the following way: - `(RTLC12e1)` If `amount` is null, not of type `Number`, not a finite number, or omitted, the library should throw an `ErrorInfo` error with `statusCode` 400 and `code` 40003, indicating that `amount` must be a valid number - `(RTLC12e2)` Set `ObjectMessage.operation.action` to `ObjectOperationAction.COUNTER_INC` @@ -530,8 +537,8 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLM18b)` `LiveMapUpdate.update` is of type `Dict` - a map of `LiveMap` keys that were either updated or removed, with the corresponding value indicating the type of change for each key - `(RTLM5)` `LiveMap#get` function: - `(RTLM5a)` Accepts a key of type String - - `(RTLM5b)` Requires the `OBJECT_SUBSCRIBE` channel mode to be granted per [RTO2](#RTO2) - - `(RTLM5c)` If the channel is in the `DETACHED` or `FAILED` state, the library should throw an `ErrorInfo` error with `statusCode` 400 and `code` 90001 + - `(RTLM5b)` This clause has been replaced by [RTO25](#RTO25); the access API preconditions are now checked by callers + - `(RTLM5c)` This clause has been replaced by [RTO25](#RTO25); the access API preconditions are now checked by callers - `(RTLM5e)` If `LiveMap.isTombstone` is `true`, return undefined/null - `(RTLM5d)` Returns the value from the current `data` at the specified key, as follows: - `(RTLM5d1)` If no `ObjectsMapEntry` exists at the key, return undefined/null @@ -549,13 +556,13 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLM5d2g)` Otherwise, return undefined/null - `(RTLM10)` `LiveMap#size`: - `(RTLM10a)` A method or property, depending on what is more idiomatic for the platform to use for a Map/Dictionary interface. For example, in JavaScript, this is a property similar to `Map.size` for the native `Map` class - - `(RTLM10b)` Requires the `OBJECT_SUBSCRIBE` channel mode to be granted per [RTO2](#RTO2) - - `(RTLM10c)` If the channel is in the `DETACHED` or `FAILED` state, the library should throw an `ErrorInfo` error with `statusCode` 400 and `code` 90001 + - `(RTLM10b)` This clause has been replaced by [RTO25](#RTO25); the access API preconditions are now checked by callers + - `(RTLM10c)` This clause has been replaced by [RTO25](#RTO25); the access API preconditions are now checked by callers - `(RTLM10d)` Returns the number of non-tombstoned entries (per [RTLM14](#RTLM14)) in the internal `data` map - `(RTLM11)` `LiveMap#entries`: - `(RTLM11a)` A method or property, depending on what is more idiomatic for the platform to use for a Map/Dictionary interface. For example, in JavaScript, this is a method similar to `Map.entries()` for the native `Map` class - - `(RTLM11b)` Requires the `OBJECT_SUBSCRIBE` channel mode to be granted per [RTO2](#RTO2) - - `(RTLM11c)` If the channel is in the `DETACHED` or `FAILED` state, the library should throw an `ErrorInfo` error with `statusCode` 400 and `code` 90001 + - `(RTLM11b)` This clause has been replaced by [RTO25](#RTO25); the access API preconditions are now checked by callers + - `(RTLM11c)` This clause has been replaced by [RTO25](#RTO25); the access API preconditions are now checked by callers - `(RTLM11d)` Returns key-value pairs from the internal `data` map: - `(RTLM11d1)` Pairs with tombstoned entries (per [RTLM14](#RTLM14)) are not returned - `(RTLM11d3)` `ObjectsMapEntry` values are mapped to user-facing values following the same procedure as in [RTLM5d2](#RTLM5d2) @@ -572,9 +579,9 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLM20a1)` `key` `String` - the key to set the value for - `(RTLM20a2)` This clause has been replaced by [RTLM20a3](#RTLM20a3). - `(RTLM20a3)` `value` `Boolean | Binary | Number | String | JsonArray | JsonObject | LiveCounterValueType | LiveMapValueType` - the value to assign to the key - - `(RTLM20b)` Requires the `OBJECT_PUBLISH` channel mode to be granted per [RTO2](#RTO2) - - `(RTLM20c)` If the channel is in the `DETACHED`, `FAILED` or `SUSPENDED` state, the library should throw an `ErrorInfo` error with `statusCode` 400 and `code` 90001 - - `(RTLM20d)` If [`echoMessages`](../features#TO3h) client option is `false`, the library should throw an `ErrorInfo` error with `statusCode` 400 and `code` 40000, indicating that `echoMessages` must be enabled for this operation + - `(RTLM20b)` This clause has been replaced by [RTO26](#RTO26); the write API preconditions are now checked by callers + - `(RTLM20c)` This clause has been replaced by [RTO26](#RTO26); the write API preconditions are now checked by callers + - `(RTLM20d)` This clause has been replaced by [RTO26](#RTO26); the write API preconditions are now checked by callers - `(RTLM20e)` Creates an `ObjectMessage` for a `MAP_SET` action in the following way: - `(RTLM20e1)` Validates the provided `key` and `value` in a similar way as described in [RTLMV4b](#RTLMV4b) and [RTLMV4c](#RTLMV4c) - `(RTLM20e2)` Set `ObjectMessage.operation.action` to `ObjectOperationAction.MAP_SET` @@ -606,9 +613,9 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLM21)` `LiveMap#remove` function: - `(RTLM21a)` Expects the following arguments: - `(RTLM21a1)` `key` `String` - the key to remove the value for - - `(RTLM21b)` Requires the `OBJECT_PUBLISH` channel mode to be granted per [RTO2](#RTO2) - - `(RTLM21c)` If the channel is in the `DETACHED`, `FAILED` or `SUSPENDED` state, the library should throw an `ErrorInfo` error with `statusCode` 400 and `code` 90001 - - `(RTLM21d)` If [`echoMessages`](../features#TO3h) client option is `false`, the library should throw an `ErrorInfo` error with `statusCode` 400 and `code` 40000, indicating that `echoMessages` must be enabled for this operation + - `(RTLM21b)` This clause has been replaced by [RTO26](#RTO26); the write API preconditions are now checked by callers + - `(RTLM21c)` This clause has been replaced by [RTO26](#RTO26); the write API preconditions are now checked by callers + - `(RTLM21d)` This clause has been replaced by [RTO26](#RTO26); the write API preconditions are now checked by callers - `(RTLM21e)` Creates an `ObjectMessage` for a `MAP_REMOVE` action in the following way: - `(RTLM21e1)` Validates the provided `key` in a similar way as described in [RTLMV4b](#RTLMV4b) - `(RTLM21e2)` Set `ObjectMessage.operation.action` to `ObjectOperationAction.MAP_REMOVE` @@ -895,85 +902,98 @@ A `PathObject` is obtained from `RealtimeObject#get` ([RTO23](#RTO23)), which re - `(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")` - `(RTPO7)` `PathObject#value` function: - - `(RTPO7a)` Resolves the path using the path resolution procedure ([RTPO3](#RTPO3)) - - `(RTPO7b)` If the resolved value is a `LiveCounter`, delegates to `LiveCounter#value` ([RTLC5](#RTLC5)) - - `(RTPO7c)` If the resolved value is a primitive (`Boolean`, `Binary`, `Number`, `String`, `JsonArray`, `JsonObject`), returns the value directly - - `(RTPO7d)` If the resolved value is a `LiveMap`, returns undefined/null - - `(RTPO7e)` If path resolution fails, returns undefined/null per [RTPO3c1](#RTPO3c1) + - `(RTPO7a)` Checks the access API preconditions per [RTO25](#RTO25) + - `(RTPO7b)` Resolves the path using the path resolution procedure ([RTPO3](#RTPO3)) + - `(RTPO7c)` If the resolved value is a `LiveCounter`, delegates to `LiveCounter#value` ([RTLC5](#RTLC5)) + - `(RTPO7d)` If the resolved value is a primitive (`Boolean`, `Binary`, `Number`, `String`, `JsonArray`, `JsonObject`), returns the value directly + - `(RTPO7e)` If the resolved value is a `LiveMap`, returns undefined/null + - `(RTPO7f)` If path resolution fails, returns undefined/null per [RTPO3c1](#RTPO3c1) - `(RTPO8)` `PathObject#instance` function: - - `(RTPO8a)` Resolves the path using the path resolution procedure ([RTPO3](#RTPO3)) - - `(RTPO8b)` If the resolved value is a `LiveObject` (i.e. a `LiveMap` or `LiveCounter`), returns a new `Instance` ([RTINS1](#RTINS1)) wrapping that `LiveObject` - - `(RTPO8c)` If the resolved value is a primitive, returns undefined/null - - `(RTPO8d)` If path resolution fails, returns undefined/null per [RTPO3c1](#RTPO3c1) + - `(RTPO8a)` Checks the access API preconditions per [RTO25](#RTO25) + - `(RTPO8b)` Resolves the path using the path resolution procedure ([RTPO3](#RTPO3)) + - `(RTPO8c)` If the resolved value is a `LiveObject` (i.e. a `LiveMap` or `LiveCounter`), returns a new `Instance` ([RTINS1](#RTINS1)) wrapping that `LiveObject` + - `(RTPO8d)` If the resolved value is a primitive, returns undefined/null + - `(RTPO8e)` If path resolution fails, returns undefined/null per [RTPO3c1](#RTPO3c1) - `(RTPO9)` `PathObject#entries` function: - - `(RTPO9a)` Resolves the path using the path resolution procedure ([RTPO3](#RTPO3)) - - `(RTPO9b)` If the resolved value is a `LiveMap`, delegates to `LiveMap#keys` ([RTLM12](#RTLM12)) and returns an array of `[key, PathObject]` pairs, where each `PathObject` is created as if by calling `PathObject#get` with the corresponding key on this `PathObject` - - `(RTPO9c)` If the resolved value is not a `LiveMap`, or if path resolution fails, returns an empty array + - `(RTPO9a)` Checks the access API preconditions per [RTO25](#RTO25) + - `(RTPO9b)` Resolves the path using the path resolution procedure ([RTPO3](#RTPO3)) + - `(RTPO9c)` If the resolved value is a `LiveMap`, delegates to `LiveMap#keys` ([RTLM12](#RTLM12)) and returns an array of `[key, PathObject]` pairs, where each `PathObject` is created as if by calling `PathObject#get` with the corresponding key on this `PathObject` + - `(RTPO9d)` If the resolved value is not a `LiveMap`, or if path resolution fails, returns an empty array - `(RTPO10)` `PathObject#keys` function: - - `(RTPO10a)` Resolves the path using the path resolution procedure ([RTPO3](#RTPO3)) - - `(RTPO10b)` If the resolved value is a `LiveMap`, delegates to `LiveMap#keys` ([RTLM12](#RTLM12)) - - `(RTPO10c)` If the resolved value is not a `LiveMap`, or if path resolution fails, returns an empty array + - `(RTPO10a)` Checks the access API preconditions per [RTO25](#RTO25) + - `(RTPO10b)` Resolves the path using the path resolution procedure ([RTPO3](#RTPO3)) + - `(RTPO10c)` If the resolved value is a `LiveMap`, delegates to `LiveMap#keys` ([RTLM12](#RTLM12)) + - `(RTPO10d)` If the resolved value is not a `LiveMap`, or if path resolution fails, returns an empty array - `(RTPO11)` `PathObject#values` function: - - `(RTPO11a)` Resolves the path using the path resolution procedure ([RTPO3](#RTPO3)) - - `(RTPO11b)` If the resolved value is a `LiveMap`, delegates to `LiveMap#keys` ([RTLM12](#RTLM12)) and returns an array of `PathObject`s, where each `PathObject` is created as if by calling `PathObject#get` with the corresponding key on this `PathObject` - - `(RTPO11c)` If the resolved value is not a `LiveMap`, or if path resolution fails, returns an empty array + - `(RTPO11a)` Checks the access API preconditions per [RTO25](#RTO25) + - `(RTPO11b)` Resolves the path using the path resolution procedure ([RTPO3](#RTPO3)) + - `(RTPO11c)` If the resolved value is a `LiveMap`, delegates to `LiveMap#keys` ([RTLM12](#RTLM12)) and returns an array of `PathObject`s, where each `PathObject` is created as if by calling `PathObject#get` with the corresponding key on this `PathObject` + - `(RTPO11d)` If the resolved value is not a `LiveMap`, or if path resolution fails, returns an empty array - `(RTPO12)` `PathObject#size` function: - - `(RTPO12a)` Resolves the path using the path resolution procedure ([RTPO3](#RTPO3)) - - `(RTPO12b)` If the resolved value is a `LiveMap`, delegates to `LiveMap#size` ([RTLM10](#RTLM10)) - - `(RTPO12c)` If the resolved value is not a `LiveMap`, or if path resolution fails, returns undefined/null + - `(RTPO12a)` Checks the access API preconditions per [RTO25](#RTO25) + - `(RTPO12b)` Resolves the path using the path resolution procedure ([RTPO3](#RTPO3)) + - `(RTPO12c)` If the resolved value is a `LiveMap`, delegates to `LiveMap#size` ([RTLM10](#RTLM10)) + - `(RTPO12d)` If the resolved value is not a `LiveMap`, or if path resolution fails, returns undefined/null - `(RTPO13)` `PathObject#compact` function: - - `(RTPO13a)` Resolves the path using the path resolution procedure ([RTPO3](#RTPO3)) - - `(RTPO13b)` If the resolved value is a `LiveMap`, returns a recursively compacted representation as a plain key-value object: - - `(RTPO13b1)` Each entry in the `LiveMap` is included in the result. Tombstoned entries are excluded - - `(RTPO13b2)` Nested `LiveMap` values are recursively compacted into nested plain key-value objects - - `(RTPO13b3)` Nested `LiveCounter` values are resolved to their numeric value - - `(RTPO13b4)` Primitive values (`Boolean`, `Binary`, `Number`, `String`, `JsonArray`, `JsonObject`) are included as-is - - `(RTPO13b5)` Cyclic references (a `LiveMap` that has already been visited during this compaction) are represented by reusing the same in-memory object reference to the already-compacted result for that `LiveMap` - - `(RTPO13c)` If the resolved value is a `LiveCounter`, returns its current numeric value (equivalent to `PathObject#value`) - - `(RTPO13d)` If the resolved value is a primitive, returns the value directly (equivalent to `PathObject#value`) - - `(RTPO13e)` If path resolution fails, returns undefined/null per [RTPO3c1](#RTPO3c1) + - `(RTPO13a)` Checks the access API preconditions per [RTO25](#RTO25) + - `(RTPO13b)` Resolves the path using the path resolution procedure ([RTPO3](#RTPO3)) + - `(RTPO13c)` If the resolved value is a `LiveMap`, returns a recursively compacted representation as a plain key-value object: + - `(RTPO13c1)` Each entry in the `LiveMap` is included in the result. Tombstoned entries are excluded + - `(RTPO13c2)` Nested `LiveMap` values are recursively compacted into nested plain key-value objects + - `(RTPO13c3)` Nested `LiveCounter` values are resolved to their numeric value + - `(RTPO13c4)` Primitive values (`Boolean`, `Binary`, `Number`, `String`, `JsonArray`, `JsonObject`) are included as-is + - `(RTPO13c5)` Cyclic references (a `LiveMap` that has already been visited during this compaction) are represented by reusing the same in-memory object reference to the already-compacted result for that `LiveMap` + - `(RTPO13d)` If the resolved value is a `LiveCounter`, returns its current numeric value (equivalent to `PathObject#value`) + - `(RTPO13e)` If the resolved value is a primitive, returns the value directly (equivalent to `PathObject#value`) + - `(RTPO13f)` If path resolution fails, returns undefined/null per [RTPO3c1](#RTPO3c1) - `(RTPO14)` `PathObject#compactJson` function: - - `(RTPO14a)` Behaves identically to `PathObject#compact` ([RTPO13](#RTPO13)) except for the following differences, which ensure the result is JSON-serializable: - - `(RTPO14a1)` `Binary` values are encoded as base64 strings instead of being included as-is - - `(RTPO14a2)` Cyclic references are represented as an object with a single `objectId` property containing the Object ID of the referenced `LiveMap`, instead of reusing the in-memory object reference + - `(RTPO14a)` Checks the access API preconditions per [RTO25](#RTO25) + - `(RTPO14b)` Behaves identically to `PathObject#compact` ([RTPO13](#RTPO13)) except for the following differences, which ensure the result is JSON-serializable: + - `(RTPO14b1)` `Binary` values are encoded as base64 strings instead of being included as-is + - `(RTPO14b2)` Cyclic references are represented as an object with a single `objectId` property containing the Object ID of the referenced `LiveMap`, instead of reusing the in-memory object reference - `(RTPO15)` `PathObject#set` function: - `(RTPO15a)` Expects the following arguments: - `(RTPO15a1)` `key` `String` - the key to set the value for - `(RTPO15a2)` `value` - the value to assign to the key. Accepted types are the same as for `LiveMap#set` ([RTLM20](#RTLM20)) - - `(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 + - `(RTPO15b)` Checks the write API preconditions per [RTO26](#RTO26) + - `(RTPO15c)` Resolves the path using the path resolution procedure ([RTPO3](#RTPO3)). On failure, throws per [RTPO3c2](#RTPO3c2) + - `(RTPO15d)` If the resolved value is a `LiveMap`, delegates to `LiveMap#set` ([RTLM20](#RTLM20)) with the provided `key` and `value` + - `(RTPO15e)` 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 - `(RTPO16)` `PathObject#remove` function: - `(RTPO16a)` Expects the following arguments: - `(RTPO16a1)` `key` `String` - the key to remove the value for - - `(RTPO16b)` Resolves the path using the path resolution procedure ([RTPO3](#RTPO3)). On failure, throws per [RTPO3c2](#RTPO3c2) - - `(RTPO16c)` If the resolved value is a `LiveMap`, delegates to `LiveMap#remove` ([RTLM21](#RTLM21)) with the provided `key` - - `(RTPO16d)` If the resolved value is not a `LiveMap`, the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 92007 + - `(RTPO16b)` Checks the write API preconditions per [RTO26](#RTO26) + - `(RTPO16c)` Resolves the path using the path resolution procedure ([RTPO3](#RTPO3)). On failure, throws per [RTPO3c2](#RTPO3c2) + - `(RTPO16d)` If the resolved value is a `LiveMap`, delegates to `LiveMap#remove` ([RTLM21](#RTLM21)) with the provided `key` + - `(RTPO16e)` If the resolved value is not a `LiveMap`, the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 92007 - `(RTPO17)` `PathObject#increment` function: - `(RTPO17a)` Expects the following arguments: - `(RTPO17a1)` `amount` `Number` (optional) - the amount by which to increment the counter value. Defaults to 1 - - `(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 + - `(RTPO17b)` Checks the write API preconditions per [RTO26](#RTO26) + - `(RTPO17c)` Resolves the path using the path resolution procedure ([RTPO3](#RTPO3)). On failure, throws per [RTPO3c2](#RTPO3c2) + - `(RTPO17d)` If the resolved value is a `LiveCounter`, delegates to `LiveCounter#increment` ([RTLC12](#RTLC12)) with the provided `amount` + - `(RTPO17e)` If the resolved value is not a `LiveCounter`, the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 92007 - `(RTPO18)` `PathObject#decrement` function: - `(RTPO18a)` Expects the following arguments: - `(RTPO18a1)` `amount` `Number` (optional) - the amount by which to decrement the counter value. Defaults to 1 - - `(RTPO18b)` Resolves the path using the path resolution procedure ([RTPO3](#RTPO3)). On failure, throws per [RTPO3c2](#RTPO3c2) - - `(RTPO18c)` If the resolved value is a `LiveCounter`, delegates to `LiveCounter#decrement` ([RTLC13](#RTLC13)) with the provided `amount` - - `(RTPO18d)` If the resolved value is not a `LiveCounter`, the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 92007 + - `(RTPO18b)` Checks the write API preconditions per [RTO26](#RTO26) + - `(RTPO18c)` Resolves the path using the path resolution procedure ([RTPO3](#RTPO3)). On failure, throws per [RTPO3c2](#RTPO3c2) + - `(RTPO18d)` If the resolved value is a `LiveCounter`, delegates to `LiveCounter#decrement` ([RTLC13](#RTLC13)) with the provided `amount` + - `(RTPO18e)` If the resolved value is not a `LiveCounter`, the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 92007 - `(RTPO19)` `PathObject#subscribe` function: - `(RTPO19a)` Expects the following arguments: - - `(RTPO19a1)` `listener` - a callback function that receives a `PathObjectSubscriptionEvent` ([RTPO19d](#RTPO19d)) + - `(RTPO19a1)` `listener` - a callback function that receives a `PathObjectSubscriptionEvent` ([RTPO19e](#RTPO19e)) - `(RTPO19a2)` `options` `PathObjectSubscriptionOptions` (optional) - subscription options - - `(RTPO19b)` `PathObjectSubscriptionOptions` has the following properties: - - `(RTPO19b1)` `depth` `Number` (optional) - controls how many levels deep in the subtree changes trigger the listener. Defaults to undefined/null. The `depth` value is interpreted by the subscription coverage rule in [RTO24c1](#RTO24c1); see [RTO24c2](#RTO24c2) for worked examples - - `(RTPO19b1a)` If `depth` is provided and is not a positive integer, the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 40003 - - `(RTPO19c)` Returns a [`Subscription`](../features#SUB1) object - - `(RTPO19d)` The listener receives a `PathObjectSubscriptionEvent` object with: - - `(RTPO19d1)` `object` - a `PathObject` pointing to the path where the change occurred - - `(RTPO19d2)` `message` `PublicAPI::ObjectMessage` (optional) - if `LiveObjectUpdate.objectMessage` from the [RTLO4b4](#RTLO4b4) emission that triggered this event is populated and its `operation` field is populated, a `PublicAPI::ObjectMessage` ([PAOM1](#PAOM1)) derived from it per [PAOM3](#PAOM3); otherwise omitted - - `(RTPO19e)` Adds a subscription to the `RealtimeObject`'s `PathObjectSubscriptionRegister` ([RTO24](#RTO24)) with subscribed path equal to this `PathObject`'s `path` (per [RTPO2a](#RTPO2a)), the provided `listener`, and the provided `options.depth` - - `(RTPO19f)` This operation must not have any side effects on `RealtimeObject`, the underlying channel, or their status + - `(RTPO19b)` Checks the access API preconditions per [RTO25](#RTO25) + - `(RTPO19c)` `PathObjectSubscriptionOptions` has the following properties: + - `(RTPO19c1)` `depth` `Number` (optional) - controls how many levels deep in the subtree changes trigger the listener. Defaults to undefined/null. The `depth` value is interpreted by the subscription coverage rule in [RTO24c1](#RTO24c1); see [RTO24c2](#RTO24c2) for worked examples + - `(RTPO19c1a)` If `depth` is provided and is not a positive integer, the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 40003 + - `(RTPO19d)` Returns a [`Subscription`](../features#SUB1) object + - `(RTPO19e)` The listener receives a `PathObjectSubscriptionEvent` object with: + - `(RTPO19e1)` `object` - a `PathObject` pointing to the path where the change occurred + - `(RTPO19e2)` `message` `PublicAPI::ObjectMessage` (optional) - if `LiveObjectUpdate.objectMessage` from the [RTLO4b4](#RTLO4b4) emission that triggered this event is populated and its `operation` field is populated, a `PublicAPI::ObjectMessage` ([PAOM1](#PAOM1)) derived from it per [PAOM3](#PAOM3); otherwise omitted + - `(RTPO19f)` Adds a subscription to the `RealtimeObject`'s `PathObjectSubscriptionRegister` ([RTO24](#RTO24)) with subscribed path equal to this `PathObject`'s `path` (per [RTPO2a](#RTPO2a)), the provided `listener`, and the provided `options.depth` + - `(RTPO19g)` This operation must not have any side effects on `RealtimeObject`, the underlying channel, or their status ### Instance @@ -987,66 +1007,79 @@ An `Instance` holds a direct reference to a specific resolved `LiveObject` or pr - `(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 - `(RTINS4)` `Instance#value` function: - - `(RTINS4a)` If the wrapped value is a `LiveCounter`, delegates to `LiveCounter#value` ([RTLC5](#RTLC5)) - - `(RTINS4b)` If the wrapped value is a primitive (`Boolean`, `Binary`, `Number`, `String`, `JsonArray`, `JsonObject`), returns the value directly - - `(RTINS4c)` If the wrapped value is a `LiveMap`, returns undefined/null + - `(RTINS4a)` Checks the access API preconditions per [RTO25](#RTO25) + - `(RTINS4b)` If the wrapped value is a `LiveCounter`, delegates to `LiveCounter#value` ([RTLC5](#RTLC5)) + - `(RTINS4c)` If the wrapped value is a primitive (`Boolean`, `Binary`, `Number`, `String`, `JsonArray`, `JsonObject`), returns the value directly + - `(RTINS4d)` If the wrapped value is a `LiveMap`, returns undefined/null - `(RTINS5)` `Instance#get` function: - `(RTINS5a)` Expects the following arguments: - `(RTINS5a1)` `key` `String` - the key to look up - - `(RTINS5b)` If the wrapped value is a `LiveMap`, looks up the value at `key` using `LiveMap#get` ([RTLM5](#RTLM5)) and returns a new `Instance` wrapping the result. If the result is undefined/null, returns undefined/null - - `(RTINS5c)` If the wrapped value is not a `LiveMap`, returns undefined/null + - `(RTINS5b)` Checks the access API preconditions per [RTO25](#RTO25) + - `(RTINS5c)` If the wrapped value is a `LiveMap`, looks up the value at `key` using `LiveMap#get` ([RTLM5](#RTLM5)) and returns a new `Instance` wrapping the result. If the result is undefined/null, returns undefined/null + - `(RTINS5d)` If the wrapped value is not a `LiveMap`, returns undefined/null - `(RTINS6)` `Instance#entries` function: - - `(RTINS6a)` If the wrapped value is a `LiveMap`, delegates to `LiveMap#entries` ([RTLM11](#RTLM11)) and returns an array of `[key, Instance]` pairs, where each `Instance` wraps the corresponding value - - `(RTINS6b)` If the wrapped value is not a `LiveMap`, returns an empty array + - `(RTINS6a)` Checks the access API preconditions per [RTO25](#RTO25) + - `(RTINS6b)` If the wrapped value is a `LiveMap`, delegates to `LiveMap#entries` ([RTLM11](#RTLM11)) and returns an array of `[key, Instance]` pairs, where each `Instance` wraps the corresponding value + - `(RTINS6c)` If the wrapped value is not a `LiveMap`, returns an empty array - `(RTINS7)` `Instance#keys` function: - - `(RTINS7a)` If the wrapped value is a `LiveMap`, delegates to `LiveMap#keys` ([RTLM12](#RTLM12)) - - `(RTINS7b)` If the wrapped value is not a `LiveMap`, returns an empty array + - `(RTINS7a)` Checks the access API preconditions per [RTO25](#RTO25) + - `(RTINS7b)` If the wrapped value is a `LiveMap`, delegates to `LiveMap#keys` ([RTLM12](#RTLM12)) + - `(RTINS7c)` If the wrapped value is not a `LiveMap`, returns an empty array - `(RTINS8)` `Instance#values` function: - - `(RTINS8a)` If the wrapped value is a `LiveMap`, delegates to `LiveMap#values` ([RTLM13](#RTLM13)) and returns an array of `Instance`s, where each `Instance` wraps the corresponding value - - `(RTINS8b)` If the wrapped value is not a `LiveMap`, returns an empty array + - `(RTINS8a)` Checks the access API preconditions per [RTO25](#RTO25) + - `(RTINS8b)` If the wrapped value is a `LiveMap`, delegates to `LiveMap#values` ([RTLM13](#RTLM13)) and returns an array of `Instance`s, where each `Instance` wraps the corresponding value + - `(RTINS8c)` If the wrapped value is not a `LiveMap`, returns an empty array - `(RTINS9)` `Instance#size` function: - - `(RTINS9a)` If the wrapped value is a `LiveMap`, delegates to `LiveMap#size` ([RTLM10](#RTLM10)) - - `(RTINS9b)` If the wrapped value is not a `LiveMap`, returns undefined/null + - `(RTINS9a)` Checks the access API preconditions per [RTO25](#RTO25) + - `(RTINS9b)` If the wrapped value is a `LiveMap`, delegates to `LiveMap#size` ([RTLM10](#RTLM10)) + - `(RTINS9c)` If the wrapped value is not a `LiveMap`, returns undefined/null - `(RTINS10)` `Instance#compact` function: - - `(RTINS10a)` Behaves identically to `PathObject#compact` ([RTPO13](#RTPO13)), but operates on the wrapped value directly instead of resolving a path + - `(RTINS10a)` Checks the access API preconditions per [RTO25](#RTO25) + - `(RTINS10b)` Behaves identically to `PathObject#compact` ([RTPO13](#RTPO13)), but operates on the wrapped value directly instead of resolving a path - `(RTINS11)` `Instance#compactJson` function: - - `(RTINS11a)` Behaves identically to `PathObject#compactJson` ([RTPO14](#RTPO14)), but operates on the wrapped value directly instead of resolving a path + - `(RTINS11a)` Checks the access API preconditions per [RTO25](#RTO25) + - `(RTINS11b)` Behaves identically to `PathObject#compactJson` ([RTPO14](#RTPO14)), but operates on the wrapped value directly instead of resolving a path - `(RTINS12)` `Instance#set` function: - `(RTINS12a)` Expects the following arguments: - `(RTINS12a1)` `key` `String` - the key to set the value for - `(RTINS12a2)` `value` - the value to assign to the key. Accepted types are the same as for `LiveMap#set` ([RTLM20](#RTLM20)) - - `(RTINS12b)` If the wrapped value is a `LiveMap`, delegates to `LiveMap#set` ([RTLM20](#RTLM20)) with the provided `key` and `value` - - `(RTINS12c)` If the wrapped value is not a `LiveMap`, the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 92007 + - `(RTINS12b)` Checks the write API preconditions per [RTO26](#RTO26) + - `(RTINS12c)` If the wrapped value is a `LiveMap`, delegates to `LiveMap#set` ([RTLM20](#RTLM20)) with the provided `key` and `value` + - `(RTINS12d)` If the wrapped value is not a `LiveMap`, the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 92007 - `(RTINS13)` `Instance#remove` function: - `(RTINS13a)` Expects the following arguments: - `(RTINS13a1)` `key` `String` - the key to remove the value for - - `(RTINS13b)` If the wrapped value is a `LiveMap`, delegates to `LiveMap#remove` ([RTLM21](#RTLM21)) with the provided `key` - - `(RTINS13c)` If the wrapped value is not a `LiveMap`, the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 92007 + - `(RTINS13b)` Checks the write API preconditions per [RTO26](#RTO26) + - `(RTINS13c)` If the wrapped value is a `LiveMap`, delegates to `LiveMap#remove` ([RTLM21](#RTLM21)) with the provided `key` + - `(RTINS13d)` If the wrapped value is not a `LiveMap`, the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 92007 - `(RTINS14)` `Instance#increment` function: - `(RTINS14a)` Expects the following arguments: - `(RTINS14a1)` `amount` `Number` (optional) - the amount by which to increment the counter value. Defaults to 1 - - `(RTINS14b)` If the wrapped value is a `LiveCounter`, delegates to `LiveCounter#increment` ([RTLC12](#RTLC12)) with the provided `amount` - - `(RTINS14c)` If the wrapped value is not a `LiveCounter`, the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 92007 + - `(RTINS14b)` Checks the write API preconditions per [RTO26](#RTO26) + - `(RTINS14c)` If the wrapped value is a `LiveCounter`, delegates to `LiveCounter#increment` ([RTLC12](#RTLC12)) with the provided `amount` + - `(RTINS14d)` If the wrapped value is not a `LiveCounter`, the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 92007 - `(RTINS15)` `Instance#decrement` function: - `(RTINS15a)` Expects the following arguments: - `(RTINS15a1)` `amount` `Number` (optional) - the amount by which to decrement the counter value. Defaults to 1 - - `(RTINS15b)` If the wrapped value is a `LiveCounter`, delegates to `LiveCounter#decrement` ([RTLC13](#RTLC13)) with the provided `amount` - - `(RTINS15c)` If the wrapped value is not a `LiveCounter`, the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 92007 + - `(RTINS15b)` Checks the write API preconditions per [RTO26](#RTO26) + - `(RTINS15c)` If the wrapped value is a `LiveCounter`, delegates to `LiveCounter#decrement` ([RTLC13](#RTLC13)) with the provided `amount` + - `(RTINS15d)` If the wrapped value is not a `LiveCounter`, the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 92007 - `(RTINS16)` `Instance#subscribe` function: - `(RTINS16a)` Expects the following arguments: - - `(RTINS16a1)` `listener` - a callback function that receives an `InstanceSubscriptionEvent` ([RTINS16d](#RTINS16d)) when the wrapped object is updated - - `(RTINS16b)` If the wrapped value is not a `LiveObject` (i.e. it is a primitive), the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 92007, indicating that subscribe is not supported for primitive values - - `(RTINS16c)` Subscribes to data updates on the underlying `LiveObject` using `LiveObject#subscribe` ([RTLO4b](#RTLO4b)) - - `(RTINS16d)` The listener receives an `InstanceSubscriptionEvent` object with: - - `(RTINS16d1)` `object` - an `Instance` wrapping the underlying `LiveObject` - - `(RTINS16d2)` `message` `PublicAPI::ObjectMessage` (optional) - if `LiveObjectUpdate.objectMessage` from the underlying `LiveObject#subscribe` notification is populated and its `operation` field is populated, a `PublicAPI::ObjectMessage` ([PAOM1](#PAOM1)) derived from it per [PAOM3](#PAOM3); otherwise omitted - - `(RTINS16e)` Returns a [`Subscription`](../features#SUB1) object - - `(RTINS16f)` The subscription is identity-based: it follows the specific `LiveObject` instance, regardless of where it sits in the tree - - `(RTINS16g)` This operation must not have any side effects on `RealtimeObject`, the underlying channel, or their status + - `(RTINS16a1)` `listener` - a callback function that receives an `InstanceSubscriptionEvent` ([RTINS16e](#RTINS16e)) when the wrapped object is updated + - `(RTINS16b)` Checks the access API preconditions per [RTO25](#RTO25) + - `(RTINS16c)` If the wrapped value is not a `LiveObject` (i.e. it is a primitive), the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 92007, indicating that subscribe is not supported for primitive values + - `(RTINS16d)` Subscribes to data updates on the underlying `LiveObject` using `LiveObject#subscribe` ([RTLO4b](#RTLO4b)) + - `(RTINS16e)` The listener receives an `InstanceSubscriptionEvent` object with: + - `(RTINS16e1)` `object` - an `Instance` wrapping the underlying `LiveObject` + - `(RTINS16e2)` `message` `PublicAPI::ObjectMessage` (optional) - if `LiveObjectUpdate.objectMessage` from the underlying `LiveObject#subscribe` notification is populated and its `operation` field is populated, a `PublicAPI::ObjectMessage` ([PAOM1](#PAOM1)) derived from it per [PAOM3](#PAOM3); otherwise omitted + - `(RTINS16f)` Returns a [`Subscription`](../features#SUB1) object + - `(RTINS16g)` The subscription is identity-based: it follows the specific `LiveObject` instance, regardless of where it sits in the tree + - `(RTINS16h)` This operation must not have any side effects on `RealtimeObject`, the underlying channel, or their status ### PublicAPI::ObjectMessage -- `(PAOM1)` A `PublicAPI::ObjectMessage` is the user-facing representation of an inbound `ObjectMessage` ([OM1](../features#OM1)) that carried an operation. It is delivered to user subscription listeners (see [RTPO19d2](#RTPO19d2), [RTINS16d2](#RTINS16d2)) so that user code can inspect the metadata of the message that triggered an object change. The `PublicAPI::` prefix is used to avoid a name clash with `ObjectMessage`; SDKs expose this type to users as `ObjectMessage`. +- `(PAOM1)` A `PublicAPI::ObjectMessage` is the user-facing representation of an inbound `ObjectMessage` ([OM1](../features#OM1)) that carried an operation. It is delivered to user subscription listeners (see [RTPO19e2](#RTPO19e2), [RTINS16e2](#RTINS16e2)) so that user code can inspect the metadata of the message that triggered an object change. The `PublicAPI::` prefix is used to avoid a name clash with `ObjectMessage`; SDKs expose this type to users as `ObjectMessage`. - `(PAOM2)` The attributes available in a `PublicAPI::ObjectMessage` are: - `(PAOM2a)` `id` string (optional) - the `id` ([OM2a](../features#OM2a)) of the source `ObjectMessage` - `(PAOM2b)` `clientId` string (optional) - the `clientId` ([OM2b](../features#OM2b)) of the source `ObjectMessage` @@ -1166,16 +1199,16 @@ Types and their properties/methods are public and exposed to users by default. A entries: Dict? // RTLMV2a, internal static create(Dict entries?) -> LiveMapValueType // RTLMV3 - interface PathObjectSubscriptionEvent: // RTPO19d - object: PathObject // RTPO19d1 - message: PublicAPI::ObjectMessage? // RTPO19d2 + interface PathObjectSubscriptionEvent: // RTPO19e + object: PathObject // RTPO19e1 + message: PublicAPI::ObjectMessage? // RTPO19e2 - interface PathObjectSubscriptionOptions: // RTPO19b - depth: Number? // RTPO19b1 + interface PathObjectSubscriptionOptions: // RTPO19c + depth: Number? // RTPO19c1 - interface InstanceSubscriptionEvent: // RTINS16d - object: Instance // RTINS16d1 - message: PublicAPI::ObjectMessage? // RTINS16d2 + interface InstanceSubscriptionEvent: // RTINS16e + object: Instance // RTINS16e1 + message: PublicAPI::ObjectMessage? // RTINS16e2 class PublicAPI::ObjectMessage: // PAOM* id: String? // PAOM2a