Skip to content

Commit bc814fb

Browse files
wip implement pattern
TODO next up: we need to also keep alive things that we're depending on in order to be delivered events — or are we doing this already just by keeping the channel around which thus keeps objects around? TODO next up write some tests Note that we have been able to remove the "keep channel alive" code from integration tests now (TODO I still don't understand why the channel would have been dying before; wouldn't the realtime instance have kept it around? Ah, no — I think that we could have probably even removed it in the previous PR, because the tests have a reference to the realtime instance) TODO make sure that we're only passing what's necessary dependency-wise; there might be some leftovers TODO make sure that we document what you need to not hold a strong reference to TODO note that we can keep our own objects and the channel alive for as long as we want given the public API, but then we're stuck after that — i.e. if keeping the channel alive doesn't keep the connection alive (not sure if it does) then there's not a whole lot we can do TODO address the stuff we did internally (keeping a reference to the public type) TODO remove the "default implementation" stuff TODO documentation TODO note that there are no weak references inside the codebase at the moment, if we need them we'll add them explain that the public classes are boring and that they just pass dependencies and map to public note that we get away with not passing CoreSDK because it's actually barely used (just for checking channel state so far) it's nice to in general not have to worry whether various properties are nil or not — that said, I guess the worry is that it's easier to accidentally capture something strongly, we'll have to think about what to do there maybe conflating channel and plugin into a single type was a mistake, because it'll get in the way whenever we want to do something non-channel related Asked Cursor to fix the tests for me — TODO check TODO umm but how is this going to work when we need to send something ove the channel? I feel like I've missed something important here? ah, no — if you want to publish, then the public will give you the CoreSDK that you need
1 parent 836f502 commit bc814fb

21 files changed

Lines changed: 915 additions & 513 deletions

Sources/AblyLiveObjects/DefaultRealtimeObjects.swift

Lines changed: 8 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,12 @@ import Ably
22
internal import AblyPlugin
33

44
/// The class that provides the public API for interacting with LiveObjects, via the ``ARTRealtimeChannel/objects`` property.
5-
internal final class DefaultRealtimeObjects: RealtimeObjects, LiveMapObjectPoolDelegate {
5+
internal final class DefaultRealtimeObjects: Sendable, LiveMapObjectPoolDelegate {
66
// 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.
77
private let mutex = NSLock()
88

99
private nonisolated(unsafe) var mutableState: MutableState!
1010

11-
private let coreSDK: CoreSDK
1211
private let logger: AblyPlugin.Logger
1312

1413
// These drive the testsOnly_* properties that expose the received ProtocolMessages to the test suite.
@@ -70,13 +69,12 @@ internal final class DefaultRealtimeObjects: RealtimeObjects, LiveMapObjectPoolD
7069
}
7170
}
7271

73-
internal init(coreSDK: CoreSDK, logger: AblyPlugin.Logger) {
74-
self.coreSDK = coreSDK
72+
internal init(logger: AblyPlugin.Logger) {
7573
self.logger = logger
7674
(receivedObjectProtocolMessages, receivedObjectProtocolMessagesContinuation) = AsyncStream.makeStream()
7775
(receivedObjectSyncProtocolMessages, receivedObjectSyncProtocolMessagesContinuation) = AsyncStream.makeStream()
7876
(waitingForSyncEvents, waitingForSyncEventsContinuation) = AsyncStream.makeStream()
79-
mutableState = .init(objectsPool: .init(rootDelegate: self, rootCoreSDK: coreSDK, logger: logger))
77+
mutableState = .init(objectsPool: .init(logger: logger))
8078
}
8179

8280
// MARK: - LiveMapObjectPoolDelegate
@@ -89,7 +87,7 @@ internal final class DefaultRealtimeObjects: RealtimeObjects, LiveMapObjectPoolD
8987

9088
// MARK: `RealtimeObjects` protocol
9189

92-
internal func getRoot() async throws(ARTErrorInfo) -> any LiveMap {
90+
internal func getRoot(coreSDK: CoreSDK) async throws(ARTErrorInfo) -> DefaultLiveMap {
9391
// RTO1b: If the channel is in the DETACHED or FAILED state, the library should indicate an error with code 90001
9492
let currentChannelState = coreSDK.channelState
9593
if currentChannelState == .detached || currentChannelState == .failed {
@@ -159,8 +157,6 @@ internal final class DefaultRealtimeObjects: RealtimeObjects, LiveMapObjectPoolD
159157
mutableState.onChannelAttached(
160158
hasObjects: hasObjects,
161159
logger: logger,
162-
mapDelegate: self,
163-
coreSDK: coreSDK,
164160
)
165161
}
166162
}
@@ -176,8 +172,6 @@ internal final class DefaultRealtimeObjects: RealtimeObjects, LiveMapObjectPoolD
176172
objectMessages: objectMessages,
177173
logger: logger,
178174
receivedObjectProtocolMessagesContinuation: receivedObjectProtocolMessagesContinuation,
179-
mapDelegate: self,
180-
coreSDK: coreSDK,
181175
)
182176
}
183177
}
@@ -194,25 +188,23 @@ internal final class DefaultRealtimeObjects: RealtimeObjects, LiveMapObjectPoolD
194188
protocolMessageChannelSerial: protocolMessageChannelSerial,
195189
logger: logger,
196190
receivedObjectSyncProtocolMessagesContinuation: receivedObjectSyncProtocolMessagesContinuation,
197-
mapDelegate: self,
198-
coreSDK: coreSDK,
199191
)
200192
}
201193
}
202194

