Skip to content

Commit f5f6294

Browse files
Implement increment / decrement / set / delete
Based on [1] at cb11ba8. Code by me, tests by Cursor. Channel mode checking and echoMessages check omitted as in 77ad484. TODO check Cursor's tests — I have looked at them but they need spec point annotation [1] ably/specification#353
1 parent 77ad484 commit f5f6294

8 files changed

Lines changed: 394 additions & 21 deletions

File tree

Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,43 @@ internal final class InternalDefaultLiveCounter: Sendable {
8989
}
9090
}
9191

92-
internal func increment(amount _: Double) async throws(ARTErrorInfo) {
93-
notYetImplemented()
92+
internal func increment(amount: Double, coreSDK: CoreSDK) async throws(ARTErrorInfo) {
93+
do throws(InternalError) {
94+
// RTLC12c
95+
do {
96+
try coreSDK.validateChannelState(notIn: [.detached, .failed, .suspended], operationDescription: "RealtimeObjects.createMap")
97+
} catch {
98+
throw error.toInternalError()
99+
}
100+
101+
// RTLC12e1
102+
if !amount.isFinite {
103+
throw LiveObjectsError.counterIncrementAmountInvalid(amount: amount).toARTErrorInfo().toInternalError()
104+
}
105+
106+
let objectMessage = OutboundObjectMessage(
107+
operation: .init(
108+
// RTLC12e2
109+
action: .known(.counterInc),
110+
// RTLC12e3
111+
objectId: objectID,
112+
counterOp: .init(
113+
// RTLC12e4
114+
amount: .init(value: amount),
115+
),
116+
),
117+
)
118+
119+
// RTLC12f
120+
try await coreSDK.publish(objectMessages: [objectMessage])
121+
} catch {
122+
throw error.toARTErrorInfo()
123+
}
94124
}
95125

96-
internal func decrement(amount _: Double) async throws(ARTErrorInfo) {
97-
notYetImplemented()
126+
internal func decrement(amount: Double, coreSDK: CoreSDK) async throws(ARTErrorInfo) {
127+
// RTLC13b
128+
try await increment(amount: -amount, coreSDK: coreSDK)
98129
}
99130

100131
@discardableResult

Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -168,12 +168,63 @@ internal final class InternalDefaultLiveMap: Sendable {
168168
try entries(coreSDK: coreSDK, delegate: delegate).map(\.value)
169169
}
170170

171-
internal func set(key _: String, value _: LiveMapValue) async throws(ARTErrorInfo) {
172-
notYetImplemented()
171+
internal func set(key: String, value: InternalLiveMapValue, coreSDK: CoreSDK) async throws(ARTErrorInfo) {
172+
do throws(InternalError) {
173+
// RTLM20c
174+
do {
175+
try coreSDK.validateChannelState(notIn: [.detached, .failed, .suspended], operationDescription: "RealtimeObjects.createMap")
176+
} catch {
177+
throw error.toInternalError()
178+
}
179+
180+
let objectMessage = OutboundObjectMessage(
181+
operation: .init(
182+
// RTLM20e2
183+
action: .known(.mapSet),
184+
// RTLM20e3
185+
objectId: objectID,
186+
mapOp: .init(
187+
// RTLM20e4
188+
key: key,
189+
// RTLM20e5
190+
data: value.toObjectData,
191+
),
192+
),
193+
)
194+
195+
try await coreSDK.publish(objectMessages: [objectMessage])
196+
} catch {
197+
throw error.toARTErrorInfo()
198+
}
173199
}
174200

175-
internal func remove(key _: String) async throws(ARTErrorInfo) {
176-
notYetImplemented()
201+
internal func remove(key: String, coreSDK: CoreSDK) async throws(ARTErrorInfo) {
202+
do throws(InternalError) {
203+
// RTLM21c
204+
do {
205+
try coreSDK.validateChannelState(notIn: [.detached, .failed, .suspended], operationDescription: "RealtimeObjects.createMap")
206+
} catch {
207+
throw error.toInternalError()
208+
}
209+
210+
let objectMessage = OutboundObjectMessage(
211+
operation: .init(
212+
// RTLM21e2
213+
action: .known(.mapRemove),
214+
// RTLM21e3
215+
objectId: objectID,
216+
mapOp: .init(
217+
// RTLM21e4
218+
key: key,
219+
),
220+
),
221+
)
222+
223+
// RTLM21f
224+
try await coreSDK.publish(objectMessages: [objectMessage])
225+
} catch {
226+
throw error.toARTErrorInfo()
227+
}
177228
}
178229

179230
@discardableResult

Sources/AblyLiveObjects/Protocol/ObjectMessage.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ internal struct InboundObjectMessage {
1818
}
1919

2020
/// An `ObjectMessage` to be sent in the `state` property of an `OBJECT` `ProtocolMessage`.
21-
internal struct OutboundObjectMessage {
21+
internal struct OutboundObjectMessage: Equatable {
2222
internal var id: String? // OM2a
2323
internal var clientId: String? // OM2b
2424
internal var connectionId: String?
@@ -44,7 +44,7 @@ internal struct PartialObjectOperation {
4444
internal var initialValue: String? // OOP3h
4545
}
4646

47-
internal struct ObjectOperation {
47+
internal struct ObjectOperation: Equatable {
4848
internal var action: WireEnum<ObjectOperationAction> // OOP3a
4949
internal var objectId: String // OOP3b
5050
internal var mapOp: ObjectsMapOp? // OOP3c
@@ -55,7 +55,7 @@ internal struct ObjectOperation {
5555
internal var initialValue: String? // OOP3h
5656
}
5757

58-
internal struct ObjectData {
58+
internal struct ObjectData: Equatable {
5959
internal var objectId: String? // OD2a
6060
internal var boolean: Bool? // OD2c
6161
internal var bytes: Data? // OD2d
@@ -64,24 +64,24 @@ internal struct ObjectData {
6464
internal var json: JSONObjectOrArray? // TODO: Needs specification (see https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/46)
6565
}
6666

67-
internal struct ObjectsMapOp {
67+
internal struct ObjectsMapOp: Equatable {
6868
internal var key: String // OMO2a
6969
internal var data: ObjectData? // OMO2b
7070
}
7171

72-
internal struct ObjectsMapEntry {
72+
internal struct ObjectsMapEntry: Equatable {
7373
internal var tombstone: Bool? // OME2a
7474
internal var timeserial: String? // OME2b
7575
internal var data: ObjectData // OME2c
7676
internal var serialTimestamp: Date? // OME2d
7777
}
7878

79-
internal struct ObjectsMap {
79+
internal struct ObjectsMap: Equatable {
8080
internal var semantics: WireEnum<ObjectsMapSemantics> // OMP3a
8181
internal var entries: [String: ObjectsMapEntry]? // OMP3b
8282
}
8383

84-
internal struct ObjectState {
84+
internal struct ObjectState: Equatable {
8585
internal var objectId: String // OST2a
8686
internal var siteTimeserials: [String: String] // OST2b
8787
internal var tombstone: Bool // OST2c

Sources/AblyLiveObjects/Protocol/WireObjectMessage.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,7 @@ extension WireObjectsMapOp: WireObjectCodable {
356356
}
357357
}
358358

359-
internal struct WireObjectsCounterOp {
359+
internal struct WireObjectsCounterOp: Equatable {
360360
internal var amount: NSNumber // OCO2a
361361
}
362362

@@ -410,7 +410,7 @@ extension WireObjectsMap: WireObjectCodable {
410410
}
411411
}
412412

413-
internal struct WireObjectsCounter {
413+
internal struct WireObjectsCounter: Equatable {
414414
internal var count: NSNumber? // OCN2a
415415
}
416416

Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicDefaultLiveCounter.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,11 @@ internal final class PublicDefaultLiveCounter: LiveCounter {
2727
}
2828

2929
internal func increment(amount: Double) async throws(ARTErrorInfo) {
30-
try await proxied.increment(amount: amount)
30+
try await proxied.increment(amount: amount, coreSDK: coreSDK)
3131
}
3232

3333
internal func decrement(amount: Double) async throws(ARTErrorInfo) {
34-
try await proxied.decrement(amount: amount)
34+
try await proxied.decrement(amount: amount, coreSDK: coreSDK)
3535
}
3636

3737
internal func subscribe(listener: @escaping LiveObjectUpdateCallback<LiveCounterUpdate>) throws(ARTErrorInfo) -> any SubscribeResponse {

Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicDefaultLiveMap.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,13 @@ internal final class PublicDefaultLiveMap: LiveMap {
7676
}
7777

7878
internal func set(key: String, value: LiveMapValue) async throws(ARTErrorInfo) {
79-
try await proxied.set(key: key, value: value)
79+
let internalValue = InternalLiveMapValue(liveMapValue: value)
80+
81+
try await proxied.set(key: key, value: internalValue, coreSDK: coreSDK)
8082
}
8183

8284
internal func remove(key: String) async throws(ARTErrorInfo) {
83-
try await proxied.remove(key: key)
85+
try await proxied.remove(key: key, coreSDK: coreSDK)
8486
}
8587

8688
internal func subscribe(listener: @escaping LiveObjectUpdateCallback<LiveMapUpdate>) throws(ARTErrorInfo) -> any SubscribeResponse {

Tests/AblyLiveObjectsTests/InternalDefaultLiveCounterTests.swift

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,4 +422,91 @@ struct InternalDefaultLiveCounterTests {
422422
#expect(subscriberInvocations.isEmpty)
423423
}
424424
}
425+
426+
/// Tests for the `increment` method, covering RTLC12 specification points
427+
struct IncrementTests {
428+
// @spec RTLC12c
429+
@Test(arguments: [.detached, .failed, .suspended] as [ARTRealtimeChannelState])
430+
func checksChannelState(channelState: ARTRealtimeChannelState) async throws {
431+
let logger = TestLogger()
432+
let counter = InternalDefaultLiveCounter.createZeroValued(objectID: "arbitrary", logger: logger, userCallbackQueue: .main, clock: MockSimpleClock())
433+
let coreSDK = MockCoreSDK(channelState: channelState)
434+
435+
await #expect {
436+
try await counter.increment(amount: 10, coreSDK: coreSDK)
437+
} throws: { error in
438+
guard let errorInfo = error as? ARTErrorInfo else {
439+
return false
440+
}
441+
442+
return errorInfo.code == 90001 && errorInfo.statusCode == 400
443+
}
444+
}
445+
446+
// @spec RTLC12e1 - Validation tests
447+
@Test(arguments: [
448+
Double.nan,
449+
Double.infinity,
450+
-Double.infinity,
451+
] as [Double])
452+
func throwsErrorForInvalidAmount(amount: Double) async throws {
453+
let logger = TestLogger()
454+
let counter = InternalDefaultLiveCounter.createZeroValued(objectID: "arbitrary", logger: logger, userCallbackQueue: .main, clock: MockSimpleClock())
455+
let coreSDK = MockCoreSDK(channelState: .attached)
456+
457+
await #expect {
458+
try await counter.increment(amount: amount, coreSDK: coreSDK)
459+
} throws: { error in
460+
guard let errorInfo = error as? ARTErrorInfo else {
461+
return false
462+
}
463+
464+
return errorInfo.code == 40003 && errorInfo.statusCode == 400
465+
}
466+
}
467+
468+
// @spec RTLC12e2, RTLC12e3, RTLC12e4 - Message creation tests
469+
func publishesCorrectObjectMessage() async throws {
470+
let logger = TestLogger()
471+
let counter = InternalDefaultLiveCounter.createZeroValued(objectID: "counter:test@123", logger: logger, userCallbackQueue: .main, clock: MockSimpleClock())
472+
let coreSDK = MockCoreSDK(channelState: .attached)
473+
474+
var publishedMessage: OutboundObjectMessage?
475+
coreSDK.setPublishHandler { messages in
476+
publishedMessage = messages.first
477+
}
478+
479+
try await counter.increment(amount: 10.5, coreSDK: coreSDK)
480+
481+
let expectedMessage = OutboundObjectMessage(
482+
operation: ObjectOperation(
483+
action: .known(.counterInc),
484+
objectId: "counter:test@123",
485+
counterOp: WireObjectsCounterOp(amount: NSNumber(value: 10.5)),
486+
),
487+
)
488+
let message = try #require(publishedMessage)
489+
#expect(message == expectedMessage)
490+
}
491+
492+
@Test
493+
func throwsErrorWhenPublishFails() async throws {
494+
let logger = TestLogger()
495+
let counter = InternalDefaultLiveCounter.createZeroValued(objectID: "counter:test@123", logger: logger, userCallbackQueue: .main, clock: MockSimpleClock())
496+
let coreSDK = MockCoreSDK(channelState: .attached)
497+
498+
coreSDK.setPublishHandler { _ throws(InternalError) in
499+
throw InternalError.other(.generic(NSError(domain: "test", code: 0, userInfo: [NSLocalizedDescriptionKey: "Publish failed"])))
500+
}
501+
502+
await #expect {
503+
try await counter.increment(amount: 10, coreSDK: coreSDK)
504+
} throws: { error in
505+
guard let errorInfo = error as? ARTErrorInfo else {
506+
return false
507+
}
508+
return errorInfo.message.contains("Publish failed")
509+
}
510+
}
511+
}
425512
}

0 commit comments

Comments
 (0)