Skip to content

Commit 2d11ea0

Browse files
Merge pull request #23 from ably/ECO-5333-apply-OBJECT
[ECO-5333] Apply `OBJECT` `ProtocolMessage` operations
2 parents 36982fe + 6430358 commit 2d11ea0

11 files changed

Lines changed: 1651 additions & 251 deletions

Sources/AblyLiveObjects/DefaultRealtimeObjects.swift

Lines changed: 88 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ internal final class DefaultRealtimeObjects: RealtimeObjects, LiveMapObjectPoolD
7373
(receivedObjectProtocolMessages, receivedObjectProtocolMessagesContinuation) = AsyncStream.makeStream()
7474
(receivedObjectSyncProtocolMessages, receivedObjectSyncProtocolMessagesContinuation) = AsyncStream.makeStream()
7575
(waitingForSyncEvents, waitingForSyncEventsContinuation) = AsyncStream.makeStream()
76-
mutableState = .init(objectsPool: .init(rootDelegate: self, rootCoreSDK: coreSDK))
76+
mutableState = .init(objectsPool: .init(rootDelegate: self, rootCoreSDK: coreSDK, logger: logger))
7777
}
7878

7979
// MARK: - LiveMapObjectPoolDelegate
@@ -166,8 +166,17 @@ internal final class DefaultRealtimeObjects: RealtimeObjects, LiveMapObjectPoolD
166166
receivedObjectProtocolMessages
167167
}
168168

169+
/// Implements the `OBJECT` handling of RTO8.
169170
internal func handleObjectProtocolMessage(objectMessages: [InboundObjectMessage]) {
170-
receivedObjectProtocolMessagesContinuation.yield(objectMessages)
171+
mutex.withLock {
172+
mutableState.handleObjectProtocolMessage(
173+
objectMessages: objectMessages,
174+
logger: logger,
175+
receivedObjectProtocolMessagesContinuation: receivedObjectProtocolMessagesContinuation,
176+
mapDelegate: self,
177+
coreSDK: coreSDK,
178+
)
179+
}
171180
}
172181

