From 1ae1dee2864bfe11561be32688f5d6f45a891c7c Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Tue, 19 Aug 2025 11:45:08 -0300 Subject: [PATCH 01/11] DRY up usage of PluginAPI.sharedInstance() Should have done this in b69a40d. --- .../AblyLiveObjects/Internal/ARTClientOptions+Objects.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/AblyLiveObjects/Internal/ARTClientOptions+Objects.swift b/Sources/AblyLiveObjects/Internal/ARTClientOptions+Objects.swift index dac94d1..e1f251a 100644 --- a/Sources/AblyLiveObjects/Internal/ARTClientOptions+Objects.swift +++ b/Sources/AblyLiveObjects/Internal/ARTClientOptions+Objects.swift @@ -14,7 +14,7 @@ internal extension ARTClientOptions { /// Can be overriden for testing purposes. var garbageCollectionOptions: InternalDefaultRealtimeObjects.GarbageCollectionOptions? { get { - let optionsValue = PluginAPI.sharedInstance().pluginOptionsValue( + let optionsValue = Plugin.defaultPluginAPI.pluginOptionsValue( forKey: Self.garbageCollectionOptionsKey, clientOptions: self, ) @@ -35,7 +35,7 @@ internal extension ARTClientOptions { preconditionFailure("Not implemented the ability to un-set GC options") } - PluginAPI.sharedInstance().setPluginOptionsValue( + Plugin.defaultPluginAPI.setPluginOptionsValue( Box(boxed: newValue), forKey: Self.garbageCollectionOptionsKey, clientOptions: self, From 5795e7cac5870ae644ee346d12032a692923728e Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Tue, 19 Aug 2025 18:25:33 -0300 Subject: [PATCH 02/11] Update for renamed AblyPlugin -> _AblyPluginSupportPrivate As required by the accompanying ably-cocoa submodule bump. --- .cursor/rules/swift.mdc | 2 +- .cursor/rules/testing.mdc | 2 +- Package.swift | 4 +- .../Internal/ARTClientOptions+Objects.swift | 2 +- .../AblyLiveObjects/Internal/CoreSDK.swift | 16 +++--- .../Internal/DefaultInternalPlugin.swift | 38 ++++++------- .../Internal/InternalDefaultLiveCounter.swift | 10 ++-- .../Internal/InternalDefaultLiveMap.swift | 18 +++--- .../InternalDefaultRealtimeObjects.swift | 6 +- .../Internal/InternalLiveObject.swift | 2 +- .../Internal/LiveObjectMutableState.swift | 2 +- .../Internal/ObjectCreationHelpers.swift | 2 +- .../Internal/ObjectsPool.swift | 14 ++--- .../Protocol/ObjectMessage.swift | 34 +++++------ .../Protocol/WireObjectMessage.swift | 4 +- .../Public/ARTRealtimeChannel+Objects.swift | 2 +- Sources/AblyLiveObjects/Public/Plugin.swift | 10 ++-- .../InternalLiveMapValue+ToPublic.swift | 4 +- .../PublicDefaultLiveCounter.swift | 6 +- .../PublicDefaultLiveMap.swift | 6 +- .../PublicDefaultRealtimeObjects.swift | 6 +- .../PublicObjectsStore.swift | 12 ++-- .../Utility/APLogger+Swift.swift | 4 +- .../AblyLiveObjects/Utility/WireValue.swift | 30 +++++----- .../AblyLiveObjectsTests.swift | 2 +- .../Helpers/TestFactories.swift | 2 +- .../Helpers/TestLogger.swift | 6 +- .../InternalDefaultLiveCounterTests.swift | 2 +- .../InternalDefaultLiveMapTests.swift | 2 +- .../InternalDefaultRealtimeObjectsTests.swift | 2 +- .../JS Integration Tests/ObjectsHelper.swift | 4 +- .../LiveObjectMutableStateTests.swift | 2 +- .../ObjectMessageTests.swift | 2 +- .../ObjectsPoolTests.swift | 2 +- .../WireObjectMessageTests.swift | 4 +- .../AblyLiveObjectsTests/WireValueTests.swift | 56 +++++++++---------- ably-cocoa | 2 +- 37 files changed, 162 insertions(+), 162 deletions(-) diff --git a/.cursor/rules/swift.mdc b/.cursor/rules/swift.mdc index 3fa734a..b4a23b5 100644 --- a/.cursor/rules/swift.mdc +++ b/.cursor/rules/swift.mdc @@ -14,7 +14,7 @@ When writing Swift: - When writing a JSON string, favour using Swift raw string literals instead of escaping double quotes. - When you need to import the following modules inside the AblyLiveObjects library code (that is, in non-test code), do so in the following way: - Ably: use `import Ably` - - AblyPlugin: use `internal import AblyPlugin` + - `_AblyPluginSupportPrivate`: use `internal import _AblyPluginSupportPrivate` - When writing an array literal that starts with an initializer expression, start the initializer expression on the line after the opening square bracket of the array literal. That is, instead of writing: ```swift objectMessages: [InboundObjectMessage( diff --git a/.cursor/rules/testing.mdc b/.cursor/rules/testing.mdc index 0736293..f21ef64 100644 --- a/.cursor/rules/testing.mdc +++ b/.cursor/rules/testing.mdc @@ -14,7 +14,7 @@ When writing tests: - When you need to import the following modules in the tests, do so in the following way: - Ably: use `import Ably` - AblyLiveObjects: use `@testable import AblyLiveObjects` - - AblyPlugin: use `import AblyPlugin`; _do not_ do `internal import` + - `_AblyPluginSupportPrivate`: use `import _AblyPluginSupportPrivate`; _do not_ do `internal import` - When you need to pass a logger to internal components in the tests, pass `TestLogger()`. - When you need to unwrap an optional value in the tests, favour using `#require` instead of `guard let`. - When creating `testsOnly_` property declarations, do not write generic comments of the form "Test-only access to the private createOperationIsMerged property"; the meaning of these properties is already well understood. diff --git a/Package.swift b/Package.swift index 25fd4fc..742aa4b 100644 --- a/Package.swift +++ b/Package.swift @@ -47,7 +47,7 @@ let package = Package( package: "ably-cocoa", ), .product( - name: "AblyPlugin", + name: "_AblyPluginSupportPrivate", package: "ably-cocoa", ), ], @@ -61,7 +61,7 @@ let package = Package( package: "ably-cocoa", ), .product( - name: "AblyPlugin", + name: "_AblyPluginSupportPrivate", package: "ably-cocoa", ), ], diff --git a/Sources/AblyLiveObjects/Internal/ARTClientOptions+Objects.swift b/Sources/AblyLiveObjects/Internal/ARTClientOptions+Objects.swift index e1f251a..8797381 100644 --- a/Sources/AblyLiveObjects/Internal/ARTClientOptions+Objects.swift +++ b/Sources/AblyLiveObjects/Internal/ARTClientOptions+Objects.swift @@ -1,4 +1,4 @@ -internal import AblyPlugin +internal import _AblyPluginSupportPrivate internal extension ARTClientOptions { private class Box { diff --git a/Sources/AblyLiveObjects/Internal/CoreSDK.swift b/Sources/AblyLiveObjects/Internal/CoreSDK.swift index 51f0546..0c93078 100644 --- a/Sources/AblyLiveObjects/Internal/CoreSDK.swift +++ b/Sources/AblyLiveObjects/Internal/CoreSDK.swift @@ -1,9 +1,9 @@ +internal import _AblyPluginSupportPrivate import Ably -internal import AblyPlugin /// The API that the internal components of the SDK (that is, `DefaultLiveObjects` and down) use to interact with our core SDK (i.e. ably-cocoa). /// -/// This provides us with a mockable interface to ably-cocoa, and it also allows internal components and their tests not to need to worry about some of the boring details of how we bridge Swift types to AblyPlugin's Objective-C API (i.e. boxing). +/// This provides us with a mockable interface to ably-cocoa, and it also allows internal components and their tests not to need to worry about some of the boring details of how we bridge Swift types to `_AblyPluginSupportPrivate`'s Objective-C API (i.e. boxing). internal protocol CoreSDK: AnyObject, Sendable { /// Implements the internal `#publish` method of RTO15. func publish(objectMessages: [OutboundObjectMessage]) async throws(InternalError) @@ -21,10 +21,10 @@ internal final class DefaultCoreSDK: CoreSDK { /// Used to synchronize access to internal mutable state. private let mutex = NSLock() - private let channel: AblyPlugin.RealtimeChannel - private let client: AblyPlugin.RealtimeClient + private let channel: _AblyPluginSupportPrivate.RealtimeChannel + private let client: _AblyPluginSupportPrivate.RealtimeClient private let pluginAPI: PluginAPIProtocol - private let logger: AblyPlugin.Logger + private let logger: _AblyPluginSupportPrivate.Logger /// If set to true, ``publish(objectMessages:)`` will behave like a no-op. /// @@ -34,10 +34,10 @@ internal final class DefaultCoreSDK: CoreSDK { private nonisolated(unsafe) var overriddenPublishImplementation: (([OutboundObjectMessage]) async throws -> Void)? internal init( - channel: AblyPlugin.RealtimeChannel, - client: AblyPlugin.RealtimeClient, + channel: _AblyPluginSupportPrivate.RealtimeChannel, + client: _AblyPluginSupportPrivate.RealtimeClient, pluginAPI: PluginAPIProtocol, - logger: AblyPlugin.Logger + logger: _AblyPluginSupportPrivate.Logger ) { self.channel = channel self.client = client diff --git a/Sources/AblyLiveObjects/Internal/DefaultInternalPlugin.swift b/Sources/AblyLiveObjects/Internal/DefaultInternalPlugin.swift index 207ffd8..970157e 100644 --- a/Sources/AblyLiveObjects/Internal/DefaultInternalPlugin.swift +++ b/Sources/AblyLiveObjects/Internal/DefaultInternalPlugin.swift @@ -1,14 +1,14 @@ -internal import AblyPlugin +internal import _AblyPluginSupportPrivate -// We explicitly import the NSObject class, else it seems to get transitively imported from `internal import AblyPlugin`, leading to the error "Class cannot be declared public because its superclass is internal". +// We explicitly import the NSObject class, else it seems to get transitively imported from `internal import _AblyPluginSupportPrivate`, leading to the error "Class cannot be declared public because its superclass is internal". import ObjectiveC.NSObject -/// The default implementation of `AblyPlugin`'s `LiveObjectsInternalPluginProtocol`. Implements the interface that ably-cocoa uses to access the functionality provided by the LiveObjects plugin. +/// The default implementation of `_AblyPluginSupportPrivate`'s `LiveObjectsInternalPluginProtocol`. Implements the interface that ably-cocoa uses to access the functionality provided by the LiveObjects plugin. @objc -internal final class DefaultInternalPlugin: NSObject, AblyPlugin.LiveObjectsInternalPluginProtocol { - private let pluginAPI: AblyPlugin.PluginAPIProtocol +internal final class DefaultInternalPlugin: NSObject, _AblyPluginSupportPrivate.LiveObjectsInternalPluginProtocol { + private let pluginAPI: _AblyPluginSupportPrivate.PluginAPIProtocol - internal init(pluginAPI: AblyPlugin.PluginAPIProtocol) { + internal init(pluginAPI: _AblyPluginSupportPrivate.PluginAPIProtocol) { self.pluginAPI = pluginAPI } @@ -20,7 +20,7 @@ internal final class DefaultInternalPlugin: NSObject, AblyPlugin.LiveObjectsInte /// Retrieves the `RealtimeObjects` for this channel. /// /// We expect this value to have been previously set by ``prepare(_:)``. - internal static func realtimeObjects(for channel: AblyPlugin.RealtimeChannel, pluginAPI: AblyPlugin.PluginAPIProtocol) -> InternalDefaultRealtimeObjects { + internal static func realtimeObjects(for channel: _AblyPluginSupportPrivate.RealtimeChannel, pluginAPI: _AblyPluginSupportPrivate.PluginAPIProtocol) -> InternalDefaultRealtimeObjects { guard let pluginData = pluginAPI.pluginDataValue(forKey: pluginDataKey, channel: channel) else { // InternalPlugin.prepare was not called fatalError("To access LiveObjects functionality, you must pass the LiveObjects plugin in the client options when creating the ARTRealtime instance: `clientOptions.plugins = [.liveObjects: AblyLiveObjects.Plugin.self]`") @@ -33,7 +33,7 @@ internal final class DefaultInternalPlugin: NSObject, AblyPlugin.LiveObjectsInte // MARK: - LiveObjectsInternalPluginProtocol // Populates the channel's `objects` property. - internal func prepare(_ channel: AblyPlugin.RealtimeChannel, client: AblyPlugin.RealtimeClient) { + internal func prepare(_ channel: _AblyPluginSupportPrivate.RealtimeChannel, client: _AblyPluginSupportPrivate.RealtimeClient) { let logger = pluginAPI.logger(for: channel) let callbackQueue = pluginAPI.callbackQueue(for: client) let options = pluginAPI.options(for: client) @@ -49,14 +49,14 @@ internal final class DefaultInternalPlugin: NSObject, AblyPlugin.LiveObjectsInte } /// Retrieves the internally-typed `objects` property for the channel. - private func realtimeObjects(for channel: AblyPlugin.RealtimeChannel) -> InternalDefaultRealtimeObjects { + private func realtimeObjects(for channel: _AblyPluginSupportPrivate.RealtimeChannel) -> InternalDefaultRealtimeObjects { Self.realtimeObjects(for: channel, pluginAPI: pluginAPI) } /// A class that wraps an object message. /// - /// We need this intermediate type because we want object messages to be structs — because they're nicer to work with internally — but a struct can't conform to the class-bound `AblyPlugin.ObjectMessageProtocol`. - private final class ObjectMessageBox: AblyPlugin.ObjectMessageProtocol where T: Sendable { + /// We need this intermediate type because we want object messages to be structs — because they're nicer to work with internally — but a struct can't conform to the class-bound `_AblyPluginSupportPrivate.ObjectMessageProtocol`. + private final class ObjectMessageBox: _AblyPluginSupportPrivate.ObjectMessageProtocol where T: Sendable { internal let objectMessage: T init(objectMessage: T) { @@ -70,7 +70,7 @@ internal final class DefaultInternalPlugin: NSObject, AblyPlugin.LiveObjectsInte format: EncodingFormat, error errorPtr: AutoreleasingUnsafeMutablePointer?, ) -> (any ObjectMessageProtocol)? { - let wireObject = WireValue.objectFromAblyPluginData(serialized) + let wireObject = WireValue.objectFrom_AblyPluginSupportPrivateData(serialized) do { let wireObjectMessage = try InboundWireObjectMessage( @@ -89,7 +89,7 @@ internal final class DefaultInternalPlugin: NSObject, AblyPlugin.LiveObjectsInte } internal func encodeObjectMessage( - _ publicObjectMessage: any AblyPlugin.ObjectMessageProtocol, + _ publicObjectMessage: any _AblyPluginSupportPrivate.ObjectMessageProtocol, format: EncodingFormat, ) -> [String: Any] { guard let outboundObjectMessageBox = publicObjectMessage as? ObjectMessageBox else { @@ -97,14 +97,14 @@ internal final class DefaultInternalPlugin: NSObject, AblyPlugin.LiveObjectsInte } let wireObjectMessage = outboundObjectMessageBox.objectMessage.toWire(format: format) - return wireObjectMessage.toWireObject.toAblyPluginDataDictionary + return wireObjectMessage.toWireObject.toPluginSupportDataDictionary } - internal func onChannelAttached(_ channel: AblyPlugin.RealtimeChannel, hasObjects: Bool) { + internal func onChannelAttached(_ channel: _AblyPluginSupportPrivate.RealtimeChannel, hasObjects: Bool) { realtimeObjects(for: channel).onChannelAttached(hasObjects: hasObjects) } - internal func handleObjectProtocolMessage(withObjectMessages publicObjectMessages: [any AblyPlugin.ObjectMessageProtocol], channel: AblyPlugin.RealtimeChannel) { + internal func handleObjectProtocolMessage(withObjectMessages publicObjectMessages: [any _AblyPluginSupportPrivate.ObjectMessageProtocol], channel: _AblyPluginSupportPrivate.RealtimeChannel) { guard let inboundObjectMessageBoxes = publicObjectMessages as? [ObjectMessageBox] else { preconditionFailure("Expected to receive the same InboundObjectMessage type as we emit") } @@ -116,7 +116,7 @@ internal final class DefaultInternalPlugin: NSObject, AblyPlugin.LiveObjectsInte ) } - internal func handleObjectSyncProtocolMessage(withObjectMessages publicObjectMessages: [any AblyPlugin.ObjectMessageProtocol], protocolMessageChannelSerial: String?, channel: AblyPlugin.RealtimeChannel) { + internal func handleObjectSyncProtocolMessage(withObjectMessages publicObjectMessages: [any _AblyPluginSupportPrivate.ObjectMessageProtocol], protocolMessageChannelSerial: String?, channel: _AblyPluginSupportPrivate.RealtimeChannel) { guard let inboundObjectMessageBoxes = publicObjectMessages as? [ObjectMessageBox] else { preconditionFailure("Expected to receive the same InboundObjectMessage type as we emit") } @@ -133,8 +133,8 @@ internal final class DefaultInternalPlugin: NSObject, AblyPlugin.LiveObjectsInte internal static func sendObject( objectMessages: [OutboundObjectMessage], - channel: AblyPlugin.RealtimeChannel, - client: AblyPlugin.RealtimeClient, + channel: _AblyPluginSupportPrivate.RealtimeChannel, + client: _AblyPluginSupportPrivate.RealtimeClient, pluginAPI: PluginAPIProtocol, ) async throws(InternalError) { let objectMessageBoxes: [ObjectMessageBox] = objectMessages.map { .init(objectMessage: $0) } diff --git a/Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift b/Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift index a5d7058..4169912 100644 --- a/Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift +++ b/Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift @@ -1,5 +1,5 @@ +internal import _AblyPluginSupportPrivate import Ably -internal import AblyPlugin import Foundation /// This provides the implementation behind ``PublicDefaultLiveCounter``, via internal versions of the ``LiveCounter`` API. @@ -21,7 +21,7 @@ internal final class InternalDefaultLiveCounter: Sendable { } } - private let logger: AblyPlugin.Logger + private let logger: _AblyPluginSupportPrivate.Logger private let userCallbackQueue: DispatchQueue private let clock: SimpleClock @@ -30,7 +30,7 @@ internal final class InternalDefaultLiveCounter: Sendable { internal convenience init( testsOnly_data data: Double, objectID: String, - logger: AblyPlugin.Logger, + logger: _AblyPluginSupportPrivate.Logger, userCallbackQueue: DispatchQueue, clock: SimpleClock ) { @@ -40,7 +40,7 @@ internal final class InternalDefaultLiveCounter: Sendable { private init( data: Double, objectID: String, - logger: AblyPlugin.Logger, + logger: _AblyPluginSupportPrivate.Logger, userCallbackQueue: DispatchQueue, clock: SimpleClock ) { @@ -56,7 +56,7 @@ internal final class InternalDefaultLiveCounter: Sendable { /// - objectID: The value for the "private objectId field" of RTO5c1b1a. internal static func createZeroValued( objectID: String, - logger: AblyPlugin.Logger, + logger: _AblyPluginSupportPrivate.Logger, userCallbackQueue: DispatchQueue, clock: SimpleClock, ) -> Self { diff --git a/Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift b/Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift index 312affa..e952cdb 100644 --- a/Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift +++ b/Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift @@ -1,5 +1,5 @@ +internal import _AblyPluginSupportPrivate import Ably -internal import AblyPlugin /// Protocol for accessing objects from the ObjectsPool. This is used by a LiveMap when it needs to return an object given an object ID. internal protocol LiveMapObjectPoolDelegate: AnyObject, Sendable { @@ -38,7 +38,7 @@ internal final class InternalDefaultLiveMap: Sendable { } } - private let logger: AblyPlugin.Logger + private let logger: _AblyPluginSupportPrivate.Logger private let userCallbackQueue: DispatchQueue private let clock: SimpleClock @@ -48,7 +48,7 @@ internal final class InternalDefaultLiveMap: Sendable { testsOnly_data data: [String: InternalObjectsMapEntry], objectID: String, testsOnly_semantics semantics: WireEnum? = nil, - logger: AblyPlugin.Logger, + logger: _AblyPluginSupportPrivate.Logger, userCallbackQueue: DispatchQueue, clock: SimpleClock, ) { @@ -66,7 +66,7 @@ internal final class InternalDefaultLiveMap: Sendable { data: [String: InternalObjectsMapEntry], objectID: String, semantics: WireEnum?, - logger: AblyPlugin.Logger, + logger: _AblyPluginSupportPrivate.Logger, userCallbackQueue: DispatchQueue, clock: SimpleClock, ) { @@ -84,7 +84,7 @@ internal final class InternalDefaultLiveMap: Sendable { internal static func createZeroValued( objectID: String, semantics: WireEnum? = nil, - logger: AblyPlugin.Logger, + logger: _AblyPluginSupportPrivate.Logger, userCallbackQueue: DispatchQueue, clock: SimpleClock, ) -> Self { @@ -398,7 +398,7 @@ internal final class InternalDefaultLiveMap: Sendable { using state: ObjectState, objectMessageSerialTimestamp: Date?, objectsPool: inout ObjectsPool, - logger: AblyPlugin.Logger, + logger: _AblyPluginSupportPrivate.Logger, clock: SimpleClock, userCallbackQueue: DispatchQueue, ) -> LiveObjectUpdate { @@ -467,7 +467,7 @@ internal final class InternalDefaultLiveMap: Sendable { internal mutating func mergeInitialValue( from operation: ObjectOperation, objectsPool: inout ObjectsPool, - logger: AblyPlugin.Logger, + logger: _AblyPluginSupportPrivate.Logger, userCallbackQueue: DispatchQueue, clock: SimpleClock, ) -> LiveObjectUpdate { @@ -621,7 +621,7 @@ internal final class InternalDefaultLiveMap: Sendable { operationTimeserial: String?, operationData: ObjectData?, objectsPool: inout ObjectsPool, - logger: AblyPlugin.Logger, + logger: _AblyPluginSupportPrivate.Logger, userCallbackQueue: DispatchQueue, clock: SimpleClock, ) -> LiveObjectUpdate { @@ -744,7 +744,7 @@ internal final class InternalDefaultLiveMap: Sendable { internal mutating func applyMapCreateOperation( _ operation: ObjectOperation, objectsPool: inout ObjectsPool, - logger: AblyPlugin.Logger, + logger: _AblyPluginSupportPrivate.Logger, userCallbackQueue: DispatchQueue, clock: SimpleClock, ) -> LiveObjectUpdate { diff --git a/Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift b/Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift index af12e8a..89770aa 100644 --- a/Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift +++ b/Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift @@ -1,5 +1,5 @@ +internal import _AblyPluginSupportPrivate import Ably -internal import AblyPlugin /// This provides the implementation behind ``PublicDefaultRealtimeObjects``, via internal versions of the ``RealtimeObjects`` API. internal final class InternalDefaultRealtimeObjects: Sendable, LiveMapObjectPoolDelegate { @@ -8,7 +8,7 @@ internal final class InternalDefaultRealtimeObjects: Sendable, LiveMapObjectPool private nonisolated(unsafe) var mutableState: MutableState! - private let logger: AblyPlugin.Logger + private let logger: _AblyPluginSupportPrivate.Logger private let userCallbackQueue: DispatchQueue private let clock: SimpleClock @@ -91,7 +91,7 @@ internal final class InternalDefaultRealtimeObjects: Sendable, LiveMapObjectPool } } - internal init(logger: AblyPlugin.Logger, userCallbackQueue: DispatchQueue, clock: SimpleClock, garbageCollectionOptions: GarbageCollectionOptions = .init()) { + internal init(logger: _AblyPluginSupportPrivate.Logger, userCallbackQueue: DispatchQueue, clock: SimpleClock, garbageCollectionOptions: GarbageCollectionOptions = .init()) { self.logger = logger self.userCallbackQueue = userCallbackQueue self.clock = clock diff --git a/Sources/AblyLiveObjects/Internal/InternalLiveObject.swift b/Sources/AblyLiveObjects/Internal/InternalLiveObject.swift index d80d53b..27b0d7e 100644 --- a/Sources/AblyLiveObjects/Internal/InternalLiveObject.swift +++ b/Sources/AblyLiveObjects/Internal/InternalLiveObject.swift @@ -1,4 +1,4 @@ -internal import AblyPlugin +internal import _AblyPluginSupportPrivate /// Provides RTLO spec point functionality common to all LiveObjects. /// diff --git a/Sources/AblyLiveObjects/Internal/LiveObjectMutableState.swift b/Sources/AblyLiveObjects/Internal/LiveObjectMutableState.swift index f332620..b471677 100644 --- a/Sources/AblyLiveObjects/Internal/LiveObjectMutableState.swift +++ b/Sources/AblyLiveObjects/Internal/LiveObjectMutableState.swift @@ -1,4 +1,4 @@ -internal import AblyPlugin +internal import _AblyPluginSupportPrivate /// This is the equivalent of the `LiveObject` abstract class described in RTLO. /// diff --git a/Sources/AblyLiveObjects/Internal/ObjectCreationHelpers.swift b/Sources/AblyLiveObjects/Internal/ObjectCreationHelpers.swift index b82f7d8..f061302 100644 --- a/Sources/AblyLiveObjects/Internal/ObjectCreationHelpers.swift +++ b/Sources/AblyLiveObjects/Internal/ObjectCreationHelpers.swift @@ -1,4 +1,4 @@ -internal import AblyPlugin +internal import _AblyPluginSupportPrivate import CryptoKit import Foundation diff --git a/Sources/AblyLiveObjects/Internal/ObjectsPool.swift b/Sources/AblyLiveObjects/Internal/ObjectsPool.swift index d2ff648..a16cd24 100644 --- a/Sources/AblyLiveObjects/Internal/ObjectsPool.swift +++ b/Sources/AblyLiveObjects/Internal/ObjectsPool.swift @@ -1,4 +1,4 @@ -internal import AblyPlugin +internal import _AblyPluginSupportPrivate /// Maintains the list of objects present on a channel, as described by RTO3. /// @@ -137,7 +137,7 @@ internal struct ObjectsPool { /// Creates an `ObjectsPool` whose root is a zero-value `LiveMap`. internal init( - logger: AblyPlugin.Logger, + logger: _AblyPluginSupportPrivate.Logger, userCallbackQueue: DispatchQueue, clock: SimpleClock, testsOnly_otherEntries otherEntries: [String: Entry]? = nil, @@ -151,7 +151,7 @@ internal struct ObjectsPool { } private init( - logger: AblyPlugin.Logger, + logger: _AblyPluginSupportPrivate.Logger, userCallbackQueue: DispatchQueue, clock: SimpleClock, otherEntries: [String: Entry]? @@ -187,7 +187,7 @@ internal struct ObjectsPool { /// - userCallbackQueue: The callback queue to use for any created LiveObject /// - clock: The clock to use for any created LiveObject /// - Returns: The existing or newly created object - internal mutating func createZeroValueObject(forObjectID objectID: String, logger: AblyPlugin.Logger, userCallbackQueue: DispatchQueue, clock: SimpleClock) -> Entry? { + internal mutating func createZeroValueObject(forObjectID objectID: String, logger: _AblyPluginSupportPrivate.Logger, userCallbackQueue: DispatchQueue, clock: SimpleClock) -> Entry? { // RTO6a: If an object with objectId exists in ObjectsPool, do not create a new object if let existingEntry = entries[objectID] { return existingEntry @@ -220,7 +220,7 @@ internal struct ObjectsPool { /// Applies the objects gathered during an `OBJECT_SYNC` to this `ObjectsPool`, per RTO5c1 and RTO5c2. internal mutating func applySyncObjectsPool( _ syncObjectsPool: [SyncObjectsPoolEntry], - logger: AblyPlugin.Logger, + logger: _AblyPluginSupportPrivate.Logger, userCallbackQueue: DispatchQueue, clock: SimpleClock, ) { @@ -316,7 +316,7 @@ internal struct ObjectsPool { /// - Returns: The existing or newly created counter object internal mutating func getOrCreateCounter( creationOperation: ObjectCreationHelpers.CounterCreationOperation, - logger: AblyPlugin.Logger, + logger: _AblyPluginSupportPrivate.Logger, userCallbackQueue: DispatchQueue, clock: SimpleClock, ) -> InternalDefaultLiveCounter { @@ -360,7 +360,7 @@ internal struct ObjectsPool { /// - Returns: The existing or newly created map object internal mutating func getOrCreateMap( creationOperation: ObjectCreationHelpers.MapCreationOperation, - logger: AblyPlugin.Logger, + logger: _AblyPluginSupportPrivate.Logger, userCallbackQueue: DispatchQueue, clock: SimpleClock, ) -> InternalDefaultLiveMap { diff --git a/Sources/AblyLiveObjects/Protocol/ObjectMessage.swift b/Sources/AblyLiveObjects/Protocol/ObjectMessage.swift index 0a24019..ac846fc 100644 --- a/Sources/AblyLiveObjects/Protocol/ObjectMessage.swift +++ b/Sources/AblyLiveObjects/Protocol/ObjectMessage.swift @@ -1,4 +1,4 @@ -internal import AblyPlugin +internal import _AblyPluginSupportPrivate import Foundation // This file contains the ObjectMessage types that we use within the codebase. We convert them to and from the corresponding wire types (e.g. `InboundWireObjectMessage`) for sending and receiving over the wire. @@ -97,7 +97,7 @@ internal extension InboundObjectMessage { /// - Throws: `InternalError` if JSON or Base64 decoding fails. init( wireObjectMessage: InboundWireObjectMessage, - format: AblyPlugin.EncodingFormat + format: _AblyPluginSupportPrivate.EncodingFormat ) throws(InternalError) { id = wireObjectMessage.id clientId = wireObjectMessage.clientId @@ -121,7 +121,7 @@ internal extension OutboundObjectMessage { /// /// - Parameters: /// - format: The format to use when applying the encoding rules of OD4. - func toWire(format: AblyPlugin.EncodingFormat) -> OutboundWireObjectMessage { + func toWire(format: _AblyPluginSupportPrivate.EncodingFormat) -> OutboundWireObjectMessage { .init( id: id, clientId: clientId, @@ -145,7 +145,7 @@ internal extension ObjectOperation { /// - Throws: `InternalError` if JSON or Base64 decoding fails. init( wireObjectOperation: WireObjectOperation, - format: AblyPlugin.EncodingFormat + format: _AblyPluginSupportPrivate.EncodingFormat ) throws(InternalError) { // Decode the action and objectId first they're not part of PartialObjectOperation action = wireObjectOperation.action @@ -177,7 +177,7 @@ internal extension ObjectOperation { /// /// - Parameters: /// - format: The format to use when applying the encoding rules of OD4. - func toWire(format: AblyPlugin.EncodingFormat) -> WireObjectOperation { + func toWire(format: _AblyPluginSupportPrivate.EncodingFormat) -> WireObjectOperation { let partialWireOperation = PartialObjectOperation( mapOp: mapOp, counterOp: counterOp, @@ -209,7 +209,7 @@ internal extension PartialObjectOperation { /// - Throws: `InternalError` if JSON or Base64 decoding fails. init( partialWireObjectOperation: PartialWireObjectOperation, - format: AblyPlugin.EncodingFormat + format: _AblyPluginSupportPrivate.EncodingFormat ) throws(InternalError) { mapOp = try partialWireObjectOperation.mapOp.map { wireObjectsMapOp throws(InternalError) in try .init(wireObjectsMapOp: wireObjectsMapOp, format: format) @@ -230,7 +230,7 @@ internal extension PartialObjectOperation { /// /// - Parameters: /// - format: The format to use when applying the encoding rules of OD4. - func toWire(format: AblyPlugin.EncodingFormat) -> PartialWireObjectOperation { + func toWire(format: _AblyPluginSupportPrivate.EncodingFormat) -> PartialWireObjectOperation { .init( mapOp: mapOp?.toWire(format: format), counterOp: counterOp, @@ -250,7 +250,7 @@ internal extension ObjectData { /// - Throws: `InternalError` if JSON or Base64 decoding fails. init( wireObjectData: WireObjectData, - format: AblyPlugin.EncodingFormat + format: _AblyPluginSupportPrivate.EncodingFormat ) throws(InternalError) { objectId = wireObjectData.objectId boolean = wireObjectData.boolean @@ -302,7 +302,7 @@ internal extension ObjectData { /// /// - Parameters: /// - format: The format to use when applying the encoding rules of OD4. - func toWire(format: AblyPlugin.EncodingFormat) -> WireObjectData { + func toWire(format: _AblyPluginSupportPrivate.EncodingFormat) -> WireObjectData { // OD4: Encode data based on format let wireBytes: StringOrData? = if let bytes { switch format { @@ -354,7 +354,7 @@ internal extension ObjectsMapOp { /// - Throws: `InternalError` if JSON or Base64 decoding fails. init( wireObjectsMapOp: WireObjectsMapOp, - format: AblyPlugin.EncodingFormat + format: _AblyPluginSupportPrivate.EncodingFormat ) throws(InternalError) { key = wireObjectsMapOp.key data = try wireObjectsMapOp.data.map { wireObjectData throws(InternalError) in @@ -366,7 +366,7 @@ internal extension ObjectsMapOp { /// /// - Parameters: /// - format: The format to use when applying the encoding rules of OD4. - func toWire(format: AblyPlugin.EncodingFormat) -> WireObjectsMapOp { + func toWire(format: _AblyPluginSupportPrivate.EncodingFormat) -> WireObjectsMapOp { .init( key: key, data: data?.toWire(format: format), @@ -382,7 +382,7 @@ internal extension ObjectsMapEntry { /// - Throws: `InternalError` if JSON or Base64 decoding fails. init( wireObjectsMapEntry: WireObjectsMapEntry, - format: AblyPlugin.EncodingFormat + format: _AblyPluginSupportPrivate.EncodingFormat ) throws(InternalError) { tombstone = wireObjectsMapEntry.tombstone timeserial = wireObjectsMapEntry.timeserial @@ -398,7 +398,7 @@ internal extension ObjectsMapEntry { /// /// - Parameters: /// - format: The format to use when applying the encoding rules of OD4. - func toWire(format: AblyPlugin.EncodingFormat) -> WireObjectsMapEntry { + func toWire(format: _AblyPluginSupportPrivate.EncodingFormat) -> WireObjectsMapEntry { .init( tombstone: tombstone, timeserial: timeserial, @@ -415,7 +415,7 @@ internal extension ObjectsMap { /// - Throws: `InternalError` if JSON or Base64 decoding fails. init( wireObjectsMap: WireObjectsMap, - format: AblyPlugin.EncodingFormat + format: _AblyPluginSupportPrivate.EncodingFormat ) throws(InternalError) { semantics = wireObjectsMap.semantics entries = try wireObjectsMap.entries?.ablyLiveObjects_mapValuesWithTypedThrow { wireMapEntry throws(InternalError) in @@ -427,7 +427,7 @@ internal extension ObjectsMap { /// /// - Parameters: /// - format: The format to use when applying the encoding rules of OD4. - func toWire(format: AblyPlugin.EncodingFormat) -> WireObjectsMap { + func toWire(format: _AblyPluginSupportPrivate.EncodingFormat) -> WireObjectsMap { .init( semantics: semantics, entries: entries?.mapValues { $0.toWire(format: format) }, @@ -443,7 +443,7 @@ internal extension ObjectState { /// - Throws: `InternalError` if JSON or Base64 decoding fails. init( wireObjectState: WireObjectState, - format: AblyPlugin.EncodingFormat + format: _AblyPluginSupportPrivate.EncodingFormat ) throws(InternalError) { objectId = wireObjectState.objectId siteTimeserials = wireObjectState.siteTimeserials @@ -461,7 +461,7 @@ internal extension ObjectState { /// /// - Parameters: /// - format: The format to use when applying the encoding rules of OD4. - func toWire(format: AblyPlugin.EncodingFormat) -> WireObjectState { + func toWire(format: _AblyPluginSupportPrivate.EncodingFormat) -> WireObjectState { .init( objectId: objectId, siteTimeserials: siteTimeserials, diff --git a/Sources/AblyLiveObjects/Protocol/WireObjectMessage.swift b/Sources/AblyLiveObjects/Protocol/WireObjectMessage.swift index fad740a..a8c55d5 100644 --- a/Sources/AblyLiveObjects/Protocol/WireObjectMessage.swift +++ b/Sources/AblyLiveObjects/Protocol/WireObjectMessage.swift @@ -1,4 +1,4 @@ -internal import AblyPlugin +internal import _AblyPluginSupportPrivate import Foundation // This file contains the ObjectMessage types that we send and receive over the wire. We convert them to and from the corresponding non-wire types (e.g. `InboundObjectMessage`) for use within the codebase. @@ -61,7 +61,7 @@ internal extension InboundWireObjectMessage { /// Decodes the `ObjectMessage` and then uses the containing `ProtocolMessage` to populate some absent fields per the rules of the specification. init( wireObject: [String: WireValue], - decodingContext: AblyPlugin.DecodingContextProtocol + decodingContext: _AblyPluginSupportPrivate.DecodingContextProtocol ) throws(InternalError) { // OM2a if let id = try wireObject.optionalStringValueForKey(WireObjectMessageWireKey.id.rawValue) { diff --git a/Sources/AblyLiveObjects/Public/ARTRealtimeChannel+Objects.swift b/Sources/AblyLiveObjects/Public/ARTRealtimeChannel+Objects.swift index da47917..67b8612 100644 --- a/Sources/AblyLiveObjects/Public/ARTRealtimeChannel+Objects.swift +++ b/Sources/AblyLiveObjects/Public/ARTRealtimeChannel+Objects.swift @@ -1,5 +1,5 @@ +internal import _AblyPluginSupportPrivate import Ably -internal import AblyPlugin public extension ARTRealtimeChannel { /// A ``RealtimeObjects`` object. diff --git a/Sources/AblyLiveObjects/Public/Plugin.swift b/Sources/AblyLiveObjects/Public/Plugin.swift index 26a377d..870a932 100644 --- a/Sources/AblyLiveObjects/Public/Plugin.swift +++ b/Sources/AblyLiveObjects/Public/Plugin.swift @@ -1,6 +1,6 @@ -internal import AblyPlugin +internal import _AblyPluginSupportPrivate -// We explicitly import the NSObject class, else it seems to get transitively imported from `internal import AblyPlugin`, leading to the error "Class cannot be declared public because its superclass is internal". +// We explicitly import the NSObject class, else it seems to get transitively imported from `internal import _AblyPluginSupportPrivate`, leading to the error "Class cannot be declared public because its superclass is internal". import ObjectiveC.NSObject /// This plugin enables LiveObjects functionality in ably-cocoa. Set the `.liveObjects` key in the ably-cocoa `plugins` client option to this class in order to enable LiveObjects. @@ -22,10 +22,10 @@ import ObjectiveC.NSObject /// ``` @objc public class Plugin: NSObject { - /// The `AblyPlugin.PluginAPIProtocol` that the LiveObjects plugin should use by default (i.e. when one hasn't been injected for test purposes). - internal static let defaultPluginAPI: AblyPlugin.PluginAPIProtocol = AblyPlugin.PluginAPI.sharedInstance() + /// The `_AblyPluginSupportPrivate.PluginAPIProtocol` that the LiveObjects plugin should use by default (i.e. when one hasn't been injected for test purposes). + internal static let defaultPluginAPI: _AblyPluginSupportPrivate.PluginAPIProtocol = _AblyPluginSupportPrivate.PluginAPI.sharedInstance() - // MARK: - Informal conformance to AblyPlugin.LiveObjectsPluginProtocol + // MARK: - Informal conformance to _AblyPluginSupportPrivate.LiveObjectsPluginProtocol @objc private static let internalPlugin = DefaultInternalPlugin(pluginAPI: defaultPluginAPI) } diff --git a/Sources/AblyLiveObjects/Public/Public Proxy Objects/InternalLiveMapValue+ToPublic.swift b/Sources/AblyLiveObjects/Public/Public Proxy Objects/InternalLiveMapValue+ToPublic.swift index bd88d2f..91fe81e 100644 --- a/Sources/AblyLiveObjects/Public/Public Proxy Objects/InternalLiveMapValue+ToPublic.swift +++ b/Sources/AblyLiveObjects/Public/Public Proxy Objects/InternalLiveMapValue+ToPublic.swift @@ -1,4 +1,4 @@ -internal import AblyPlugin +internal import _AblyPluginSupportPrivate internal extension InternalLiveMapValue { // MARK: - Mapping to public types @@ -6,7 +6,7 @@ internal extension InternalLiveMapValue { struct PublicValueCreationArgs { internal var coreSDK: CoreSDK internal var mapDelegate: LiveMapObjectPoolDelegate - internal var logger: AblyPlugin.Logger + internal var logger: _AblyPluginSupportPrivate.Logger internal var toCounterCreationArgs: PublicObjectsStore.CounterCreationArgs { .init(coreSDK: coreSDK, logger: logger) diff --git a/Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicDefaultLiveCounter.swift b/Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicDefaultLiveCounter.swift index 146d05e..6388aa5 100644 --- a/Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicDefaultLiveCounter.swift +++ b/Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicDefaultLiveCounter.swift @@ -1,5 +1,5 @@ +internal import _AblyPluginSupportPrivate import Ably -internal import AblyPlugin /// Our default implementation of ``LiveCounter``. /// @@ -10,9 +10,9 @@ internal final class PublicDefaultLiveCounter: LiveCounter { // MARK: - Dependencies that hold a strong reference to `proxied` private let coreSDK: CoreSDK - private let logger: AblyPlugin.Logger + private let logger: _AblyPluginSupportPrivate.Logger - internal init(proxied: InternalDefaultLiveCounter, coreSDK: CoreSDK, logger: AblyPlugin.Logger) { + internal init(proxied: InternalDefaultLiveCounter, coreSDK: CoreSDK, logger: _AblyPluginSupportPrivate.Logger) { self.proxied = proxied self.coreSDK = coreSDK self.logger = logger diff --git a/Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicDefaultLiveMap.swift b/Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicDefaultLiveMap.swift index 52e207d..a6674a4 100644 --- a/Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicDefaultLiveMap.swift +++ b/Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicDefaultLiveMap.swift @@ -1,5 +1,5 @@ +internal import _AblyPluginSupportPrivate import Ably -internal import AblyPlugin /// Our default implementation of ``LiveMap``. /// @@ -11,9 +11,9 @@ internal final class PublicDefaultLiveMap: LiveMap { private let coreSDK: CoreSDK private let delegate: LiveMapObjectPoolDelegate - private let logger: AblyPlugin.Logger + private let logger: _AblyPluginSupportPrivate.Logger - internal init(proxied: InternalDefaultLiveMap, coreSDK: CoreSDK, delegate: LiveMapObjectPoolDelegate, logger: AblyPlugin.Logger) { + internal init(proxied: InternalDefaultLiveMap, coreSDK: CoreSDK, delegate: LiveMapObjectPoolDelegate, logger: _AblyPluginSupportPrivate.Logger) { self.proxied = proxied self.coreSDK = coreSDK self.delegate = delegate diff --git a/Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicDefaultRealtimeObjects.swift b/Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicDefaultRealtimeObjects.swift index 3d97b8b..19a723c 100644 --- a/Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicDefaultRealtimeObjects.swift +++ b/Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicDefaultRealtimeObjects.swift @@ -1,5 +1,5 @@ +internal import _AblyPluginSupportPrivate import Ably -internal import AblyPlugin /// The class that provides the public API for interacting with LiveObjects, via the ``ARTRealtimeChannel/objects`` property. /// @@ -13,9 +13,9 @@ internal final class PublicDefaultRealtimeObjects: RealtimeObjects { // MARK: - Dependencies that hold a strong reference to `proxied` private let coreSDK: CoreSDK - private let logger: AblyPlugin.Logger + private let logger: _AblyPluginSupportPrivate.Logger - internal init(proxied: InternalDefaultRealtimeObjects, coreSDK: CoreSDK, logger: AblyPlugin.Logger) { + internal init(proxied: InternalDefaultRealtimeObjects, coreSDK: CoreSDK, logger: _AblyPluginSupportPrivate.Logger) { self.proxied = proxied self.coreSDK = coreSDK self.logger = logger diff --git a/Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicObjectsStore.swift b/Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicObjectsStore.swift index 4be0753..a6e7033 100644 --- a/Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicObjectsStore.swift +++ b/Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicObjectsStore.swift @@ -1,4 +1,4 @@ -internal import AblyPlugin +internal import _AblyPluginSupportPrivate import Foundation /// Stores the public objects that wrap the SDK's internal components. @@ -21,7 +21,7 @@ internal final class PublicObjectsStore: Sendable { internal struct RealtimeObjectsCreationArgs { internal var coreSDK: CoreSDK - internal var logger: AblyPlugin.Logger + internal var logger: _AblyPluginSupportPrivate.Logger } /// Fetches the cached `PublicDefaultRealtimeObjects` that wraps a given `InternalDefaultRealtimeObjects`, creating a new public object if there isn't already one. @@ -33,7 +33,7 @@ internal final class PublicObjectsStore: Sendable { internal struct CounterCreationArgs { internal var coreSDK: CoreSDK - internal var logger: AblyPlugin.Logger + internal var logger: _AblyPluginSupportPrivate.Logger } /// Fetches the cached `PublicDefaultLiveCounter` that wraps a given `InternalDefaultLiveCounter`, creating a new public object if there isn't already one. @@ -46,7 +46,7 @@ internal final class PublicObjectsStore: Sendable { internal struct MapCreationArgs { internal var coreSDK: CoreSDK internal var delegate: LiveMapObjectPoolDelegate - internal var logger: AblyPlugin.Logger + internal var logger: _AblyPluginSupportPrivate.Logger } /// Fetches the cached `PublicDefaultLiveMap` that wraps a given `InternalDefaultLiveMap`, creating a new public object if there isn't already one. @@ -68,7 +68,7 @@ internal final class PublicObjectsStore: Sendable { /// Fetches the proxy that wraps `proxied`, creating a new proxy if there isn't already one. Stores a weak reference to the proxy. mutating func getOrCreate( proxying proxied: some AnyObject, - logger: AblyPlugin.Logger, + logger: _AblyPluginSupportPrivate.Logger, logObjectType: String, createProxy: () -> Proxy, ) -> Proxy { @@ -90,7 +90,7 @@ internal final class PublicObjectsStore: Sendable { return created } - private mutating func removeDeallocatedEntries(logger: AblyPlugin.Logger, logObjectType: String) { + private mutating func removeDeallocatedEntries(logger: _AblyPluginSupportPrivate.Logger, logObjectType: String) { var keysToRemove: Set = [] for (proxiedObjectIdentifier, weakProxyRef) in proxiesByProxiedObjectIdentifier where weakProxyRef.referenced == nil { logger.log("Clearing unused \(logObjectType) proxy from cache (proxied: \(proxiedObjectIdentifier))", level: .debug) diff --git a/Sources/AblyLiveObjects/Utility/APLogger+Swift.swift b/Sources/AblyLiveObjects/Utility/APLogger+Swift.swift index 276621d..9236155 100644 --- a/Sources/AblyLiveObjects/Utility/APLogger+Swift.swift +++ b/Sources/AblyLiveObjects/Utility/APLogger+Swift.swift @@ -1,6 +1,6 @@ -internal import AblyPlugin +internal import _AblyPluginSupportPrivate -internal extension AblyPlugin.Logger { +internal extension _AblyPluginSupportPrivate.Logger { /// A convenience method that provides default values for `file` and `line`. func log(_ message: String, level: ARTLogLevel, fileID: String = #fileID, line: Int = #line) { log(message, with: level, file: fileID, line: line) diff --git a/Sources/AblyLiveObjects/Utility/WireValue.swift b/Sources/AblyLiveObjects/Utility/WireValue.swift index e2e6f5d..e5d2996 100644 --- a/Sources/AblyLiveObjects/Utility/WireValue.swift +++ b/Sources/AblyLiveObjects/Utility/WireValue.swift @@ -1,7 +1,7 @@ import Ably import Foundation -/// A wire value that can be represents the kinds of data that we expect to find inside a deserialized wire object received from AblyPlugin, or which we may put inside a serialized wire object that we send to AblyPlugin. +/// A wire value that can be represents the kinds of data that we expect to find inside a deserialized wire object received from `_AblyPluginSupportPrivate`, or which we may put inside a serialized wire object that we send to `_AblyPluginSupportPrivate`. /// /// Its cases are a superset of those of ``JSONValue``, adding a further `data` case for binary data (we expect to be able to send and receive binary data in the case where ably-cocoa is using the MessagePack format). internal indirect enum WireValue: Sendable, Equatable { @@ -118,27 +118,27 @@ extension WireValue: ExpressibleByBooleanLiteral { // MARK: - Bridging with ably-cocoa internal extension WireValue { - /// Creates a `WireValue` from an AblyPlugin deserialized wire object. + /// Creates a `WireValue` from an `_AblyPluginSupportPrivate` deserialized wire object. /// - /// Specifically, `ablyPluginData` can be a value that was passed to `LiveObjectsPlugin.decodeObjectMessage:…`. - init(ablyPluginData: Any) { + /// Specifically, `pluginSupportData` can be a value that was passed to `LiveObjectsPlugin.decodeObjectMessage:…`. + init(pluginSupportData: Any) { // swiftlint:disable:next trailing_closure - let extendedJSONValue = ExtendedJSONValue(deserialized: ablyPluginData, createExtraValue: { deserializedExtraValue in + let extendedJSONValue = ExtendedJSONValue(deserialized: pluginSupportData, createExtraValue: { deserializedExtraValue in // We support binary data (used for MessagePack format) in addition to JSON values if let data = deserializedExtraValue as? Data { return .data(data) } // ably-cocoa is not conforming to our assumptions; our assumptions are probably wrong. Either way, bring this loudly to our attention instead of trying to carry on - preconditionFailure("WireValue(ablyPluginData:) was given unsupported value \(deserializedExtraValue)") + preconditionFailure("WireValue(pluginSupportData:) was given unsupported value \(deserializedExtraValue)") }) self.init(extendedJSONValue: extendedJSONValue) } - /// Creates a `WireValue` from an AblyPlugin deserialized wire object. Specifically, `ablyPluginData` can be a value that was passed to `LiveObjectsPlugin.decodeObjectMessage:…`. - static func objectFromAblyPluginData(_ ablyPluginData: [String: Any]) -> [String: WireValue] { - let wireValue = WireValue(ablyPluginData: ablyPluginData) + /// Creates a `WireValue` from an `_AblyPluginSupportPrivate` deserialized wire object. Specifically, `pluginSupportData` can be a value that was passed to `LiveObjectsPlugin.decodeObjectMessage:…`. + static func objectFrom_AblyPluginSupportPrivateData(_ pluginSupportData: [String: Any]) -> [String: WireValue] { + let wireValue = WireValue(pluginSupportData: pluginSupportData) guard case let .object(wireObject) = wireValue else { preconditionFailure() } @@ -146,10 +146,10 @@ internal extension WireValue { return wireObject } - /// Creates an AblyPlugin deserialized wire object from a `WireValue`. + /// Creates an `_AblyPluginSupportPrivate` deserialized wire object from a `WireValue`. /// - /// Used by `[String: WireValue].toAblyPluginDataDictionary`. - var toAblyPluginData: Any { + /// Used by `[String: WireValue].toPluginSupportDataDictionary`. + var toPluginSupportData: Any { // swiftlint:disable:next trailing_closure toExtendedJSONValue.serialized(serializeExtraValue: { extendedValue in switch extendedValue { @@ -161,11 +161,11 @@ internal extension WireValue { } internal extension [String: WireValue] { - /// Creates an AblyPlugin deserialized wire object from a dictionary that has string keys and `WireValue` values. + /// Creates an `_AblyPluginSupportPrivate` deserialized wire object from a dictionary that has string keys and `WireValue` values. /// /// Specifically, the value of this property can be returned from `APLiveObjectsPlugin.encodeObjectMessage:`. - var toAblyPluginDataDictionary: [String: Any] { - mapValues(\.toAblyPluginData) + var toPluginSupportDataDictionary: [String: Any] { + mapValues(\.toPluginSupportData) } } diff --git a/Tests/AblyLiveObjectsTests/AblyLiveObjectsTests.swift b/Tests/AblyLiveObjectsTests/AblyLiveObjectsTests.swift index e71959d..feaa871 100644 --- a/Tests/AblyLiveObjectsTests/AblyLiveObjectsTests.swift +++ b/Tests/AblyLiveObjectsTests/AblyLiveObjectsTests.swift @@ -1,6 +1,6 @@ +import _AblyPluginSupportPrivate import Ably @testable import AblyLiveObjects -import AblyPlugin import Testing struct AblyLiveObjectsTests { diff --git a/Tests/AblyLiveObjectsTests/Helpers/TestFactories.swift b/Tests/AblyLiveObjectsTests/Helpers/TestFactories.swift index 441bf12..89b2ddb 100644 --- a/Tests/AblyLiveObjectsTests/Helpers/TestFactories.swift +++ b/Tests/AblyLiveObjectsTests/Helpers/TestFactories.swift @@ -1,5 +1,5 @@ +import _AblyPluginSupportPrivate @testable import AblyLiveObjects -import AblyPlugin import Foundation // Note that this file was created entirely by Cursor upon my giving it some guidelines — I have not checked its contents in any detail and it may well turn out that there are mistakes here which we need to fix in the future. diff --git a/Tests/AblyLiveObjectsTests/Helpers/TestLogger.swift b/Tests/AblyLiveObjectsTests/Helpers/TestLogger.swift index 59c51e1..3b6d6f5 100644 --- a/Tests/AblyLiveObjectsTests/Helpers/TestLogger.swift +++ b/Tests/AblyLiveObjectsTests/Helpers/TestLogger.swift @@ -1,8 +1,8 @@ -import AblyPlugin +import _AblyPluginSupportPrivate import os -/// An implementation of `AblyPlugin.Logger` to use when testing internal components of the LiveObjects plugin. -final class TestLogger: NSObject, AblyPlugin.Logger { +/// An implementation of `_AblyPluginSupportPrivate.Logger` to use when testing internal components of the LiveObjects plugin. +final class TestLogger: NSObject, _AblyPluginSupportPrivate.Logger { // By default, we don’t log in tests to keep the test logs easy to read. You can set this property to `true` to temporarily turn logging on if you want to debug a test. static let loggingEnabled = false diff --git a/Tests/AblyLiveObjectsTests/InternalDefaultLiveCounterTests.swift b/Tests/AblyLiveObjectsTests/InternalDefaultLiveCounterTests.swift index 039ec51..a474738 100644 --- a/Tests/AblyLiveObjectsTests/InternalDefaultLiveCounterTests.swift +++ b/Tests/AblyLiveObjectsTests/InternalDefaultLiveCounterTests.swift @@ -1,5 +1,5 @@ +import _AblyPluginSupportPrivate @testable import AblyLiveObjects -import AblyPlugin import Foundation import Testing diff --git a/Tests/AblyLiveObjectsTests/InternalDefaultLiveMapTests.swift b/Tests/AblyLiveObjectsTests/InternalDefaultLiveMapTests.swift index 9d982e9..0f33211 100644 --- a/Tests/AblyLiveObjectsTests/InternalDefaultLiveMapTests.swift +++ b/Tests/AblyLiveObjectsTests/InternalDefaultLiveMapTests.swift @@ -1,5 +1,5 @@ +import _AblyPluginSupportPrivate @testable import AblyLiveObjects -import AblyPlugin import Foundation import Testing diff --git a/Tests/AblyLiveObjectsTests/InternalDefaultRealtimeObjectsTests.swift b/Tests/AblyLiveObjectsTests/InternalDefaultRealtimeObjectsTests.swift index b4bedee..7d64d96 100644 --- a/Tests/AblyLiveObjectsTests/InternalDefaultRealtimeObjectsTests.swift +++ b/Tests/AblyLiveObjectsTests/InternalDefaultRealtimeObjectsTests.swift @@ -1,6 +1,6 @@ +import _AblyPluginSupportPrivate import Ably @testable import AblyLiveObjects -import AblyPlugin import Testing /// Tests for `InternalDefaultRealtimeObjects`. diff --git a/Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsHelper.swift b/Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsHelper.swift index 9f6671e..77b78e1 100644 --- a/Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsHelper.swift +++ b/Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsHelper.swift @@ -1,6 +1,6 @@ +import _AblyPluginSupportPrivate import Ably @testable import AblyLiveObjects -import AblyPlugin import Foundation // This file is copied from the file objects.test.js in ably-js. @@ -330,7 +330,7 @@ final class ObjectsHelper: Sendable { logger: channel.internal.logger, ) - let foundationObject = deserialized.toAblyPluginDataDictionary + let foundationObject = deserialized.toPluginSupportDataDictionary let protocolMessage = withExtendedLifetime(jsonLikeEncoderDelegate) { encoder.protocolMessage(from: foundationObject)! } diff --git a/Tests/AblyLiveObjectsTests/LiveObjectMutableStateTests.swift b/Tests/AblyLiveObjectsTests/LiveObjectMutableStateTests.swift index ff39d7d..06cae4b 100644 --- a/Tests/AblyLiveObjectsTests/LiveObjectMutableStateTests.swift +++ b/Tests/AblyLiveObjectsTests/LiveObjectMutableStateTests.swift @@ -1,6 +1,6 @@ +import _AblyPluginSupportPrivate import Ably @testable import AblyLiveObjects -import AblyPlugin import Testing /// Tests for `LiveObjectMutableState`. diff --git a/Tests/AblyLiveObjectsTests/ObjectMessageTests.swift b/Tests/AblyLiveObjectsTests/ObjectMessageTests.swift index a973dea..85fd9f4 100644 --- a/Tests/AblyLiveObjectsTests/ObjectMessageTests.swift +++ b/Tests/AblyLiveObjectsTests/ObjectMessageTests.swift @@ -1,5 +1,5 @@ +import _AblyPluginSupportPrivate @testable import AblyLiveObjects -import AblyPlugin import Foundation import Testing diff --git a/Tests/AblyLiveObjectsTests/ObjectsPoolTests.swift b/Tests/AblyLiveObjectsTests/ObjectsPoolTests.swift index 89514d5..8a35002 100644 --- a/Tests/AblyLiveObjectsTests/ObjectsPoolTests.swift +++ b/Tests/AblyLiveObjectsTests/ObjectsPoolTests.swift @@ -1,5 +1,5 @@ +import _AblyPluginSupportPrivate @testable import AblyLiveObjects -import AblyPlugin import Testing struct ObjectsPoolTests { diff --git a/Tests/AblyLiveObjectsTests/WireObjectMessageTests.swift b/Tests/AblyLiveObjectsTests/WireObjectMessageTests.swift index 16c28f1..d222814 100644 --- a/Tests/AblyLiveObjectsTests/WireObjectMessageTests.swift +++ b/Tests/AblyLiveObjectsTests/WireObjectMessageTests.swift @@ -1,11 +1,11 @@ +import _AblyPluginSupportPrivate @testable import AblyLiveObjects -import AblyPlugin import Foundation import Testing enum WireObjectMessageTests { // Helper: Fake decoding context - final class FakeDecodingContext: AblyPlugin.DecodingContextProtocol, @unchecked Sendable { + final class FakeDecodingContext: _AblyPluginSupportPrivate.DecodingContextProtocol, @unchecked Sendable { let parentID: String? let parentConnectionID: String? let parentTimestamp: Date? diff --git a/Tests/AblyLiveObjectsTests/WireValueTests.swift b/Tests/AblyLiveObjectsTests/WireValueTests.swift index 91e557e..74a31d3 100644 --- a/Tests/AblyLiveObjectsTests/WireValueTests.swift +++ b/Tests/AblyLiveObjectsTests/WireValueTests.swift @@ -4,35 +4,35 @@ import Foundation import Testing struct WireValueTests { - // MARK: Conversion from AblyPlugin data + // MARK: Conversion from _AblyPluginSupportPrivate data @Test(arguments: [ // object - (ablyPluginData: ["someKey": "someValue"], expectedResult: ["someKey": "someValue"]), + (pluginSupportData: ["someKey": "someValue"], expectedResult: ["someKey": "someValue"]), // array - (ablyPluginData: ["someElement"], expectedResult: ["someElement"]), + (pluginSupportData: ["someElement"], expectedResult: ["someElement"]), // string - (ablyPluginData: "someString", expectedResult: "someString"), + (pluginSupportData: "someString", expectedResult: "someString"), // number - (ablyPluginData: NSNumber(value: 0), expectedResult: 0), - (ablyPluginData: NSNumber(value: 1), expectedResult: 1), - (ablyPluginData: NSNumber(value: 123), expectedResult: 123), - (ablyPluginData: NSNumber(value: 123.456), expectedResult: 123.456), + (pluginSupportData: NSNumber(value: 0), expectedResult: 0), + (pluginSupportData: NSNumber(value: 1), expectedResult: 1), + (pluginSupportData: NSNumber(value: 123), expectedResult: 123), + (pluginSupportData: NSNumber(value: 123.456), expectedResult: 123.456), // bool - (ablyPluginData: NSNumber(value: true), expectedResult: true), - (ablyPluginData: NSNumber(value: false), expectedResult: false), + (pluginSupportData: NSNumber(value: true), expectedResult: true), + (pluginSupportData: NSNumber(value: false), expectedResult: false), // null - (ablyPluginData: NSNull(), expectedResult: .null), + (pluginSupportData: NSNull(), expectedResult: .null), // data - (ablyPluginData: Data([0x01, 0x02, 0x03]), expectedResult: .data(Data([0x01, 0x02, 0x03]))), - ] as[(ablyPluginData: Sendable, expectedResult: WireValue?)]) - func initWithAblyPluginData(ablyPluginData: Sendable, expectedResult: WireValue?) { - #expect(WireValue(ablyPluginData: ablyPluginData) == expectedResult) + (pluginSupportData: Data([0x01, 0x02, 0x03]), expectedResult: .data(Data([0x01, 0x02, 0x03]))), + ] as[(pluginSupportData: Sendable, expectedResult: WireValue?)]) + func initWithPluginSupportData(pluginSupportData: Sendable, expectedResult: WireValue?) { + #expect(WireValue(pluginSupportData: pluginSupportData) == expectedResult) } // Tests that it correctly handles an object deserialized by `JSONSerialization` (which is what ably-cocoa uses for JSON deserialization). @Test - func initWithAblyPluginData_endToEnd_json() throws { + func initWithPluginSupportData_endToEnd_json() throws { let jsonString = """ { "someArray": [ @@ -54,7 +54,7 @@ struct WireValueTests { } """ - let ablyPluginData = try JSONSerialization.jsonObject(with: #require(jsonString.data(using: .utf8))) + let pluginSupportData = try JSONSerialization.jsonObject(with: #require(jsonString.data(using: .utf8))) let expected: WireValue = [ "someArray": [ @@ -75,12 +75,12 @@ struct WireValueTests { ], ] - #expect(WireValue(ablyPluginData: ablyPluginData) == expected) + #expect(WireValue(pluginSupportData: pluginSupportData) == expected) } // Tests that it correctly handles an object deserialized by `ARTMsgPackEncoder` (which is what ably-cocoa uses for MessagePack deserialization). @Test - func initWithAblyPluginData_endToEnd_msgpack() throws { + func initWithPluginSupportData_endToEnd_msgpack() throws { // MessagePack representation of the same data structure as in the JSON test above, plus binary data // This represents: // { @@ -167,7 +167,7 @@ struct WireValueTests { 0xAE, 0x73, 0x6F, 0x6D, 0x65, 0x4F, 0x74, 0x68, 0x65, 0x72, 0x56, 0x61, 0x6C, 0x75, 0x65, // value (14 chars) ]) - let ablyPluginData = try ARTMsgPackEncoder().decode(msgpackData) + let pluginSupportData = try ARTMsgPackEncoder().decode(msgpackData) let expected: WireValue = [ "someArray": [ @@ -189,10 +189,10 @@ struct WireValueTests { ], ] - #expect(WireValue(ablyPluginData: ablyPluginData) == expected) + #expect(WireValue(pluginSupportData: pluginSupportData) == expected) } - // MARK: Conversion to AblyPlugin data + // MARK: Conversion to _AblyPluginSupportPrivate data @Test(arguments: [ // object @@ -214,15 +214,15 @@ struct WireValueTests { // data (value: .data(Data([0x01, 0x02, 0x03])), expectedResult: Data([0x01, 0x02, 0x03])), ] as[(value: WireValue, expectedResult: Sendable)]) - func toAblyPluginData(value: WireValue, expectedResult: Sendable) throws { - let resultAsNSObject = try #require(value.toAblyPluginData as? NSObject) + func toPluginSupportData(value: WireValue, expectedResult: Sendable) throws { + let resultAsNSObject = try #require(value.toPluginSupportData as? NSObject) let expectedResultAsNSObject = try #require(expectedResult as? NSObject) #expect(resultAsNSObject == expectedResultAsNSObject) } // Tests that it creates an object that can be serialized by `JSONSerialization` (which is what ably-cocoa uses for JSON serialization), and that the result of this serialization is what we’d expect. @Test - func toAblyPluginData_endToEnd_json() throws { + func toPluginSupportData_endToEnd_json() throws { let value: WireValue = [ "someArray": [ [ @@ -265,7 +265,7 @@ struct WireValueTests { let jsonSerializationOptions: JSONSerialization.WritingOptions = [.sortedKeys] - let valueData = try JSONSerialization.data(withJSONObject: value.toAblyPluginData, options: jsonSerializationOptions) + let valueData = try JSONSerialization.data(withJSONObject: value.toPluginSupportData, options: jsonSerializationOptions) let expectedData = try { let serialized = try JSONSerialization.jsonObject(with: #require(expectedJSONString.data(using: .utf8))) return try JSONSerialization.data(withJSONObject: serialized, options: jsonSerializationOptions) @@ -276,7 +276,7 @@ struct WireValueTests { // Tests that it creates an object that can be serialized by `ARTMsgPackEncoder` (which is what ably-cocoa uses for MessagePack serialization), and that the result of this serialization is what we’d expect. @Test - func toAblyPluginData_endToEnd_msgpack() throws { + func toPluginSupportData_endToEnd_msgpack() throws { let value: WireValue = [ "someArray": [ [ @@ -365,7 +365,7 @@ struct WireValueTests { 0xAE, 0x73, 0x6F, 0x6D, 0x65, 0x4F, 0x74, 0x68, 0x65, 0x72, 0x56, 0x61, 0x6C, 0x75, 0x65, // value (14 chars) ]) - let actualMsgPackData = try ARTMsgPackEncoder().encode(value.toAblyPluginData) + let actualMsgPackData = try ARTMsgPackEncoder().encode(value.toPluginSupportData) // Verify that both decode to the same Foundation object structure let expectedDecoded = try ARTMsgPackEncoder().decode(expectedMsgPackData) diff --git a/ably-cocoa b/ably-cocoa index 5096ca3..9f1a651 160000 --- a/ably-cocoa +++ b/ably-cocoa @@ -1 +1 @@ -Subproject commit 5096ca37c6c39f8f33e261f49faf2a6f9d03e529 +Subproject commit 9f1a65179b69b6041b3a65664c5fa6c48d722fec From b4e1899a86b063bf670f4b83407ba20f27458eb9 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Tue, 19 Aug 2025 09:00:59 -0300 Subject: [PATCH 03/11] Get ably-cocoa plugin APIs from an external repo As required by the accompanying ably-cocoa submodule bump (see the ably-cocoa commit message for explanation of why we're making this change). --- .../xcshareddata/swiftpm/Package.resolved | 11 +++- Package.resolved | 11 +++- Package.swift | 8 ++- .../Internal/ARTClientOptions+Objects.swift | 5 +- .../AblyLiveObjects/Internal/CoreSDK.swift | 12 ++--- .../Internal/DefaultInternalPlugin.swift | 12 +++-- .../Internal/InternalDefaultLiveCounter.swift | 8 +-- .../Internal/InternalDefaultLiveMap.swift | 16 +++--- .../InternalDefaultRealtimeObjects.swift | 4 +- .../Internal/LiveObjectMutableState.swift | 1 + .../Internal/ObjectsPool.swift | 12 ++--- .../Public/ARTRealtimeChannel+Objects.swift | 5 +- Sources/AblyLiveObjects/Public/Plugin.swift | 2 +- .../InternalLiveMapValue+ToPublic.swift | 2 +- .../PublicDefaultLiveCounter.swift | 4 +- .../PublicDefaultLiveMap.swift | 4 +- .../PublicDefaultRealtimeObjects.swift | 4 +- .../PublicObjectsStore.swift | 10 ++-- .../Utility/APLogger+Swift.swift | 8 --- Sources/AblyLiveObjects/Utility/Errors.swift | 3 +- .../Utility/InternalError.swift | 7 +++ Sources/AblyLiveObjects/Utility/Logger.swift | 41 ++++++++++++++ .../Utility/MarkerProtocolHelpers.swift | 54 +++++++++++++++++++ .../AblyLiveObjects/Utility/WireValue.swift | 2 +- .../Helpers/TestLogger.swift | 11 ++-- .../InternalDefaultLiveCounterTests.swift | 9 ++-- .../InternalDefaultLiveMapTests.swift | 17 +++--- .../InternalDefaultRealtimeObjectsTests.swift | 12 ++--- .../LiveObjectMutableStateTests.swift | 4 +- .../Mocks/MockCoreSDK.swift | 7 +-- ably-cocoa | 2 +- 31 files changed, 217 insertions(+), 91 deletions(-) delete mode 100644 Sources/AblyLiveObjects/Utility/APLogger+Swift.swift create mode 100644 Sources/AblyLiveObjects/Utility/Logger.swift create mode 100644 Sources/AblyLiveObjects/Utility/MarkerProtocolHelpers.swift diff --git a/AblyLiveObjects.xcworkspace/xcshareddata/swiftpm/Package.resolved b/AblyLiveObjects.xcworkspace/xcshareddata/swiftpm/Package.resolved index bfe1d58..cd28d5b 100644 --- a/AblyLiveObjects.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/AblyLiveObjects.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,6 +1,15 @@ { - "originHash" : "6884be3a1fb838d3f9a3288b0cb994d0dfce4e90c7d648bca08e61fb802c9cda", + "originHash" : "7ffc8893e3a0652bc31d38d048052def20308c84ae9888411b48b5c89c2ec6c6", "pins" : [ + { + "identity" : "ably-cocoa-plugin-support", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ably/ably-cocoa-plugin-support", + "state" : { + "revision" : "cec94ed123d60e39e3f8df665c30a57482d37612", + "version" : "0.1.0" + } + }, { "identity" : "delta-codec-cocoa", "kind" : "remoteSourceControl", diff --git a/Package.resolved b/Package.resolved index 773dea5..44dc373 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,6 +1,15 @@ { - "originHash" : "9d42be7ef9d81adeb6ac28ccc2a7a4dd43dbf0d952b6f8331e73ab665d36df3a", + "originHash" : "cf81894c95bf31f3c45009841b1a7ee58b50fdcf93ceeecdade367eef5e57c58", "pins" : [ + { + "identity" : "ably-cocoa-plugin-support", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ably/ably-cocoa-plugin-support", + "state" : { + "revision" : "cec94ed123d60e39e3f8df665c30a57482d37612", + "version" : "0.1.0" + } + }, { "identity" : "delta-codec-cocoa", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index 742aa4b..ce7b62f 100644 --- a/Package.swift +++ b/Package.swift @@ -21,6 +21,10 @@ let package = Package( .package( path: "ably-cocoa", ), + .package( + url: "https://github.com/ably/ably-cocoa-plugin-support", + from: "0.1.0", + ), .package( url: "https://github.com/apple/swift-argument-parser", from: "1.5.0", @@ -48,7 +52,7 @@ let package = Package( ), .product( name: "_AblyPluginSupportPrivate", - package: "ably-cocoa", + package: "ably-cocoa-plugin-support", ), ], ), @@ -62,7 +66,7 @@ let package = Package( ), .product( name: "_AblyPluginSupportPrivate", - package: "ably-cocoa", + package: "ably-cocoa-plugin-support", ), ], resources: [ diff --git a/Sources/AblyLiveObjects/Internal/ARTClientOptions+Objects.swift b/Sources/AblyLiveObjects/Internal/ARTClientOptions+Objects.swift index 8797381..6855434 100644 --- a/Sources/AblyLiveObjects/Internal/ARTClientOptions+Objects.swift +++ b/Sources/AblyLiveObjects/Internal/ARTClientOptions+Objects.swift @@ -1,4 +1,5 @@ internal import _AblyPluginSupportPrivate +import Ably internal extension ARTClientOptions { private class Box { @@ -16,7 +17,7 @@ internal extension ARTClientOptions { get { let optionsValue = Plugin.defaultPluginAPI.pluginOptionsValue( forKey: Self.garbageCollectionOptionsKey, - clientOptions: self, + clientOptions: asPluginPublicClientOptions, ) guard let optionsValue else { @@ -38,7 +39,7 @@ internal extension ARTClientOptions { Plugin.defaultPluginAPI.setPluginOptionsValue( Box(boxed: newValue), forKey: Self.garbageCollectionOptionsKey, - clientOptions: self, + clientOptions: asPluginPublicClientOptions, ) } } diff --git a/Sources/AblyLiveObjects/Internal/CoreSDK.swift b/Sources/AblyLiveObjects/Internal/CoreSDK.swift index 0c93078..1e99994 100644 --- a/Sources/AblyLiveObjects/Internal/CoreSDK.swift +++ b/Sources/AblyLiveObjects/Internal/CoreSDK.swift @@ -14,7 +14,7 @@ internal protocol CoreSDK: AnyObject, Sendable { func testsOnly_overridePublish(with newImplementation: @escaping ([OutboundObjectMessage]) async throws(InternalError) -> Void) /// Returns the current state of the Realtime channel that this wraps. - var channelState: ARTRealtimeChannelState { get } + var channelState: _AblyPluginSupportPrivate.RealtimeChannelState { get } } internal final class DefaultCoreSDK: CoreSDK { @@ -24,7 +24,7 @@ internal final class DefaultCoreSDK: CoreSDK { private let channel: _AblyPluginSupportPrivate.RealtimeChannel private let client: _AblyPluginSupportPrivate.RealtimeClient private let pluginAPI: PluginAPIProtocol - private let logger: _AblyPluginSupportPrivate.Logger + private let logger: Logger /// If set to true, ``publish(objectMessages:)`` will behave like a no-op. /// @@ -37,7 +37,7 @@ internal final class DefaultCoreSDK: CoreSDK { channel: _AblyPluginSupportPrivate.RealtimeChannel, client: _AblyPluginSupportPrivate.RealtimeClient, pluginAPI: PluginAPIProtocol, - logger: _AblyPluginSupportPrivate.Logger + logger: Logger ) { self.channel = channel self.client = client @@ -81,8 +81,8 @@ internal final class DefaultCoreSDK: CoreSDK { } } - internal var channelState: ARTRealtimeChannelState { - channel.state + internal var channelState: _AblyPluginSupportPrivate.RealtimeChannelState { + pluginAPI.state(for: channel) } } @@ -97,7 +97,7 @@ internal extension CoreSDK { /// - operationDescription: A description of the operation being performed, used in error messages /// - Throws: `ARTErrorInfo` with code 90001 and statusCode 400 if the channel is in any of the invalid states func validateChannelState( - notIn invalidStates: [ARTRealtimeChannelState], + notIn invalidStates: [_AblyPluginSupportPrivate.RealtimeChannelState], operationDescription: String, ) throws(ARTErrorInfo) { let currentChannelState = channelState diff --git a/Sources/AblyLiveObjects/Internal/DefaultInternalPlugin.swift b/Sources/AblyLiveObjects/Internal/DefaultInternalPlugin.swift index 970157e..ac081fc 100644 --- a/Sources/AblyLiveObjects/Internal/DefaultInternalPlugin.swift +++ b/Sources/AblyLiveObjects/Internal/DefaultInternalPlugin.swift @@ -1,4 +1,5 @@ internal import _AblyPluginSupportPrivate +import Ably // We explicitly import the NSObject class, else it seems to get transitively imported from `internal import _AblyPluginSupportPrivate`, leading to the error "Class cannot be declared public because its superclass is internal". import ObjectiveC.NSObject @@ -34,10 +35,11 @@ internal final class DefaultInternalPlugin: NSObject, _AblyPluginSupportPrivate. // Populates the channel's `objects` property. internal func prepare(_ channel: _AblyPluginSupportPrivate.RealtimeChannel, client: _AblyPluginSupportPrivate.RealtimeClient) { - let logger = pluginAPI.logger(for: channel) + let pluginLogger = pluginAPI.logger(for: channel) let callbackQueue = pluginAPI.callbackQueue(for: client) - let options = pluginAPI.options(for: client) + let options = ARTClientOptions.castPluginPublicClientOptions(pluginAPI.options(for: client)) + let logger = DefaultLogger(pluginLogger: pluginLogger, pluginAPI: pluginAPI) logger.log("LiveObjects.DefaultInternalPlugin received prepare(_:)", level: .debug) let liveObjects = InternalDefaultRealtimeObjects( logger: logger, @@ -68,9 +70,9 @@ internal final class DefaultInternalPlugin: NSObject, _AblyPluginSupportPrivate. _ serialized: [String: Any], context: DecodingContextProtocol, format: EncodingFormat, - error errorPtr: AutoreleasingUnsafeMutablePointer?, + error errorPtr: AutoreleasingUnsafeMutablePointer<_AblyPluginSupportPrivate.PublicErrorInfo?>?, ) -> (any ObjectMessageProtocol)? { - let wireObject = WireValue.objectFrom_AblyPluginSupportPrivateData(serialized) + let wireObject = WireValue.objectFromPluginSupportData(serialized) do { let wireObjectMessage = try InboundWireObjectMessage( @@ -83,7 +85,7 @@ internal final class DefaultInternalPlugin: NSObject, _AblyPluginSupportPrivate. ) return ObjectMessageBox(objectMessage: objectMessage) } catch { - errorPtr?.pointee = error.toARTErrorInfo() + errorPtr?.pointee = error.toARTErrorInfo().asPluginPublicErrorInfo return nil } } diff --git a/Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift b/Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift index 4169912..9c314bc 100644 --- a/Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift +++ b/Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift @@ -21,7 +21,7 @@ internal final class InternalDefaultLiveCounter: Sendable { } } - private let logger: _AblyPluginSupportPrivate.Logger + private let logger: Logger private let userCallbackQueue: DispatchQueue private let clock: SimpleClock @@ -30,7 +30,7 @@ internal final class InternalDefaultLiveCounter: Sendable { internal convenience init( testsOnly_data data: Double, objectID: String, - logger: _AblyPluginSupportPrivate.Logger, + logger: Logger, userCallbackQueue: DispatchQueue, clock: SimpleClock ) { @@ -40,7 +40,7 @@ internal final class InternalDefaultLiveCounter: Sendable { private init( data: Double, objectID: String, - logger: _AblyPluginSupportPrivate.Logger, + logger: Logger, userCallbackQueue: DispatchQueue, clock: SimpleClock ) { @@ -56,7 +56,7 @@ internal final class InternalDefaultLiveCounter: Sendable { /// - objectID: The value for the "private objectId field" of RTO5c1b1a. internal static func createZeroValued( objectID: String, - logger: _AblyPluginSupportPrivate.Logger, + logger: Logger, userCallbackQueue: DispatchQueue, clock: SimpleClock, ) -> Self { diff --git a/Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift b/Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift index e952cdb..84a94c2 100644 --- a/Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift +++ b/Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift @@ -38,7 +38,7 @@ internal final class InternalDefaultLiveMap: Sendable { } } - private let logger: _AblyPluginSupportPrivate.Logger + private let logger: Logger private let userCallbackQueue: DispatchQueue private let clock: SimpleClock @@ -48,7 +48,7 @@ internal final class InternalDefaultLiveMap: Sendable { testsOnly_data data: [String: InternalObjectsMapEntry], objectID: String, testsOnly_semantics semantics: WireEnum? = nil, - logger: _AblyPluginSupportPrivate.Logger, + logger: Logger, userCallbackQueue: DispatchQueue, clock: SimpleClock, ) { @@ -66,7 +66,7 @@ internal final class InternalDefaultLiveMap: Sendable { data: [String: InternalObjectsMapEntry], objectID: String, semantics: WireEnum?, - logger: _AblyPluginSupportPrivate.Logger, + logger: Logger, userCallbackQueue: DispatchQueue, clock: SimpleClock, ) { @@ -84,7 +84,7 @@ internal final class InternalDefaultLiveMap: Sendable { internal static func createZeroValued( objectID: String, semantics: WireEnum? = nil, - logger: _AblyPluginSupportPrivate.Logger, + logger: Logger, userCallbackQueue: DispatchQueue, clock: SimpleClock, ) -> Self { @@ -398,7 +398,7 @@ internal final class InternalDefaultLiveMap: Sendable { using state: ObjectState, objectMessageSerialTimestamp: Date?, objectsPool: inout ObjectsPool, - logger: _AblyPluginSupportPrivate.Logger, + logger: Logger, clock: SimpleClock, userCallbackQueue: DispatchQueue, ) -> LiveObjectUpdate { @@ -467,7 +467,7 @@ internal final class InternalDefaultLiveMap: Sendable { internal mutating func mergeInitialValue( from operation: ObjectOperation, objectsPool: inout ObjectsPool, - logger: _AblyPluginSupportPrivate.Logger, + logger: Logger, userCallbackQueue: DispatchQueue, clock: SimpleClock, ) -> LiveObjectUpdate { @@ -621,7 +621,7 @@ internal final class InternalDefaultLiveMap: Sendable { operationTimeserial: String?, operationData: ObjectData?, objectsPool: inout ObjectsPool, - logger: _AblyPluginSupportPrivate.Logger, + logger: Logger, userCallbackQueue: DispatchQueue, clock: SimpleClock, ) -> LiveObjectUpdate { @@ -744,7 +744,7 @@ internal final class InternalDefaultLiveMap: Sendable { internal mutating func applyMapCreateOperation( _ operation: ObjectOperation, objectsPool: inout ObjectsPool, - logger: _AblyPluginSupportPrivate.Logger, + logger: Logger, userCallbackQueue: DispatchQueue, clock: SimpleClock, ) -> LiveObjectUpdate { diff --git a/Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift b/Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift index 89770aa..c989756 100644 --- a/Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift +++ b/Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift @@ -8,7 +8,7 @@ internal final class InternalDefaultRealtimeObjects: Sendable, LiveMapObjectPool private nonisolated(unsafe) var mutableState: MutableState! - private let logger: _AblyPluginSupportPrivate.Logger + private let logger: Logger private let userCallbackQueue: DispatchQueue private let clock: SimpleClock @@ -91,7 +91,7 @@ internal final class InternalDefaultRealtimeObjects: Sendable, LiveMapObjectPool } } - internal init(logger: _AblyPluginSupportPrivate.Logger, userCallbackQueue: DispatchQueue, clock: SimpleClock, garbageCollectionOptions: GarbageCollectionOptions = .init()) { + internal init(logger: Logger, userCallbackQueue: DispatchQueue, clock: SimpleClock, garbageCollectionOptions: GarbageCollectionOptions = .init()) { self.logger = logger self.userCallbackQueue = userCallbackQueue self.clock = clock diff --git a/Sources/AblyLiveObjects/Internal/LiveObjectMutableState.swift b/Sources/AblyLiveObjects/Internal/LiveObjectMutableState.swift index b471677..191d312 100644 --- a/Sources/AblyLiveObjects/Internal/LiveObjectMutableState.swift +++ b/Sources/AblyLiveObjects/Internal/LiveObjectMutableState.swift @@ -1,4 +1,5 @@ internal import _AblyPluginSupportPrivate +import Ably /// This is the equivalent of the `LiveObject` abstract class described in RTLO. /// diff --git a/Sources/AblyLiveObjects/Internal/ObjectsPool.swift b/Sources/AblyLiveObjects/Internal/ObjectsPool.swift index a16cd24..3cef1de 100644 --- a/Sources/AblyLiveObjects/Internal/ObjectsPool.swift +++ b/Sources/AblyLiveObjects/Internal/ObjectsPool.swift @@ -137,7 +137,7 @@ internal struct ObjectsPool { /// Creates an `ObjectsPool` whose root is a zero-value `LiveMap`. internal init( - logger: _AblyPluginSupportPrivate.Logger, + logger: Logger, userCallbackQueue: DispatchQueue, clock: SimpleClock, testsOnly_otherEntries otherEntries: [String: Entry]? = nil, @@ -151,7 +151,7 @@ internal struct ObjectsPool { } private init( - logger: _AblyPluginSupportPrivate.Logger, + logger: Logger, userCallbackQueue: DispatchQueue, clock: SimpleClock, otherEntries: [String: Entry]? @@ -187,7 +187,7 @@ internal struct ObjectsPool { /// - userCallbackQueue: The callback queue to use for any created LiveObject /// - clock: The clock to use for any created LiveObject /// - Returns: The existing or newly created object - internal mutating func createZeroValueObject(forObjectID objectID: String, logger: _AblyPluginSupportPrivate.Logger, userCallbackQueue: DispatchQueue, clock: SimpleClock) -> Entry? { + internal mutating func createZeroValueObject(forObjectID objectID: String, logger: Logger, userCallbackQueue: DispatchQueue, clock: SimpleClock) -> Entry? { // RTO6a: If an object with objectId exists in ObjectsPool, do not create a new object if let existingEntry = entries[objectID] { return existingEntry @@ -220,7 +220,7 @@ internal struct ObjectsPool { /// Applies the objects gathered during an `OBJECT_SYNC` to this `ObjectsPool`, per RTO5c1 and RTO5c2. internal mutating func applySyncObjectsPool( _ syncObjectsPool: [SyncObjectsPoolEntry], - logger: _AblyPluginSupportPrivate.Logger, + logger: Logger, userCallbackQueue: DispatchQueue, clock: SimpleClock, ) { @@ -316,7 +316,7 @@ internal struct ObjectsPool { /// - Returns: The existing or newly created counter object internal mutating func getOrCreateCounter( creationOperation: ObjectCreationHelpers.CounterCreationOperation, - logger: _AblyPluginSupportPrivate.Logger, + logger: Logger, userCallbackQueue: DispatchQueue, clock: SimpleClock, ) -> InternalDefaultLiveCounter { @@ -360,7 +360,7 @@ internal struct ObjectsPool { /// - Returns: The existing or newly created map object internal mutating func getOrCreateMap( creationOperation: ObjectCreationHelpers.MapCreationOperation, - logger: _AblyPluginSupportPrivate.Logger, + logger: Logger, userCallbackQueue: DispatchQueue, clock: SimpleClock, ) -> InternalDefaultLiveMap { diff --git a/Sources/AblyLiveObjects/Public/ARTRealtimeChannel+Objects.swift b/Sources/AblyLiveObjects/Public/ARTRealtimeChannel+Objects.swift index 67b8612..cec5ca9 100644 --- a/Sources/AblyLiveObjects/Public/ARTRealtimeChannel+Objects.swift +++ b/Sources/AblyLiveObjects/Public/ARTRealtimeChannel+Objects.swift @@ -9,10 +9,11 @@ public extension ARTRealtimeChannel { private var nonTypeErasedObjects: PublicDefaultRealtimeObjects { let pluginAPI = Plugin.defaultPluginAPI - let underlyingObjects = pluginAPI.underlyingObjects(forPublicRealtimeChannel: self) + let underlyingObjects = pluginAPI.underlyingObjects(for: asPluginPublicRealtimeChannel) let internalObjects = DefaultInternalPlugin.realtimeObjects(for: underlyingObjects.channel, pluginAPI: pluginAPI) - let logger = pluginAPI.logger(for: underlyingObjects.channel) + let pluginLogger = pluginAPI.logger(for: underlyingObjects.channel) + let logger = DefaultLogger(pluginLogger: pluginLogger, pluginAPI: pluginAPI) let coreSDK = DefaultCoreSDK( channel: underlyingObjects.channel, diff --git a/Sources/AblyLiveObjects/Public/Plugin.swift b/Sources/AblyLiveObjects/Public/Plugin.swift index 870a932..799c354 100644 --- a/Sources/AblyLiveObjects/Public/Plugin.swift +++ b/Sources/AblyLiveObjects/Public/Plugin.swift @@ -23,7 +23,7 @@ import ObjectiveC.NSObject @objc public class Plugin: NSObject { /// The `_AblyPluginSupportPrivate.PluginAPIProtocol` that the LiveObjects plugin should use by default (i.e. when one hasn't been injected for test purposes). - internal static let defaultPluginAPI: _AblyPluginSupportPrivate.PluginAPIProtocol = _AblyPluginSupportPrivate.PluginAPI.sharedInstance() + internal static let defaultPluginAPI = _AblyPluginSupportPrivate.DependencyStore.sharedInstance().fetchPluginAPI() // MARK: - Informal conformance to _AblyPluginSupportPrivate.LiveObjectsPluginProtocol diff --git a/Sources/AblyLiveObjects/Public/Public Proxy Objects/InternalLiveMapValue+ToPublic.swift b/Sources/AblyLiveObjects/Public/Public Proxy Objects/InternalLiveMapValue+ToPublic.swift index 91fe81e..fa7e7bb 100644 --- a/Sources/AblyLiveObjects/Public/Public Proxy Objects/InternalLiveMapValue+ToPublic.swift +++ b/Sources/AblyLiveObjects/Public/Public Proxy Objects/InternalLiveMapValue+ToPublic.swift @@ -6,7 +6,7 @@ internal extension InternalLiveMapValue { struct PublicValueCreationArgs { internal var coreSDK: CoreSDK internal var mapDelegate: LiveMapObjectPoolDelegate - internal var logger: _AblyPluginSupportPrivate.Logger + internal var logger: Logger internal var toCounterCreationArgs: PublicObjectsStore.CounterCreationArgs { .init(coreSDK: coreSDK, logger: logger) diff --git a/Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicDefaultLiveCounter.swift b/Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicDefaultLiveCounter.swift index 6388aa5..90a408d 100644 --- a/Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicDefaultLiveCounter.swift +++ b/Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicDefaultLiveCounter.swift @@ -10,9 +10,9 @@ internal final class PublicDefaultLiveCounter: LiveCounter { // MARK: - Dependencies that hold a strong reference to `proxied` private let coreSDK: CoreSDK - private let logger: _AblyPluginSupportPrivate.Logger + private let logger: Logger - internal init(proxied: InternalDefaultLiveCounter, coreSDK: CoreSDK, logger: _AblyPluginSupportPrivate.Logger) { + internal init(proxied: InternalDefaultLiveCounter, coreSDK: CoreSDK, logger: Logger) { self.proxied = proxied self.coreSDK = coreSDK self.logger = logger diff --git a/Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicDefaultLiveMap.swift b/Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicDefaultLiveMap.swift index a6674a4..5aad18f 100644 --- a/Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicDefaultLiveMap.swift +++ b/Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicDefaultLiveMap.swift @@ -11,9 +11,9 @@ internal final class PublicDefaultLiveMap: LiveMap { private let coreSDK: CoreSDK private let delegate: LiveMapObjectPoolDelegate - private let logger: _AblyPluginSupportPrivate.Logger + private let logger: Logger - internal init(proxied: InternalDefaultLiveMap, coreSDK: CoreSDK, delegate: LiveMapObjectPoolDelegate, logger: _AblyPluginSupportPrivate.Logger) { + internal init(proxied: InternalDefaultLiveMap, coreSDK: CoreSDK, delegate: LiveMapObjectPoolDelegate, logger: Logger) { self.proxied = proxied self.coreSDK = coreSDK self.delegate = delegate diff --git a/Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicDefaultRealtimeObjects.swift b/Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicDefaultRealtimeObjects.swift index 19a723c..34221ad 100644 --- a/Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicDefaultRealtimeObjects.swift +++ b/Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicDefaultRealtimeObjects.swift @@ -13,9 +13,9 @@ internal final class PublicDefaultRealtimeObjects: RealtimeObjects { // MARK: - Dependencies that hold a strong reference to `proxied` private let coreSDK: CoreSDK - private let logger: _AblyPluginSupportPrivate.Logger + private let logger: Logger - internal init(proxied: InternalDefaultRealtimeObjects, coreSDK: CoreSDK, logger: _AblyPluginSupportPrivate.Logger) { + internal init(proxied: InternalDefaultRealtimeObjects, coreSDK: CoreSDK, logger: Logger) { self.proxied = proxied self.coreSDK = coreSDK self.logger = logger diff --git a/Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicObjectsStore.swift b/Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicObjectsStore.swift index a6e7033..264a92c 100644 --- a/Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicObjectsStore.swift +++ b/Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicObjectsStore.swift @@ -21,7 +21,7 @@ internal final class PublicObjectsStore: Sendable { internal struct RealtimeObjectsCreationArgs { internal var coreSDK: CoreSDK - internal var logger: _AblyPluginSupportPrivate.Logger + internal var logger: Logger } /// Fetches the cached `PublicDefaultRealtimeObjects` that wraps a given `InternalDefaultRealtimeObjects`, creating a new public object if there isn't already one. @@ -33,7 +33,7 @@ internal final class PublicObjectsStore: Sendable { internal struct CounterCreationArgs { internal var coreSDK: CoreSDK - internal var logger: _AblyPluginSupportPrivate.Logger + internal var logger: Logger } /// Fetches the cached `PublicDefaultLiveCounter` that wraps a given `InternalDefaultLiveCounter`, creating a new public object if there isn't already one. @@ -46,7 +46,7 @@ internal final class PublicObjectsStore: Sendable { internal struct MapCreationArgs { internal var coreSDK: CoreSDK internal var delegate: LiveMapObjectPoolDelegate - internal var logger: _AblyPluginSupportPrivate.Logger + internal var logger: Logger } /// Fetches the cached `PublicDefaultLiveMap` that wraps a given `InternalDefaultLiveMap`, creating a new public object if there isn't already one. @@ -68,7 +68,7 @@ internal final class PublicObjectsStore: Sendable { /// Fetches the proxy that wraps `proxied`, creating a new proxy if there isn't already one. Stores a weak reference to the proxy. mutating func getOrCreate( proxying proxied: some AnyObject, - logger: _AblyPluginSupportPrivate.Logger, + logger: Logger, logObjectType: String, createProxy: () -> Proxy, ) -> Proxy { @@ -90,7 +90,7 @@ internal final class PublicObjectsStore: Sendable { return created } - private mutating func removeDeallocatedEntries(logger: _AblyPluginSupportPrivate.Logger, logObjectType: String) { + private mutating func removeDeallocatedEntries(logger: Logger, logObjectType: String) { var keysToRemove: Set = [] for (proxiedObjectIdentifier, weakProxyRef) in proxiesByProxiedObjectIdentifier where weakProxyRef.referenced == nil { logger.log("Clearing unused \(logObjectType) proxy from cache (proxied: \(proxiedObjectIdentifier))", level: .debug) diff --git a/Sources/AblyLiveObjects/Utility/APLogger+Swift.swift b/Sources/AblyLiveObjects/Utility/APLogger+Swift.swift deleted file mode 100644 index 9236155..0000000 --- a/Sources/AblyLiveObjects/Utility/APLogger+Swift.swift +++ /dev/null @@ -1,8 +0,0 @@ -internal import _AblyPluginSupportPrivate - -internal extension _AblyPluginSupportPrivate.Logger { - /// A convenience method that provides default values for `file` and `line`. - func log(_ message: String, level: ARTLogLevel, fileID: String = #fileID, line: Int = #line) { - log(message, with: level, file: fileID, line: line) - } -} diff --git a/Sources/AblyLiveObjects/Utility/Errors.swift b/Sources/AblyLiveObjects/Utility/Errors.swift index 941d3e8..1ab2e66 100644 --- a/Sources/AblyLiveObjects/Utility/Errors.swift +++ b/Sources/AblyLiveObjects/Utility/Errors.swift @@ -1,3 +1,4 @@ +internal import _AblyPluginSupportPrivate import Ably /** @@ -5,7 +6,7 @@ import Ably */ internal enum LiveObjectsError { // operationDescription should be a description of a method like "LiveCounter.value"; it will be interpolated into an error message - case objectsOperationFailedInvalidChannelState(operationDescription: String, channelState: ARTRealtimeChannelState) + case objectsOperationFailedInvalidChannelState(operationDescription: String, channelState: _AblyPluginSupportPrivate.RealtimeChannelState) case counterInitialValueInvalid(value: Double) case counterIncrementAmountInvalid(amount: Double) diff --git a/Sources/AblyLiveObjects/Utility/InternalError.swift b/Sources/AblyLiveObjects/Utility/InternalError.swift index e9fbf70..4492cf6 100644 --- a/Sources/AblyLiveObjects/Utility/InternalError.swift +++ b/Sources/AblyLiveObjects/Utility/InternalError.swift @@ -1,3 +1,4 @@ +internal import _AblyPluginSupportPrivate import Ably /// An error thrown by the internals of the LiveObjects SDK. @@ -35,3 +36,9 @@ internal extension ARTErrorInfo { .errorInfo(self) } } + +internal extension _AblyPluginSupportPrivate.PublicErrorInfo { + func toInternalError() -> InternalError { + ARTErrorInfo.castPluginPublicErrorInfo(self).toInternalError() + } +} diff --git a/Sources/AblyLiveObjects/Utility/Logger.swift b/Sources/AblyLiveObjects/Utility/Logger.swift new file mode 100644 index 0000000..94cfca9 --- /dev/null +++ b/Sources/AblyLiveObjects/Utility/Logger.swift @@ -0,0 +1,41 @@ +internal import _AblyPluginSupportPrivate + +/// A reference to a line within a source code file. +internal struct CodeLocation: Equatable { + /// A file identifier in the format used by Swift’s `#fileID` macro. For example, `"AblyChat/Room.swift"`. + internal var fileID: String + /// The line number in the source code file referred to by ``fileID``. + internal var line: Int +} + +internal protocol Logger: Sendable { + func log(_ message: String, level: _AblyPluginSupportPrivate.LogLevel, codeLocation: CodeLocation) +} + +internal extension AblyLiveObjects.Logger { + /// A convenience method that provides default values for `file` and `line`. + func log(_ message: String, level: _AblyPluginSupportPrivate.LogLevel, fileID: String = #fileID, line: Int = #line) { + let codeLocation = CodeLocation(fileID: fileID, line: line) + log(message, level: level, codeLocation: codeLocation) + } +} + +internal final class DefaultLogger: Logger { + private let pluginLogger: _AblyPluginSupportPrivate.Logger + private let pluginAPI: _AblyPluginSupportPrivate.PluginAPIProtocol + + internal init(pluginLogger: _AblyPluginSupportPrivate.Logger, pluginAPI: _AblyPluginSupportPrivate.PluginAPIProtocol) { + self.pluginLogger = pluginLogger + self.pluginAPI = pluginAPI + } + + internal func log(_ message: String, level: LogLevel, codeLocation: CodeLocation) { + pluginAPI.log( + message, + with: level, + file: codeLocation.fileID, + line: codeLocation.line, + logger: pluginLogger, + ) + } +} diff --git a/Sources/AblyLiveObjects/Utility/MarkerProtocolHelpers.swift b/Sources/AblyLiveObjects/Utility/MarkerProtocolHelpers.swift new file mode 100644 index 0000000..28bf619 --- /dev/null +++ b/Sources/AblyLiveObjects/Utility/MarkerProtocolHelpers.swift @@ -0,0 +1,54 @@ +internal import _AblyPluginSupportPrivate +import Ably + +/// Upcasts an instance of an `_AblyPluginSupportPrivate` marker protocol to the concrete type that this marker protocol represents. +internal func castPluginPublicMarkerProtocolValue(_ pluginMarkerProtocolValue: Any, to _: T.Type) -> T { + guard let actualPublicValue = pluginMarkerProtocolValue as? T else { + preconditionFailure("Expected \(T.self), got \(type(of: pluginMarkerProtocolValue))") + } + + return actualPublicValue +} + +internal extension ARTRealtimeChannel { + /// Downcasts this `ARTRealtimeChannel` to its `_AblyPluginSupportPrivate` equivalent type `PublicRealtimeChannel`. + /// + /// - Note: Swift compiler restrictions prevent us from declaring `ARTRealtimeChannel` as conforming to `PublicRealtimeChannel` (this is due to our use of `internal import`). + var asPluginPublicRealtimeChannel: _AblyPluginSupportPrivate.PublicRealtimeChannel { + // In order for this cast to succeed, we rely on the fact that ably-cocoa internally declares ARTRealtimeChannel as conforming to PublicRealtimeChannel. + // swiftlint:disable:next force_cast + self as! _AblyPluginSupportPrivate.PublicRealtimeChannel + } +} + +internal extension ARTClientOptions { + /// Downcasts this `ARTClientOptions` to its `_AblyPluginSupportPrivate` marker protocol type `PublicClientOptions`. + /// + /// - Note: Swift compiler restrictions prevent us from declaring `ARTClientOptions` as conforming to `PublicClientOptions` (this is due to our use of `internal import`). + var asPluginPublicClientOptions: _AblyPluginSupportPrivate.PublicClientOptions { + // In order for this cast to succeed, we rely on the fact that ably-cocoa internally declares ARTClientOptions as conforming to PublicClientOptions. + // swiftlint:disable:next force_cast + self as! _AblyPluginSupportPrivate.PublicClientOptions + } + + /// Upcasts an instance of `_AblyPluginSupportPrivate`'s `PublicClientOptions`, which is the marker protocol that it uses to represent an `ARTClientOptions`, to an `ARTClientOptions`. + static func castPluginPublicClientOptions(_ pluginPublicClientOptions: PublicClientOptions) -> Self { + castPluginPublicMarkerProtocolValue(pluginPublicClientOptions, to: Self.self) + } +} + +internal extension ARTErrorInfo { + /// Downcasts this `ARTErrorInfo` to its `_AblyPluginSupportPrivate` marker protocol type `PublicErrorInfo`. + /// + /// - Note: Swift compiler restrictions prevent us from declaring `ARTErrorInfo` as conforming to `PublicErrorInfo` (this is due to our use of `internal import`). + var asPluginPublicErrorInfo: _AblyPluginSupportPrivate.PublicErrorInfo { + // In order for this cast to succeed, we rely on the fact that ably-cocoa internally declares ARTErrorInfo as conforming to PublicErrorInfo. + // swiftlint:disable:next force_cast + self as! _AblyPluginSupportPrivate.PublicErrorInfo + } + + /// Upcasts an instance of `_AblyPluginSupportPrivate`'s `PublicErrorInfo`, which is the marker protocol that it uses to represent an `ARTErrorInfo`, to an `ARTErrorInfo`. + static func castPluginPublicErrorInfo(_ pluginPublicErrorInfo: PublicErrorInfo) -> Self { + castPluginPublicMarkerProtocolValue(pluginPublicErrorInfo, to: Self.self) + } +} diff --git a/Sources/AblyLiveObjects/Utility/WireValue.swift b/Sources/AblyLiveObjects/Utility/WireValue.swift index e5d2996..6e82a25 100644 --- a/Sources/AblyLiveObjects/Utility/WireValue.swift +++ b/Sources/AblyLiveObjects/Utility/WireValue.swift @@ -137,7 +137,7 @@ internal extension WireValue { } /// Creates a `WireValue` from an `_AblyPluginSupportPrivate` deserialized wire object. Specifically, `pluginSupportData` can be a value that was passed to `LiveObjectsPlugin.decodeObjectMessage:…`. - static func objectFrom_AblyPluginSupportPrivateData(_ pluginSupportData: [String: Any]) -> [String: WireValue] { + static func objectFromPluginSupportData(_ pluginSupportData: [String: Any]) -> [String: WireValue] { let wireValue = WireValue(pluginSupportData: pluginSupportData) guard case let .object(wireObject) = wireValue else { preconditionFailure() diff --git a/Tests/AblyLiveObjectsTests/Helpers/TestLogger.swift b/Tests/AblyLiveObjectsTests/Helpers/TestLogger.swift index 3b6d6f5..3d53bda 100644 --- a/Tests/AblyLiveObjectsTests/Helpers/TestLogger.swift +++ b/Tests/AblyLiveObjectsTests/Helpers/TestLogger.swift @@ -1,23 +1,24 @@ import _AblyPluginSupportPrivate +@testable import AblyLiveObjects import os -/// An implementation of `_AblyPluginSupportPrivate.Logger` to use when testing internal components of the LiveObjects plugin. -final class TestLogger: NSObject, _AblyPluginSupportPrivate.Logger { +/// An implementation of `Logger` to use when testing internal components of the LiveObjects plugin. +final class TestLogger: NSObject, AblyLiveObjects.Logger { // By default, we don’t log in tests to keep the test logs easy to read. You can set this property to `true` to temporarily turn logging on if you want to debug a test. static let loggingEnabled = false private let underlyingLogger = os.Logger() - func log(_ message: String, with level: ARTLogLevel, file fileName: UnsafePointer, line: Int) { + func log(_ message: String, level: LogLevel, codeLocation: CodeLocation) { guard Self.loggingEnabled else { return } - underlyingLogger.log(level: level.toOSLogType, "(\(String(cString: fileName)):\(line)): \(message)") + underlyingLogger.log(level: level.toOSLogType, "(\(codeLocation.fileID):\(codeLocation.line)): \(message)") } } -private extension ARTLogLevel { +private extension _AblyPluginSupportPrivate.LogLevel { var toOSLogType: OSLogType { // Not much thought has gone into this conversion switch self { diff --git a/Tests/AblyLiveObjectsTests/InternalDefaultLiveCounterTests.swift b/Tests/AblyLiveObjectsTests/InternalDefaultLiveCounterTests.swift index a474738..048a64c 100644 --- a/Tests/AblyLiveObjectsTests/InternalDefaultLiveCounterTests.swift +++ b/Tests/AblyLiveObjectsTests/InternalDefaultLiveCounterTests.swift @@ -1,4 +1,5 @@ import _AblyPluginSupportPrivate +import Ably @testable import AblyLiveObjects import Foundation import Testing @@ -7,8 +8,8 @@ struct InternalDefaultLiveCounterTests { /// Tests for the `value` property, covering RTLC5 specification points struct ValueTests { // @spec RTLC5b - @Test(arguments: [.detached, .failed] as [ARTRealtimeChannelState]) - func valueThrowsIfChannelIsDetachedOrFailed(channelState: ARTRealtimeChannelState) async throws { + @Test(arguments: [.detached, .failed] as [_AblyPluginSupportPrivate.RealtimeChannelState]) + func valueThrowsIfChannelIsDetachedOrFailed(channelState: _AblyPluginSupportPrivate.RealtimeChannelState) async throws { let logger = TestLogger() let counter = InternalDefaultLiveCounter.createZeroValued(objectID: "arbitrary", logger: logger, userCallbackQueue: .main, clock: MockSimpleClock()) let coreSDK = MockCoreSDK(channelState: channelState) @@ -426,8 +427,8 @@ struct InternalDefaultLiveCounterTests { /// Tests for the `increment` method, covering RTLC12 specification points struct IncrementTests { // @spec RTLC12c - @Test(arguments: [.detached, .failed, .suspended] as [ARTRealtimeChannelState]) - func throwsErrorForInvalidChannelState(channelState: ARTRealtimeChannelState) async throws { + @Test(arguments: [.detached, .failed, .suspended] as [_AblyPluginSupportPrivate.RealtimeChannelState]) + func throwsErrorForInvalidChannelState(channelState: _AblyPluginSupportPrivate.RealtimeChannelState) async throws { let logger = TestLogger() let counter = InternalDefaultLiveCounter.createZeroValued(objectID: "arbitrary", logger: logger, userCallbackQueue: .main, clock: MockSimpleClock()) let coreSDK = MockCoreSDK(channelState: channelState) diff --git a/Tests/AblyLiveObjectsTests/InternalDefaultLiveMapTests.swift b/Tests/AblyLiveObjectsTests/InternalDefaultLiveMapTests.swift index 0f33211..f5b8890 100644 --- a/Tests/AblyLiveObjectsTests/InternalDefaultLiveMapTests.swift +++ b/Tests/AblyLiveObjectsTests/InternalDefaultLiveMapTests.swift @@ -1,4 +1,5 @@ import _AblyPluginSupportPrivate +import Ably @testable import AblyLiveObjects import Foundation import Testing @@ -7,8 +8,8 @@ struct InternalDefaultLiveMapTests { /// Tests for the `get` method, covering RTLM5 specification points struct GetTests { // @spec RTLM5c - @Test(arguments: [.detached, .failed] as [ARTRealtimeChannelState]) - func getThrowsIfChannelIsDetachedOrFailed(channelState: ARTRealtimeChannelState) async throws { + @Test(arguments: [.detached, .failed] as [_AblyPluginSupportPrivate.RealtimeChannelState]) + func getThrowsIfChannelIsDetachedOrFailed(channelState: _AblyPluginSupportPrivate.RealtimeChannelState) async throws { let logger = TestLogger() let map = InternalDefaultLiveMap.createZeroValued(objectID: "arbitrary", logger: logger, userCallbackQueue: .main, clock: MockSimpleClock()) @@ -275,8 +276,8 @@ struct InternalDefaultLiveMapTests { // @spec RTLM11c // @spec RTLM12b // @spec RTLM13b - @Test(arguments: [.detached, .failed] as [ARTRealtimeChannelState]) - func allPropertiesThrowIfChannelIsDetachedOrFailed(channelState: ARTRealtimeChannelState) async throws { + @Test(arguments: [.detached, .failed] as [_AblyPluginSupportPrivate.RealtimeChannelState]) + func allPropertiesThrowIfChannelIsDetachedOrFailed(channelState: _AblyPluginSupportPrivate.RealtimeChannelState) async throws { let logger = TestLogger() let map = InternalDefaultLiveMap.createZeroValued(objectID: "arbitrary", logger: logger, userCallbackQueue: .main, clock: MockSimpleClock()) let coreSDK = MockCoreSDK(channelState: channelState) @@ -1230,8 +1231,8 @@ struct InternalDefaultLiveMapTests { /// Tests for the `set` method, covering RTLM20 specification points struct SetTests { // @spec RTLM20c - @Test(arguments: [.detached, .failed, .suspended] as [ARTRealtimeChannelState]) - func throwsErrorForInvalidChannelState(channelState: ARTRealtimeChannelState) async throws { + @Test(arguments: [.detached, .failed, .suspended] as [_AblyPluginSupportPrivate.RealtimeChannelState]) + func throwsErrorForInvalidChannelState(channelState: _AblyPluginSupportPrivate.RealtimeChannelState) async throws { let logger = TestLogger() let map = InternalDefaultLiveMap.createZeroValued(objectID: "arbitrary", logger: logger, userCallbackQueue: .main, clock: MockSimpleClock()) let coreSDK = MockCoreSDK(channelState: channelState) @@ -1330,8 +1331,8 @@ struct InternalDefaultLiveMapTests { /// Tests for the `remove` method, covering RTLM21 specification points struct RemoveTests { // @spec RTLM21c - @Test(arguments: [.detached, .failed, .suspended] as [ARTRealtimeChannelState]) - func throwsErrorForInvalidChannelState(channelState: ARTRealtimeChannelState) async throws { + @Test(arguments: [.detached, .failed, .suspended] as [_AblyPluginSupportPrivate.RealtimeChannelState]) + func throwsErrorForInvalidChannelState(channelState: _AblyPluginSupportPrivate.RealtimeChannelState) async throws { let logger = TestLogger() let map = InternalDefaultLiveMap.createZeroValued(objectID: "arbitrary", logger: logger, userCallbackQueue: .main, clock: MockSimpleClock()) let coreSDK = MockCoreSDK(channelState: channelState) diff --git a/Tests/AblyLiveObjectsTests/InternalDefaultRealtimeObjectsTests.swift b/Tests/AblyLiveObjectsTests/InternalDefaultRealtimeObjectsTests.swift index 7d64d96..0406224 100644 --- a/Tests/AblyLiveObjectsTests/InternalDefaultRealtimeObjectsTests.swift +++ b/Tests/AblyLiveObjectsTests/InternalDefaultRealtimeObjectsTests.swift @@ -672,8 +672,8 @@ struct InternalDefaultRealtimeObjectsTests { // MARK: - RTO1b Tests // @spec RTO1b - @Test(arguments: [.detached, .failed] as [ARTRealtimeChannelState]) - func getRootThrowsIfChannelIsDetachedOrFailed(channelState: ARTRealtimeChannelState) async throws { + @Test(arguments: [.detached, .failed] as [_AblyPluginSupportPrivate.RealtimeChannelState]) + func getRootThrowsIfChannelIsDetachedOrFailed(channelState: _AblyPluginSupportPrivate.RealtimeChannelState) async throws { let realtimeObjects = InternalDefaultRealtimeObjectsTests.createDefaultRealtimeObjects() let coreSDK = MockCoreSDK(channelState: channelState) @@ -1044,8 +1044,8 @@ struct InternalDefaultRealtimeObjectsTests { /// Tests for `InternalDefaultRealtimeObjects.createMap`, covering RTO11 specification points (these are largely a smoke test, the rest being tested in ObjectCreationHelpers tests) struct CreateMapTests { // @spec RTO11d - @Test(arguments: [.detached, .failed, .suspended] as [ARTRealtimeChannelState]) - func throwsIfChannelIsInInvalidState(channelState: ARTRealtimeChannelState) async throws { + @Test(arguments: [.detached, .failed, .suspended] as [_AblyPluginSupportPrivate.RealtimeChannelState]) + func throwsIfChannelIsInInvalidState(channelState: _AblyPluginSupportPrivate.RealtimeChannelState) async throws { let realtimeObjects = InternalDefaultRealtimeObjectsTests.createDefaultRealtimeObjects() let coreSDK = MockCoreSDK(channelState: channelState) let entries: [String: InternalLiveMapValue] = ["testKey": .string("testValue")] @@ -1175,8 +1175,8 @@ struct InternalDefaultRealtimeObjectsTests { /// Tests for `InternalDefaultRealtimeObjects.createCounter`, covering RTO12 specification points (these are largely a smoke test, the rest being tested in ObjectCreationHelpers tests) struct CreateCounterTests { // @spec RTO12d - @Test(arguments: [.detached, .failed, .suspended] as [ARTRealtimeChannelState]) - func throwsIfChannelIsInInvalidState(channelState: ARTRealtimeChannelState) async throws { + @Test(arguments: [.detached, .failed, .suspended] as [_AblyPluginSupportPrivate.RealtimeChannelState]) + func throwsIfChannelIsInInvalidState(channelState: _AblyPluginSupportPrivate.RealtimeChannelState) async throws { let realtimeObjects = InternalDefaultRealtimeObjectsTests.createDefaultRealtimeObjects() let coreSDK = MockCoreSDK(channelState: channelState) diff --git a/Tests/AblyLiveObjectsTests/LiveObjectMutableStateTests.swift b/Tests/AblyLiveObjectsTests/LiveObjectMutableStateTests.swift index 06cae4b..dbb8857 100644 --- a/Tests/AblyLiveObjectsTests/LiveObjectMutableStateTests.swift +++ b/Tests/AblyLiveObjectsTests/LiveObjectMutableStateTests.swift @@ -119,8 +119,8 @@ struct LiveObjectMutableStateTests { // @spec RTLO4b2 @available(iOS 17.0.0, tvOS 17.0.0, *) - @Test(arguments: [.detached, .failed] as [ARTRealtimeChannelState]) - func subscribeThrowsIfChannelIsDetachedOrFailed(channelState: ARTRealtimeChannelState) async throws { + @Test(arguments: [.detached, .failed] as [_AblyPluginSupportPrivate.RealtimeChannelState]) + func subscribeThrowsIfChannelIsDetachedOrFailed(channelState: _AblyPluginSupportPrivate.RealtimeChannelState) async throws { var mutableState = LiveObjectMutableState(objectID: "foo") let queue = DispatchQueue.main let subscriber = Subscriber(callbackQueue: queue) diff --git a/Tests/AblyLiveObjectsTests/Mocks/MockCoreSDK.swift b/Tests/AblyLiveObjectsTests/Mocks/MockCoreSDK.swift index 3bb48bb..0c3980a 100644 --- a/Tests/AblyLiveObjectsTests/Mocks/MockCoreSDK.swift +++ b/Tests/AblyLiveObjectsTests/Mocks/MockCoreSDK.swift @@ -1,3 +1,4 @@ +import _AblyPluginSupportPrivate import Ably @testable import AblyLiveObjects @@ -5,10 +6,10 @@ final class MockCoreSDK: CoreSDK { /// Synchronizes access to all of this instance's mutable state. private let mutex = NSLock() - private nonisolated(unsafe) var _channelState: ARTRealtimeChannelState + private nonisolated(unsafe) var _channelState: _AblyPluginSupportPrivate.RealtimeChannelState private nonisolated(unsafe) var _publishHandler: (([OutboundObjectMessage]) async throws(InternalError) -> Void)? - init(channelState: ARTRealtimeChannelState) { + init(channelState: _AblyPluginSupportPrivate.RealtimeChannelState) { _channelState = channelState } @@ -24,7 +25,7 @@ final class MockCoreSDK: CoreSDK { protocolRequirementNotImplemented() } - var channelState: ARTRealtimeChannelState { + var channelState: _AblyPluginSupportPrivate.RealtimeChannelState { get { mutex.withLock { _channelState diff --git a/ably-cocoa b/ably-cocoa index 9f1a651..9e172dd 160000 --- a/ably-cocoa +++ b/ably-cocoa @@ -1 +1 @@ -Subproject commit 9f1a65179b69b6041b3a65664c5fa6c48d722fec +Subproject commit 9e172dd3e744c6d8c2bb0be09a22853aa702005b From a54eb3c76c97470562bd8d26ab15f589721da7d0 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Thu, 21 Aug 2025 10:46:25 -0300 Subject: [PATCH 04/11] Update for renamed repo --- .github/workflows/check.yaml | 4 ++-- CONTRIBUTING.md | 4 ++-- Sources/AblyLiveObjects/Internal/CoreSDK.swift | 2 +- .../Internal/InternalDefaultLiveCounter.swift | 2 +- .../Internal/InternalDefaultLiveMap.swift | 4 ++-- .../InternalDefaultRealtimeObjects.swift | 6 +++--- .../Internal/InternalLiveMapValue.swift | 4 ++-- .../AblyLiveObjects/Internal/ObjectsPool.swift | 4 ++-- .../Protocol/ObjectMessage.swift | 6 +++--- .../Protocol/WireObjectMessage.swift | 2 +- .../InternalDefaultLiveMapTests.swift | 8 ++++---- .../InternalDefaultRealtimeObjectsTests.swift | 4 ++-- .../LiveObjectMutableStateTests.swift | 2 +- .../ObjectLifetimesTests.swift | 6 +++--- .../ObjectMessageTests.swift | 18 +++++++++--------- ably-cocoa | 2 +- package-lock.json | 4 ++-- package.json | 4 ++-- 18 files changed, 43 insertions(+), 43 deletions(-) diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 668bed5..960704c 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -40,7 +40,7 @@ jobs: - run: swift run BuildTool lint - # TODO: Restore in https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/2 once we've seen what form the LiveObjects spec takes + # TODO: Restore in https://github.com/ably/ably-liveobjects-swift-plugin/issues/2 once we've seen what form the LiveObjects spec takes # # spec-coverage: # runs-on: macos-15 @@ -254,7 +254,7 @@ jobs: uses: aws-actions/configure-aws-credentials@v4 with: aws-region: eu-west-2 - role-to-assume: arn:aws:iam::${{ secrets.ABLY_AWS_ACCOUNT_ID_SDK }}:role/ably-sdk-builds-ably-cocoa-liveobjects-plugin + role-to-assume: arn:aws:iam::${{ secrets.ABLY_AWS_ACCOUNT_ID_SDK }}:role/ably-sdk-builds-ably-liveobjects-swift-plugin role-session-name: "${{ github.run_id }}-${{ github.run_number }}" # Upload the generated documentation diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 896d7bc..9eca49c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -127,7 +127,7 @@ Example: For the initial stage of development of this plugin, where we need to also iterate heavily on ably-cocoa, I've added ably-cocoa as a Git submodule, which can be found in [`ably-cocoa`](./ably-cocoa). This allows you to edit ably-cocoa from within this repo's Xcode workspace. -Nearer launch, we'll remove this submodule in https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/7. +Nearer launch, we'll remove this submodule in https://github.com/ably/ably-liveobjects-swift-plugin/issues/7. ## Release process @@ -137,7 +137,7 @@ For each release, the following needs to be done: - Update the following (we have https://github.com/ably/ably-chat-swift/issues/277 for adding a script to do this): - the `version` constant in [`Sources/AblyLiveObjects/Version.swift`](Sources/AblyLiveObjects/Version.swift) - the `from: "…"` in the SPM installation instructions in [`README.md`](README.md) -- Go to [Github releases](https://github.com/ably/ably-cocoa-liveobjects-plugin/releases) and press the `Draft a new release` button. Choose your new branch as a target +- Go to [Github releases](https://github.com/ably/ably-liveobjects-swift-plugin/releases) and press the `Draft a new release` button. Choose your new branch as a target - Press the `Choose a tag` dropdown and start typing a new tag, Github will suggest the `Create new tag x.x.x on publish` option. After you select it Github will unveil the `Generate release notes` button - From the newly generated changes remove everything that don't make much sense to the library user - Copy the final list of changes to the top of the `CHANGELOG.md` file. Modify as necessary to fit the existing format of this file diff --git a/Sources/AblyLiveObjects/Internal/CoreSDK.swift b/Sources/AblyLiveObjects/Internal/CoreSDK.swift index 1e99994..07f3c3a 100644 --- a/Sources/AblyLiveObjects/Internal/CoreSDK.swift +++ b/Sources/AblyLiveObjects/Internal/CoreSDK.swift @@ -66,7 +66,7 @@ internal final class DefaultCoreSDK: CoreSDK { return } - // TODO: Implement the full spec of RTO15 (https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/47) + // TODO: Implement the full spec of RTO15 (https://github.com/ably/ably-liveobjects-swift-plugin/issues/47) try await DefaultInternalPlugin.sendObject( objectMessages: objectMessages, channel: channel, diff --git a/Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift b/Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift index 9c314bc..6e54d5c 100644 --- a/Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift +++ b/Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift @@ -4,7 +4,7 @@ import Foundation /// This provides the implementation behind ``PublicDefaultLiveCounter``, via internal versions of the ``LiveCounter`` API. internal final class InternalDefaultLiveCounter: Sendable { - // Used for synchronizing access to all of this instance's mutable state. This is a temporary solution just to allow us to implement `Sendable`, and we'll revisit it in https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/3. + // Used for synchronizing access to all of this instance's mutable state. This is a temporary solution just to allow us to implement `Sendable`, and we'll revisit it in https://github.com/ably/ably-liveobjects-swift-plugin/issues/3. private let mutex = NSLock() private nonisolated(unsafe) var mutableState: MutableState diff --git a/Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift b/Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift index 84a94c2..8b59104 100644 --- a/Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift +++ b/Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift @@ -9,7 +9,7 @@ internal protocol LiveMapObjectPoolDelegate: AnyObject, Sendable { /// This provides the implementation behind ``PublicDefaultLiveMap``, via internal versions of the ``LiveMap`` API. internal final class InternalDefaultLiveMap: Sendable { - // Used for synchronizing access to all of this instance's mutable state. This is a temporary solution just to allow us to implement `Sendable`, and we'll revisit it in https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/3. + // Used for synchronizing access to all of this instance's mutable state. This is a temporary solution just to allow us to implement `Sendable`, and we'll revisit it in https://github.com/ably/ably-liveobjects-swift-plugin/issues/3. private let mutex = NSLock() private nonisolated(unsafe) var mutableState: MutableState @@ -905,7 +905,7 @@ internal final class InternalDefaultLiveMap: Sendable { return .string(string) } - // TODO: Needs specification (see https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/46) + // TODO: Needs specification (see https://github.com/ably/ably-liveobjects-swift-plugin/issues/46) if let json = entry.data?.json { switch json { case let .array(array): diff --git a/Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift b/Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift index c989756..2bd0f40 100644 --- a/Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift +++ b/Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift @@ -3,7 +3,7 @@ import Ably /// This provides the implementation behind ``PublicDefaultRealtimeObjects``, via internal versions of the ``RealtimeObjects`` API. internal final class InternalDefaultRealtimeObjects: Sendable, LiveMapObjectPoolDelegate { - // Used for synchronizing access to all of this instance's mutable state. This is a temporary solution just to allow us to implement `Sendable`, and we'll revisit it in https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/3. + // Used for synchronizing access to all of this instance's mutable state. This is a temporary solution just to allow us to implement `Sendable`, and we'll revisit it in https://github.com/ably/ably-liveobjects-swift-plugin/issues/3. private let mutex = NSLock() private nonisolated(unsafe) var mutableState: MutableState! @@ -168,7 +168,7 @@ internal final class InternalDefaultRealtimeObjects: Sendable, LiveMapObjectPool } // RTO11f - // TODO: This is a stopgap; change to use server time per RTO11f5 (https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/50) + // TODO: This is a stopgap; change to use server time per RTO11f5 (https://github.com/ably/ably-liveobjects-swift-plugin/issues/50) let timestamp = clock.now let creationOperation = ObjectCreationHelpers.creationOperationForLiveMap( entries: entries, @@ -213,7 +213,7 @@ internal final class InternalDefaultRealtimeObjects: Sendable, LiveMapObjectPool // RTO12f - // TODO: This is a stopgap; change to use server time per RTO12f5 (https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/50) + // TODO: This is a stopgap; change to use server time per RTO12f5 (https://github.com/ably/ably-liveobjects-swift-plugin/issues/50) let timestamp = clock.now let creationOperation = ObjectCreationHelpers.creationOperationForLiveCounter( count: count, diff --git a/Sources/AblyLiveObjects/Internal/InternalLiveMapValue.swift b/Sources/AblyLiveObjects/Internal/InternalLiveMapValue.swift index 9ebeca8..3b41bf0 100644 --- a/Sources/AblyLiveObjects/Internal/InternalLiveMapValue.swift +++ b/Sources/AblyLiveObjects/Internal/InternalLiveMapValue.swift @@ -32,13 +32,13 @@ internal enum InternalLiveMapValue: Sendable, Equatable { self = .jsonObject(value) case let .liveMap(publicLiveMap): guard let publicDefaultLiveMap = publicLiveMap as? PublicDefaultLiveMap else { - // TODO: Try and remove this runtime check and know this type statically, see https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/37 + // TODO: Try and remove this runtime check and know this type statically, see https://github.com/ably/ably-liveobjects-swift-plugin/issues/37 preconditionFailure("Expected PublicDefaultLiveMap, got \(publicLiveMap)") } self = .liveMap(publicDefaultLiveMap.proxied) case let .liveCounter(publicLiveCounter): guard let publicDefaultLiveCounter = publicLiveCounter as? PublicDefaultLiveCounter else { - // TODO: Try and remove this runtime check and know this type statically, see https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/37 + // TODO: Try and remove this runtime check and know this type statically, see https://github.com/ably/ably-liveobjects-swift-plugin/issues/37 preconditionFailure("Expected PublicDefaultLiveCounter, got \(publicLiveCounter)") } self = .liveCounter(publicDefaultLiveCounter.proxied) diff --git a/Sources/AblyLiveObjects/Internal/ObjectsPool.swift b/Sources/AblyLiveObjects/Internal/ObjectsPool.swift index 3cef1de..f4054dc 100644 --- a/Sources/AblyLiveObjects/Internal/ObjectsPool.swift +++ b/Sources/AblyLiveObjects/Internal/ObjectsPool.swift @@ -326,7 +326,7 @@ internal struct ObjectsPool { case let .counter(counter): return counter case .map: - // TODO: Add the ability to statically reason about the type of pool entries in https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/36 + // TODO: Add the ability to statically reason about the type of pool entries in https://github.com/ably/ably-liveobjects-swift-plugin/issues/36 preconditionFailure("Expected counter object with ID \(creationOperation.objectID) but found map object") } } @@ -370,7 +370,7 @@ internal struct ObjectsPool { case let .map(map): return map case .counter: - // TODO: Add the ability to statically reason about the type of pool entries in https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/36 + // TODO: Add the ability to statically reason about the type of pool entries in https://github.com/ably/ably-liveobjects-swift-plugin/issues/36 preconditionFailure("Expected map object with ID \(creationOperation.objectID) but found counter object") } } diff --git a/Sources/AblyLiveObjects/Protocol/ObjectMessage.swift b/Sources/AblyLiveObjects/Protocol/ObjectMessage.swift index ac846fc..b882d60 100644 --- a/Sources/AblyLiveObjects/Protocol/ObjectMessage.swift +++ b/Sources/AblyLiveObjects/Protocol/ObjectMessage.swift @@ -60,7 +60,7 @@ internal struct ObjectData: Equatable { internal var bytes: Data? // OD2d internal var number: NSNumber? // OD2e internal var string: String? // OD2f - internal var json: JSONObjectOrArray? // TODO: Needs specification (see https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/46) + internal var json: JSONObjectOrArray? // TODO: Needs specification (see https://github.com/ably/ably-liveobjects-swift-plugin/issues/46) } internal struct ObjectsMapOp: Equatable { @@ -289,7 +289,7 @@ internal extension ObjectData { } } - // TODO: Needs specification (see https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/46) + // TODO: Needs specification (see https://github.com/ably/ably-liveobjects-swift-plugin/issues/46) if let wireJson = wireObjectData.json { let jsonValue = try JSONObjectOrArray(jsonString: wireJson) json = jsonValue @@ -340,7 +340,7 @@ internal extension ObjectData { // OD4c4: A string payload is encoded as a MessagePack string type, and the result is set on the ObjectData.string attribute // OD4d4: A string payload is represented as a JSON string and set on the ObjectData.string attribute string: string, - // TODO: Needs specification (see https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/46) + // TODO: Needs specification (see https://github.com/ably/ably-liveobjects-swift-plugin/issues/46) json: json?.toJSONString, ) } diff --git a/Sources/AblyLiveObjects/Protocol/WireObjectMessage.swift b/Sources/AblyLiveObjects/Protocol/WireObjectMessage.swift index a8c55d5..828ae0a 100644 --- a/Sources/AblyLiveObjects/Protocol/WireObjectMessage.swift +++ b/Sources/AblyLiveObjects/Protocol/WireObjectMessage.swift @@ -476,7 +476,7 @@ internal struct WireObjectData { internal var bytes: StringOrData? // OD2d internal var number: NSNumber? // OD2e internal var string: String? // OD2f - internal var json: String? // TODO: Needs specification (see https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/46) + internal var json: String? // TODO: Needs specification (see https://github.com/ably/ably-liveobjects-swift-plugin/issues/46) } extension WireObjectData: WireObjectCodable { diff --git a/Tests/AblyLiveObjectsTests/InternalDefaultLiveMapTests.swift b/Tests/AblyLiveObjectsTests/InternalDefaultLiveMapTests.swift index f5b8890..cf5c68a 100644 --- a/Tests/AblyLiveObjectsTests/InternalDefaultLiveMapTests.swift +++ b/Tests/AblyLiveObjectsTests/InternalDefaultLiveMapTests.swift @@ -93,7 +93,7 @@ struct InternalDefaultLiveMapTests { #expect(result?.stringValue == "test") } - // TODO: Needs specification (see https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/46) + // TODO: Needs specification (see https://github.com/ably/ably-liveobjects-swift-plugin/issues/46) // Tests when `json` is a JSON array @Test func returnsJSONArrayValue() throws { @@ -105,7 +105,7 @@ struct InternalDefaultLiveMapTests { #expect(result?.jsonArrayValue == ["foo"]) } - // TODO: Needs specification (see https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/46) + // TODO: Needs specification (see https://github.com/ably/ably-liveobjects-swift-plugin/issues/46) // Tests when `json` is a JSON object @Test func returnsJSONObjectValue() throws { @@ -412,8 +412,8 @@ struct InternalDefaultLiveMapTests { "bytes": TestFactories.internalMapEntry(data: ObjectData(bytes: Data([0x01, 0x02, 0x03]))), // RTLM5d2c "number": TestFactories.internalMapEntry(data: ObjectData(number: NSNumber(value: 42))), // RTLM5d2d "string": TestFactories.internalMapEntry(data: ObjectData(string: "hello")), // RTLM5d2e - "jsonArray": TestFactories.internalMapEntry(data: ObjectData(json: .array(["foo"]))), // TODO: Needs specification (see https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/46) - "jsonObject": TestFactories.internalMapEntry(data: ObjectData(json: .object(["foo": "bar"]))), // TODO: Needs specification (see https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/46) + "jsonArray": TestFactories.internalMapEntry(data: ObjectData(json: .array(["foo"]))), // TODO: Needs specification (see https://github.com/ably/ably-liveobjects-swift-plugin/issues/46) + "jsonObject": TestFactories.internalMapEntry(data: ObjectData(json: .object(["foo": "bar"]))), // TODO: Needs specification (see https://github.com/ably/ably-liveobjects-swift-plugin/issues/46) "mapRef": TestFactories.internalMapEntry(data: ObjectData(objectId: "map:ref@123")), // RTLM5d2f2 "counterRef": TestFactories.internalMapEntry(data: ObjectData(objectId: "counter:ref@456")), // RTLM5d2f2 ], diff --git a/Tests/AblyLiveObjectsTests/InternalDefaultRealtimeObjectsTests.swift b/Tests/AblyLiveObjectsTests/InternalDefaultRealtimeObjectsTests.swift index 0406224..566b2a0 100644 --- a/Tests/AblyLiveObjectsTests/InternalDefaultRealtimeObjectsTests.swift +++ b/Tests/AblyLiveObjectsTests/InternalDefaultRealtimeObjectsTests.swift @@ -1091,7 +1091,7 @@ struct InternalDefaultRealtimeObjectsTests { #expect(publishedMessage.operation?.action == .known(.mapCreate)) let objectID = try #require(publishedMessage.operation?.objectId) #expect(objectID.hasPrefix("map:")) - // TODO: This is a stopgap; change to use server time per RTO11f5 (https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/50) + // TODO: This is a stopgap; change to use server time per RTO11f5 (https://github.com/ably/ably-liveobjects-swift-plugin/issues/50) #expect(objectID.contains("1754042434000")) // check contains the mock clock's timestamp in milliseconds #expect(publishedMessage.operation?.map?.entries == [ "stringKey": .init(data: .init(string: "stringValue")), @@ -1216,7 +1216,7 @@ struct InternalDefaultRealtimeObjectsTests { #expect(publishedMessage.operation?.action == .known(.counterCreate)) let objectID = try #require(publishedMessage.operation?.objectId) #expect(objectID.hasPrefix("counter:")) - // TODO: This is a stopgap; change to use server time per RTO11f5 (https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/50) + // TODO: This is a stopgap; change to use server time per RTO11f5 (https://github.com/ably/ably-liveobjects-swift-plugin/issues/50) #expect(objectID.contains("1754042434000")) // check contains the mock clock's timestamp in milliseconds #expect(publishedMessage.operation?.counter?.count == 10.5) diff --git a/Tests/AblyLiveObjectsTests/LiveObjectMutableStateTests.swift b/Tests/AblyLiveObjectsTests/LiveObjectMutableStateTests.swift index dbb8857..5b304bc 100644 --- a/Tests/AblyLiveObjectsTests/LiveObjectMutableStateTests.swift +++ b/Tests/AblyLiveObjectsTests/LiveObjectMutableStateTests.swift @@ -237,7 +237,7 @@ struct LiveObjectMutableStateTests { // @specOneOf(2/3) RTLO4b5b - Check we can unsubscribe using the `response` that's passed to the listener, and that when two updates are emitted back-to-back, the unsubscribe in the first listener causes us to not recieve the second update @available(iOS 17.0.0, tvOS 17.0.0, *) - @Test(.disabled("This doesn't currently work and I don't think it's a priority, nor do I want to dwell on it right now or rush trying to fix it; see https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/28")) + @Test(.disabled("This doesn't currently work and I don't think it's a priority, nor do I want to dwell on it right now or rush trying to fix it; see https://github.com/ably/ably-liveobjects-swift-plugin/issues/28")) func unsubscribeInsideCallback_backToBackUpdates() async throws { // Given let store = MutableStateStore(stored: .init(objectID: "foo")) diff --git a/Tests/AblyLiveObjectsTests/ObjectLifetimesTests.swift b/Tests/AblyLiveObjectsTests/ObjectLifetimesTests.swift index 23c8498..9c83e4d 100644 --- a/Tests/AblyLiveObjectsTests/ObjectLifetimesTests.swift +++ b/Tests/AblyLiveObjectsTests/ObjectLifetimesTests.swift @@ -68,7 +68,7 @@ struct ObjectLifetimesTests { #expect(createdObjects.weakInternalChannel != nil) #expect(createdObjects.weakInternalRealtimeObjects != nil) - // TODO: test that we can receive events on a LiveObject (https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/30) + // TODO: test that we can receive events on a LiveObject (https://github.com/ably/ably-liveobjects-swift-plugin/issues/30) // Note that after this return we no longer have a reference to createdObjects and thus no longer have a strong reference to our public RealtimeObjects instance return .init( @@ -185,7 +185,7 @@ struct ObjectLifetimesTests { #expect(createdObjects.weakInternalRealtimeObjects != nil) #expect(createdObjects.weakInternalLiveObject != nil) - // TODO: test that we can receive events on a LiveObject (https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/30) + // TODO: test that we can receive events on a LiveObject (https://github.com/ably/ably-liveobjects-swift-plugin/issues/30) // Note that after this return we no longer have a reference to createdObjects and thus no longer have a strong reference to our public LiveObject instance return .init( @@ -232,6 +232,6 @@ struct ObjectLifetimesTests { #expect(objects as AnyObject === objectsAgain as AnyObject) #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-cocoa-liveobjects-plugin/issues/30) + // 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) } } diff --git a/Tests/AblyLiveObjectsTests/ObjectMessageTests.swift b/Tests/AblyLiveObjectsTests/ObjectMessageTests.swift index 85fd9f4..57a913c 100644 --- a/Tests/AblyLiveObjectsTests/ObjectMessageTests.swift +++ b/Tests/AblyLiveObjectsTests/ObjectMessageTests.swift @@ -76,7 +76,7 @@ struct ObjectMessageTests { #expect(wireData.json == nil) } - // TODO: Needs specification (see https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/46) + // TODO: Needs specification (see https://github.com/ably/ably-liveobjects-swift-plugin/issues/46) @Test(arguments: [ // We intentionally use a single-element object so that we get a stable encoding to JSON (jsonObjectOrArray: ["key": "value"] as JSONObjectOrArray, expectedJSONString: #"{"key":"value"}"#), @@ -159,7 +159,7 @@ struct ObjectMessageTests { #expect(wireData.json == nil) } - // TODO: Needs specification (see https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/46) + // TODO: Needs specification (see https://github.com/ably/ably-liveobjects-swift-plugin/issues/46) @Test(arguments: [ // We intentionally use a single-element object so that we get a stable encoding to JSON (jsonObjectOrArray: ["key": "value"] as JSONObjectOrArray, expectedJSONString: #"{"key":"value"}"#), @@ -255,14 +255,14 @@ struct ObjectMessageTests { #expect(objectData.json == nil) } - // TODO: Needs specification (see https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/46) + // TODO: Needs specification (see https://github.com/ably/ably-liveobjects-swift-plugin/issues/46) @Test func json() throws { let jsonString = "{\"key\":\"value\",\"number\":123}" let wireData = WireObjectData(json: jsonString) let objectData = try ObjectData(wireObjectData: wireData, format: .messagePack) - // TODO: Needs specification (see https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/46) + // TODO: Needs specification (see https://github.com/ably/ably-liveobjects-swift-plugin/issues/46) #expect(objectData.boolean == nil) #expect(objectData.bytes == nil) #expect(objectData.number == nil) @@ -270,7 +270,7 @@ struct ObjectMessageTests { #expect(objectData.json == ["key": "value", "number": 123]) } - // TODO: Needs specification (see https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/46) + // TODO: Needs specification (see https://github.com/ably/ably-liveobjects-swift-plugin/issues/46) // The spec doesn't say what to do if JSON parsing fails; I'm choosing to treat it as an error @Test func json_invalidJson() { @@ -283,7 +283,7 @@ struct ObjectMessageTests { } } - // TODO: Needs specification (see https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/46) + // TODO: Needs specification (see https://github.com/ably/ably-liveobjects-swift-plugin/issues/46) // The spec doesn't say what to do if given serialized JSON that contains a non-object-or-array value; I'm choosing to treat it as an error @Test(arguments: [ // string @@ -380,7 +380,7 @@ struct ObjectMessageTests { } } - // TODO: Needs specification (see https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/46) + // TODO: Needs specification (see https://github.com/ably/ably-liveobjects-swift-plugin/issues/46) @Test func json() throws { let jsonString = "{\"key\":\"value\",\"number\":123}" @@ -394,7 +394,7 @@ struct ObjectMessageTests { #expect(objectData.json == ["key": "value", "number": 123]) } - // TODO: Needs specification (see https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/46) + // TODO: Needs specification (see https://github.com/ably/ably-liveobjects-swift-plugin/issues/46) // The spec doesn't say what to do if JSON parsing fails; I'm choosing to treat it as an error @Test func json_invalidJson() { @@ -407,7 +407,7 @@ struct ObjectMessageTests { } } - // TODO: Needs specification (see https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/46) + // TODO: Needs specification (see https://github.com/ably/ably-liveobjects-swift-plugin/issues/46) // The spec doesn't say what to do if given serialized JSON that contains a non-object-or-array value; I'm choosing to treat it as an error @Test(arguments: [ // string diff --git a/ably-cocoa b/ably-cocoa index 9e172dd..a646835 160000 --- a/ably-cocoa +++ b/ably-cocoa @@ -1 +1 @@ -Subproject commit 9e172dd3e744c6d8c2bb0be09a22853aa702005b +Subproject commit a64683548c4147d8de06afbdd89bb2d8540f547e diff --git a/package-lock.json b/package-lock.json index 59e4ef2..7ac4a17 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "ably-cocoa-liveobjects-plugin-dev-tooling", + "name": "ably-liveobjects-swift-plugin-dev-tooling", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "ably-cocoa-liveobjects-plugin-dev-tooling", + "name": "ably-liveobjects-swift-plugin-dev-tooling", "version": "0.1.0", "devDependencies": { "prettier": "^3.3.3" diff --git a/package.json b/package.json index fb991e1..94eb349 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "ably-cocoa-liveobjects-plugin-dev-tooling", + "name": "ably-liveobjects-swift-plugin-dev-tooling", "version": "0.1.0", - "description": "Development tooling for the ably-cocoa-liveobjects-plugin repo", + "description": "Development tooling for the ably-liveobjects-swift-plugin repo", "devDependencies": { "prettier": "^3.3.3" }, From 2b1677b096d2dbf9790f1519fb1388c91f72534c Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Thu, 21 Aug 2025 11:25:22 -0300 Subject: [PATCH 05/11] Remove declaration of batch API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We have not yet implemented this and will not be doing so before our initial release. Also the API is not fully correct — as things stand, if you extract a LiveObject from a map inside the batch callback (see first example on [1]) you have to use an async API to interact with it, which is not what we want. [1] https://ably.com/docs/liveobjects/batch --- .../InternalDefaultRealtimeObjects.swift | 4 - .../PublicDefaultRealtimeObjects.swift | 4 - .../AblyLiveObjects/Public/PublicTypes.swift | 77 ------------------- 3 files changed, 85 deletions(-) diff --git a/Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift b/Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift index 2bd0f40..4a9c283 100644 --- a/Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift +++ b/Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift @@ -242,10 +242,6 @@ internal final class InternalDefaultRealtimeObjects: Sendable, LiveMapObjectPool try await createCounter(count: 0, coreSDK: coreSDK) } - internal func batch(callback _: sending BatchCallback) async throws { - notYetImplemented() - } - @discardableResult internal func on(event _: ObjectsEvent, callback _: ObjectsEventCallback) -> any OnObjectsEventResponse { notYetImplemented() diff --git a/Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicDefaultRealtimeObjects.swift b/Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicDefaultRealtimeObjects.swift index 34221ad..eec2508 100644 --- a/Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicDefaultRealtimeObjects.swift +++ b/Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicDefaultRealtimeObjects.swift @@ -86,10 +86,6 @@ internal final class PublicDefaultRealtimeObjects: RealtimeObjects { ) } - internal func batch(callback: sending BatchCallback) async throws { - try await proxied.batch(callback: callback) - } - internal func on(event: ObjectsEvent, callback: @escaping ObjectsEventCallback) -> any OnObjectsEventResponse { proxied.on(event: event, callback: callback) } diff --git a/Sources/AblyLiveObjects/Public/PublicTypes.swift b/Sources/AblyLiveObjects/Public/PublicTypes.swift index 6e79b29..086ee98 100644 --- a/Sources/AblyLiveObjects/Public/PublicTypes.swift +++ b/Sources/AblyLiveObjects/Public/PublicTypes.swift @@ -16,11 +16,6 @@ public typealias ObjectsEventCallback = @Sendable (_ subscription: OnObjectsEven /// - Parameter subscription: A ``OnLiveObjectLifecycleEventResponse`` object that allows the provided listener to deregister itself from future updates. public typealias LiveObjectLifecycleEventCallback = @Sendable (_ subscription: OnLiveObjectLifecycleEventResponse) -> Void -/// A function passed to ``RealtimeObjects/batch(callback:)`` to group multiple Objects operations into a single channel message. -/// -/// - Parameter batchContext: A ``BatchContext`` object that allows grouping Objects operations for this batch. -public typealias BatchCallback = (_ batchContext: sending BatchContext) -> Void - /// Describes the events emitted by an ``RealtimeObjects`` object. public enum ObjectsEvent: Sendable { /// The local copy of Objects on a channel is currently being synchronized with the Ably service. @@ -56,18 +51,6 @@ public protocol RealtimeObjects: Sendable { /// Creates a new ``LiveCounter`` object instance with a value of zero. func createCounter() async throws(ARTErrorInfo) -> any LiveCounter - /// Allows you to group multiple operations together and send them to the Ably service in a single channel message. - /// As a result, other clients will receive the changes as a single channel message after the batch function has completed. - /// - /// This method accepts a synchronous callback, which is provided with a ``BatchContext`` object. - /// Use the context object to access Objects on a channel and batch operations for them. - /// - /// The objects' data is not modified inside the callback function. Instead, the objects will be updated - /// when the batched operations are applied by the Ably service and echoed back to the client. - /// - /// - Parameter callback: A batch callback function used to group operations together. - func batch(callback: sending BatchCallback) async throws - /// Registers the provided listener for the specified event. If `on()` is called more than once with the same listener and event, the listener is added multiple times to its listener registry. Therefore, as an example, assuming the same listener is registered twice using `on()`, and an event is emitted once, the listener would be invoked twice. /// /// - Parameters: @@ -250,66 +233,6 @@ public protocol OnObjectsEventResponse: Sendable { func off() } -/// Enables grouping multiple Objects operations together by providing `BatchContext*` wrapper objects. -public protocol BatchContext: Sendable { - /// Mirrors the ``RealtimeObjects/getRoot()`` method and returns a ``BatchContextLiveMap`` wrapper for the root object on a channel. - /// - /// - Returns: A ``BatchContextLiveMap`` object. - func getRoot() -> BatchContextLiveMap -} - -/// A wrapper around the ``LiveMap`` object that enables batching operations inside a ``BatchCallback``. -public protocol BatchContextLiveMap: AnyObject, Sendable { - /// Mirrors the ``LiveMap/get(key:)`` method and returns the value associated with a key in the map. - /// - /// - Parameter key: The key to retrieve the value for. - /// - Returns: A ``LiveObject``, a primitive type (string, number, boolean, JSON-serializable object or array ,or binary data) or `nil` if the key doesn't exist in a map or the associated ``LiveObject`` has been deleted. Always `nil` if this map object is deleted. - func get(key: String) -> LiveMapValue? - - /// Returns the number of key-value pairs in the map. - var size: Int { get } - - /// Similar to the ``LiveMap/set(key:value:)`` method, but instead, it adds an operation to set a key in the map with the provided value to the current batch, to be sent in a single message to the Ably service. - /// - /// This does not modify the underlying data of this object. Instead, the change is applied when - /// the published operation is echoed back to the client and applied to the object. - /// To get notified when object gets updated, use the ``LiveObject/subscribe(listener:)`` method. - /// - /// - Parameters: - /// - key: The key to set the value for. - /// - value: The value to assign to the key. - func set(key: String, value: LiveMapValue?) - - /// Similar to the ``LiveMap/remove(key:)`` method, but instead, it adds an operation to remove a key from the map to the current batch, to be sent in a single message to the Ably service. - /// - /// This does not modify the underlying data of this object. Instead, the change is applied when - /// the published operation is echoed back to the client and applied to the object. - /// To get notified when object gets updated, use the ``LiveObject/subscribe(listener:)`` method. - /// - /// - Parameter key: The key to set the value for. - func remove(key: String) -} - -/// A wrapper around the ``LiveCounter`` object that enables batching operations inside a ``BatchCallback``. -public protocol BatchContextLiveCounter: AnyObject, Sendable { - /// Returns the current value of the counter. - var value: Double { get } - - /// Similar to the ``LiveCounter/increment(amount:)`` method, but instead, it adds an operation to increment the counter value to the current batch, to be sent in a single message to the Ably service. - /// - /// This does not modify the underlying data of this object. Instead, the change is applied when - /// the published operation is echoed back to the client and applied to the object. - /// To get notified when object gets updated, use the ``LiveObject/subscribe(listener:)`` method. - /// - /// - Parameter amount: The amount by which to increase the counter value. - func increment(amount: Double) - - /// An alias for calling [`increment(-amount)`](doc:BatchContextLiveCounter/increment(amount:)). - /// - /// - Parameter amount: The amount by which to decrease the counter value. - func decrement(amount: Double) -} - /// The `LiveMap` class represents a key-value map data structure, similar to a Swift `Dictionary`, where all changes are synchronized across clients in realtime. /// Conflicts in a LiveMap are automatically resolved with last-write-wins (LWW) semantics, /// meaning that if two clients update the same key in the map, the update with the most recent timestamp wins. From 30e9020eb578a0425f56b38e8944cfe6faf74f75 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Mon, 4 Aug 2025 14:25:01 +0100 Subject: [PATCH 06/11] Generate a unique channel name for each run of an integration test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This allows us to use "run repeatedly" without the runs interfering with each other. wip adding test UUID to logs this isn't quite right — we need to be able to do client2 and have the ID there too (probably should bake the test into the context) also Test overlaps with Swift Testing terminology now --- Tests/AblyLiveObjectsTests/Helpers/Test.swift | 26 ++ .../Helpers/TestLogger.swift | 2 +- .../ObjectsIntegrationTests.swift | 238 ++++++++++-------- 3 files changed, 164 insertions(+), 102 deletions(-) create mode 100644 Tests/AblyLiveObjectsTests/Helpers/Test.swift diff --git a/Tests/AblyLiveObjectsTests/Helpers/Test.swift b/Tests/AblyLiveObjectsTests/Helpers/Test.swift new file mode 100644 index 0000000..10f138e --- /dev/null +++ b/Tests/AblyLiveObjectsTests/Helpers/Test.swift @@ -0,0 +1,26 @@ +import Foundation + +/// Represents an execution of a test case method. +/// +/// This is the equivalent of what ably-cocoa's tests call `Test` (but this name is already taken here by Swift Testing). +struct TestCaseExecution: ~Copyable { + var id = UUID() + var description: String + + init(description: String) { + NSLog("CREATE TestCaseExecution \(id): \(description)") + self.description = description + } + + consuming func execute(_ testAction: () async throws(E) -> T) async throws(E) -> T { + do { + NSLog("BEGIN TestCaseExecution \(id): \(description)") + let returnValue = try await testAction() + NSLog("FINISH TestCaseExecution \(id): success") + return returnValue + } catch { + NSLog("FINISH TestCaseExecution \(id): error \(error)") + throw error + } + } +} diff --git a/Tests/AblyLiveObjectsTests/Helpers/TestLogger.swift b/Tests/AblyLiveObjectsTests/Helpers/TestLogger.swift index 3d53bda..4e6cf71 100644 --- a/Tests/AblyLiveObjectsTests/Helpers/TestLogger.swift +++ b/Tests/AblyLiveObjectsTests/Helpers/TestLogger.swift @@ -5,7 +5,7 @@ import os /// An implementation of `Logger` to use when testing internal components of the LiveObjects plugin. final class TestLogger: NSObject, AblyLiveObjects.Logger { // By default, we don’t log in tests to keep the test logs easy to read. You can set this property to `true` to temporarily turn logging on if you want to debug a test. - static let loggingEnabled = false + static let loggingEnabled = true private let underlyingLogger = os.Logger() diff --git a/Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsIntegrationTests.swift b/Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsIntegrationTests.swift index 016f06c..fdb228a 100644 --- a/Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsIntegrationTests.swift +++ b/Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsIntegrationTests.swift @@ -213,26 +213,45 @@ private let countersFixtures: [(name: String, count: Double?)] = [ /// The output of `forScenarios`. One element of the one-dimensional arguments array that is passed to a Swift Testing test. private struct TestCase: Identifiable, CustomStringConvertible { + init(disabled: Bool, scenario: TestScenario, baseOptions: ClientHelper.PartialClientOptions, baseChannelName: String) { + self.disabled = disabled + self.scenario = scenario + self.baseOptions = baseOptions + self.baseChannelName = baseChannelName + } + var disabled: Bool var scenario: TestScenario - var options: ClientHelper.PartialClientOptions - var channelName: String + private var baseOptions: ClientHelper.PartialClientOptions + private var baseChannelName: String /// This `Identifiable` conformance allows us to re-run individual test cases from the Xcode UI (https://developer.apple.com/documentation/testing/parameterizedtesting#Run-selected-test-cases) var id: TestCaseID { - .init(description: scenario.description, options: options) + .init(description: scenario.description, options: baseOptions) } /// This seems to determine the nice name that you see for this when it's used as a test case parameter. (I can't see anywhere that this is documented; found it by experimentation). var description: String { var result = scenario.description - if let useBinaryProtocol = options.useBinaryProtocol { + if let useBinaryProtocol = baseOptions.useBinaryProtocol { result += " (\(useBinaryProtocol ? "binary" : "text"))" } return result } + + /// Generates a unique channel name based on ``baseChannelName``. + func generateUniqueChannelName(for execution: borrowing TestCaseExecution) -> String { + "\(execution.id) \(baseChannelName)" + } + + /// Generates client options based on ``baseOptions``, so that the log messages emitted by a client identify the test execution in which the client created. + func options(for execution: borrowing TestCaseExecution) -> ClientHelper.PartialClientOptions { + var options = baseOptions + options.logIdentifier = execution.id.uuidString + return options + } } /// Enables `TestCase`'s conformance to `Identifiable`. @@ -260,12 +279,12 @@ private func forScenarios(_ scenarios: [TestScenario]) -> [Tes return .init( disabled: scenario.disabled, scenario: scenario, - options: clientOptions, - channelName: "\(scenario.description) \(useBinaryProtocol ? "binary" : "text")", + baseOptions: clientOptions, + baseChannelName: "\(scenario.description) \(useBinaryProtocol ? "binary" : "text")", ) } } else { - return [.init(disabled: scenario.disabled, scenario: scenario, options: clientOptions, channelName: scenario.description)] + return [.init(disabled: scenario.disabled, scenario: scenario, baseOptions: clientOptions, baseChannelName: scenario.description)] } } .flatMap(\.self) @@ -3205,27 +3224,33 @@ private struct ObjectsIntegrationTests { return } - let objectsHelper = try await ObjectsHelper() - let client = try await realtimeWithObjects(options: testCase.options) + let testCaseExecution = TestCaseExecution(description: testCase.description) + let options = testCase.options(for: testCaseExecution) + let channelName = testCase.generateUniqueChannelName(for: testCaseExecution) - try await monitorConnectionThenCloseAndFinishAsync(client) { - let channel = client.channels.get(testCase.channelName, options: channelOptionsWithObjects()) - let objects = channel.objects + try await testCaseExecution.execute { + let objectsHelper = try await ObjectsHelper() + let client = try await realtimeWithObjects(options: options) - try await channel.attachAsync() - let root = try await objects.getRoot() + try await monitorConnectionThenCloseAndFinishAsync(client) { + let channel = client.channels.get(channelName, options: channelOptionsWithObjects()) + let objects = channel.objects - try await testCase.scenario.action( - .init( - objects: objects, - root: root, - objectsHelper: objectsHelper, - channelName: testCase.channelName, - channel: channel, - client: client, - clientOptions: testCase.options, - ), - ) + try await channel.attachAsync() + let root = try await objects.getRoot() + + try await testCase.scenario.action( + .init( + objects: objects, + root: root, + objectsHelper: objectsHelper, + channelName: channelName, + channel: channel, + client: client, + clientOptions: options, + ), + ) + } } } @@ -3660,59 +3685,65 @@ private struct ObjectsIntegrationTests { return } - let objectsHelper = try await ObjectsHelper() - let client = try await realtimeWithObjects(options: testCase.options) + let testCaseExecution = TestCaseExecution(description: testCase.description) + let channelName = testCase.generateUniqueChannelName(for: testCaseExecution) + let options = testCase.options(for: testCaseExecution) - try await monitorConnectionThenCloseAndFinishAsync(client) { - let channel = client.channels.get(testCase.channelName, options: channelOptionsWithObjects()) - let objects = channel.objects + try await testCaseExecution.execute { + let objectsHelper = try await ObjectsHelper() + let client = try await realtimeWithObjects(options: options) - try await channel.attachAsync() - let root = try await objects.getRoot() + try await monitorConnectionThenCloseAndFinishAsync(client) { + let channel = client.channels.get(channelName, options: channelOptionsWithObjects()) + let objects = channel.objects - let sampleMapKey = "sampleMap" - let sampleCounterKey = "sampleCounter" + try await channel.attachAsync() + let root = try await objects.getRoot() - // Create promises for waiting for object updates - let objectsCreatedPromiseUpdates1 = try root.updates() - let objectsCreatedPromiseUpdates2 = try root.updates() - async let objectsCreatedPromise: Void = withThrowingTaskGroup(of: Void.self) { group in - group.addTask { - await waitForMapKeyUpdate(objectsCreatedPromiseUpdates1, sampleMapKey) - } - group.addTask { - await waitForMapKeyUpdate(objectsCreatedPromiseUpdates2, sampleCounterKey) + let sampleMapKey = "sampleMap" + let sampleCounterKey = "sampleCounter" + + // Create promises for waiting for object updates + let objectsCreatedPromiseUpdates1 = try root.updates() + let objectsCreatedPromiseUpdates2 = try root.updates() + async let objectsCreatedPromise: Void = withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + await waitForMapKeyUpdate(objectsCreatedPromiseUpdates1, sampleMapKey) + } + group.addTask { + await waitForMapKeyUpdate(objectsCreatedPromiseUpdates2, sampleCounterKey) + } + while try await group.next() != nil {} } - while try await group.next() != nil {} - } - // Prepare map and counter objects for use by the scenario - let sampleMapResult = try await objectsHelper.createAndSetOnMap( - channelName: testCase.channelName, - mapObjectId: "root", - key: sampleMapKey, - createOp: objectsHelper.mapCreateRestOp(), - ) - let sampleCounterResult = try await objectsHelper.createAndSetOnMap( - channelName: testCase.channelName, - mapObjectId: "root", - key: sampleCounterKey, - createOp: objectsHelper.counterCreateRestOp(), - ) - _ = try await objectsCreatedPromise - - try await testCase.scenario.action( - .init( - root: root, - objectsHelper: objectsHelper, - channelName: testCase.channelName, - channel: channel, - sampleMapKey: sampleMapKey, - sampleMapObjectId: sampleMapResult.objectId, - sampleCounterKey: sampleCounterKey, - sampleCounterObjectId: sampleCounterResult.objectId, - ), - ) + // Prepare map and counter objects for use by the scenario + let sampleMapResult = try await objectsHelper.createAndSetOnMap( + channelName: channelName, + mapObjectId: "root", + key: sampleMapKey, + createOp: objectsHelper.mapCreateRestOp(), + ) + let sampleCounterResult = try await objectsHelper.createAndSetOnMap( + channelName: channelName, + mapObjectId: "root", + key: sampleCounterKey, + createOp: objectsHelper.counterCreateRestOp(), + ) + _ = try await objectsCreatedPromise + + try await testCase.scenario.action( + .init( + root: root, + objectsHelper: objectsHelper, + channelName: channelName, + channel: channel, + sampleMapKey: sampleMapKey, + sampleMapObjectId: sampleMapResult.objectId, + sampleCounterKey: sampleCounterKey, + sampleCounterObjectId: sampleCounterResult.objectId, + ), + ) + } } } @@ -3877,48 +3908,53 @@ private struct ObjectsIntegrationTests { return } + let testCaseExecution = TestCaseExecution(description: testCase.description) + let channelName = testCase.generateUniqueChannelName(for: testCaseExecution) + // Configure GC options with shorter intervals for testing - var options = testCase.options + var options = testCase.options(for: testCaseExecution) options.garbageCollectionOptions = .init( interval: 2.0, // JS uses 0.5s but I've found that, at least testing locally, this was not enough to compensate for the clock skew between my local clock and whatever was used to generate the tombstonedAt timestamps server-side. gracePeriod: 0.25, ) - let objectsHelper = try await ObjectsHelper() - let client = try await realtimeWithObjects(options: options) + try await testCaseExecution.execute { + let objectsHelper = try await ObjectsHelper() + let client = try await realtimeWithObjects(options: options) - try await monitorConnectionThenCloseAndFinishAsync(client) { - let channel = client.channels.get(testCase.channelName, options: channelOptionsWithObjects()) - let objects = channel.objects + try await monitorConnectionThenCloseAndFinishAsync(client) { + let channel = client.channels.get(channelName, options: channelOptionsWithObjects()) + let objects = channel.objects - try await channel.attachAsync() - let root = try await objects.getRoot() + try await channel.attachAsync() + let root = try await objects.getRoot() - // Helper function to wait for a specific number of GC cycles - let internallyTypedObjects = try #require(objects as? PublicDefaultRealtimeObjects) - let waitForGCCycles: @Sendable (Int) async -> Void = { cycles in - let gcEvents = internallyTypedObjects.testsOnly_proxied.testsOnly_completedGarbageCollectionEvents + // Helper function to wait for a specific number of GC cycles + let internallyTypedObjects = try #require(objects as? PublicDefaultRealtimeObjects) + let waitForGCCycles: @Sendable (Int) async -> Void = { cycles in + let gcEvents = internallyTypedObjects.testsOnly_proxied.testsOnly_completedGarbageCollectionEvents - var gcCalledTimes = 0 - for await _ in gcEvents { - gcCalledTimes += 1 - if gcCalledTimes >= cycles { - break + var gcCalledTimes = 0 + for await _ in gcEvents { + gcCalledTimes += 1 + if gcCalledTimes >= cycles { + break + } } } - } - try await testCase.scenario.action( - .init( - root: root, - objectsHelper: objectsHelper, - channelName: testCase.channelName, - channel: channel, - objects: objects, - client: client, - waitForGCCycles: waitForGCCycles, - ), - ) + try await testCase.scenario.action( + .init( + root: root, + objectsHelper: objectsHelper, + channelName: channelName, + channel: channel, + objects: objects, + client: client, + waitForGCCycles: waitForGCCycles, + ), + ) + } } } } From 3ba3fccf2eef157147505443797e8dc11797d84e Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Thu, 21 Aug 2025 16:27:29 -0300 Subject: [PATCH 07/11] Add a script that Claude wrote for seeing which TestCaseExecution didn't finish --- analyze-logs.rb | 125 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100755 analyze-logs.rb diff --git a/analyze-logs.rb b/analyze-logs.rb new file mode 100755 index 0000000..7d79f32 --- /dev/null +++ b/analyze-logs.rb @@ -0,0 +1,125 @@ +#!/usr/bin/env ruby + +# Usage: ruby test_analyzer.rb + +if ARGV.empty? + puts "Usage: #{$0} " + exit 1 +end + +log_file = ARGV[0] + +unless File.exist?(log_file) + puts "Error: File '#{log_file}' not found" + exit 1 +end + +# Track test executions +created_tests = {} +begun_tests = {} +finished_tests = {} + +# Parse the log file +File.foreach(log_file) do |line| + # Match CREATE lines + if match = line.match(/CREATE TestCaseExecution ([A-F0-9-]+): (.+)$/) + test_id = match[1] + test_name = match[2] + created_tests[test_id] = test_name + end + + # Match BEGIN lines + if match = line.match(/BEGIN TestCaseExecution ([A-F0-9-]+): (.+)$/) + test_id = match[1] + test_name = match[2] + begun_tests[test_id] = test_name + end + + # Match FINISH lines + if match = line.match(/FINISH TestCaseExecution ([A-F0-9-]+): (.+)$/) + test_id = match[1] + result = match[2] + finished_tests[test_id] = result + end +end + +# Find tests that were created/begun but never finished +unfinished_tests = [] + +created_tests.each do |test_id, test_name| + unless finished_tests.key?(test_id) + status = begun_tests.key?(test_id) ? "BEGUN" : "CREATED" + unfinished_tests << { + id: test_id, + name: test_name, + status: status + } + end +end + +# Output results +puts "Test Execution Analysis" +puts "=" * 50 +puts "Total tests created: #{created_tests.size}" +puts "Total tests begun: #{begun_tests.size}" +puts "Total tests finished: #{finished_tests.size}" +puts "Unfinished tests: #{unfinished_tests.size}" +puts + +if unfinished_tests.empty? + puts "✅ All tests completed!" +else + puts "❌ Tests that did not finish:" + puts + + unfinished_tests.each_with_index do |test, index| + puts "#{index + 1}. #{test[:id]} (#{test[:status]})" + puts " #{test[:name]}" + puts + end +end + +# Summary by status +created_only = unfinished_tests.select { |t| t[:status] == "CREATED" } +begun_only = unfinished_tests.select { |t| t[:status] == "BEGUN" } + +if created_only.any? + puts "Tests that were CREATED but never BEGUN (#{created_only.size}):" + created_only.each { |t| puts " - #{t[:name]}" } + puts +end + +if begun_only.any? + puts "Tests that were BEGUN but never FINISHED (#{begun_only.size}):" + begun_only.each { |t| puts " - #{t[:name]}" } + puts +end + +# Show some finished test results summary +success_count = finished_tests.values.count("success") +error_count = finished_tests.values.count { |result| result.start_with?("error") } + +puts "Finished test results:" +puts " ✅ Success: #{success_count}" +puts " ❌ Error: #{error_count}" + +if error_count > 0 + puts + puts "Common error patterns:" + error_patterns = Hash.new(0) + finished_tests.values.each do |result| + if result.start_with?("error") + if result.include?("connection limit exceeded") + error_patterns["Connection limit exceeded"] += 1 + elsif result.include?("ExpectationFailedError") + error_patterns["Expectation failed"] += 1 + else + error_patterns["Other error"] += 1 + end + end + end + + error_patterns.each do |pattern, count| + puts " - #{pattern}: #{count}" + end +end \ No newline at end of file From 073abb8424d594a28ece5022a03b4895d2b45f95 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Thu, 21 Aug 2025 16:57:11 -0300 Subject: [PATCH 08/11] Add more logging - number of concurrent Realtime instances - when monitorConnectionThenCloseAndFinishAsync fails (to see whether it's actually failing the test when it should be) --- .../ObjectsIntegrationTests.swift | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsIntegrationTests.swift b/Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsIntegrationTests.swift index fdb228a..6a31069 100644 --- a/Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsIntegrationTests.swift +++ b/Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsIntegrationTests.swift @@ -39,9 +39,24 @@ private func lexicoTimeserial(seriesId: String, timestamp: Int64, counter: Int, return result } +nonisolated(unsafe) var realtimeInstanceCount = 0 +let mutex = NSLock() + func monitorConnectionThenCloseAndFinishAsync(_ realtime: ARTRealtime, action: @escaping @Sendable () async throws -> Void) async throws { - defer { realtime.connection.close() } + defer { + let localRealtimeInstanceCount = mutex.withLock { + realtimeInstanceCount -= 1 + return realtimeInstanceCount + } + print("realtimeInstanceCount decreased to \(localRealtimeInstanceCount)") + realtime.connection.close() + } + let localRealtimeInstanceCount = mutex.withLock { + realtimeInstanceCount += 1 + return realtimeInstanceCount + } + print("realtimeInstanceCount increased to \(localRealtimeInstanceCount)") try await withThrowingTaskGroup { group in // Monitor connection state for state in [ARTRealtimeConnectionEvent.failed, .suspended] { @@ -49,6 +64,7 @@ func monitorConnectionThenCloseAndFinishAsync(_ realtime: ARTRealtime, action: @ let (stream, continuation) = AsyncThrowingStream.makeStream() let subscription = realtime.connection.on(state) { _ in + print("monitorConnectionThenCloseAndFinishAsync got error state \(state)") realtime.close() let error = NSError( From 1ef0fc1e3f9434b6f8e9939b92e65342023041d6 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Thu, 21 Aug 2025 17:29:52 -0300 Subject: [PATCH 09/11] attempting to limit concurrency asked Cursor to write this trait that limits concurrency this is because there seem to be issues with running concurrent tests (hangs, and failures that I think are because of it exercising edge cases in the tests, but also Realtime's "Error 40111 - account restricted (connection limit exceeded)" and connection timeouts) my logging suggests that we have up to 100 Realtime instances active at a time I was going to just make things .serialized for now but that makes the test suite really slow (like 4 minutes) so I hoped we could find a middle ground I tried making it a suite trait that has isRecursive: true (which I thought was the default which is why I made those earlier changes to the fixtures trait) but it doesn't seem to be. I've however observed this hanging at least once locally so won't be taking this forward for now (need to get the test suite fixed and have a time pressure) --- .../ObjectsIntegrationTests.swift | 77 ++++++++++++++++++- 1 file changed, 74 insertions(+), 3 deletions(-) diff --git a/Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsIntegrationTests.swift b/Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsIntegrationTests.swift index 6a31069..2e51bfd 100644 --- a/Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsIntegrationTests.swift +++ b/Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsIntegrationTests.swift @@ -353,6 +353,77 @@ extension Trait where Self == ObjectsFixturesTrait { static var objectsFixtures: Self { Self() } } +/// Limits the number of concurrently-executing tests to a given number. +/// +/// This trait uses an actor-based counter to ensure that only a limited number of tests can run simultaneously, +/// which can be useful for preventing resource exhaustion or rate limiting issues. +private actor ConcurrencyLimitingTrait: TestTrait, TestScoping { + private let maxConcurrentTests: Int + private var currentRunningTests: Int = 0 + private var waitingTests: [CheckedContinuation] = [] + + init(maxConcurrentTests: Int) { + self.maxConcurrentTests = maxConcurrentTests + print("ConcurrencyLimitingTrait initialized with limit: \(maxConcurrentTests)") + } + +// nonisolated var isRecursive: Bool { +// // if we make this true the tests crash with EXC_BREAKPOINT +// false +// } + + func provideScope(for _: Test, testCase _: Test.Case?, performing function: () async throws -> Void) async throws { + // Wait for a slot to become available + await waitForSlot() + + // Increment the counter + currentRunningTests += 1 + print("currentRunningTests increased to \(currentRunningTests)") + + defer { + // Decrement the counter and signal waiting tests + currentRunningTests -= 1 + print("currentRunningTests decreased to \(currentRunningTests)") + signalWaitingTests() + } + + try await function() + } + + private func waitForSlot() async { + if currentRunningTests < maxConcurrentTests { + print("waitForSlot: slot available, currentRunningTests=\(currentRunningTests)") + return + } + + print("waitForSlot: waiting, currentRunningTests=\(currentRunningTests), waitingTests.count=\(waitingTests.count)") + return await withCheckedContinuation { continuation in + waitingTests.append(continuation) + print("waitForSlot: added to waiting queue, waitingTests.count=\(waitingTests.count)") + } + } + + private func signalWaitingTests() { + // Only signal one test at a time to prevent race conditions + // and ensure we don't exceed the limit + print("signalWaitingTests: currentRunningTests=\(currentRunningTests), waitingTests.count=\(waitingTests.count)") + if !waitingTests.isEmpty, currentRunningTests < maxConcurrentTests { + let nextTest = waitingTests.removeFirst() + print("signalWaitingTests: resuming next test, waitingTests.count=\(waitingTests.count)") + // Resume the next test, which will then call waitForSlot() and increment the counter + nextTest.resume() + } + } +} + +extension Trait where Self == ConcurrencyLimitingTrait { + // Use a singleton instance to ensure all tests share the same trait + static var concurrencyLimit5: Self { + // This will create a single instance that's shared across all tests + Self(maxConcurrentTests: 5) + } +} + // MARK: - Utility types /// A class that isolates arbitrary mutable state to the main actor. @@ -3231,7 +3302,7 @@ private struct ObjectsIntegrationTests { }() } - @Test(arguments: FirstSetOfScenarios.testCases) + @Test(.concurrencyLimit5, arguments: FirstSetOfScenarios.testCases) func firstSetOfScenarios(testCase: TestCase) async throws { guard !testCase.disabled else { withKnownIssue { @@ -3692,7 +3763,7 @@ private struct ObjectsIntegrationTests { } @available(iOS 17.0.0, tvOS 17.0.0, *) - @Test(arguments: SubscriptionCallbacksScenarios.testCases) + @Test(.concurrencyLimit5, arguments: SubscriptionCallbacksScenarios.testCases) func subscriptionCallbacksScenarios(testCase: TestCase) async throws { guard !testCase.disabled else { withKnownIssue { @@ -3915,7 +3986,7 @@ private struct ObjectsIntegrationTests { ] } - @Test(arguments: TombstonesGCScenarios.testCases) + @Test(.concurrencyLimit5, arguments: TombstonesGCScenarios.testCases) func tombstonesGCScenarios(testCase: TestCase) async throws { guard !testCase.disabled else { withKnownIssue { From 3c4cba1df092e70801897ed024a1f3169c599862 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Thu, 21 Aug 2025 14:50:12 -0300 Subject: [PATCH 10/11] Fix ordering of maps created from fixtures in test Mistake in 70306a0. Correct approach copied from "Objects.createCounter sends COUNTER_CREATE operation" test. --- .../ObjectsIntegrationTests.swift | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsIntegrationTests.swift b/Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsIntegrationTests.swift index 2e51bfd..3dd25a0 100644 --- a/Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsIntegrationTests.swift +++ b/Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsIntegrationTests.swift @@ -3022,22 +3022,23 @@ private struct ObjectsIntegrationTests { action: { ctx in let objects = ctx.objects - let maps = try await withThrowingTaskGroup(of: (any LiveMap).self, returning: [any LiveMap].self) { group in - for mapFixture in primitiveMapsFixtures { + let maps = try await withThrowingTaskGroup(of: (index: Int, map: any LiveMap).self, returning: [any LiveMap].self) { group in + for (index, mapFixture) in primitiveMapsFixtures.enumerated() { group.addTask { - if let entries = mapFixture.liveMapEntries { + let map = if let entries = mapFixture.liveMapEntries { try await objects.createMap(entries: entries) } else { try await objects.createMap() } + return (index: index, map: map) } } - var results: [any LiveMap] = [] - while let map = try await group.next() { - results.append(map) + var results: [(index: Int, map: any LiveMap)] = [] + while let result = try await group.next() { + results.append(result) } - return results + return results.sorted { $0.index < $1.index }.map(\.map) } for (i, map) in maps.enumerated() { From 478f9579262e08c57732f6cfe32e234ff9dda847 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Thu, 21 Aug 2025 15:51:41 -0300 Subject: [PATCH 11/11] Make waiting for GC more robust in integration tests Wait until we're sure that GC of an entry should have occurred, regardless of clock skew. Also fix what may have been a bug in 792683d where we may not wait for as many GC cycles as we intended to (because of counting AsyncStream buffered values). --- .../InternalDefaultRealtimeObjects.swift | 18 ++++---- .../ObjectsIntegrationTests.swift | 46 ++++++++++--------- 2 files changed, 33 insertions(+), 31 deletions(-) diff --git a/Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift b/Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift index 4a9c283..b70b428 100644 --- a/Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift +++ b/Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift @@ -98,7 +98,7 @@ internal final class InternalDefaultRealtimeObjects: Sendable, LiveMapObjectPool (receivedObjectProtocolMessages, receivedObjectProtocolMessagesContinuation) = AsyncStream.makeStream() (receivedObjectSyncProtocolMessages, receivedObjectSyncProtocolMessagesContinuation) = AsyncStream.makeStream() (waitingForSyncEvents, waitingForSyncEventsContinuation) = AsyncStream.makeStream() - (completedGarbageCollectionEvents, completedGarbageCollectionsEventsContinuation) = AsyncStream.makeStream() + (completedGarbageCollectionEventsWithoutBuffering, completedGarbageCollectionEventsWithoutBufferingContinuation) = AsyncStream.makeStream(bufferingPolicy: .bufferingNewest(0)) mutableState = .init(objectsPool: .init(logger: logger, userCallbackQueue: userCallbackQueue, clock: clock)) garbageCollectionInterval = garbageCollectionOptions.interval garbageCollectionGracePeriod = garbageCollectionOptions.gracePeriod @@ -328,17 +328,17 @@ internal final class InternalDefaultRealtimeObjects: Sendable, LiveMapObjectPool gracePeriod: garbageCollectionGracePeriod, clock: clock, logger: logger, - eventsContinuation: completedGarbageCollectionsEventsContinuation, + eventsContinuation: completedGarbageCollectionEventsWithoutBufferingContinuation, ) } } - // These drive the testsOnly_completedGarbageCollectionEvents property that informs the test suite when a garbage collection cycle has completed. - private let completedGarbageCollectionEvents: AsyncStream - private let completedGarbageCollectionsEventsContinuation: AsyncStream.Continuation + // These drive the testsOnly_completedGarbageCollectionEventsWithoutBuffering property that informs the test suite when a garbage collection cycle has completed. + private let completedGarbageCollectionEventsWithoutBuffering: AsyncStream + private let completedGarbageCollectionEventsWithoutBufferingContinuation: AsyncStream.Continuation /// Emits an element whenever a garbage collection cycle has completed. - internal var testsOnly_completedGarbageCollectionEvents: AsyncStream { - completedGarbageCollectionEvents + internal var testsOnly_completedGarbageCollectionEventsWithoutBuffering: AsyncStream { + completedGarbageCollectionEventsWithoutBuffering } // MARK: - Testing @@ -348,12 +348,12 @@ internal final class InternalDefaultRealtimeObjects: Sendable, LiveMapObjectPool /// - testsOnly_receivedObjectProtocolMessages /// - testsOnly_receivedObjectStateProtocolMessages /// - testsOnly_waitingForSyncEvents - /// - testsOnly_completedGarbageCollectionEvents + /// - testsOnly_completedGarbageCollectionEventsWithoutBuffering internal func testsOnly_finishAllTestHelperStreams() { receivedObjectProtocolMessagesContinuation.finish() receivedObjectSyncProtocolMessagesContinuation.finish() waitingForSyncEventsContinuation.finish() - completedGarbageCollectionsEventsContinuation.finish() + completedGarbageCollectionEventsWithoutBufferingContinuation.finish() } // MARK: - Mutable state and the operations that affect it diff --git a/Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsIntegrationTests.swift b/Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsIntegrationTests.swift index 3dd25a0..996d899 100644 --- a/Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsIntegrationTests.swift +++ b/Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsIntegrationTests.swift @@ -3847,7 +3847,7 @@ private struct ObjectsIntegrationTests { var channel: ARTRealtimeChannel var objects: any RealtimeObjects var client: ARTRealtime - var waitForGCCycles: @Sendable (Int) async -> Void + var waitForTombstonedObjectsToBeCollected: @Sendable (Date) async throws -> Void } static let scenarios: [TestScenario] = [ @@ -3860,7 +3860,7 @@ private struct ObjectsIntegrationTests { let channelName = ctx.channelName let channel = ctx.channel let objects = ctx.objects - let waitForGCCycles = ctx.waitForGCCycles + let waitForTombstonedObjectsToBeCollected = ctx.waitForTombstonedObjectsToBeCollected // Wait for counter creation async let counterCreatedPromise: Void = waitForObjectOperation(ctx.objects, .counterCreate) @@ -3898,9 +3898,10 @@ private struct ObjectsIntegrationTests { "Check object's \"tombstone\" flag is set to \"true\" after OBJECT_DELETE", ) - // We expect 2 cycles to guarantee that grace period has expired, which will always be - // true based on the test config used - await waitForGCCycles(2) + let tombstonedAt = try #require(poolEntry.tombstonedAt) + + // Wait for objects tombstoned at this time to be garbage collected + try await waitForTombstonedObjectsToBeCollected(tombstonedAt) // Object should be removed from the local pool entirely now, as the GC grace period has passed #expect( @@ -3917,7 +3918,7 @@ private struct ObjectsIntegrationTests { let root = ctx.root let objectsHelper = ctx.objectsHelper let channelName = ctx.channelName - let waitForGCCycles = ctx.waitForGCCycles + let waitForTombstonedObjectsToBeCollected = ctx.waitForTombstonedObjectsToBeCollected let keyUpdatedPromise = try root.updates() async let keyUpdatedWait: Void = { @@ -3972,9 +3973,10 @@ private struct ObjectsIntegrationTests { "Check map entry for \"foo\" on root has \"tombstone\" flag set to \"true\" after MAP_REMOVE", ) - // We expect 2 cycles to guarantee that grace period has expired, which will always be - // true based on the test config used - await waitForGCCycles(2) + let tombstonedAt = try #require(underlyingData["foo"]?.tombstonedAt) + + // Wait for objects tombstoned at this time to be garbage collected + try await waitForTombstonedObjectsToBeCollected(tombstonedAt) // The entry should be removed from the underlying map now let underlyingDataAfterGC = internalRoot.testsOnly_data @@ -4001,10 +4003,11 @@ private struct ObjectsIntegrationTests { // Configure GC options with shorter intervals for testing var options = testCase.options(for: testCaseExecution) - options.garbageCollectionOptions = .init( - interval: 2.0, // JS uses 0.5s but I've found that, at least testing locally, this was not enough to compensate for the clock skew between my local clock and whatever was used to generate the tombstonedAt timestamps server-side. + let garbageCollectionOptions = InternalDefaultRealtimeObjects.GarbageCollectionOptions( + interval: 0.5, gracePeriod: 0.25, ) + options.garbageCollectionOptions = garbageCollectionOptions try await testCaseExecution.execute { let objectsHelper = try await ObjectsHelper() @@ -4017,18 +4020,17 @@ private struct ObjectsIntegrationTests { try await channel.attachAsync() let root = try await objects.getRoot() - // Helper function to wait for a specific number of GC cycles + // Helper function to wait for enough GC cycles to occur such that objects tombstoned at a specific time should have been garbage collected. This is a slightly different approach to the JS tests, which wait for a certain number of GC cycles to occur, but I think that this is a bit more robust in the face of clock skew between the local clock and whatever was used to generate the tombstonedAt timestamps server-side. let internallyTypedObjects = try #require(objects as? PublicDefaultRealtimeObjects) - let waitForGCCycles: @Sendable (Int) async -> Void = { cycles in - let gcEvents = internallyTypedObjects.testsOnly_proxied.testsOnly_completedGarbageCollectionEvents - - var gcCalledTimes = 0 - for await _ in gcEvents { - gcCalledTimes += 1 - if gcCalledTimes >= cycles { - break - } + let waitForTombstonedObjectsToBeCollected: @Sendable (Date) async throws -> Void = { (tombstonedAt: Date) in + // Sleep until we're sure we're past tombstonedAt + gracePeriod + let timeUntilGracePeriodExpires = (tombstonedAt + garbageCollectionOptions.gracePeriod).timeIntervalSince(.init()) + if timeUntilGracePeriodExpires > 0 { + try await Task.sleep(nanoseconds: UInt64(timeUntilGracePeriodExpires * Double(NSEC_PER_SEC))) } + + // Wait for the next GC event + await internallyTypedObjects.testsOnly_proxied.testsOnly_completedGarbageCollectionEventsWithoutBuffering.first { _ in true } } try await testCase.scenario.action( @@ -4039,7 +4041,7 @@ private struct ObjectsIntegrationTests { channel: channel, objects: objects, client: client, - waitForGCCycles: waitForGCCycles, + waitForTombstonedObjectsToBeCollected: waitForTombstonedObjectsToBeCollected, ), ) }