203195
/// Creates a zero-value LiveObject in the object pool for this object ID.
204196
///
205197
/// Intended as a way for tests to populate the object pool.
206-
internal func testsOnly_createZeroValueLiveObject(forObjectID objectID: String, coreSDK: CoreSDK) -> ObjectsPool.Entry? {
198+
internal func testsOnly_createZeroValueLiveObject(forObjectID objectID: String) -> ObjectsPool.Entry? {
207199
mutex.withLock {
208-
mutableState.objectsPool.createZeroValueObject(forObjectID: objectID, mapDelegate: self, coreSDK: coreSDK, logger: logger)
200+
mutableState.objectsPool.createZeroValueObject(forObjectID: objectID, logger: logger)
209201
}
210202
}
211203

212204
// MARK: - Sending `OBJECT` ProtocolMessage
213205

214206
// 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.
215-
internal func testsOnly_sendObject(objectMessages: [OutboundObjectMessage]) async throws(InternalError) {
207+
internal func testsOnly_sendObject(objectMessages: [OutboundObjectMessage], coreSDK: CoreSDK) async throws(InternalError) {
216208
try await coreSDK.sendObject(objectMessages: objectMessages)
217209
}
218210

@@ -241,8 +233,6 @@ internal final class DefaultRealtimeObjects: RealtimeObjects, LiveMapObjectPoolD
241233
internal mutating func onChannelAttached(
242234
hasObjects: Bool,
243235
logger: Logger,
244-
mapDelegate: LiveMapObjectPoolDelegate,
245-
coreSDK: CoreSDK,
246236
) {
247237
logger.log("onChannelAttached(hasObjects: \(hasObjects)", level: .debug)
248238

@@ -255,7 +245,7 @@ internal final class DefaultRealtimeObjects: RealtimeObjects, LiveMapObjectPoolD
255245

256246
// RTO4b1, RTO4b2: Reset the ObjectsPool to have a single empty root object
257247
// TODO: this one is unclear (are we meant to replace the root or just clear its data?) https://github.com/ably/specification/pull/333/files#r2183493458
258-
objectsPool = .init(rootDelegate: mapDelegate, rootCoreSDK: coreSDK, logger: logger)
248+
objectsPool = .init(logger: logger)
259249

260250
// I have, for now, not directly implemented the "perform the actions for object sync completion" of RTO4b4 since my implementation doesn't quite match the model given there; here you only have a SyncObjectsPool if you have an OBJECT_SYNC in progress, which you might not have upon receiving an ATTACHED. Instead I've just implemented what seem like the relevant side effects. Can revisit this if "the actions for object sync completion" get more complex.
261251

@@ -270,8 +260,6 @@ internal final class DefaultRealtimeObjects: RealtimeObjects, LiveMapObjectPoolD
270260
protocolMessageChannelSerial: String?,
271261
logger: Logger,
272262
receivedObjectSyncProtocolMessagesContinuation: AsyncStream<[InboundObjectMessage]>.Continuation,
273-
mapDelegate: LiveMapObjectPoolDelegate,
274-
coreSDK: CoreSDK,
275263
) {
276264
logger.log("handleObjectSyncProtocolMessage(objectMessages: \(objectMessages), protocolMessageChannelSerial: \(String(describing: protocolMessageChannelSerial)))", level: .debug)
277265

@@ -326,8 +314,6 @@ internal final class DefaultRealtimeObjects: RealtimeObjects, LiveMapObjectPoolD
326314
// RTO5c
327315
objectsPool.applySyncObjectsPool(
328316
completedSyncObjectsPool,
329-
mapDelegate: mapDelegate,
330-
coreSDK: coreSDK,
331317
logger: logger,
332318
)
333319

@@ -338,8 +324,6 @@ internal final class DefaultRealtimeObjects: RealtimeObjects, LiveMapObjectPoolD
338324
applyObjectProtocolMessageObjectMessage(
339325
objectMessage,
340326
logger: logger,
341-
mapDelegate: mapDelegate,
342-
coreSDK: coreSDK,
343327
)
344328
}
345329
}
@@ -356,8 +340,6 @@ internal final class DefaultRealtimeObjects: RealtimeObjects, LiveMapObjectPoolD
356340
objectMessages: [InboundObjectMessage],
357341
logger: Logger,
358342
receivedObjectProtocolMessagesContinuation: AsyncStream<[InboundObjectMessage]>.Continuation,
359-
mapDelegate: LiveMapObjectPoolDelegate,
360-
coreSDK: CoreSDK,
361343
) {
362344
receivedObjectProtocolMessagesContinuation.yield(objectMessages)
363345

@@ -375,8 +357,6 @@ internal final class DefaultRealtimeObjects: RealtimeObjects, LiveMapObjectPoolD
375357
applyObjectProtocolMessageObjectMessage(
376358
objectMessage,
377359
logger: logger,
378-
mapDelegate: mapDelegate,
379-
coreSDK: coreSDK,
380360
)
381361
}
382362
}
@@ -386,8 +366,6 @@ internal final class DefaultRealtimeObjects: RealtimeObjects, LiveMapObjectPoolD
386366
private mutating func applyObjectProtocolMessageObjectMessage(
387367
_ objectMessage: InboundObjectMessage,
388368
logger: Logger,
389-
mapDelegate: LiveMapObjectPoolDelegate,
390-
coreSDK: CoreSDK,
391369
) {
392370
guard let operation = objectMessage.operation else {
393371
// RTO9a1
@@ -402,8 +380,6 @@ internal final class DefaultRealtimeObjects: RealtimeObjects, LiveMapObjectPoolD
402380
} else {
403381
guard let newEntry = objectsPool.createZeroValueObject(
404382
forObjectID: operation.objectId,
405-
mapDelegate: mapDelegate,
406-
coreSDK: coreSDK,
407383
logger: logger,
408384
) else {
409385
logger.log("Unable to create zero-value object for \(operation.objectId) when processing OBJECT message; dropping", level: .warn)

Sources/AblyLiveObjects/Internal/CoreSDK.swift

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,29 +12,17 @@ internal protocol CoreSDK: AnyObject, Sendable {
1212
}
1313

1414
internal final class DefaultCoreSDK: CoreSDK {
15-
// 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.
16-
private let weakChannel: WeakRef<AblyPlugin.RealtimeChannel>
15+
private let channel: AblyPlugin.RealtimeChannel
1716
private let pluginAPI: PluginAPIProtocol
1817

1918
internal init(
2019
channel: AblyPlugin.RealtimeChannel,
2120
pluginAPI: PluginAPIProtocol
2221
) {
23-
weakChannel = .init(referenced: channel)
22+
self.channel = channel
2423
self.pluginAPI = pluginAPI
2524
}
2625

27-
// MARK: - Fetching channel
28-
29-
private var channel: AblyPlugin.RealtimeChannel {
30-
guard let channel = weakChannel.referenced else {
31-
// 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.
32-
preconditionFailure("Expected channel to not become deallocated during usage of LiveObjects SDK")
33-
}
34-
35-
return channel
36-
}
37-
3826
// MARK: - CoreSDK conformance
3927

4028
internal func sendObject(objectMessages: [OutboundObjectMessage]) async throws(InternalError) {

Sources/AblyLiveObjects/Internal/DefaultInternalPlugin.swift

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,10 @@ internal final class DefaultInternalPlugin: NSObject, AblyPlugin.LiveObjectsInte
1717
/// The `pluginDataValue(forKey:channel:)` key that we use to store the value of the `ARTRealtimeChannel.objects` property.
1818
private static let pluginDataKey = "LiveObjects"
1919

20-
/// Retrieves the value that should be returned by `ARTRealtimeChannel.objects`.
21-
///
22-
/// We expect this value to have been previously set by ``prepare(_:)``.
23-
internal static func objectsProperty(for channel: ARTRealtimeChannel, pluginAPI: AblyPlugin.PluginAPIProtocol) -> DefaultRealtimeObjects {
24-
let pluginChannel = pluginAPI.channel(forPublicRealtimeChannel: channel)
25-
return realtimeObjects(for: pluginChannel, pluginAPI: pluginAPI)
26-
}
27-
2820
/// Retrieves the `RealtimeObjects` for this channel.
2921
///
3022
/// We expect this value to have been previously set by ``prepare(_:)``.
31-
private static func realtimeObjects(for channel: AblyPlugin.RealtimeChannel, pluginAPI: AblyPlugin.PluginAPIProtocol) -> DefaultRealtimeObjects {
23+
internal static func realtimeObjects(for channel: AblyPlugin.RealtimeChannel, pluginAPI: AblyPlugin.PluginAPIProtocol) -> DefaultRealtimeObjects {
3224
guard let pluginData = pluginAPI.pluginDataValue(forKey: pluginDataKey, channel: channel) else {
3325
// InternalPlugin.prepare was not called
3426
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]`")
@@ -45,8 +37,7 @@ internal final class DefaultInternalPlugin: NSObject, AblyPlugin.LiveObjectsInte
4537
let logger = pluginAPI.logger(for: channel)
4638

4739
logger.log("LiveObjects.DefaultInternalPlugin received prepare(_:)", level: .debug)
48-
let coreSDK = DefaultCoreSDK(channel: channel, pluginAPI: pluginAPI)
49-
let liveObjects = DefaultRealtimeObjects(coreSDK: coreSDK, logger: logger)
40+
let liveObjects = DefaultRealtimeObjects(logger: logger)
5041
pluginAPI.setPluginDataValue(liveObjects, forKey: Self.pluginDataKey, channel: channel)
5142
}
5243

Sources/AblyLiveObjects/Internal/DefaultLiveCounter.swift

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ internal import AblyPlugin
33
import Foundation
44

55
/// Our default implementation of ``LiveCounter``.
6-
internal final class DefaultLiveCounter: LiveCounter {
6+
internal final class DefaultLiveCounter: Sendable {
77
// 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.
88
private let mutex = NSLock()
99

@@ -27,28 +27,24 @@ internal final class DefaultLiveCounter: LiveCounter {
2727
}
2828
}
2929

30-
private let coreSDK: CoreSDK
3130
private let logger: AblyPlugin.Logger
3231

3332
// MARK: - Initialization
3433

3534
internal convenience init(
3635
testsOnly_data data: Double,
3736
objectID: String,
38-
coreSDK: CoreSDK,
3937
logger: AblyPlugin.Logger
4038
) {
41-
self.init(data: data, objectID: objectID, coreSDK: coreSDK, logger: logger)
39+
self.init(data: data, objectID: objectID, logger: logger)
4240
}
4341

4442
private init(
4543
data: Double,
4644
objectID: String,
47-
coreSDK: CoreSDK,
4845
logger: AblyPlugin.Logger
4946
) {
5047
mutableState = .init(liveObject: .init(objectID: objectID), data: data)
51-
self.coreSDK = coreSDK
5248
self.logger = logger
5349
}
5450

@@ -58,35 +54,31 @@ internal final class DefaultLiveCounter: LiveCounter {
5854
/// - objectID: The value for the "private objectId field" of RTO5c1b1a.
5955
internal static func createZeroValued(
6056
objectID: String,
61-
coreSDK: CoreSDK,
6257
logger: AblyPlugin.Logger,
6358
) -> Self {
6459
.init(
6560
data: 0,
6661
objectID: objectID,
67-
coreSDK: coreSDK,
6862
logger: logger,
6963
)
7064
}
7165

7266
// MARK: - LiveCounter conformance
7367

74-
internal var value: Double {
75-
get throws(ARTErrorInfo) {
76-
// RTLC5b: If the channel is in the DETACHED or FAILED state, the library should indicate an error with code 90001
77-
let currentChannelState = coreSDK.channelState
78-
if currentChannelState == .detached || currentChannelState == .failed {
79-
throw LiveObjectsError.objectsOperationFailedInvalidChannelState(
80-
operationDescription: "LiveCounter.value",
81-
channelState: currentChannelState,
82-
)
83-
.toARTErrorInfo()
84-
}
68+
internal func value(coreSDK: CoreSDK) throws(ARTErrorInfo) -> Double {
69+
// RTLC5b: If the channel is in the DETACHED or FAILED state, the library should indicate an error with code 90001
70+
let currentChannelState = coreSDK.channelState
71+
if currentChannelState == .detached || currentChannelState == .failed {
72+
throw LiveObjectsError.objectsOperationFailedInvalidChannelState(
73+
operationDescription: "LiveCounter.value",
74+
channelState: currentChannelState,
75+
)
76+
.toARTErrorInfo()
77+
}
8578

86-
return mutex.withLock {
87-
// RTLC5c
88-
mutableState.data
89-
}
79+
return mutex.withLock {
80+
// RTLC5c
81+
mutableState.data
9082
}
9183
}
9284

0 commit comments

Comments
 (0)