Share native refresh coalescing through RastrilloNative
The refresh gate was copied into Keymail, so fixes could diverge between clients. Preserve the public LChatCore type and its regression tests while using one pinned implementation.
3 files changed,
+17
−46
ios/LChatCore/Package.resolved+8 −0ios/LChatCore/Package.swift+5 −1ios/LChatCore/Sources/LChatCore/CoalescedRunner.swift+4 −45
diff --git a/ios/LChatCore/Package.resolved b/ios/LChatCore/Package.resolved| index d82f91f..d37d716 100644 |
| --- a/ios/LChatCore/Package.resolved |
| +++ b/ios/LChatCore/Package.resolved |
| @@ -1,5 +1,13 @@ |
| { |
| "pins" : [ |
| + { |
| + "identity" : "native", |
| + "kind" : "remoteSourceControl", |
| + "location" : "https://amadan.net/rastrillo/native", |
| + "state" : { |
| + "revision" : "f8b282fdceeb0dedc767fba473d5c60616cebf45" |
| + } |
| + }, |
| { |
| "identity" : "swift-cmark", |
| "kind" : "remoteSourceControl", |
diff --git a/ios/LChatCore/Package.swift b/ios/LChatCore/Package.swift| index 9ab7205..bf911c9 100644 |
| --- a/ios/LChatCore/Package.swift |
| +++ b/ios/LChatCore/Package.swift |
| @@ -12,6 +12,7 @@ let package = Package( |
| .library(name: "LChatCore", targets: ["LChatCore"]) |
| ], |
| dependencies: [ |
| + .package(url: "https://amadan.net/rastrillo/native", revision: "f8b282fdceeb0dedc767fba473d5c60616cebf45"), |
| // The native half of the twin-parsed CommonMark subset (#531): |
| // cmark-gfm via swift-markdown, walked with the same allowlist as |
| // web/static/markdown.js and held to the same vectors |
| @@ -27,7 +28,10 @@ let package = Package( |
| // test/schema-engine.test.mjs). |
| .target( |
| name: "LChatCore", |
| - dependencies: [.product(name: "Markdown", package: "swift-markdown")], |
| + dependencies: [ |
| + .product(name: "Markdown", package: "swift-markdown"), |
| + .product(name: "RastrilloNative", package: "native"), |
| + ], |
| resources: [.copy("Flows"), .copy("Icons"), .copy("Schemas"), .copy("Marks"), .copy("Seed")] |
| ), |
| // The cross-implementation drift gates (fixtures generated by the web |
diff --git a/ios/LChatCore/Sources/LChatCore/CoalescedRunner.swift b/ios/LChatCore/Sources/LChatCore/CoalescedRunner.swift| index 9d8df8c..4c5f8c6 100644 |
| --- a/ios/LChatCore/Sources/LChatCore/CoalescedRunner.swift |
| +++ b/ios/LChatCore/Sources/LChatCore/CoalescedRunner.swift |
| @@ -1,46 +1,5 @@ |
| -import Foundation |
| +import RastrilloNative |
| -// 🤖 Serialize fire-and-forget async work with trailing-edge coalescing: |
| -// while one pass runs, any number of further run() calls collapse into ONE |
| -// rerun after it finishes (using the running call's closure — callers all |
| -// request the same work). Built for ThreadStore.refresh, which refires on |
| -// every socket frame and from several UI paths while a full pass takes a |
| -// second-plus on a big space: uncoalesced, overlapping passes raced |
| -// duplicate /api/threads fetches and stacked their folds' publishes into |
| -// the 120–270ms frame hitches the 2026-07-20 device trace pinned on |
| -// NativeChatScreen's re-render. |
| -// |
| -// A caller that arrives mid-pass AWAITS the trailing rerun rather than |
| -// returning early: callers like createChat read the published state right |
| -// after run() and rely on a pass that STARTED after their call (the new |
| -// thread must be in `threads` when run() returns). The early-return first |
| -// cut broke that quietly — repaint callers didn't care, createChat did. |
| -@MainActor |
| -public final class CoalescedRunner { |
| - private var current: Task<Void, Never>? |
| - private var queued = false |
| - |
| - public init() {} |
| - |
| - public func run(_ pass: @escaping @MainActor () async -> Void) async { |
| - if let current { |
| - // Mid-pass: the trailing rerun starts after this call, so |
| - // awaiting the whole task is the freshness guarantee. |
| - queued = true |
| - await current.value |
| - return |
| - } |
| - let task = Task { @MainActor [weak self] in |
| - repeat { |
| - self?.queued = false |
| - await pass() |
| - } while self?.queued == true |
| - // Cleared as the task's LAST act, with no suspension after the |
| - // loop — so no caller can ever observe a finished-but-current |
| - // task and return without its covering pass. |
| - self?.current = nil |
| - } |
| - current = task |
| - await task.value |
| - } |
| -} |
| +// Preserve the app core API while sharing the refresh and awaiter-freshness |
| +// contract with the other native clients. Keep the app regression tests. |
| +public typealias CoalescedRunner = RastrilloNative.CoalescedRunner |