Skip to content

Commit e30e98d

Browse files
Update map "replace data", MAP_SET, MAP_REMOVE to set entry tombstonedAt
1 parent fb5f8fe commit e30e98d

4 files changed

Lines changed: 85 additions & 39 deletions

File tree

Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -373,11 +373,14 @@ internal final class InternalDefaultLiveMap: Sendable {
373373
/// Applies a `MAP_REMOVE` operation to a key, per RTLM8.
374374
///
375375
/// 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.
376-
internal func testsOnly_applyMapRemoveOperation(key: String, operationTimeserial: String?) -> LiveObjectUpdate<DefaultLiveMapUpdate> {
376+
internal func testsOnly_applyMapRemoveOperation(key: String, operationTimeserial: String?, operationSerialTimestamp: Date?) -> LiveObjectUpdate<DefaultLiveMapUpdate> {
377377
mutex.withLock {
378378
mutableState.applyMapRemoveOperation(
379379
key: key,
380380
operationTimeserial: operationTimeserial,
381+
operationSerialTimestamp: operationSerialTimestamp,
382+
logger: logger,
383+
clock: clock,
381384
)
382385
}
383386
}
@@ -449,7 +452,25 @@ internal final class InternalDefaultLiveMap: Sendable {
449452
liveObjectMutableState.createOperationIsMerged = false
450453

451454
// RTLM6c: Set data to ObjectState.map.entries, or to an empty map if it does not exist
452-
data = state.map?.entries?.mapValues { .init(objectsMapEntry: $0) } ?? [:]
455+
data = state.map?.entries?.mapValues { entry in
456+
// Set tombstonedAt for tombstoned entries
457+
let tombstonedAt: Date?
458+
if entry.tombstone == true {
459+
// RTLM6c1a
460+
if let serialTimestamp = entry.serialTimestamp {
461+
tombstonedAt = serialTimestamp
462+
} else {
463+
// RTLM6c1b
464+
logger.log("serialTimestamp not found in ObjectsMapEntry, using local clock for tombstone timestamp", level: .debug)
465+
// RTLM6cb1
466+
tombstonedAt = clock.now
467+
}
468+
} else {
469+
tombstonedAt = nil
470+
}
471+
472+
return .init(objectsMapEntry: entry, tombstonedAt: tombstonedAt)
473+
} ?? [:]
453474

454475
// RTLM6d: If ObjectState.createOp is present, merge the initial value into the LiveMap as described in RTLM17
455476
return if let createOp = state.createOp {
@@ -479,10 +500,13 @@ internal final class InternalDefaultLiveMap: Sendable {
479500
entries.map { key, entry in
480501
if entry.tombstone == true {
481502
// RTLM17a2: If ObjectsMapEntry.tombstone is true, apply the MAP_REMOVE operation
482-
// as described in RTLM8, passing in the current key as ObjectsMapOp, and ObjectsMapEntry.timeserial as the operation's serial
503+
// 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
483504
applyMapRemoveOperation(
484505
key: key,
485506
operationTimeserial: entry.timeserial,
507+
operationSerialTimestamp: entry.serialTimestamp,
508+
logger: logger,
509+
clock: clock,
486510
)
487511
} else {
488512
// RTLM17a1: If ObjectsMapEntry.tombstone is false, apply the MAP_SET operation
@@ -591,6 +615,9 @@ internal final class InternalDefaultLiveMap: Sendable {
591615
let update = applyMapRemoveOperation(
592616
key: mapOp.key,
593617
operationTimeserial: applicableOperation.objectMessageSerial,
618+
operationSerialTimestamp: objectMessageSerialTimestamp,
619+
logger: logger,
620+
clock: clock,
594621
)
595622
// RTLM15d3a
596623
liveObjectMutableState.emit(update, on: userCallbackQueue)
@@ -631,17 +658,17 @@ internal final class InternalDefaultLiveMap: Sendable {
631658
// RTLM7a2: Otherwise, apply the operation
632659
// RTLM7a2a: Set ObjectsMapEntry.data to the ObjectData from the operation
633660
// RTLM7a2b: Set ObjectsMapEntry.timeserial to the operation's serial
634-
// RTLM7a2c: Set ObjectsMapEntry.tombstone to false
661+
// RTLM7a2c: Set ObjectsMapEntry.tombstone to false (same as RTLM7a2d: Set ObjectsMapEntry.tombstonedAt to nil)
635662
var updatedEntry = existingEntry
636663
updatedEntry.data = operationData
637664
updatedEntry.timeserial = operationTimeserial
638-
updatedEntry.tombstone = false
665+
updatedEntry.tombstonedAt = nil
639666
data[key] = updatedEntry
640667
} else {
641668
// RTLM7b: If an entry does not exist in the private data for the specified key
642669
// RTLM7b1: Create a new entry in data for the specified key with the provided ObjectData and the operation's serial
643-
// RTLM7b2: Set ObjectsMapEntry.tombstone for the new entry to false
644-
data[key] = InternalObjectsMapEntry(tombstone: false, timeserial: operationTimeserial, data: operationData)
670+
// RTLM7b2: Set ObjectsMapEntry.tombstone for the new entry to false (same as RTLM7b3: Set tombstonedAt to nil)
671+
data[key] = InternalObjectsMapEntry(tombstonedAt: nil, timeserial: operationTimeserial, data: operationData)
645672
}
646673

647674
// RTLM7c: If the operation has a non-empty ObjectData.objectId attribute
@@ -655,9 +682,21 @@ internal final class InternalDefaultLiveMap: Sendable {
655682
}
656683

657684
/// Applies a `MAP_REMOVE` operation to a key, per RTLM8.
658-
internal mutating func applyMapRemoveOperation(key: String, operationTimeserial: String?) -> LiveObjectUpdate<DefaultLiveMapUpdate> {
685+
internal mutating func applyMapRemoveOperation(key: String, operationTimeserial: String?, operationSerialTimestamp: Date?, logger: Logger, clock: SimpleClock) -> LiveObjectUpdate<DefaultLiveMapUpdate> {
659686
// (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)
660687

688+
// Calculate the tombstonedAt for the new or updated entry per RTLM8f
689+
let tombstonedAt: Date?
690+
if let operationSerialTimestamp {
691+
// RTLM8f1
692+
tombstonedAt = operationSerialTimestamp
693+
} else {
694+
// RTLM8f2
695+
logger.log("serialTimestamp not provided for MAP_REMOVE, using local clock for tombstone timestamp", level: .debug)
696+
// RTLM8f2a
697+
tombstonedAt = clock.now
698+
}
699+
661700
// RTLM8a: If an entry exists in the private data for the specified key
662701
if let existingEntry = data[key] {
663702
// RTLM8a1: If the operation cannot be applied as per RTLM9, discard the operation
@@ -667,17 +706,19 @@ internal final class InternalDefaultLiveMap: Sendable {
667706
// RTLM8a2: Otherwise, apply the operation
668707
// RTLM8a2a: Set ObjectsMapEntry.data to undefined/null
669708
// RTLM8a2b: Set ObjectsMapEntry.timeserial to the operation's serial
670-
// RTLM8a2c: Set ObjectsMapEntry.tombstone to true
709+
// RTLM8a2c: Set ObjectsMapEntry.tombstone to true (equivalent to next point)
710+
// RTLM8a2d: Set ObjectsMapEntry.tombstonedAt per RTLM8a2d
671711
var updatedEntry = existingEntry
672712
updatedEntry.data = ObjectData()
673713
updatedEntry.timeserial = operationTimeserial
674-
updatedEntry.tombstone = true
714+
updatedEntry.tombstonedAt = tombstonedAt
675715
data[key] = updatedEntry
676716
} else {
677717
// RTLM8b: If an entry does not exist in the private data for the specified key
678718
// RTLM8b1: Create a new entry in data for the specified key, with ObjectsMapEntry.data set to undefined/null and the operation's serial
679719
// RTLM8b2: Set ObjectsMapEntry.tombstone for the new entry to true
680-
data[key] = InternalObjectsMapEntry(tombstone: true, timeserial: operationTimeserial, data: ObjectData())
720+
// RTLM8b3: Set ObjectsMapEntry.tombstonedAt per RTLM8f
721+
data[key] = InternalObjectsMapEntry(tombstonedAt: tombstonedAt, timeserial: operationTimeserial, data: ObjectData())
681722
}
682723

683724
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: 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)
@@ -317,12 +317,11 @@ struct InternalDefaultLiveMapTests {
317317
let delegate = MockLiveMapObjectPoolDelegate()
318318
let map = InternalDefaultLiveMap(
319319
testsOnly_data: [
320-
// tombstone is nil, so not considered tombstoned
320+
// tombstonedAt is nil, so not considered tombstoned
321321
"active1": TestFactories.internalMapEntry(data: ObjectData(string: "value1")),
322-
// tombstone is false, so not considered tombstoned[
323-
"active2": TestFactories.internalMapEntry(tombstone: false, data: ObjectData(string: "value2")),
324-
"tombstoned": TestFactories.internalMapEntry(tombstone: true, data: ObjectData(string: "tombstoned")),
325-
"tombstoned2": TestFactories.internalMapEntry(tombstone: true, data: ObjectData(string: "tombstoned2")),
322+
// tombstonedAt is false, so not considered tombstoned
323+
"tombstoned": TestFactories.internalMapEntry(tombstonedAt: Date(), data: ObjectData(string: "tombstoned")),
324+
"tombstoned2": TestFactories.internalMapEntry(tombstonedAt: Date(), data: ObjectData(string: "tombstoned2")),
326325
],
327326
objectID: "arbitrary",
328327
logger: logger,
@@ -332,24 +331,23 @@ struct InternalDefaultLiveMapTests {
332331

333332
// Test size - should only count non-tombstoned entries
334333
let size = try map.size(coreSDK: coreSDK)
335-
#expect(size == 2)
334+
#expect(size == 1)
336335

337336
// Test entries - should only return non-tombstoned entries
338337
let entries = try map.entries(coreSDK: coreSDK, delegate: delegate)
339-
#expect(entries.count == 2)
340-
#expect(Set(entries.map(\.key)) == ["active1", "active2"])
338+
#expect(entries.count == 1)
339+
#expect(Set(entries.map(\.key)) == ["active1"])
341340
#expect(entries.first { $0.key == "active1" }?.value.stringValue == "value1")
342-
#expect(entries.first { $0.key == "active2" }?.value.stringValue == "value2")
343341

344342
// Test keys - should only return keys from non-tombstoned entries
345343
let keys = try map.keys(coreSDK: coreSDK, delegate: delegate)
346-
#expect(keys.count == 2)
347-
#expect(Set(keys) == ["active1", "active2"])
344+
#expect(keys.count == 1)
345+
#expect(Set(keys) == ["active1"])
348346

349347
// Test values - should only return values from non-tombstoned entries
350348
let values = try map.values(coreSDK: coreSDK, delegate: delegate)
351-
#expect(values.count == 2)
352-
#expect(Set(values.compactMap(\.stringValue)) == Set(["value1", "value2"]))
349+
#expect(values.count == 1)
350+
#expect(Set(values.compactMap(\.stringValue)) == Set(["value1"]))
353351
}
354352

355353
// MARK: - Consistency Tests
@@ -507,7 +505,7 @@ struct InternalDefaultLiveMapTests {
507505
let delegate = MockLiveMapObjectPoolDelegate()
508506
let coreSDK = MockCoreSDK(channelState: .attaching)
509507
let map = InternalDefaultLiveMap(
510-
testsOnly_data: ["key1": TestFactories.internalMapEntry(tombstone: true, timeserial: "ts1", data: ObjectData(string: "existing"))],
508+
testsOnly_data: ["key1": TestFactories.internalMapEntry(tombstonedAt: Date(), timeserial: "ts1", data: ObjectData(string: "existing"))],
511509
objectID: "arbitrary",
512510
logger: logger,
513511
userCallbackQueue: .main,
@@ -687,7 +685,7 @@ struct InternalDefaultLiveMapTests {
687685
)
688686

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

692690
// Verify the operation was discarded - existing data unchanged
693691
#expect(try map.get(key: "key1", coreSDK: coreSDK, delegate: delegate)?.stringValue == "existing")
@@ -705,15 +703,15 @@ struct InternalDefaultLiveMapTests {
705703
let delegate = MockLiveMapObjectPoolDelegate()
706704
let coreSDK = MockCoreSDK(channelState: .attaching)
707705
let map = InternalDefaultLiveMap(
708-
testsOnly_data: ["key1": TestFactories.internalMapEntry(tombstone: false, timeserial: "ts1", data: ObjectData(string: "existing"))],
706+
testsOnly_data: ["key1": TestFactories.internalMapEntry(tombstonedAt: nil, timeserial: "ts1", data: ObjectData(string: "existing"))],
709707
objectID: "arbitrary",
710708
logger: logger,
711709
userCallbackQueue: .main,
712710
clock: MockSimpleClock(),
713711
)
714712

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

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

750-
let update = map.testsOnly_applyMapRemoveOperation(key: "newKey", operationTimeserial: "ts1")
748+
let update = map.testsOnly_applyMapRemoveOperation(key: "newKey", operationTimeserial: "ts1", operationSerialTimestamp: nil)
751749

752750
// Verify new entry was created
753751
let entry = map.testsOnly_data["newKey"]
@@ -769,7 +767,7 @@ struct InternalDefaultLiveMapTests {
769767
let logger = TestLogger()
770768
let map = InternalDefaultLiveMap.createZeroValued(objectID: "arbitrary", logger: logger, userCallbackQueue: .main, clock: MockSimpleClock())
771769

772-
_ = map.testsOnly_applyMapRemoveOperation(key: "newKey", operationTimeserial: "ts1")
770+
_ = map.testsOnly_applyMapRemoveOperation(key: "newKey", operationTimeserial: "ts1", operationSerialTimestamp: nil)
773771

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

0 commit comments

Comments
 (0)