Skip to content

Commit 1474140

Browse files
Merge pull request #51 from ably/ECO-5435-write-API
[ECO-5435] Implement the write spec
2 parents 93c41af + 4e8e076 commit 1474140

21 files changed

Lines changed: 1477 additions & 59 deletions

.cursor/rules/swift.mdc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ When writing Swift:
1111
- When writing initializer expressions, when the type that is being initialized can be inferred, favour using the implicit `.init(…)` form instead of explicitly writing the type name.
1212
- When writing enum value expressions, when the type that is being initialized can be inferred, favour using the implicit `.caseName` form instead of explicitly writing the type name.
1313
- When writing JSONValue or WireValue types, favour using the literal syntax enabled by their conformance to the `ExpressibleBy*Literal` protocols where possible.
14+
- When writing a JSON string, favour using Swift raw string literals instead of escaping double quotes.
1415
- When you need to import the following modules inside the AblyLiveObjects library code (that is, in non-test code), do so in the following way:
1516
- Ably: use `import Ably`
1617
- AblyPlugin: use `internal import AblyPlugin`

Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,46 @@ 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(
97+
notIn: [.detached, .failed, .suspended],
98+
operationDescription: "LiveCounter.increment",
99+
)
100+
} catch {
101+
throw error.toInternalError()
102+
}
103+
104+
// RTLC12e1
105+
if !amount.isFinite {
106+
throw LiveObjectsError.counterIncrementAmountInvalid(amount: amount).toARTErrorInfo().toInternalError()
107+
}
108+
109+
let objectMessage = OutboundObjectMessage(
110+
operation: .init(
111+
// RTLC12e2
112+
action: .known(.counterInc),
113+
// RTLC12e3
114+
objectId: objectID,
115+
counterOp: .init(
116+
// RTLC12e4
117+
amount: .init(value: amount),
118+
),
119+
),
120+
)
121+
122+
// RTLC12f
123+
try await coreSDK.publish(objectMessages: [objectMessage])
124+
} catch {
125+
throw error.toARTErrorInfo()
126+
}
94127
}
95128

96-
internal func decrement(amount _: Double) async throws(ARTErrorInfo) {
97-
notYetImplemented()
129+
internal func decrement(amount: Double, coreSDK: CoreSDK) async throws(ARTErrorInfo) {
130+
// RTLC13b
131+
try await increment(amount: -amount, coreSDK: coreSDK)
98132
}
99133

100134
@discardableResult

Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift

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

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

180-
internal func remove(key _: String) async throws(ARTErrorInfo) {
181-
notYetImplemented()
206+
internal func remove(key: String, coreSDK: CoreSDK) async throws(ARTErrorInfo) {
207+
do throws(InternalError) {
208+
// RTLM21c
209+
do {
210+
try coreSDK.validateChannelState(notIn: [.detached, .failed, .suspended], operationDescription: "LiveMap.remove")
211+
} catch {
212+
throw error.toInternalError()
213+
}
214+
215+
let objectMessage = OutboundObjectMessage(
216+
operation: .init(
217+
// RTLM21e2
218+
action: .known(.mapRemove),
219+
// RTLM21e3
220+
objectId: objectID,
221+
mapOp: .init(
222+
// RTLM21e4
223+
key: key,
224+
),
225+
),
226+
)
227+
228+
// RTLM21f
229+
try await coreSDK.publish(objectMessages: [objectMessage])
230+
} catch {
231+
throw error.toARTErrorInfo()
232+
}
182233
}
183234

184235
@discardableResult

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.createCounter")
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: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,60 @@ 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+
33+
// MARK: - Representation in the Realtime protocol
34+
35+
/// 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.
36+
internal var toObjectData: ObjectData {
37+
// RTO11f4c1: Create an ObjectsMapEntry for the current value
38+
switch self {
39+
case let .primitive(primitiveValue):
40+
switch primitiveValue {
41+
case let .bool(value):
42+
.init(boolean: value)
43+
case let .data(value):
44+
.init(bytes: value)
45+
case let .number(value):
46+
.init(number: NSNumber(value: value))
47+
case let .string(value):
48+
.init(string: value)
49+
case let .jsonArray(value):
50+
.init(json: .array(value))
51+
case let .jsonObject(value):
52+
.init(json: .object(value))
53+
}
54+
case let .liveMap(liveMap):
55+
// RTO11f4c1a: If the value is of type LiveMap, set ObjectsMapEntry.data.objectId to the objectId of that object
56+
.init(objectId: liveMap.objectID)
57+
case let .liveCounter(liveCounter):
58+
// RTO11f4c1a: If the value is of type LiveCounter, set ObjectsMapEntry.data.objectId to the objectId of that object
59+
.init(objectId: liveCounter.objectID)
60+
}
61+
}
62+
963
// MARK: - Convenience getters for associated values
1064

1165
/// If this `InternalLiveMapValue` has case `primitive`, this returns the associated value. Else, it returns `nil`.

Sources/AblyLiveObjects/Internal/InternalObjectsMapEntry.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Foundation
22

33
/// The entries stored in a `LiveMap`'s data. Same as an `ObjectsMapEntry` but with an additional `tombstonedAt` property, per RTLM3a.
4-
internal struct InternalObjectsMapEntry {
4+
internal struct InternalObjectsMapEntry: Equatable {
55
internal var tombstonedAt: Date? // RTLM3a
66
internal var tombstone: Bool {
77
// TODO: Confirm that we don't need to store this (https://github.com/ably/specification/pull/350/files#r2213895661)

0 commit comments

Comments
 (0)