Skip to content

[AIT-1023] Path-based public API - #134

Merged
maratal merged 2 commits into
feature/path-based-liveobjectsfrom
path-based-public-api
Jul 10, 2026
Merged

[AIT-1023] Path-based public API#134
maratal merged 2 commits into
feature/path-based-liveobjectsfrom
path-based-public-api

Conversation

@maratal

@maratal maratal commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Overview

Replaces ably/ably-cocoa#2218

(Note feature/path-based-liveobjects branch 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 via notImplemented(), 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)

Instance is now a public enum { case liveMap / liveCounter / primitive } instead of a loosely-typed protocol, so callers can exhaustively switch and get the correctly-typed payload with no undefined mismatch path. PathObject.instance() returns Instance?; InstanceSubscriptionEvent.object is now Instance. The as* narrowing helpers are removed from the instance side, and the obsolete DefaultInstance base 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 of RTTS9.

§3 — added getType() / exists() / ValueType

New ValueType enum (RTTS2). Added PathObject.exists() (RTTS4a) and PathObject.getType() (RTTS4b), both methods (O(path length)). ValueType is also surfaced as PrimitiveInstance.type and Instance.type.

§4 — typed Instance accessors made non-optional

LiveMapInstance.size, LiveCounterInstance.value, PrimitiveInstance.value and compactJson() are non-optional (kept throws for the RTO25 check), since an Instance is 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 from RTTS6c/RTTS10c.

§6 — as* moved out of the protocol extension

asLiveMap() / asLiveCounter() / asPrimitive() are now PathObject protocol 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 all PathObject accessors remain methods, with complexity documented. Property names carry no get prefix (e.g. type, not getType).

§9 — AsyncSequence subscription variants

Added @available-gated subscribe(...) -> AsyncStream<…> variants on PathObject, LiveMapInstance and LiveCounterInstance, 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 — CounterInc field name

Reverted PublicAPI.CounterInc.amount to the wire/spec name number (CIN2a). LiveMapValue's read-oriented convenience getters were kept.

§11 — offAll removed

Removed RealtimeObject.offAll(); deregistration is via StatusSubscription.off().

Test changes

The public path-based API is an unimplemented skeleton, so tests are triaged accordingly:

  • Tests that only exercise plugin↔ably-cocoa plumbing are adapted to the new channel.object entry point, reaching the internal machinery through a new ARTRealtimeChannel.internalRealtimeObjectsWithCoreSDK test helper.
  • Lifetime tests: the RealtimeObject-reference test stays live; the LiveObject-reference and stable-identity tests are restored using get() but have only their @Test attribute commented out, since get() traps today. The LiveObject test tracks just the public path object (a path object retains its resolution context, not a specific internal map).
  • The old direct-reference integration suite and public-API smoke test are dropped until the public API is implemented.

Example app

Fails on CI until API implemented and the app re-written.

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 2580bb8c-588c-4ef1-b4c9-c85dfd4a5d2e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

This PR replaces the previous plural "objects" LiveObjects API with a new path-based public API surface (PathObject, Instance, ValueTypes, Subscriptions, RealtimeObject), backed by default stub implementations that call notImplemented(). The channel entry point, object store, and internal conversion logic are migrated to a single-object model, old public proxy types (PublicDefaultLiveMap, PublicDefaultLiveCounter, PublicDefaultRealtimeObjects, PublicTypes.swift) are removed, and tests are updated to the new shape.

Changes

Path-based LiveObjects API

