Skip to content

Commit d1047bc

Browse files
Merge pull request #111 from ably/AIT-255-fix-replace-data-events
[AIT-255] Fix events emitted upon sync
2 parents 8114e17 + 1185b08 commit d1047bc

File tree

7 files changed

+427
-14
lines changed

7 files changed

+427
-14
lines changed

Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -331,19 +331,23 @@ internal final class InternalDefaultLiveCounter: Sendable {
331331
return .update(.init(amount: -dataBeforeTombstoning))
332332
}
333333

334+
// RTLC6g: Store the current data value as previousData for use in RTLC6h
335+
let previousData = data
336+
334337
// RTLC6b: Set the private flag createOperationIsMerged to false
335338
liveObjectMutableState.createOperationIsMerged = false
336339

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

340343
// RTLC6d: If ObjectState.createOp is present, merge the initial value into the LiveCounter as described in RTLC10
341-
return if let createOp = state.createOp {
342-
mergeInitialValue(from: createOp)
343-
} else {
344-
// TODO: I assume this is what to do, clarify in https://github.com/ably/specification/pull/346/files#r2201363446
345-
.noop
344+
// Discard the LiveCounterUpdate object returned by the merge operation
345+
if let createOp = state.createOp {
346+
_ = mergeInitialValue(from: createOp)
346347
}
348+
349+
// RTLC6h: Calculate the diff between previousData and the current data per RTLC14
350+
return ObjectDiffHelpers.calculateCounterDiff(previousData: previousData, newData: data)
347351
}
348352

349353
/// Merges the initial value from an ObjectOperation into this LiveCounter, per RTLC10.

Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,9 @@ internal final class InternalDefaultLiveMap: Sendable {
468468
return .update(.init(update: dataBeforeTombstoning.mapValues { _ in .removed }))
469469
}
470470

471+
// RTLM6g: Store the current data value as previousData for use in RTLM6h
472+
let previousData = data
473+
471474
// RTLM6b: Set the private flag createOperationIsMerged to false
472475
liveObjectMutableState.createOperationIsMerged = false
473476

@@ -493,19 +496,20 @@ internal final class InternalDefaultLiveMap: Sendable {
493496
} ?? [:]
494497

