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
1 change: 1 addition & 0 deletions .cursor/rules/swift.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ When writing Swift:
- 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.
- 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.
- When writing JSONValue or WireValue types, favour using the literal syntax enabled by their conformance to the `ExpressibleBy*Literal` protocols where possible.
- When writing a JSON string, favour using Swift raw string literals instead of escaping double quotes.
- 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:
- Ably: use `import Ably`
- AblyPlugin: use `internal import AblyPlugin`
Expand Down
42 changes: 38 additions & 4 deletions Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,46 @@ internal final class InternalDefaultLiveCounter: Sendable {
}
}

internal func increment(amount _: Double) async throws(ARTErrorInfo) {
notYetImplemented()
internal func increment(amount: Double, coreSDK: CoreSDK) async throws(ARTErrorInfo) {
do throws(InternalError) {
// RTLC12c
do {
try coreSDK.validateChannelState(
notIn: [.detached, .failed, .suspended],
operationDescription: "LiveCounter.increment",
)
} catch {
throw error.toInternalError()
}

// RTLC12e1
if !amount.isFinite {
throw LiveObjectsError.counterIncrementAmountInvalid(amount: amount).toARTErrorInfo().toInternalError()
}

let objectMessage = OutboundObjectMessage(
operation: .init(
// RTLC12e2
action: .known(.counterInc),
// RTLC12e3
objectId: objectID,
counterOp: .init(
// RTLC12e4
amount: .init(value: amount),
),
),
)

// RTLC12f
try await coreSDK.publish(objectMessages: [objectMessage])
} catch {
throw error.toARTErrorInfo()
}
}

internal func decrement(amount _: Double) async throws(ARTErrorInfo) {
notYetImplemented()
internal func decrement(amount: Double, coreSDK: CoreSDK) async throws(ARTErrorInfo) {
// RTLC13b
try await increment(amount: -amount, coreSDK: coreSDK)
}

@discardableResult
Expand Down
59 changes: 55 additions & 4 deletions Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift
Original file line number Diff line number Diff line change
Expand Up @@ -173,12 +173,63 @@ internal final class InternalDefaultLiveMap: Sendable {
try entries(coreSDK: coreSDK, delegate: delegate).map(\.value)
}

internal func set(key _: String, value _: LiveMapValue) async throws(ARTErrorInfo) {
notYetImplemented()
internal func set(key: String, value: InternalLiveMapValue, coreSDK: CoreSDK) async throws(ARTErrorInfo) {
do throws(InternalError) {
// RTLM20c
do {
try coreSDK.validateChannelState(notIn: [.detached, .failed, .suspended], operationDescription: "LiveMap.set")
} catch {
throw error.toInternalError()
}

let objectMessage = OutboundObjectMessage(
operation: .init(
// RTLM20e2
action: .known(.mapSet),
// RTLM20e3
objectId: objectID,
mapOp: .init(
// RTLM20e4
key: key,
// RTLM20e5
data: value.toObjectData,
),
),
)

try await coreSDK.publish(objectMessages: [objectMessage])
} catch {
throw error.toARTErrorInfo()
}
}

internal func remove(key _: String) async throws(ARTErrorInfo) {
notYetImplemented()
internal func remove(key: String, coreSDK: CoreSDK) async throws(ARTErrorInfo) {
do throws(InternalError) {
// RTLM21c
do {
try coreSDK.validateChannelState(notIn: [.detached, .failed, .suspended], operationDescription: "LiveMap.remove")
} catch {
throw error.toInternalError()
}

let objectMessage = OutboundObjectMessage(
operation: .init(
// RTLM21e2
action: .known(.mapRemove),
// RTLM21e3
objectId: objectID,
mapOp: .init(
// RTLM21e4
key: key,
),
),
)

// RTLM21f
try await coreSDK.publish(objectMessages: [objectMessage])
} catch {
throw error.toARTErrorInfo()
}
}

@discardableResult
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,20 +157,88 @@ internal final class InternalDefaultRealtimeObjects: Sendable, LiveMapObjectPool
}
}

internal func createMap(entries _: [String: LiveMapValue]) async throws(ARTErrorInfo) -> any LiveMap {
notYetImplemented()
internal func createMap(entries: [String: InternalLiveMapValue], coreSDK: CoreSDK) async throws(ARTErrorInfo) -> InternalDefaultLiveMap {
do throws(InternalError) {
// RTO11d
do {
try coreSDK.validateChannelState(notIn: [.detached, .failed, .suspended], operationDescription: "RealtimeObjects.createMap")
} catch {
throw error.toInternalError()
}

// RTO11f
// TODO: This is a stopgap; change to use server time per RTO11f5 (https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/50)
let timestamp = clock.now
let creationOperation = ObjectCreationHelpers.creationOperationForLiveMap(
entries: entries,
timestamp: timestamp,
)

// RTO11g
try await coreSDK.publish(objectMessages: [creationOperation.objectMessage])

// RTO11h
return mutex.withLock {
mutableState.objectsPool.getOrCreateMap(
creationOperation: creationOperation,
logger: logger,
userCallbackQueue: userCallbackQueue,
clock: clock,
)
}
} catch {
throw error.toARTErrorInfo()
}
}

internal func createMap() async throws(ARTErrorInfo) -> any LiveMap {
notYetImplemented()
internal func createMap(coreSDK: CoreSDK) async throws(ARTErrorInfo) -> InternalDefaultLiveMap {
// RTO11f4b
try await createMap(entries: [:], coreSDK: coreSDK)
}

internal func createCounter(count _: Double) async throws(ARTErrorInfo) -> any LiveCounter {
notYetImplemented()
internal func createCounter(count: Double, coreSDK: CoreSDK) async throws(ARTErrorInfo) -> InternalDefaultLiveCounter {
do throws(InternalError) {
// RTO12d
do {
try coreSDK.validateChannelState(notIn: [.detached, .failed, .suspended], operationDescription: "RealtimeObjects.createCounter")
} catch {
throw error.toInternalError()
}

// RTO12f1
if !count.isFinite {
throw LiveObjectsError.counterInitialValueInvalid(value: count).toARTErrorInfo().toInternalError()
}

// RTO12f

// TODO: This is a stopgap; change to use server time per RTO12f5 (https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/50)
let timestamp = clock.now
let creationOperation = ObjectCreationHelpers.creationOperationForLiveCounter(
count: count,
timestamp: timestamp,
)

// RTO12g
try await coreSDK.publish(objectMessages: [creationOperation.objectMessage])

// RTO12h
return mutex.withLock {
mutableState.objectsPool.getOrCreateCounter(
creationOperation: creationOperation,
logger: logger,
userCallbackQueue: userCallbackQueue,
clock: clock,
)
}
} catch {
throw error.toARTErrorInfo()
}
}

internal func createCounter() async throws(ARTErrorInfo) -> any LiveCounter {
notYetImplemented()
internal func createCounter(coreSDK: CoreSDK) async throws(ARTErrorInfo) -> InternalDefaultLiveCounter {
// RTO12f2a
try await createCounter(count: 0, coreSDK: coreSDK)
}

internal func batch(callback _: sending BatchCallback) async throws {
Expand Down
54 changes: 54 additions & 0 deletions Sources/AblyLiveObjects/Internal/InternalLiveMapValue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,60 @@ internal enum InternalLiveMapValue: Sendable, Equatable {
case liveMap(InternalDefaultLiveMap)
case liveCounter(InternalDefaultLiveCounter)

// MARK: - Creating from a public LiveMapValue

/// Converts a public ``LiveMapValue`` into an ``InternalLiveMapValue``.
///
/// Needed in order to access the internals of user-provided LiveObject-valued LiveMap entries to extract their object ID.
internal init(liveMapValue: LiveMapValue) {
switch liveMapValue {
case let .primitive(primitiveValue):
self = .primitive(primitiveValue)
case let .liveMap(publicLiveMap):
guard let publicDefaultLiveMap = publicLiveMap as? PublicDefaultLiveMap else {
// TODO: Try and remove this runtime check and know this type statically, see https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/37
preconditionFailure("Expected PublicDefaultLiveMap, got \(publicLiveMap)")
}
self = .liveMap(publicDefaultLiveMap.proxied)
case let .liveCounter(publicLiveCounter):
guard let publicDefaultLiveCounter = publicLiveCounter as? PublicDefaultLiveCounter else {
// TODO: Try and remove this runtime check and know this type statically, see https://github.com/ably/ably-cocoa-liveobjects-plugin/issues/37
preconditionFailure("Expected PublicDefaultLiveCounter, got \(publicLiveCounter)")
}
self = .liveCounter(publicDefaultLiveCounter.proxied)
}
}

// MARK: - Representation in the Realtime protocol

/// 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.
internal var toObjectData: ObjectData {
// RTO11f4c1: Create an ObjectsMapEntry for the current value
switch self {
case let .primitive(primitiveValue):
switch primitiveValue {
case let .bool(value):
.init(boolean: value)
case let .data(value):
.init(bytes: value)
case let .number(value):
.init(number: NSNumber(value: value))
case let .string(value):
.init(string: value)
case let .jsonArray(value):
.init(json: .array(value))
case let .jsonObject(value):
.init(json: .object(value))
}
case let .liveMap(liveMap):
// RTO11f4c1a: If the value is of type LiveMap, set ObjectsMapEntry.data.objectId to the objectId of that object
.init(objectId: liveMap.objectID)
case let .liveCounter(liveCounter):
// RTO11f4c1a: If the value is of type LiveCounter, set ObjectsMapEntry.data.objectId to the objectId of that object
.init(objectId: liveCounter.objectID)
}
}

// MARK: - Convenience getters for associated values

/// If this `InternalLiveMapValue` has case `primitive`, this returns the associated value. Else, it returns `nil`.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Foundation

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