ProcessNode is now in LumaCore, exposing AsyncStream for events, REPL results, ITrace captures, module snapshots, and session detach notifications. No delegates, no @Published — consumers subscribe with for-await. Luma retains ProcessNodeViewModel as a thin ObservableObject wrapper that subscribes to the streams and handles SwiftData persistence (inserting REPLCells, ITraceCaptures) and R2 integration. RuntimeEvent is now a self-contained LumaCore value type with source and payload enums instead of object references. The Luma-side DisplayEvent bridges to views by carrying a ProcessNodeViewModel reference alongside the core event. WIP: views not yet updated for the rename — the Xcode build needs mechanical updates across ~14 files to use ProcessNodeViewModel and DisplayEvent.
73 lines
1.8 KiB
Swift
73 lines
1.8 KiB
Swift
public actor AsyncEventSource<Event: Sendable> {
|
|
public typealias Stream = AsyncStream<Event>
|
|
|
|
private var nextID: Int = 0
|
|
private var continuations: [Int: Stream.Continuation] = [:]
|
|
private var isFinished = false
|
|
private var terminalEvent: Event? = nil
|
|
|
|
public init() {}
|
|
|
|
public nonisolated func makeStream() -> Stream {
|
|
Stream { continuation in
|
|
Task { await self.addSubscriber(continuation) }
|
|
}
|
|
}
|
|
|
|
public nonisolated func yield(_ event: Event) {
|
|
Task { await self.emit(event) }
|
|
}
|
|
|
|
public nonisolated func finish(replayLast last: Event? = nil) {
|
|
Task { await self.finishAll(replayLast: last) }
|
|
}
|
|
|
|
private func addSubscriber(_ continuation: Stream.Continuation) {
|
|
if isFinished {
|
|
if let event = terminalEvent {
|
|
continuation.yield(event)
|
|
}
|
|
continuation.finish()
|
|
return
|
|
}
|
|
|
|
let id = nextID
|
|
nextID &+= 1
|
|
continuations[id] = continuation
|
|
|
|
continuation.onTermination = { [weak self] _ in
|
|
guard let self else { return }
|
|
Task { await self.removeSubscriber(id) }
|
|
}
|
|
}
|
|
|
|
private func removeSubscriber(_ id: Int) {
|
|
continuations.removeValue(forKey: id)
|
|
}
|
|
|
|
private func emit(_ event: Event) {
|
|
if isFinished { return }
|
|
|
|
for c in continuations.values {
|
|
c.yield(event)
|
|
}
|
|
}
|
|
|
|
private func finishAll(replayLast last: Event?) {
|
|
if isFinished { return }
|
|
|
|
isFinished = true
|
|
terminalEvent = last
|
|
|
|
let subs = Array(continuations.values)
|
|
continuations.removeAll()
|
|
|
|
for c in subs {
|
|
if let event = last {
|
|
c.yield(event)
|
|
}
|
|
c.finish()
|
|
}
|
|
}
|
|
}
|