495498
// RTLM6d: If ObjectState.createOp is present, merge the initial value into the LiveMap as described in RTLM17
496-
return if let createOp = state.createOp {
497-
mergeInitialValue(
499+
// Discard the LiveMapUpdate object returned by the merge operation
500+
if let createOp = state.createOp {
501+
_ = mergeInitialValue(
498502
from: createOp,
499503
objectsPool: &objectsPool,
500504
logger: logger,
501505
internalQueue: internalQueue,
502506
userCallbackQueue: userCallbackQueue,
503507
clock: clock,
504508
)
505-
} else {
506-
// TODO: I assume this is what to do, clarify in https://github.com/ably/specification/pull/346/files#r2201363446
507-
.noop
508509
}
510+
511+
// RTLM6h: Calculate the diff between previousData and the current data per RTLM22
512+
return ObjectDiffHelpers.calculateMapDiff(previousData: previousData, newData: data)
509513
}
510514

511515
/// Merges the initial value from an ObjectOperation into this LiveMap, per RTLM17.
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import Foundation
2+
3+
/// Helper methods for calculating diffs between LiveObject data values.
4+
internal enum ObjectDiffHelpers {
5+
/// Calculates the diff between two LiveCounter data values, per RTLC14.
6+
///
7+
/// - Parameters:
8+
/// - previousData: The previous `data` value (RTLC14a1).
9+
/// - newData: The new `data` value (RTLC14a2).
10+
/// - Returns: Per RTLC14b.
11+
internal static func calculateCounterDiff(
12+
previousData: Double,
13+
newData: Double,
14+
) -> LiveObjectUpdate<DefaultLiveCounterUpdate> {
15+
// RTLC14b
16+
.update(DefaultLiveCounterUpdate(amount: newData - previousData))
17+
}
18+
19+
/// Calculates the diff between two LiveMap data values, per RTLM22.
20+
///
21+
/// - Parameters:
22+
/// - previousData: The previous `data` value (RTLM22a1).
23+
/// - newData: The new `data` value (RTLM22a2).
24+
/// - Returns: Per RTLM22b.
25+
internal static func calculateMapDiff(
26+
previousData: [String: InternalObjectsMapEntry],
27+
newData: [String: InternalObjectsMapEntry],
28+
) -> LiveObjectUpdate<DefaultLiveMapUpdate> {
29+
// RTLM22b
30+
let previousNonTombstonedKeys = Set(previousData.filter { !$0.value.tombstone }.keys)
31+
let newNonTombstonedKeys = Set(newData.filter { !$0.value.tombstone }.keys)
32+
33+
var update: [String: LiveMapUpdateAction] = [:]
34+
35+
// RTLM22b1
36+
for key in previousNonTombstonedKeys.subtracting(newNonTombstonedKeys) {
37+
update[key] = .removed
38+
}
39+
40+
// RTLM22b2
41+
for key in newNonTombstonedKeys.subtracting(previousNonTombstonedKeys) {
42+
update[key] = .updated
43+
}
44+
45+
// RTLM22b3
46+
for key in previousNonTombstonedKeys.intersection(newNonTombstonedKeys) {
47+
let previousEntry = previousData[key]!
48+
let newEntry = newData[key]!
49+
50+
if previousEntry.data != newEntry.data {
51+
update[key] = .updated
52+
}
53+
}
54+
55+
return .update(DefaultLiveMapUpdate(update: update))
56+
}
57+
}

Tests/AblyLiveObjectsTests/InternalDefaultLiveCounterTests.swift

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,64 @@ struct InternalDefaultLiveCounterTests {
149149
#expect(counter.testsOnly_createOperationIsMerged)
150150
}
151151
}
152+
153+
/// Tests for RTLC6h (diff calculation on replaceData)
154+
struct DiffCalculationTests {
155+
// @specOneOf(1/2) RTLC6h - Tests that replaceData returns the diff calculated via RTLC14
156+
@Test
157+
func returnsCorrectDiffWithoutCreateOp() throws {
158+
let logger = TestLogger()
159+
let internalQueue = TestFactories.createInternalQueue()
160+
let counter = InternalDefaultLiveCounter.createZeroValued(objectID: "arbitrary", logger: logger, internalQueue: internalQueue, userCallbackQueue: .main, clock: MockSimpleClock())
161+
let coreSDK = MockCoreSDK(channelState: .attaching, internalQueue: internalQueue)
162+
163+
// Set initial data to 10
164+
internalQueue.ably_syncNoDeadlock {
165+
_ = counter.nosync_replaceData(using: TestFactories.counterObjectState(count: 10), objectMessageSerialTimestamp: nil)
166+
}
167+
#expect(try counter.value(coreSDK: coreSDK) == 10)
168+
169+
// Replace data with count 25 (no createOp)
170+
let update = internalQueue.ably_syncNoDeadlock {
171+
counter.nosync_replaceData(using: TestFactories.counterObjectState(count: 25), objectMessageSerialTimestamp: nil)
172+
}
173+
174+
// RTLC6h: Should return diff from previousData (10) to newData (25) = 15
175+
#expect(try #require(update.update).amount == 15)
176+
#expect(try counter.value(coreSDK: coreSDK) == 25)
177+
}
178+
179+
// @specOneOf(2/2) RTLC6h - Tests that replaceData returns the diff after merging createOp
180+
@Test
181+
func returnsCorrectDiffWithCreateOp() throws {
182+
let logger = TestLogger()
183+
let internalQueue = TestFactories.createInternalQueue()
184+
let counter = InternalDefaultLiveCounter.createZeroValued(objectID: "arbitrary", logger: logger, internalQueue: internalQueue, userCallbackQueue: .main, clock: MockSimpleClock())
185+
let coreSDK = MockCoreSDK(channelState: .attaching, internalQueue: internalQueue)
186+
187+
// Set initial data to 10
188+
internalQueue.ably_syncNoDeadlock {
189+
_ = counter.nosync_replaceData(using: TestFactories.counterObjectState(count: 10), objectMessageSerialTimestamp: nil)
190+
}
191+
#expect(try counter.value(coreSDK: coreSDK) == 10)
192+
193+
// Replace data with count 5 and createOp with count 8
194+
// This should set data to 5, then add 8 (mergeInitialValue), resulting in 13
195+
let update = internalQueue.ably_syncNoDeadlock {
196+
counter.nosync_replaceData(
197+
using: TestFactories.counterObjectState(
198+
createOp: TestFactories.counterCreateOperation(count: 8),
199+
count: 5,
200+
),
201+
objectMessageSerialTimestamp: nil,
202+
)
203+
}
204+
205+
// RTLC6h: Should return diff from previousData (10) to newData (13) = 3
206+
#expect(try #require(update.update).amount == 3)
207+
#expect(try counter.value(coreSDK: coreSDK) == 13)
208+
}
209+
}
152210
}
153211

154212
/// Tests for the `mergeInitialValue` method, covering RTLC10 specification points

