Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion Sources/AblyLiveObjects/DefaultRealtimeObjects.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,11 @@ internal final class DefaultRealtimeObjects: RealtimeObjects, LiveMapObjectPoolD
// RTO1b: If the channel is in the DETACHED or FAILED state, the library should indicate an error with code 90001
let currentChannelState = coreSDK.channelState
if currentChannelState == .detached || currentChannelState == .failed {
throw ARTErrorInfo.create(withCode: Int(ARTErrorCode.channelOperationFailedInvalidState.rawValue), message: "getRoot operation failed (invalid channel state: \(currentChannelState))")
throw LiveObjectsError.objectsOperationFailedInvalidChannelState(
operationDescription: "getRoot",
channelState: currentChannelState,
)
.toARTErrorInfo()
}

let syncStatus = mutex.withLock {
Expand Down
6 changes: 5 additions & 1 deletion Sources/AblyLiveObjects/Internal/DefaultLiveCounter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,11 @@ internal final class DefaultLiveCounter: LiveCounter {
// RTLC5b: If the channel is in the DETACHED or FAILED state, the library should indicate an error with code 90001
let currentChannelState = coreSDK.channelState
if currentChannelState == .detached || currentChannelState == .failed {
throw ARTErrorInfo.create(withCode: Int(ARTErrorCode.channelOperationFailedInvalidState.rawValue), message: "LiveCounter.value operation failed (invalid channel state: \(currentChannelState))")
throw LiveObjectsError.objectsOperationFailedInvalidChannelState(
operationDescription: "LiveCounter.value",
channelState: currentChannelState,
)
.toARTErrorInfo()
}

return mutex.withLock {
Expand Down
173 changes: 116 additions & 57 deletions Sources/AblyLiveObjects/Internal/DefaultLiveMap.swift
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,11 @@ internal final class DefaultLiveMap: LiveMap {
// RTLM5c: If the channel is in the DETACHED or FAILED state, the library should indicate an error with code 90001
let currentChannelState = coreSDK.channelState
if currentChannelState == .detached || currentChannelState == .failed {
throw ARTErrorInfo.create(withCode: Int(ARTErrorCode.channelOperationFailedInvalidState.rawValue), message: "LiveMap.get operation failed (invalid channel state: \(currentChannelState))")
throw LiveObjectsError.objectsOperationFailedInvalidChannelState(
operationDescription: "LiveMap.get",
channelState: currentChannelState,
)
.toARTErrorInfo()
}

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

// RTLM5d2: If a ObjectsMapEntry exists at the key

// RTLM5d2a: If ObjectsMapEntry.tombstone is true, return undefined/null
if entry.tombstone == true {
return nil
}

// Handle primitive values in the order specified by RTLM5d2b through RTLM5d2e

// RTLM5d2b: If ObjectsMapEntry.data.boolean exists, return it
if let boolean = entry.data.boolean {
return .primitive(.bool(boolean))
}

// RTLM5d2c: If ObjectsMapEntry.data.bytes exists, return it
if let bytes = entry.data.bytes {
return .primitive(.data(bytes))
}
// RTLM5d2: If a ObjectsMapEntry exists at the key, convert it using the shared logic
return convertEntryToLiveMapValue(entry)
}

// RTLM5d2d: If ObjectsMapEntry.data.number exists, return it
if let number = entry.data.number {
return .primitive(.number(number.doubleValue))
}
internal var size: Int {
get throws(ARTErrorInfo) {
// RTLM10c: If the channel is in the DETACHED or FAILED state, the library should throw an ErrorInfo error with statusCode 400 and code 90001
let currentChannelState = coreSDK.channelState
if currentChannelState == .detached || currentChannelState == .failed {
throw LiveObjectsError.objectsOperationFailedInvalidChannelState(
operationDescription: "LiveMap.size",
channelState: currentChannelState,
)
.toARTErrorInfo()
}

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

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

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

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

internal var size: Int {
mutex.withLock {
// 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
mutableState.data.count
return result
}
}
}

internal var entries: [(key: String, value: LiveMapValue)] {
notYetImplemented()
}

internal var keys: [String] {
notYetImplemented()
get throws(ARTErrorInfo) {
// RTLM12b: Identical to LiveMap#entries, except that it returns only the keys from the internal data map
try entries.map(\.key)
}
}

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

internal func set(key _: String, value _: LiveMapValue) async throws(ARTErrorInfo) {
Expand Down Expand Up @@ -441,4 +441,63 @@ internal final class DefaultLiveMap: LiveMap {
}
}
}

// MARK: - Helper Methods

/// Converts an ObjectsMapEntry to LiveMapValue using the same logic as get(key:)
/// This is used by entries to ensure consistent value conversion
private func convertEntryToLiveMapValue(_ entry: ObjectsMapEntry) -> LiveMapValue? {
// RTLM5d2a: If ObjectsMapEntry.tombstone is true, return undefined/null
// This is also equivalent to the RTLM14 check
if entry.tombstone == true {
return nil
}

// Handle primitive values in the order specified by RTLM5d2b through RTLM5d2e

// RTLM5d2b: If ObjectsMapEntry.data.boolean exists, return it
if let boolean = entry.data.boolean {
return .primitive(.bool(boolean))
}

// RTLM5d2c: If ObjectsMapEntry.data.bytes exists, return it
if let bytes = entry.data.bytes {
return .primitive(.data(bytes))
}

// RTLM5d2d: If ObjectsMapEntry.data.number exists, return it
if let number = entry.data.number {
return .primitive(.number(number.doubleValue))
}

// RTLM5d2e: If ObjectsMapEntry.data.string exists, return it
if let string = entry.data.string {
switch string {
case let .string(string):
return .primitive(.string(string))
case .json:
// TODO: Understand how to handle JSON values (https://github.com/ably/specification/pull/333/files#r2164561055)
notYetImplemented()
}
}

// RTLM5d2f: If ObjectsMapEntry.data.objectId exists, get the object stored at that objectId from the internal ObjectsPool
if let objectId = entry.data.objectId {
// RTLM5d2f1: If an object with id objectId does not exist, return undefined/null
guard let poolEntry = delegate.referenced?.getObjectFromPool(id: objectId) else {
return nil
}

// RTLM5d2f2: If an object with id objectId exists, return it
switch poolEntry {
case let .map(map):
return .liveMap(map)
case let .counter(counter):
return .liveCounter(counter)
}
}

// RTLM5d2g: Otherwise, return undefined/null
return nil
}
}
8 changes: 4 additions & 4 deletions Sources/AblyLiveObjects/Public/PublicTypes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -211,16 +211,16 @@ public protocol LiveMap: LiveObject where Update == LiveMapUpdate {
func get(key: String) throws(ARTErrorInfo) -> LiveMapValue?

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

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

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

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

/// Sends an operation to the Ably system to set a key on this `LiveMap` object to a specified value.
///
Expand Down
41 changes: 41 additions & 0 deletions Sources/AblyLiveObjects/Utility/Errors.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import Ably

/**
Describes the errors that can be thrown by the LiveObjects SDK. Use ``toARTErrorInfo()`` to convert to an `ARTErrorInfo` that you can throw.
*/
internal enum LiveObjectsError {
// operationDescription should be a description of a method like "LiveCounter.value"; it will be interpolated into an error message
case objectsOperationFailedInvalidChannelState(operationDescription: String, channelState: ARTRealtimeChannelState)

/// The ``ARTErrorInfo/code`` that should be returned for this error.
internal var code: ARTErrorCode {
switch self {
case .objectsOperationFailedInvalidChannelState:
.channelOperationFailedInvalidState
}
}

/// The ``ARTErrorInfo/statusCode`` that should be returned for this error.
internal var statusCode: Int {
switch self {
case .objectsOperationFailedInvalidChannelState:
400
}
}

/// The ``ARTErrorInfo/localizedDescription`` that should be returned for this error.
internal var localizedDescription: String {
switch self {
case let .objectsOperationFailedInvalidChannelState(operationDescription: operationDescription, channelState: channelState):
"\(operationDescription) operation failed (invalid channel state: \(channelState))"
}
}

internal func toARTErrorInfo() -> ARTErrorInfo {
ARTErrorInfo.create(
withCode: Int(code.rawValue),
status: statusCode,
message: localizedDescription,
)
}
}
2 changes: 1 addition & 1 deletion Tests/AblyLiveObjectsTests/DefaultLiveCounterTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ struct DefaultLiveCounterTests {
return false
}

return errorInfo.code == 90001
return errorInfo.code == 90001 && errorInfo.statusCode == 400
}
}

Expand Down
Loading