Skip to content

Commit 36982fe

Browse files
Merge pull request #22 from ably/ECO-5330-LiveMap-access-API
[ECO-5330] Implement remaining `LiveMap` access API
2 parents 9ce0b13 + 8d881e2 commit 36982fe

9 files changed

Lines changed: 350 additions & 74 deletions

File tree

Sources/AblyLiveObjects/DefaultRealtimeObjects.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,11 @@ internal final class DefaultRealtimeObjects: RealtimeObjects, LiveMapObjectPoolD
9090
// RTO1b: If the channel is in the DETACHED or FAILED state, the library should indicate an error with code 90001
9191
let currentChannelState = coreSDK.channelState
9292
if currentChannelState == .detached || currentChannelState == .failed {
93-
throw ARTErrorInfo.create(withCode: Int(ARTErrorCode.channelOperationFailedInvalidState.rawValue), message: "getRoot operation failed (invalid channel state: \(currentChannelState))")
93+
throw LiveObjectsError.objectsOperationFailedInvalidChannelState(
94+
operationDescription: "getRoot",
95+
channelState: currentChannelState,
96+
)
97+
.toARTErrorInfo()
9498
}
9599

96100
let syncStatus = mutex.withLock {

Sources/AblyLiveObjects/Internal/DefaultLiveCounter.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,11 @@ internal final class DefaultLiveCounter: LiveCounter {
6969
// RTLC5b: If the channel is in the DETACHED or FAILED state, the library should indicate an error with code 90001
7070
let currentChannelState = coreSDK.channelState
7171
if currentChannelState == .detached || currentChannelState == .failed {
72-
throw ARTErrorInfo.create(withCode: Int(ARTErrorCode.channelOperationFailedInvalidState.rawValue), message: "LiveCounter.value operation failed (invalid channel state: \(currentChannelState))")
72+
throw LiveObjectsError.objectsOperationFailedInvalidChannelState(
73+
operationDescription: "LiveCounter.value",
74+
channelState: currentChannelState,
75+
)
76+
.toARTErrorInfo()
7377
}
7478

7579
return mutex.withLock {

Sources/AblyLiveObjects/Internal/DefaultLiveMap.swift

Lines changed: 116 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,11 @@ internal final class DefaultLiveMap: LiveMap {
108108
// RTLM5c: If the channel is in the DETACHED or FAILED state, the library should indicate an error with code 90001
109109
let currentChannelState = coreSDK.channelState
110110
if currentChannelState == .detached || currentChannelState == .failed {
111-
throw ARTErrorInfo.create(withCode: Int(ARTErrorCode.channelOperationFailedInvalidState.rawValue), message: "LiveMap.get operation failed (invalid channel state: \(currentChannelState))")
111+
throw LiveObjectsError.objectsOperationFailedInvalidChannelState(
112+
operationDescription: "LiveMap.get",
113+
channelState: currentChannelState,
114+
)
115+
.toARTErrorInfo()
112116
}
113117

114118
let entry = mutex.withLock {
@@ -120,78 +124,74 @@ internal final class DefaultLiveMap: LiveMap {
120124
return nil
121125
}
122126

123-
// RTLM5d2: If a ObjectsMapEntry exists at the key
124-
125-
// RTLM5d2a: If ObjectsMapEntry.tombstone is true, return undefined/null
126-
if entry.tombstone == true {
127-
return nil
128-
}
129-
130-
// Handle primitive values in the order specified by RTLM5d2b through RTLM5d2e
131-
132-
// RTLM5d2b: If ObjectsMapEntry.data.boolean exists, return it
133-
if let boolean = entry.data.boolean {
134-
return .primitive(.bool(boolean))
135-
}
136-
137-
// RTLM5d2c: If ObjectsMapEntry.data.bytes exists, return it
138-
if let bytes = entry.data.bytes {
139-
return .primitive(.data(bytes))
140-
}
127+
// RTLM5d2: If a ObjectsMapEntry exists at the key, convert it using the shared logic
128+
return convertEntryToLiveMapValue(entry)
129+
}
141130

142-
// RTLM5d2d: If ObjectsMapEntry.data.number exists, return it
143-
if let number = entry.data.number {
144-
return .primitive(.number(number.doubleValue))
145-
}
131+
internal var size: Int {
132+
get throws(ARTErrorInfo) {
133+
// RTLM10c: If the channel is in the DETACHED or FAILED state, the library should throw an ErrorInfo error with statusCode 400 and code 90001
134+
let currentChannelState = coreSDK.channelState
135+
if currentChannelState == .detached || currentChannelState == .failed {
136+
throw LiveObjectsError.objectsOperationFailedInvalidChannelState(
137+
operationDescription: "LiveMap.size",
138+
channelState: currentChannelState,
139+
)
140+
.toARTErrorInfo()
141+
}
146142

147-
// RTLM5d2e: If ObjectsMapEntry.data.string exists, return it
148-
if let string = entry.data.string {
149-
switch string {
150-
case let .string(string):
151-
return .primitive(.string(string))
152-
case .json:
153-
// TODO: Understand how to handle JSON values (https://github.com/ably/specification/pull/333/files#r2164561055)
154-
notYetImplemented()
143+
return mutex.withLock {
144+
// RTLM10d: Returns the number of non-tombstoned entries (per RTLM14) in the internal data map
145+
mutableState.data.values.count { entry in
146+
// RTLM14a: The method returns true if ObjectsMapEntry.tombstone is true
147+
// RTLM14b: Otherwise, it returns false
148+
entry.tombstone != true
149+
}
155150
}
156151
}
152+
}
157153

158-
// RTLM5d2f: If ObjectsMapEntry.data.objectId exists, get the object stored at that objectId from the internal ObjectsPool
159-
if let objectId = entry.data.objectId {
160-
// RTLM5d2f1: If an object with id objectId does not exist, return undefined/null
161-
guard let poolEntry = delegate.referenced?.getObjectFromPool(id: objectId) else {
162-
return nil
154+
internal var entries: [(key: String, value: LiveMapValue)] {
155+
get throws(ARTErrorInfo) {
156+
// RTLM11c: If the channel is in the DETACHED or FAILED state, the library should throw an ErrorInfo error with statusCode 400 and code 90001
157+
let currentChannelState = coreSDK.channelState
158+
if currentChannelState == .detached || currentChannelState == .failed {
159+
throw LiveObjectsError.objectsOperationFailedInvalidChannelState(
160+
operationDescription: "LiveMap.entries",
161+
channelState: currentChannelState,
162+
)
163+
.toARTErrorInfo()
163164
}
164165

165-
// RTLM5d2f2: If an object with id objectId exists, return it
166-
switch poolEntry {
167-
case let .map(map):
168-
return .liveMap(map)
169-
case let .counter(counter):
170-
return .liveCounter(counter)
171-
}
172-
}
166+
return mutex.withLock {
167+
// RTLM11d: Returns key-value pairs from the internal data map
168+
// RTLM11d1: Pairs with tombstoned entries (per RTLM14) are not returned
169+
var result: [(key: String, value: LiveMapValue)] = []
173170

174-
// RTLM5d2g: Otherwise, return undefined/null
175-
return nil
176-
}
171+
for (key, entry) in mutableState.data {
172+
// Convert entry to LiveMapValue using the same logic as get(key:)
173+
if let value = convertEntryToLiveMapValue(entry) {
174+
result.append((key: key, value: value))
175+
}
176+
}
177177

178-
internal var size: Int {
179-
mutex.withLock {
180-
// TODO: this is not yet specified, but it seems like the obvious right thing and it unlocks some integration tests; add spec point once specified
181-
mutableState.data.count
178+
return result
179+
}
182180
}
183181
}
184182

185-
internal var entries: [(key: String, value: LiveMapValue)] {
186-
notYetImplemented()
187-
}
188-
189183
internal var keys: [String] {
190-
notYetImplemented()
184+
get throws(ARTErrorInfo) {
185+
// RTLM12b: Identical to LiveMap#entries, except that it returns only the keys from the internal data map
186+
try entries.map(\.key)
187+
}
191188
}
192189

193190
internal var values: [LiveMapValue] {
194-
notYetImplemented()
191+
get throws(ARTErrorInfo) {
192+
// RTLM13b: Identical to LiveMap#entries, except that it returns only the values from the internal data map
193+
try entries.map(\.value)
194+
}
195195
}
196196

197197
internal func set(key _: String, value _: LiveMapValue) async throws(ARTErrorInfo) {
@@ -441,4 +441,63 @@ internal final class DefaultLiveMap: LiveMap {
441441
}
442442
}
443443
}
444+
445+
// MARK: - Helper Methods
446+
447+
/// Converts an ObjectsMapEntry to LiveMapValue using the same logic as get(key:)
448+
/// This is used by entries to ensure consistent value conversion
449+
private func convertEntryToLiveMapValue(_ entry: ObjectsMapEntry) -> LiveMapValue? {
450+
// RTLM5d2a: If ObjectsMapEntry.tombstone is true, return undefined/null
451+
// This is also equivalent to the RTLM14 check
452+
if entry.tombstone == true {
453+
return nil
454+
}
455+
456+
// Handle primitive values in the order specified by RTLM5d2b through RTLM5d2e
457+
458+
// RTLM5d2b: If ObjectsMapEntry.data.boolean exists, return it
459+
if let boolean = entry.data.boolean {
460+
return .primitive(.bool(boolean))
461+
}
462+
463+
// RTLM5d2c: If ObjectsMapEntry.data.bytes exists, return it
464+
if let bytes = entry.data.bytes {
465+
return .primitive(.data(bytes))
466+
}
467+
468+
// RTLM5d2d: If ObjectsMapEntry.data.number exists, return it
469+
if let number = entry.data.number {
470+
return .primitive(.number(number.doubleValue))
471+
}
472+
473+
// RTLM5d2e: If ObjectsMapEntry.data.string exists, return it
474+
if let string = entry.data.string {
475+
switch string {
476+
case let .string(string):
477+
return .primitive(.string(string))
478+
case .json:
479+
// TODO: Understand how to handle JSON values (https://github.com/ably/specification/pull/333/files#r2164561055)
480+
notYetImplemented()
481+
}
482+
}
483+
484+
// RTLM5d2f: If ObjectsMapEntry.data.objectId exists, get the object stored at that objectId from the internal ObjectsPool
485+
if let objectId = entry.data.objectId {
486+
// RTLM5d2f1: If an object with id objectId does not exist, return undefined/null
487+
guard let poolEntry = delegate.referenced?.getObjectFromPool(id: objectId) else {
488+
return nil
489+
}
490+
491+
// RTLM5d2f2: If an object with id objectId exists, return it
492+
switch poolEntry {
493+
case let .map(map):
494+
return .liveMap(map)
495+
case let .counter(counter):
496+
return .liveCounter(counter)
497+
}
498+
}
499+
500+
// RTLM5d2g: Otherwise, return undefined/null
501+
return nil
502+
}
444503
}

Sources/AblyLiveObjects/Public/PublicTypes.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -211,16 +211,16 @@ public protocol LiveMap: LiveObject where Update == LiveMapUpdate {
211211
func get(key: String) throws(ARTErrorInfo) -> LiveMapValue?
212212

213213
/// Returns the number of key-value pairs in the map.
214-
var size: Int { get }
214+
var size: Int { get throws(ARTErrorInfo) }
215215

216216
/// Returns an array of key-value pairs for every entry in the map.
217-
var entries: [(key: String, value: LiveMapValue)] { get }
217+
var entries: [(key: String, value: LiveMapValue)] { get throws(ARTErrorInfo) }
218218

219219
/// Returns an array of keys in the map.
220-
var keys: [String] { get }
220+
var keys: [String] { get throws(ARTErrorInfo) }
221221

222222
/// Returns an iterable of values in the map.
223-
var values: [LiveMapValue] { get }
223+
var values: [LiveMapValue] { get throws(ARTErrorInfo) }
224224

225225
/// Sends an operation to the Ably system to set a key on this `LiveMap` object to a specified value.
226226
///
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import Ably
2+
3+
/**
4+
Describes the errors that can be thrown by the LiveObjects SDK. Use ``toARTErrorInfo()`` to convert to an `ARTErrorInfo` that you can throw.
5+
*/
6+
internal enum LiveObjectsError {
7+
// operationDescription should be a description of a method like "LiveCounter.value"; it will be interpolated into an error message
8+
case objectsOperationFailedInvalidChannelState(operationDescription: String, channelState: ARTRealtimeChannelState)
9+
10+
/// The ``ARTErrorInfo/code`` that should be returned for this error.
11+
internal var code: ARTErrorCode {
12+
switch self {
13+
case .objectsOperationFailedInvalidChannelState:
14+
.channelOperationFailedInvalidState
15+
}
16+
}
17+
18+
/// The ``ARTErrorInfo/statusCode`` that should be returned for this error.
19+
internal var statusCode: Int {
20+
switch self {
21+
case .objectsOperationFailedInvalidChannelState:
22+
400
23+
}
24+
}
25+
26+
/// The ``ARTErrorInfo/localizedDescription`` that should be returned for this error.
27+
internal var localizedDescription: String {
28+
switch self {
29+
case let .objectsOperationFailedInvalidChannelState(operationDescription: operationDescription, channelState: channelState):
30+
"\(operationDescription) operation failed (invalid channel state: \(channelState))"
31+
}
32+
}
33+
34+
internal func toARTErrorInfo() -> ARTErrorInfo {
35+
ARTErrorInfo.create(
36+
withCode: Int(code.rawValue),
37+
status: statusCode,
38+
message: localizedDescription,
39+
)
40+
}
41+
}

Tests/AblyLiveObjectsTests/DefaultLiveCounterTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ struct DefaultLiveCounterTests {
1818
return false
1919
}
2020

21-
return errorInfo.code == 90001
21+
return errorInfo.code == 90001 && errorInfo.statusCode == 400
2222
}
2323
}
2424

0 commit comments

Comments
 (0)