173182
internal var testsOnly_receivedObjectSyncProtocolMessages: AsyncStream<[InboundObjectMessage]> {
@@ -193,7 +202,7 @@ internal final class DefaultRealtimeObjects: RealtimeObjects, LiveMapObjectPoolD
193202
/// Intended as a way for tests to populate the object pool.
194203
internal func testsOnly_createZeroValueLiveObject(forObjectID objectID: String, coreSDK: CoreSDK) -> ObjectsPool.Entry? {
195204
mutex.withLock {
196-
mutableState.objectsPool.createZeroValueObject(forObjectID: objectID, mapDelegate: self, coreSDK: coreSDK)
205+
mutableState.objectsPool.createZeroValueObject(forObjectID: objectID, mapDelegate: self, coreSDK: coreSDK, logger: logger)
197206
}
198207
}
199208

@@ -242,7 +251,7 @@ internal final class DefaultRealtimeObjects: RealtimeObjects, LiveMapObjectPoolD
242251

243252
// RTO4b1, RTO4b2: Reset the ObjectsPool to have a single empty root object
244253
// 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
245-
objectsPool = .init(rootDelegate: mapDelegate, rootCoreSDK: coreSDK)
254+
objectsPool = .init(rootDelegate: mapDelegate, rootCoreSDK: coreSDK, logger: logger)
246255

247256
// 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.
248257

@@ -320,5 +329,80 @@ internal final class DefaultRealtimeObjects: RealtimeObjects, LiveMapObjectPoolD
320329
syncStatus.signalSyncComplete()
321330
}
322331
}
332+
333+
/// Implements the `OBJECT` handling of RTO8.
334+
internal mutating func handleObjectProtocolMessage(
335+
objectMessages: [InboundObjectMessage],
336+
logger: Logger,
337+
receivedObjectProtocolMessagesContinuation: AsyncStream<[InboundObjectMessage]>.Continuation,
338+
mapDelegate: LiveMapObjectPoolDelegate,
339+
coreSDK: CoreSDK,
340+
) {
341+
receivedObjectProtocolMessagesContinuation.yield(objectMessages)
342+
343+
logger.log("handleObjectProtocolMessage(objectMessages: \(objectMessages))", level: .debug)
344+
345+
// TODO: RTO8a's buffering
346+
347+
// RTO8b
348+
for objectMessage in objectMessages {
349+
applyObjectProtocolMessageObjectMessage(
350+
objectMessage,
351+
logger: logger,
352+
mapDelegate: mapDelegate,
353+
coreSDK: coreSDK,
354+
)
355+
}
356+
}
357+
358+
/// Implements the `OBJECT` application of RTO9.
359+
private mutating func applyObjectProtocolMessageObjectMessage(
360+
_ objectMessage: InboundObjectMessage,
361+
logger: Logger,
362+
mapDelegate: LiveMapObjectPoolDelegate,
363+
coreSDK: CoreSDK,
364+
) {
365+
guard let operation = objectMessage.operation else {
366+
// RTO9a1
367+
logger.log("Unsupported OBJECT message received (no operation); \(objectMessage)", level: .warn)
368+
return
369+
}
370+
371+
// RTO9a2a1, RTO9a2a2
372+
let entry: ObjectsPool.Entry
373+
if let existingEntry = objectsPool.entries[operation.objectId] {
374+
entry = existingEntry
375+
} else {
376+
guard let newEntry = objectsPool.createZeroValueObject(
377+
forObjectID: operation.objectId,
378+
mapDelegate: mapDelegate,
379+
coreSDK: coreSDK,
380+
logger: logger,
381+
) else {
382+
logger.log("Unable to create zero-value object for \(operation.objectId) when processing OBJECT message; dropping", level: .warn)
383+
return
384+
}
385+
386+
entry = newEntry
387+
}
388+
389+
switch operation.action {
390+
case let .known(action):
391+
switch action {
392+
case .mapCreate, .mapSet, .mapRemove, .counterCreate, .counterInc, .objectDelete:
393+
// RTO9a2a3
394+
entry.apply(
395+
operation,
396+
objectMessageSerial: objectMessage.serial,
397+
objectMessageSiteCode: objectMessage.siteCode,
398+
objectsPool: &objectsPool,
399+
)
400+
}
401+
case let .unknown(rawValue):
402+
// RTO9a2b
403+
logger.log("Unsupported OBJECT operation action \(rawValue) received", level: .warn)
404+
return
405+
}
406+
}
323407
}
324408
}

Sources/AblyLiveObjects/Internal/DefaultLiveCounter.swift

Lines changed: 134 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Ably
2+
internal import AblyPlugin
23
import Foundation
34

