open-standards/wai/wai-sink-swift/Sources/WaiSink/Collector.swift
Transaction Science 643f86f8d7 wai: the Swift package of the native sink, and a log that keeps its order
wai/wai-sink-swift: the native sink for the platforms whose native player is reached
from Swift. WaiSink wraps the C interface (every call JSON in, JSON out); WaiRelay is the
loopback server the player is given in place of the origin: it fetches each object,
gives the sink a playlist with its receipt and the chain's parents, and answers the
player only with bytes the sink presents (502 otherwise), every URI of a playlist
rewritten to it; WaiPlayerSession drives the platform's player through it, observes it
every 200 ms, logs its first frame, stalls and end, ends a session nobody can see, and
posts the bundle to the owner's collector; WaiViewAttention reads the platform's signals
of who can see the player. scripts/build-xcframework.sh builds WaiFFI.xcframework from
wai-rs. Tests: the corpus through the wrapper, the relay over the reference floor, and
the player end to end over a presentation made with the platform's own encoder.

A native host's player and its requests run on threads of their own, so an object can
reach the sink with a time before the log's last entry. delivery-telemetry §7.1 has a
log's times never decrease and nothing after end or abandon: the sink logs such an entry
at the last entry's time, and takes no entry after end or abandon, while an object
received then is still verified and acknowledged (native-sink §5.2). The corpus gains
log-order, and the second implementation checks both rules.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 22:55:08 -04:00

33 lines
1.4 KiB
Swift

// The owner's collector (stream-audit §12), as a player reaches it: a session's bundle to
// `v1/sessions` when it ends, and its Event-mode CMCD batches to `v1/cmcd` while it plays.
import Foundation
public final class WaiCollector: @unchecked Sendable {
public let base: URL
private let session: URLSession
public init(base: URL, session: URLSession = URLSession(configuration: .ephemeral)) {
self.base = base
self.session = session
}
/// Post a bundle; the collector's status and answer.
public func postSession(_ bundle: Data, completion: @escaping @Sendable (Int, Data?) -> Void = { _, _ in }) {
post("v1/sessions", type: "text/plain;charset=UTF-8", body: bundle, completion: completion)
}
/// Post an `application/cmcd` batch. Telemetry is advisory: a failure is dropped
/// (stream-audit §7.2).
public func postCMCD(_ batch: String) {
guard !batch.isEmpty else { return }
post("v1/cmcd", type: "application/cmcd", body: Data(batch.utf8)) { _, _ in }
}
private func post(_ path: String, type: String, body: Data, completion: @escaping @Sendable (Int, Data?) -> Void) {
var r = URLRequest(url: base.appendingPathComponent(path))
r.httpMethod = "POST"
r.setValue(type, forHTTPHeaderField: "Content-Type")
r.httpBody = body
session.dataTask(with: r) { d, resp, _ in completion((resp as? HTTPURLResponse)?.statusCode ?? 0, d) }.resume()
}
}