Skip to content

Commit 448b53d

Browse files
Update map "replace data", MAP_SET, MAP_REMOVE to set entry tombstonedAt
Based on spec referenced in 040987b; same comment re testing applies too.
1 parent d1e5b18 commit 448b53d

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
@@ -320,11 +320,14 @@ internal final class InternalDefaultLiveMap: Sendable {
320320
/// Applies a `MAP_REMOVE` operation to a key, per RTLM8.
321321
///
322322
/// 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.
323-
internal func testsOnly_applyMapRemoveOperation(key: String, operationTimeserial: String?) -> LiveObjectUpdate<DefaultLiveMapUpdate> {
323+
internal func testsOnly_applyMapRemoveOperation(key: String, operationTimeserial: String?, operationSerialTimestamp: Date?) -> LiveObjectUpdate<DefaultLiveMapUpdate> {
324324
mutex.withLock {
325325
mutableState.applyMapRemoveOperation(
326326
key: key,
327327
operationTimeserial: operationTimeserial,
328+
operationSerialTimestamp: operationSerialTimestamp,
329+
logger: logger,
330+
clock: clock,
328331
)
329332
}
330333
}
@@ -396,7 +399,25 @@ internal final class InternalDefaultLiveMap: Sendable {
396399
liveObjectMutableState.createOperationIsMerged = false
397400

398401
// RTLM6c: Set data to ObjectState.map.entries, or to an empty map if it does not exist
399-
data = state.map?.entries?.mapValues { .init(objectsMapEntry: $0) } ?? [:]
402+
data = state.map?.entries?.mapValues { entry in
403+
// Set tombstonedAt for tombstoned entries
404+
let tombstonedAt: Date?
405+
if entry.tombstone == true {
406+
// RTLM6c1a
407+
if let serialTimestamp = entry.serialTimestamp {
408+
tombstonedAt = serialTimestamp
409+
} else {
410+
// RTLM6c1b
411+
logger.log("serialTimestamp not found in ObjectsMapEntry, using local clock for tombstone timestamp", level: .debug)
412+
// RTLM6cb1
413+
tombstonedAt = clock.now
414+
}
415+
} else {
416+
tombstonedAt = nil
417+
}
418+
419+
return .init(objectsMapEntry: entry, tombstonedAt: tombstonedAt)
420+
} ?? [:]
400421

401422
// RTLM6d: If ObjectState.createOp is present, merge the initial value into the LiveMap as described in RTLM17
402423
return if let createOp = state.createOp {
@@ -426,10 +447,13 @@ internal final class InternalDefaultLiveMap: Sendable {
426447
entries.map { key, entry in
427448
if entry.tombstone == true {
428449
// RTLM17a2: If ObjectsMapEntry.tombstone is true, apply the MAP_REMOVE operation
429-
// as described in RTLM8, passing in the current key as ObjectsMapOp, and ObjectsMapEntry.timeserial as the operation's serial
450+
// 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
430451
applyMapRemoveOperation(
431452
key: key,
432453
operationTimeserial: entry.timeserial,
454+
operationSerialTimestamp: entry.serialTimestamp,
455+
logger: logger,
456+
clock: clock,
433457
)
434458
} else {
435459
// RTLM17a1: If ObjectsMapEntry.tombstone is false, apply the MAP_SET operation
@@ -538,6 +562,9 @@ internal final class InternalDefaultLiveMap: Sendable {
538562
let update = applyMapRemoveOperation(
539563
key: mapOp.key,
540564
operationTimeserial: applicableOperation.objectMessageSerial,
565+
operationSerialTimestamp: objectMessageSerialTimestamp,
566+
logger: logger,
567+
clock: clock,
541568
)
542569
// RTLM15d3a
543570
liveObjectMutableState.emit(update, on: userCallbackQueue)
@@ -578,17 +605,17 @@ internal final class InternalDefaultLiveMap: Sendable {
578605
// RTLM7a2: Otherwise, apply the operation
579606
// RTLM7a2a: Set ObjectsMapEntry.data to the ObjectData from the operation
580607
// RTLM7a2b: Set ObjectsMapEntry.timeserial to the operation's serial
581-
// RTLM7a2c: Set ObjectsMapEntry.tombstone to false
608+
// RTLM7a2c: Set ObjectsMapEntry.tombstone to false (same as RTLM7a2d: Set ObjectsMapEntry.tombstonedAt to nil)
582609
var updatedEntry = existingEntry
583610
updatedEntry.data = operationData
584611
updatedEntry.timeserial = operationTimeserial
585-
updatedEntry.tombstone = false
612+
updatedEntry.tombstonedAt = nil
586613
data[key] = updatedEntry
587614
} else {
588615
// RTLM7b: If an entry does not exist in the private data for the specified key
589616
// RTLM7b1: Create a new entry in data for the specified key with the provided ObjectData and the operation's serial
590-
// RTLM7b2: Set ObjectsMapEntry.tombstone for the new entry to false
591-
data[key] = InternalObjectsMapEntry(tombstone: false, timeserial: operationTimeserial, data: operationData)
617+
// RTLM7b2: Set ObjectsMapEntry.tombstone for the new entry to false (same as RTLM7b3: Set tombstonedAt to nil)
618+
data[key] = InternalObjectsMapEntry(tombstonedAt: nil, timeserial: operationTimeserial, data: operationData)
592619
}
593620

594621
// RTLM7c: If the operation has a non-empty ObjectData.objectId attribute
@@ -602,9 +629,21 @@ internal final class InternalDefaultLiveMap: Sendable {
602629
}
603630

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

635+
// Calculate the tombstonedAt for the new or updated entry per RTLM8f
636+
let tombstonedAt: Date?
637+
if let operationSerialTimestamp {
638+
// RTLM8f1
639+
tombstonedAt = operationSerialTimestamp
640+
} else {
641+
// RTLM8f2
642+
logger.log("serialTimestamp not provided for MAP_REMOVE, using local clock for tombstone timestamp", level: .debug)
643+
// RTLM8f2a
644+
tombstonedAt = clock.now
645+
}
646+
608647
// RTLM8a: If an entry exists in the private data for the specified key
609648
if let existingEntry = data[key] {
610649
// RTLM8a1: If the operation cannot be applied as per RTLM9, discard the operation
@@ -614,17 +653,19 @@ internal final class InternalDefaultLiveMap: Sendable {
614653
// RTLM8a2: Otherwise, apply the operation
615654
// RTLM8a2a: Set ObjectsMapEntry.data to undefined/null
616655
// RTLM8a2b: Set ObjectsMapEntry.timeserial to the operation's serial
617-
// RTLM8a2c: Set ObjectsMapEntry.tombstone to true
656+
// RTLM8a2c: Set ObjectsMapEntry.tombstone to true (equivalent to next point)
657+
// RTLM8a2d: Set ObjectsMapEntry.tombstonedAt per RTLM8a2d
618658
var updatedEntry = existingEntry
619659
updatedEntry.data = ObjectData()
620660
updatedEntry.timeserial = operationTimeserial
621-
updatedEntry.tombstone = true
661+
updatedEntry.tombstonedAt = tombstonedAt
622662
data[key] = updatedEntry
623663
} else {
624664
// RTLM8b: If an entry does not exist in the private data for the specified key
625665
// RTLM8b1: Create a new entry in data for the specified key, with ObjectsMapEntry.data set to undefined/null and the operation's serial
626666
// RTLM8b2: Set ObjectsMapEntry.tombstone for the new entry to true
627-
data[key] = InternalObjectsMapEntry(tombstone: true, timeserial: operationTimeserial, data: ObjectData())
667+
// RTLM8b3: Set ObjectsMapEntry.tombstonedAt per RTLM8f
668+
data[key] = InternalObjectsMapEntry(tombstonedAt: tombstonedAt, timeserial: operationTimeserial, data: ObjectData())
628669
}
629670

630671
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)