Skip to content

Commit e31f65e

Browse files
Update map "replace data", MAP_SET, MAP_REMOVE to set entry tombstonedAt
1 parent 51eb113 commit e31f65e

File tree

4 files changed

+85
-39
lines changed

4 files changed

+85
-39
lines changed

Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -341,11 +341,14 @@ internal final class InternalDefaultLiveMap: Sendable {
341341
/// Applies a `MAP_REMOVE` operation to a key, per RTLM8.
342342
///
343343
/// This is currently exposed just so that the tests can test RTLM8 without having to go through a convoluted replaceData(…) call, but I _think_ that it's going to be used in further contexts when we introduce the handling of incoming object operations in a future spec PR.
344-
internal func testsOnly_applyMapRemoveOperation(key: String, operationTimeserial: String?) -> LiveObjectUpdate<DefaultLiveMapUpdate> {
344+
internal func testsOnly_applyMapRemoveOperation(key: String, operationTimeserial: String?, operationSerialTimestamp: Date?) -> LiveObjectUpdate<DefaultLiveMapUpdate> {
345345
mutex.withLock {
346346
mutableState.applyMapRemoveOperation(
347347
key: key,
348348
operationTimeserial: operationTimeserial,
349+
operationSerialTimestamp: operationSerialTimestamp,
350+
logger: logger,
351+
clock: clock,
349352
)
350353
}
351354
}
@@ -417,7 +420,25 @@ internal final class InternalDefaultLiveMap: Sendable {
417420
liveObjectMutableState.createOperationIsMerged = false
418421

419422
// RTLM6c: Set data to ObjectState.map.entries, or to an empty map if it does not exist
420-
data = state.map?.entries?.mapValues { .init(objectsMapEntry: $0) } ?? [:]
423+
data = state.map?.entries?.mapValues { entry in
424+
// Set tombstonedAt for tombstoned entries
425+
let tombstonedAt: Date?
426+
if entry.tombstone == true {
427+
// RTLM6c1a
428+
if let serialTimestamp = entry.serialTimestamp {
429+
tombstonedAt = serialTimestamp
430+
} else {
431+
// RTLM6c1b
432+
logger.log("serialTimestamp not found in ObjectsMapEntry, using local clock for tombstone timestamp", level: .debug)
433+
// RTLM6cb1
434+
tombstonedAt = clock.now
435+
}
436+
} else {
437+
tombstonedAt = nil
438+
}
439+
440+
return .init(objectsMapEntry: entry, tombstonedAt: tombstonedAt)
441+
} ?? [:]
421442

422443
// RTLM6d: If ObjectState.createOp is present, merge the initial value into the LiveMap as described in RTLM17
423444
return if let createOp = state.createOp {
@@ -447,10 +468,13 @@ internal final class InternalDefaultLiveMap: Sendable {
447468
entries.map { key, entry in
448469
if entry.tombstone == true {
449470
// RTLM17a2: If ObjectsMapEntry.tombstone is true, apply the MAP_REMOVE operation
450-
// as described in RTLM8, passing in the current key as ObjectsMapOp, and ObjectsMapEntry.timeserial as the operation's serial
471+
// as described in RTLM8, passing in the current key as ObjectsMapOp, ObjectsMapEntry.timeserial as the operation's serial, and ObjectsMapEntry.serialTimestamp as the operation's serial timestamp
451472
applyMapRemoveOperation(
452473
key: key,
453474
operationTimeserial: entry.timeserial,
475+
operationSerialTimestamp: entry.serialTimestamp,
476+
logger: logger,
477+
clock: clock,
454478
)
455479
} else {
456480
// RTLM17a1: If ObjectsMapEntry.tombstone is false, apply the MAP_SET operation
@@ -559,6 +583,9 @@ internal final class InternalDefaultLiveMap: Sendable {
559583
let update = applyMapRemoveOperation(
560584
key: mapOp.key,
561585
operationTimeserial: applicableOperation.objectMessageSerial,
586+
operationSerialTimestamp: objectMessageSerialTimestamp,
587+
logger: logger,
588+
clock: clock,
562589
)
563590
// RTLM15d3a
564591
liveObjectMutableState.emit(update, on: userCallbackQueue)
@@ -599,17 +626,17 @@ internal final class InternalDefaultLiveMap: Sendable {
599626
// RTLM7a2: Otherwise, apply the operation
600627
// RTLM7a2a: Set ObjectsMapEntry.data to the ObjectData from the operation
601628
// RTLM7a2b: Set ObjectsMapEntry.timeserial to the operation's serial
602-
// RTLM7a2c: Set ObjectsMapEntry.tombstone to false
629+
// RTLM7a2c: Set ObjectsMapEntry.tombstone to false (same as RTLM7a2d: Set ObjectsMapEntry.tombstonedAt to nil)
603630
var updatedEntry = existingEntry
604631
updatedEntry.data = operationData
605632
updatedEntry.timeserial = operationTimeserial
606-
updatedEntry.tombstone = false
633+
updatedEntry.tombstonedAt = nil
607634
data[key] = updatedEntry
608635
} else {
609636
// RTLM7b: If an entry does not exist in the private data for the specified key
610637
// RTLM7b1: Create a new entry in data for the specified key with the provided ObjectData and the operation's serial
611-
// RTLM7b2: Set ObjectsMapEntry.tombstone for the new entry to false
612-
data[key] = InternalObjectsMapEntry(tombstone: false, timeserial: operationTimeserial, data: operationData)
638+
// RTLM7b2: Set ObjectsMapEntry.tombstone for the new entry to false (same as RTLM7b3: Set tombstonedAt to nil)
639+
data[key] = InternalObjectsMapEntry(tombstonedAt: nil, timeserial: operationTimeserial, data: operationData)
613640
}
614641

615642
// RTLM7c: If the operation has a non-empty ObjectData.objectId attribute
@@ -623,9 +650,21 @@ internal final class InternalDefaultLiveMap: Sendable {
623650
}
624651

625652
/// Applies a `MAP_REMOVE` operation to a key, per RTLM8.
626-
internal mutating func applyMapRemoveOperation(key: String, operationTimeserial: String?) -> LiveObjectUpdate<DefaultLiveMapUpdate> {
653+
internal mutating func applyMapRemoveOperation(key: String, operationTimeserial: String?, operationSerialTimestamp: Date?, logger: Logger, clock: SimpleClock) -> LiveObjectUpdate<DefaultLiveMapUpdate> {
627654
// (Note that, where the spec tells us to set ObjectsMapEntry.data to nil, we actually set it to an empty ObjectData, which is equivalent, since it contains no data)
628655

656+
// Calculate the tombstonedAt for the new or updated entry per RTLM8f
657+
let tombstonedAt: Date?
658+
if let operationSerialTimestamp {
659+
// RTLM8f1
660+
tombstonedAt = operationSerialTimestamp
661+
} else {
662+
// RTLM8f2
663+
logger.log("serialTimestamp not provided for MAP_REMOVE, using local clock for tombstone timestamp", level: .debug)
664+
// RTLM8f2a
665+
tombstonedAt = clock.now
666+
}
667+
629668
// RTLM8a: If an entry exists in the private data for the specified key
630669
if let existingEntry = data[key] {
631670
// RTLM8a1: If the operation cannot be applied as per RTLM9, discard the operation
@@ -635,17 +674,19 @@ internal final class InternalDefaultLiveMap: Sendable {
635674
// RTLM8a2: Otherwise, apply the operation
636675
// RTLM8a2a: Set ObjectsMapEntry.data to undefined/null
637676
// RTLM8a2b: Set ObjectsMapEntry.timeserial to the operation's serial
638-
// RTLM8a2c: Set ObjectsMapEntry.tombstone to true
677+
// RTLM8a2c: Set ObjectsMapEntry.tombstone to true (equivalent to next point)
678+
// RTLM8a2d: Set ObjectsMapEntry.tombstonedAt per RTLM8a2d
639679
var updatedEntry = existingEntry
640680
updatedEntry.data = ObjectData()
641681
updatedEntry.timeserial = operationTimeserial
642-
updatedEntry.tombstone = true
682+
updatedEntry.tombstonedAt = tombstonedAt
643683
data[key] = updatedEntry
644684
} else {
645685
// RTLM8b: If an entry does not exist in the private data for the specified key
646686
// RTLM8b1: Create a new entry in data for the specified key, with ObjectsMapEntry.data set to undefined/null and the operation's serial
647687
// RTLM8b2: Set ObjectsMapEntry.tombstone for the new entry to true
648-
data[key] = InternalObjectsMapEntry(tombstone: true, timeserial: operationTimeserial, data: ObjectData())
688+
// RTLM8b3: Set ObjectsMapEntry.tombstonedAt per RTLM8f
689+
data[key] = InternalObjectsMapEntry(tombstonedAt: tombstonedAt, timeserial: operationTimeserial, data: ObjectData())
649690
}
650691

651692
return .update(DefaultLiveMapUpdate(update: [key: .removed]))

Sources/AblyLiveObjects/Internal/InternalObjectsMapEntry.swift

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
1-
/// The entries stored in a `LiveMap`'s data. Same as an `ObjectsMapEntry` but with an additional `tombstonedAt` property, per RTLM3a. (This property will be added in an upcoming commit.)
1+
import Foundation
2+
3+
/// The entries stored in a `LiveMap`'s data. Same as an `ObjectsMapEntry` but with an additional `tombstonedAt` property, per RTLM3a.
24
internal struct InternalObjectsMapEntry {
3-
internal var tombstone: Bool? // OME2a
5+
internal var tombstonedAt: Date? // RTLM3a
6+
internal var tombstone: Bool {
7+
// TODO: Confirm that we don't need to store this (https://github.com/ably/specification/pull/350/files#r2213895661)
8+
tombstonedAt != nil
9+
}
10+
411
internal var timeserial: String? // OME2b
512
internal var data: ObjectData // OME2c
613
}
714

815
internal extension InternalObjectsMapEntry {
9-
init(objectsMapEntry: ObjectsMapEntry) {
10-
tombstone = objectsMapEntry.tombstone
16+
init(objectsMapEntry: ObjectsMapEntry, tombstonedAt: Date?) {
17+
self.tombstonedAt = tombstonedAt
1118
timeserial = objectsMapEntry.timeserial
1219
data = objectsMapEntry.data
1320
}

Tests/AblyLiveObjectsTests/Helpers/TestFactories.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -406,12 +406,12 @@ struct TestFactories {
406406
///
407407
/// This should be kept in sync with ``mapEntry``.
408408
static func internalMapEntry(
409-
tombstone: Bool? = false,
409+
tombstonedAt: Date? = nil,
410410
timeserial: String? = "ts1",
411411
data: ObjectData,
412412
) -> InternalObjectsMapEntry {
413413
InternalObjectsMapEntry(
414-
tombstone: tombstone,
414+
tombstonedAt: tombstonedAt,
415415
timeserial: timeserial,
416416
data: data,
417417
)
@@ -440,13 +440,13 @@ struct TestFactories {
440440
static func internalStringMapEntry(
441441
key: String = "testKey",
442442
value: String = "testValue",
443-
tombstone: Bool? = false,
443+
tombstonedAt: Date? = nil,
444444
timeserial: String? = "ts1",
445445
) -> (key: String, entry: InternalObjectsMapEntry) {
446446
(
447447
key: key,
448448
entry: internalMapEntry(
449-
tombstone: tombstone,
449+
tombstonedAt: nil,
450450
timeserial: timeserial,
451451
data: ObjectData(string: .string(value)),
452452
),

Tests/AblyLiveObjectsTests/InternalDefaultLiveMapTests.swift

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ struct InternalDefaultLiveMapTests {
3939
func returnsNilWhenEntryIsTombstoned() throws {
4040
let logger = TestLogger()
4141
let entry = TestFactories.internalMapEntry(
42-
tombstone: true,
42+
tombstonedAt: Date(),
4343
data: ObjectData(boolean: true), // Value doesn't matter as it's tombstoned
4444
)
4545
let coreSDK = MockCoreSDK(channelState: .attaching)
@@ -293,12 +293,11 @@ struct InternalDefaultLiveMapTests {
293293
let delegate = MockLiveMapObjectPoolDelegate()
294294
let map = InternalDefaultLiveMap(
295295
testsOnly_data: [
296-
// tombstone is nil, so not considered tombstoned
296+
// tombstonedAt is nil, so not considered tombstoned
297297
"active1": TestFactories.internalMapEntry(data: ObjectData(string: .string("value1"))),
298-
// tombstone is false, so not considered tombstoned[
299-
"active2": TestFactories.internalMapEntry(tombstone: false, data: ObjectData(string: .string("value2"))),
300-
"tombstoned": TestFactories.internalMapEntry(tombstone: true, data: ObjectData(string: .string("tombstoned"))),
301-
"tombstoned2": TestFactories.internalMapEntry(tombstone: true, data: ObjectData(string: .string("tombstoned2"))),
298+
// tombstonedAt is false, so not considered tombstoned
299+
"tombstoned": TestFactories.internalMapEntry(tombstonedAt: Date(), data: ObjectData(string: .string("tombstoned"))),
300+
"tombstoned2": TestFactories.internalMapEntry(tombstonedAt: Date(), data: ObjectData(string: .string("tombstoned2"))),
302301
],
303302
objectID: "arbitrary",
304303
logger: logger,
@@ -308,24 +307,23 @@ struct InternalDefaultLiveMapTests {
308307

309308
// Test size - should only count non-tombstoned entries
310309
let size = try map.size(coreSDK: coreSDK)
311-
#expect(size == 2)
310+
#expect(size == 1)
312311

313312
// Test entries - should only return non-tombstoned entries
314313
let entries = try map.entries(coreSDK: coreSDK, delegate: delegate)
315-
#expect(entries.count == 2)
316-
#expect(Set(entries.map(\.key)) == ["active1", "active2"])
314+
#expect(entries.count == 1)
315+
#expect(Set(entries.map(\.key)) == ["active1"])
317316
#expect(entries.first { $0.key == "active1" }?.value.stringValue == "value1")
318-
#expect(entries.first { $0.key == "active2" }?.value.stringValue == "value2")
319317

320318
// Test keys - should only return keys from non-tombstoned entries
321319
let keys = try map.keys(coreSDK: coreSDK, delegate: delegate)
322-
#expect(keys.count == 2)
323-
#expect(Set(keys) == ["active1", "active2"])
320+
#expect(keys.count == 1)
321+
#expect(Set(keys) == ["active1"])
324322

325323
// Test values - should only return values from non-tombstoned entries
326324
let values = try map.values(coreSDK: coreSDK, delegate: delegate)
327-
#expect(values.count == 2)
328-
#expect(Set(values.compactMap(\.stringValue)) == Set(["value1", "value2"]))
325+
#expect(values.count == 1)
326+
#expect(Set(values.compactMap(\.stringValue)) == Set(["value1"]))
329327
}
330328

331329
// MARK: - Consistency Tests
@@ -477,7 +475,7 @@ struct InternalDefaultLiveMapTests {
477475
let delegate = MockLiveMapObjectPoolDelegate()
478476
let coreSDK = MockCoreSDK(channelState: .attaching)
479477
let map = InternalDefaultLiveMap(
480-
testsOnly_data: ["key1": TestFactories.internalMapEntry(tombstone: true, timeserial: "ts1", data: ObjectData(string: .string("existing")))],
478+
testsOnly_data: ["key1": TestFactories.internalMapEntry(tombstonedAt: Date(), timeserial: "ts1", data: ObjectData(string: .string("existing")))],
481479
objectID: "arbitrary",
482480
logger: logger,
483481
userCallbackQueue: .main,
@@ -657,7 +655,7 @@ struct InternalDefaultLiveMapTests {
657655
)
658656

659657
// Try to apply operation with lower timeserial (ts1 < ts2), cannot be applied per RTLM9
660-
let update = map.testsOnly_applyMapRemoveOperation(key: "key1", operationTimeserial: "ts1")
658+
let update = map.testsOnly_applyMapRemoveOperation(key: "key1", operationTimeserial: "ts1", operationSerialTimestamp: nil)
661659

662660
// Verify the operation was discarded - existing data unchanged
663661
#expect(try map.get(key: "key1", coreSDK: coreSDK, delegate: delegate)?.stringValue == "existing")
@@ -675,15 +673,15 @@ struct InternalDefaultLiveMapTests {
675673
let delegate = MockLiveMapObjectPoolDelegate()
676674
let coreSDK = MockCoreSDK(channelState: .attaching)
677675
let map = InternalDefaultLiveMap(
678-
testsOnly_data: ["key1": TestFactories.internalMapEntry(tombstone: false, timeserial: "ts1", data: ObjectData(string: .string("existing")))],
676+
testsOnly_data: ["key1": TestFactories.internalMapEntry(tombstonedAt: nil, timeserial: "ts1", data: ObjectData(string: .string("existing")))],
679677
objectID: "arbitrary",
680678
logger: logger,
681679
userCallbackQueue: .main,
682680
clock: MockSimpleClock(),
683681
)
684682

685683
// Apply operation with higher timeserial (ts2 > ts1), so can be applied per RTLM9
686-
let update = map.testsOnly_applyMapRemoveOperation(key: "key1", operationTimeserial: "ts2")
684+
let update = map.testsOnly_applyMapRemoveOperation(key: "key1", operationTimeserial: "ts2", operationSerialTimestamp: nil)
687685

688686
// Verify the operation was applied
689687
#expect(try map.get(key: "key1", coreSDK: coreSDK, delegate: delegate) == nil)
@@ -717,7 +715,7 @@ struct InternalDefaultLiveMapTests {
717715
let logger = TestLogger()
718716
let map = InternalDefaultLiveMap.createZeroValued(objectID: "arbitrary", logger: logger, userCallbackQueue: .main, clock: MockSimpleClock())
719717

720-
let update = map.testsOnly_applyMapRemoveOperation(key: "newKey", operationTimeserial: "ts1")
718+
let update = map.testsOnly_applyMapRemoveOperation(key: "newKey", operationTimeserial: "ts1", operationSerialTimestamp: nil)
721719

722720
// Verify new entry was created
723721
let entry = map.testsOnly_data["newKey"]
@@ -739,7 +737,7 @@ struct InternalDefaultLiveMapTests {
739737
let logger = TestLogger()
740738
let map = InternalDefaultLiveMap.createZeroValued(objectID: "arbitrary", logger: logger, userCallbackQueue: .main, clock: MockSimpleClock())
741739

742-
_ = map.testsOnly_applyMapRemoveOperation(key: "newKey", operationTimeserial: "ts1")
740+
_ = map.testsOnly_applyMapRemoveOperation(key: "newKey", operationTimeserial: "ts1", operationSerialTimestamp: nil)
743741

744742
// Verify tombstone is true for new entry
745743
#expect(map.testsOnly_data["newKey"]?.tombstone == true)

0 commit comments

Comments
 (0)