Uh oh!
There was an error while loading. Please reload this page.
Add typed-SDK LiveObjects API spec section (RTTS1-RTTS10) - #491
Conversation
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.
eea4f9c to
454fb1cCompare- 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
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 <noreply@anthropic.com>
…e is not present Updated respective sub-clauses
… instance is returned
There was a problem hiding this comment.
Pull request overview
This PR updates the LiveObjects specification to normatively define a typed (statically/strongly-typed) SDK public API surface for PathObject and Instance, including a class hierarchy partition (RTTS1–RTTS10) and supporting cross-references, while keeping the dynamic-SDK form intact.
Changes:
- Adds a new normative “Typed-SDK public API” section (RTTS1–RTTS10) describing the typed class hierarchy and method partitioning for
PathObject/Instance. - Introduces minimal inbound cross-references from existing
RTPO*/RTINS*/RTO*clauses to the new typed partition, and tightens the universalInstance#compactJsonnon-null invariant (RTINS11c). - Extends the Interface Definition section with an appended typed-SDK IDL block and clarifies creation-value vs graph-object types for
LiveMap/LiveCounter.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
e21cb4e to
c0c80d2Compare…lue() 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) <noreply@anthropic.com>
…alue() 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) <noreply@anthropic.com>
| // place of the monomorphic `class PathObject` / `class Instance` above. | ||
| // ========================================================================== | ||
| enum ValueType: // RTTS2 |
There was a problem hiding this comment.
I know we discussed this name plenty already, but I don't remember whether we discussed TypeOfValue as an option? The term "value type" already has a meaning and I think especially in Swift it might be a bit confusing.
There was a problem hiding this comment.
Fair point — TypeOfValue (or similar) would avoid the "value type" collision, especially in Swift. Treating it as a separate naming decision since renaming touches RTTS2, the IDL, ValueType.java and the docs; will follow up on it independently rather than bundling it into this change.
| asJsonArray: JsonArrayInstance // RTTS9c | ||
| class LiveMapInstance extends Instance: // RTTS10a | ||
| id: String // RTINS3a (narrowed to non-nullable) |
There was a problem hiding this comment.
Narrowed when compared to what? Instance? Instance doesn't have an id property in the IDL; if that's intentional then "narrowed" isn't the term here
There was a problem hiding this comment.
Fixed in c5cc6b8 — dropped narrowed; the IDL comment now reads (non-nullable; id exists only on LiveObject sub-types), and the RTTS10a prose states the plain return type.
| entries() -> [String, Instance][] // RTINS6 | ||
| keys() -> String[] // RTINS7 | ||
| values() -> Instance[] // RTINS8 | ||
| size() -> Number // RTINS9 (narrowed to non-nullable) |
There was a problem hiding this comment.
Narrowed when compared to what? Instance? Instance doesn't have an id property in the IDL; if that's intentional then "narrowed" isn't the term here
There was a problem hiding this comment.
Yeah, will remove narrowed from everywhere, for Instance it just exists on sub-types
There was a problem hiding this comment.
Done in c5cc6b8 — narrowed removed everywhere it described members added on sub-types (id/size/value). It is kept only for compactJson, where it is genuine covariant return-type narrowing relative to the base Instance#compactJson (RTTS7a).
| subscribe((InstanceSubscriptionEvent) -> listener) -> Subscription // RTINS16, RTTS7b | ||
| class LiveCounterInstance extends Instance: // RTTS10b | ||
| id: String // RTINS3a (narrowed to non-nullable) |
There was a problem hiding this comment.
Same q re narrowed from what
There was a problem hiding this comment.
Fixed in c5cc6b8 — same rewording as the LiveMapInstanceid line.
| class LiveCounterInstance extends Instance: // RTTS10b | ||
| id: String // RTINS3a (narrowed to non-nullable) | ||
| compactJson() -> JsonPrimitive // RTTS7a3 (narrowed) | ||
| value() -> Number // RTINS4 (narrowed to non-nullable) |
There was a problem hiding this comment.
Same q re narrowed compared to what
There was a problem hiding this comment.
Fixed in c5cc6b8 — now (non-nullable), with the RTTS10b prose stating the plain Number return type.
| - `(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 - 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 |
There was a problem hiding this comment.
Not all languages have the concept of a throwing property, I think — Swift didn't until recently. In that case they'll have to use a method
There was a problem hiding this comment.
Good catch — addressed in c5cc6b8. RTTS9 now reads: "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".
| - `(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), 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 |
There was a problem hiding this comment.
I thought we'd previously said we wouldn't use the word "cast" for anything related to PathObject since it's not really doing a cast? And "best-effort" isn't really correct either; who is making a best effort to do what exactly? It's just a view that represents the type of value that a user expects to find at a given path
There was a problem hiding this comment.
Agreed — reworded in c5cc6b8. The as* helpers are now view helpers throughout, which also matches the ably-java Javadoc (it consistently calls what they return "a ... view of this path/instance"). PathObject views are described as unchecked accessors "that return a sub-class view representing the type the caller expects at this path, without performing any validation", and Instance views as checked accessors that throw on mismatch. best-effort is gone from the as* prose ("unchecked" carries the meaning), and the exists/getType path helpers are now described as point-in-time instead.
…rface 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).
…ross-sdk-types-spec
…d-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())
…-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 #491
Summary
### Typed-SDK public API (statically-typed languages)section inobjects-features.md(RTTS1–RTTS10) that normatively partitions
PathObjectandInstanceacross a class hierarchy for SDKs whose hostlanguage is strongly typed (Java, Swift, Kotlin). The dynamic-SDK form (JavaScript) is unchanged; the new section is
purely a refinement of the existing
RTPO1a/RTINS1aallowance for type-specific sub-classes.RTPO1b,RTINS1b,RTPO13g,RTINS10c,RTO23f,RTINS11c) so readers ofany existing
RTPO*/RTINS*clause find the typed-SDK partition. TightenRTINS11cso the non-nullcompactJsoninvariant on
Instanceapplies universally (Instance is always bound — the existing JS behaviour already satisfiesthis).
Interface Definitionsection, alongside (not replacing) the dynamic-SDK form,with a banner that explains the relationship.
# Typed-SDK LiveObjects APIsection toapi-docstrings.mdwith language-agnostic docstrings for every newtype, each citing its
RTTS*clause.Key normative decisions
subscribe()restructure (RTTS7b).subscribeis removed from the base typedInstanceand lives only onLiveMapInstance/LiveCounterInstance. This turns the existing runtime throw ofRTINS16con primitives into acompile-time error and matches the official docs which state that
subscribe()is "only available onLiveMapandLiveCounterinstances".SubscriptionEventpayload (RTTS3d, RTTS10a, RTTS10b). Typed SDKs MUST deliver bothobjectandmessagefields per
RTPO19e/RTINS16e— a subscription-event type that omitsmessageis non-conformant.getType()semantics (RTTS4b, RTTS8a).PathObject#getType()returnsUNKNOWNon unresolved paths (silent,consistent with
RTPO3c1);Instance#getTypenever returnsUNKNOWNin normal operation (Instance is bound).compactdeferred (RTTS3f / RTTS7d). Typed SDKs are not required to implementcompact;compactJsonissufficient.
channel.objects.get()returnsLiveMapPathObject(RTTS6d / RTO23f).Instance#compactJsonnon-null invariant (RTINS11c). One-line tightening of the dynamic spec;reflects a property that already held in practice.