[AIT-1023] Path-based public API - #134
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
WalkthroughThis PR replaces the previous plural "objects" LiveObjects API with a new path-based public API surface ( ChangesPath-based LiveObjects API
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Channel as ARTRealtimeChannel
participant Store as PublicObjectsStore
participant DefaultObject as DefaultRealtimeObject
participant PathObj as DefaultLiveMapPathObject
Channel->>Store: getOrCreateRealtimeObject(internalObjects, creationArgs)
Store->>DefaultObject: create or reuse cached instance
Store-->>Channel: return DefaultRealtimeObject
Channel-->>Channel: expose as object: any RealtimeObject
Channel->>DefaultObject: object.get()
DefaultObject->>PathObj: notImplemented() (stub)
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (3)
Sources/AblyLiveObjects/Path Based API/Public/ValueTypes.swift (1)
234-268: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMove
publicfrom members to the extension declaration.Each of these six
ExpressibleBy*Literalextensions marks the individualinitaspublicinstead of the extension itself.♻️ Proposed fix
-extension LiveMapValue: ExpressibleByDictionaryLiteral { - public init(dictionaryLiteral elements: (String, JSONValue)...) { +public extension LiveMapValue { + init(dictionaryLiteral elements: (String, JSONValue)...) { self = .primitive(.jsonObject(.init(uniqueKeysWithValues: elements))) } } -extension LiveMapValue: ExpressibleByArrayLiteral { - public init(arrayLiteral elements: JSONValue...) { +public extension LiveMapValue { + init(arrayLiteral elements: JSONValue...) { self = .primitive(.jsonArray(elements)) } } -extension LiveMapValue: ExpressibleByStringLiteral { - public init(stringLiteral value: String) { +public extension LiveMapValue { + init(stringLiteral value: String) { self = .primitive(.string(value)) } } -extension LiveMapValue: ExpressibleByIntegerLiteral { - public init(integerLiteral value: Int) { +public extension LiveMapValue { + init(integerLiteral value: Int) { self = .primitive(.number(Double(value))) } } -extension LiveMapValue: ExpressibleByFloatLiteral { - public init(floatLiteral value: Double) { +public extension LiveMapValue { + init(floatLiteral value: Double) { self = .primitive(.number(value)) } } -extension LiveMapValue: ExpressibleByBooleanLiteral { - public init(booleanLiteral value: Bool) { +public extension LiveMapValue { + init(booleanLiteral value: Bool) { self = .primitive(.bool(value)) } }(Note: the conformance to each
ExpressibleBy*Literalprotocol is preserved implicitly via the same extension; you can also keep the protocol name on theextensionline if you prefer one extension per conformance, as long aspublicsits on theextensionkeyword rather than the member.)As per coding guidelines: "When writing an
extensionof a type, prefer placing the access level on the extension declaration rather than on each individual member."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Sources/AblyLiveObjects/Path` Based API/Public/ValueTypes.swift around lines 234 - 268, The six LiveMapValue literal-conformance extensions should move the access level from each init onto the extension declarations themselves. Update the ExpressibleByDictionaryLiteral, ExpressibleByArrayLiteral, ExpressibleByStringLiteral, ExpressibleByIntegerLiteral, ExpressibleByFloatLiteral, and ExpressibleByBooleanLiteral extensions so the extension is public and the init members are no longer individually marked public, keeping the same conformances and initializer behavior.Source: Coding guidelines
Sources/AblyLiveObjects/Path Based API/Public/Instance.swift (1)
169-208: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDuplicate
AsyncStreambridging logic.The
LiveMapInstanceandLiveCounterInstancesubscribe()extensions are byte-identical, and the same continuation/termination pattern is repeated again inPathObject.swift(lines 90-99) andInternalTypes.swift'sLiveObject.updates()(lines 136-148). Consider extracting a shared generic helper (typed throws works fine with generics per SE-0413) to avoid four near-identical copies diverging over time.♻️ Proposed shared helper
internal func makeSubscriptionStream<Event: Sendable>( subscribe: (`@escaping` `@Sendable` (Event) -> Void) throws(ARTErrorInfo) -> any Subscription ) throws(ARTErrorInfo) -> AsyncStream<Event> { let (stream, continuation) = AsyncStream.makeStream(of: Event.self) let subscription = try subscribe { event in continuation.yield(event) } continuation.onTermination = { _ in subscription.unsubscribe() } return stream }`@available`(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) public extension LiveMapInstance { func subscribe() throws(ARTErrorInfo) -> AsyncStream<InstanceSubscriptionEvent> { - let (stream, continuation) = AsyncStream.makeStream(of: InstanceSubscriptionEvent.self) - let subscription = try subscribe { event in - continuation.yield(event) - } - continuation.onTermination = { _ in - subscription.unsubscribe() - } - return stream + try makeSubscriptionStream(subscribe: subscribe(listener:)) } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Sources/AblyLiveObjects/Path` Based API/Public/Instance.swift around lines 169 - 208, The `subscribe()` AsyncStream bridging in `LiveMapInstance` and `LiveCounterInstance` is duplicated and matches the same pattern used in `PathObject` and `LiveObject.updates()`. Extract this continuation/termination setup into a shared generic helper (for example, a reusable function in the subscription/internal utilities area) that takes the existing subscribe closure and returns an `AsyncStream<Event>`, then update these `subscribe()` extensions to call it so the `subscription.unsubscribe()` cleanup stays centralized and the four copies cannot drift.Sources/AblyLiveObjects/Path Based API/Public/PublicObjectMessage.swift (1)
118-129: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winConsider forward-compatibility for unknown wire actions.
PublicAPI.ObjectOperationActionexhaustively lists the currently-known actions with nounknown/catch-all case, unlike the internal wireObjectOperationActionwhich wraps values in.known/.unknown(seeObjectCreationHelpers.swiftusage of.known(.counterCreate)). Once the mapping from wire messages to this public type is implemented, a server-sent action not in this list will have no representable value here, forcing either a crash, a dropped event, or a source-breaking enum case addition down the line.Worth deciding now, while the public surface is still being defined, whether to add a fallback case (e.g.
case other(String)) to preserve forward compatibility for future wire protocol additions.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Sources/AblyLiveObjects/Path` Based API/Public/PublicObjectMessage.swift around lines 118 - 129, The public ObjectOperationAction enum in PublicObjectMessage is currently closed over only the known wire actions, so add a forward-compatible fallback case while the API is still being defined. Update PublicAPI.ObjectOperationAction to include an unknown/catch-all representation (for example a String-backed case) and make sure any future mapping from wire ObjectOperationAction preserves unrecognized values instead of crashing or dropping them. Keep the existing known cases intact and align the design with the internal .known/.unknown pattern used elsewhere.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@Sources/AblyLiveObjects/Path` Based API/Public/Instance.swift:
- Around line 169-208: The `subscribe()` AsyncStream bridging in
`LiveMapInstance` and `LiveCounterInstance` is duplicated and matches the same
pattern used in `PathObject` and `LiveObject.updates()`. Extract this
continuation/termination setup into a shared generic helper (for example, a
reusable function in the subscription/internal utilities area) that takes the
existing subscribe closure and returns an `AsyncStream<Event>`, then update
these `subscribe()` extensions to call it so the `subscription.unsubscribe()`
cleanup stays centralized and the four copies cannot drift.
In `@Sources/AblyLiveObjects/Path` Based API/Public/PublicObjectMessage.swift:
- Around line 118-129: The public ObjectOperationAction enum in
PublicObjectMessage is currently closed over only the known wire actions, so add
a forward-compatible fallback case while the API is still being defined. Update
PublicAPI.ObjectOperationAction to include an unknown/catch-all representation
(for example a String-backed case) and make sure any future mapping from wire
ObjectOperationAction preserves unrecognized values instead of crashing or
dropping them. Keep the existing known cases intact and align the design with
the internal .known/.unknown pattern used elsewhere.
In `@Sources/AblyLiveObjects/Path` Based API/Public/ValueTypes.swift:
- Around line 234-268: The six LiveMapValue literal-conformance extensions
should move the access level from each init onto the extension declarations
themselves. Update the ExpressibleByDictionaryLiteral,
ExpressibleByArrayLiteral, ExpressibleByStringLiteral,
ExpressibleByIntegerLiteral, ExpressibleByFloatLiteral, and
ExpressibleByBooleanLiteral extensions so the extension is public and the init
members are no longer individually marked public, keeping the same conformances
and initializer behavior.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: e6597fb8-5265-4f09-806c-c399d82b66c6
📒 Files selected for processing (31)
Sources/AblyLiveObjects/Internal/InternalLiveMapValue.swiftSources/AblyLiveObjects/Internal/InternalTypes.swiftSources/AblyLiveObjects/Path Based API/Default/DefaultLiveCounterInstance.swiftSources/AblyLiveObjects/Path Based API/Default/DefaultLiveCounterPathObject.swiftSources/AblyLiveObjects/Path Based API/Default/DefaultLiveMapInstance.swiftSources/AblyLiveObjects/Path Based API/Default/DefaultLiveMapPathObject.swiftSources/AblyLiveObjects/Path Based API/Default/DefaultPathObject.swiftSources/AblyLiveObjects/Path Based API/Default/DefaultPrimitiveInstance.swiftSources/AblyLiveObjects/Path Based API/Default/DefaultPrimitivePathObject.swiftSources/AblyLiveObjects/Path Based API/Default/DefaultRealtimeObject.swiftSources/AblyLiveObjects/Path Based API/Default/DefaultStatusSubscription.swiftSources/AblyLiveObjects/Path Based API/Default/DefaultSubscription.swiftSources/AblyLiveObjects/Path Based API/NotImplemented.swiftSources/AblyLiveObjects/Path Based API/Public/Channel+Object.swiftSources/AblyLiveObjects/Path Based API/Public/Instance.swiftSources/AblyLiveObjects/Path Based API/Public/PathObject.swiftSources/AblyLiveObjects/Path Based API/Public/PublicObjectMessage.swiftSources/AblyLiveObjects/Path Based API/Public/RealtimeObject.swiftSources/AblyLiveObjects/Path Based API/Public/Subscriptions.swiftSources/AblyLiveObjects/Path Based API/Public/ValueTypes.swiftSources/AblyLiveObjects/Public/Public Proxy Objects/InternalLiveMapValue+ToPublic.swiftSources/AblyLiveObjects/Public/Public Proxy Objects/PublicDefaultLiveCounter.swiftSources/AblyLiveObjects/Public/Public Proxy Objects/PublicDefaultLiveMap.swiftSources/AblyLiveObjects/Public/Public Proxy Objects/PublicDefaultRealtimeObjects.swiftSources/AblyLiveObjects/Public/Public Proxy Objects/PublicObjectsStore.swiftSources/AblyLiveObjects/Public/PublicTypes.swiftTests/AblyLiveObjectsTests/AblyLiveObjectsTests.swiftTests/AblyLiveObjectsTests/Helpers/ARTRealtimeChannel+TestHelpers.swiftTests/AblyLiveObjectsTests/JS Integration Tests/ObjectsHelper.swiftTests/AblyLiveObjectsTests/JS Integration Tests/ObjectsIntegrationTests.swiftTests/AblyLiveObjectsTests/ObjectLifetimesTests.swift
💤 Files with no reviewable changes (6)
- Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicDefaultLiveCounter.swift
- Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsHelper.swift
- Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicDefaultRealtimeObjects.swift
- Sources/AblyLiveObjects/Public/Public Proxy Objects/InternalLiveMapValue+ToPublic.swift
- Sources/AblyLiveObjects/Public/PublicTypes.swift
- Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicDefaultLiveMap.swift
There was a problem hiding this comment.
I would recommend to create feature branch feature/path-based-liveobjects as a base branch to merge all new changes, instead of merging to main. It will avoid polluting main branch.
Also, you can keep focus of the PR only limited to new public API changes, no relevant impl. for the same.
There was a problem hiding this comment.
Largely LGTM. Please could you tidy up the commits? I think one groundwork for the ObjectMessage namespacing and then one for everything else?
Basically as I understand it the approach that you've taken is to get rid of anything that depends on the public API (which, luckily, turns out to not be very much)? And the intention for the ported JS integration tests is that they'll be replaced by the UTS?
What's your plan for the example apps? Are we just going to leave them red in CI until you've implemented the new API?
| /// ``InstanceSubscriptionEvent``) so user code can inspect the metadata of the message that triggered | ||
| /// an object change. They are modelled as plain `Sendable` value types rather than protocols: they | ||
| /// carry no behaviour, only data. | ||
| public enum PublicAPI { |
There was a problem hiding this comment.
PAOM1:
The PublicAPI:: prefix is used to avoid a name clash with ObjectMessage; SDKs expose this type to users as ObjectMessage.
We need to invert what you've proposed here; the internal types need a namespace. I'd suggest a groundwork commit that puts all the types declared in ObjectMessage.swift into a namespace (maybe Protocol)? (Separate commit because I think it's going to be a lot of diff noise from having to qualify the names in a bunch of places.)
There was a problem hiding this comment.
Done: internal types namespaced in 6ebc05f, public types un-namespaced in 4f1b647. Note: I used ProtocolTypes rather than Protocol, since Protocol clashes with the Objective-C Protocol type imported via Foundation (ambiguous for type lookup in importing modules such as the test target). Happy to bikeshed the name.
| /// Returns a dot-delimited string representation of the stored path segments. Dot characters | ||
| /// occurring within individual segments are escaped with a backslash. An empty path (the root) | ||
| /// returns an empty string. Spec: `RTPO4`. | ||
| func path() -> String |
There was a problem hiding this comment.
I think this would be neater as a property (it's constant for the PathObject so it can be trivially cached)
|
|
||
| /// Resolves the path and returns the ``ValueType`` of the value there, or `nil` if nothing | ||
| /// resolves at the path. Spec: `RTTS4b`. | ||
| func getType() throws(ARTErrorInfo) -> ValueType? |
There was a problem hiding this comment.
The get prefix isn't very Swifty — what about just valueType()? (to be changed to typeOfValue() if we do the rename I suggested on the spec)
There was a problem hiding this comment.
Or even just type() for consistency with Instance. The get() thing feels like a Java-ism
There was a problem hiding this comment.
Done in 8dc30cb — renamed to type() (per the thread below), for consistency with Instance.type.
| /// Returns an `AsyncSequence` that emits a ``PathObjectSubscriptionEvent`` each time the object at | ||
| /// this path is updated. The underlying subscription is removed when the stream is terminated. | ||
| /// Spec: `RTPO19`. | ||
| func subscribe(options: PathObjectSubscriptionOptions? = nil) throws(ARTErrorInfo) -> AsyncStream<PathObjectSubscriptionEvent> { |
There was a problem hiding this comment.
I wouldn't call this method subscribe(), and instead name it after what it returns (c.f. e.g. https://developer.apple.com/documentation/foundation/urlsession/bytes(for:delegate:)) — this is what our existing LiveObject.updates() does. Could just call it events(). Same comment for Instance methods.
There was a problem hiding this comment.
Done in 2eb5e12 — renamed to events() on both PathObject and Instance.
| /// | ||
| /// `AsyncStream` requires a newer deployment target than this package's floor, so these are gated | ||
| /// behind `@available`. | ||
| @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) |
There was a problem hiding this comment.
I think that these annotations throughout no longer are needed; weren't they just for ably-cocoa?
There was a problem hiding this comment.
Done in 0d233cf — removed; the package floor (macOS 11 / iOS 14 / tvOS 14) is already above the AsyncStream requirement (macOS 10.15 / iOS 13 / tvOS 13), so they were only needed for ably-cocoa.
| } | ||
|
|
||
| /// The internal live key-value map data structure. Spec: `RTLM`. | ||
| internal protocol InternalLiveMap: LiveObject where Update == LiveMapUpdate { |
There was a problem hiding this comment.
InternalLiveMap, InternalLiveCounter, and LiveObject are no longer used or implemented. I think we can get rid of them?
There was a problem hiding this comment.
Done in 88de02f — removed all three; their supporting types (update descriptors, callbacks, subscription-handle protocols) are still used by the engine and were kept.
| /// ``ARTRealtimeChannel/object``. | ||
| /// | ||
| /// This is largely a wrapper around ``InternalDefaultRealtimeObjects``. | ||
| internal final class DefaultRealtimeObject: RealtimeObject { |
There was a problem hiding this comment.
Can we keep the Public prefix here; it expresses the contrast with InternalDefaultRealtimeObjects per the documented memory management policy. Also, it belongs in the Public/Public Proxy Objects directory.
There was a problem hiding this comment.
Done in 2ebee8b — renamed back to PublicDefaultRealtimeObject and moved into Public/Public Proxy Objects.
| /// allows us to consistently return the same `DefaultRealtimeObject` instance across multiple calls to | ||
| /// `ARTRealtimeChannel.object`. It mirrors the mechanism previously used for the (now-removed) | ||
| /// `objects` API, and the generic `Proxies` helper is retained so it can be extended to cache the | ||
| /// path-based map/counter objects once those are implemented. |
There was a problem hiding this comment.
so it can be extended to cache the path-based map/counter objects once those are implemented.
I don't think that we're going to want to do this; I don't think that ably-js offers pointer identity for PathObject or Instance types (i.e. pointer identity only exists at the level of the now-internal InternalLiveMap and InternalLiveCounter types).
Keeping this pointer-identity mechanism just for the RealtimeObject might be overkill. Let's keep it around for now given that it's already done though
There was a problem hiding this comment.
Done in 6e87139 — kept the mechanism as suggested, and reworded the comment to drop the claim about caching path-based objects.
| #expect(objects as AnyObject === objectsAgain as AnyObject) | ||
| #expect(object === objectAgain) | ||
| #expect(root === rootAgain) | ||
| // TODO: when we have an easy way of populating the ObjectsPool (i.e. once we have a write API) then also test with a non-root LiveMap and a counter (https://github.com/ably/ably-liveobjects-swift-plugin/issues/30) |
There was a problem hiding this comment.
I think preserve this comment until we (probably – see my note on the objects store) get rid of this test
| /// | ||
| /// The returned ``InternalDefaultRealtimeObjects`` is stable across calls: it is stored in the | ||
| /// channel's plugin data by ``DefaultInternalPlugin`` and looked up here. | ||
| var internalRealtimeObjectsWithCoreSDK: (objects: InternalDefaultRealtimeObjects, coreSDK: CoreSDK) { |
There was a problem hiding this comment.
The plumbingSmokeTest isn't really a plumbing test if all the plumbing is actually done by a test helper. Can't we just expose a testsOnly_coreSDK on the DefaultRealtimeObject and use channel.testsOnly_nonTypeErasedObject, dropping the helper?
There was a problem hiding this comment.
Done in 7d5082b — dropped the helper; exposed testsOnly_coreSDK on PublicDefaultRealtimeObject and reach everything via channel.testsOnly_nonTypeErasedObject.
5895231 to
6fc8aa4
Compare
Done, but I addressed your comments in an individual commit, will combine them in the "everything else" once approved.
Correct
Most likely, if not, update them to the new API once implemented.
Update once API implemented. |
|
(claude-authored comment) Small commit-hygiene note: the |
Groundwork for the path-based public API. Per Lawrence's review (PAOM1), the public/internal `ObjectMessage` name clash is resolved by giving the *internal* types a namespace (rather than prefixing the public ones with `PublicAPI`), so the plain names (`ObjectMessage`, `ObjectOperation`, `ObjectData`, …) are freed up for the user-facing types added in the following commit. This moves *all* the internal types that would otherwise clash into an `internal enum ProtocolTypes` namespace, and qualifies every reference across the codebase: - The eleven domain types declared in `Protocol/ObjectMessage.swift` (`InboundObjectMessage`, `OutboundObjectMessage`, `ObjectOperation`, `ObjectData`, `MapSet`, `MapCreate`, `MapCreateWithObjectId`, `CounterCreateWithObjectId`, `ObjectsMapEntry`, `ObjectsMap`, `ObjectState`). - The two shared protocol enums declared in `Protocol/WireObjectMessage.swift` (`ObjectOperationAction`, `ObjectsMapSemantics`). The spec-suggested namespace name `Protocol` clashes with the Objective-C `Protocol` type imported via Foundation (ambiguous for type lookup in importing modules such as the test target), so `ProtocolTypes` is used instead. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replaces the previous public LiveObjects surface (the `objects` proxy objects and `PublicTypes`) with the new path-based / instance API: - `ARTRealtimeChannel.object` returning a `RealtimeObject` entry point. - `PathObject` (and its typed `LiveMap`/`LiveCounter`/`Primitive` refinements), `Instance`, and the user-facing value types (`ObjectMessage`, `ObjectData`, value types, subscription events/options). These keep their plain names now that the internal protocol types are namespaced under `ProtocolTypes` (previous commit); the `PublicAPI` enum wrapper is therefore removed. - `Default*` skeletons backing the public protocols (most operations currently trap via `notImplemented()`), wired through `PublicObjectsStore`. The ported JS integration tests and the old proxy-object types/tests are removed; they will be superseded by the UTS. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
6fc8aa4 to
c7ede2f
Compare
f29aafd
into
feature/path-based-liveobjects
Overview
Replaces ably/ably-cocoa#2218
(Note
feature/path-based-liveobjectsbranch is a base for this PR and all subsequent work for LO until all is implemented)This introduces the new path-based public API for LiveObjects. The API shape is defined here as a skeleton — every
Default*path/instance method currently traps vianotImplemented(), while the internal engine (InternalDefault*) remains fully implemented and unit-tested.Original Lawrence's feedback - https://github.com/ably/ably-cocoa/blob/lawrence-review-path-based-api/review/Review%20by%20Claude%2C%203%20Jul%202026%20with%20Lawrence's%20thoughts.md
The following points addressed in 04355e1 commit.
§2 —
instance()returns a decided enum (AIT-1023)Instanceis now apublic enum { case liveMap / liveCounter / primitive }instead of a loosely-typed protocol, so callers can exhaustivelyswitchand get the correctly-typed payload with no undefined mismatch path.PathObject.instance()returnsInstance?;InstanceSubscriptionEvent.objectis nowInstance. Theas*narrowing helpers are removed from the instance side, and the obsoleteDefaultInstancebase class is gone (there is no base protocol anymore). This is a deliberate Swift-specific divergence from the language-agnostic base-type +as*-cast model ofRTTS9.§3 — added
getType()/exists()/ValueTypeNew
ValueTypeenum (RTTS2). AddedPathObject.exists()(RTTS4a) andPathObject.getType()(RTTS4b), both methods (O(path length)).ValueTypeis also surfaced asPrimitiveInstance.typeandInstance.type.§4 — typed
Instanceaccessors made non-optionalLiveMapInstance.size,LiveCounterInstance.value,PrimitiveInstance.valueandcompactJson()are non-optional (keptthrowsfor theRTO25check), since anInstanceis bound to an already-resolved object.LiveMapInstance.get(key:)stays optional (the key may be absent).§5 — primitive collapse documented
Kept the single
Primitive/PrimitivePathObject/PrimitiveInstance(rather than the six spec primitive sub-types) and documented it as a deliberate divergence fromRTTS6c/RTTS10c.§6 —
as*moved out of the protocol extensionasLiveMap()/asLiveCounter()/asPrimitive()are nowPathObjectprotocol requirements, so the single implementation lives on the concrete type and is dynamically dispatched — rather than a statically-dispatched extension default that cannot reach the concrete's internal state.§8 — accessor property/method split
O(1) instance accessors (
id,size,value,type) are throwing computed properties; O(n) ones (entries/keys/values/compactJson) and allPathObjectaccessors remain methods, with complexity documented. Property names carry nogetprefix (e.g.type, notgetType).§9 — AsyncSequence subscription variants
Added
@available-gatedsubscribe(...) -> AsyncStream<…>variants onPathObject,LiveMapInstanceandLiveCounterInstance, bridging the callback subscribe. Self-deregistration from within a listener stays dropped (event-only callbacks), now that the async variant gives an unsubscribe story.§10 —
CounterIncfield nameReverted
PublicAPI.CounterInc.amountto the wire/spec namenumber(CIN2a). LiveMapValue's read-oriented convenience getters were kept.§11 —
offAllremovedRemoved
RealtimeObject.offAll(); deregistration is viaStatusSubscription.off().Test changes
The public path-based API is an unimplemented skeleton, so tests are triaged accordingly:
channel.objectentry point, reaching the internal machinery through a newARTRealtimeChannel.internalRealtimeObjectsWithCoreSDKtest helper.get()but have only their@Testattribute commented out, sinceget()traps today. The LiveObject test tracks just the public path object (a path object retains its resolution context, not a specific internal map).Example app
Fails on CI until API implemented and the app re-written.