diff --git a/.cursor/rules/swift.mdc b/.cursor/rules/swift.mdc index 3fa734a6..b4a23b5b 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 07362939..f21ef642 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/.github/workflows/check.yaml b/.github/workflows/check.yaml index 668bed56..960704c9 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/AblyLiveObjects.xcworkspace/xcshareddata/swiftpm/Package.resolved b/AblyLiveObjects.xcworkspace/xcshareddata/swiftpm/Package.resolved index bfe1d581..cd28d5ba 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/CONTRIBUTING.md b/CONTRIBUTING.md index 896d7bc2..9eca49ca 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/Package.resolved b/Package.resolved index 773dea53..44dc3735 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 25fd4fce..ce7b62f2 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", @@ -47,8 +51,8 @@ let package = Package( package: "ably-cocoa", ), .product( - name: "AblyPlugin", - package: "ably-cocoa", + name: "_AblyPluginSupportPrivate", + package: "ably-cocoa-plugin-support", ), ], ), @@ -61,8 +65,8 @@ let package = Package( package: "ably-cocoa", ), .product( - name: "AblyPlugin", - package: "ably-cocoa", + name: "_AblyPluginSupportPrivate", + package: "ably-cocoa-plugin-support", ), ], resources: [ diff --git a/Sources/AblyLiveObjects/Internal/ARTClientOptions+Objects.swift b/Sources/AblyLiveObjects/Internal/ARTClientOptions+Objects.swift index dac94d1a..68554345 100644 --- a/Sources/AblyLiveObjects/Internal/ARTClientOptions+Objects.swift +++ b/Sources/AblyLiveObjects/Internal/ARTClientOptions+Objects.swift @@ -1,4 +1,5 @@ -internal import AblyPlugin +internal import _AblyPluginSupportPrivate +import Ably internal extension ARTClientOptions { private class Box { @@ -14,9 +15,9 @@ 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, + clientOptions: asPluginPublicClientOptions, ) guard let optionsValue else { @@ -35,10 +36,10 @@ 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, + clientOptions: asPluginPublicClientOptions, ) } } diff --git a/Sources/AblyLiveObjects/Internal/CoreSDK.swift b/Sources/AblyLiveObjects/Internal/CoreSDK.swift index 51f05460..07f3c3a8 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) @@ -14,17 +14,17 @@ 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 { /// 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: 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: Logger ) { self.channel = channel self.client = client @@ -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, @@ -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 207ffd80..ac081fc2 100644 --- a/Sources/AblyLiveObjects/Internal/DefaultInternalPlugin.swift +++ b/Sources/AblyLiveObjects/Internal/DefaultInternalPlugin.swift @@ -1,14 +1,15 @@ -internal import AblyPlugin +internal import _AblyPluginSupportPrivate +import Ably -// 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 +21,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,11 +34,12 @@ internal final class DefaultInternalPlugin: NSObject, AblyPlugin.LiveObjectsInte // MARK: - LiveObjectsInternalPluginProtocol // Populates the channel's `objects` property. - internal func prepare(_ channel: AblyPlugin.RealtimeChannel, client: AblyPlugin.RealtimeClient) { - let logger = pluginAPI.logger(for: channel) + internal func prepare(_ channel: _AblyPluginSupportPrivate.RealtimeChannel, client: _AblyPluginSupportPrivate.RealtimeClient) { + 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, @@ -49,14 +51,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) { @@ -68,9 +70,9 @@ internal final class DefaultInternalPlugin: NSObject, AblyPlugin.LiveObjectsInte _ serialized: [String: Any], context: DecodingContextProtocol, format: EncodingFormat, - error errorPtr: AutoreleasingUnsafeMutablePointer?, + error errorPtr: AutoreleasingUnsafeMutablePointer<_AblyPluginSupportPrivate.PublicErrorInfo?>?, ) -> (any ObjectMessageProtocol)? { - let wireObject = WireValue.objectFromAblyPluginData(serialized) + let wireObject = WireValue.objectFromPluginSupportData(serialized) do { let wireObjectMessage = try InboundWireObjectMessage( @@ -83,13 +85,13 @@ internal final class DefaultInternalPlugin: NSObject, AblyPlugin.LiveObjectsInte ) return ObjectMessageBox(objectMessage: objectMessage) } catch { - errorPtr?.pointee = error.toARTErrorInfo() + errorPtr?.pointee = error.toARTErrorInfo().asPluginPublicErrorInfo return nil } } internal func encodeObjectMessage( - _ publicObjectMessage: any AblyPlugin.ObjectMessageProtocol, + _ publicObjectMessage: any _AblyPluginSupportPrivate.ObjectMessageProtocol, format: EncodingFormat, ) -> [String: Any] { guard let outboundObjectMessageBox = publicObjectMessage as? ObjectMessageBox else { @@ -97,14 +99,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 +118,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 +135,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 a5d7058a..6e54d5c2 100644 --- a/Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift +++ b/Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift @@ -1,10 +1,10 @@ +internal import _AblyPluginSupportPrivate import Ably -internal import AblyPlugin 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 @@ -21,7 +21,7 @@ internal final class InternalDefaultLiveCounter: Sendable { } } - private let logger: AblyPlugin.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: AblyPlugin.Logger, + logger: Logger, userCallbackQueue: DispatchQueue, clock: SimpleClock ) { @@ -40,7 +40,7 @@ internal final class InternalDefaultLiveCounter: Sendable { private init( data: Double, objectID: String, - logger: AblyPlugin.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: AblyPlugin.Logger, + logger: Logger, userCallbackQueue: DispatchQueue, clock: SimpleClock, ) -> Self { diff --git a/Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift b/Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift index 312affaa..8b591049 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 { @@ -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 @@ -38,7 +38,7 @@ internal final class InternalDefaultLiveMap: Sendable { } } - private let logger: AblyPlugin.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: AblyPlugin.Logger, + logger: 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: 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: 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: 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: 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: 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: Logger, userCallbackQueue: DispatchQueue, clock: SimpleClock, ) -> LiveObjectUpdate { @@ -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 af12e8a5..b70b4289 100644 --- a/Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift +++ b/Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift @@ -1,14 +1,14 @@ +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 { - // 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! - private let logger: AblyPlugin.Logger + private let logger: Logger private let userCallbackQueue: DispatchQueue private let clock: SimpleClock @@ -91,14 +91,14 @@ internal final class InternalDefaultRealtimeObjects: Sendable, LiveMapObjectPool } } - internal init(logger: AblyPlugin.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 (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 @@ -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, @@ -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() @@ -332,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 @@ -352,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/Sources/AblyLiveObjects/Internal/InternalLiveMapValue.swift b/Sources/AblyLiveObjects/Internal/InternalLiveMapValue.swift index 9ebeca85..3b41bf06 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/InternalLiveObject.swift b/Sources/AblyLiveObjects/Internal/InternalLiveObject.swift index d80d53b4..27b0d7e8 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 f3326208..191d312c 100644 --- a/Sources/AblyLiveObjects/Internal/LiveObjectMutableState.swift +++ b/Sources/AblyLiveObjects/Internal/LiveObjectMutableState.swift @@ -1,4 +1,5 @@ -internal import AblyPlugin +internal import _AblyPluginSupportPrivate +import Ably /// 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 b82f7d84..f061302e 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 d2ff6481..f4054dca 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: Logger, userCallbackQueue: DispatchQueue, clock: SimpleClock, testsOnly_otherEntries otherEntries: [String: Entry]? = nil, @@ -151,7 +151,7 @@ internal struct ObjectsPool { } private init( - logger: AblyPlugin.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: AblyPlugin.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: AblyPlugin.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: AblyPlugin.Logger, + logger: Logger, userCallbackQueue: DispatchQueue, clock: SimpleClock, ) -> InternalDefaultLiveCounter { @@ -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") } } @@ -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: Logger, userCallbackQueue: DispatchQueue, clock: SimpleClock, ) -> InternalDefaultLiveMap { @@ -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 0a240199..b882d600 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. @@ -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 { @@ -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 @@ -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 @@ -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 { @@ -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, ) } @@ -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 fad740a3..828ae0a8 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) { @@ -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/Sources/AblyLiveObjects/Public/ARTRealtimeChannel+Objects.swift b/Sources/AblyLiveObjects/Public/ARTRealtimeChannel+Objects.swift index da479178..cec5ca94 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. @@ -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 26a377d4..799c3542 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.DependencyStore.sharedInstance().fetchPluginAPI() - // 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 bd88d2fb..fa7e7bb3 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: 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 146d05e6..90a408da 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: Logger - internal init(proxied: InternalDefaultLiveCounter, coreSDK: CoreSDK, logger: AblyPlugin.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 52e207d2..5aad18fa 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: Logger - internal init(proxied: InternalDefaultLiveMap, coreSDK: CoreSDK, delegate: LiveMapObjectPoolDelegate, logger: AblyPlugin.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 3d97b8b5..eec2508a 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: Logger - internal init(proxied: InternalDefaultRealtimeObjects, coreSDK: CoreSDK, logger: AblyPlugin.Logger) { + internal init(proxied: InternalDefaultRealtimeObjects, coreSDK: CoreSDK, logger: Logger) { self.proxied = proxied self.coreSDK = coreSDK self.logger = logger @@ -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/Public Proxy Objects/PublicObjectsStore.swift b/Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicObjectsStore.swift index 4be07534..264a92cc 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: 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: 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: 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: 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: 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/Public/PublicTypes.swift b/Sources/AblyLiveObjects/Public/PublicTypes.swift index 6e79b290..086ee986 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. diff --git a/Sources/AblyLiveObjects/Utility/APLogger+Swift.swift b/Sources/AblyLiveObjects/Utility/APLogger+Swift.swift deleted file mode 100644 index 276621d1..00000000 --- a/Sources/AblyLiveObjects/Utility/APLogger+Swift.swift +++ /dev/null @@ -1,8 +0,0 @@ -internal import AblyPlugin - -internal extension AblyPlugin.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 941d3e8b..1ab2e661 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 e9fbf705..4492cf6a 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 00000000..94cfca9a --- /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 00000000..28bf619c --- /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 e2e6f5d8..6e82a258 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 objectFromPluginSupportData(_ 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 e71959de..feaa8719 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/Test.swift b/Tests/AblyLiveObjectsTests/Helpers/Test.swift new file mode 100644 index 00000000..10f138e0 --- /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/TestFactories.swift b/Tests/AblyLiveObjectsTests/Helpers/TestFactories.swift index 441bf12f..89b2ddb5 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 59c51e17..4e6cf71e 100644 --- a/Tests/AblyLiveObjectsTests/Helpers/TestLogger.swift +++ b/Tests/AblyLiveObjectsTests/Helpers/TestLogger.swift @@ -1,23 +1,24 @@ -import AblyPlugin +import _AblyPluginSupportPrivate +@testable import AblyLiveObjects 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 `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() - 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 039ec517..048a64ce 100644 --- a/Tests/AblyLiveObjectsTests/InternalDefaultLiveCounterTests.swift +++ b/Tests/AblyLiveObjectsTests/InternalDefaultLiveCounterTests.swift @@ -1,5 +1,6 @@ +import _AblyPluginSupportPrivate +import Ably @testable import AblyLiveObjects -import AblyPlugin 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 9d982e99..cf5c68a7 100644 --- a/Tests/AblyLiveObjectsTests/InternalDefaultLiveMapTests.swift +++ b/Tests/AblyLiveObjectsTests/InternalDefaultLiveMapTests.swift @@ -1,5 +1,6 @@ +import _AblyPluginSupportPrivate +import Ably @testable import AblyLiveObjects -import AblyPlugin 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()) @@ -92,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 { @@ -104,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 { @@ -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) @@ -411,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 ], @@ -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 b4bedee9..566b2a0b 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`. @@ -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")] @@ -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")), @@ -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) @@ -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/JS Integration Tests/ObjectsHelper.swift b/Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsHelper.swift index 9f6671e5..77b78e16 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/JS Integration Tests/ObjectsIntegrationTests.swift b/Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsIntegrationTests.swift index 016f06c0..996d8991 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( @@ -213,26 +229,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 +295,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) @@ -318,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. @@ -2916,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() { @@ -3196,7 +3303,7 @@ private struct ObjectsIntegrationTests { }() } - @Test(arguments: FirstSetOfScenarios.testCases) + @Test(.concurrencyLimit5, arguments: FirstSetOfScenarios.testCases) func firstSetOfScenarios(testCase: TestCase) async throws { guard !testCase.disabled else { withKnownIssue { @@ -3205,27 +3312,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, + ), + ) + } } } @@ -3651,7 +3764,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 { @@ -3660,59 +3773,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, + ), + ) + } } } @@ -3728,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] = [ @@ -3741,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) @@ -3779,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( @@ -3798,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 = { @@ -3853,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 @@ -3868,7 +3989,7 @@ private struct ObjectsIntegrationTests { ] } - @Test(arguments: TombstonesGCScenarios.testCases) + @Test(.concurrencyLimit5, arguments: TombstonesGCScenarios.testCases) func tombstonesGCScenarios(testCase: TestCase) async throws { guard !testCase.disabled else { withKnownIssue { @@ -3877,48 +3998,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 - 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. + var options = testCase.options(for: testCaseExecution) + let garbageCollectionOptions = InternalDefaultRealtimeObjects.GarbageCollectionOptions( + interval: 0.5, gracePeriod: 0.25, ) - - 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 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 - - var gcCalledTimes = 0 - for await _ in gcEvents { - gcCalledTimes += 1 - if gcCalledTimes >= cycles { - break + options.garbageCollectionOptions = garbageCollectionOptions + + 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(channelName, options: channelOptionsWithObjects()) + let objects = channel.objects + + try await channel.attachAsync() + let root = try await objects.getRoot() + + // 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 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( - .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, + waitForTombstonedObjectsToBeCollected: waitForTombstonedObjectsToBeCollected, + ), + ) + } } } } diff --git a/Tests/AblyLiveObjectsTests/LiveObjectMutableStateTests.swift b/Tests/AblyLiveObjectsTests/LiveObjectMutableStateTests.swift index ff39d7d4..5b304bc0 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`. @@ -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) @@ -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/Mocks/MockCoreSDK.swift b/Tests/AblyLiveObjectsTests/Mocks/MockCoreSDK.swift index 3bb48bbf..0c3980a6 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/Tests/AblyLiveObjectsTests/ObjectLifetimesTests.swift b/Tests/AblyLiveObjectsTests/ObjectLifetimesTests.swift index 23c84980..9c83e4d8 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 a973dea7..57a913cf 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 @@ -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/Tests/AblyLiveObjectsTests/ObjectsPoolTests.swift b/Tests/AblyLiveObjectsTests/ObjectsPoolTests.swift index 89514d5f..8a350029 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 16c28f12..d2228142 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 91e557e0..74a31d32 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 5096ca37..a6468354 160000 --- a/ably-cocoa +++ b/ably-cocoa @@ -1 +1 @@ -Subproject commit 5096ca37c6c39f8f33e261f49faf2a6f9d03e529 +Subproject commit a64683548c4147d8de06afbdd89bb2d8540f547e diff --git a/analyze-logs.rb b/analyze-logs.rb new file mode 100755 index 00000000..7d79f32f --- /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 diff --git a/package-lock.json b/package-lock.json index 59e4ef29..7ac4a172 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 fb991e12..94eb3492 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" },