Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 22 additions & 21 deletions Sources/AblyLiveObjects/DefaultRealtimeObjects.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,22 @@ import Ably
internal import AblyPlugin

/// The class that provides the public API for interacting with LiveObjects, via the ``ARTRealtimeChannel/objects`` property.
internal class DefaultRealtimeObjects: RealtimeObjects {
private weak var channel: ARTRealtimeChannel?
internal final class DefaultRealtimeObjects: RealtimeObjects {
// 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.
private let mutex = NSLock()

private let coreSDK: CoreSDK
private let logger: AblyPlugin.Logger
private let pluginAPI: AblyPlugin.PluginAPIProtocol

// These drive the testsOnly_* properties that expose the received ProtocolMessages to the test suite.
private let receivedObjectProtocolMessages: AsyncStream<[InboundObjectMessage]>
private let receivedObjectProtocolMessagesContinuation: AsyncStream<[InboundObjectMessage]>.Continuation
private let receivedObjectSyncProtocolMessages: AsyncStream<[InboundObjectMessage]>
private let receivedObjectSyncProtocolMessagesContinuation: AsyncStream<[InboundObjectMessage]>.Continuation

internal init(channel: ARTRealtimeChannel, logger: AblyPlugin.Logger, pluginAPI: AblyPlugin.PluginAPIProtocol) {
self.channel = channel
internal init(coreSDK: CoreSDK, logger: AblyPlugin.Logger) {
self.coreSDK = coreSDK
self.logger = logger
self.pluginAPI = pluginAPI
(receivedObjectProtocolMessages, receivedObjectProtocolMessagesContinuation) = AsyncStream.makeStream()
(receivedObjectSyncProtocolMessages, receivedObjectSyncProtocolMessagesContinuation) = AsyncStream.makeStream()
}
Expand All @@ -27,23 +28,23 @@ internal class DefaultRealtimeObjects: RealtimeObjects {
notYetImplemented()
}

internal func createMap(entries _: any LiveMap) async throws(ARTErrorInfo) -> any LiveMap {
internal func createMap(entries _: [String: LiveMapValue]) async throws(ARTErrorInfo) -> any LiveMap {
notYetImplemented()
}

internal func createMap() async throws(ARTErrorInfo) -> any LiveMap {
notYetImplemented()
}

internal func createCounter(count _: Int) async throws(ARTErrorInfo) -> any LiveCounter {
internal func createCounter(count _: Double) async throws(ARTErrorInfo) -> any LiveCounter {
notYetImplemented()
}

internal func createCounter() async throws(ARTErrorInfo) -> any LiveCounter {
notYetImplemented()
}

internal func batch(callback _: (any BatchContext) -> Void) async throws {
internal func batch(callback _: sending (sending any BatchContext) -> Void) async throws {
notYetImplemented()
}

Expand All @@ -57,9 +58,17 @@ internal class DefaultRealtimeObjects: RealtimeObjects {

// MARK: Handling channel events

internal private(set) var testsOnly_onChannelAttachedHasObjects: Bool?
private nonisolated(unsafe) var onChannelAttachedHasObjects: Bool?
internal var testsOnly_onChannelAttachedHasObjects: Bool? {
mutex.withLock {
onChannelAttachedHasObjects
}
}

internal func onChannelAttached(hasObjects: Bool) {
testsOnly_onChannelAttachedHasObjects = hasObjects
mutex.withLock {
onChannelAttachedHasObjects = hasObjects
}
}

internal var testsOnly_receivedObjectProtocolMessages: AsyncStream<[InboundObjectMessage]> {
Expand All @@ -74,22 +83,14 @@ internal class DefaultRealtimeObjects: RealtimeObjects {
receivedObjectSyncProtocolMessages
}

internal func handleObjectSyncProtocolMessage(objectMessages: [InboundObjectMessage], protocolMessageChannelSerial _: String) {
internal func handleObjectSyncProtocolMessage(objectMessages: [InboundObjectMessage], protocolMessageChannelSerial _: String?) {
receivedObjectSyncProtocolMessagesContinuation.yield(objectMessages)
}

// MARK: - Sending `OBJECT` ProtocolMessage

// This is currently exposed so that we can try calling it from the tests in the early days of the SDK to check that we can send an OBJECT ProtocolMessage. We'll probably make it private later on.
internal func testsOnly_sendObject(objectMessages: [OutboundObjectMessage]) async throws(InternalError) {
guard let channel else {
return
}

try await DefaultInternalPlugin.sendObject(
objectMessages: objectMessages,
channel: channel,
pluginAPI: pluginAPI,
)
try await coreSDK.sendObject(objectMessages: objectMessages)
}
}
44 changes: 44 additions & 0 deletions Sources/AblyLiveObjects/Internal/CoreSDK.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
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).
internal protocol CoreSDK: AnyObject, Sendable {
func sendObject(objectMessages: [OutboundObjectMessage]) async throws(InternalError)
}

internal final class DefaultCoreSDK: CoreSDK {
// We hold a weak reference to the channel so that `DefaultLiveObjects` can hold a strong reference to us without causing a strong reference cycle. We'll revisit this in https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/9.
private let weakChannel: WeakRef<ARTRealtimeChannel>
private let pluginAPI: PluginAPIProtocol

internal init(
channel: ARTRealtimeChannel,
pluginAPI: PluginAPIProtocol
) {
weakChannel = .init(referenced: channel)
self.pluginAPI = pluginAPI
}

// MARK: - Fetching channel

private var channel: ARTRealtimeChannel {
guard let channel = weakChannel.referenced else {
// It's currently completely possible that the channel _does_ become deallocated during the usage of the LiveObjects SDK; in https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/9 we'll figure out how to prevent this.
preconditionFailure("Expected channel to not become deallocated during usage of LiveObjects SDK")
}

return channel
}

// MARK: - CoreSDK conformance

internal func sendObject(objectMessages: [OutboundObjectMessage]) async throws(InternalError) {
try await DefaultInternalPlugin.sendObject(
objectMessages: objectMessages,
channel: channel,
pluginAPI: pluginAPI,
)
}
}
5 changes: 3 additions & 2 deletions Sources/AblyLiveObjects/Internal/DefaultInternalPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ internal final class DefaultInternalPlugin: NSObject, AblyPlugin.LiveObjectsInte
let logger = pluginAPI.logger(for: channel)

logger.log("LiveObjects.DefaultInternalPlugin received prepare(_:)", level: .debug)
let liveObjects = DefaultRealtimeObjects(channel: channel, logger: logger, pluginAPI: pluginAPI)
let coreSDK = DefaultCoreSDK(channel: channel, pluginAPI: pluginAPI)
let liveObjects = DefaultRealtimeObjects(coreSDK: coreSDK, logger: logger)
pluginAPI.setPluginDataValue(liveObjects, forKey: Self.pluginDataKey, channel: channel)
}

Expand Down Expand Up @@ -109,7 +110,7 @@ internal final class DefaultInternalPlugin: NSObject, AblyPlugin.LiveObjectsInte
)
}

internal func handleObjectSyncProtocolMessage(withObjectMessages publicObjectMessages: [any AblyPlugin.ObjectMessageProtocol], protocolMessageChannelSerial: String, channel: ARTRealtimeChannel) {
internal func handleObjectSyncProtocolMessage(withObjectMessages publicObjectMessages: [any AblyPlugin.ObjectMessageProtocol], protocolMessageChannelSerial: String?, channel: ARTRealtimeChannel) {
guard let inboundObjectMessageBoxes = publicObjectMessages as? [ObjectMessageBox<InboundObjectMessage>] else {
preconditionFailure("Expected to receive the same InboundObjectMessage type as we emit")
}
Expand Down
Loading