Skip to content

Commit 57de324

Browse files
Implement the createMap and createCounter methods
Based on [1] at cb11ba8. Implementation and tests by Cursor (both with some tweaking by me). TODO look at Cursor's tests and annotate Haven't implemented: - the RTO11e etc echoMessages check — deferred to #49 - the RTO11c etc channel mode checking - same reason as 392fae3 - using server time for generating object ID — deferred to #50 [1] ably/specification#353
1 parent 95845d7 commit 57de324

6 files changed

Lines changed: 358 additions & 15 deletions

File tree

Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift

Lines changed: 76 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -157,20 +157,88 @@ internal final class InternalDefaultRealtimeObjects: Sendable, LiveMapObjectPool
157157
}
158158
}
159159

160-
internal func createMap(entries _: [String: LiveMapValue]) async throws(ARTErrorInfo) -> any LiveMap {
161-
notYetImplemented()
160+
internal func createMap(entries: [String: InternalLiveMapValue], coreSDK: CoreSDK) async throws(ARTErrorInfo) -> InternalDefaultLiveMap {
161+
do throws(InternalError) {
162+
// RTO11d
163+
do {
164+
try coreSDK.validateChannelState(notIn: [.detached, .failed, .suspended], operationDescription: "RealtimeObjects.createMap")
165+
} catch {
166+
throw error.toInternalError()
167+
}
168+
169+
// RTO11f
170+
// TODO: This is a stopgap; change to use server time per RTO11f5 (https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/50)
171+
let timestamp = clock.now
172+
let creationOperation = ObjectCreationHelpers.creationOperationForLiveMap(
173+
entries: entries,
174+
timestamp: timestamp,
175+
)
176+
177+
// RTO11g
178+
try await coreSDK.publish(objectMessages: [creationOperation.objectMessage])
179+
180+
// RTO11h
181+
return mutex.withLock {
182+
mutableState.objectsPool.getOrCreateMap(
183+
creationOperation: creationOperation,
184+
logger: logger,
185+
userCallbackQueue: userCallbackQueue,
186+
clock: clock,
187+
)
188+
}
189+
} catch {
190+
throw error.toARTErrorInfo()
191+
}
162192
}
163193

164-
internal func createMap() async throws(ARTErrorInfo) -> any LiveMap {
165-
notYetImplemented()
194+
internal func createMap(coreSDK: CoreSDK) async throws(ARTErrorInfo) -> InternalDefaultLiveMap {
195+
// RTO11f4b
196+
try await createMap(entries: [:], coreSDK: coreSDK)
166197
}
167198

168-
internal func createCounter(count _: Double) async throws(ARTErrorInfo) -> any LiveCounter {
169-
notYetImplemented()
199+
internal func createCounter(count: Double, coreSDK: CoreSDK) async throws(ARTErrorInfo) -> InternalDefaultLiveCounter {
200+
do throws(InternalError) {
201+
// RTO12d
202+
do {
203+
try coreSDK.validateChannelState(notIn: [.detached, .failed, .suspended], operationDescription: "RealtimeObjects.createMap")
204+
} catch {
205+
throw error.toInternalError()
206+
}
207+
208+
// RTO12f1
209+
if !count.isFinite {
210+
throw LiveObjectsError.counterInitialValueInvalid(value: count).toARTErrorInfo().toInternalError()
211+
}
212+
213+
// RTO12f
214+
215+
// TODO: This is a stopgap; change to use server time per RTO12f5 (https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/50)
216+
let timestamp = clock.now
217+
let creationOperation = ObjectCreationHelpers.creationOperationForLiveCounter(
218+
count: count,
219+
timestamp: timestamp,
220+
)
221+
222+
// RTO12g
223+
try await coreSDK.publish(objectMessages: [creationOperation.objectMessage])
224+
225+
// RTO12h
226+
return mutex.withLock {
227+
mutableState.objectsPool.getOrCreateCounter(
228+
creationOperation: creationOperation,
229+
logger: logger,
230+
userCallbackQueue: userCallbackQueue,
231+
clock: clock,
232+
)
233+
}
234+
} catch {
235+
throw error.toARTErrorInfo()
236+
}
170237
}
171238

172-
internal func createCounter() async throws(ARTErrorInfo) -> any LiveCounter {
173-
notYetImplemented()
239+
internal func createCounter(coreSDK: CoreSDK) async throws(ARTErrorInfo) -> InternalDefaultLiveCounter {
240+
// RTO12f2a
241+
try await createCounter(count: 0, coreSDK: coreSDK)
174242
}
175243

176244
internal func batch(callback _: sending BatchCallback) async throws {

Sources/AblyLiveObjects/Internal/InternalLiveMapValue.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,30 @@ internal enum InternalLiveMapValue: Sendable, Equatable {
66
case liveMap(InternalDefaultLiveMap)
77
case liveCounter(InternalDefaultLiveCounter)
88

9+
// MARK: - Creating from a public LiveMapValue
10+
11+
/// Converts a public ``LiveMapValue`` into an ``InternalLiveMapValue``.
12+
///
13+
/// Needed in order to access the internals of user-provided LiveObject-valued LiveMap entries to extract their object ID.
14+
internal init(liveMapValue: LiveMapValue) {
15+
switch liveMapValue {
16+
case let .primitive(primitiveValue):
17+
self = .primitive(primitiveValue)
18+
case let .liveMap(publicLiveMap):
19+
guard let publicDefaultLiveMap = publicLiveMap as? PublicDefaultLiveMap else {
20+
// TODO: Try and remove this runtime check and know this type statically, see https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/37
21+
preconditionFailure("Expected PublicDefaultLiveMap, got \(publicLiveMap)")
22+
}
23+
self = .liveMap(publicDefaultLiveMap.proxied)
24+
case let .liveCounter(publicLiveCounter):
25+
guard let publicDefaultLiveCounter = publicLiveCounter as? PublicDefaultLiveCounter else {
26+
// TODO: Try and remove this runtime check and know this type statically, see https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/37
27+
preconditionFailure("Expected PublicDefaultLiveCounter, got \(publicLiveCounter)")
28+
}
29+
self = .liveCounter(publicDefaultLiveCounter.proxied)
30+
}
31+
}
32+
933
// MARK: - Representation in the Realtime protocol
1034

1135
/// Converts an `InternalLiveMapValue` to the value that should be used when creating or updating a map entry in the Realtime protocol, per the rules of RTO11f4 and RTLM20e4.

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

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,19 +36,54 @@ internal final class PublicDefaultRealtimeObjects: RealtimeObjects {
3636
}
3737

3838
internal func createMap(entries: [String: LiveMapValue]) async throws(ARTErrorInfo) -> any LiveMap {
39-
try await proxied.createMap(entries: entries)
39+
let internalEntries: [String: InternalLiveMapValue] = entries.mapValues { .init(liveMapValue: $0) }
40+
let internalMap = try await proxied.createMap(entries: internalEntries, coreSDK: coreSDK)
41+
42+
return PublicObjectsStore.shared.getOrCreateMap(
43+
proxying: internalMap,
44+
creationArgs: .init(
45+
coreSDK: coreSDK,
46+
delegate: proxied,
47+
logger: logger,
48+
),
49+
)
4050
}
4151

4252
internal func createMap() async throws(ARTErrorInfo) -> any LiveMap {
43-
try await proxied.createMap()
53+
let internalMap = try await proxied.createMap(coreSDK: coreSDK)
54+
55+
return PublicObjectsStore.shared.getOrCreateMap(
56+
proxying: internalMap,
57+
creationArgs: .init(
58+
coreSDK: coreSDK,
59+
delegate: proxied,
60+
logger: logger,
61+
),
62+
)
4463
}
4564

4665
internal func createCounter(count: Double) async throws(ARTErrorInfo) -> any LiveCounter {
47-
try await proxied.createCounter(count: count)
66+
let internalCounter = try await proxied.createCounter(count: count, coreSDK: coreSDK)
67+
68+
return PublicObjectsStore.shared.getOrCreateCounter(
69+
proxying: internalCounter,
70+
creationArgs: .init(
71+
coreSDK: coreSDK,
72+
logger: logger,
73+
),
74+
)
4875
}
4976

5077
internal func createCounter() async throws(ARTErrorInfo) -> any LiveCounter {
51-
try await proxied.createCounter()
78+
let internalCounter = try await proxied.createCounter(coreSDK: coreSDK)
79+
80+
return PublicObjectsStore.shared.getOrCreateCounter(
81+
proxying: internalCounter,
82+
creationArgs: .init(
83+
coreSDK: coreSDK,
84+
logger: logger,
85+
),
86+
)
5287
}
5388

5489
internal func batch(callback: sending BatchCallback) async throws {

Sources/AblyLiveObjects/Utility/Errors.swift

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,26 @@ import Ably
66
internal enum LiveObjectsError {
77
// operationDescription should be a description of a method like "LiveCounter.value"; it will be interpolated into an error message
88
case objectsOperationFailedInvalidChannelState(operationDescription: String, channelState: ARTRealtimeChannelState)
9+
case counterInitialValueInvalid(value: Double)
10+
case counterIncrementAmountInvalid(amount: Double)
911

1012
/// The ``ARTErrorInfo/code`` that should be returned for this error.
1113
internal var code: ARTErrorCode {
1214
switch self {
1315
case .objectsOperationFailedInvalidChannelState:
1416
.channelOperationFailedInvalidState
17+
case .counterInitialValueInvalid, .counterIncrementAmountInvalid:
18+
// RTO12f1, RTLC12e1
19+
.invalidParameterValue
1520
}
1621
}
1722

1823
/// The ``ARTErrorInfo/statusCode`` that should be returned for this error.
1924
internal var statusCode: Int {
2025
switch self {
21-
case .objectsOperationFailedInvalidChannelState:
26+
case .objectsOperationFailedInvalidChannelState,
27+
.counterInitialValueInvalid,
28+
.counterIncrementAmountInvalid:
2229
400
2330
}
2431
}
@@ -28,6 +35,10 @@ internal enum LiveObjectsError {
2835
switch self {
2936
case let .objectsOperationFailedInvalidChannelState(operationDescription: operationDescription, channelState: channelState):
3037
"\(operationDescription) operation failed (invalid channel state: \(channelState))"
38+
case let .counterInitialValueInvalid(value: value):
39+
"Invalid counter initial value (must be a finite number): \(value)"
40+
case let .counterIncrementAmountInvalid(amount: amount):
41+
"Invalid counter increment amount (must be a finite number): \(amount)"
3142
}
3243
}
3344

0 commit comments

Comments
 (0)