Tests/AblyLiveObjectsTests/InternalDefaultLiveMapTests.swift

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,107 @@ struct InternalDefaultLiveMapTests {
293293
#expect(try map.get(key: "keyFromCreateOp", coreSDK: coreSDK, delegate: delegate)?.stringValue == "valueFromCreateOp")
294294
#expect(map.testsOnly_createOperationIsMerged)
295295
}
296+
297+
/// Tests for RTLM6h (diff calculation on replaceData)
298+
struct DiffCalculationTests {
299+
// @specOneOf(1/2) RTLM6h - Tests that replaceData returns the diff calculated via RTLM22
300+
@Test
301+
func returnsCorrectDiffWithoutCreateOp() throws {
302+
let logger = TestLogger()
303+
let internalQueue = TestFactories.createInternalQueue()
304+
let map = InternalDefaultLiveMap.createZeroValued(objectID: "arbitrary", logger: logger, internalQueue: internalQueue, userCallbackQueue: .main, clock: MockSimpleClock())
305+
306+
// Set initial data
307+
var pool = ObjectsPool(logger: logger, internalQueue: internalQueue, userCallbackQueue: .main, clock: MockSimpleClock())
308+
internalQueue.ably_syncNoDeadlock {
309+
_ = map.nosync_replaceData(
310+
using: TestFactories.mapObjectState(
311+
objectId: "arbitrary-id",
312+
entries: [
313+
"key1": TestFactories.stringMapEntry(key: "key1", value: "value1").entry,
314+
"key2": TestFactories.stringMapEntry(key: "key2", value: "value2").entry,
315+
],
316+
),
317+
objectMessageSerialTimestamp: nil,
318+
objectsPool: &pool,
319+
)
320+
}
321+
322+
// Replace data with modified entries (no createOp)
323+
let update = internalQueue.ably_syncNoDeadlock {
324+
map.nosync_replaceData(
325+
using: TestFactories.mapObjectState(
326+
objectId: "arbitrary-id",
327+
entries: [
328+
"key1": TestFactories.stringMapEntry(key: "key1", value: "updatedValue").entry,
329+
"key3": TestFactories.stringMapEntry(key: "key3", value: "value3").entry,
330+
],
331+
),
332+
objectMessageSerialTimestamp: nil,
333+
objectsPool: &pool,
334+
)
335+
}
336+
337+
// RTLM6h: Should return diff per RTLM22
338+
// key1: updated (changed value), key2: removed, key3: added
339+
let updateDict = try #require(update.update).update
340+
#expect(updateDict["key1"] == .updated) // value changed
341+
#expect(updateDict["key2"] == .removed) // removed
342+
#expect(updateDict["key3"] == .updated) // added
343+
}
344+
345+
// @specOneOf(2/2) RTLM6h - Tests that replaceData returns the diff after merging createOp
346+
@Test
347+
func returnsCorrectDiffWithCreateOp() throws {
348+
let logger = TestLogger()
349+
let internalQueue = TestFactories.createInternalQueue()
350+
let map = InternalDefaultLiveMap.createZeroValued(objectID: "arbitrary", logger: logger, internalQueue: internalQueue, userCallbackQueue: .main, clock: MockSimpleClock())
351+
352+
// Set initial data
353+
var pool = ObjectsPool(logger: logger, internalQueue: internalQueue, userCallbackQueue: .main, clock: MockSimpleClock())
354+
internalQueue.ably_syncNoDeadlock {
355+
_ = map.nosync_replaceData(
356+
using: TestFactories.mapObjectState(
357+
objectId: "arbitrary-id",
358+
entries: [
359+
"existing": TestFactories.stringMapEntry(key: "existing", value: "value").entry,
360+
],
361+
),
362+
objectMessageSerialTimestamp: nil,
363+
objectsPool: &pool,
364+
)
365+
}
366+
367+
// Replace data with entries and createOp
368+
let update = internalQueue.ably_syncNoDeadlock {
369+
map.nosync_replaceData(
370+
using: TestFactories.objectState(
371+
objectId: "arbitrary-id",
372+
createOp: TestFactories.mapCreateOperation(
373+
objectId: "arbitrary-id",
374+
entries: [
375+
"fromCreateOp": TestFactories.stringMapEntry(key: "fromCreateOp", value: "value").entry,
376+
],
377+
),
378+
map: ObjectsMap(
379+
semantics: .known(.lww),
380+
entries: [
381+
"fromEntries": TestFactories.stringMapEntry(key: "fromEntries", value: "value").entry,
382+
],
383+
),
384+
),
385+
objectMessageSerialTimestamp: nil,
386+
objectsPool: &pool,
387+
)
388+
}
389+
390+
// RTLM6h: Should return diff from previousData to final data (after createOp merge)
391+
let updateDict = try #require(update.update).update
392+
#expect(updateDict["existing"] == .removed) // removed
393+
#expect(updateDict["fromEntries"] == .updated) // added
394+
#expect(updateDict["fromCreateOp"] == .updated) // added via createOp
395+
}
396+
}
296397
}
297398

298399
/// Tests for the `size`, `entries`, `keys`, and `values` properties, covering RTLM10, RTLM11, RTLM12, and RTLM13 specification points

0 commit comments

Comments
 (0)