45
/// Our default implementation of ``LiveCounter``.
@@ -8,57 +9,63 @@ internal final class DefaultLiveCounter: LiveCounter {
89

910
private nonisolated(unsafe) var mutableState: MutableState
1011

11-
internal var testsOnly_siteTimeserials: [String: String]? {
12+
internal var testsOnly_siteTimeserials: [String: String] {
1213
mutex.withLock {
13-
mutableState.siteTimeserials
14+
mutableState.liveObject.siteTimeserials
1415
}
1516
}
1617

17-
internal var testsOnly_createOperationIsMerged: Bool? {
18+
internal var testsOnly_createOperationIsMerged: Bool {
1819
mutex.withLock {
19-
mutableState.createOperationIsMerged
20+
mutableState.liveObject.createOperationIsMerged
2021
}
2122
}
2223

23-
internal var testsOnly_objectID: String? {
24+
internal var testsOnly_objectID: String {
2425
mutex.withLock {
25-
mutableState.objectID
26+
mutableState.liveObject.objectID
2627
}
2728
}
2829

2930
private let coreSDK: CoreSDK
31+
private let logger: AblyPlugin.Logger
3032

3133
// MARK: - Initialization
3234

3335
internal convenience init(
3436
testsOnly_data data: Double,
35-
objectID: String?,
36-
coreSDK: CoreSDK
37+
objectID: String,
38+
coreSDK: CoreSDK,
39+
logger: AblyPlugin.Logger
3740
) {
38-
self.init(data: data, objectID: objectID, coreSDK: coreSDK)
41+
self.init(data: data, objectID: objectID, coreSDK: coreSDK, logger: logger)
3942
}
4043

4144
private init(
4245
data: Double,
43-
objectID: String?,
44-
coreSDK: CoreSDK
46+
objectID: String,
47+
coreSDK: CoreSDK,
48+
logger: AblyPlugin.Logger
4549
) {
46-
mutableState = .init(data: data, objectID: objectID)
50+
mutableState = .init(liveObject: .init(objectID: objectID), data: data)
4751
self.coreSDK = coreSDK
52+
self.logger = logger
4853
}
4954

5055
/// Creates a "zero-value LiveCounter", per RTLC4.
5156
///
5257
/// - Parameters:
5358
/// - objectID: The value for the "private objectId field" of RTO5c1b1a.
5459
internal static func createZeroValued(
55-
objectID: String? = nil,
60+
objectID: String,
5661
coreSDK: CoreSDK,
62+
logger: AblyPlugin.Logger,
5763
) -> Self {
5864
.init(
5965
data: 0,
6066
objectID: objectID,
6167
coreSDK: coreSDK,
68+
logger: logger,
6269
)
6370
}
6471

@@ -116,41 +123,137 @@ internal final class DefaultLiveCounter: LiveCounter {
116123
}
117124
}
118125

126+
/// Test-only method to merge initial value from an ObjectOperation, per RTLC10.
127+
internal func testsOnly_mergeInitialValue(from operation: ObjectOperation) {
128+
mutex.withLock {
129+
mutableState.mergeInitialValue(from: operation)
130+
}
131+
}
132+
133+
/// Test-only method to apply a COUNTER_CREATE operation, per RTLC8.
134+
internal func testsOnly_applyCounterCreateOperation(_ operation: ObjectOperation) {
135+
mutex.withLock {
136+
mutableState.applyCounterCreateOperation(operation, logger: logger)
137+
}
138+
}
139+
140+
/// Test-only method to apply a COUNTER_INC operation, per RTLC9.
141+
internal func testsOnly_applyCounterIncOperation(_ operation: WireObjectsCounterOp?) {
142+
mutex.withLock {
143+
mutableState.applyCounterIncOperation(operation)
144+
}
145+
}
146+
147+
/// Attempts to apply an operation from an inbound `ObjectMessage`, per RTLC7.
148+
internal func apply(
149+
_ operation: ObjectOperation,
150+
objectMessageSerial: String?,
151+
objectMessageSiteCode: String?,
152+
objectsPool: inout ObjectsPool,
153+
) {
154+
mutex.withLock {
155+
mutableState.apply(
156+
operation,
157+
objectMessageSerial: objectMessageSerial,
158+
objectMessageSiteCode: objectMessageSiteCode,
159+
objectsPool: &objectsPool,
160+
logger: logger,
161+
)
162+
}
163+
}
164+
119165
// MARK: - Mutable state and the operations that affect it
120166

121167
private struct MutableState {
168+
/// The mutable state common to all LiveObjects.
169+
internal var liveObject: LiveObjectMutableState
170+
122171
/// The internal data that this map holds, per RTLC3.
123172
internal var data: Double
124173

125-
/// The site timeserials for this counter, per RTLC6a.
126-
internal var siteTimeserials: [String: String]?
127-
128-
/// Whether the create operation has been merged, per RTLC6b and RTLC6d2.
129-
internal var createOperationIsMerged: Bool?
130-
131-
/// The "private `objectId` field" of RTO5c1b1a.
132-
internal var objectID: String?
133-
134174
/// Replaces the internal data of this counter with the provided ObjectState, per RTLC6.
135175
internal mutating func replaceData(using state: ObjectState) {
136176
// RTLC6a: Replace the private siteTimeserials with the value from ObjectState.siteTimeserials
137-
siteTimeserials = state.siteTimeserials
177+
liveObject.siteTimeserials = state.siteTimeserials
138178

139179
// RTLC6b: Set the private flag createOperationIsMerged to false
140-
createOperationIsMerged = false
180+
liveObject.createOperationIsMerged = false
141181

142182
// RTLC6c: Set data to the value of ObjectState.counter.count, or to 0 if it does not exist
143183
data = state.counter?.count?.doubleValue ?? 0
144184

145-
// RTLC6d: If ObjectState.createOp is present
185+
// RTLC6d: If ObjectState.createOp is present, merge the initial value into the LiveCounter as described in RTLC10
146186
if let createOp = state.createOp {
147-
// RTLC6d1: Add ObjectState.createOp.counter.count to data, if it exists
148-
if let createOpCount = createOp.counter?.count?.doubleValue {
149-
data += createOpCount
150-
}
151-
// RTLC6d2: Set the private flag createOperationIsMerged to true
152-
createOperationIsMerged = true
187+
mergeInitialValue(from: createOp)
188+
}
189+
}
190+
191+
/// Merges the initial value from an ObjectOperation into this LiveCounter, per RTLC10.
192+
internal mutating func mergeInitialValue(from operation: ObjectOperation) {
193+
// RTLC10a: Add ObjectOperation.counter.count to data, if it exists
194+
if let operationCount = operation.counter?.count?.doubleValue {
195+
data += operationCount
196+
}
197+
// RTLC10b: Set the private flag createOperationIsMerged to true
198+
liveObject.createOperationIsMerged = true
199+
}
200+
201+
/// Attempts to apply an operation from an inbound `ObjectMessage`, per RTLC7.
202+
internal mutating func apply(
203+
_ operation: ObjectOperation,
204+
objectMessageSerial: String?,
205+
objectMessageSiteCode: String?,
206+
objectsPool: inout ObjectsPool,
207+
logger: Logger,
208+
) {
209+
guard let applicableOperation = liveObject.canApplyOperation(objectMessageSerial: objectMessageSerial, objectMessageSiteCode: objectMessageSiteCode, logger: logger) else {
210+
// RTLC7b
211+
logger.log("Operation \(operation) (serial: \(String(describing: objectMessageSerial)), siteCode: \(String(describing: objectMessageSiteCode))) should not be applied; discarding", level: .debug)
212+
return
213+
}
214+
215+
// RTLC7c
216+
liveObject.siteTimeserials[applicableOperation.objectMessageSiteCode] = applicableOperation.objectMessageSerial
217+
218+
switch operation.action {
219+
case .known(.counterCreate):
220+
// RTLC7d1
221+
applyCounterCreateOperation(
222+
operation,
223+
logger: logger,
224+
)
225+
case .known(.counterInc):
226+
// RTLC7d2
227+
applyCounterIncOperation(operation.counterOp)
228+
default:
229+
// RTLC7d3
230+
logger.log("Operation \(operation) has unsupported action for LiveCounter; discarding", level: .warn)
231+
}
232+
}
233+
234+
/// Applies a `COUNTER_CREATE` operation, per RTLC8.
235+
internal mutating func applyCounterCreateOperation(
236+
_ operation: ObjectOperation,
237+
logger: Logger,
238+
) {
239+
if liveObject.createOperationIsMerged {
240+
// RTLC8b
241+
logger.log("Not applying COUNTER_CREATE because a COUNTER_CREATE has already been applied", level: .warn)
242+
return
153243
}
244+
245+
// RTLC8c
246+
mergeInitialValue(from: operation)
247+
}
248+
249+
/// Applies a `COUNTER_INC` operation, per RTLC9.
250+
internal mutating func applyCounterIncOperation(_ operation: WireObjectsCounterOp?) {
251+
guard let operation else {
252+
return
253+
}
254+
255+
// RTLC9b
256+
data += operation.amount.doubleValue
154257
}
155258
}
156259
}

0 commit comments

Comments
 (0)