Skip to content
This repository was archived by the owner on Jun 19, 2026. It is now read-only.
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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ jobs:

swift-conformance:
name: Conformance / Swift
runs-on: depot-macos-14
runs-on: depot-macos-15
steps:
- uses: actions/checkout@v4

Expand All @@ -178,7 +178,7 @@ jobs:
uses: Swatinem/rust-cache@v2

- name: Build subject
run: swift build --package-path swift/subject
run: swift build -c release --package-path swift/subject

- name: Run spec tests
run: cargo nextest run -p spec-tests
Expand Down
2 changes: 1 addition & 1 deletion rust/roam-codegen/src/targets/swift/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ fn generate_preregister_channels(w: &mut CodeWriter<&mut String>, service: &Serv
.unwrap();
w.writeln("/// race conditions where Data arrives before channels are registered.")
.unwrap();
w.writeln("public func preregisterChannels(methodId: UInt64, payload: Data) async {")
w.writeln("public static func preregisterChannels(methodId: UInt64, payload: Data, registry: ChannelRegistry) async {")
.unwrap();
{
let _indent = w.indent();
Expand Down
11 changes: 5 additions & 6 deletions rust/roam-codegen/src/targets/swift/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,20 +126,20 @@ pub fn generate_named_types(named_types: &[(String, &'static Shape)]) -> String
ShapeKind::Enum(EnumInfo { variants, .. }) => {
out.push_str(&format!("public enum {name}: Codable, Sendable {{\n"));
for variant in variants {
let variant_name = variant.name.to_lower_camel_case();
match classify_variant(variant) {
VariantKind::Unit => {
out.push_str(&format!(" case {}\n", variant.name));
out.push_str(&format!(" case {variant_name}\n"));
}
VariantKind::Newtype { inner } => {
let inner_type = swift_type_base(inner);
out.push_str(&format!(" case {}({inner_type})\n", variant.name));
out.push_str(&format!(" case {variant_name}({inner_type})\n"));
}
VariantKind::Tuple { fields } => {
let field_types: Vec<_> =
fields.iter().map(|f| swift_type_base(f.shape())).collect();
out.push_str(&format!(
" case {}({})\n",
variant.name,
" case {variant_name}({})\n",
field_types.join(", ")
));
}
Expand All @@ -155,8 +155,7 @@ pub fn generate_named_types(named_types: &[(String, &'static Shape)]) -> String
})
.collect();
out.push_str(&format!(
" case {}({})\n",
variant.name,
" case {variant_name}({})\n",
field_decls.join(", ")
));
}
Expand Down
3 changes: 2 additions & 1 deletion swift/roam-runtime/Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions swift/roam-runtime/Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version: 5.9
// swift-tools-version: 6.0
import PackageDescription

let package = Package(
Expand All @@ -10,7 +10,7 @@ let package = Package(
.library(name: "RoamRuntime", targets: ["RoamRuntime"])
],
dependencies: [
.package(url: "https://github.com/apple/swift-nio.git", from: "2.92.1")
.package(url: "https://github.com/apple/swift-nio.git", from: "2.92.0")
],
targets: [
.target(
Expand Down
4 changes: 3 additions & 1 deletion swift/roam-runtime/Sources/RoamRuntime/Binding.swift
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,9 @@ private func bindValue(
}

case .option(let inner):
if let opt = value as? Any?, let unwrapped = opt {
// Use Mirror to check if value is Some(x) vs None
let mirror = Mirror(reflecting: value)
if mirror.displayStyle == .optional, let (_, unwrapped) = mirror.children.first {
await bindValue(
schema: inner,
value: unwrapped,
Expand Down
4 changes: 4 additions & 0 deletions swift/roam-runtime/Sources/RoamRuntime/COBS.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
/// COBS (Consistent Overhead Byte Stuffing) framing.
///
/// r[impl transport.bytestream.cobs] - Messages are COBS-encoded with 0x00 delimiter.
/// r[impl transport.message.binary] - All messages are binary (not text).
/// r[impl transport.message.one-to-one] - Each frame contains exactly one roam message.
///
/// COBS encodes data so that it contains no zero bytes, allowing
/// zero to be used as a frame delimiter.

Expand Down
59 changes: 57 additions & 2 deletions swift/roam-runtime/Sources/RoamRuntime/Channel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ public enum Role: Sendable {
// MARK: - Channel ID Allocator

/// Allocates unique channel IDs with correct parity.
///
/// r[impl channeling.id.uniqueness] - IDs are unique within a connection.
/// r[impl channeling.id.parity] - Initiator uses odd, Acceptor uses even.
/// r[impl channeling.allocation.caller] - Caller allocates ALL channel IDs.
public final class ChannelIdAllocator: @unchecked Sendable {
private var next: UInt64
private let lock = NSLock()
Expand Down Expand Up @@ -71,6 +75,16 @@ public actor ChannelReceiver {
}
}

/// Handle reset - abruptly close without delivering buffered data.
public func deliverReset() {
closed = true
buffer.removeAll()
if let w = waiter {
waiter = nil
w.resume(returning: nil)
}
}

public func recv() async -> [UInt8]? {
if !buffer.isEmpty {
return buffer.removeFirst()
Expand All @@ -88,7 +102,9 @@ public actor ChannelReceiver {

/// Handle for sending data on a channel.
///
/// From the caller's perspective, Tx means "I send on this channel".
/// r[impl channeling.caller-pov] - From caller's perspective, Tx means "I send".
/// r[impl channeling.type] - Serializes as u64 channel ID on wire.
/// r[impl channeling.holder-semantics] - The holder sends on this channel.
public final class Tx<T: Sendable>: @unchecked Sendable {
public var channelId: ChannelId = 0
private var taskTx: (@Sendable (TaskMessage) -> Void)?
Expand All @@ -105,6 +121,8 @@ public final class Tx<T: Sendable>: @unchecked Sendable {
}

/// Send a value.
///
/// r[impl channeling.data] - Data messages carry serialized values.
public func send(_ value: T) throws {
guard let taskTx = taskTx else {
throw ChannelError.notBound
Expand All @@ -114,6 +132,9 @@ public final class Tx<T: Sendable>: @unchecked Sendable {
}

/// Close this channel.
///
/// r[impl channeling.close] - Close terminates the channel.
/// r[impl channeling.lifecycle.caller-closes-pushes] - Caller sends Close when done.
public func close() {
taskTx?(.close(channelId: channelId))
}
Expand All @@ -123,7 +144,9 @@ public final class Tx<T: Sendable>: @unchecked Sendable {

/// Handle for receiving data on a channel.
///
/// From the caller's perspective, Rx means "I receive from this channel".
/// r[impl channeling.caller-pov] - From caller's perspective, Rx means "I receive".
/// r[impl channeling.type] - Serializes as u64 channel ID on wire.
/// r[impl channeling.holder-semantics] - The holder receives from this channel.
public final class Rx<T: Sendable>: @unchecked Sendable {
public var channelId: ChannelId = 0
private var receiver: ChannelReceiver?
Expand Down Expand Up @@ -172,6 +195,11 @@ extension Rx: AsyncSequence {
// MARK: - Channel Registry

/// Registry for incoming channels.
///
/// r[impl channeling.unknown] - Unknown channel IDs cause Goodbye.
/// r[impl channeling.data] - Data messages routed by channel_id.
/// r[impl channeling.channels-outlive-response] - Channels may outlive the response.
/// r[impl channeling.call-complete] - Call completion independent of channel lifecycle.
public actor ChannelRegistry {
private var receivers: [ChannelId: ChannelReceiver] = [:]
private var pendingData: [ChannelId: [[UInt8]]] = [:]
Expand Down Expand Up @@ -211,6 +239,9 @@ public actor ChannelRegistry {
}

/// Deliver data to a channel. Returns true if known.
///
/// r[impl channeling.data-after-close] - Data after close is rejected.
/// r[impl channeling.data.size-limit] - Data size bounded by max_payload_size.
public func deliverData(channelId: ChannelId, payload: [UInt8]) async -> Bool {
if let receiver = receivers[channelId] {
await receiver.deliver(payload)
Expand Down Expand Up @@ -241,6 +272,30 @@ public actor ChannelRegistry {
public func isKnown(_ channelId: ChannelId) -> Bool {
knownChannels.contains(channelId) || receivers[channelId] != nil
}

/// Deliver reset to a channel.
///
/// r[impl channeling.reset] - Reset abruptly terminates channel.
public func deliverReset(channelId: ChannelId) async {
if let receiver = receivers[channelId] {
await receiver.deliverReset()
receivers.removeValue(forKey: channelId)
}
knownChannels.remove(channelId)
pendingData.removeValue(forKey: channelId)
pendingClose.remove(channelId)
}

/// Deliver credit to a channel.
///
/// r[impl flow.channel.credit-grant] - Credit message grants permission.
/// r[impl flow.channel.infinite-credit] - Infinite credit mode bypasses accounting.
public func deliverCredit(channelId: ChannelId, bytes: UInt32) async {
// TODO: Implement credit tracking for flow control
// For now, we operate in "infinite credit" mode
_ = channelId
_ = bytes
}
}

// MARK: - Errors
Expand Down
Loading
Loading