LavaUI docs

enum MainQueue

Swift

enum MainQueue

Work handed to the main thread from another one.

The reason this has to exist, rather than a worker simply writing @State when it finishes: StateStorage is @Observable, and withObservationTracking's onChange fires synchronously on whichever thread performed the write. A background write would therefore run ViewInvalidation.markBodyDirty on that thread — and ViewInvalidation, FrameScheduler and FrameTasks are all single-threaded statics the run loop reads every iteration. The race is real, not theoretical.

So the division is: a worker computes pure Sendable values and posts the application of them here. async is the only member safe to call from another thread, and everything it enqueues runs on the main thread.

AgentServer already had the shape of this — watcher thread, lock, wake the loop, do the work on the main thread — but kept it private and specific to its socket.

Sources/LavaUI/State.swift:461

Properties

static var hasPending: Bool { get }

Work posted before the loop installed its wake, or between drain and the next pumpEvents. The loop must not park while this is true — that is the switcher opening on an already-queued SubscribeWindows snapshot and sitting on "no windows" until the pointer moves.

Sources/LavaUI/State.swift:502

Methods

static func async(_ work: @escaping @Sendable () -> Void)

Safe from any thread.

Sources/LavaUI/State.swift:476

static func drain()

Drained by LavaApp.run before invalidation is consumed, so a result a worker delivered while the loop was parked lands in this frame rather than the next one.

Sources/LavaUI/State.swift:490

static func install(wake: @escaping @Sendable () -> Void)

Installed once by LavaApp.run. Without it posts still arrive, but cannot unblock a loop parked in pumpEvents — they would wait for unrelated input, which for an idle window can be arbitrarily long.

Sources/LavaUI/State.swift:469