eleven / messenger Public

Clone
git clone https://amadan.net/eleven/messenger

Plain git — no account needed to clone.

Download

Download this file

1import Foundation
2
3// 🤖 Serialize fire-and-forget async work with trailing-edge coalescing:
4// while one pass runs, any number of further run() calls collapse into ONE
5// rerun after it finishes (using the running call's closure — callers all
6// request the same work). Built for ThreadStore.refresh, which refires on
7// every socket frame and from several UI paths while a full pass takes a
8// second-plus on a big space: uncoalesced, overlapping passes raced
9// duplicate /api/threads fetches and stacked their folds' publishes into
10// the 120–270ms frame hitches the 2026-07-20 device trace pinned on
11// NativeChatScreen's re-render.
12//
13// A caller that arrives mid-pass AWAITS the trailing rerun rather than
14// returning early: callers like createChat read the published state right
15// after run() and rely on a pass that STARTED after their call (the new
16// thread must be in `threads` when run() returns). The early-return first
17// cut broke that quietly — repaint callers didn't care, createChat did.
18@MainActor
19public final class CoalescedRunner {
20 private var current: Task<Void, Never>?
21 private var queued = false
22
23 public init() {}
24
25 public func run(_ pass: @escaping @MainActor () async -> Void) async {
26 if let current {
27 // Mid-pass: the trailing rerun starts after this call, so
28 // awaiting the whole task is the freshness guarantee.
29 queued = true
30 await current.value
31 return
32 }
33 let task = Task { @MainActor [weak self] in
34 repeat {
35 self?.queued = false
36 await pass()
37 } while self?.queued == true
38 // Cleared as the task's LAST act, with no suspension after the
39 // loop — so no caller can ever observe a finished-but-current
40 // task and return without its covering pass.
41 self?.current = nil
42 }
43 current = task
44 await task.value
45 }
46}
47