From 454fb1ccf982f3296132270c1ad6d23c21ebe957 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Tue, 9 Jun 2026 17:54:20 +0530 Subject: [PATCH 01/13] Add typed-SDK LiveObjects API spec section (RTTS1-RTTS10) Introduce a new `### Typed-SDK public API (strongly-typed languages)` section in `objects-features.md` that normatively partitions the `PathObject` and `Instance` API surfaces across a class hierarchy for SDKs in strongly-typed host languages (Java, Swift, Kotlin). The dynamic-SDK form (JavaScript) is unchanged. --- specifications/objects-features.md | 176 ++++++++++++++++++++++++++++- 1 file changed, 175 insertions(+), 1 deletion(-) diff --git a/specifications/objects-features.md b/specifications/objects-features.md index afc036b85..fdaab7d4a 100644 --- a/specifications/objects-features.md +++ b/specifications/objects-features.md @@ -21,6 +21,7 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTO23e)` Perform the *ensure-active-channel* procedure ([RTL33](../features#RTL33)) on the underlying `RealtimeChannel`. If the procedure fails, the `get` function must reject with the same `ErrorInfo` that caused the procedure to fail - `(RTO23c)` If the [RTO17](#RTO17) sync state is not `SYNCED`, waits for the sync state to transition to `SYNCED` - `(RTO23d)` Returns a new `PathObject` ([RTPO1](#RTPO1)) with `path` ([RTPO2a](#RTPO2a)) set to an empty list and `root` ([RTPO2b](#RTPO2b)) set to the `InternalLiveMap` with id `root` from the internal `ObjectsPool` + - `(RTO23f)` In typed SDKs the returned `PathObject` is of static type `LiveMapPathObject` per [RTTS6d](#RTTS6d). - `(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). @@ -874,6 +875,7 @@ A `PathObject` is obtained from `RealtimeObject#get` ([RTO23](#RTO23)), which re - `(RTPO1)` The `PathObject` class provides a path-based view over the LiveObjects graph - `(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 + - `(RTPO1b)` Typed SDKs (e.g. Java, Swift, Kotlin) must partition this class as described in [RTTS3](#RTTS3) through [RTTS6](#RTTS6). - `(RTPO2)` `PathObject` has the following internal properties: - `(RTPO2a)` `path` - an ordered list of string segments representing the path from the root `InternalLiveMap` to this position in the graph - `(RTPO2b)` `root` - a reference to the root `InternalLiveMap` instance from the internal `ObjectsPool` @@ -947,6 +949,7 @@ A `PathObject` is obtained from `RealtimeObject#get` ([RTO23](#RTO23)), which re - `(RTPO13d)` If the resolved value is an `InternalLiveCounter`, 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) + - `(RTPO13g)` Typed SDKs are not required to implement this method; see [RTTS3f](#RTTS3f). - `(RTPO14)` `PathObject#compactJson` function: - `(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: @@ -1002,6 +1005,7 @@ An `Instance` holds a direct reference to a specific resolved `LiveObject` or pr - `(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 + - `(RTINS1b)` Typed SDKs (e.g. Java, Swift, Kotlin) must partition this class as described in [RTTS7](#RTTS7) through [RTTS10](#RTTS10). - `(RTINS2)` `Instance` has the following internal properties: - `(RTINS2a)` `value` - a reference to the wrapped `LiveObject` or primitive value - `(RTINS3)` `Instance#id` property: @@ -1037,9 +1041,11 @@ An `Instance` holds a direct reference to a specific resolved `LiveObject` or pr - `(RTINS10)` `Instance#compact` function: - `(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 + - `(RTINS10c)` Typed SDKs are not required to implement this method; see [RTTS7d](#RTTS7d). - `(RTINS11)` `Instance#compactJson` function: - `(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 + - `(RTINS11c)` Because an `Instance` is bound to an already-resolved value at construction time, the path-resolution-failure case of [RTPO14](#RTPO14) (inherited via [RTINS11b](#RTINS11b)) cannot arise; this method MUST NOT return null. - `(RTINS12)` `Instance#set` function: - `(RTINS12a)` Expects the following arguments: - `(RTINS12a1)` `key` `String` - the key to set the value for @@ -1078,6 +1084,81 @@ An `Instance` holds a direct reference to a specific resolved `LiveObject` or pr - `(RTINS16g)` The subscription is identity-based: it follows the specific `LiveObject` instance, regardless of where it sits in the graph - `(RTINS16h)` This operation must not have any side effects on `RealtimeObject`, the underlying channel, or their status +### Typed-SDK public API (strongly-typed languages) + +This section refines [RTPO1a](#RTPO1a) and [RTINS1a](#RTINS1a) for SDKs whose host language is strongly typed (e.g. Java, Swift, Kotlin). It is normative for those SDKs. SDKs in dynamically-typed languages (e.g. JavaScript) MAY continue to expose the full `PathObject` ([RTPO1](#RTPO1)) and `Instance` ([RTINS1](#RTINS1)) classes unchanged. + +Rationale (non-normative): the overwhelming majority of customer use-cases operate on a static domain model; typed SDKs should provide compile-time type-safety for that majority while keeping a dynamic escape hatch via the base classes ([RTTS3](#RTTS3), [RTTS7](#RTTS7)) and the `as*` cast helpers ([RTTS5](#RTTS5), [RTTS9](#RTTS9)). + +This section partitions the API surface of `PathObject` and `Instance` across a class hierarchy. It does not redefine per-method semantics; those remain authoritative in [RTPO\*](#RTPO1) and [RTINS\*](#RTINS1). + +- `(RTTS1)` Scope + - `(RTTS1a)` This section is normative for SDKs whose host language is strongly typed and which expose typed sub-classes per [RTPO1a](#RTPO1a) and [RTINS1a](#RTINS1a). All such SDKs must agree on the partition described here, so that user code is portable between them + - `(RTTS1b)` SDKs in dynamically-typed host languages are not required to expose these sub-classes; if they do, they must follow this partition +- `(RTTS2)` `ValueType` enum - the set of value categories a `PathObject` or `Instance` may wrap. Used by [RTTS4b](#RTTS4b) and [RTTS8a](#RTTS8a) + - `(RTTS2a)` The enum has the following members. Names are illustrative; SDKs may use platform-idiomatic casing: + - `(RTTS2a1)` `STRING` - corresponds to the `String` primitive + - `(RTTS2a2)` `NUMBER` - corresponds to the `Number` primitive + - `(RTTS2a3)` `BOOLEAN` - corresponds to the `Boolean` primitive + - `(RTTS2a4)` `BINARY` - corresponds to the `Binary` primitive + - `(RTTS2a5)` `JSON_OBJECT` - corresponds to the `JsonObject` primitive + - `(RTTS2a6)` `JSON_ARRAY` - corresponds to the `JsonArray` primitive + - `(RTTS2a7)` `LIVE_MAP` - corresponds to an `InternalLiveMap` ([RTLM1](#RTLM1)) + - `(RTTS2a8)` `LIVE_COUNTER` - corresponds to an `InternalLiveCounter` ([RTLC1](#RTLC1)) + - `(RTTS2a9)` `UNKNOWN` - returned by [RTTS4b](#RTTS4b) when path resolution fails or the resolved value has none of the above types. Available on [RTTS8a](#RTTS8a) only as a future-compatibility fallback - an `Instance` is bound to a known value at construction time and so MUST NOT produce `UNKNOWN` in normal operation +- `(RTTS3)` Typed `PathObject` base class - the typed SDK's public root of the `PathObject` hierarchy. The base class exposes only those methods of [RTPO1](#RTPO1) whose return type and semantics are identical regardless of the resolved underlying type + - `(RTTS3a)` Exposes `path` per [RTPO4](#RTPO4) + - `(RTTS3b)` Exposes `instance` per [RTPO8](#RTPO8). Sub-classes other than `LiveMapPathObject` ([RTTS6a](#RTTS6a)) and `LiveCounterPathObject` ([RTTS6b](#RTTS6b)) may narrow this further per [RTTS6e](#RTTS6e) + - `(RTTS3c)` Exposes `compactJson` per [RTPO14](#RTPO14). Where the host language supports covariant return types, sub-classes MAY narrow the return type (e.g. `JsonObjectPathObject#compactJson` returning the SDK's `JsonObject` rather than its generic JSON element type). Implementations MAY choose not to narrow + - `(RTTS3d)` Exposes `subscribe` per [RTPO19](#RTPO19). Typed SDKs MUST deliver the full `PathObjectSubscriptionEvent` payload defined in [RTPO19e](#RTPO19e) to the listener - both the `object` field ([RTPO19e1](#RTPO19e1)) and the optional `message` field ([RTPO19e2](#RTPO19e2)) - so that user code can inspect the `PublicAPI::ObjectMessage` ([PAOM1](#PAOM1)) that triggered the change. Typed SDKs MUST NOT expose a subscription-event type that omits the `message` accessor + - `(RTTS3e)` Does not expose `get` ([RTPO5](#RTPO5)), `at` ([RTPO6](#RTPO6)), `value` ([RTPO7](#RTPO7)), `entries` ([RTPO9](#RTPO9)), `keys` ([RTPO10](#RTPO10)), `values` ([RTPO11](#RTPO11)), `size` ([RTPO12](#RTPO12)), `set` ([RTPO15](#RTPO15)), `remove` ([RTPO16](#RTPO16)), `increment` ([RTPO17](#RTPO17)) or `decrement` ([RTPO18](#RTPO18)) on the base class. Those are partitioned onto the sub-classes per [RTTS6](#RTTS6) + - `(RTTS3f)` Does not expose `compact` ([RTPO13](#RTPO13)). Typed SDKs are not required to implement `compact`; `compactJson` is sufficient for the static-domain use-cases. This may be revisited in a future spec version + - `(RTTS3g)` (non-normative) To traverse from a base `PathObject` reference into a map, callers cast first via `asLiveMap` ([RTTS5a](#RTTS5a)). For example, for a base `PathObject p`, deep navigation `p.asLiveMap().at("a.b.c")` is equivalent to successive `get` calls +- `(RTTS4)` Typed `PathObject` extension methods - best-effort helpers added by the typed-SDK variant. They have O(n) complexity in the path length because they resolve the path at call time, and are therefore exposed as methods (not properties) even in host languages that distinguish the two + - `(RTTS4a)` `exists()` - returns `Boolean` indicating whether a value currently resolves at the stored path. The check is best-effort and evaluates the local object tree at call time; the answer may change between this call and a subsequent operation as remote operations are applied. Useful as a guard before a write whose semantics depend on existence: + - `(RTTS4a1)` Checks the access API preconditions per [RTO25](#RTO25) + - `(RTTS4a2)` Performs the path resolution procedure ([RTPO3](#RTPO3)) + - `(RTTS4a3)` Returns `true` if path resolution succeeds; `false` if it fails per [RTPO3c](#RTPO3c) + - `(RTTS4b)` `getType()` - returns the `ValueType` ([RTTS2](#RTTS2)) of the value currently resolved at the stored path: + - `(RTTS4b1)` Checks the access API preconditions per [RTO25](#RTO25) + - `(RTTS4b2)` Performs the path resolution procedure ([RTPO3](#RTPO3)) + - `(RTTS4b3)` Returns the matching `ValueType` member per [RTTS2a](#RTTS2a). Returns `UNKNOWN` if path resolution fails or the resolved value falls into none of the categories defined in [RTTS2a1](#RTTS2a1) through [RTTS2a8](#RTTS2a8) +- `(RTTS5)` Typed `PathObject` `as*` cast helpers - best-effort accessors that return a sub-class view of this `PathObject` without performing any validation against current path resolution. The returned wrapper shares this `PathObject`'s `path` ([RTPO2a](#RTPO2a)) and `root` ([RTPO2b](#RTPO2b)) + - `(RTTS5a)` `asLiveMap()` returns a `LiveMapPathObject` ([RTTS6a](#RTTS6a)) + - `(RTTS5b)` `asLiveCounter()` returns a `LiveCounterPathObject` ([RTTS6b](#RTTS6b)) + - `(RTTS5c)` `asNumber()`, `asString()`, `asBoolean()`, `asBinary()`, `asJsonObject()`, `asJsonArray()` each return the corresponding primitive sub-class per [RTTS6c](#RTTS6c) + - `(RTTS5d)` These helpers MUST NOT throw based on the current resolved type; they only re-wrap. Operations on the returned wrapper carry their own failure semantics: + - `(RTTS5d1)` Read operations (`value`, `instance`, `entries`, `keys`, `values`, `size`, `compactJson`) follow [RTPO3c1](#RTPO3c1) - return null/empty if the resolved type does not match the wrapper's expectation + - `(RTTS5d2)` Write and terminal operations (`set`, `remove`, `increment`, `decrement`) follow [RTPO3c2](#RTPO3c2) - throw an `ErrorInfo` with `statusCode` 400 and `code` 92007 if the resolved type does not match + - `(RTTS5e)` (non-normative) Callers needing a strict type-check before casting should use [RTTS4b](#RTTS4b) `getType` or [RTTS4a](#RTTS4a) `exists` +- `(RTTS6)` Typed `PathObject` sub-classes - the partition of methods across the hierarchy. Each sub-class extends [RTTS3](#RTTS3) and therefore inherits `path`, `instance`, `compactJson`, `subscribe`, `exists`, `getType`, and all `as*` helpers + - `(RTTS6a)` `LiveMapPathObject` - adds `get` per [RTPO5](#RTPO5), `at` per [RTPO6](#RTPO6), `entries` per [RTPO9](#RTPO9), `keys` per [RTPO10](#RTPO10), `values` per [RTPO11](#RTPO11), `size` per [RTPO12](#RTPO12), `set` per [RTPO15](#RTPO15), `remove` per [RTPO16](#RTPO16). Failure semantics for each method when the path does not resolve to an `InternalLiveMap` are inherited from the referenced `RTPO*` clauses unchanged + - `(RTTS6b)` `LiveCounterPathObject` - adds `increment` per [RTPO17](#RTPO17), `decrement` per [RTPO18](#RTPO18), and a `value()` method delegating to [RTPO7](#RTPO7) with the return type narrowed to `Number?` (returns null per [RTPO3c1](#RTPO3c1) / [RTPO7e](#RTPO7e) if the resolved value is not an `InternalLiveCounter`) + - `(RTTS6c)` Primitive `PathObject` sub-classes - one per primitive: `NumberPathObject`, `StringPathObject`, `BooleanPathObject`, `BinaryPathObject`, `JsonObjectPathObject`, `JsonArrayPathObject`. Each adds only a `value()` method delegating to [RTPO7](#RTPO7) with the return type narrowed to its corresponding primitive (returning null if the resolved value is not of that primitive type, per [RTPO3c1](#RTPO3c1)). They do not expose `get`, `at`, or any write / iteration methods + - `(RTTS6d)` The `PathObject` returned by `RealtimeObject#get` ([RTO23](#RTO23)) MUST be of static type `LiveMapPathObject` in typed SDKs. This is well-defined because [RTO23d](#RTO23d) always returns a `PathObject` rooted at the channel's root `InternalLiveMap` with an empty path + - `(RTTS6e)` On all sub-classes other than `LiveMapPathObject` and `LiveCounterPathObject`, calling `instance()` ([RTTS3b](#RTTS3b)) MUST either return null without side-effects (consistent with [RTPO8d](#RTPO8d)) or throw an `ErrorInfo` with `statusCode` 400 and `code` 92007. SDKs should prefer the null behaviour for consistency with [RTPO8d](#RTPO8d); the throwing behaviour is permitted because the typed contract makes it statically obvious that `instance` is meaningless on a primitive view + - `(RTTS6f)` (non-normative) Sub-classes MAY share a common abstract intermediate super-class (e.g. `PrimitivePathObject`) for implementation convenience, as long as the public API surface seen by user code matches the partition above +- `(RTTS7)` Typed `Instance` base class - the typed SDK's public root of the `Instance` hierarchy. The base class exposes only those methods of [RTINS1](#RTINS1) whose return type and semantics are identical regardless of the wrapped underlying type + - `(RTTS7a)` Exposes `compactJson` per [RTINS11](#RTINS11). The non-null invariant is universal per [RTINS11c](#RTINS11c); typed SDKs MUST reflect this by giving `Instance#compactJson` a non-nullable return type in the public API. Where the host language additionally supports covariant return types, sub-classes MAY further narrow per [RTTS3c](#RTTS3c) (e.g. `JsonObjectInstance#compactJson` returning the SDK's `JsonObject` type directly); implementations MAY choose not to narrow + - `(RTTS7b)` Does NOT expose `subscribe` ([RTINS16](#RTINS16)) on the base class. `subscribe` is partitioned onto `LiveMapInstance` ([RTTS10a](#RTTS10a)) and `LiveCounterInstance` ([RTTS10b](#RTTS10b)) only. Rationale: in a typed SDK every `Instance` reference has a known concrete type (or knowable via [RTTS8a](#RTTS8a) `getType`), and `subscribe` is meaningful only on `LiveObject` instances ([RTINS16c](#RTINS16c) already requires the dynamic-SDK form to throw on primitives). Moving the method off the base turns that limitation into a compile-time contract, matching the official user docs' statement that `subscribe()` is "only available on `LiveMap` and `LiveCounter` instances" + - `(RTTS7c)` Does not expose `id` ([RTINS3](#RTINS3)), `value` ([RTINS4](#RTINS4)), `get` ([RTINS5](#RTINS5)), `entries` ([RTINS6](#RTINS6)), `keys` ([RTINS7](#RTINS7)), `values` ([RTINS8](#RTINS8)), `size` ([RTINS9](#RTINS9)), `set` ([RTINS12](#RTINS12)), `remove` ([RTINS13](#RTINS13)), `increment` ([RTINS14](#RTINS14)) or `decrement` ([RTINS15](#RTINS15)) on the base class. Those are partitioned onto the sub-classes per [RTTS10](#RTTS10) + - `(RTTS7d)` Does not expose `compact` ([RTINS10](#RTINS10)). See [RTTS3f](#RTTS3f) for rationale +- `(RTTS8)` Typed `Instance` extension property - best-effort helper introduced by the typed-SDK variant. Unlike [RTTS4](#RTTS4) on `PathObject`, an `Instance` is bound to a resolved value at construction time, so the helper has O(1) complexity and is therefore exposed as a property in host languages that distinguish properties from methods + - `(RTTS8a)` `getType` - returns the `ValueType` ([RTTS2](#RTTS2)) of the wrapped value. Returns the matching `ValueType` member per [RTTS2a](#RTTS2a). MUST NOT return `UNKNOWN` in normal operation because an `Instance` is always constructed from a resolved value (see e.g. [RTPO8c](#RTPO8c), [RTINS5c](#RTINS5c)); see [RTTS2a9](#RTTS2a9) for the future-compatibility caveat + - `(RTTS8b)` There is no `exists` helper on `Instance`, because an `Instance` is only ever constructed when it wraps a resolved value; the existence question is unambiguous +- `(RTTS9)` Typed `Instance` `as*` cast helpers - best-effort accessors mirroring [RTTS5](#RTTS5). Exposed as properties in host languages that distinguish properties from methods, since the wrapped value is already resolved + - `(RTTS9a)` `asLiveMap` returns a `LiveMapInstance` ([RTTS10a](#RTTS10a)) + - `(RTTS9b)` `asLiveCounter` returns a `LiveCounterInstance` ([RTTS10b](#RTTS10b)) + - `(RTTS9c)` `asNumber`, `asString`, `asBoolean`, `asBinary`, `asJsonObject`, `asJsonArray` each return the corresponding primitive sub-class per [RTTS10c](#RTTS10c) + - `(RTTS9d)` These helpers MUST NOT throw based on the wrapped type. Operations on the returned wrapper carry their own failure semantics: + - `(RTTS9d1)` Read operations follow [RTINS4d](#RTINS4d), [RTINS5d](#RTINS5d), [RTINS6c](#RTINS6c), [RTINS7c](#RTINS7c), [RTINS8c](#RTINS8c), [RTINS9c](#RTINS9c) - return null/empty if the wrapped type does not match + - `(RTTS9d2)` Write and terminal operations follow [RTINS12d](#RTINS12d), [RTINS13d](#RTINS13d), [RTINS14d](#RTINS14d), [RTINS15d](#RTINS15d) - throw an `ErrorInfo` with `statusCode` 400 and `code` 92007 if the wrapped type does not match +- `(RTTS10)` Typed `Instance` sub-classes - the partition of methods across the hierarchy. Each sub-class extends [RTTS7](#RTTS7) and therefore inherits `compactJson`, `getType`, and all `as*` helpers. Note that `subscribe` is NOT inherited from the base - see [RTTS7b](#RTTS7b) + - `(RTTS10a)` `LiveMapInstance` - adds `id` per [RTINS3a](#RTINS3a) with the static return type narrowed to non-nullable `String`; `get` per [RTINS5](#RTINS5); `entries` per [RTINS6](#RTINS6); `keys` per [RTINS7](#RTINS7); `values` per [RTINS8](#RTINS8); `size` per [RTINS9](#RTINS9) with the return type narrowed to non-nullable `Number` (since the wrapped value is always an `InternalLiveMap`, [RTINS9c](#RTINS9c) cannot trigger); `set` per [RTINS12](#RTINS12); `remove` per [RTINS13](#RTINS13); `subscribe` per [RTINS16](#RTINS16). Typed SDKs MUST deliver the full `InstanceSubscriptionEvent` payload defined in [RTINS16e](#RTINS16e) to the listener - both the `object` field ([RTINS16e1](#RTINS16e1)) and the optional `message` field ([RTINS16e2](#RTINS16e2)) - and MUST NOT expose a subscription-event type that omits the `message` accessor + - `(RTTS10b)` `LiveCounterInstance` - adds `id` per [RTINS3a](#RTINS3a) (narrowed to non-nullable `String`); a `value()` method delegating to [RTINS4](#RTINS4) with the return type narrowed to non-nullable `Number`; `increment` per [RTINS14](#RTINS14); `decrement` per [RTINS15](#RTINS15); `subscribe` per [RTINS16](#RTINS16) with the same `InstanceSubscriptionEvent` payload requirements as [RTTS10a](#RTTS10a) + - `(RTTS10c)` Primitive `Instance` sub-classes - one per primitive: `NumberInstance`, `StringInstance`, `BooleanInstance`, `BinaryInstance`, `JsonObjectInstance`, `JsonArrayInstance`. Each adds only a `value()` method delegating to [RTINS4](#RTINS4) with the return type narrowed to non-nullable instance of its corresponding primitive. These sub-classes are read-only - they do not expose `id` (primitive instances have no object id per [RTINS3b](#RTINS3b)), `get`, `set`, `remove`, `increment`, `decrement`, `entries`, `keys`, `values`, `size`, or `subscribe`. The absence of `subscribe` is a compile-time enforcement of the constraint already imposed at runtime by [RTINS16c](#RTINS16c) + - `(RTTS10d)` (non-normative) Sub-classes MAY share a common abstract intermediate super-class (e.g. `PrimitiveInstance`) for implementation convenience, as long as the public API surface seen by user code matches the partition above + ### 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 [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`. @@ -1261,9 +1342,102 @@ Types and their properties/methods are public and exposed to users by default. A values() -> Instance[] // RTINS8 size() -> Number? // RTINS9 compact() -> Object? // RTINS10 - compactJson() -> Object? // RTINS11 + 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 subscribe((InstanceSubscriptionEvent) -> listener) -> Subscription // RTINS16 + + // === Typed-SDK variant (RTTS1-RTTS10) ===================================== + // Strongly-typed SDKs (Java/Swift/Kotlin) expose the hierarchy below in + // place of the monomorphic `class PathObject` / `class Instance` above. + // ========================================================================== + + enum ValueType: // RTTS2 + STRING // RTTS2a1 + NUMBER // RTTS2a2 + BOOLEAN // RTTS2a3 + BINARY // RTTS2a4 + JSON_OBJECT // RTTS2a5 + JSON_ARRAY // RTTS2a6 + LIVE_MAP // RTTS2a7 + LIVE_COUNTER // RTTS2a8 + UNKNOWN // RTTS2a9 + + class PathObject: // RTTS3 + path() -> String // RTPO4 + instance() -> Instance? // RTPO8 + compactJson() -> Object? // RTPO14 + subscribe((PathObjectSubscriptionEvent) -> listener, PathObjectSubscriptionOptions? options) -> Subscription // RTPO19 + exists() -> Boolean // RTTS4a + getType() -> ValueType // RTTS4b + asLiveMap() -> LiveMapPathObject // RTTS5a + asLiveCounter() -> LiveCounterPathObject // RTTS5b + asNumber() -> NumberPathObject // RTTS5c + asString() -> StringPathObject // RTTS5c + asBoolean() -> BooleanPathObject // RTTS5c + asBinary() -> BinaryPathObject // RTTS5c + asJsonObject() -> JsonObjectPathObject // RTTS5c + asJsonArray() -> JsonArrayPathObject // RTTS5c + + class LiveMapPathObject extends PathObject: // RTTS6a + get(String key) -> PathObject // RTPO5 + at(String path) -> PathObject // RTPO6 + entries() -> [String, PathObject][] // RTPO9 + keys() -> String[] // RTPO10 + values() -> PathObject[] // RTPO11 + size() -> Number? // RTPO12 + set(String key, (Boolean | Binary | Number | String | JsonArray | JsonObject | LiveCounter | LiveMap) value) => io // RTPO15 + remove(String key) => io // RTPO16 + + class LiveCounterPathObject extends PathObject: // RTTS6b + value() -> Number? // RTPO7 + increment(Number amount?) => io // RTPO17 + decrement(Number amount?) => io // RTPO18 + + class NumberPathObject extends PathObject: value() -> Number? // RTTS6c + class StringPathObject extends PathObject: value() -> String? // RTTS6c + class BooleanPathObject extends PathObject: value() -> Boolean? // RTTS6c + class BinaryPathObject extends PathObject: value() -> Binary? // RTTS6c + class JsonObjectPathObject extends PathObject: value() -> JsonObject? // RTTS6c + class JsonArrayPathObject extends PathObject: value() -> JsonArray? // RTTS6c + + class Instance: // RTTS7 + compactJson() -> Object // RTINS11 (non-nullable per RTINS11c - universal invariant) + getType: ValueType // RTTS8a + // Note: subscribe is NOT on base Instance per RTTS7b; partitioned onto + // LiveMapInstance / LiveCounterInstance below. + asLiveMap: LiveMapInstance // RTTS9a + asLiveCounter: LiveCounterInstance // RTTS9b + asNumber: NumberInstance // RTTS9c + asString: StringInstance // RTTS9c + asBoolean: BooleanInstance // RTTS9c + asBinary: BinaryInstance // RTTS9c + asJsonObject: JsonObjectInstance // RTTS9c + asJsonArray: JsonArrayInstance // RTTS9c + + class LiveMapInstance extends Instance: // RTTS10a + id: String // RTINS3a (narrowed to non-nullable) + get(String key) -> Instance? // RTINS5 + entries() -> [String, Instance][] // RTINS6 + keys() -> String[] // RTINS7 + values() -> Instance[] // RTINS8 + size() -> Number // RTINS9 (narrowed to non-nullable) + set(String key, (Boolean | Binary | Number | String | JsonArray | JsonObject | LiveCounter | LiveMap) value) => io // RTINS12 + remove(String key) => io // RTINS13 + subscribe((InstanceSubscriptionEvent) -> listener) -> Subscription // RTINS16 + + class LiveCounterInstance extends Instance: // RTTS10b + id: String // RTINS3a (narrowed to non-nullable) + value() -> Number // RTINS4 (narrowed to non-nullable) + increment(Number amount?) => io // RTINS14 + decrement(Number amount?) => io // RTINS15 + subscribe((InstanceSubscriptionEvent) -> listener) -> Subscription // RTINS16 + + class NumberInstance extends Instance: value() -> Number // RTTS10c + class StringInstance extends Instance: value() -> String // RTTS10c + class BooleanInstance extends Instance: value() -> Boolean // RTTS10c + class BinaryInstance extends Instance: value() -> Binary // RTTS10c + class JsonObjectInstance extends Instance: value() -> JsonObject // RTTS10c + class JsonArrayInstance extends Instance: value() -> JsonArray // RTTS10c From 1d72322b9a63f0bf7240bb786f0eb37a0d7c22c5 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Thu, 11 Jun 2026 16:06:28 +0530 Subject: [PATCH 02/13] Refine typed-SDK LiveObjects spec section per review - Normalize RFC 2119 keywords to the file's lowercase convention throughout RTTS1-RTTS10 and the new hook clauses - Remove reliance on user-facing docs as authority in RTTS7b; mark its rationale non-normative - Reword RTTS3b: RTTS6e constrains the failure behaviour of `instance` on primitive sub-classes, it does not narrow the return type - Restructure RTINS11c as a normative requirement plus a non-normative explanation of the underlying invariant - Rewrite RTTS2a9 as a pure enum-member definition, leaving UNKNOWN behaviour to RTTS4b3 and RTTS8a (DRY) - Replace "strongly typed" with "statically typed" (the distinction drawn is static vs dynamic typing) - Retitle RTTS4 to avoid "extension methods" terminology - Remove trailing full stops from new hook clauses to match file style - IDL: drop redundant base-Instance subscribe note; annotate subscribe on LiveMapInstance/LiveCounterInstance with RTTS7b --- specifications/objects-features.md | 58 +++++++++++++++--------------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/specifications/objects-features.md b/specifications/objects-features.md index fdaab7d4a..cfbffd9b2 100644 --- a/specifications/objects-features.md +++ b/specifications/objects-features.md @@ -21,7 +21,7 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTO23e)` Perform the *ensure-active-channel* procedure ([RTL33](../features#RTL33)) on the underlying `RealtimeChannel`. If the procedure fails, the `get` function must reject with the same `ErrorInfo` that caused the procedure to fail - `(RTO23c)` If the [RTO17](#RTO17) sync state is not `SYNCED`, waits for the sync state to transition to `SYNCED` - `(RTO23d)` Returns a new `PathObject` ([RTPO1](#RTPO1)) with `path` ([RTPO2a](#RTPO2a)) set to an empty list and `root` ([RTPO2b](#RTPO2b)) set to the `InternalLiveMap` with id `root` from the internal `ObjectsPool` - - `(RTO23f)` In typed SDKs the returned `PathObject` is of static type `LiveMapPathObject` per [RTTS6d](#RTTS6d). + - `(RTO23f)` In typed SDKs the returned `PathObject` is of static type `LiveMapPathObject` per [RTTS6d](#RTTS6d) - `(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). @@ -875,7 +875,7 @@ A `PathObject` is obtained from `RealtimeObject#get` ([RTO23](#RTO23)), which re - `(RTPO1)` The `PathObject` class provides a path-based view over the LiveObjects graph - `(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 - - `(RTPO1b)` Typed SDKs (e.g. Java, Swift, Kotlin) must partition this class as described in [RTTS3](#RTTS3) through [RTTS6](#RTTS6). + - `(RTPO1b)` Typed SDKs (e.g. Java, Swift, Kotlin) must partition this class as described in [RTTS3](#RTTS3) through [RTTS6](#RTTS6) - `(RTPO2)` `PathObject` has the following internal properties: - `(RTPO2a)` `path` - an ordered list of string segments representing the path from the root `InternalLiveMap` to this position in the graph - `(RTPO2b)` `root` - a reference to the root `InternalLiveMap` instance from the internal `ObjectsPool` @@ -949,7 +949,7 @@ A `PathObject` is obtained from `RealtimeObject#get` ([RTO23](#RTO23)), which re - `(RTPO13d)` If the resolved value is an `InternalLiveCounter`, 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) - - `(RTPO13g)` Typed SDKs are not required to implement this method; see [RTTS3f](#RTTS3f). + - `(RTPO13g)` Typed SDKs are not required to implement this method; see [RTTS3f](#RTTS3f) - `(RTPO14)` `PathObject#compactJson` function: - `(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: @@ -1005,7 +1005,7 @@ An `Instance` holds a direct reference to a specific resolved `LiveObject` or pr - `(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 - - `(RTINS1b)` Typed SDKs (e.g. Java, Swift, Kotlin) must partition this class as described in [RTTS7](#RTTS7) through [RTTS10](#RTTS10). + - `(RTINS1b)` Typed SDKs (e.g. Java, Swift, Kotlin) must partition this class as described in [RTTS7](#RTTS7) through [RTTS10](#RTTS10) - `(RTINS2)` `Instance` has the following internal properties: - `(RTINS2a)` `value` - a reference to the wrapped `LiveObject` or primitive value - `(RTINS3)` `Instance#id` property: @@ -1041,11 +1041,11 @@ An `Instance` holds a direct reference to a specific resolved `LiveObject` or pr - `(RTINS10)` `Instance#compact` function: - `(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 - - `(RTINS10c)` Typed SDKs are not required to implement this method; see [RTTS7d](#RTTS7d). + - `(RTINS10c)` Typed SDKs are not required to implement this method; see [RTTS7d](#RTTS7d) - `(RTINS11)` `Instance#compactJson` function: - `(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 - - `(RTINS11c)` Because an `Instance` is bound to an already-resolved value at construction time, the path-resolution-failure case of [RTPO14](#RTPO14) (inherited via [RTINS11b](#RTINS11b)) cannot arise; this method MUST NOT return null. + - `(RTINS11c)` This method must not return null. (Non-normative: the only null-returning case of [RTPO14](#RTPO14), path-resolution failure per [RTPO3c1](#RTPO3c1), cannot arise here because an `Instance` is always constructed from an already-resolved value - see [RTPO8c](#RTPO8c), [RTINS5c](#RTINS5c)) - `(RTINS12)` `Instance#set` function: - `(RTINS12a)` Expects the following arguments: - `(RTINS12a1)` `key` `String` - the key to set the value for @@ -1084,16 +1084,16 @@ An `Instance` holds a direct reference to a specific resolved `LiveObject` or pr - `(RTINS16g)` The subscription is identity-based: it follows the specific `LiveObject` instance, regardless of where it sits in the graph - `(RTINS16h)` This operation must not have any side effects on `RealtimeObject`, the underlying channel, or their status -### Typed-SDK public API (strongly-typed languages) +### Typed-SDK public API (statically-typed languages) -This section refines [RTPO1a](#RTPO1a) and [RTINS1a](#RTINS1a) for SDKs whose host language is strongly typed (e.g. Java, Swift, Kotlin). It is normative for those SDKs. SDKs in dynamically-typed languages (e.g. JavaScript) MAY continue to expose the full `PathObject` ([RTPO1](#RTPO1)) and `Instance` ([RTINS1](#RTINS1)) classes unchanged. +This section refines [RTPO1a](#RTPO1a) and [RTINS1a](#RTINS1a) for SDKs whose host language is statically typed (e.g. Java, Swift, Kotlin). It is normative for those SDKs. SDKs in dynamically-typed languages (e.g. JavaScript) may continue to expose the full `PathObject` ([RTPO1](#RTPO1)) and `Instance` ([RTINS1](#RTINS1)) classes unchanged. Rationale (non-normative): the overwhelming majority of customer use-cases operate on a static domain model; typed SDKs should provide compile-time type-safety for that majority while keeping a dynamic escape hatch via the base classes ([RTTS3](#RTTS3), [RTTS7](#RTTS7)) and the `as*` cast helpers ([RTTS5](#RTTS5), [RTTS9](#RTTS9)). This section partitions the API surface of `PathObject` and `Instance` across a class hierarchy. It does not redefine per-method semantics; those remain authoritative in [RTPO\*](#RTPO1) and [RTINS\*](#RTINS1). - `(RTTS1)` Scope - - `(RTTS1a)` This section is normative for SDKs whose host language is strongly typed and which expose typed sub-classes per [RTPO1a](#RTPO1a) and [RTINS1a](#RTINS1a). All such SDKs must agree on the partition described here, so that user code is portable between them + - `(RTTS1a)` This section is normative for SDKs whose host language is statically typed and which expose typed sub-classes per [RTPO1a](#RTPO1a) and [RTINS1a](#RTINS1a). All such SDKs must agree on the partition described here, so that user code is portable between them - `(RTTS1b)` SDKs in dynamically-typed host languages are not required to expose these sub-classes; if they do, they must follow this partition - `(RTTS2)` `ValueType` enum - the set of value categories a `PathObject` or `Instance` may wrap. Used by [RTTS4b](#RTTS4b) and [RTTS8a](#RTTS8a) - `(RTTS2a)` The enum has the following members. Names are illustrative; SDKs may use platform-idiomatic casing: @@ -1105,16 +1105,16 @@ This section partitions the API surface of `PathObject` and `Instance` across a - `(RTTS2a6)` `JSON_ARRAY` - corresponds to the `JsonArray` primitive - `(RTTS2a7)` `LIVE_MAP` - corresponds to an `InternalLiveMap` ([RTLM1](#RTLM1)) - `(RTTS2a8)` `LIVE_COUNTER` - corresponds to an `InternalLiveCounter` ([RTLC1](#RTLC1)) - - `(RTTS2a9)` `UNKNOWN` - returned by [RTTS4b](#RTTS4b) when path resolution fails or the resolved value has none of the above types. Available on [RTTS8a](#RTTS8a) only as a future-compatibility fallback - an `Instance` is bound to a known value at construction time and so MUST NOT produce `UNKNOWN` in normal operation + - `(RTTS2a9)` `UNKNOWN` - indicates that the value's type falls into none of the categories above, or (for `PathObject#getType`, [RTTS4b](#RTTS4b)) that path resolution failed. See [RTTS8a](#RTTS8a) for when this member may appear on `Instance#getType` - `(RTTS3)` Typed `PathObject` base class - the typed SDK's public root of the `PathObject` hierarchy. The base class exposes only those methods of [RTPO1](#RTPO1) whose return type and semantics are identical regardless of the resolved underlying type - `(RTTS3a)` Exposes `path` per [RTPO4](#RTPO4) - - `(RTTS3b)` Exposes `instance` per [RTPO8](#RTPO8). Sub-classes other than `LiveMapPathObject` ([RTTS6a](#RTTS6a)) and `LiveCounterPathObject` ([RTTS6b](#RTTS6b)) may narrow this further per [RTTS6e](#RTTS6e) - - `(RTTS3c)` Exposes `compactJson` per [RTPO14](#RTPO14). Where the host language supports covariant return types, sub-classes MAY narrow the return type (e.g. `JsonObjectPathObject#compactJson` returning the SDK's `JsonObject` rather than its generic JSON element type). Implementations MAY choose not to narrow - - `(RTTS3d)` Exposes `subscribe` per [RTPO19](#RTPO19). Typed SDKs MUST deliver the full `PathObjectSubscriptionEvent` payload defined in [RTPO19e](#RTPO19e) to the listener - both the `object` field ([RTPO19e1](#RTPO19e1)) and the optional `message` field ([RTPO19e2](#RTPO19e2)) - so that user code can inspect the `PublicAPI::ObjectMessage` ([PAOM1](#PAOM1)) that triggered the change. Typed SDKs MUST NOT expose a subscription-event type that omits the `message` accessor + - `(RTTS3b)` Exposes `instance` per [RTPO8](#RTPO8). On sub-classes other than `LiveMapPathObject` ([RTTS6a](#RTTS6a)) and `LiveCounterPathObject` ([RTTS6b](#RTTS6b)), the behaviour of `instance` is constrained by [RTTS6e](#RTTS6e) + - `(RTTS3c)` Exposes `compactJson` per [RTPO14](#RTPO14). Where the host language supports covariant return types, sub-classes may narrow the return type (e.g. `JsonObjectPathObject#compactJson` returning the SDK's `JsonObject` rather than its generic JSON element type). Implementations may choose not to narrow + - `(RTTS3d)` Exposes `subscribe` per [RTPO19](#RTPO19). Typed SDKs must deliver the full `PathObjectSubscriptionEvent` payload defined in [RTPO19e](#RTPO19e) to the listener - both the `object` field ([RTPO19e1](#RTPO19e1)) and the optional `message` field ([RTPO19e2](#RTPO19e2)) - so that user code can inspect the `PublicAPI::ObjectMessage` ([PAOM1](#PAOM1)) that triggered the change. Typed SDKs must not expose a subscription-event type that omits the `message` accessor - `(RTTS3e)` Does not expose `get` ([RTPO5](#RTPO5)), `at` ([RTPO6](#RTPO6)), `value` ([RTPO7](#RTPO7)), `entries` ([RTPO9](#RTPO9)), `keys` ([RTPO10](#RTPO10)), `values` ([RTPO11](#RTPO11)), `size` ([RTPO12](#RTPO12)), `set` ([RTPO15](#RTPO15)), `remove` ([RTPO16](#RTPO16)), `increment` ([RTPO17](#RTPO17)) or `decrement` ([RTPO18](#RTPO18)) on the base class. Those are partitioned onto the sub-classes per [RTTS6](#RTTS6) - `(RTTS3f)` Does not expose `compact` ([RTPO13](#RTPO13)). Typed SDKs are not required to implement `compact`; `compactJson` is sufficient for the static-domain use-cases. This may be revisited in a future spec version - `(RTTS3g)` (non-normative) To traverse from a base `PathObject` reference into a map, callers cast first via `asLiveMap` ([RTTS5a](#RTTS5a)). For example, for a base `PathObject p`, deep navigation `p.asLiveMap().at("a.b.c")` is equivalent to successive `get` calls -- `(RTTS4)` Typed `PathObject` extension methods - best-effort helpers added by the typed-SDK variant. They have O(n) complexity in the path length because they resolve the path at call time, and are therefore exposed as methods (not properties) even in host languages that distinguish the two +- `(RTTS4)` Additional typed `PathObject` methods - best-effort helpers that exist only on the typed base class. They have O(n) complexity in the path length because they resolve the path at call time, and are therefore exposed as methods (not properties) even in host languages that distinguish the two - `(RTTS4a)` `exists()` - returns `Boolean` indicating whether a value currently resolves at the stored path. The check is best-effort and evaluates the local object tree at call time; the answer may change between this call and a subsequent operation as remote operations are applied. Useful as a guard before a write whose semantics depend on existence: - `(RTTS4a1)` Checks the access API preconditions per [RTO25](#RTO25) - `(RTTS4a2)` Performs the path resolution procedure ([RTPO3](#RTPO3)) @@ -1127,7 +1127,7 @@ This section partitions the API surface of `PathObject` and `Instance` across a - `(RTTS5a)` `asLiveMap()` returns a `LiveMapPathObject` ([RTTS6a](#RTTS6a)) - `(RTTS5b)` `asLiveCounter()` returns a `LiveCounterPathObject` ([RTTS6b](#RTTS6b)) - `(RTTS5c)` `asNumber()`, `asString()`, `asBoolean()`, `asBinary()`, `asJsonObject()`, `asJsonArray()` each return the corresponding primitive sub-class per [RTTS6c](#RTTS6c) - - `(RTTS5d)` These helpers MUST NOT throw based on the current resolved type; they only re-wrap. Operations on the returned wrapper carry their own failure semantics: + - `(RTTS5d)` These helpers must not throw based on the current resolved type; they only re-wrap. Operations on the returned wrapper carry their own failure semantics: - `(RTTS5d1)` Read operations (`value`, `instance`, `entries`, `keys`, `values`, `size`, `compactJson`) follow [RTPO3c1](#RTPO3c1) - return null/empty if the resolved type does not match the wrapper's expectation - `(RTTS5d2)` Write and terminal operations (`set`, `remove`, `increment`, `decrement`) follow [RTPO3c2](#RTPO3c2) - throw an `ErrorInfo` with `statusCode` 400 and `code` 92007 if the resolved type does not match - `(RTTS5e)` (non-normative) Callers needing a strict type-check before casting should use [RTTS4b](#RTTS4b) `getType` or [RTTS4a](#RTTS4a) `exists` @@ -1135,29 +1135,29 @@ This section partitions the API surface of `PathObject` and `Instance` across a - `(RTTS6a)` `LiveMapPathObject` - adds `get` per [RTPO5](#RTPO5), `at` per [RTPO6](#RTPO6), `entries` per [RTPO9](#RTPO9), `keys` per [RTPO10](#RTPO10), `values` per [RTPO11](#RTPO11), `size` per [RTPO12](#RTPO12), `set` per [RTPO15](#RTPO15), `remove` per [RTPO16](#RTPO16). Failure semantics for each method when the path does not resolve to an `InternalLiveMap` are inherited from the referenced `RTPO*` clauses unchanged - `(RTTS6b)` `LiveCounterPathObject` - adds `increment` per [RTPO17](#RTPO17), `decrement` per [RTPO18](#RTPO18), and a `value()` method delegating to [RTPO7](#RTPO7) with the return type narrowed to `Number?` (returns null per [RTPO3c1](#RTPO3c1) / [RTPO7e](#RTPO7e) if the resolved value is not an `InternalLiveCounter`) - `(RTTS6c)` Primitive `PathObject` sub-classes - one per primitive: `NumberPathObject`, `StringPathObject`, `BooleanPathObject`, `BinaryPathObject`, `JsonObjectPathObject`, `JsonArrayPathObject`. Each adds only a `value()` method delegating to [RTPO7](#RTPO7) with the return type narrowed to its corresponding primitive (returning null if the resolved value is not of that primitive type, per [RTPO3c1](#RTPO3c1)). They do not expose `get`, `at`, or any write / iteration methods - - `(RTTS6d)` The `PathObject` returned by `RealtimeObject#get` ([RTO23](#RTO23)) MUST be of static type `LiveMapPathObject` in typed SDKs. This is well-defined because [RTO23d](#RTO23d) always returns a `PathObject` rooted at the channel's root `InternalLiveMap` with an empty path - - `(RTTS6e)` On all sub-classes other than `LiveMapPathObject` and `LiveCounterPathObject`, calling `instance()` ([RTTS3b](#RTTS3b)) MUST either return null without side-effects (consistent with [RTPO8d](#RTPO8d)) or throw an `ErrorInfo` with `statusCode` 400 and `code` 92007. SDKs should prefer the null behaviour for consistency with [RTPO8d](#RTPO8d); the throwing behaviour is permitted because the typed contract makes it statically obvious that `instance` is meaningless on a primitive view - - `(RTTS6f)` (non-normative) Sub-classes MAY share a common abstract intermediate super-class (e.g. `PrimitivePathObject`) for implementation convenience, as long as the public API surface seen by user code matches the partition above + - `(RTTS6d)` The `PathObject` returned by `RealtimeObject#get` ([RTO23](#RTO23)) must be of static type `LiveMapPathObject` in typed SDKs. This is well-defined because [RTO23d](#RTO23d) always returns a `PathObject` rooted at the channel's root `InternalLiveMap` with an empty path + - `(RTTS6e)` On all sub-classes other than `LiveMapPathObject` and `LiveCounterPathObject`, calling `instance()` ([RTTS3b](#RTTS3b)) must either return null without side-effects (consistent with [RTPO8d](#RTPO8d)) or throw an `ErrorInfo` with `statusCode` 400 and `code` 92007. SDKs should prefer the null behaviour for consistency with [RTPO8d](#RTPO8d); the throwing behaviour is permitted because the typed contract makes it statically obvious that `instance` is meaningless on a primitive view + - `(RTTS6f)` (non-normative) Sub-classes may share a common abstract intermediate super-class (e.g. `PrimitivePathObject`) for implementation convenience, as long as the public API surface seen by user code matches the partition above - `(RTTS7)` Typed `Instance` base class - the typed SDK's public root of the `Instance` hierarchy. The base class exposes only those methods of [RTINS1](#RTINS1) whose return type and semantics are identical regardless of the wrapped underlying type - - `(RTTS7a)` Exposes `compactJson` per [RTINS11](#RTINS11). The non-null invariant is universal per [RTINS11c](#RTINS11c); typed SDKs MUST reflect this by giving `Instance#compactJson` a non-nullable return type in the public API. Where the host language additionally supports covariant return types, sub-classes MAY further narrow per [RTTS3c](#RTTS3c) (e.g. `JsonObjectInstance#compactJson` returning the SDK's `JsonObject` type directly); implementations MAY choose not to narrow - - `(RTTS7b)` Does NOT expose `subscribe` ([RTINS16](#RTINS16)) on the base class. `subscribe` is partitioned onto `LiveMapInstance` ([RTTS10a](#RTTS10a)) and `LiveCounterInstance` ([RTTS10b](#RTTS10b)) only. Rationale: in a typed SDK every `Instance` reference has a known concrete type (or knowable via [RTTS8a](#RTTS8a) `getType`), and `subscribe` is meaningful only on `LiveObject` instances ([RTINS16c](#RTINS16c) already requires the dynamic-SDK form to throw on primitives). Moving the method off the base turns that limitation into a compile-time contract, matching the official user docs' statement that `subscribe()` is "only available on `LiveMap` and `LiveCounter` instances" + - `(RTTS7a)` Exposes `compactJson` per [RTINS11](#RTINS11). The non-null invariant is universal per [RTINS11c](#RTINS11c); typed SDKs must reflect this by giving `Instance#compactJson` a non-nullable return type in the public API. Where the host language additionally supports covariant return types, sub-classes may further narrow per [RTTS3c](#RTTS3c) (e.g. `JsonObjectInstance#compactJson` returning the SDK's `JsonObject` type directly); implementations may choose not to narrow + - `(RTTS7b)` Does not expose `subscribe` ([RTINS16](#RTINS16)) on the base class. `subscribe` is partitioned onto `LiveMapInstance` ([RTTS10a](#RTTS10a)) and `LiveCounterInstance` ([RTTS10b](#RTTS10b)) only. Rationale (non-normative): in a typed SDK every `Instance` reference has a known concrete type (or knowable via [RTTS8a](#RTTS8a) `getType`), and `subscribe` is meaningful only on `LiveObject` instances ([RTINS16c](#RTINS16c) already requires the dynamic-SDK form to throw on primitives). Moving the method off the base turns that limitation into a compile-time contract - `(RTTS7c)` Does not expose `id` ([RTINS3](#RTINS3)), `value` ([RTINS4](#RTINS4)), `get` ([RTINS5](#RTINS5)), `entries` ([RTINS6](#RTINS6)), `keys` ([RTINS7](#RTINS7)), `values` ([RTINS8](#RTINS8)), `size` ([RTINS9](#RTINS9)), `set` ([RTINS12](#RTINS12)), `remove` ([RTINS13](#RTINS13)), `increment` ([RTINS14](#RTINS14)) or `decrement` ([RTINS15](#RTINS15)) on the base class. Those are partitioned onto the sub-classes per [RTTS10](#RTTS10) - `(RTTS7d)` Does not expose `compact` ([RTINS10](#RTINS10)). See [RTTS3f](#RTTS3f) for rationale - `(RTTS8)` Typed `Instance` extension property - best-effort helper introduced by the typed-SDK variant. Unlike [RTTS4](#RTTS4) on `PathObject`, an `Instance` is bound to a resolved value at construction time, so the helper has O(1) complexity and is therefore exposed as a property in host languages that distinguish properties from methods - - `(RTTS8a)` `getType` - returns the `ValueType` ([RTTS2](#RTTS2)) of the wrapped value. Returns the matching `ValueType` member per [RTTS2a](#RTTS2a). MUST NOT return `UNKNOWN` in normal operation because an `Instance` is always constructed from a resolved value (see e.g. [RTPO8c](#RTPO8c), [RTINS5c](#RTINS5c)); see [RTTS2a9](#RTTS2a9) for the future-compatibility caveat + - `(RTTS8a)` `getType` - returns the `ValueType` ([RTTS2](#RTTS2)) of the wrapped value. Returns the matching `ValueType` member per [RTTS2a](#RTTS2a). Must not return `UNKNOWN` in normal operation because an `Instance` is always constructed from a resolved value (see e.g. [RTPO8c](#RTPO8c), [RTINS5c](#RTINS5c)); see [RTTS2a9](#RTTS2a9) for the future-compatibility caveat - `(RTTS8b)` There is no `exists` helper on `Instance`, because an `Instance` is only ever constructed when it wraps a resolved value; the existence question is unambiguous - `(RTTS9)` Typed `Instance` `as*` cast helpers - best-effort accessors mirroring [RTTS5](#RTTS5). Exposed as properties in host languages that distinguish properties from methods, since the wrapped value is already resolved - `(RTTS9a)` `asLiveMap` returns a `LiveMapInstance` ([RTTS10a](#RTTS10a)) - `(RTTS9b)` `asLiveCounter` returns a `LiveCounterInstance` ([RTTS10b](#RTTS10b)) - `(RTTS9c)` `asNumber`, `asString`, `asBoolean`, `asBinary`, `asJsonObject`, `asJsonArray` each return the corresponding primitive sub-class per [RTTS10c](#RTTS10c) - - `(RTTS9d)` These helpers MUST NOT throw based on the wrapped type. Operations on the returned wrapper carry their own failure semantics: + - `(RTTS9d)` These helpers must not throw based on the wrapped type. Operations on the returned wrapper carry their own failure semantics: - `(RTTS9d1)` Read operations follow [RTINS4d](#RTINS4d), [RTINS5d](#RTINS5d), [RTINS6c](#RTINS6c), [RTINS7c](#RTINS7c), [RTINS8c](#RTINS8c), [RTINS9c](#RTINS9c) - return null/empty if the wrapped type does not match - `(RTTS9d2)` Write and terminal operations follow [RTINS12d](#RTINS12d), [RTINS13d](#RTINS13d), [RTINS14d](#RTINS14d), [RTINS15d](#RTINS15d) - throw an `ErrorInfo` with `statusCode` 400 and `code` 92007 if the wrapped type does not match -- `(RTTS10)` Typed `Instance` sub-classes - the partition of methods across the hierarchy. Each sub-class extends [RTTS7](#RTTS7) and therefore inherits `compactJson`, `getType`, and all `as*` helpers. Note that `subscribe` is NOT inherited from the base - see [RTTS7b](#RTTS7b) - - `(RTTS10a)` `LiveMapInstance` - adds `id` per [RTINS3a](#RTINS3a) with the static return type narrowed to non-nullable `String`; `get` per [RTINS5](#RTINS5); `entries` per [RTINS6](#RTINS6); `keys` per [RTINS7](#RTINS7); `values` per [RTINS8](#RTINS8); `size` per [RTINS9](#RTINS9) with the return type narrowed to non-nullable `Number` (since the wrapped value is always an `InternalLiveMap`, [RTINS9c](#RTINS9c) cannot trigger); `set` per [RTINS12](#RTINS12); `remove` per [RTINS13](#RTINS13); `subscribe` per [RTINS16](#RTINS16). Typed SDKs MUST deliver the full `InstanceSubscriptionEvent` payload defined in [RTINS16e](#RTINS16e) to the listener - both the `object` field ([RTINS16e1](#RTINS16e1)) and the optional `message` field ([RTINS16e2](#RTINS16e2)) - and MUST NOT expose a subscription-event type that omits the `message` accessor +- `(RTTS10)` Typed `Instance` sub-classes - the partition of methods across the hierarchy. Each sub-class extends [RTTS7](#RTTS7) and therefore inherits `compactJson`, `getType`, and all `as*` helpers. Note that `subscribe` is not inherited from the base - see [RTTS7b](#RTTS7b) + - `(RTTS10a)` `LiveMapInstance` - adds `id` per [RTINS3a](#RTINS3a) with the static return type narrowed to non-nullable `String`; `get` per [RTINS5](#RTINS5); `entries` per [RTINS6](#RTINS6); `keys` per [RTINS7](#RTINS7); `values` per [RTINS8](#RTINS8); `size` per [RTINS9](#RTINS9) with the return type narrowed to non-nullable `Number` (since the wrapped value is always an `InternalLiveMap`, [RTINS9c](#RTINS9c) cannot trigger); `set` per [RTINS12](#RTINS12); `remove` per [RTINS13](#RTINS13); `subscribe` per [RTINS16](#RTINS16). Typed SDKs must deliver the full `InstanceSubscriptionEvent` payload defined in [RTINS16e](#RTINS16e) to the listener - both the `object` field ([RTINS16e1](#RTINS16e1)) and the optional `message` field ([RTINS16e2](#RTINS16e2)) - and must not expose a subscription-event type that omits the `message` accessor - `(RTTS10b)` `LiveCounterInstance` - adds `id` per [RTINS3a](#RTINS3a) (narrowed to non-nullable `String`); a `value()` method delegating to [RTINS4](#RTINS4) with the return type narrowed to non-nullable `Number`; `increment` per [RTINS14](#RTINS14); `decrement` per [RTINS15](#RTINS15); `subscribe` per [RTINS16](#RTINS16) with the same `InstanceSubscriptionEvent` payload requirements as [RTTS10a](#RTTS10a) - `(RTTS10c)` Primitive `Instance` sub-classes - one per primitive: `NumberInstance`, `StringInstance`, `BooleanInstance`, `BinaryInstance`, `JsonObjectInstance`, `JsonArrayInstance`. Each adds only a `value()` method delegating to [RTINS4](#RTINS4) with the return type narrowed to non-nullable instance of its corresponding primitive. These sub-classes are read-only - they do not expose `id` (primitive instances have no object id per [RTINS3b](#RTINS3b)), `get`, `set`, `remove`, `increment`, `decrement`, `entries`, `keys`, `values`, `size`, or `subscribe`. The absence of `subscribe` is a compile-time enforcement of the constraint already imposed at runtime by [RTINS16c](#RTINS16c) - - `(RTTS10d)` (non-normative) Sub-classes MAY share a common abstract intermediate super-class (e.g. `PrimitiveInstance`) for implementation convenience, as long as the public API surface seen by user code matches the partition above + - `(RTTS10d)` (non-normative) Sub-classes may share a common abstract intermediate super-class (e.g. `PrimitiveInstance`) for implementation convenience, as long as the public API surface seen by user code matches the partition above ### PublicAPI::ObjectMessage @@ -1350,7 +1350,7 @@ Types and their properties/methods are public and exposed to users by default. A subscribe((InstanceSubscriptionEvent) -> listener) -> Subscription // RTINS16 // === Typed-SDK variant (RTTS1-RTTS10) ===================================== - // Strongly-typed SDKs (Java/Swift/Kotlin) expose the hierarchy below in + // Statically-typed SDKs (Java/Swift/Kotlin) expose the hierarchy below in // place of the monomorphic `class PathObject` / `class Instance` above. // ========================================================================== @@ -1406,8 +1406,6 @@ Types and their properties/methods are public and exposed to users by default. A class Instance: // RTTS7 compactJson() -> Object // RTINS11 (non-nullable per RTINS11c - universal invariant) getType: ValueType // RTTS8a - // Note: subscribe is NOT on base Instance per RTTS7b; partitioned onto - // LiveMapInstance / LiveCounterInstance below. asLiveMap: LiveMapInstance // RTTS9a asLiveCounter: LiveCounterInstance // RTTS9b asNumber: NumberInstance // RTTS9c @@ -1426,14 +1424,14 @@ Types and their properties/methods are public and exposed to users by default. A size() -> Number // RTINS9 (narrowed to non-nullable) set(String key, (Boolean | Binary | Number | String | JsonArray | JsonObject | LiveCounter | LiveMap) value) => io // RTINS12 remove(String key) => io // RTINS13 - subscribe((InstanceSubscriptionEvent) -> listener) -> Subscription // RTINS16 + subscribe((InstanceSubscriptionEvent) -> listener) -> Subscription // RTINS16, RTTS7b class LiveCounterInstance extends Instance: // RTTS10b id: String // RTINS3a (narrowed to non-nullable) value() -> Number // RTINS4 (narrowed to non-nullable) increment(Number amount?) => io // RTINS14 decrement(Number amount?) => io // RTINS15 - subscribe((InstanceSubscriptionEvent) -> listener) -> Subscription // RTINS16 + subscribe((InstanceSubscriptionEvent) -> listener) -> Subscription // RTINS16, RTTS7b class NumberInstance extends Instance: value() -> Number // RTTS10c class StringInstance extends Instance: value() -> String // RTTS10c From 839790b0d8146755d9b76f4a013d8366d651643b Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Fri, 12 Jun 2026 18:07:18 +0530 Subject: [PATCH 03/13] Clarify InternalLiveMap read/write type asymmetry Read methods return live graph objects (InternalLiveMap/ InternalLiveCounter); object-valued writes accept only the LiveCounter/ LiveMap creation value types. This is intentional (reference assignment was removed when the value-type API replaced RTLM20e5a) but was easy to misread after the value types took over the LiveMap/LiveCounter names. - Add a non-normative intro to the InternalLiveMap section explaining the asymmetry - Anchor RTLM20a3's LiveCounter/LiveMap to RTLCV1/RTLMV1 - Add RTLMV4c1: graph objects and their PathObject/Instance wrappers are not accepted as values and should be rejected with 40013 - IDL: annotate InternalLiveMap#set and the LiveCounter/LiveMap value type class declarations to distinguish them from graph objects Co-Authored-By: Claude Fable 5 --- specifications/objects-features.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/specifications/objects-features.md b/specifications/objects-features.md index cfbffd9b2..97d9d89b2 100644 --- a/specifications/objects-features.md +++ b/specifications/objects-features.md @@ -523,6 +523,8 @@ Objects feature enables clients to store shared data as "objects" on a channel. ### InternalLiveMap +Read methods (`InternalLiveMap#get` ([RTLM5](#RTLM5)), `entries` ([RTLM11](#RTLM11)), `values` ([RTLM13](#RTLM13))) return the live graph objects (`InternalLiveMap`, `InternalLiveCounter`) resolved from the internal `ObjectsPool`. Write methods do not accept graph objects: object-valued writes to `InternalLiveMap#set` ([RTLM20](#RTLM20)) take the immutable creation value types `LiveCounter` ([RTLCV1](#RTLCV1)) and `LiveMap` ([RTLMV1](#RTLMV1)), which are evaluated into `*_CREATE` operations at write time ([RTLM20e7g](#RTLM20e7g)). Assigning a reference to an existing graph object is not supported by this API. + - `(RTLM1)` The `InternalLiveMap` extends `LiveObject` - `(RTLM2)` Represents the map object type for Object IDs of type `map` - `(RTLM3)` Holds a `Dict` as a private `data` map @@ -580,7 +582,7 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTLM20a)` Expects the following arguments: - `(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 | LiveCounter | LiveMap` - the value to assign to the key + - `(RTLM20a3)` `value` `Boolean | Binary | Number | String | JsonArray | JsonObject | LiveCounter | LiveMap` - the value to assign to the key. `LiveCounter` and `LiveMap` here are the creation value types per [RTLCV1](#RTLCV1) and [RTLMV1](#RTLMV1) - `(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 @@ -842,6 +844,7 @@ A `LiveMap` is an immutable blueprint for creating a new `InternalLiveMap` objec - `(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 + - `(RTLMV4c1)` For the avoidance of doubt, live graph objects (`InternalLiveMap` ([RTLM1](#RTLM1)), `InternalLiveCounter` ([RTLC1](#RTLC1))) and the public objects that wrap them (`PathObject` ([RTPO1](#RTPO1)), `Instance` ([RTINS1](#RTINS1))) are not expected types, and per [RTLMV4c](#RTLMV4c) the library should throw an `ErrorInfo` error with `statusCode` 400 and `code` 40013 if one is provided as a value - `(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 `LiveCounter`, 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 `LiveMap`, 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 (which is the `MAP_CREATE` for the `InternalLiveMap` whose creation this `LiveMap` represents, per [RTLMV4k](#RTLMV4k); earlier entries create objects nested within it) @@ -1267,17 +1270,17 @@ Types and their properties/methods are public and exposed to users by default. A entries() -> [String, (Boolean | Binary | Number | String | JsonArray | JsonObject | InternalLiveCounter | InternalLiveMap)?][] // RTLM11 keys() -> String[] // RTLM12 values() -> (Boolean | Binary | Number | String | JsonArray | JsonObject | InternalLiveCounter | InternalLiveMap)?[] // RTLM13 - set(String key, (Boolean | Binary | Number | String | JsonArray | JsonObject | LiveCounter | LiveMap) value) => io // RTLM20 + set(String key, (Boolean | Binary | Number | String | JsonArray | JsonObject | LiveCounter | LiveMap) value) => io // RTLM20; LiveCounter/LiveMap are creation value types (RTLCV1, RTLMV1), not graph objects remove(String key) => io // RTLM21 interface LiveMapUpdate extends LiveObjectUpdate: // RTLM18, RTLM18a, internal update: Dict // RTLM18b - class LiveCounter: // RTLCV* + class LiveCounter: // RTLCV*; immutable creation value type, evaluated per RTLCV4 - distinct from InternalLiveCounter count: Number // RTLCV2a, internal static create(Number initialCount?) -> LiveCounter // RTLCV3 - class LiveMap: // RTLMV* + class LiveMap: // RTLMV*; immutable creation value type, evaluated per RTLMV4 - distinct from InternalLiveMap entries: Dict? // RTLMV2a, internal static create(Dict entries?) -> LiveMap // RTLMV3 From 7840a53dc10c559d021544b5b46a64f4afc734aa Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Thu, 18 Jun 2026 16:16:55 +0530 Subject: [PATCH 04/13] Marked `PathObject#getType` as nullable, so it returns null when value is not present Updated respective sub-clauses --- specifications/objects-features.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/specifications/objects-features.md b/specifications/objects-features.md index 97d9d89b2..4481fc807 100644 --- a/specifications/objects-features.md +++ b/specifications/objects-features.md @@ -1108,7 +1108,7 @@ This section partitions the API surface of `PathObject` and `Instance` across a - `(RTTS2a6)` `JSON_ARRAY` - corresponds to the `JsonArray` primitive - `(RTTS2a7)` `LIVE_MAP` - corresponds to an `InternalLiveMap` ([RTLM1](#RTLM1)) - `(RTTS2a8)` `LIVE_COUNTER` - corresponds to an `InternalLiveCounter` ([RTLC1](#RTLC1)) - - `(RTTS2a9)` `UNKNOWN` - indicates that the value's type falls into none of the categories above, or (for `PathObject#getType`, [RTTS4b](#RTTS4b)) that path resolution failed. See [RTTS8a](#RTTS8a) for when this member may appear on `Instance#getType` + - `(RTTS2a9)` `UNKNOWN` - indicates that a value is present but its type falls into none of the categories above. This is distinct from the absence of a value: `PathObject#getType` ([RTTS4b](#RTTS4b)) returns `null` (not `UNKNOWN`) when no value resolves at the path. See [RTTS8a](#RTTS8a) for when this member may appear on `Instance#getType` - `(RTTS3)` Typed `PathObject` base class - the typed SDK's public root of the `PathObject` hierarchy. The base class exposes only those methods of [RTPO1](#RTPO1) whose return type and semantics are identical regardless of the resolved underlying type - `(RTTS3a)` Exposes `path` per [RTPO4](#RTPO4) - `(RTTS3b)` Exposes `instance` per [RTPO8](#RTPO8). On sub-classes other than `LiveMapPathObject` ([RTTS6a](#RTTS6a)) and `LiveCounterPathObject` ([RTTS6b](#RTTS6b)), the behaviour of `instance` is constrained by [RTTS6e](#RTTS6e) @@ -1122,10 +1122,10 @@ This section partitions the API surface of `PathObject` and `Instance` across a - `(RTTS4a1)` Checks the access API preconditions per [RTO25](#RTO25) - `(RTTS4a2)` Performs the path resolution procedure ([RTPO3](#RTPO3)) - `(RTTS4a3)` Returns `true` if path resolution succeeds; `false` if it fails per [RTPO3c](#RTPO3c) - - `(RTTS4b)` `getType()` - returns the `ValueType` ([RTTS2](#RTTS2)) of the value currently resolved at the stored path: + - `(RTTS4b)` `getType()` - returns the `ValueType` ([RTTS2](#RTTS2)) of the value currently resolved at the stored path, or `null` when no value resolves at the path (i.e. in exactly the cases where [RTTS4a](#RTTS4a) `exists` returns `false`). The return type must therefore be nullable in the public API: - `(RTTS4b1)` Checks the access API preconditions per [RTO25](#RTO25) - `(RTTS4b2)` Performs the path resolution procedure ([RTPO3](#RTPO3)) - - `(RTTS4b3)` Returns the matching `ValueType` member per [RTTS2a](#RTTS2a). Returns `UNKNOWN` if path resolution fails or the resolved value falls into none of the categories defined in [RTTS2a1](#RTTS2a1) through [RTTS2a8](#RTTS2a8) + - `(RTTS4b3)` If path resolution fails per [RTPO3c](#RTPO3c), returns `null`, indicating that no value exists at the path. Otherwise returns the matching `ValueType` member per [RTTS2a](#RTTS2a), or `UNKNOWN` if the resolved value falls into none of the categories defined in [RTTS2a1](#RTTS2a1) through [RTTS2a8](#RTTS2a8) - `(RTTS5)` Typed `PathObject` `as*` cast helpers - best-effort accessors that return a sub-class view of this `PathObject` without performing any validation against current path resolution. The returned wrapper shares this `PathObject`'s `path` ([RTPO2a](#RTPO2a)) and `root` ([RTPO2b](#RTPO2b)) - `(RTTS5a)` `asLiveMap()` returns a `LiveMapPathObject` ([RTTS6a](#RTTS6a)) - `(RTTS5b)` `asLiveCounter()` returns a `LiveCounterPathObject` ([RTTS6b](#RTTS6b)) @@ -1136,7 +1136,7 @@ This section partitions the API surface of `PathObject` and `Instance` across a - `(RTTS5e)` (non-normative) Callers needing a strict type-check before casting should use [RTTS4b](#RTTS4b) `getType` or [RTTS4a](#RTTS4a) `exists` - `(RTTS6)` Typed `PathObject` sub-classes - the partition of methods across the hierarchy. Each sub-class extends [RTTS3](#RTTS3) and therefore inherits `path`, `instance`, `compactJson`, `subscribe`, `exists`, `getType`, and all `as*` helpers - `(RTTS6a)` `LiveMapPathObject` - adds `get` per [RTPO5](#RTPO5), `at` per [RTPO6](#RTPO6), `entries` per [RTPO9](#RTPO9), `keys` per [RTPO10](#RTPO10), `values` per [RTPO11](#RTPO11), `size` per [RTPO12](#RTPO12), `set` per [RTPO15](#RTPO15), `remove` per [RTPO16](#RTPO16). Failure semantics for each method when the path does not resolve to an `InternalLiveMap` are inherited from the referenced `RTPO*` clauses unchanged - - `(RTTS6b)` `LiveCounterPathObject` - adds `increment` per [RTPO17](#RTPO17), `decrement` per [RTPO18](#RTPO18), and a `value()` method delegating to [RTPO7](#RTPO7) with the return type narrowed to `Number?` (returns null per [RTPO3c1](#RTPO3c1) / [RTPO7e](#RTPO7e) if the resolved value is not an `InternalLiveCounter`) + - `(RTTS6b)` `LiveCounterPathObject` - adds `increment` per [RTPO17](#RTPO17), `decrement` per [RTPO18](#RTPO18), and a `value()` method delegating to [RTPO7](#RTPO7) with the return type narrowed to `Number?` (returns null per [RTPO3c1](#RTPO3c1) / [RTPO7e](#RTPO7e) if the resolved value is not an `InternalLiveCounter`). SDKs may narrow this further to the host's idiomatic floating-point type (e.g. `Double?`), since a counter's value is always a finite floating-point number per [RTLCV4a](#RTLCV4a) - `(RTTS6c)` Primitive `PathObject` sub-classes - one per primitive: `NumberPathObject`, `StringPathObject`, `BooleanPathObject`, `BinaryPathObject`, `JsonObjectPathObject`, `JsonArrayPathObject`. Each adds only a `value()` method delegating to [RTPO7](#RTPO7) with the return type narrowed to its corresponding primitive (returning null if the resolved value is not of that primitive type, per [RTPO3c1](#RTPO3c1)). They do not expose `get`, `at`, or any write / iteration methods - `(RTTS6d)` The `PathObject` returned by `RealtimeObject#get` ([RTO23](#RTO23)) must be of static type `LiveMapPathObject` in typed SDKs. This is well-defined because [RTO23d](#RTO23d) always returns a `PathObject` rooted at the channel's root `InternalLiveMap` with an empty path - `(RTTS6e)` On all sub-classes other than `LiveMapPathObject` and `LiveCounterPathObject`, calling `instance()` ([RTTS3b](#RTTS3b)) must either return null without side-effects (consistent with [RTPO8d](#RTPO8d)) or throw an `ErrorInfo` with `statusCode` 400 and `code` 92007. SDKs should prefer the null behaviour for consistency with [RTPO8d](#RTPO8d); the throwing behaviour is permitted because the typed contract makes it statically obvious that `instance` is meaningless on a primitive view @@ -1158,7 +1158,7 @@ This section partitions the API surface of `PathObject` and `Instance` across a - `(RTTS9d2)` Write and terminal operations follow [RTINS12d](#RTINS12d), [RTINS13d](#RTINS13d), [RTINS14d](#RTINS14d), [RTINS15d](#RTINS15d) - throw an `ErrorInfo` with `statusCode` 400 and `code` 92007 if the wrapped type does not match - `(RTTS10)` Typed `Instance` sub-classes - the partition of methods across the hierarchy. Each sub-class extends [RTTS7](#RTTS7) and therefore inherits `compactJson`, `getType`, and all `as*` helpers. Note that `subscribe` is not inherited from the base - see [RTTS7b](#RTTS7b) - `(RTTS10a)` `LiveMapInstance` - adds `id` per [RTINS3a](#RTINS3a) with the static return type narrowed to non-nullable `String`; `get` per [RTINS5](#RTINS5); `entries` per [RTINS6](#RTINS6); `keys` per [RTINS7](#RTINS7); `values` per [RTINS8](#RTINS8); `size` per [RTINS9](#RTINS9) with the return type narrowed to non-nullable `Number` (since the wrapped value is always an `InternalLiveMap`, [RTINS9c](#RTINS9c) cannot trigger); `set` per [RTINS12](#RTINS12); `remove` per [RTINS13](#RTINS13); `subscribe` per [RTINS16](#RTINS16). Typed SDKs must deliver the full `InstanceSubscriptionEvent` payload defined in [RTINS16e](#RTINS16e) to the listener - both the `object` field ([RTINS16e1](#RTINS16e1)) and the optional `message` field ([RTINS16e2](#RTINS16e2)) - and must not expose a subscription-event type that omits the `message` accessor - - `(RTTS10b)` `LiveCounterInstance` - adds `id` per [RTINS3a](#RTINS3a) (narrowed to non-nullable `String`); a `value()` method delegating to [RTINS4](#RTINS4) with the return type narrowed to non-nullable `Number`; `increment` per [RTINS14](#RTINS14); `decrement` per [RTINS15](#RTINS15); `subscribe` per [RTINS16](#RTINS16) with the same `InstanceSubscriptionEvent` payload requirements as [RTTS10a](#RTTS10a) + - `(RTTS10b)` `LiveCounterInstance` - adds `id` per [RTINS3a](#RTINS3a) (narrowed to non-nullable `String`); a `value()` method delegating to [RTINS4](#RTINS4) with the return type narrowed to non-nullable `Number` (which SDKs may narrow further to the host's idiomatic floating-point type, e.g. `Double`, since a counter's value is always a finite floating-point number per [RTLCV4a](#RTLCV4a)); `increment` per [RTINS14](#RTINS14); `decrement` per [RTINS15](#RTINS15); `subscribe` per [RTINS16](#RTINS16) with the same `InstanceSubscriptionEvent` payload requirements as [RTTS10a](#RTTS10a) - `(RTTS10c)` Primitive `Instance` sub-classes - one per primitive: `NumberInstance`, `StringInstance`, `BooleanInstance`, `BinaryInstance`, `JsonObjectInstance`, `JsonArrayInstance`. Each adds only a `value()` method delegating to [RTINS4](#RTINS4) with the return type narrowed to non-nullable instance of its corresponding primitive. These sub-classes are read-only - they do not expose `id` (primitive instances have no object id per [RTINS3b](#RTINS3b)), `get`, `set`, `remove`, `increment`, `decrement`, `entries`, `keys`, `values`, `size`, or `subscribe`. The absence of `subscribe` is a compile-time enforcement of the constraint already imposed at runtime by [RTINS16c](#RTINS16c) - `(RTTS10d)` (non-normative) Sub-classes may share a common abstract intermediate super-class (e.g. `PrimitiveInstance`) for implementation convenience, as long as the public API surface seen by user code matches the partition above @@ -1374,7 +1374,7 @@ Types and their properties/methods are public and exposed to users by default. A compactJson() -> Object? // RTPO14 subscribe((PathObjectSubscriptionEvent) -> listener, PathObjectSubscriptionOptions? options) -> Subscription // RTPO19 exists() -> Boolean // RTTS4a - getType() -> ValueType // RTTS4b + getType() -> ValueType? // RTTS4b asLiveMap() -> LiveMapPathObject // RTTS5a asLiveCounter() -> LiveCounterPathObject // RTTS5b asNumber() -> NumberPathObject // RTTS5c From a3386db46229f3c20199094e556f97e6d8d0ac8c Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Thu, 18 Jun 2026 16:32:07 +0530 Subject: [PATCH 05/13] Fixed `Instance#as*` spec point, made them fail-fast unline `PathObject#as*` --- 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 4481fc807..1ac98e80a 100644 --- a/specifications/objects-features.md +++ b/specifications/objects-features.md @@ -1149,13 +1149,12 @@ This section partitions the API surface of `PathObject` and `Instance` across a - `(RTTS8)` Typed `Instance` extension property - best-effort helper introduced by the typed-SDK variant. Unlike [RTTS4](#RTTS4) on `PathObject`, an `Instance` is bound to a resolved value at construction time, so the helper has O(1) complexity and is therefore exposed as a property in host languages that distinguish properties from methods - `(RTTS8a)` `getType` - returns the `ValueType` ([RTTS2](#RTTS2)) of the wrapped value. Returns the matching `ValueType` member per [RTTS2a](#RTTS2a). Must not return `UNKNOWN` in normal operation because an `Instance` is always constructed from a resolved value (see e.g. [RTPO8c](#RTPO8c), [RTINS5c](#RTINS5c)); see [RTTS2a9](#RTTS2a9) for the future-compatibility caveat - `(RTTS8b)` There is no `exists` helper on `Instance`, because an `Instance` is only ever constructed when it wraps a resolved value; the existence question is unambiguous -- `(RTTS9)` Typed `Instance` `as*` cast helpers - best-effort accessors mirroring [RTTS5](#RTTS5). Exposed as properties in host languages that distinguish properties from methods, since the wrapped value is already resolved +- `(RTTS9)` Typed `Instance` `as*` cast helpers - type-checked accessors. Unlike the best-effort `PathObject` casts ([RTTS5](#RTTS5)), an `Instance` wraps an already-resolved value of a known, fixed type ([RTTS8a](#RTTS8a)), so a mismatched cast is a programming error and these helpers throw rather than re-wrap ([RTTS9d](#RTTS9d)). Exposed as properties in host languages that distinguish properties from methods, since the wrapped value is already resolved - `(RTTS9a)` `asLiveMap` returns a `LiveMapInstance` ([RTTS10a](#RTTS10a)) - `(RTTS9b)` `asLiveCounter` returns a `LiveCounterInstance` ([RTTS10b](#RTTS10b)) - `(RTTS9c)` `asNumber`, `asString`, `asBoolean`, `asBinary`, `asJsonObject`, `asJsonArray` each return the corresponding primitive sub-class per [RTTS10c](#RTTS10c) - - `(RTTS9d)` These helpers must not throw based on the wrapped type. Operations on the returned wrapper carry their own failure semantics: - - `(RTTS9d1)` Read operations follow [RTINS4d](#RTINS4d), [RTINS5d](#RTINS5d), [RTINS6c](#RTINS6c), [RTINS7c](#RTINS7c), [RTINS8c](#RTINS8c), [RTINS9c](#RTINS9c) - return null/empty if the wrapped type does not match - - `(RTTS9d2)` Write and terminal operations follow [RTINS12d](#RTINS12d), [RTINS13d](#RTINS13d), [RTINS14d](#RTINS14d), [RTINS15d](#RTINS15d) - throw an `ErrorInfo` with `statusCode` 400 and `code` 92007 if the wrapped type does not match + - `(RTTS9d)` If the wrapped value is not of the requested type, these helpers must fail fast: throw a platform-appropriate unchecked exception indicating the type mismatch (e.g. `IllegalStateException`), or equivalently an `ErrorInfo` with `statusCode` 400 and `code` 92007. This differs from the best-effort `PathObject` casts ([RTTS5d](#RTTS5d)), which never throw on cast, because an `Instance`'s wrapped type is known and fixed at construction time and so a mismatched cast cannot be a transient resolution artefact. A matching cast returns the typed sub-class view; that view therefore always matches the wrapped type, so its own read/write operations cannot fail on a type mismatch + - `(RTTS9d1)` (non-normative) Callers needing to discriminate the type before casting should use [RTTS8a](#RTTS8a) `getType` - `(RTTS10)` Typed `Instance` sub-classes - the partition of methods across the hierarchy. Each sub-class extends [RTTS7](#RTTS7) and therefore inherits `compactJson`, `getType`, and all `as*` helpers. Note that `subscribe` is not inherited from the base - see [RTTS7b](#RTTS7b) - `(RTTS10a)` `LiveMapInstance` - adds `id` per [RTINS3a](#RTINS3a) with the static return type narrowed to non-nullable `String`; `get` per [RTINS5](#RTINS5); `entries` per [RTINS6](#RTINS6); `keys` per [RTINS7](#RTINS7); `values` per [RTINS8](#RTINS8); `size` per [RTINS9](#RTINS9) with the return type narrowed to non-nullable `Number` (since the wrapped value is always an `InternalLiveMap`, [RTINS9c](#RTINS9c) cannot trigger); `set` per [RTINS12](#RTINS12); `remove` per [RTINS13](#RTINS13); `subscribe` per [RTINS16](#RTINS16). Typed SDKs must deliver the full `InstanceSubscriptionEvent` payload defined in [RTINS16e](#RTINS16e) to the listener - both the `object` field ([RTINS16e1](#RTINS16e1)) and the optional `message` field ([RTINS16e2](#RTINS16e2)) - and must not expose a subscription-event type that omits the `message` accessor - `(RTTS10b)` `LiveCounterInstance` - adds `id` per [RTINS3a](#RTINS3a) (narrowed to non-nullable `String`); a `value()` method delegating to [RTINS4](#RTINS4) with the return type narrowed to non-nullable `Number` (which SDKs may narrow further to the host's idiomatic floating-point type, e.g. `Double`, since a counter's value is always a finite floating-point number per [RTLCV4a](#RTLCV4a)); `increment` per [RTINS14](#RTINS14); `decrement` per [RTINS15](#RTINS15); `subscribe` per [RTINS16](#RTINS16) with the same `InstanceSubscriptionEvent` payload requirements as [RTTS10a](#RTTS10a) From 2e0b608dda29fb07a8b4778a549a5fc6ad0540f2 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Thu, 18 Jun 2026 16:59:27 +0530 Subject: [PATCH 06/13] Removed unnecessary intro section from `InternalLiveMap` --- specifications/objects-features.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/specifications/objects-features.md b/specifications/objects-features.md index 1ac98e80a..714fb93d4 100644 --- a/specifications/objects-features.md +++ b/specifications/objects-features.md @@ -523,8 +523,6 @@ Objects feature enables clients to store shared data as "objects" on a channel. ### InternalLiveMap -Read methods (`InternalLiveMap#get` ([RTLM5](#RTLM5)), `entries` ([RTLM11](#RTLM11)), `values` ([RTLM13](#RTLM13))) return the live graph objects (`InternalLiveMap`, `InternalLiveCounter`) resolved from the internal `ObjectsPool`. Write methods do not accept graph objects: object-valued writes to `InternalLiveMap#set` ([RTLM20](#RTLM20)) take the immutable creation value types `LiveCounter` ([RTLCV1](#RTLCV1)) and `LiveMap` ([RTLMV1](#RTLMV1)), which are evaluated into `*_CREATE` operations at write time ([RTLM20e7g](#RTLM20e7g)). Assigning a reference to an existing graph object is not supported by this API. - - `(RTLM1)` The `InternalLiveMap` extends `LiveObject` - `(RTLM2)` Represents the map object type for Object IDs of type `map` - `(RTLM3)` Holds a `Dict` as a private `data` map From 88dffd7a0fb355734623b8ecb42b216b67dc2d6e Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Thu, 18 Jun 2026 17:24:03 +0530 Subject: [PATCH 07/13] Marked `Instance` as abstract since resolved value type is known when instance is returned --- specifications/objects-features.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/specifications/objects-features.md b/specifications/objects-features.md index 714fb93d4..7e1e7339a 100644 --- a/specifications/objects-features.md +++ b/specifications/objects-features.md @@ -1115,6 +1115,7 @@ This section partitions the API surface of `PathObject` and `Instance` across a - `(RTTS3e)` Does not expose `get` ([RTPO5](#RTPO5)), `at` ([RTPO6](#RTPO6)), `value` ([RTPO7](#RTPO7)), `entries` ([RTPO9](#RTPO9)), `keys` ([RTPO10](#RTPO10)), `values` ([RTPO11](#RTPO11)), `size` ([RTPO12](#RTPO12)), `set` ([RTPO15](#RTPO15)), `remove` ([RTPO16](#RTPO16)), `increment` ([RTPO17](#RTPO17)) or `decrement` ([RTPO18](#RTPO18)) on the base class. Those are partitioned onto the sub-classes per [RTTS6](#RTTS6) - `(RTTS3f)` Does not expose `compact` ([RTPO13](#RTPO13)). Typed SDKs are not required to implement `compact`; `compactJson` is sufficient for the static-domain use-cases. This may be revisited in a future spec version - `(RTTS3g)` (non-normative) To traverse from a base `PathObject` reference into a map, callers cast first via `asLiveMap` ([RTTS5a](#RTTS5a)). For example, for a base `PathObject p`, deep navigation `p.asLiveMap().at("a.b.c")` is equivalent to successive `get` calls + - `(RTTS3h)` (non-normative) Unlike the typed `Instance` base ([RTTS7e](#RTTS7e)), the typed `PathObject` base type must be directly instantiable. Navigation via `get` ([RTPO5](#RTPO5)) and `at` ([RTPO6](#RTPO6)) returns a base `PathObject` for a location whose underlying value has not been resolved - and may not even exist - so its type is not yet known and there is no typed sub-class to return. A typed sub-class is only produced once the caller narrows the view via an `as*` cast ([RTTS5](#RTTS5)) - `(RTTS4)` Additional typed `PathObject` methods - best-effort helpers that exist only on the typed base class. They have O(n) complexity in the path length because they resolve the path at call time, and are therefore exposed as methods (not properties) even in host languages that distinguish the two - `(RTTS4a)` `exists()` - returns `Boolean` indicating whether a value currently resolves at the stored path. The check is best-effort and evaluates the local object tree at call time; the answer may change between this call and a subsequent operation as remote operations are applied. Useful as a guard before a write whose semantics depend on existence: - `(RTTS4a1)` Checks the access API preconditions per [RTO25](#RTO25) @@ -1144,6 +1145,7 @@ This section partitions the API surface of `PathObject` and `Instance` across a - `(RTTS7b)` Does not expose `subscribe` ([RTINS16](#RTINS16)) on the base class. `subscribe` is partitioned onto `LiveMapInstance` ([RTTS10a](#RTTS10a)) and `LiveCounterInstance` ([RTTS10b](#RTTS10b)) only. Rationale (non-normative): in a typed SDK every `Instance` reference has a known concrete type (or knowable via [RTTS8a](#RTTS8a) `getType`), and `subscribe` is meaningful only on `LiveObject` instances ([RTINS16c](#RTINS16c) already requires the dynamic-SDK form to throw on primitives). Moving the method off the base turns that limitation into a compile-time contract - `(RTTS7c)` Does not expose `id` ([RTINS3](#RTINS3)), `value` ([RTINS4](#RTINS4)), `get` ([RTINS5](#RTINS5)), `entries` ([RTINS6](#RTINS6)), `keys` ([RTINS7](#RTINS7)), `values` ([RTINS8](#RTINS8)), `size` ([RTINS9](#RTINS9)), `set` ([RTINS12](#RTINS12)), `remove` ([RTINS13](#RTINS13)), `increment` ([RTINS14](#RTINS14)) or `decrement` ([RTINS15](#RTINS15)) on the base class. Those are partitioned onto the sub-classes per [RTTS10](#RTTS10) - `(RTTS7d)` Does not expose `compact` ([RTINS10](#RTINS10)). See [RTTS3f](#RTTS3f) for rationale + - `(RTTS7e)` (non-normative) Unlike the typed `PathObject` base ([RTTS3h](#RTTS3h)), the typed `Instance` base type need never be instantiated directly and may be abstract / non-instantiable. An `Instance` is only ever obtained from an already-resolved value whose type is therefore known at construction time - for example via `PathObject#instance` ([RTPO8c](#RTPO8c)) or `Instance#get` ([RTINS5c](#RTINS5c)) - so a typed SDK can always construct the matching concrete sub-class of [RTTS10](#RTTS10). Consequently there is no "untyped `Instance`" the way there is an unresolved base `PathObject`: every `Instance` a caller can hold is one of the concrete typed sub-classes - `(RTTS8)` Typed `Instance` extension property - best-effort helper introduced by the typed-SDK variant. Unlike [RTTS4](#RTTS4) on `PathObject`, an `Instance` is bound to a resolved value at construction time, so the helper has O(1) complexity and is therefore exposed as a property in host languages that distinguish properties from methods - `(RTTS8a)` `getType` - returns the `ValueType` ([RTTS2](#RTTS2)) of the wrapped value. Returns the matching `ValueType` member per [RTTS2a](#RTTS2a). Must not return `UNKNOWN` in normal operation because an `Instance` is always constructed from a resolved value (see e.g. [RTPO8c](#RTPO8c), [RTINS5c](#RTINS5c)); see [RTTS2a9](#RTTS2a9) for the future-compatibility caveat - `(RTTS8b)` There is no `exists` helper on `Instance`, because an `Instance` is only ever constructed when it wraps a resolved value; the existence question is unambiguous @@ -1403,7 +1405,7 @@ Types and their properties/methods are public and exposed to users by default. A class JsonObjectPathObject extends PathObject: value() -> JsonObject? // RTTS6c class JsonArrayPathObject extends PathObject: value() -> JsonArray? // RTTS6c - class Instance: // RTTS7 + abstract class Instance: // RTTS7 (abstract / non-instantiable - see RTTS7e) compactJson() -> Object // RTINS11 (non-nullable per RTINS11c - universal invariant) getType: ValueType // RTTS8a asLiveMap: LiveMapInstance // RTTS9a From c0c80d2b51246c3bde2ca457fcc43024ec06f453 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Thu, 18 Jun 2026 17:44:39 +0530 Subject: [PATCH 08/13] Updated `Instance#compactJson` sub-classes to support specific return types --- specifications/objects-features.md | 44 ++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/specifications/objects-features.md b/specifications/objects-features.md index 7e1e7339a..1210a15a7 100644 --- a/specifications/objects-features.md +++ b/specifications/objects-features.md @@ -1141,7 +1141,10 @@ This section partitions the API surface of `PathObject` and `Instance` across a - `(RTTS6e)` On all sub-classes other than `LiveMapPathObject` and `LiveCounterPathObject`, calling `instance()` ([RTTS3b](#RTTS3b)) must either return null without side-effects (consistent with [RTPO8d](#RTPO8d)) or throw an `ErrorInfo` with `statusCode` 400 and `code` 92007. SDKs should prefer the null behaviour for consistency with [RTPO8d](#RTPO8d); the throwing behaviour is permitted because the typed contract makes it statically obvious that `instance` is meaningless on a primitive view - `(RTTS6f)` (non-normative) Sub-classes may share a common abstract intermediate super-class (e.g. `PrimitivePathObject`) for implementation convenience, as long as the public API surface seen by user code matches the partition above - `(RTTS7)` Typed `Instance` base class - the typed SDK's public root of the `Instance` hierarchy. The base class exposes only those methods of [RTINS1](#RTINS1) whose return type and semantics are identical regardless of the wrapped underlying type - - `(RTTS7a)` Exposes `compactJson` per [RTINS11](#RTINS11). The non-null invariant is universal per [RTINS11c](#RTINS11c); typed SDKs must reflect this by giving `Instance#compactJson` a non-nullable return type in the public API. Where the host language additionally supports covariant return types, sub-classes may further narrow per [RTTS3c](#RTTS3c) (e.g. `JsonObjectInstance#compactJson` returning the SDK's `JsonObject` type directly); implementations may choose not to narrow + - `(RTTS7a)` Exposes `compactJson` per [RTINS11](#RTINS11). The non-null invariant is universal per [RTINS11c](#RTINS11c); typed SDKs must reflect this by giving `Instance#compactJson` a non-nullable return type in the public API. The base `Instance#compactJson` returns the SDK's generic JSON element type (the common super-type of the narrowed types below). Where the host language additionally supports covariant return types, sub-classes may further narrow per [RTTS3c](#RTTS3c); implementations may choose not to narrow. When an SDK does narrow, the canonical narrowed return type per sub-class is: + - `(RTTS7a1)` a JSON object for `LiveMapInstance` ([RTTS10a](#RTTS10a)) and `JsonObjectInstance` ([RTTS10c](#RTTS10c)) + - `(RTTS7a2)` a JSON array for `JsonArrayInstance` ([RTTS10c](#RTTS10c)) + - `(RTTS7a3)` a JSON primitive (a scalar JSON value) for `LiveCounterInstance` ([RTTS10b](#RTTS10b)), `NumberInstance`, `StringInstance`, `BooleanInstance` and `BinaryInstance` ([RTTS10c](#RTTS10c)) - `(RTTS7b)` Does not expose `subscribe` ([RTINS16](#RTINS16)) on the base class. `subscribe` is partitioned onto `LiveMapInstance` ([RTTS10a](#RTTS10a)) and `LiveCounterInstance` ([RTTS10b](#RTTS10b)) only. Rationale (non-normative): in a typed SDK every `Instance` reference has a known concrete type (or knowable via [RTTS8a](#RTTS8a) `getType`), and `subscribe` is meaningful only on `LiveObject` instances ([RTINS16c](#RTINS16c) already requires the dynamic-SDK form to throw on primitives). Moving the method off the base turns that limitation into a compile-time contract - `(RTTS7c)` Does not expose `id` ([RTINS3](#RTINS3)), `value` ([RTINS4](#RTINS4)), `get` ([RTINS5](#RTINS5)), `entries` ([RTINS6](#RTINS6)), `keys` ([RTINS7](#RTINS7)), `values` ([RTINS8](#RTINS8)), `size` ([RTINS9](#RTINS9)), `set` ([RTINS12](#RTINS12)), `remove` ([RTINS13](#RTINS13)), `increment` ([RTINS14](#RTINS14)) or `decrement` ([RTINS15](#RTINS15)) on the base class. Those are partitioned onto the sub-classes per [RTTS10](#RTTS10) - `(RTTS7d)` Does not expose `compact` ([RTINS10](#RTINS10)). See [RTTS3f](#RTTS3f) for rationale @@ -1156,9 +1159,9 @@ This section partitions the API surface of `PathObject` and `Instance` across a - `(RTTS9d)` If the wrapped value is not of the requested type, these helpers must fail fast: throw a platform-appropriate unchecked exception indicating the type mismatch (e.g. `IllegalStateException`), or equivalently an `ErrorInfo` with `statusCode` 400 and `code` 92007. This differs from the best-effort `PathObject` casts ([RTTS5d](#RTTS5d)), which never throw on cast, because an `Instance`'s wrapped type is known and fixed at construction time and so a mismatched cast cannot be a transient resolution artefact. A matching cast returns the typed sub-class view; that view therefore always matches the wrapped type, so its own read/write operations cannot fail on a type mismatch - `(RTTS9d1)` (non-normative) Callers needing to discriminate the type before casting should use [RTTS8a](#RTTS8a) `getType` - `(RTTS10)` Typed `Instance` sub-classes - the partition of methods across the hierarchy. Each sub-class extends [RTTS7](#RTTS7) and therefore inherits `compactJson`, `getType`, and all `as*` helpers. Note that `subscribe` is not inherited from the base - see [RTTS7b](#RTTS7b) - - `(RTTS10a)` `LiveMapInstance` - adds `id` per [RTINS3a](#RTINS3a) with the static return type narrowed to non-nullable `String`; `get` per [RTINS5](#RTINS5); `entries` per [RTINS6](#RTINS6); `keys` per [RTINS7](#RTINS7); `values` per [RTINS8](#RTINS8); `size` per [RTINS9](#RTINS9) with the return type narrowed to non-nullable `Number` (since the wrapped value is always an `InternalLiveMap`, [RTINS9c](#RTINS9c) cannot trigger); `set` per [RTINS12](#RTINS12); `remove` per [RTINS13](#RTINS13); `subscribe` per [RTINS16](#RTINS16). Typed SDKs must deliver the full `InstanceSubscriptionEvent` payload defined in [RTINS16e](#RTINS16e) to the listener - both the `object` field ([RTINS16e1](#RTINS16e1)) and the optional `message` field ([RTINS16e2](#RTINS16e2)) - and must not expose a subscription-event type that omits the `message` accessor - - `(RTTS10b)` `LiveCounterInstance` - adds `id` per [RTINS3a](#RTINS3a) (narrowed to non-nullable `String`); a `value()` method delegating to [RTINS4](#RTINS4) with the return type narrowed to non-nullable `Number` (which SDKs may narrow further to the host's idiomatic floating-point type, e.g. `Double`, since a counter's value is always a finite floating-point number per [RTLCV4a](#RTLCV4a)); `increment` per [RTINS14](#RTINS14); `decrement` per [RTINS15](#RTINS15); `subscribe` per [RTINS16](#RTINS16) with the same `InstanceSubscriptionEvent` payload requirements as [RTTS10a](#RTTS10a) - - `(RTTS10c)` Primitive `Instance` sub-classes - one per primitive: `NumberInstance`, `StringInstance`, `BooleanInstance`, `BinaryInstance`, `JsonObjectInstance`, `JsonArrayInstance`. Each adds only a `value()` method delegating to [RTINS4](#RTINS4) with the return type narrowed to non-nullable instance of its corresponding primitive. These sub-classes are read-only - they do not expose `id` (primitive instances have no object id per [RTINS3b](#RTINS3b)), `get`, `set`, `remove`, `increment`, `decrement`, `entries`, `keys`, `values`, `size`, or `subscribe`. The absence of `subscribe` is a compile-time enforcement of the constraint already imposed at runtime by [RTINS16c](#RTINS16c) + - `(RTTS10a)` `LiveMapInstance` - adds `id` per [RTINS3a](#RTINS3a) with the static return type narrowed to non-nullable `String`; `get` per [RTINS5](#RTINS5); `entries` per [RTINS6](#RTINS6); `keys` per [RTINS7](#RTINS7); `values` per [RTINS8](#RTINS8); `size` per [RTINS9](#RTINS9) with the return type narrowed to non-nullable `Number` (since the wrapped value is always an `InternalLiveMap`, [RTINS9c](#RTINS9c) cannot trigger); `set` per [RTINS12](#RTINS12); `remove` per [RTINS13](#RTINS13); `subscribe` per [RTINS16](#RTINS16); and `compactJson` narrowed to a JSON object per [RTTS7a1](#RTTS7a1). Typed SDKs must deliver the full `InstanceSubscriptionEvent` payload defined in [RTINS16e](#RTINS16e) to the listener - both the `object` field ([RTINS16e1](#RTINS16e1)) and the optional `message` field ([RTINS16e2](#RTINS16e2)) - and must not expose a subscription-event type that omits the `message` accessor + - `(RTTS10b)` `LiveCounterInstance` - adds `id` per [RTINS3a](#RTINS3a) (narrowed to non-nullable `String`); a `value()` method delegating to [RTINS4](#RTINS4) with the return type narrowed to non-nullable `Number` (which SDKs may narrow further to the host's idiomatic floating-point type, e.g. `Double`, since a counter's value is always a finite floating-point number per [RTLCV4a](#RTLCV4a)); `increment` per [RTINS14](#RTINS14); `decrement` per [RTINS15](#RTINS15); `subscribe` per [RTINS16](#RTINS16) with the same `InstanceSubscriptionEvent` payload requirements as [RTTS10a](#RTTS10a); and `compactJson` narrowed to a JSON primitive per [RTTS7a3](#RTTS7a3) + - `(RTTS10c)` Primitive `Instance` sub-classes - one per primitive: `NumberInstance`, `StringInstance`, `BooleanInstance`, `BinaryInstance`, `JsonObjectInstance`, `JsonArrayInstance`. Each adds a `value()` method delegating to [RTINS4](#RTINS4) with the return type narrowed to non-nullable instance of its corresponding primitive, and narrows `compactJson` per [RTTS7a](#RTTS7a): `JsonObjectInstance` to a JSON object ([RTTS7a1](#RTTS7a1)), `JsonArrayInstance` to a JSON array ([RTTS7a2](#RTTS7a2)), and `NumberInstance`/`StringInstance`/`BooleanInstance`/`BinaryInstance` to a JSON primitive ([RTTS7a3](#RTTS7a3)). These sub-classes are read-only - they do not expose `id` (primitive instances have no object id per [RTINS3b](#RTINS3b)), `get`, `set`, `remove`, `increment`, `decrement`, `entries`, `keys`, `values`, `size`, or `subscribe`. The absence of `subscribe` is a compile-time enforcement of the constraint already imposed at runtime by [RTINS16c](#RTINS16c) - `(RTTS10d)` (non-normative) Sub-classes may share a common abstract intermediate super-class (e.g. `PrimitiveInstance`) for implementation convenience, as long as the public API surface seen by user code matches the partition above ### PublicAPI::ObjectMessage @@ -1406,7 +1409,7 @@ Types and their properties/methods are public and exposed to users by default. A class JsonArrayPathObject extends PathObject: value() -> JsonArray? // RTTS6c abstract class Instance: // RTTS7 (abstract / non-instantiable - see RTTS7e) - compactJson() -> Object // RTINS11 (non-nullable per RTINS11c - universal invariant) + compactJson() -> Object // RTINS11 (non-nullable per RTINS11c; generic JSON element type, sub-classes narrow per RTTS7a) getType: ValueType // RTTS8a asLiveMap: LiveMapInstance // RTTS9a asLiveCounter: LiveCounterInstance // RTTS9b @@ -1419,6 +1422,7 @@ Types and their properties/methods are public and exposed to users by default. A class LiveMapInstance extends Instance: // RTTS10a id: String // RTINS3a (narrowed to non-nullable) + compactJson() -> JsonObject // RTTS7a1 (narrowed) get(String key) -> Instance? // RTINS5 entries() -> [String, Instance][] // RTINS6 keys() -> String[] // RTINS7 @@ -1430,14 +1434,32 @@ Types and their properties/methods are public and exposed to users by default. A class LiveCounterInstance extends Instance: // RTTS10b id: String // RTINS3a (narrowed to non-nullable) + compactJson() -> JsonPrimitive // RTTS7a3 (narrowed) value() -> Number // RTINS4 (narrowed to non-nullable) increment(Number amount?) => io // RTINS14 decrement(Number amount?) => io // RTINS15 subscribe((InstanceSubscriptionEvent) -> listener) -> Subscription // RTINS16, RTTS7b - class NumberInstance extends Instance: value() -> Number // RTTS10c - class StringInstance extends Instance: value() -> String // RTTS10c - class BooleanInstance extends Instance: value() -> Boolean // RTTS10c - class BinaryInstance extends Instance: value() -> Binary // RTTS10c - class JsonObjectInstance extends Instance: value() -> JsonObject // RTTS10c - class JsonArrayInstance extends Instance: value() -> JsonArray // RTTS10c + class NumberInstance extends Instance: // RTTS10c + value() -> Number + compactJson() -> JsonPrimitive // RTTS7a3 (narrowed) + + class StringInstance extends Instance: // RTTS10c + value() -> String + compactJson() -> JsonPrimitive // RTTS7a3 (narrowed) + + class BooleanInstance extends Instance: // RTTS10c + value() -> Boolean + compactJson() -> JsonPrimitive // RTTS7a3 (narrowed) + + class BinaryInstance extends Instance: // RTTS10c + value() -> Binary + compactJson() -> JsonPrimitive // RTTS7a3 (narrowed) + + class JsonObjectInstance extends Instance: // RTTS10c + value() -> JsonObject + compactJson() -> JsonObject // RTTS7a1 (narrowed) + + class JsonArrayInstance extends Instance: // RTTS10c + value() -> JsonArray + compactJson() -> JsonArray // RTTS7a2 (narrowed) From 7592b8e13fb6be1199a591bfbcb6a9b8cd710319 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Thu, 18 Jun 2026 19:54:03 +0530 Subject: [PATCH 09/13] Address review: fix RTTS5d2 error-code refs and RTTS6b/6c filtered value() semantics - RTTS5d2: distinguish path-not-resolved (92005, RTPO3c2) from type-mismatch (92007, RTPO15e/16e/17e/18e) for typed PathObject write/terminal casts - RTTS6b/6c: describe filtered value() semantics (return null unless the resolved value is of the expected category) instead of plain delegation to RTPO7, which also returns counter/primitive values Co-Authored-By: Claude Opus 4.8 (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 1210a15a7..5722be20b 100644 --- a/specifications/objects-features.md +++ b/specifications/objects-features.md @@ -1131,12 +1131,12 @@ This section partitions the API surface of `PathObject` and `Instance` across a - `(RTTS5c)` `asNumber()`, `asString()`, `asBoolean()`, `asBinary()`, `asJsonObject()`, `asJsonArray()` each return the corresponding primitive sub-class per [RTTS6c](#RTTS6c) - `(RTTS5d)` These helpers must not throw based on the current resolved type; they only re-wrap. Operations on the returned wrapper carry their own failure semantics: - `(RTTS5d1)` Read operations (`value`, `instance`, `entries`, `keys`, `values`, `size`, `compactJson`) follow [RTPO3c1](#RTPO3c1) - return null/empty if the resolved type does not match the wrapper's expectation - - `(RTTS5d2)` Write and terminal operations (`set`, `remove`, `increment`, `decrement`) follow [RTPO3c2](#RTPO3c2) - throw an `ErrorInfo` with `statusCode` 400 and `code` 92007 if the resolved type does not match + - `(RTTS5d2)` Write and terminal operations (`set`, `remove`, `increment`, `decrement`) carry their own per-method failure semantics. They throw an `ErrorInfo` with `statusCode` 400 and `code` 92005 if the path does not resolve (per [RTPO3c2](#RTPO3c2)), or `code` 92007 if the path resolves but the value is not of the type the operation requires (per [RTPO15e](#RTPO15e) / [RTPO16e](#RTPO16e) for `set` / `remove`, and [RTPO17e](#RTPO17e) / [RTPO18e](#RTPO18e) for `increment` / `decrement`) - `(RTTS5e)` (non-normative) Callers needing a strict type-check before casting should use [RTTS4b](#RTTS4b) `getType` or [RTTS4a](#RTTS4a) `exists` - `(RTTS6)` Typed `PathObject` sub-classes - the partition of methods across the hierarchy. Each sub-class extends [RTTS3](#RTTS3) and therefore inherits `path`, `instance`, `compactJson`, `subscribe`, `exists`, `getType`, and all `as*` helpers - `(RTTS6a)` `LiveMapPathObject` - adds `get` per [RTPO5](#RTPO5), `at` per [RTPO6](#RTPO6), `entries` per [RTPO9](#RTPO9), `keys` per [RTPO10](#RTPO10), `values` per [RTPO11](#RTPO11), `size` per [RTPO12](#RTPO12), `set` per [RTPO15](#RTPO15), `remove` per [RTPO16](#RTPO16). Failure semantics for each method when the path does not resolve to an `InternalLiveMap` are inherited from the referenced `RTPO*` clauses unchanged - - `(RTTS6b)` `LiveCounterPathObject` - adds `increment` per [RTPO17](#RTPO17), `decrement` per [RTPO18](#RTPO18), and a `value()` method delegating to [RTPO7](#RTPO7) with the return type narrowed to `Number?` (returns null per [RTPO3c1](#RTPO3c1) / [RTPO7e](#RTPO7e) if the resolved value is not an `InternalLiveCounter`). SDKs may narrow this further to the host's idiomatic floating-point type (e.g. `Double?`), since a counter's value is always a finite floating-point number per [RTLCV4a](#RTLCV4a) - - `(RTTS6c)` Primitive `PathObject` sub-classes - one per primitive: `NumberPathObject`, `StringPathObject`, `BooleanPathObject`, `BinaryPathObject`, `JsonObjectPathObject`, `JsonArrayPathObject`. Each adds only a `value()` method delegating to [RTPO7](#RTPO7) with the return type narrowed to its corresponding primitive (returning null if the resolved value is not of that primitive type, per [RTPO3c1](#RTPO3c1)). They do not expose `get`, `at`, or any write / iteration methods + - `(RTTS6b)` `LiveCounterPathObject` - adds `increment` per [RTPO17](#RTPO17), `decrement` per [RTPO18](#RTPO18), and a `value()` method with the return type narrowed to `Number?`. It returns the counter's value (per [RTPO7c](#RTPO7c)) when the resolved value is an `InternalLiveCounter`, and `null` otherwise - including when the path does not resolve (per [RTPO3c1](#RTPO3c1)) or resolves to a primitive or an `InternalLiveMap`. Unlike the general `value` ([RTPO7](#RTPO7)), it never returns a primitive value. SDKs may narrow this further to the host's idiomatic floating-point type (e.g. `Double?`), since a counter's value is always a finite floating-point number per [RTLCV4a](#RTLCV4a) + - `(RTTS6c)` Primitive `PathObject` sub-classes - one per primitive: `NumberPathObject`, `StringPathObject`, `BooleanPathObject`, `BinaryPathObject`, `JsonObjectPathObject`, `JsonArrayPathObject`. Each adds a `value()` method with the return type narrowed to its corresponding primitive. It returns the resolved value only when that value is of the sub-class's exact primitive type, and `null` otherwise - including when the path does not resolve (per [RTPO3c1](#RTPO3c1)), resolves to a different primitive, or resolves to a `LiveObject` (so, unlike the general `value` ([RTPO7](#RTPO7)), `NumberPathObject#value()` never returns an `InternalLiveCounter`'s value). They do not expose `get`, `at`, or any write / iteration methods - `(RTTS6d)` The `PathObject` returned by `RealtimeObject#get` ([RTO23](#RTO23)) must be of static type `LiveMapPathObject` in typed SDKs. This is well-defined because [RTO23d](#RTO23d) always returns a `PathObject` rooted at the channel's root `InternalLiveMap` with an empty path - `(RTTS6e)` On all sub-classes other than `LiveMapPathObject` and `LiveCounterPathObject`, calling `instance()` ([RTTS3b](#RTTS3b)) must either return null without side-effects (consistent with [RTPO8d](#RTPO8d)) or throw an `ErrorInfo` with `statusCode` 400 and `code` 92007. SDKs should prefer the null behaviour for consistency with [RTPO8d](#RTPO8d); the throwing behaviour is permitted because the typed contract makes it statically obvious that `instance` is meaningless on a primitive view - `(RTTS6f)` (non-normative) Sub-classes may share a common abstract intermediate super-class (e.g. `PrimitivePathObject`) for implementation convenience, as long as the public API surface seen by user code matches the partition above From 9d4ad9471b571fe06352d9a633e0e705bdc5dc90 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Thu, 18 Jun 2026 20:27:42 +0530 Subject: [PATCH 10/13] Consistency follow-ups: PathObject compactJson narrowing + filtered value() note - RTTS3c: clarify typed PathObject sub-classes do NOT narrow compactJson (best-effort cast, nullable, type not guaranteed) - unlike Instance (RTTS7a), whose fixed type makes covariant narrowing sound. Matches ably-java, where compactJson is declared only on the base PathObject - RTTS7a: drop the stale "narrow per RTTS3c" reference; state narrowing is sound for Instance because its type is fixed, contrasting PathObject - RTTS6g: add non-normative note that the type-filtered value() on LiveCounterPathObject/primitive PathObjects is a typed-SDK refinement with no RTPO7 (dynamic) equivalent Co-Authored-By: Claude Opus 4.8 (1M context) --- specifications/objects-features.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/specifications/objects-features.md b/specifications/objects-features.md index 5722be20b..b38993e2d 100644 --- a/specifications/objects-features.md +++ b/specifications/objects-features.md @@ -1110,7 +1110,7 @@ This section partitions the API surface of `PathObject` and `Instance` across a - `(RTTS3)` Typed `PathObject` base class - the typed SDK's public root of the `PathObject` hierarchy. The base class exposes only those methods of [RTPO1](#RTPO1) whose return type and semantics are identical regardless of the resolved underlying type - `(RTTS3a)` Exposes `path` per [RTPO4](#RTPO4) - `(RTTS3b)` Exposes `instance` per [RTPO8](#RTPO8). On sub-classes other than `LiveMapPathObject` ([RTTS6a](#RTTS6a)) and `LiveCounterPathObject` ([RTTS6b](#RTTS6b)), the behaviour of `instance` is constrained by [RTTS6e](#RTTS6e) - - `(RTTS3c)` Exposes `compactJson` per [RTPO14](#RTPO14). Where the host language supports covariant return types, sub-classes may narrow the return type (e.g. `JsonObjectPathObject#compactJson` returning the SDK's `JsonObject` rather than its generic JSON element type). Implementations may choose not to narrow + - `(RTTS3c)` Exposes `compactJson` per [RTPO14](#RTPO14), with the SDK's generic, nullable JSON element type as its return type. Unlike the typed `Instance` sub-classes ([RTTS7a](#RTTS7a)), the typed `PathObject` sub-classes do not narrow this return type: a `PathObject` cast is best-effort and `compactJson` may resolve to a value of any type, or fail to resolve and return null (per [RTPO3c1](#RTPO3c1)), so no narrower type can be guaranteed for any sub-class - `(RTTS3d)` Exposes `subscribe` per [RTPO19](#RTPO19). Typed SDKs must deliver the full `PathObjectSubscriptionEvent` payload defined in [RTPO19e](#RTPO19e) to the listener - both the `object` field ([RTPO19e1](#RTPO19e1)) and the optional `message` field ([RTPO19e2](#RTPO19e2)) - so that user code can inspect the `PublicAPI::ObjectMessage` ([PAOM1](#PAOM1)) that triggered the change. Typed SDKs must not expose a subscription-event type that omits the `message` accessor - `(RTTS3e)` Does not expose `get` ([RTPO5](#RTPO5)), `at` ([RTPO6](#RTPO6)), `value` ([RTPO7](#RTPO7)), `entries` ([RTPO9](#RTPO9)), `keys` ([RTPO10](#RTPO10)), `values` ([RTPO11](#RTPO11)), `size` ([RTPO12](#RTPO12)), `set` ([RTPO15](#RTPO15)), `remove` ([RTPO16](#RTPO16)), `increment` ([RTPO17](#RTPO17)) or `decrement` ([RTPO18](#RTPO18)) on the base class. Those are partitioned onto the sub-classes per [RTTS6](#RTTS6) - `(RTTS3f)` Does not expose `compact` ([RTPO13](#RTPO13)). Typed SDKs are not required to implement `compact`; `compactJson` is sufficient for the static-domain use-cases. This may be revisited in a future spec version @@ -1140,8 +1140,9 @@ This section partitions the API surface of `PathObject` and `Instance` across a - `(RTTS6d)` The `PathObject` returned by `RealtimeObject#get` ([RTO23](#RTO23)) must be of static type `LiveMapPathObject` in typed SDKs. This is well-defined because [RTO23d](#RTO23d) always returns a `PathObject` rooted at the channel's root `InternalLiveMap` with an empty path - `(RTTS6e)` On all sub-classes other than `LiveMapPathObject` and `LiveCounterPathObject`, calling `instance()` ([RTTS3b](#RTTS3b)) must either return null without side-effects (consistent with [RTPO8d](#RTPO8d)) or throw an `ErrorInfo` with `statusCode` 400 and `code` 92007. SDKs should prefer the null behaviour for consistency with [RTPO8d](#RTPO8d); the throwing behaviour is permitted because the typed contract makes it statically obvious that `instance` is meaningless on a primitive view - `(RTTS6f)` (non-normative) Sub-classes may share a common abstract intermediate super-class (e.g. `PrimitivePathObject`) for implementation convenience, as long as the public API surface seen by user code matches the partition above + - `(RTTS6g)` (non-normative) The type-filtered `value()` semantics of `LiveCounterPathObject` ([RTTS6b](#RTTS6b)) and the primitive sub-classes ([RTTS6c](#RTTS6c)) are a typed-SDK refinement and have no equivalent in the dynamic `PathObject#value` ([RTPO7](#RTPO7)): [RTPO7](#RTPO7) returns a resolved `InternalLiveCounter`'s value and any resolved primitive without regard to an expected type, whereas these typed accessors return `null` unless the resolved value matches the sub-class's specific category. Implementers must not assume the broader [RTPO7](#RTPO7) behaviour when implementing these typed accessors - `(RTTS7)` Typed `Instance` base class - the typed SDK's public root of the `Instance` hierarchy. The base class exposes only those methods of [RTINS1](#RTINS1) whose return type and semantics are identical regardless of the wrapped underlying type - - `(RTTS7a)` Exposes `compactJson` per [RTINS11](#RTINS11). The non-null invariant is universal per [RTINS11c](#RTINS11c); typed SDKs must reflect this by giving `Instance#compactJson` a non-nullable return type in the public API. The base `Instance#compactJson` returns the SDK's generic JSON element type (the common super-type of the narrowed types below). Where the host language additionally supports covariant return types, sub-classes may further narrow per [RTTS3c](#RTTS3c); implementations may choose not to narrow. When an SDK does narrow, the canonical narrowed return type per sub-class is: + - `(RTTS7a)` Exposes `compactJson` per [RTINS11](#RTINS11). The non-null invariant is universal per [RTINS11c](#RTINS11c); typed SDKs must reflect this by giving `Instance#compactJson` a non-nullable return type in the public API. The base `Instance#compactJson` returns the SDK's generic JSON element type (the common super-type of the narrowed types below). Where the host language additionally supports covariant return types, sub-classes may narrow this return type covariantly (sound here because an `Instance`'s wrapped type is fixed and known - contrast `PathObject` per [RTTS3c](#RTTS3c)); implementations may choose not to narrow. When an SDK does narrow, the canonical narrowed return type per sub-class is: - `(RTTS7a1)` a JSON object for `LiveMapInstance` ([RTTS10a](#RTTS10a)) and `JsonObjectInstance` ([RTTS10c](#RTTS10c)) - `(RTTS7a2)` a JSON array for `JsonArrayInstance` ([RTTS10c](#RTTS10c)) - `(RTTS7a3)` a JSON primitive (a scalar JSON value) for `LiveCounterInstance` ([RTTS10b](#RTTS10b)), `NumberInstance`, `StringInstance`, `BooleanInstance` and `BinaryInstance` ([RTTS10c](#RTTS10c)) From 10aa475f0e9cba65b2142ee05b57a84239487603 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Thu, 6 Aug 2026 20:28:03 +0530 Subject: [PATCH 11/13] Add informative Swift-specific IDL section documenting the shipped surface and RTTS deviations Records the AblyLiveObjects public surface (declaration listing incl. LiveMapValue, LiveMap/LiveCounter creation blueprints and JSONValue) and a 13-row table of deviations from the RTTS partition, each verifiable against the shipped source. Also notes the Swift-additive events() AsyncStream contract (stream termination auto-unsubscribes). --- specifications/objects-features.md | 170 +++++++++++++++++++++++++++++ 1 file changed, 170 insertions(+) diff --git a/specifications/objects-features.md b/specifications/objects-features.md index b38993e2d..e6ad1ce50 100644 --- a/specifications/objects-features.md +++ b/specifications/objects-features.md @@ -1464,3 +1464,173 @@ Types and their properties/methods are public and exposed to users by default. A class JsonArrayInstance extends Instance: // RTTS10c value() -> JsonArray compactJson() -> JsonArray // RTTS7a2 (narrowed) + +### Swift-specific IDL (informative) + +This subsection is **informative and non-normative**. Statically-typed SDKs follow the RTTS partition ([RTTS3](#RTTS3) through [RTTS10](#RTTS10)); the shipped Swift SDK (`AblyLiveObjects`) satisfies the RTTS *semantics* but expresses them through platform-idiomatic constructs that deviate from the *letter* of the partition in the enumerated places below. It is recorded here so that cross-SDK readers and test authors know the exact Swift surface. The portability intent of [RTTS1a](#RTTS1a) is that user code be portable *between statically-typed SDKs that agree on the partition*; Swift trades that letter-for-letter portability for compile-time type-safety (exhaustive enum discrimination, non-optional typed properties, typed `throws`), in the same spirit as the non-normative rationale under [RTTS1](#RTTS1). Every member below is verifiable against the shipped source under `LiveObjects/Sources/AblyLiveObjects/Path Based API/Public/`. + +The Swift surface (all top-level declarations carry an availability annotation, and closure-taking members carry the `@escaping`/`@Sendable` and `@discardableResult` attributes — both the annotation and these attributes are omitted here for brevity; read ops are `throws(ARTErrorInfo)` per [RTO25](#RTO25); mutating ops are `async throws(ARTErrorInfo)`; JSON is the native `JSONValue`): + + // === Swift-specific variant (informative) ================================= + // Shipped AblyLiveObjects public surface. Deviations from the RTTS partition + // are enumerated in the table below. `throws(ARTErrorInfo)` = typed throws; + // `any P` = an existential of protocol P; convenience/extension members are + // noted inline in comments rather than repeated as separate declarations. + // ========================================================================== + + enum ValueType: Sendable, Equatable // RTTS2 (lowerCamelCase, 9 cases) + case string // RTTS2a1 + case number // RTTS2a2 + case boolean // RTTS2a3 + case binary // RTTS2a4 + case jsonObject // RTTS2a5 + case jsonArray // RTTS2a6 + case liveMap // RTTS2a7 + case liveCounter // RTTS2a8 + case unknown // RTTS2a9 + + enum Primitive: Sendable, Equatable // Swift-only; collapses the 6 RTTS6c / RTTS10c primitives + case string(String) + case number(Double) + case bool(Bool) + case data(Data) + case jsonArray([JSONValue]) + case jsonObject([String: JSONValue]) + // + stringValue / numberValue / boolValue / dataValue / jsonArrayValue / jsonObjectValue getters + + protocol PathObject: Sendable // RTTS3 + var path: String { get } // RTPO4 + func instance() throws(ARTErrorInfo) -> Instance? // RTPO8 + func compactJson() throws(ARTErrorInfo) -> JSONValue? // RTPO14, RTTS3c (nullable; base-only, NOT narrowed) + func subscribe(options: PathObjectSubscriptionOptions?, listener: PathObjectSubscriptionCallback) throws(ARTErrorInfo) -> any Subscription // RTPO19 + func exists() throws(ARTErrorInfo) -> Bool // RTTS4a + func type() throws(ARTErrorInfo) -> ValueType? // RTTS4b (named `type`, NOT `getType`) + func asLiveMap() -> any LiveMapPathObject // RTTS5a + func asLiveCounter() -> any LiveCounterPathObject // RTTS5b + func asPrimitive() -> any PrimitivePathObject // RTTS5c (single cast; collapses asNumber/asString/asBoolean/asBinary/asJsonObject/asJsonArray) + // extension: subscribe(listener:) options-less convenience; events(options:) -> AsyncStream // RTPO19 (also on LiveMapInstance/LiveCounterInstance; Swift-additive - terminating the stream auto-calls the underlying Subscription's unsubscribe() per SUB2a; no spec point mandates it) + + protocol LiveMapPathObject: PathObject // RTTS6a + func get(key: String) -> any PathObject // RTPO5 + func at(path: String) -> any PathObject // RTPO6 + func entries() throws(ARTErrorInfo) -> [(key: String, value: any PathObject)] // RTPO9 + func keys() throws(ARTErrorInfo) -> [String] // RTPO10 + func values() throws(ARTErrorInfo) -> [any PathObject] // RTPO11 + func size() throws(ARTErrorInfo) -> Int? // RTPO12 + func set(key: String, value: LiveMapValue) async throws(ARTErrorInfo) // RTPO15 + func remove(key: String) async throws(ARTErrorInfo) // RTPO16 + + protocol LiveCounterPathObject: PathObject // RTTS6b + func value() throws(ARTErrorInfo) -> Double? // RTTS6b (counter-filtered: nil unless the resolved value is a counter) + func increment(amount: Double) async throws(ARTErrorInfo) // RTPO17 (+ increment() convenience) + func decrement(amount: Double) async throws(ARTErrorInfo) // RTPO18 (+ decrement() convenience) + + protocol PrimitivePathObject: PathObject // RTTS6c (collapsed) + func value() throws(ARTErrorInfo) -> Primitive? // RTTS6c, RTTS6g (nil unless a primitive; NOT filtered to one exact primitive type - see deviation 12) + + enum Instance: Sendable // RTTS7 + RTTS9 (abstract base + 8 throwing casts replaced by an enum) + case liveMap(any LiveMapInstance) + case liveCounter(any LiveCounterInstance) + case primitive(any PrimitiveInstance) + var type: ValueType { get } // RTTS8 (convenience kept alongside exhaustive `switch`; O(1)) + func compactJson() throws(ARTErrorInfo) -> JSONValue // RTINS11, RTINS11c (non-null), RTTS7a (NOT narrowed) + + protocol LiveMapInstance: Sendable // RTTS10a + var id: String { get } // RTINS3a (non-nullable) + func get(key: String) throws(ARTErrorInfo) -> Instance? // RTINS5 + func entries() throws(ARTErrorInfo) -> [(key: String, value: Instance)] // RTINS6 + func keys() throws(ARTErrorInfo) -> [String] // RTINS7 + func values() throws(ARTErrorInfo) -> [Instance] // RTINS8 + var size: Int { get throws(ARTErrorInfo) } // RTINS9, RTTS10a (throwing property; non-optional) + func set(key: String, value: LiveMapValue) async throws(ARTErrorInfo) // RTINS12 + func remove(key: String) async throws(ARTErrorInfo) // RTINS13 + func subscribe(listener: InstanceSubscriptionCallback) throws(ARTErrorInfo) -> any Subscription // RTINS16 (+ events()) + func compactJson() throws(ARTErrorInfo) -> JSONValue // RTINS11 (NOT narrowed to a JSON object; cf RTTS7a1) + + protocol LiveCounterInstance: Sendable // RTTS10b + var id: String { get } // RTINS3a (non-nullable) + var value: Double { get throws(ARTErrorInfo) } // RTINS4, RTTS10b (throwing property; non-optional) + func increment(amount: Double) async throws(ARTErrorInfo) // RTINS14 (+ increment() convenience) + func decrement(amount: Double) async throws(ARTErrorInfo) // RTINS15 (+ decrement() convenience) + func subscribe(listener: InstanceSubscriptionCallback) throws(ARTErrorInfo) -> any Subscription // RTINS16 (+ events()) + func compactJson() throws(ARTErrorInfo) -> JSONValue // RTINS11 (NOT narrowed to a JSON primitive; cf RTTS7a3) + + protocol PrimitiveInstance: Sendable // RTTS10c (collapsed; read-only, no id/subscribe) + var value: Primitive { get throws(ARTErrorInfo) } // RTINS4, RTTS10c (non-optional) + var type: ValueType { get } // RTTS8 (O(1)) + func compactJson() throws(ARTErrorInfo) -> JSONValue // RTINS11 (NOT narrowed; cf RTTS7a) + + protocol RealtimeObject: Sendable // RTO + func get() async throws(ARTErrorInfo) -> any LiveMapPathObject // RTO23, RTTS6d + func on(event: ObjectsEvent, callback: () -> Void) -> any StatusSubscription // RTO18 (zero-arg callback) + // channel entry point: ARTRealtimeChannel.object -> any RealtimeObject // RTL27 + + enum ObjectsEvent: Sendable // RTO18b + case syncing // RTO18b1 + case synced // RTO18b2 + + protocol Subscription: Sendable // SUB + func unsubscribe() // SUB2a, SUB2b + + protocol StatusSubscription: Sendable // RTO18f + func off() // RTO18f1 (per-token removal; there is no offAll()) + + struct PathObjectSubscriptionEvent: Sendable // RTPO19e + let object: any PathObject // RTPO19e1 + let message: ObjectMessage? // RTPO19e2 (stays Optional) + + struct PathObjectSubscriptionOptions: Sendable // RTPO19c + let depth: Int? // RTPO19c1 (init(depth:) is non-throwing; depth<=0 (40003, RTPO19c1a) validated in subscribe, not here) + + typealias PathObjectSubscriptionCallback = @Sendable (PathObjectSubscriptionEvent) -> Void // RTPO19a1 (non-throwing) + + struct InstanceSubscriptionEvent: Sendable // RTINS16e + let object: Instance // RTINS16e1 + let message: ObjectMessage? // RTINS16e2 + + typealias InstanceSubscriptionCallback = @Sendable (InstanceSubscriptionEvent) -> Void // RTINS16a1 (non-throwing) + + // Write-side value types referenced by set(key:value:) above. Swift replaces the language-agnostic + // set-value union (Boolean|Binary|Number|String|JsonArray|JsonObject|LiveCounter|LiveMap - RTLM20 / + // RTPO15 / RTINS12) with the LiveMapValue enum, and reuses the shared LiveMap/LiveCounter creation + // value types (RTLMV* / RTLCV*). ObjectMessage (see PathObjectSubscriptionEvent.message) is the shared + // PublicAPI::ObjectMessage type, not repeated here. + enum LiveMapValue: Sendable, Equatable + case primitive(Primitive) + case liveMap(LiveMap) + case liveCounter(LiveCounter) + // + ExpressibleBy{String,Integer,Float,Boolean,Array,Dictionary}Literal (with JSONValue's own literal + // conformances), so set(key:value:) accepts "x" / 1 / true / ["k": "v"] directly + // + primitiveValue / liveMapValue / liveCounterValue (+ 6 primitive) convenience getters + + struct LiveMap: Sendable, Equatable // RTLMV* blueprint (inert; live map created on set, not a graph object) + static func create(entries: [String: LiveMapValue]) -> LiveMap // RTLMV3 + static func create() -> LiveMap // RTLMV3 (empty; Swift no-arg overload of the optional-arg factory) + + struct LiveCounter: Sendable, Equatable // RTLCV* blueprint (inert; live counter created on set, not a graph object) + static func create(initialCount: Double) -> LiveCounter // RTLCV3 + static func create() -> LiveCounter // RTLCV3 (initial 0; Swift no-arg overload) + + // JSONValue: native indirect enum (.object/.array/.string/.number/.bool/.null); Sendable, Equatable, + // ExpressibleBy*Literal. The native JSON type returned/accepted everywhere JSON travels (compactJson, + // ObjectMessage.extras, primitive JSON values) - replaces the generic JSON element type (deviation 8) + +Deviations of the shipped Swift surface from the RTTS partition. Each row is verifiable against the source files referenced above: + +| # | RTTS point | Swift deviation | Description / rationale | +|---|---|---|---| +| 1 | [RTTS7](#RTTS7), [RTTS9](#RTTS9) (and [RTTS8](#RTTS8)) | `enum Instance { .liveMap / .liveCounter / .primitive }` with `switch` discrimination replaces the abstract base class plus the 8 throwing `as*` casts | Discrimination is compile-time-exhaustive, so there is no undefined type-mismatch path (the [RTTS9d](#RTTS9d) 92007 throw is unrepresentable). The [RTTS8](#RTTS8) `type` is kept as an O(1) convenience `var type: ValueType` that coexists with the `switch` | +| 2 | [RTTS5c](#RTTS5c), [RTTS6c](#RTTS6c) / [RTTS9c](#RTTS9c), [RTTS10c](#RTTS10c) | A single `asPrimitive() -> any PrimitivePathObject` + `PrimitivePathObject`, and a single `PrimitiveInstance`, replace the 6 primitive casts and the 6 primitive sub-classes; both front one `Primitive` enum | The six spec primitive categories become cases of the `Primitive` enum that the caller pattern-matches. Smaller, Swift-idiomatic surface; the 6 `asNumber/asString/...` casts and the `NumberPathObject`/`NumberInstance`/... sub-classes do not exist | +| 3 | [RTTS4b](#RTTS4b), [RTTS8a](#RTTS8a) | `type()` (method on `PathObject`) / `type` (property on `Instance`, `PrimitiveInstance`) instead of `getType` | Swift API Design Guidelines: a `getX` accessor becomes `x`. `PathObject.type()` is a method (O(path length) resolution per call); `Instance.type` is a property (O(1)) | +| 4 | [RTO25](#RTO25) (all `PathObject`/`Instance` reads) | All read operations are uniformly `throws(ARTErrorInfo)` | Uniform typed-throws surface for the RTO25 access-preconditions, broader than the spec's per-method choice (Java leaves most reads unchecked) | +| 5 | [RTPO15](#RTPO15)-[RTPO18](#RTPO18) / [RTINS12](#RTINS12)-[RTINS15](#RTINS15) (`=> io`) | Blocking + `*Async` callback pairs collapse to a single `async throws(ARTErrorInfo)` method | One structured-concurrency suspension model; no callback variants | +| 6 | [RTPO19](#RTPO19) / [RTINS16](#RTINS16) | Listener interfaces become closure `typealias`es (`PathObjectSubscriptionCallback`, `InstanceSubscriptionCallback`) plus additional `AsyncStream`-returning `events()` accessors | Swift closure + `AsyncSequence` idiom; `events()` is an additive convenience layered on `subscribe` | +| 7 | [RTO18](#RTO18), [RTO18f](#RTO18f) | Status callback is zero-arg `() -> Void`; deregistration is per-token `StatusSubscription.off()`; there is no `offAll()` and no listener-identity `off(listener)` | The event is known at registration (`on(event:)`), so it need not be passed to the callback; subscription-token removal is the Swift idiom | +| 8 | [RTPO14](#RTPO14) / [RTINS11](#RTINS11) / [RTTS7a](#RTTS7a) | Native `JSONValue` replaces the generic/gson JSON element type everywhere JSON is returned or accepted (`compactJson`, `ObjectMessage.extras`, primitive JSON values) | No third-party type dependency in the public surface | +| 9 | [RTPO19c1a](#RTPO19c1a) | `depth <= 0` (40003) is validated inside `subscribe(options:listener:)`, not in the `PathObjectSubscriptionOptions` initializer, which is non-throwing | The shipped `init(depth:)` is frozen non-throwing; the check moves to the subscribe call site | +| 10 | [RTO24b2c](#RTO24b2c) | Subscription callbacks are non-throwing (`PathObjectSubscriptionCallback` / `InstanceSubscriptionCallback` cannot `throw`) | The "a listener throws -> it is caught and the other listeners still receive the event" contract has no Swift surface; independent-listener delivery is provided, but a listener cannot signal failure back | +| 11 | [RTTS7a](#RTTS7a), [RTTS7a1](#RTTS7a1)-[RTTS7a3](#RTTS7a3) | The `Instance` sub-classes do **not** narrow `compactJson`; every `compactJson` (base enum, `LiveMapInstance`, `LiveCounterInstance`, `PrimitiveInstance`) returns `JSONValue` (non-optional) | Explicitly permitted: [RTTS7a](#RTTS7a) says implementations "may choose not to narrow". Swift uses one JSON type throughout rather than the canonical `JsonObject`/`JsonArray`/`JsonPrimitive` narrowings. (Relevant to recent commits `c0c80d2b`/`9d4ad947`, which added those narrowings to the language-agnostic IDL) | +| 12 | [RTTS6c](#RTTS6c), [RTTS6g](#RTTS6g) | `PrimitivePathObject.value() -> Primitive?` returns **any** resolved primitive, filtered only at the primitive-vs-`LiveObject` boundary; it is NOT filtered to one exact primitive type as the six [RTTS6c](#RTTS6c) sub-classes are. `LiveCounterPathObject.value() -> Double?` does match [RTTS6b](#RTTS6b) (counter-filtered) | Consequence of the primitive collapse (deviation 2): per-primitive-type filtering ([RTTS6c](#RTTS6c)) is performed by the caller via the `Primitive` enum. The [RTTS6g](#RTTS6g) "never returns a `LiveObject`'s value" property is preserved (a counter/map resolves to `nil`); the finer per-primitive filtering is not. (Relevant to recent commits `7592b8e1`/`9d4ad947`, which defined the filtered-`value()` semantics) | +| 13 | [RTTS6e](#RTTS6e) (and base [RTPO8d](#RTPO8d)) | `instance()` lives on the base `PathObject` and is inherited unchanged by `PrimitivePathObject`; on a primitive view it returns the resolved `Instance` (a `.primitive` case) rather than applying the RTTS6e narrowing (return `nil`, or throw `92007`, on a non-map/non-counter view) | RTTS6e (consistent with [RTPO8d](#RTPO8d)) requires the primitive path view's `instance()` to return null or throw `92007`. Because the six primitive sub-classes are collapsed into one that shares `DefaultPathObject.instance()` (deviation 2) and reads are uniform (deviation 4), the primitive view resolves and wraps the value like any other view (a `.primitive` `Instance` for a primitive), following [RTPO8c](#RTPO8c) uniformly rather than the [RTPO8d](#RTPO8d) primitive-returns-null branch. Verifiable in `DefaultPathObject.instance()` and `DefaultPrimitivePathObject` | + +> Not reproduced above (Swift conforms to the spec as written, so no deviation row): the base `PathObject` exposes no `value()` ([RTTS3e](#RTTS3e)) - callers use the typed `asPrimitive().value()` / `asLiveCounter().value()` route; `compact` is not implemented ([RTTS3f](#RTTS3f) / [RTTS7d](#RTTS7d) already make it optional, `compactJson` only); and the typed `PathObject` sub-classes do not narrow `compactJson` ([RTTS3c](#RTTS3c)), which Swift satisfies with a single nullable `compactJson() -> JSONValue?` on the base protocol (recent commit `9d4ad947`); the `Instance` base `enum` exposes no `subscribe`, which is partitioned onto `LiveMapInstance` / `LiveCounterInstance` only ([RTTS7b](#RTTS7b)); and there is no `exists` helper on `Instance` ([RTTS8b](#RTTS8b)), since an `Instance` always wraps an already-resolved value. From 9cf20867edc1afcecc7310bc8aa80c19330f6ea8 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Thu, 6 Aug 2026 21:25:36 +0530 Subject: [PATCH 12/13] Replace the Swift-specific IDL appendix with per-point notes and typed-SDK corrections The per-language declaration listing re-declared mostly-conforming surface and had no precedent in the spec suite. Cross-SDK verification against ably-js and ably-java showed most recorded deviations were already licensed by the normative text, so the appendix is removed in favour of: - RTTS6e replaced by RTPO8f (no SDK implements the null/92007 narrowing; primitive views wrap and return per RTPO8f), with the RTTS3b cross-ref updated to match - RTO19 softened: off/offAll may be omitted when on() returns the RTO18f subscription object (JS and Swift rely on per-token off) - New RTTS11/RTTS11a: LiveMapValue as the typed-SDK carrier of the map set-value union (Java abstract class + of() factories; Swift enum) - New non-normative notes for the genuinely Swift-only shapes: RTTS9e (exhaustive sum-type discrimination), RTTS6h (single asPrimitive() primitive collapse), RTPO19h (idiomatic streaming subscribe forms, e.g. subscribeIterator()/events()) --- specifications/objects-features.md | 181 ++--------------------------- 1 file changed, 8 insertions(+), 173 deletions(-) diff --git a/specifications/objects-features.md b/specifications/objects-features.md index 844f67746..039de566b 100644 --- a/specifications/objects-features.md +++ b/specifications/objects-features.md @@ -302,7 +302,7 @@ Objects feature enables clients to store shared data as "objects" on a channel. - `(RTO18f)` The client library may return a subscription object (or the idiomatic equivalent for the language) as a result of this operation: - `(RTO18f1)` The subscription object includes an `off` function - `(RTO18f2)` Calling `off` deregisters the listener previously registered by the user via the corresponding `on` call -- `(RTO19)` `RealtimeObject#off` function - deregisters an event listener previously registered via `RealtimeObject#on` ([RTO18](#RTO18)) +- `(RTO19)` `RealtimeObject#off` function - deregisters an event listener previously registered via `RealtimeObject#on` ([RTO18](#RTO18)). SDKs whose `on` returns the [RTO18f](#RTO18f) subscription object may omit this function (and any `offAll` variant) in favour of per-subscription deregistration via [RTO18f1](#RTO18f1) - `(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 @@ -1007,6 +1007,7 @@ A `PathObject` is obtained from `RealtimeObject#get` ([RTO23](#RTO23)), which re - `(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 + - `(RTPO19h)` (non-normative) SDKs may additionally expose an idiomatic streaming form of `subscribe`, on `PathObject` and on the `Instance` types that expose `subscribe` ([RTINS16](#RTINS16)) - e.g. JavaScript's `subscribeIterator()` returning an async iterator, or Swift's `events()` returning an `AsyncStream`. Terminating the stream deregisters the listener per [SUB2a](../features#SUB2a) ### Instance @@ -1117,7 +1118,7 @@ This section partitions the API surface of `PathObject` and `Instance` across a - `(RTTS2a9)` `UNKNOWN` - indicates that a value is present but its type falls into none of the categories above. This is distinct from the absence of a value: `PathObject#getType` ([RTTS4b](#RTTS4b)) returns `null` (not `UNKNOWN`) when no value resolves at the path. See [RTTS8a](#RTTS8a) for when this member may appear on `Instance#getType` - `(RTTS3)` Typed `PathObject` base class - the typed SDK's public root of the `PathObject` hierarchy. The base class exposes only those methods of [RTPO1](#RTPO1) whose return type and semantics are identical regardless of the resolved underlying type - `(RTTS3a)` Exposes `path` per [RTPO4](#RTPO4) - - `(RTTS3b)` Exposes `instance` per [RTPO8](#RTPO8). On sub-classes other than `LiveMapPathObject` ([RTTS6a](#RTTS6a)) and `LiveCounterPathObject` ([RTTS6b](#RTTS6b)), the behaviour of `instance` is constrained by [RTTS6e](#RTTS6e) + - `(RTTS3b)` Exposes `instance` per [RTPO8](#RTPO8). The behaviour is identical on all sub-classes, including [RTPO8f](#RTPO8f) for primitive views (see [RTTS6e](#RTTS6e)) - `(RTTS3c)` Exposes `compactJson` per [RTPO14](#RTPO14), with the SDK's generic, nullable JSON element type as its return type. Unlike the typed `Instance` sub-classes ([RTTS7a](#RTTS7a)), the typed `PathObject` sub-classes do not narrow this return type: a `PathObject` cast is best-effort and `compactJson` may resolve to a value of any type, or fail to resolve and return null (per [RTPO3c1](#RTPO3c1)), so no narrower type can be guaranteed for any sub-class - `(RTTS3d)` Exposes `subscribe` per [RTPO19](#RTPO19). Typed SDKs must deliver the full `PathObjectSubscriptionEvent` payload defined in [RTPO19e](#RTPO19e) to the listener - both the `object` field ([RTPO19e1](#RTPO19e1)) and the optional `message` field ([RTPO19e2](#RTPO19e2)) - so that user code can inspect the `PublicAPI::ObjectMessage` ([PAOM1](#PAOM1)) that triggered the change. Typed SDKs must not expose a subscription-event type that omits the `message` accessor - `(RTTS3e)` Does not expose `get` ([RTPO5](#RTPO5)), `at` ([RTPO6](#RTPO6)), `value` ([RTPO7](#RTPO7)), `entries` ([RTPO9](#RTPO9)), `keys` ([RTPO10](#RTPO10)), `values` ([RTPO11](#RTPO11)), `size` ([RTPO12](#RTPO12)), `set` ([RTPO15](#RTPO15)), `remove` ([RTPO16](#RTPO16)), `increment` ([RTPO17](#RTPO17)) or `decrement` ([RTPO18](#RTPO18)) on the base class. Those are partitioned onto the sub-classes per [RTTS6](#RTTS6) @@ -1146,9 +1147,10 @@ This section partitions the API surface of `PathObject` and `Instance` across a - `(RTTS6b)` `LiveCounterPathObject` - adds `increment` per [RTPO17](#RTPO17), `decrement` per [RTPO18](#RTPO18), and a `value()` method with the return type narrowed to `Number?`. It returns the counter's value (per [RTPO7c](#RTPO7c)) when the resolved value is an `InternalLiveCounter`, and `null` otherwise - including when the path does not resolve (per [RTPO3c1](#RTPO3c1)) or resolves to a primitive or an `InternalLiveMap`. Unlike the general `value` ([RTPO7](#RTPO7)), it never returns a primitive value. SDKs may narrow this further to the host's idiomatic floating-point type (e.g. `Double?`), since a counter's value is always a finite floating-point number per [RTLCV4a](#RTLCV4a) - `(RTTS6c)` Primitive `PathObject` sub-classes - one per primitive: `NumberPathObject`, `StringPathObject`, `BooleanPathObject`, `BinaryPathObject`, `JsonObjectPathObject`, `JsonArrayPathObject`. Each adds a `value()` method with the return type narrowed to its corresponding primitive. It returns the resolved value only when that value is of the sub-class's exact primitive type, and `null` otherwise - including when the path does not resolve (per [RTPO3c1](#RTPO3c1)), resolves to a different primitive, or resolves to a `LiveObject` (so, unlike the general `value` ([RTPO7](#RTPO7)), `NumberPathObject#value()` never returns an `InternalLiveCounter`'s value). They do not expose `get`, `at`, or any write / iteration methods - `(RTTS6d)` The `PathObject` returned by `RealtimeObject#get` ([RTO23](#RTO23)) must be of static type `LiveMapPathObject` in typed SDKs. This is well-defined because [RTO23d](#RTO23d) always returns a `PathObject` rooted at the channel's root `InternalLiveMap` with an empty path - - `(RTTS6e)` On all sub-classes other than `LiveMapPathObject` and `LiveCounterPathObject`, calling `instance()` ([RTTS3b](#RTTS3b)) must either return null without side-effects (consistent with [RTPO8d](#RTPO8d)) or throw an `ErrorInfo` with `statusCode` 400 and `code` 92007. SDKs should prefer the null behaviour for consistency with [RTPO8d](#RTPO8d); the throwing behaviour is permitted because the typed contract makes it statically obvious that `instance` is meaningless on a primitive view + - `(RTTS6e)` This clause has been replaced by [RTPO8f](#RTPO8f): `instance()` ([RTTS3b](#RTTS3b)) behaves per [RTPO8](#RTPO8) on all sub-classes, and the typed views do not filter or suppress it - on a primitive view it returns a new `Instance` wrapping the resolved primitive value - `(RTTS6f)` (non-normative) Sub-classes may share a common abstract intermediate super-class (e.g. `PrimitivePathObject`) for implementation convenience, as long as the public API surface seen by user code matches the partition above - `(RTTS6g)` (non-normative) The type-filtered `value()` semantics of `LiveCounterPathObject` ([RTTS6b](#RTTS6b)) and the primitive sub-classes ([RTTS6c](#RTTS6c)) are a typed-SDK refinement and have no equivalent in the dynamic `PathObject#value` ([RTPO7](#RTPO7)): [RTPO7](#RTPO7) returns a resolved `InternalLiveCounter`'s value and any resolved primitive without regard to an expected type, whereas these typed accessors return `null` unless the resolved value matches the sub-class's specific category. Implementers must not assume the broader [RTPO7](#RTPO7) behaviour when implementing these typed accessors + - `(RTTS6h)` (non-normative) The Swift SDK collapses the six primitive casts ([RTTS5c](#RTTS5c)) and sub-classes ([RTTS6c](#RTTS6c); [RTTS10c](#RTTS10c) on the `Instance` side) into a single `asPrimitive()` / `PrimitivePathObject` / `PrimitiveInstance` fronting a `Primitive` enum; the per-type filtering above is performed by the caller via pattern-matching over the enum - `(RTTS7)` Typed `Instance` base class - the typed SDK's public root of the `Instance` hierarchy. The base class exposes only those methods of [RTINS1](#RTINS1) whose return type and semantics are identical regardless of the wrapped underlying type - `(RTTS7a)` Exposes `compactJson` per [RTINS11](#RTINS11). The non-null invariant is universal per [RTINS11c](#RTINS11c); typed SDKs must reflect this by giving `Instance#compactJson` a non-nullable return type in the public API. The base `Instance#compactJson` returns the SDK's generic JSON element type (the common super-type of the narrowed types below). Where the host language additionally supports covariant return types, sub-classes may narrow this return type covariantly (sound here because an `Instance`'s wrapped type is fixed and known - contrast `PathObject` per [RTTS3c](#RTTS3c)); implementations may choose not to narrow. When an SDK does narrow, the canonical narrowed return type per sub-class is: - `(RTTS7a1)` a JSON object for `LiveMapInstance` ([RTTS10a](#RTTS10a)) and `JsonObjectInstance` ([RTTS10c](#RTTS10c)) @@ -1167,11 +1169,14 @@ This section partitions the API surface of `PathObject` and `Instance` across a - `(RTTS9c)` `asNumber`, `asString`, `asBoolean`, `asBinary`, `asJsonObject`, `asJsonArray` each return the corresponding primitive sub-class per [RTTS10c](#RTTS10c) - `(RTTS9d)` If the wrapped value is not of the requested type, these helpers must fail fast: throw a platform-appropriate unchecked exception indicating the type mismatch (e.g. `IllegalStateException`), or equivalently an `ErrorInfo` with `statusCode` 400 and `code` 92007. This differs from the best-effort `PathObject` casts ([RTTS5d](#RTTS5d)), which never throw on cast, because an `Instance`'s wrapped type is known and fixed at construction time and so a mismatched cast cannot be a transient resolution artefact. A matching cast returns the typed sub-class view; that view therefore always matches the wrapped type, so its own read/write operations cannot fail on a type mismatch - `(RTTS9d1)` (non-normative) Callers needing to discriminate the type before casting should use [RTTS8a](#RTTS8a) `getType` + - `(RTTS9e)` (non-normative) Languages with exhaustive sum types may replace the abstract `Instance` base and these cast helpers with a discriminated union (e.g. Swift's `enum Instance` with `liveMap` / `liveCounter` / `primitive` cases); discrimination is then compile-time-exhaustive and the [RTTS9d](#RTTS9d) failure path cannot occur - `(RTTS10)` Typed `Instance` sub-classes - the partition of methods across the hierarchy. Each sub-class extends [RTTS7](#RTTS7) and therefore inherits `compactJson`, `getType`, and all `as*` helpers. Note that `subscribe` is not inherited from the base - see [RTTS7b](#RTTS7b) - `(RTTS10a)` `LiveMapInstance` - adds `id` per [RTINS3a](#RTINS3a) with the static return type narrowed to non-nullable `String`; `get` per [RTINS5](#RTINS5); `entries` per [RTINS6](#RTINS6); `keys` per [RTINS7](#RTINS7); `values` per [RTINS8](#RTINS8); `size` per [RTINS9](#RTINS9) with the return type narrowed to non-nullable `Number` (since the wrapped value is always an `InternalLiveMap`, [RTINS9c](#RTINS9c) cannot trigger); `set` per [RTINS12](#RTINS12); `remove` per [RTINS13](#RTINS13); `subscribe` per [RTINS16](#RTINS16); and `compactJson` narrowed to a JSON object per [RTTS7a1](#RTTS7a1). Typed SDKs must deliver the full `InstanceSubscriptionEvent` payload defined in [RTINS16e](#RTINS16e) to the listener - both the `object` field ([RTINS16e1](#RTINS16e1)) and the optional `message` field ([RTINS16e2](#RTINS16e2)) - and must not expose a subscription-event type that omits the `message` accessor - `(RTTS10b)` `LiveCounterInstance` - adds `id` per [RTINS3a](#RTINS3a) (narrowed to non-nullable `String`); a `value()` method delegating to [RTINS4](#RTINS4) with the return type narrowed to non-nullable `Number` (which SDKs may narrow further to the host's idiomatic floating-point type, e.g. `Double`, since a counter's value is always a finite floating-point number per [RTLCV4a](#RTLCV4a)); `increment` per [RTINS14](#RTINS14); `decrement` per [RTINS15](#RTINS15); `subscribe` per [RTINS16](#RTINS16) with the same `InstanceSubscriptionEvent` payload requirements as [RTTS10a](#RTTS10a); and `compactJson` narrowed to a JSON primitive per [RTTS7a3](#RTTS7a3) - `(RTTS10c)` Primitive `Instance` sub-classes - one per primitive: `NumberInstance`, `StringInstance`, `BooleanInstance`, `BinaryInstance`, `JsonObjectInstance`, `JsonArrayInstance`. Each adds a `value()` method delegating to [RTINS4](#RTINS4) with the return type narrowed to non-nullable instance of its corresponding primitive, and narrows `compactJson` per [RTTS7a](#RTTS7a): `JsonObjectInstance` to a JSON object ([RTTS7a1](#RTTS7a1)), `JsonArrayInstance` to a JSON array ([RTTS7a2](#RTTS7a2)), and `NumberInstance`/`StringInstance`/`BooleanInstance`/`BinaryInstance` to a JSON primitive ([RTTS7a3](#RTTS7a3)). These sub-classes are read-only - they do not expose `id` (primitive instances have no object id per [RTINS3b](#RTINS3b)), `get`, `set`, `remove`, `increment`, `decrement`, `entries`, `keys`, `values`, `size`, or `subscribe`. The absence of `subscribe` is a compile-time enforcement of the constraint already imposed at runtime by [RTINS16c](#RTINS16c) - `(RTTS10d)` (non-normative) Sub-classes may share a common abstract intermediate super-class (e.g. `PrimitiveInstance`) for implementation convenience, as long as the public API surface seen by user code matches the partition above +- `(RTTS11)` Typed write-value type - typed SDKs should expose a public value type, commonly named `LiveMapValue`, as the carrier of the map set-value union (`Boolean | Binary | Number | String | JsonArray | JsonObject | LiveCounter | LiveMap` - see [RTLM20](#RTLM20), [RTPO15](#RTPO15), [RTINS12](#RTINS12)) accepted by `set`, constructed in a platform-idiomatic way + - `(RTTS11a)` (non-normative) For example, Java exposes `LiveMapValue` as an abstract class with `of(...)` static factories; Swift exposes it as an enum with literal conformances ### PublicAPI::ObjectMessage @@ -1472,173 +1477,3 @@ Types and their properties/methods are public and exposed to users by default. A class JsonArrayInstance extends Instance: // RTTS10c value() -> JsonArray compactJson() -> JsonArray // RTTS7a2 (narrowed) - -### Swift-specific IDL (informative) - -This subsection is **informative and non-normative**. Statically-typed SDKs follow the RTTS partition ([RTTS3](#RTTS3) through [RTTS10](#RTTS10)); the shipped Swift SDK (`AblyLiveObjects`) satisfies the RTTS *semantics* but expresses them through platform-idiomatic constructs that deviate from the *letter* of the partition in the enumerated places below. It is recorded here so that cross-SDK readers and test authors know the exact Swift surface. The portability intent of [RTTS1a](#RTTS1a) is that user code be portable *between statically-typed SDKs that agree on the partition*; Swift trades that letter-for-letter portability for compile-time type-safety (exhaustive enum discrimination, non-optional typed properties, typed `throws`), in the same spirit as the non-normative rationale under [RTTS1](#RTTS1). Every member below is verifiable against the shipped source under `LiveObjects/Sources/AblyLiveObjects/Path Based API/Public/`. - -The Swift surface (all top-level declarations carry an availability annotation, and closure-taking members carry the `@escaping`/`@Sendable` and `@discardableResult` attributes — both the annotation and these attributes are omitted here for brevity; read ops are `throws(ARTErrorInfo)` per [RTO25](#RTO25); mutating ops are `async throws(ARTErrorInfo)`; JSON is the native `JSONValue`): - - // === Swift-specific variant (informative) ================================= - // Shipped AblyLiveObjects public surface. Deviations from the RTTS partition - // are enumerated in the table below. `throws(ARTErrorInfo)` = typed throws; - // `any P` = an existential of protocol P; convenience/extension members are - // noted inline in comments rather than repeated as separate declarations. - // ========================================================================== - - enum ValueType: Sendable, Equatable // RTTS2 (lowerCamelCase, 9 cases) - case string // RTTS2a1 - case number // RTTS2a2 - case boolean // RTTS2a3 - case binary // RTTS2a4 - case jsonObject // RTTS2a5 - case jsonArray // RTTS2a6 - case liveMap // RTTS2a7 - case liveCounter // RTTS2a8 - case unknown // RTTS2a9 - - enum Primitive: Sendable, Equatable // Swift-only; collapses the 6 RTTS6c / RTTS10c primitives - case string(String) - case number(Double) - case bool(Bool) - case data(Data) - case jsonArray([JSONValue]) - case jsonObject([String: JSONValue]) - // + stringValue / numberValue / boolValue / dataValue / jsonArrayValue / jsonObjectValue getters - - protocol PathObject: Sendable // RTTS3 - var path: String { get } // RTPO4 - func instance() throws(ARTErrorInfo) -> Instance? // RTPO8 - func compactJson() throws(ARTErrorInfo) -> JSONValue? // RTPO14, RTTS3c (nullable; base-only, NOT narrowed) - func subscribe(options: PathObjectSubscriptionOptions?, listener: PathObjectSubscriptionCallback) throws(ARTErrorInfo) -> any Subscription // RTPO19 - func exists() throws(ARTErrorInfo) -> Bool // RTTS4a - func type() throws(ARTErrorInfo) -> ValueType? // RTTS4b (named `type`, NOT `getType`) - func asLiveMap() -> any LiveMapPathObject // RTTS5a - func asLiveCounter() -> any LiveCounterPathObject // RTTS5b - func asPrimitive() -> any PrimitivePathObject // RTTS5c (single cast; collapses asNumber/asString/asBoolean/asBinary/asJsonObject/asJsonArray) - // extension: subscribe(listener:) options-less convenience; events(options:) -> AsyncStream // RTPO19 (also on LiveMapInstance/LiveCounterInstance; Swift-additive - terminating the stream auto-calls the underlying Subscription's unsubscribe() per SUB2a; no spec point mandates it) - - protocol LiveMapPathObject: PathObject // RTTS6a - func get(key: String) -> any PathObject // RTPO5 - func at(path: String) -> any PathObject // RTPO6 - func entries() throws(ARTErrorInfo) -> [(key: String, value: any PathObject)] // RTPO9 - func keys() throws(ARTErrorInfo) -> [String] // RTPO10 - func values() throws(ARTErrorInfo) -> [any PathObject] // RTPO11 - func size() throws(ARTErrorInfo) -> Int? // RTPO12 - func set(key: String, value: LiveMapValue) async throws(ARTErrorInfo) // RTPO15 - func remove(key: String) async throws(ARTErrorInfo) // RTPO16 - - protocol LiveCounterPathObject: PathObject // RTTS6b - func value() throws(ARTErrorInfo) -> Double? // RTTS6b (counter-filtered: nil unless the resolved value is a counter) - func increment(amount: Double) async throws(ARTErrorInfo) // RTPO17 (+ increment() convenience) - func decrement(amount: Double) async throws(ARTErrorInfo) // RTPO18 (+ decrement() convenience) - - protocol PrimitivePathObject: PathObject // RTTS6c (collapsed) - func value() throws(ARTErrorInfo) -> Primitive? // RTTS6c, RTTS6g (nil unless a primitive; NOT filtered to one exact primitive type - see deviation 12) - - enum Instance: Sendable // RTTS7 + RTTS9 (abstract base + 8 throwing casts replaced by an enum) - case liveMap(any LiveMapInstance) - case liveCounter(any LiveCounterInstance) - case primitive(any PrimitiveInstance) - var type: ValueType { get } // RTTS8 (convenience kept alongside exhaustive `switch`; O(1)) - func compactJson() throws(ARTErrorInfo) -> JSONValue // RTINS11, RTINS11c (non-null), RTTS7a (NOT narrowed) - - protocol LiveMapInstance: Sendable // RTTS10a - var id: String { get } // RTINS3a (non-nullable) - func get(key: String) throws(ARTErrorInfo) -> Instance? // RTINS5 - func entries() throws(ARTErrorInfo) -> [(key: String, value: Instance)] // RTINS6 - func keys() throws(ARTErrorInfo) -> [String] // RTINS7 - func values() throws(ARTErrorInfo) -> [Instance] // RTINS8 - var size: Int { get throws(ARTErrorInfo) } // RTINS9, RTTS10a (throwing property; non-optional) - func set(key: String, value: LiveMapValue) async throws(ARTErrorInfo) // RTINS12 - func remove(key: String) async throws(ARTErrorInfo) // RTINS13 - func subscribe(listener: InstanceSubscriptionCallback) throws(ARTErrorInfo) -> any Subscription // RTINS16 (+ events()) - func compactJson() throws(ARTErrorInfo) -> JSONValue // RTINS11 (NOT narrowed to a JSON object; cf RTTS7a1) - - protocol LiveCounterInstance: Sendable // RTTS10b - var id: String { get } // RTINS3a (non-nullable) - var value: Double { get throws(ARTErrorInfo) } // RTINS4, RTTS10b (throwing property; non-optional) - func increment(amount: Double) async throws(ARTErrorInfo) // RTINS14 (+ increment() convenience) - func decrement(amount: Double) async throws(ARTErrorInfo) // RTINS15 (+ decrement() convenience) - func subscribe(listener: InstanceSubscriptionCallback) throws(ARTErrorInfo) -> any Subscription // RTINS16 (+ events()) - func compactJson() throws(ARTErrorInfo) -> JSONValue // RTINS11 (NOT narrowed to a JSON primitive; cf RTTS7a3) - - protocol PrimitiveInstance: Sendable // RTTS10c (collapsed; read-only, no id/subscribe) - var value: Primitive { get throws(ARTErrorInfo) } // RTINS4, RTTS10c (non-optional) - var type: ValueType { get } // RTTS8 (O(1)) - func compactJson() throws(ARTErrorInfo) -> JSONValue // RTINS11 (NOT narrowed; cf RTTS7a) - - protocol RealtimeObject: Sendable // RTO - func get() async throws(ARTErrorInfo) -> any LiveMapPathObject // RTO23, RTTS6d - func on(event: ObjectsEvent, callback: () -> Void) -> any StatusSubscription // RTO18 (zero-arg callback) - // channel entry point: ARTRealtimeChannel.object -> any RealtimeObject // RTL27 - - enum ObjectsEvent: Sendable // RTO18b - case syncing // RTO18b1 - case synced // RTO18b2 - - protocol Subscription: Sendable // SUB - func unsubscribe() // SUB2a, SUB2b - - protocol StatusSubscription: Sendable // RTO18f - func off() // RTO18f1 (per-token removal; there is no offAll()) - - struct PathObjectSubscriptionEvent: Sendable // RTPO19e - let object: any PathObject // RTPO19e1 - let message: ObjectMessage? // RTPO19e2 (stays Optional) - - struct PathObjectSubscriptionOptions: Sendable // RTPO19c - let depth: Int? // RTPO19c1 (init(depth:) is non-throwing; depth<=0 (40003, RTPO19c1a) validated in subscribe, not here) - - typealias PathObjectSubscriptionCallback = @Sendable (PathObjectSubscriptionEvent) -> Void // RTPO19a1 (non-throwing) - - struct InstanceSubscriptionEvent: Sendable // RTINS16e - let object: Instance // RTINS16e1 - let message: ObjectMessage? // RTINS16e2 - - typealias InstanceSubscriptionCallback = @Sendable (InstanceSubscriptionEvent) -> Void // RTINS16a1 (non-throwing) - - // Write-side value types referenced by set(key:value:) above. Swift replaces the language-agnostic - // set-value union (Boolean|Binary|Number|String|JsonArray|JsonObject|LiveCounter|LiveMap - RTLM20 / - // RTPO15 / RTINS12) with the LiveMapValue enum, and reuses the shared LiveMap/LiveCounter creation - // value types (RTLMV* / RTLCV*). ObjectMessage (see PathObjectSubscriptionEvent.message) is the shared - // PublicAPI::ObjectMessage type, not repeated here. - enum LiveMapValue: Sendable, Equatable - case primitive(Primitive) - case liveMap(LiveMap) - case liveCounter(LiveCounter) - // + ExpressibleBy{String,Integer,Float,Boolean,Array,Dictionary}Literal (with JSONValue's own literal - // conformances), so set(key:value:) accepts "x" / 1 / true / ["k": "v"] directly - // + primitiveValue / liveMapValue / liveCounterValue (+ 6 primitive) convenience getters - - struct LiveMap: Sendable, Equatable // RTLMV* blueprint (inert; live map created on set, not a graph object) - static func create(entries: [String: LiveMapValue]) -> LiveMap // RTLMV3 - static func create() -> LiveMap // RTLMV3 (empty; Swift no-arg overload of the optional-arg factory) - - struct LiveCounter: Sendable, Equatable // RTLCV* blueprint (inert; live counter created on set, not a graph object) - static func create(initialCount: Double) -> LiveCounter // RTLCV3 - static func create() -> LiveCounter // RTLCV3 (initial 0; Swift no-arg overload) - - // JSONValue: native indirect enum (.object/.array/.string/.number/.bool/.null); Sendable, Equatable, - // ExpressibleBy*Literal. The native JSON type returned/accepted everywhere JSON travels (compactJson, - // ObjectMessage.extras, primitive JSON values) - replaces the generic JSON element type (deviation 8) - -Deviations of the shipped Swift surface from the RTTS partition. Each row is verifiable against the source files referenced above: - -| # | RTTS point | Swift deviation | Description / rationale | -|---|---|---|---| -| 1 | [RTTS7](#RTTS7), [RTTS9](#RTTS9) (and [RTTS8](#RTTS8)) | `enum Instance { .liveMap / .liveCounter / .primitive }` with `switch` discrimination replaces the abstract base class plus the 8 throwing `as*` casts | Discrimination is compile-time-exhaustive, so there is no undefined type-mismatch path (the [RTTS9d](#RTTS9d) 92007 throw is unrepresentable). The [RTTS8](#RTTS8) `type` is kept as an O(1) convenience `var type: ValueType` that coexists with the `switch` | -| 2 | [RTTS5c](#RTTS5c), [RTTS6c](#RTTS6c) / [RTTS9c](#RTTS9c), [RTTS10c](#RTTS10c) | A single `asPrimitive() -> any PrimitivePathObject` + `PrimitivePathObject`, and a single `PrimitiveInstance`, replace the 6 primitive casts and the 6 primitive sub-classes; both front one `Primitive` enum | The six spec primitive categories become cases of the `Primitive` enum that the caller pattern-matches. Smaller, Swift-idiomatic surface; the 6 `asNumber/asString/...` casts and the `NumberPathObject`/`NumberInstance`/... sub-classes do not exist | -| 3 | [RTTS4b](#RTTS4b), [RTTS8a](#RTTS8a) | `type()` (method on `PathObject`) / `type` (property on `Instance`, `PrimitiveInstance`) instead of `getType` | Swift API Design Guidelines: a `getX` accessor becomes `x`. `PathObject.type()` is a method (O(path length) resolution per call); `Instance.type` is a property (O(1)) | -| 4 | [RTO25](#RTO25) (all `PathObject`/`Instance` reads) | All read operations are uniformly `throws(ARTErrorInfo)` | Uniform typed-throws surface for the RTO25 access-preconditions, broader than the spec's per-method choice (Java leaves most reads unchecked) | -| 5 | [RTPO15](#RTPO15)-[RTPO18](#RTPO18) / [RTINS12](#RTINS12)-[RTINS15](#RTINS15) (`=> io`) | Blocking + `*Async` callback pairs collapse to a single `async throws(ARTErrorInfo)` method | One structured-concurrency suspension model; no callback variants | -| 6 | [RTPO19](#RTPO19) / [RTINS16](#RTINS16) | Listener interfaces become closure `typealias`es (`PathObjectSubscriptionCallback`, `InstanceSubscriptionCallback`) plus additional `AsyncStream`-returning `events()` accessors | Swift closure + `AsyncSequence` idiom; `events()` is an additive convenience layered on `subscribe` | -| 7 | [RTO18](#RTO18), [RTO18f](#RTO18f) | Status callback is zero-arg `() -> Void`; deregistration is per-token `StatusSubscription.off()`; there is no `offAll()` and no listener-identity `off(listener)` | The event is known at registration (`on(event:)`), so it need not be passed to the callback; subscription-token removal is the Swift idiom | -| 8 | [RTPO14](#RTPO14) / [RTINS11](#RTINS11) / [RTTS7a](#RTTS7a) | Native `JSONValue` replaces the generic/gson JSON element type everywhere JSON is returned or accepted (`compactJson`, `ObjectMessage.extras`, primitive JSON values) | No third-party type dependency in the public surface | -| 9 | [RTPO19c1a](#RTPO19c1a) | `depth <= 0` (40003) is validated inside `subscribe(options:listener:)`, not in the `PathObjectSubscriptionOptions` initializer, which is non-throwing | The shipped `init(depth:)` is frozen non-throwing; the check moves to the subscribe call site | -| 10 | [RTO24b2c](#RTO24b2c) | Subscription callbacks are non-throwing (`PathObjectSubscriptionCallback` / `InstanceSubscriptionCallback` cannot `throw`) | The "a listener throws -> it is caught and the other listeners still receive the event" contract has no Swift surface; independent-listener delivery is provided, but a listener cannot signal failure back | -| 11 | [RTTS7a](#RTTS7a), [RTTS7a1](#RTTS7a1)-[RTTS7a3](#RTTS7a3) | The `Instance` sub-classes do **not** narrow `compactJson`; every `compactJson` (base enum, `LiveMapInstance`, `LiveCounterInstance`, `PrimitiveInstance`) returns `JSONValue` (non-optional) | Explicitly permitted: [RTTS7a](#RTTS7a) says implementations "may choose not to narrow". Swift uses one JSON type throughout rather than the canonical `JsonObject`/`JsonArray`/`JsonPrimitive` narrowings. (Relevant to recent commits `c0c80d2b`/`9d4ad947`, which added those narrowings to the language-agnostic IDL) | -| 12 | [RTTS6c](#RTTS6c), [RTTS6g](#RTTS6g) | `PrimitivePathObject.value() -> Primitive?` returns **any** resolved primitive, filtered only at the primitive-vs-`LiveObject` boundary; it is NOT filtered to one exact primitive type as the six [RTTS6c](#RTTS6c) sub-classes are. `LiveCounterPathObject.value() -> Double?` does match [RTTS6b](#RTTS6b) (counter-filtered) | Consequence of the primitive collapse (deviation 2): per-primitive-type filtering ([RTTS6c](#RTTS6c)) is performed by the caller via the `Primitive` enum. The [RTTS6g](#RTTS6g) "never returns a `LiveObject`'s value" property is preserved (a counter/map resolves to `nil`); the finer per-primitive filtering is not. (Relevant to recent commits `7592b8e1`/`9d4ad947`, which defined the filtered-`value()` semantics) | -| 13 | [RTTS6e](#RTTS6e) (and base [RTPO8d](#RTPO8d)) | `instance()` lives on the base `PathObject` and is inherited unchanged by `PrimitivePathObject`; on a primitive view it returns the resolved `Instance` (a `.primitive` case) rather than applying the RTTS6e narrowing (return `nil`, or throw `92007`, on a non-map/non-counter view) | RTTS6e (consistent with [RTPO8d](#RTPO8d)) requires the primitive path view's `instance()` to return null or throw `92007`. Because the six primitive sub-classes are collapsed into one that shares `DefaultPathObject.instance()` (deviation 2) and reads are uniform (deviation 4), the primitive view resolves and wraps the value like any other view (a `.primitive` `Instance` for a primitive), following [RTPO8c](#RTPO8c) uniformly rather than the [RTPO8d](#RTPO8d) primitive-returns-null branch. Verifiable in `DefaultPathObject.instance()` and `DefaultPrimitivePathObject` | - -> Not reproduced above (Swift conforms to the spec as written, so no deviation row): the base `PathObject` exposes no `value()` ([RTTS3e](#RTTS3e)) - callers use the typed `asPrimitive().value()` / `asLiveCounter().value()` route; `compact` is not implemented ([RTTS3f](#RTTS3f) / [RTTS7d](#RTTS7d) already make it optional, `compactJson` only); and the typed `PathObject` sub-classes do not narrow `compactJson` ([RTTS3c](#RTTS3c)), which Swift satisfies with a single nullable `compactJson() -> JSONValue?` on the base protocol (recent commit `9d4ad947`); the `Instance` base `enum` exposes no `subscribe`, which is partitioned onto `LiveMapInstance` / `LiveCounterInstance` only ([RTTS7b](#RTTS7b)); and there is no `exists` helper on `Instance` ([RTTS8b](#RTTS8b)), since an `Instance` always wraps an already-resolved value. From c5cc6b8acee46151298e0ab9cc32bd4f3174d050 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Thu, 6 Aug 2026 21:49:29 +0530 Subject: [PATCH 13/13] docs(objects): reword typed-SDK as* helpers as "views"; drop category-error "narrowed" RTTS5/RTTS9: rename "as* cast helpers" -> "as* view helpers"; describe PathObject views as unchecked/expectation-based and Instance views as checked/fail-fast. Drop "cast"/"best-effort" from the as* prose (addresses review comment 3519566527). Replace category-error "narrowed to non-nullable" on sub-type-added members id/size/value with plain non-nullable typing; keep the genuine covariant-return narrowing wording for compactJson (RTTS7a) unchanged (addresses 3519416077/3519418317/3519422611/3519424137; per 3519771736). RTTS9: note that host languages without throwing property accessors may expose the as* helpers as methods (addresses 3519461990). Out of scope: ValueType->TypeOfValue rename (3519333807); public API member names unchanged; ably-docs narrative alignment to follow. Refs ably/specification#491 --- specifications/objects-features.md | 46 +++++++++++++++--------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/specifications/objects-features.md b/specifications/objects-features.md index 039de566b..13e4db0b8 100644 --- a/specifications/objects-features.md +++ b/specifications/objects-features.md @@ -1098,7 +1098,7 @@ An `Instance` holds a direct reference to a specific resolved `LiveObject` or pr This section refines [RTPO1a](#RTPO1a) and [RTINS1a](#RTINS1a) for SDKs whose host language is statically typed (e.g. Java, Swift, Kotlin). It is normative for those SDKs. SDKs in dynamically-typed languages (e.g. JavaScript) may continue to expose the full `PathObject` ([RTPO1](#RTPO1)) and `Instance` ([RTINS1](#RTINS1)) classes unchanged. -Rationale (non-normative): the overwhelming majority of customer use-cases operate on a static domain model; typed SDKs should provide compile-time type-safety for that majority while keeping a dynamic escape hatch via the base classes ([RTTS3](#RTTS3), [RTTS7](#RTTS7)) and the `as*` cast helpers ([RTTS5](#RTTS5), [RTTS9](#RTTS9)). +Rationale (non-normative): the overwhelming majority of customer use-cases operate on a static domain model; typed SDKs should provide compile-time type-safety for that majority while keeping a dynamic escape hatch via the base classes ([RTTS3](#RTTS3), [RTTS7](#RTTS7)) and the `as*` view helpers ([RTTS5](#RTTS5), [RTTS9](#RTTS9)). This section partitions the API surface of `PathObject` and `Instance` across a class hierarchy. It does not redefine per-method semantics; those remain authoritative in [RTPO\*](#RTPO1) and [RTINS\*](#RTINS1). @@ -1119,14 +1119,14 @@ This section partitions the API surface of `PathObject` and `Instance` across a - `(RTTS3)` Typed `PathObject` base class - the typed SDK's public root of the `PathObject` hierarchy. The base class exposes only those methods of [RTPO1](#RTPO1) whose return type and semantics are identical regardless of the resolved underlying type - `(RTTS3a)` Exposes `path` per [RTPO4](#RTPO4) - `(RTTS3b)` Exposes `instance` per [RTPO8](#RTPO8). The behaviour is identical on all sub-classes, including [RTPO8f](#RTPO8f) for primitive views (see [RTTS6e](#RTTS6e)) - - `(RTTS3c)` Exposes `compactJson` per [RTPO14](#RTPO14), with the SDK's generic, nullable JSON element type as its return type. Unlike the typed `Instance` sub-classes ([RTTS7a](#RTTS7a)), the typed `PathObject` sub-classes do not narrow this return type: a `PathObject` cast is best-effort and `compactJson` may resolve to a value of any type, or fail to resolve and return null (per [RTPO3c1](#RTPO3c1)), so no narrower type can be guaranteed for any sub-class + - `(RTTS3c)` Exposes `compactJson` per [RTPO14](#RTPO14), with the SDK's generic, nullable JSON element type as its return type. Unlike the typed `Instance` sub-classes ([RTTS7a](#RTTS7a)), the typed `PathObject` sub-classes do not narrow this return type: a `PathObject` `as*` view is unchecked and `compactJson` may resolve to a value of any type, or fail to resolve and return null (per [RTPO3c1](#RTPO3c1)), so no narrower type can be guaranteed for any sub-class - `(RTTS3d)` Exposes `subscribe` per [RTPO19](#RTPO19). Typed SDKs must deliver the full `PathObjectSubscriptionEvent` payload defined in [RTPO19e](#RTPO19e) to the listener - both the `object` field ([RTPO19e1](#RTPO19e1)) and the optional `message` field ([RTPO19e2](#RTPO19e2)) - so that user code can inspect the `PublicAPI::ObjectMessage` ([PAOM1](#PAOM1)) that triggered the change. Typed SDKs must not expose a subscription-event type that omits the `message` accessor - `(RTTS3e)` Does not expose `get` ([RTPO5](#RTPO5)), `at` ([RTPO6](#RTPO6)), `value` ([RTPO7](#RTPO7)), `entries` ([RTPO9](#RTPO9)), `keys` ([RTPO10](#RTPO10)), `values` ([RTPO11](#RTPO11)), `size` ([RTPO12](#RTPO12)), `set` ([RTPO15](#RTPO15)), `remove` ([RTPO16](#RTPO16)), `increment` ([RTPO17](#RTPO17)) or `decrement` ([RTPO18](#RTPO18)) on the base class. Those are partitioned onto the sub-classes per [RTTS6](#RTTS6) - `(RTTS3f)` Does not expose `compact` ([RTPO13](#RTPO13)). Typed SDKs are not required to implement `compact`; `compactJson` is sufficient for the static-domain use-cases. This may be revisited in a future spec version - - `(RTTS3g)` (non-normative) To traverse from a base `PathObject` reference into a map, callers cast first via `asLiveMap` ([RTTS5a](#RTTS5a)). For example, for a base `PathObject p`, deep navigation `p.asLiveMap().at("a.b.c")` is equivalent to successive `get` calls - - `(RTTS3h)` (non-normative) Unlike the typed `Instance` base ([RTTS7e](#RTTS7e)), the typed `PathObject` base type must be directly instantiable. Navigation via `get` ([RTPO5](#RTPO5)) and `at` ([RTPO6](#RTPO6)) returns a base `PathObject` for a location whose underlying value has not been resolved - and may not even exist - so its type is not yet known and there is no typed sub-class to return. A typed sub-class is only produced once the caller narrows the view via an `as*` cast ([RTTS5](#RTTS5)) -- `(RTTS4)` Additional typed `PathObject` methods - best-effort helpers that exist only on the typed base class. They have O(n) complexity in the path length because they resolve the path at call time, and are therefore exposed as methods (not properties) even in host languages that distinguish the two - - `(RTTS4a)` `exists()` - returns `Boolean` indicating whether a value currently resolves at the stored path. The check is best-effort and evaluates the local object tree at call time; the answer may change between this call and a subsequent operation as remote operations are applied. Useful as a guard before a write whose semantics depend on existence: + - `(RTTS3g)` (non-normative) To traverse from a base `PathObject` reference into a map, callers take the map view first via `asLiveMap` ([RTTS5a](#RTTS5a)). For example, for a base `PathObject p`, deep navigation `p.asLiveMap().at("a.b.c")` is equivalent to successive `get` calls + - `(RTTS3h)` (non-normative) Unlike the typed `Instance` base ([RTTS7e](#RTTS7e)), the typed `PathObject` base type must be directly instantiable. Navigation via `get` ([RTPO5](#RTPO5)) and `at` ([RTPO6](#RTPO6)) returns a base `PathObject` for a location whose underlying value has not been resolved - and may not even exist - so its type is not yet known and there is no typed sub-class to return. A typed sub-class is only produced once the caller selects one via an `as*` view helper ([RTTS5](#RTTS5)) +- `(RTTS4)` Additional typed `PathObject` methods - point-in-time helpers that exist only on the typed base class. They have O(n) complexity in the path length because they resolve the path at call time, and are therefore exposed as methods (not properties) even in host languages that distinguish the two + - `(RTTS4a)` `exists()` - returns `Boolean` indicating whether a value currently resolves at the stored path. The check is point-in-time and evaluates the local object tree at call time; the answer may change between this call and a subsequent operation as remote operations are applied. Useful as a guard before a write whose semantics depend on existence: - `(RTTS4a1)` Checks the access API preconditions per [RTO25](#RTO25) - `(RTTS4a2)` Performs the path resolution procedure ([RTPO3](#RTPO3)) - `(RTTS4a3)` Returns `true` if path resolution succeeds; `false` if it fails per [RTPO3c](#RTPO3c) @@ -1134,23 +1134,23 @@ This section partitions the API surface of `PathObject` and `Instance` across a - `(RTTS4b1)` Checks the access API preconditions per [RTO25](#RTO25) - `(RTTS4b2)` Performs the path resolution procedure ([RTPO3](#RTPO3)) - `(RTTS4b3)` If path resolution fails per [RTPO3c](#RTPO3c), returns `null`, indicating that no value exists at the path. Otherwise returns the matching `ValueType` member per [RTTS2a](#RTTS2a), or `UNKNOWN` if the resolved value falls into none of the categories defined in [RTTS2a1](#RTTS2a1) through [RTTS2a8](#RTTS2a8) -- `(RTTS5)` Typed `PathObject` `as*` cast helpers - best-effort accessors that return a sub-class view of this `PathObject` without performing any validation against current path resolution. The returned wrapper shares this `PathObject`'s `path` ([RTPO2a](#RTPO2a)) and `root` ([RTPO2b](#RTPO2b)) +- `(RTTS5)` Typed `PathObject` `as*` view helpers - unchecked accessors that return a sub-class view representing the type the caller expects at this path, without performing any validation against current path resolution. The returned wrapper shares this `PathObject`'s `path` ([RTPO2a](#RTPO2a)) and `root` ([RTPO2b](#RTPO2b)) - `(RTTS5a)` `asLiveMap()` returns a `LiveMapPathObject` ([RTTS6a](#RTTS6a)) - `(RTTS5b)` `asLiveCounter()` returns a `LiveCounterPathObject` ([RTTS6b](#RTTS6b)) - `(RTTS5c)` `asNumber()`, `asString()`, `asBoolean()`, `asBinary()`, `asJsonObject()`, `asJsonArray()` each return the corresponding primitive sub-class per [RTTS6c](#RTTS6c) - `(RTTS5d)` These helpers must not throw based on the current resolved type; they only re-wrap. Operations on the returned wrapper carry their own failure semantics: - `(RTTS5d1)` Read operations (`value`, `instance`, `entries`, `keys`, `values`, `size`, `compactJson`) follow [RTPO3c1](#RTPO3c1) - return null/empty if the resolved type does not match the wrapper's expectation - `(RTTS5d2)` Write and terminal operations (`set`, `remove`, `increment`, `decrement`) carry their own per-method failure semantics. They throw an `ErrorInfo` with `statusCode` 400 and `code` 92005 if the path does not resolve (per [RTPO3c2](#RTPO3c2)), or `code` 92007 if the path resolves but the value is not of the type the operation requires (per [RTPO15e](#RTPO15e) / [RTPO16e](#RTPO16e) for `set` / `remove`, and [RTPO17e](#RTPO17e) / [RTPO18e](#RTPO18e) for `increment` / `decrement`) - - `(RTTS5e)` (non-normative) Callers needing a strict type-check before casting should use [RTTS4b](#RTTS4b) `getType` or [RTTS4a](#RTTS4a) `exists` + - `(RTTS5e)` (non-normative) Callers needing a strict type-check before taking a view should use [RTTS4b](#RTTS4b) `getType` or [RTTS4a](#RTTS4a) `exists` - `(RTTS6)` Typed `PathObject` sub-classes - the partition of methods across the hierarchy. Each sub-class extends [RTTS3](#RTTS3) and therefore inherits `path`, `instance`, `compactJson`, `subscribe`, `exists`, `getType`, and all `as*` helpers - `(RTTS6a)` `LiveMapPathObject` - adds `get` per [RTPO5](#RTPO5), `at` per [RTPO6](#RTPO6), `entries` per [RTPO9](#RTPO9), `keys` per [RTPO10](#RTPO10), `values` per [RTPO11](#RTPO11), `size` per [RTPO12](#RTPO12), `set` per [RTPO15](#RTPO15), `remove` per [RTPO16](#RTPO16). Failure semantics for each method when the path does not resolve to an `InternalLiveMap` are inherited from the referenced `RTPO*` clauses unchanged - - `(RTTS6b)` `LiveCounterPathObject` - adds `increment` per [RTPO17](#RTPO17), `decrement` per [RTPO18](#RTPO18), and a `value()` method with the return type narrowed to `Number?`. It returns the counter's value (per [RTPO7c](#RTPO7c)) when the resolved value is an `InternalLiveCounter`, and `null` otherwise - including when the path does not resolve (per [RTPO3c1](#RTPO3c1)) or resolves to a primitive or an `InternalLiveMap`. Unlike the general `value` ([RTPO7](#RTPO7)), it never returns a primitive value. SDKs may narrow this further to the host's idiomatic floating-point type (e.g. `Double?`), since a counter's value is always a finite floating-point number per [RTLCV4a](#RTLCV4a) - - `(RTTS6c)` Primitive `PathObject` sub-classes - one per primitive: `NumberPathObject`, `StringPathObject`, `BooleanPathObject`, `BinaryPathObject`, `JsonObjectPathObject`, `JsonArrayPathObject`. Each adds a `value()` method with the return type narrowed to its corresponding primitive. It returns the resolved value only when that value is of the sub-class's exact primitive type, and `null` otherwise - including when the path does not resolve (per [RTPO3c1](#RTPO3c1)), resolves to a different primitive, or resolves to a `LiveObject` (so, unlike the general `value` ([RTPO7](#RTPO7)), `NumberPathObject#value()` never returns an `InternalLiveCounter`'s value). They do not expose `get`, `at`, or any write / iteration methods + - `(RTTS6b)` `LiveCounterPathObject` - adds `increment` per [RTPO17](#RTPO17), `decrement` per [RTPO18](#RTPO18), and a `value()` method returning `Number?`. It returns the counter's value (per [RTPO7c](#RTPO7c)) when the resolved value is an `InternalLiveCounter`, and `null` otherwise - including when the path does not resolve (per [RTPO3c1](#RTPO3c1)) or resolves to a primitive or an `InternalLiveMap`. Unlike the general `value` ([RTPO7](#RTPO7)), it never returns a primitive value. SDKs may instead return the host's idiomatic floating-point type (e.g. `Double?`), since a counter's value is always a finite floating-point number per [RTLCV4a](#RTLCV4a) + - `(RTTS6c)` Primitive `PathObject` sub-classes - one per primitive: `NumberPathObject`, `StringPathObject`, `BooleanPathObject`, `BinaryPathObject`, `JsonObjectPathObject`, `JsonArrayPathObject`. Each adds a `value()` method returning its corresponding primitive. It returns the resolved value only when that value is of the sub-class's exact primitive type, and `null` otherwise - including when the path does not resolve (per [RTPO3c1](#RTPO3c1)), resolves to a different primitive, or resolves to a `LiveObject` (so, unlike the general `value` ([RTPO7](#RTPO7)), `NumberPathObject#value()` never returns an `InternalLiveCounter`'s value). They do not expose `get`, `at`, or any write / iteration methods - `(RTTS6d)` The `PathObject` returned by `RealtimeObject#get` ([RTO23](#RTO23)) must be of static type `LiveMapPathObject` in typed SDKs. This is well-defined because [RTO23d](#RTO23d) always returns a `PathObject` rooted at the channel's root `InternalLiveMap` with an empty path - `(RTTS6e)` This clause has been replaced by [RTPO8f](#RTPO8f): `instance()` ([RTTS3b](#RTTS3b)) behaves per [RTPO8](#RTPO8) on all sub-classes, and the typed views do not filter or suppress it - on a primitive view it returns a new `Instance` wrapping the resolved primitive value - `(RTTS6f)` (non-normative) Sub-classes may share a common abstract intermediate super-class (e.g. `PrimitivePathObject`) for implementation convenience, as long as the public API surface seen by user code matches the partition above - `(RTTS6g)` (non-normative) The type-filtered `value()` semantics of `LiveCounterPathObject` ([RTTS6b](#RTTS6b)) and the primitive sub-classes ([RTTS6c](#RTTS6c)) are a typed-SDK refinement and have no equivalent in the dynamic `PathObject#value` ([RTPO7](#RTPO7)): [RTPO7](#RTPO7) returns a resolved `InternalLiveCounter`'s value and any resolved primitive without regard to an expected type, whereas these typed accessors return `null` unless the resolved value matches the sub-class's specific category. Implementers must not assume the broader [RTPO7](#RTPO7) behaviour when implementing these typed accessors - - `(RTTS6h)` (non-normative) The Swift SDK collapses the six primitive casts ([RTTS5c](#RTTS5c)) and sub-classes ([RTTS6c](#RTTS6c); [RTTS10c](#RTTS10c) on the `Instance` side) into a single `asPrimitive()` / `PrimitivePathObject` / `PrimitiveInstance` fronting a `Primitive` enum; the per-type filtering above is performed by the caller via pattern-matching over the enum + - `(RTTS6h)` (non-normative) The Swift SDK collapses the six primitive view helpers ([RTTS5c](#RTTS5c)) and sub-classes ([RTTS6c](#RTTS6c); [RTTS10c](#RTTS10c) on the `Instance` side) into a single `asPrimitive()` / `PrimitivePathObject` / `PrimitiveInstance` fronting a `Primitive` enum; the per-type filtering above is performed by the caller via pattern-matching over the enum - `(RTTS7)` Typed `Instance` base class - the typed SDK's public root of the `Instance` hierarchy. The base class exposes only those methods of [RTINS1](#RTINS1) whose return type and semantics are identical regardless of the wrapped underlying type - `(RTTS7a)` Exposes `compactJson` per [RTINS11](#RTINS11). The non-null invariant is universal per [RTINS11c](#RTINS11c); typed SDKs must reflect this by giving `Instance#compactJson` a non-nullable return type in the public API. The base `Instance#compactJson` returns the SDK's generic JSON element type (the common super-type of the narrowed types below). Where the host language additionally supports covariant return types, sub-classes may narrow this return type covariantly (sound here because an `Instance`'s wrapped type is fixed and known - contrast `PathObject` per [RTTS3c](#RTTS3c)); implementations may choose not to narrow. When an SDK does narrow, the canonical narrowed return type per sub-class is: - `(RTTS7a1)` a JSON object for `LiveMapInstance` ([RTTS10a](#RTTS10a)) and `JsonObjectInstance` ([RTTS10c](#RTTS10c)) @@ -1160,20 +1160,20 @@ This section partitions the API surface of `PathObject` and `Instance` across a - `(RTTS7c)` Does not expose `id` ([RTINS3](#RTINS3)), `value` ([RTINS4](#RTINS4)), `get` ([RTINS5](#RTINS5)), `entries` ([RTINS6](#RTINS6)), `keys` ([RTINS7](#RTINS7)), `values` ([RTINS8](#RTINS8)), `size` ([RTINS9](#RTINS9)), `set` ([RTINS12](#RTINS12)), `remove` ([RTINS13](#RTINS13)), `increment` ([RTINS14](#RTINS14)) or `decrement` ([RTINS15](#RTINS15)) on the base class. Those are partitioned onto the sub-classes per [RTTS10](#RTTS10) - `(RTTS7d)` Does not expose `compact` ([RTINS10](#RTINS10)). See [RTTS3f](#RTTS3f) for rationale - `(RTTS7e)` (non-normative) Unlike the typed `PathObject` base ([RTTS3h](#RTTS3h)), the typed `Instance` base type need never be instantiated directly and may be abstract / non-instantiable. An `Instance` is only ever obtained from an already-resolved value whose type is therefore known at construction time - for example via `PathObject#instance` ([RTPO8c](#RTPO8c)) or `Instance#get` ([RTINS5c](#RTINS5c)) - so a typed SDK can always construct the matching concrete sub-class of [RTTS10](#RTTS10). Consequently there is no "untyped `Instance`" the way there is an unresolved base `PathObject`: every `Instance` a caller can hold is one of the concrete typed sub-classes -- `(RTTS8)` Typed `Instance` extension property - best-effort helper introduced by the typed-SDK variant. Unlike [RTTS4](#RTTS4) on `PathObject`, an `Instance` is bound to a resolved value at construction time, so the helper has O(1) complexity and is therefore exposed as a property in host languages that distinguish properties from methods +- `(RTTS8)` Typed `Instance` extension property - a helper introduced by the typed-SDK variant. Unlike [RTTS4](#RTTS4) on `PathObject`, an `Instance` is bound to a resolved value at construction time, so the helper has O(1) complexity and is therefore exposed as a property in host languages that distinguish properties from methods - `(RTTS8a)` `getType` - returns the `ValueType` ([RTTS2](#RTTS2)) of the wrapped value. Returns the matching `ValueType` member per [RTTS2a](#RTTS2a). Must not return `UNKNOWN` in normal operation because an `Instance` is always constructed from a resolved value (see e.g. [RTPO8c](#RTPO8c), [RTINS5c](#RTINS5c)); see [RTTS2a9](#RTTS2a9) for the future-compatibility caveat - `(RTTS8b)` There is no `exists` helper on `Instance`, because an `Instance` is only ever constructed when it wraps a resolved value; the existence question is unambiguous -- `(RTTS9)` Typed `Instance` `as*` cast helpers - type-checked accessors. Unlike the best-effort `PathObject` casts ([RTTS5](#RTTS5)), an `Instance` wraps an already-resolved value of a known, fixed type ([RTTS8a](#RTTS8a)), so a mismatched cast is a programming error and these helpers throw rather than re-wrap ([RTTS9d](#RTTS9d)). Exposed as properties in host languages that distinguish properties from methods, since the wrapped value is already resolved +- `(RTTS9)` Typed `Instance` `as*` view helpers - checked accessors. Unlike the unchecked `PathObject` views ([RTTS5](#RTTS5)), an `Instance` wraps an already-resolved value of a known, fixed type ([RTTS8a](#RTTS8a)), so a mismatched request is a programming error and these helpers throw rather than re-wrap ([RTTS9d](#RTTS9d)). Exposed as properties in host languages that distinguish properties from methods and support throwing property accessors; host languages without throwing property accessors may expose these as methods instead, since the wrapped value is already resolved - `(RTTS9a)` `asLiveMap` returns a `LiveMapInstance` ([RTTS10a](#RTTS10a)) - `(RTTS9b)` `asLiveCounter` returns a `LiveCounterInstance` ([RTTS10b](#RTTS10b)) - `(RTTS9c)` `asNumber`, `asString`, `asBoolean`, `asBinary`, `asJsonObject`, `asJsonArray` each return the corresponding primitive sub-class per [RTTS10c](#RTTS10c) - - `(RTTS9d)` If the wrapped value is not of the requested type, these helpers must fail fast: throw a platform-appropriate unchecked exception indicating the type mismatch (e.g. `IllegalStateException`), or equivalently an `ErrorInfo` with `statusCode` 400 and `code` 92007. This differs from the best-effort `PathObject` casts ([RTTS5d](#RTTS5d)), which never throw on cast, because an `Instance`'s wrapped type is known and fixed at construction time and so a mismatched cast cannot be a transient resolution artefact. A matching cast returns the typed sub-class view; that view therefore always matches the wrapped type, so its own read/write operations cannot fail on a type mismatch - - `(RTTS9d1)` (non-normative) Callers needing to discriminate the type before casting should use [RTTS8a](#RTTS8a) `getType` - - `(RTTS9e)` (non-normative) Languages with exhaustive sum types may replace the abstract `Instance` base and these cast helpers with a discriminated union (e.g. Swift's `enum Instance` with `liveMap` / `liveCounter` / `primitive` cases); discrimination is then compile-time-exhaustive and the [RTTS9d](#RTTS9d) failure path cannot occur + - `(RTTS9d)` If the wrapped value is not of the requested type, these helpers must fail fast: throw a platform-appropriate unchecked exception indicating the type mismatch (e.g. `IllegalStateException`), or equivalently an `ErrorInfo` with `statusCode` 400 and `code` 92007. This differs from the unchecked `PathObject` views ([RTTS5d](#RTTS5d)), which never throw, because an `Instance`'s wrapped type is known and fixed at construction time and so a mismatched request cannot be a transient resolution artefact. A matching request returns the typed sub-class view; that view therefore always matches the wrapped type, so its own read/write operations cannot fail on a type mismatch + - `(RTTS9d1)` (non-normative) Callers needing to discriminate the type before requesting a view should use [RTTS8a](#RTTS8a) `getType` + - `(RTTS9e)` (non-normative) Languages with exhaustive sum types may replace the abstract `Instance` base and these view helpers with a discriminated union (e.g. Swift's `enum Instance` with `liveMap` / `liveCounter` / `primitive` cases); discrimination is then compile-time-exhaustive and the [RTTS9d](#RTTS9d) failure path cannot occur - `(RTTS10)` Typed `Instance` sub-classes - the partition of methods across the hierarchy. Each sub-class extends [RTTS7](#RTTS7) and therefore inherits `compactJson`, `getType`, and all `as*` helpers. Note that `subscribe` is not inherited from the base - see [RTTS7b](#RTTS7b) - - `(RTTS10a)` `LiveMapInstance` - adds `id` per [RTINS3a](#RTINS3a) with the static return type narrowed to non-nullable `String`; `get` per [RTINS5](#RTINS5); `entries` per [RTINS6](#RTINS6); `keys` per [RTINS7](#RTINS7); `values` per [RTINS8](#RTINS8); `size` per [RTINS9](#RTINS9) with the return type narrowed to non-nullable `Number` (since the wrapped value is always an `InternalLiveMap`, [RTINS9c](#RTINS9c) cannot trigger); `set` per [RTINS12](#RTINS12); `remove` per [RTINS13](#RTINS13); `subscribe` per [RTINS16](#RTINS16); and `compactJson` narrowed to a JSON object per [RTTS7a1](#RTTS7a1). Typed SDKs must deliver the full `InstanceSubscriptionEvent` payload defined in [RTINS16e](#RTINS16e) to the listener - both the `object` field ([RTINS16e1](#RTINS16e1)) and the optional `message` field ([RTINS16e2](#RTINS16e2)) - and must not expose a subscription-event type that omits the `message` accessor - - `(RTTS10b)` `LiveCounterInstance` - adds `id` per [RTINS3a](#RTINS3a) (narrowed to non-nullable `String`); a `value()` method delegating to [RTINS4](#RTINS4) with the return type narrowed to non-nullable `Number` (which SDKs may narrow further to the host's idiomatic floating-point type, e.g. `Double`, since a counter's value is always a finite floating-point number per [RTLCV4a](#RTLCV4a)); `increment` per [RTINS14](#RTINS14); `decrement` per [RTINS15](#RTINS15); `subscribe` per [RTINS16](#RTINS16) with the same `InstanceSubscriptionEvent` payload requirements as [RTTS10a](#RTTS10a); and `compactJson` narrowed to a JSON primitive per [RTTS7a3](#RTTS7a3) - - `(RTTS10c)` Primitive `Instance` sub-classes - one per primitive: `NumberInstance`, `StringInstance`, `BooleanInstance`, `BinaryInstance`, `JsonObjectInstance`, `JsonArrayInstance`. Each adds a `value()` method delegating to [RTINS4](#RTINS4) with the return type narrowed to non-nullable instance of its corresponding primitive, and narrows `compactJson` per [RTTS7a](#RTTS7a): `JsonObjectInstance` to a JSON object ([RTTS7a1](#RTTS7a1)), `JsonArrayInstance` to a JSON array ([RTTS7a2](#RTTS7a2)), and `NumberInstance`/`StringInstance`/`BooleanInstance`/`BinaryInstance` to a JSON primitive ([RTTS7a3](#RTTS7a3)). These sub-classes are read-only - they do not expose `id` (primitive instances have no object id per [RTINS3b](#RTINS3b)), `get`, `set`, `remove`, `increment`, `decrement`, `entries`, `keys`, `values`, `size`, or `subscribe`. The absence of `subscribe` is a compile-time enforcement of the constraint already imposed at runtime by [RTINS16c](#RTINS16c) + - `(RTTS10a)` `LiveMapInstance` - adds `id` per [RTINS3a](#RTINS3a) with a non-nullable `String` return type (id is exposed only on LiveObject sub-types); `get` per [RTINS5](#RTINS5); `entries` per [RTINS6](#RTINS6); `keys` per [RTINS7](#RTINS7); `values` per [RTINS8](#RTINS8); `size` per [RTINS9](#RTINS9) with a non-nullable `Number` return type (since the wrapped value is always an `InternalLiveMap`, [RTINS9c](#RTINS9c) cannot trigger); `set` per [RTINS12](#RTINS12); `remove` per [RTINS13](#RTINS13); `subscribe` per [RTINS16](#RTINS16); and `compactJson` narrowed to a JSON object per [RTTS7a1](#RTTS7a1). Typed SDKs must deliver the full `InstanceSubscriptionEvent` payload defined in [RTINS16e](#RTINS16e) to the listener - both the `object` field ([RTINS16e1](#RTINS16e1)) and the optional `message` field ([RTINS16e2](#RTINS16e2)) - and must not expose a subscription-event type that omits the `message` accessor + - `(RTTS10b)` `LiveCounterInstance` - adds `id` per [RTINS3a](#RTINS3a) (non-nullable `String`); a `value()` method delegating to [RTINS4](#RTINS4) with a non-nullable `Number` return type (which SDKs may instead expose as the host's idiomatic floating-point type, e.g. `Double`, since a counter's value is always a finite floating-point number per [RTLCV4a](#RTLCV4a)); `increment` per [RTINS14](#RTINS14); `decrement` per [RTINS15](#RTINS15); `subscribe` per [RTINS16](#RTINS16) with the same `InstanceSubscriptionEvent` payload requirements as [RTTS10a](#RTTS10a); and `compactJson` narrowed to a JSON primitive per [RTTS7a3](#RTTS7a3) + - `(RTTS10c)` Primitive `Instance` sub-classes - one per primitive: `NumberInstance`, `StringInstance`, `BooleanInstance`, `BinaryInstance`, `JsonObjectInstance`, `JsonArrayInstance`. Each adds a `value()` method delegating to [RTINS4](#RTINS4) with a non-nullable return type of its corresponding primitive, and covariantly narrows `compactJson` per [RTTS7a](#RTTS7a): `JsonObjectInstance` to a JSON object ([RTTS7a1](#RTTS7a1)), `JsonArrayInstance` to a JSON array ([RTTS7a2](#RTTS7a2)), and `NumberInstance`/`StringInstance`/`BooleanInstance`/`BinaryInstance` to a JSON primitive ([RTTS7a3](#RTTS7a3)). These sub-classes are read-only - they do not expose `id` (primitive instances have no object id per [RTINS3b](#RTINS3b)), `get`, `set`, `remove`, `increment`, `decrement`, `entries`, `keys`, `values`, `size`, or `subscribe`. The absence of `subscribe` is a compile-time enforcement of the constraint already imposed at runtime by [RTINS16c](#RTINS16c) - `(RTTS10d)` (non-normative) Sub-classes may share a common abstract intermediate super-class (e.g. `PrimitiveInstance`) for implementation convenience, as long as the public API surface seen by user code matches the partition above - `(RTTS11)` Typed write-value type - typed SDKs should expose a public value type, commonly named `LiveMapValue`, as the carrier of the map set-value union (`Boolean | Binary | Number | String | JsonArray | JsonObject | LiveCounter | LiveMap` - see [RTLM20](#RTLM20), [RTPO15](#RTPO15), [RTINS12](#RTINS12)) accepted by `set`, constructed in a platform-idiomatic way - `(RTTS11a)` (non-normative) For example, Java exposes `LiveMapValue` as an abstract class with `of(...)` static factories; Swift exposes it as an enum with literal conformances @@ -1435,21 +1435,21 @@ Types and their properties/methods are public and exposed to users by default. A asJsonArray: JsonArrayInstance // RTTS9c class LiveMapInstance extends Instance: // RTTS10a - id: String // RTINS3a (narrowed to non-nullable) + id: String // RTINS3a (non-nullable; id exists only on LiveObject sub-types) compactJson() -> JsonObject // RTTS7a1 (narrowed) get(String key) -> Instance? // RTINS5 entries() -> [String, Instance][] // RTINS6 keys() -> String[] // RTINS7 values() -> Instance[] // RTINS8 - size() -> Number // RTINS9 (narrowed to non-nullable) + size() -> Number // RTINS9 (non-nullable; RTINS9c cannot trigger) set(String key, (Boolean | Binary | Number | String | JsonArray | JsonObject | LiveCounter | LiveMap) value) => io // RTINS12 remove(String key) => io // RTINS13 subscribe((InstanceSubscriptionEvent) -> listener) -> Subscription // RTINS16, RTTS7b class LiveCounterInstance extends Instance: // RTTS10b - id: String // RTINS3a (narrowed to non-nullable) + id: String // RTINS3a (non-nullable; id exists only on LiveObject sub-types) compactJson() -> JsonPrimitive // RTTS7a3 (narrowed) - value() -> Number // RTINS4 (narrowed to non-nullable) + value() -> Number // RTINS4 (non-nullable) increment(Number amount?) => io // RTINS14 decrement(Number amount?) => io // RTINS15 subscribe((InstanceSubscriptionEvent) -> listener) -> Subscription // RTINS16, RTTS7b