Skip to content

Commit 77ad484

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 87aa972 commit 77ad484

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
@@ -113,20 +113,88 @@ internal final class InternalDefaultRealtimeObjects: Sendable, LiveMapObjectPool
113113
}
114114
}
115115

116-
internal func createMap(entries _: [String: LiveMapValue]) async throws(ARTErrorInfo) -> any LiveMap {
117-
notYetImplemented()
116+
internal func createMap(entries: [String: InternalLiveMapValue], coreSDK: CoreSDK) async throws(ARTErrorInfo) -> InternalDefaultLiveMap {
117+
do throws(InternalError) {
118+
// RTO11d
119+
do {
120+
try coreSDK.validateChannelState(notIn: [.detached, .failed, .suspended], operationDescription: "RealtimeObjects.createMap")
121+
} catch {
122+
throw error.toInternalError()
123+
}
124+
125+
// RTO11f
126+
// TODO: This is a stopgap; change to use server time per RTO11f5 (https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/50)
127+
let timestamp = clock.now
128+
let creationOperation = ObjectCreationHelpers.creationOperationForLiveMap(
129+
entries: entries,
130+
timestamp: timestamp,
131+
)
132+
133+
// RTO11g
134+
try await coreSDK.publish(objectMessages: [creationOperation.objectMessage])
135+
136+
// RTO11h
137+
return mutex.withLock {
138+
mutableState.objectsPool.getOrCreateMap(
139+
creationOperation: creationOperation,
140+
logger: logger,
141+
userCallbackQueue: userCallbackQueue,
142+
clock: clock,
143+
)
144+
}
145+
} catch {
146+
throw error.toARTErrorInfo()
147+
}
118148
}
119149

120-
internal func createMap() async throws(ARTErrorInfo) -> any LiveMap {
121-
notYetImplemented()
150+
internal func createMap(coreSDK: CoreSDK) async throws(ARTErrorInfo) -> InternalDefaultLiveMap {
151+
// RTO11f4b
152+
try await createMap(entries: [:], coreSDK: coreSDK)
122153
}
123154

124-
internal func createCounter(count _: Double) async throws(ARTErrorInfo) -> any LiveCounter {
125-
notYetImplemented()
155+
internal func createCounter(count: Double, coreSDK: CoreSDK) async throws(ARTErrorInfo) -> InternalDefaultLiveCounter {
156+
do throws(InternalError) {
157+
// RTO12d
158+
do {
159+
try coreSDK.validateChannelState(notIn: [.detached, .failed, .suspended], operationDescription: "RealtimeObjects.createMap")
160+
} catch {
161+
throw error.toInternalError()
162+
}
163+
164+
// RTO12f1
165+
if !count.isFinite {
166+
throw LiveObjectsError.counterInitialValueInvalid(value: count).toARTErrorInfo().toInternalError()
167+
}
168+
169+
// RTO12f
170+
171+
// TODO: This is a stopgap; change to use server time per RTO12f5 (https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/50)
172+
let timestamp = clock.now
173+
let creationOperation = ObjectCreationHelpers.creationOperationForLiveCounter(
174+
count: count,
175+
timestamp: timestamp,
176+
)
177+
178+
// RTO12g
179+
try await coreSDK.publish(objectMessages: [creationOperation.objectMessage])
180+
181+
// RTO12h
182+
return mutex.withLock {
183+
mutableState.objectsPool.getOrCreateCounter(
184+
creationOperation: creationOperation,
185+
logger: logger,
186+
userCallbackQueue: userCallbackQueue,
187+
clock: clock,
188+
)
189+
}
190+
} catch {
191+
throw error.toARTErrorInfo()
192+
}
126193
}
127194

128-
internal func createCounter() async throws(ARTErrorInfo) -> any LiveCounter {
129-
notYetImplemented()
195+
internal func createCounter(coreSDK: CoreSDK) async throws(ARTErrorInfo) -> InternalDefaultLiveCounter {
196+
// RTO12f2a
197+
try await createCounter(count: 0, coreSDK: coreSDK)
130198
}
131199

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