Skip to content
This repository was archived by the owner on Jun 19, 2026. It is now read-only.

Commit 140608b

Browse files
Add Swift structured lane rejection
1 parent b07edb7 commit 140608b

9 files changed

Lines changed: 204 additions & 18 deletions

File tree

swift/vox-runtime/Sources/VoxRuntime/Connection.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import Foundation
22
import PhonSchema
33

44
// r[impl session]
5-
// r[impl connection.root]
5+
// r[impl connection.model]
6+
// r[impl connection.lifecycle.driven]
67
public final class Connection: @unchecked Sendable {
78
public let role: Role
89
let controlLane: Lane
@@ -25,6 +26,7 @@ public final class Connection: @unchecked Sendable {
2526
}
2627

2728
public func run() async throws {
29+
// r[impl connection.lifecycle.driven]
2830
try await driver.run()
2931
}
3032

@@ -48,6 +50,7 @@ public final class Connection: @unchecked Sendable {
4850
}
4951

5052
public func shutdown() {
53+
// r[impl connection.shutdown.explicit]
5154
handle.shutdown()
5255
}
5356

swift/vox-runtime/Sources/VoxRuntime/ConnectionError.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ public enum ConnectionError: Error {
44
case connectionClosed
55
case timeout
66
case transportError(String)
7-
case rejected(metadata: Metadata)
7+
case rejected(LaneRejection)
88
case goodbye(reason: String)
99
case protocolViolation(rule: String)
1010
case handshakeFailed(String)

swift/vox-runtime/Sources/VoxRuntime/ConnectionHandle.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public final class ConnectionHandle: @unchecked Sendable {
1616
///
1717
/// r[impl rpc.virtual-connection.open]
1818
/// r[impl connection.open]
19+
/// r[impl lane.open]
1920
public func openLane(
2021
settings: ConnectionSettings,
2122
metadata: Metadata = emptyMetadata(),
@@ -67,6 +68,7 @@ public final class ConnectionHandle: @unchecked Sendable {
6768
}
6869

6970
/// Request shutdown of the driven connection.
71+
/// r[impl connection.shutdown.explicit]
7072
public func shutdown() {
7173
eventContinuation.finish()
7274
}

swift/vox-runtime/Sources/VoxRuntime/Driver+Incoming.swift

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ extension Driver {
7474
// r[impl connection.open.rejection]
7575
// r[impl connection.open]
7676
// r[impl connection.parity]
77+
// r[impl lane.open]
78+
// r[impl lane.wire.compat]
7779
let peerRole = oppositeRole(role)
7880
guard idMatchesRole(msg.connectionId, peerRole) else {
7981
try await sendProtocolError("connection.open")
@@ -86,14 +88,27 @@ extension Driver {
8688
if let acceptor = laneAcceptor {
8789
let metadata = open.metadata
8890
guard let service = metadata.metaStr("vox-service") else {
89-
// Missing or non-string vox-service metadata — reject
91+
let rejection = LaneRejection.withMessage(
92+
.unknownService,
93+
"missing vox-service metadata"
94+
)
9095
try await conduit.send(
91-
messageReject(connectionId: msg.connectionId, metadata: .null))
96+
messageReject(
97+
connectionId: msg.connectionId,
98+
metadata: rejection.toMetadata()
99+
))
92100
return
93101
}
94102
guard open.connectionSettings.initialChannelCredit > 0 else {
103+
let rejection = LaneRejection.withMessage(
104+
.policyRejected,
105+
"initial_channel_credit must be greater than zero"
106+
)
95107
try await conduit.send(
96-
messageReject(connectionId: msg.connectionId, metadata: .null))
108+
messageReject(
109+
connectionId: msg.connectionId,
110+
metadata: rejection.toMetadata()
111+
))
97112
return
98113
}
99114
let localSettings = try makeConnectionSettings(
@@ -120,21 +135,34 @@ extension Driver {
120135
))
121136
}
122137
},
123-
reject: { [weak self] in
138+
reject: { [weak self] rejection in
124139
guard let self else { return }
125140
Task {
126141
try? await self.conduit.send(
127-
messageReject(connectionId: connId, metadata: .null))
142+
messageReject(
143+
connectionId: connId,
144+
metadata: rejection.toMetadata()
145+
))
128146
}
129147
}
130148
)
131149
acceptor.accept(request: request, lane: pending)
132150
} else {
133-
try await conduit.send(messageReject(connectionId: msg.connectionId, metadata: .null))
151+
let rejection = LaneRejection.withMessage(
152+
.notReady,
153+
"no lane acceptor configured"
154+
)
155+
try await conduit.send(
156+
messageReject(
157+
connectionId: msg.connectionId,
158+
metadata: rejection.toMetadata()
159+
))
134160
}
135161
case .laneAccept(let accept):
136162
// r[impl connection.open]
137163
// r[impl rpc.virtual-connection.open]
164+
// r[impl lane.open.result]
165+
// r[impl lane.wire.compat]
138166
guard let pending = await laneState.takePendingOutbound(msg.connectionId) else {
139167
break
140168
}
@@ -159,10 +187,12 @@ extension Driver {
159187
pending.responseTx(.success(lane))
160188
case .laneReject(let reject):
161189
// r[impl connection.open.rejection]
190+
// r[impl lane.open.result]
191+
// r[impl lane.wire.compat]
162192
guard let pending = await laneState.takePendingOutbound(msg.connectionId) else {
163193
break
164194
}
165-
pending.responseTx(.failure(.rejected(metadata: reject.metadata)))
195+
pending.responseTx(.failure(.rejected(LaneRejection.fromMetadata(reject.metadata))))
166196
case .laneClose:
167197
// r[impl connection.close]
168198
warnLog("received LaneClose conn_id=\(msg.connectionId)")

swift/vox-runtime/Sources/VoxRuntime/Driver+Outgoing.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,8 @@ extension Driver {
264264
)
265265
}
266266
case .openLane(let settings, let metadata, let dispatcher, let responseTx):
267+
// r[impl lane.open]
268+
// r[impl lane.wire.compat]
267269
let isClosed = await state.isConnectionClosed()
268270
guard !isClosed else {
269271
responseTx(.failure(.connectionClosed))
@@ -317,6 +319,8 @@ extension Driver {
317319
}
318320

319321
if connectionId == 0 {
322+
// r[impl connection.root]
323+
// r[impl lane.control]
320324
responseTx(.failure(.protocolViolation(rule: "connection.close")))
321325
return
322326
}

swift/vox-runtime/Sources/VoxRuntime/Lane.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import Foundation
22
import PhonSchema
33

4+
// r[impl lane]
5+
// r[impl lane.control]
46
// r[impl rpc.caller]
57
public final class Lane: @unchecked Sendable {
68
let handle: LaneHandle

swift/vox-runtime/Sources/VoxRuntime/LaneAcceptor.swift

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@ public final class PendingLane: @unchecked Sendable {
2121
private let lock = NSLock()
2222
private var handled = false
2323
private let acceptFn: @Sendable (any ServiceDispatcher) -> Void
24-
private let rejectFn: @Sendable () -> Void
24+
private let rejectFn: @Sendable (LaneRejection) -> Void
2525

2626
init(
2727
accept: @escaping @Sendable (any ServiceDispatcher) -> Void,
28-
reject: @escaping @Sendable () -> Void
28+
reject: @escaping @Sendable (LaneRejection) -> Void
2929
) {
3030
self.acceptFn = accept
3131
self.rejectFn = reject
@@ -43,12 +43,25 @@ public final class PendingLane: @unchecked Sendable {
4343
}
4444
}
4545

46+
/// Reject the lane with structured metadata for the peer.
47+
/// Safe to call multiple times; only the first call takes effect.
48+
public func reject(_ rejection: LaneRejection = .new(.policyRejected)) {
49+
lock.lock()
50+
let wasHandled = handled
51+
handled = true
52+
lock.unlock()
53+
if !wasHandled {
54+
rejectFn(rejection)
55+
}
56+
}
57+
4658
deinit {
4759
lock.lock()
4860
let wasHandled = handled
61+
handled = true
4962
lock.unlock()
5063
if !wasHandled {
51-
rejectFn()
64+
rejectFn(.new(.policyRejected))
5265
}
5366
}
5467
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import Foundation
2+
3+
public let voxLaneRejectReasonMetadataKey = "vox-lane-reject-reason"
4+
public let voxLaneRejectMessageMetadataKey = "vox-lane-reject-message"
5+
6+
public enum LaneRejectReason: String, CaseIterable, Sendable {
7+
case unknownService = "unknown-service"
8+
case forbidden
9+
case notReady = "not-ready"
10+
case draining
11+
case schemaIncompatible = "schema-incompatible"
12+
case policyRejected = "policy-rejected"
13+
}
14+
15+
// r[impl lane.open.result]
16+
public struct LaneRejection: Sendable {
17+
public let reason: LaneRejectReason
18+
public let metadata: Metadata
19+
20+
private init(reason: LaneRejectReason, metadata: Metadata) {
21+
self.reason = reason
22+
self.metadata = metadata
23+
}
24+
25+
public static func new(_ reason: LaneRejectReason) -> LaneRejection {
26+
withMetadata(reason)
27+
}
28+
29+
public static func withMessage(
30+
_ reason: LaneRejectReason,
31+
_ message: String
32+
) -> LaneRejection {
33+
var metadata = emptyMetadata()
34+
metadata.metaSet(voxLaneRejectMessageMetadataKey, .string(message))
35+
return withMetadata(reason, metadata)
36+
}
37+
38+
public static func withMetadata(
39+
_ reason: LaneRejectReason,
40+
_ metadata: Metadata = emptyMetadata()
41+
) -> LaneRejection {
42+
var next = metadata
43+
next.metaSet(voxLaneRejectReasonMetadataKey, .string(reason.rawValue))
44+
return LaneRejection(reason: reason, metadata: next)
45+
}
46+
47+
public static func fromMetadata(_ metadata: Metadata) -> LaneRejection {
48+
let rawReason = metadata.metaStr(voxLaneRejectReasonMetadataKey)
49+
let reason = rawReason.flatMap(LaneRejectReason.init(rawValue:)) ?? .policyRejected
50+
return withMetadata(reason, metadata)
51+
}
52+
53+
public func message() -> String? {
54+
metadata.metaStr(voxLaneRejectMessageMetadataKey) ?? metadata.metaStr("error")
55+
}
56+
57+
public func toMetadata() -> Metadata {
58+
metadata
59+
}
60+
}

0 commit comments

Comments
 (0)