Skip to content

Commit 8ec90e4

Browse files
Pull "apply initial value" into a separate operation, per the spec
This applies the changes from [1] at 29276a5. Development approach as described in 4494033. [1] ably/specification#343
1 parent 519332a commit 8ec90e4

File tree

4 files changed

+212
-121
lines changed

4 files changed

+212
-121
lines changed

Sources/AblyLiveObjects/Internal/DefaultLiveCounter.swift

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,13 @@ internal final class DefaultLiveCounter: LiveCounter {
123123
}
124124
}
125125

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+
126133
// MARK: - Mutable state and the operations that affect it
127134

128135
private struct MutableState {
@@ -143,15 +150,20 @@ internal final class DefaultLiveCounter: LiveCounter {
143150
// RTLC6c: Set data to the value of ObjectState.counter.count, or to 0 if it does not exist
144151
data = state.counter?.count?.doubleValue ?? 0
145152

146-
// RTLC6d: If ObjectState.createOp is present
153+
// RTLC6d: If ObjectState.createOp is present, merge the initial value into the LiveCounter as described in RTLC10
147154
if let createOp = state.createOp {
148-
// RTLC6d1: Add ObjectState.createOp.counter.count to data, if it exists
149-
if let createOpCount = createOp.counter?.count?.doubleValue {
150-
data += createOpCount
151-
}
152-
// RTLC6d2: Set the private flag createOperationIsMerged to true
153-
liveObject.createOperationIsMerged = true
155+
mergeInitialValue(from: createOp)
156+
}
157+
}
158+
159+
/// Merges the initial value from an ObjectOperation into this LiveCounter, per RTLC10.
160+
internal mutating func mergeInitialValue(from operation: ObjectOperation) {
161+
// RTLC10a: Add ObjectOperation.counter.count to data, if it exists
162+
if let operationCount = operation.counter?.count?.doubleValue {
163+
data += operationCount
154164
}
165+
// RTLC10b: Set the private flag createOperationIsMerged to true
166+
liveObject.createOperationIsMerged = true
155167
}
156168
}
157169
}

Sources/AblyLiveObjects/Internal/DefaultLiveMap.swift

Lines changed: 56 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,19 @@ internal final class DefaultLiveMap: LiveMap {
244244
}
245245
}
246246

