From e680537e4a30ddd36e3e2bf3bcc59df779b1dd02 Mon Sep 17 00:00:00 2001 From: Brent Deverman Date: Tue, 23 Jun 2026 12:38:36 +0800 Subject: [PATCH] Serialize dev rebuild callbacks --- Sources/Saga/Saga+Build.swift | 50 +++++++++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/Sources/Saga/Saga+Build.swift b/Sources/Saga/Saga+Build.swift index 118c83f..fc8d2b0 100644 --- a/Sources/Saga/Saga+Build.swift +++ b/Sources/Saga/Saga+Build.swift @@ -105,6 +105,7 @@ extension Saga { /// Signals saga-cli via SIGUSR1 if Swift source files change (so it can recompile and relaunch). func watchAndRebuild() async throws { let watchPaths = [inputPath, rootPath + "Sources"] + let coordinator = DevRebuildCoordinator(saga: self) let monitor = FolderMonitor(paths: watchPaths, ignoredPatterns: ignoreChangesPatterns) { [weak self] changedPaths in guard let self else { return } @@ -115,17 +116,8 @@ extension Saga { return } - Task { - do { - try self.reset() - if let changedPath = changedPaths.first { - self.buildReason = .fileChange(changedPath) - } - try await self.build() - self.signalParent(SIGUSR2) - } catch { - self.fileIO.log("💥 Rebuild failed: \(error)") - } + Task { [coordinator] in + await coordinator.enqueue(changedPaths) } } @@ -137,3 +129,39 @@ extension Saga { } } } + +private actor DevRebuildCoordinator { + private let saga: Saga + private var isBuilding = false + private var pendingPaths = Set() + + init(saga: Saga) { + self.saga = saga + } + + func enqueue(_ changedPaths: Set) async { + pendingPaths.formUnion(changedPaths) + guard !isBuilding else { return } + + // Saga's mutable pipeline state is process-wide. Rebuild callbacks must + // run one at a time, otherwise file-save bursts race in reset/build. + isBuilding = true + defer { isBuilding = false } + + while !pendingPaths.isEmpty { + let paths = pendingPaths + pendingPaths.removeAll() + + do { + try saga.reset() + if let changedPath = paths.first { + saga.buildReason = .fileChange(changedPath) + } + try await saga.build() + saga.signalParent(SIGUSR2) + } catch { + saga.fileIO.log("💥 Rebuild failed: \(error)") + } + } + } +}