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
2 changes: 2 additions & 0 deletions .cursor/rules/specification.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ alwaysApply: false
- If you are given a task that requires knowledge of the Specification Document, you must consult the Specification Document before proceeding. You must never make a guess about the contents of the Specification Document.
- If you are given a task that requires you to interpret the Specification Document, but the Specification Document is unclear, be sure to mention this.
- The Specification Document is structured as a list of specification points, each with an identifier. An example identifier is "OD1". In the Specification Document, the start of specification point OD1 would be represented by the string @(OD1)@. These specification points are sometimes referred to as "specification items".
- Some specification points have subpoints. For example REC2 has (amongst others) the subpoint RSC2a, which has subpoints REC2a1 and REC2a2.
- The LiveObjects functionality is referred to the in the Specification simply as "Objects".
5 changes: 4 additions & 1 deletion .cursor/rules/swift.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,7 @@ When writing Swift:

- Be sure to satisfy SwiftLint's `explicit_acl` rule ("All declarations should specify Access Control Level keywords explicitly).
- When writing an `extension` of a type, favour placing the access level on the declaration of the extension rather than each of its individual members.
- This does not apply when writing test code.
- This does not apply when writing test code.
- 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.
22 changes: 11 additions & 11 deletions Sources/AblyLiveObjects/DefaultLiveObjects.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ internal class DefaultLiveObjects: Objects {
private let pluginAPI: AblyPlugin.PluginAPIProtocol

// These drive the testsOnly_* properties that expose the received ProtocolMessages to the test suite.
private let receivedObjectProtocolMessages: AsyncStream<[InboundWireObjectMessage]>
private let receivedObjectProtocolMessagesContinuation: AsyncStream<[InboundWireObjectMessage]>.Continuation
private let receivedObjectSyncProtocolMessages: AsyncStream<[InboundWireObjectMessage]>
private let receivedObjectSyncProtocolMessagesContinuation: AsyncStream<[InboundWireObjectMessage]>.Continuation
private let receivedObjectProtocolMessages: AsyncStream<[InboundObjectMessage]>
private let receivedObjectProtocolMessagesContinuation: AsyncStream<[InboundObjectMessage]>.Continuation
private let receivedObjectSyncProtocolMessages: AsyncStream<[InboundObjectMessage]>
private let receivedObjectSyncProtocolMessagesContinuation: AsyncStream<[InboundObjectMessage]>.Continuation

internal init(channel: ARTRealtimeChannel, logger: AblyPlugin.Logger, pluginAPI: AblyPlugin.PluginAPIProtocol) {
self.channel = channel
Expand Down Expand Up @@ -62,26 +62,26 @@ internal class DefaultLiveObjects: Objects {
testsOnly_onChannelAttachedHasObjects = hasObjects
}

internal var testsOnly_receivedObjectProtocolMessages: AsyncStream<[InboundWireObjectMessage]> {
internal var testsOnly_receivedObjectProtocolMessages: AsyncStream<[InboundObjectMessage]> {
receivedObjectProtocolMessages
}

internal func handleObjectProtocolMessage(wireObjectMessages: [InboundWireObjectMessage]) {
receivedObjectProtocolMessagesContinuation.yield(wireObjectMessages)
internal func handleObjectProtocolMessage(objectMessages: [InboundObjectMessage]) {
receivedObjectProtocolMessagesContinuation.yield(objectMessages)
}

internal var testsOnly_receivedObjectSyncProtocolMessages: AsyncStream<[InboundWireObjectMessage]> {
internal var testsOnly_receivedObjectSyncProtocolMessages: AsyncStream<[InboundObjectMessage]> {
receivedObjectSyncProtocolMessages
}

internal func handleObjectSyncProtocolMessage(wireObjectMessages: [InboundWireObjectMessage], protocolMessageChannelSerial _: String) {
receivedObjectSyncProtocolMessagesContinuation.yield(wireObjectMessages)
internal func handleObjectSyncProtocolMessage(objectMessages: [InboundObjectMessage], protocolMessageChannelSerial _: String) {
receivedObjectSyncProtocolMessagesContinuation.yield(objectMessages)
}

// MARK: - Sending `OBJECT` ProtocolMessage

// This is currently exposed so that we can try calling it from the tests in the early days of the SDK to check that we can send an OBJECT ProtocolMessage. We'll probably make it private later on.
internal func testsOnly_sendObject(objectMessages: [OutboundWireObjectMessage]) async throws(InternalError) {
internal func testsOnly_sendObject(objectMessages: [OutboundObjectMessage]) async throws(InternalError) {
guard let channel else {
return
}
Expand Down
61 changes: 37 additions & 24 deletions Sources/AblyLiveObjects/Internal/DefaultInternalPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,77 +46,90 @@ internal final class DefaultInternalPlugin: NSObject, AblyPlugin.LiveObjectsInte
Self.objectsProperty(for: channel, pluginAPI: pluginAPI)
}

/// A class that wraps a ``WireObjectMessage``.
/// A class that wraps an object message.
///
/// We need this intermediate type because we want `WireObjectMessage` to be a struct — because it's nicer to work with internally — but a struct can't conform to the class-bound `AblyPlugin.WireObjectMessage` protocol.
private final class WireObjectMessageBox<T>: AblyPlugin.ObjectMessageProtocol where T: Sendable {
internal let wireObjectMessage: T
/// We need this intermediate type because we want object messages to be structs — because they're nicer to work with internally — but a struct can't conform to the class-bound `AblyPlugin.ObjectMessageProtocol`.
private final class ObjectMessageBox<T>: AblyPlugin.ObjectMessageProtocol where T: Sendable {
internal let objectMessage: T

init(wireObjectMessage: T) {
self.wireObjectMessage = wireObjectMessage
init(objectMessage: T) {
self.objectMessage = objectMessage
}
}

internal func decodeObjectMessage(_ serialized: [String: Any], context: DecodingContextProtocol, error errorPtr: AutoreleasingUnsafeMutablePointer<ARTErrorInfo?>?) -> (any ObjectMessageProtocol)? {
let jsonObject = JSONValue.objectFromAblyPluginData(serialized)
internal func decodeObjectMessage(
_ serialized: [String: Any],
context: DecodingContextProtocol,
format: EncodingFormat,
error errorPtr: AutoreleasingUnsafeMutablePointer<ARTErrorInfo?>?,
) -> (any ObjectMessageProtocol)? {
let wireObject = WireValue.objectFromAblyPluginData(serialized)

do {
let wireObjectMessage = try InboundWireObjectMessage(
jsonObject: jsonObject,
wireObject: wireObject,
decodingContext: context,
)
return WireObjectMessageBox(wireObjectMessage: wireObjectMessage)
let objectMessage = try InboundObjectMessage(
wireObjectMessage: wireObjectMessage,
format: format,
)
return ObjectMessageBox(objectMessage: objectMessage)
} catch {
errorPtr?.pointee = error.toARTErrorInfo()
return nil
}
}

internal func encodeObjectMessage(_ publicObjectMessage: any AblyPlugin.ObjectMessageProtocol) -> [String: Any] {
guard let wireObjectMessageBox = publicObjectMessage as? WireObjectMessageBox<OutboundWireObjectMessage> else {
preconditionFailure("Expected to receive the same WireObjectMessage type as we emit")
internal func encodeObjectMessage(
_ publicObjectMessage: any AblyPlugin.ObjectMessageProtocol,
format: EncodingFormat,
) -> [String: Any] {
guard let outboundObjectMessageBox = publicObjectMessage as? ObjectMessageBox<OutboundObjectMessage> else {
preconditionFailure("Expected to receive the same OutboundObjectMessage type as we emit")
}

return wireObjectMessageBox.wireObjectMessage.toJSONObject.toAblyPluginDataDictionary
let wireObjectMessage = outboundObjectMessageBox.objectMessage.toWire(format: format)
return wireObjectMessage.toWireObject.toAblyPluginDataDictionary
}

internal func onChannelAttached(_ channel: ARTRealtimeChannel, hasObjects: Bool) {
objectsProperty(for: channel).onChannelAttached(hasObjects: hasObjects)
}

internal func handleObjectProtocolMessage(withObjectMessages publicObjectMessages: [any AblyPlugin.ObjectMessageProtocol], channel: ARTRealtimeChannel) {
guard let wireObjectMessageBoxes = publicObjectMessages as? [WireObjectMessageBox<InboundWireObjectMessage>] else {
preconditionFailure("Expected to receive the same WireObjectMessage type as we emit")
guard let inboundObjectMessageBoxes = publicObjectMessages as? [ObjectMessageBox<InboundObjectMessage>] else {
preconditionFailure("Expected to receive the same InboundObjectMessage type as we emit")
}

let wireObjectMessages = wireObjectMessageBoxes.map(\.wireObjectMessage)
let objectMessages = inboundObjectMessageBoxes.map(\.objectMessage)

objectsProperty(for: channel).handleObjectProtocolMessage(
wireObjectMessages: wireObjectMessages,
objectMessages: objectMessages,
)
}

internal func handleObjectSyncProtocolMessage(withObjectMessages publicObjectMessages: [any AblyPlugin.ObjectMessageProtocol], protocolMessageChannelSerial: String, channel: ARTRealtimeChannel) {
guard let objectMessageBoxes = publicObjectMessages as? [WireObjectMessageBox<InboundWireObjectMessage>] else {
preconditionFailure("Expected to receive the same WireObjectMessage type as we emit")
guard let inboundObjectMessageBoxes = publicObjectMessages as? [ObjectMessageBox<InboundObjectMessage>] else {
preconditionFailure("Expected to receive the same InboundObjectMessage type as we emit")
}

let wireObjectMessages = objectMessageBoxes.map(\.wireObjectMessage)
let objectMessages = inboundObjectMessageBoxes.map(\.objectMessage)

objectsProperty(for: channel).handleObjectSyncProtocolMessage(
wireObjectMessages: wireObjectMessages,
objectMessages: objectMessages,
protocolMessageChannelSerial: protocolMessageChannelSerial,
)
}

// MARK: - Sending `OBJECT` ProtocolMessage

internal static func sendObject(
objectMessages: [OutboundWireObjectMessage],
objectMessages: [OutboundObjectMessage],
channel: ARTRealtimeChannel,
pluginAPI: PluginAPIProtocol,
) async throws(InternalError) {
let objectMessageBoxes: [WireObjectMessageBox<OutboundWireObjectMessage>] = objectMessages.map { .init(wireObjectMessage: $0) }
let objectMessageBoxes: [ObjectMessageBox<OutboundObjectMessage>] = objectMessages.map { .init(objectMessage: $0) }

try await withCheckedContinuation { (continuation: CheckedContinuation<Result<Void, InternalError>, _>) in
pluginAPI.sendObject(
Expand Down
Loading