247+
/// Test-only method to merge initial value from an ObjectOperation, per RTLM17.
248+
internal func testsOnly_mergeInitialValue(from operation: ObjectOperation, objectsPool: inout ObjectsPool) {
249+
mutex.withLock {
250+
mutableState.mergeInitialValue(
251+
from: operation,
252+
objectsPool: &objectsPool,
253+
mapDelegate: delegate.referenced,
254+
coreSDK: coreSDK,
255+
logger: logger,
256+
)
257+
}
258+
}
259+
247260
/// Applies a `MAP_SET` operation to a key, per RTLM7.
248261
///
249262
/// This is currently exposed just so that the tests can test RTLM7 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.
@@ -310,36 +323,53 @@ internal final class DefaultLiveMap: LiveMap {
310323
// RTLM6c: Set data to ObjectState.map.entries, or to an empty map if it does not exist
311324
data = state.map?.entries ?? [:]
312325

313-
// RTLM6d: If ObjectState.createOp is present
326+
// RTLM6d: If ObjectState.createOp is present, merge the initial value into the LiveMap as described in RTLM17
314327
if let createOp = state.createOp {
315-
// RTLM6d1: For each key–ObjectsMapEntry pair in ObjectState.createOp.map.entries
316-
if let entries = createOp.map?.entries {
317-
for (key, entry) in entries {
318-
if entry.tombstone == true {
319-
// RTLM6d1b: If ObjectsMapEntry.tombstone is true, apply the MAP_REMOVE operation
320-
// to the specified key using ObjectsMapEntry.timeserial per RTLM8
321-
applyMapRemoveOperation(
322-
key: key,
323-
operationTimeserial: entry.timeserial,
324-
)
325-
} else {
326-
// RTLM6d1a: If ObjectsMapEntry.tombstone is false, apply the MAP_SET operation
327-
// to the specified key using ObjectsMapEntry.timeserial and ObjectsMapEntry.data per RTLM7
328-
applyMapSetOperation(
329-
key: key,
330-
operationTimeserial: entry.timeserial,
331-
operationData: entry.data,
332-
objectsPool: &objectsPool,
333-
mapDelegate: mapDelegate,
334-
coreSDK: coreSDK,
335-
logger: logger,
336-
)
337-
}
328+
mergeInitialValue(
329+
from: createOp,
330+
objectsPool: &objectsPool,
331+
mapDelegate: mapDelegate,
332+
coreSDK: coreSDK,
333+
logger: logger,
334+
)
335+
}
336+
}
337+
338+
/// Merges the initial value from an ObjectOperation into this LiveMap, per RTLM17.
339+
internal mutating func mergeInitialValue(
340+
from operation: ObjectOperation,
341+
objectsPool: inout ObjectsPool,
342+
mapDelegate: LiveMapObjectPoolDelegate?,
343+
coreSDK: CoreSDK,
344+
logger: AblyPlugin.Logger,
345+
) {
346+
// RTLM17a: For each key–ObjectsMapEntry pair in ObjectOperation.map.entries
347+
if let entries = operation.map?.entries {
348+
for (key, entry) in entries {
349+
if entry.tombstone == true {
350+
// RTLM17a2: If ObjectsMapEntry.tombstone is true, apply the MAP_REMOVE operation
351+
// as described in RTLM8, passing in the current key as ObjectsMapOp, and ObjectsMapEntry.timeserial as the operation's serial
352+
applyMapRemoveOperation(
353+
key: key,
354+
operationTimeserial: entry.timeserial,
355+
)
356+
} else {
357+
// RTLM17a1: If ObjectsMapEntry.tombstone is false, apply the MAP_SET operation
358+
// as described in RTLM7, passing in ObjectsMapEntry.data and the current key as ObjectsMapOp, and ObjectsMapEntry.timeserial as the operation's serial
359+
applyMapSetOperation(
360+
key: key,
361+
operationTimeserial: entry.timeserial,
362+
operationData: entry.data,
363+
objectsPool: &objectsPool,
364+
mapDelegate: mapDelegate,
365+
coreSDK: coreSDK,
366+
logger: logger,
367+
)
338368
}
339369
}
340-
// RTLM6d2: Set the private flag createOperationIsMerged to true
341-
liveObject.createOperationIsMerged = true
342370
}
371+
// RTLM17b: Set the private flag createOperationIsMerged to true
372+
liveObject.createOperationIsMerged = true
343373
}
344374

345375
/// Applies a `MAP_SET` operation to a key, per RTLM7.

Tests/AblyLiveObjectsTests/DefaultLiveCounterTests.swift

Lines changed: 57 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ struct DefaultLiveCounterTests {
5252

5353
/// Tests for the case where createOp is not present
5454
struct WithoutCreateOpTests {
55-
// @spec RTLC6b - Tests the case without createOp, as RTLC6d2 takes precedence when createOp exists
55+
// @spec RTLC6b - Tests the case without createOp, as RTLC10b takes precedence when createOp exists
5656
@Test
5757
func setsCreateOperationIsMergedToFalse() {
5858
let logger = TestLogger()
@@ -88,52 +88,74 @@ struct DefaultLiveCounterTests {
8888
}
8989
}
9090

91-
/// Tests for RTLC6d (with createOp present)
91+
/// Tests for RTLC10 (merge initial value from createOp)
9292
struct WithCreateOpTests {
93-
// @specOneOf(1/2) RTLC6d1 - with count
94-
// @specOneOf(3/4) RTLC6c - count and createOp
93+
// @spec RTLC10 - Tests that replaceData merges initial value when createOp is present
9594
@Test
96-
func setsDataToCounterCountThenAddsCreateOpCounterCount() throws {
95+
func mergesInitialValueWhenCreateOpPresent() throws {
9796
let logger = TestLogger()
9897
let counter = DefaultLiveCounter.createZeroValued(objectID: "arbitrary", coreSDK: MockCoreSDK(channelState: .attaching), logger: logger)
9998
let state = TestFactories.counterObjectState(
10099
createOp: TestFactories.counterCreateOperation(count: 10), // Test value - must exist
101100
count: 5, // Test value - must exist
102101
)
103102
counter.replaceData(using: state)
104-
#expect(try counter.value == 15) // First sets to 5 (RTLC6c) then adds 10 (RTLC6d1)
103+
#expect(try counter.value == 15) // First sets to 5 (RTLC6c) then adds 10 (RTLC10a)
104+
#expect(counter.testsOnly_createOperationIsMerged == true)
105105
}
106+
}
107+
}
106108

107-
// @specOneOf(2/2) RTLC6d1 - no count
108-
// @specOneOf(4/4) RTLC6c - no count but createOp
109-
@Test
110-
func doesNotModifyDataWhenCreateOpCounterCountDoesNotExist() throws {
111-
let logger = TestLogger()
112-
let counter = DefaultLiveCounter.createZeroValued(objectID: "arbitrary", coreSDK: MockCoreSDK(channelState: .attaching), logger: logger)
113-
let state = TestFactories.counterObjectState(
114-
createOp: TestFactories.objectOperation(
115-
action: .known(.counterCreate),
116-
counter: nil, // Test value - must be nil
117-
),
118-
count: 5, // Test value
119-
)
120-
counter.replaceData(using: state)
121-
#expect(try counter.value == 5) // Only the base counter.count value
122-
}
109+
/// Tests for the `testsOnly_mergeInitialValue` method, covering RTLC10 specification points
110+
struct MergeInitialValueTests {
111+
// @specOneOf(1/2) RTLC10a - with count
112+
@Test
113+
func addsCounterCountToData() throws {
114+
let logger = TestLogger()
115+
let counter = DefaultLiveCounter.createZeroValued(objectID: "arbitrary", coreSDK: MockCoreSDK(channelState: .attaching), logger: logger)
123116

124-
// @spec RTLC6d2
125-
@Test
126-
func setsCreateOperationIsMergedToTrue() {
127-
let logger = TestLogger()
128-
let counter = DefaultLiveCounter.createZeroValued(objectID: "arbitrary", coreSDK: MockCoreSDK(channelState: .attaching), logger: logger)
129-
let state = TestFactories.counterObjectState(
130-
createOp: TestFactories.objectOperation( // Test value - must be non-nil
131-
action: .known(.counterCreate),
132-
),
133-
)
134-
counter.replaceData(using: state)
135-
#expect(counter.testsOnly_createOperationIsMerged == true)
136-
}
117+
// Set initial data
118+
counter.replaceData(using: TestFactories.counterObjectState(count: 5))
119+
#expect(try counter.value == 5)
120+
121+
// Apply merge operation
122+
let operation = TestFactories.counterCreateOperation(count: 10) // Test value - must exist
123+
counter.testsOnly_mergeInitialValue(from: operation)
124+
125+
#expect(try counter.value == 15) // 5 + 10
126+
}
127+
128+
// @specOneOf(2/2) RTLC10a - no count
129+
@Test
130+
func doesNotModifyDataWhenCounterCountDoesNotExist() throws {
131+
let logger = TestLogger()
132+
let counter = DefaultLiveCounter.createZeroValued(objectID: "arbitrary", coreSDK: MockCoreSDK(channelState: .attaching), logger: logger)
133+
134+
// Set initial data
135+
counter.replaceData(using: TestFactories.counterObjectState(count: 5))
136+
#expect(try counter.value == 5)
137+
138+
// Apply merge operation with no count
139+
let operation = TestFactories.objectOperation(
140+
action: .known(.counterCreate),
141+
counter: nil, // Test value - must be nil
142+
)
143+
counter.testsOnly_mergeInitialValue(from: operation)
144+
145+
#expect(try counter.value == 5) // Unchanged
146+
}
147+
148+
// @spec RTLC10b
149+
@Test
150+
func setsCreateOperationIsMergedToTrue() {
151+
let logger = TestLogger()
152+
let counter = DefaultLiveCounter.createZeroValued(objectID: "arbitrary", coreSDK: MockCoreSDK(channelState: .attaching), logger: logger)
153+
154+
// Apply merge operation
155+
let operation = TestFactories.counterCreateOperation(count: 10) // Test value - must exist
156+
counter.testsOnly_mergeInitialValue(from: operation)
157+
158+
#expect(counter.testsOnly_createOperationIsMerged == true)
137159
}
138160
}
139161
}

0 commit comments

Comments
 (0)