Files
luma/Sources/LumaCore/AsyncEventSource.swift
Ole André Vadla Ravnås 7b16f92357 Move ProcessNode to LumaCore with event streams
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.
2026-04-06 19:44:59 +02:00

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()
}
}
}