Layer / File(s) Summary
Public value types and protocols
Sources/AblyLiveObjects/Path Based API/Public/ValueTypes.swift, Instance.swift, PathObject.swift, RealtimeObject.swift, Subscriptions.swift, PublicObjectMessage.swift
New public API: ValueType, Primitive, LiveCounter, LiveMap, LiveMapValue (with literal conformances); Instance/LiveMapInstance/LiveCounterInstance/PrimitiveInstance with AsyncStream subscribe helpers; PathObject/LiveMapPathObject/LiveCounterPathObject/PrimitivePathObject; RealtimeObject/ObjectsEvent; Subscription/StatusSubscription and event/option types; PublicAPI wire message structs.
Default stub implementations
Sources/AblyLiveObjects/Path Based API/Default/*, NotImplemented.swift
Adds notImplemented() helper and default classes (DefaultPathObject, DefaultLiveMapPathObject, DefaultLiveCounterPathObject, DefaultPrimitivePathObject, DefaultLiveMapInstance, DefaultLiveCounterInstance, DefaultPrimitiveInstance, DefaultRealtimeObject, DefaultStatusSubscription, DefaultSubscription) conforming to the new protocols but currently unimplemented.
Channel entry point migration and old proxy removal
Sources/AblyLiveObjects/Path Based API/Public/Channel+Object.swift, Public Proxy Objects/PublicObjectsStore.swift, PublicDefaultLiveMap.swift, PublicDefaultLiveCounter.swift, PublicDefaultRealtimeObjects.swift, Public/PublicTypes.swift, Internal/InternalLiveMapValue.swift, Internal/InternalTypes.swift
channel.objects: RealtimeObjects is replaced by channel.object: any RealtimeObject; PublicObjectsStore caches DefaultRealtimeObject via renamed getOrCreateRealtimeObject; old public proxy types and PublicTypes.swift are removed; InternalLiveMapValue's public-to-internal init is removed; new InternalTypes.swift adds internal lifecycle/update scaffolding.
Tests updated to single-object API
Tests/AblyLiveObjectsTests/AblyLiveObjectsTests.swift, ObjectLifetimesTests.swift, Helpers/ARTRealtimeChannel+TestHelpers.swift, JS Integration Tests/ObjectsHelper.swift
Tests reference channel.object instead of channel.objects, new test helper exposes internal objects + CoreSDK directly, lifetime/identity assertions updated to singular types, unimplemented-dependent tests commented out, and obsolete JS integration helper removed.

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)
Loading

Suggested reviewers: ttypic

Poem

A rabbit hops through paths anew,
One "object" now instead of two 🐇
Stubs stand ready, crash on call,
Waiting for logic to fill them all.
Old proxies swept, new roots take hold,
A cleaner burrow, brave and bold!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly reflects the main change: introducing a path-based public API.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch path-based-public-api

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions
github-actions Bot temporarily deployed to staging/pull/134/AblyLiveObjects July 7, 2026 02:07 Inactive
@maratal maratal changed the title Path-based public API [AIT-1023] Path-based public API Jul 7, 2026
@maratal
maratal marked this pull request as draft July 7, 2026 02:13

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
Sources/AblyLiveObjects/Path Based API/Public/ValueTypes.swift (1)

234-268: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Move public from members to the extension declaration.

Each of these six ExpressibleBy*Literal extensions marks the individual init as public instead 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*Literal protocol is preserved implicitly via the same extension; you can also keep the protocol name on the extension line if you prefer one extension per conformance, as long as public sits on the extension keyword rather than the member.)

As per coding guidelines: "When writing an extension of 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 win

Duplicate AsyncStream bridging logic.

The LiveMapInstance and LiveCounterInstance subscribe() extensions are byte-identical, and the same continuation/termination pattern is repeated again in PathObject.swift (lines 90-99) and InternalTypes.swift's LiveObject.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 win

Consider forward-compatibility for unknown wire actions.

PublicAPI.ObjectOperationAction exhaustively lists the currently-known actions with no unknown/catch-all case, unlike the internal wire ObjectOperationAction which wraps values in .known/.unknown (see ObjectCreationHelpers.swift usage 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

📥 Commits

Reviewing files that changed from the base of the PR and between a3bf63d and 2afaf8c.

📒 Files selected for processing (31)
  • Sources/AblyLiveObjects/Internal/InternalLiveMapValue.swift
  • Sources/AblyLiveObjects/Internal/InternalTypes.swift
  • Sources/AblyLiveObjects/Path Based API/Default/DefaultLiveCounterInstance.swift
  • Sources/AblyLiveObjects/Path Based API/Default/DefaultLiveCounterPathObject.swift
  • Sources/AblyLiveObjects/Path Based API/Default/DefaultLiveMapInstance.swift
  • Sources/AblyLiveObjects/Path Based API/Default/DefaultLiveMapPathObject.swift
  • Sources/AblyLiveObjects/Path Based API/Default/DefaultPathObject.swift
  • Sources/AblyLiveObjects/Path Based API/Default/DefaultPrimitiveInstance.swift
  • Sources/AblyLiveObjects/Path Based API/Default/DefaultPrimitivePathObject.swift
  • Sources/AblyLiveObjects/Path Based API/Default/DefaultRealtimeObject.swift
  • Sources/AblyLiveObjects/Path Based API/Default/DefaultStatusSubscription.swift
  • Sources/AblyLiveObjects/Path Based API/Default/DefaultSubscription.swift
  • Sources/AblyLiveObjects/Path Based API/NotImplemented.swift
  • Sources/AblyLiveObjects/Path Based API/Public/Channel+Object.swift
  • Sources/AblyLiveObjects/Path Based API/Public/Instance.swift
  • Sources/AblyLiveObjects/Path Based API/Public/PathObject.swift
  • Sources/AblyLiveObjects/Path Based API/Public/PublicObjectMessage.swift
  • Sources/AblyLiveObjects/Path Based API/Public/RealtimeObject.swift
  • Sources/AblyLiveObjects/Path Based API/Public/Subscriptions.swift
  • Sources/AblyLiveObjects/Path Based API/Public/ValueTypes.swift
  • Sources/AblyLiveObjects/Public/Public Proxy Objects/InternalLiveMapValue+ToPublic.swift
  • Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicDefaultLiveCounter.swift
  • Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicDefaultLiveMap.swift
  • Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicDefaultRealtimeObjects.swift
  • Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicObjectsStore.swift
  • Sources/AblyLiveObjects/Public/PublicTypes.swift
  • Tests/AblyLiveObjectsTests/AblyLiveObjectsTests.swift
  • Tests/AblyLiveObjectsTests/Helpers/ARTRealtimeChannel+TestHelpers.swift
  • Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsHelper.swift
  • Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsIntegrationTests.swift
  • Tests/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

@github-actions
github-actions Bot temporarily deployed to staging/pull/134/AblyLiveObjects July 7, 2026 12:33 Inactive

@sacOO7 sacOO7 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@maratal
maratal requested a review from ttypic July 7, 2026 13:26
@maratal
maratal changed the base branch from main to feature/path-based-liveobjects July 7, 2026 13:30
@maratal
maratal marked this pull request as ready for review July 7, 2026 13:30

@lawrence-forooghian lawrence-forooghian left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would be neater as a property (it's constant for the PathObject so it can be trivially cached)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in fabd648.


/// 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?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or even just type() for consistency with Instance. The get() thing feels like a Java-ism

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, *)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that these annotations throughout no longer are needed; weren't they just for ably-cocoa?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

InternalLiveMap, InternalLiveCounter, and LiveObject are no longer used or implemented. I think we can get rid of them?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think preserve this comment until we (probably – see my note on the objects store) get rid of this test

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 8c316e1 — restored the TODO.

///
/// 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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 7d5082b — dropped the helper; exposed testsOnly_coreSDK on PublicDefaultRealtimeObject and reach everything via channel.testsOnly_nonTypeErasedObject.

@maratal
maratal force-pushed the path-based-public-api branch from 5895231 to 6fc8aa4 Compare July 9, 2026 23:07
@github-actions
github-actions Bot temporarily deployed to staging/pull/134/AblyLiveObjects July 9, 2026 23:08 Inactive
@maratal

maratal commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

Largely LGTM. Please could you tidy up the commits? I think one groundwork for the ObjectMessage namespacing and then one for everything else?

Done, but I addressed your comments in an individual commit, will combine them in the "everything else" once approved.

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)?

Correct

And the intention for the ported JS integration tests is that they'll be replaced by the UTS?

Most likely, if not, update them to the new API once implemented.

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?

Update once API implemented.

@lawrence-forooghian

Copy link
Copy Markdown
Contributor

(claude-authored comment)

Small commit-hygiene note: the ProtocolTypes namespacing of ObjectOperationAction and ObjectsMapSemantics (in WireObjectMessage.swift) landed in a later commit rather than the groundwork commit (6ebc05f), so a bit of namespacing churn leaks into the rest of the PR. Could you fold it into 6ebc05f so that commit holds all the internal namespacing? Non-blocking — just keeps the groundwork-vs-rest split clean.

maratal and others added 2 commits July 10, 2026 15:13
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants