Skip to main content

webui_dev_server/
worker.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT license.
3
4//! Generic rebuild-worker driver for dev servers.
5//!
6//! Spawns a background OS thread that owns the rebuild loop:
7//!  1. Block waiting for a tick.
8//!  2. Drain any extra ticks queued before the thread woke up — they
9//!     coalesce into a single rebuild.
10//!  3. Run the user-supplied rebuild closure.
11//!  4. Drain any ticks that arrived during the rebuild — if any did,
12//!     re-tick ourselves so the next iteration runs without waiting.
13//!  5. Report success / error via [`RebuildReporter`] and broadcast
14//!     the corresponding live-reload event.
15//!
16//! The closure is the only thing that varies between consumers
17//! (webui-cli builds and renders an app, webui-press rebuilds a docs
18//! site). It returns `Result<(), String>` so the worker can
19//! uniformly format the error and broadcast it to connected browsers.
20
21use std::sync::mpsc::{sync_channel, SyncSender};
22use std::thread;
23
24use crate::livereload::LiveReload;
25use crate::reporter::RebuildReporter;
26
27/// Tick-channel capacity. Bounded so a watcher event burst can't
28/// unboundedly queue inside the rebuild worker. The worker drains all
29/// pending ticks before each build, so a dropped `try_send` on a full
30/// channel coalesces the same way the drain loop does.
31const TICK_CHANNEL_CAPACITY: usize = 8;
32
33/// Tick sender given to the watcher closure. Cheap to clone.
34pub type TickSender = SyncSender<()>;
35
36/// Spawn the rebuild worker on a dedicated OS thread and return the
37/// sender used to enqueue rebuild ticks. The watcher closure should
38/// call `tx.try_send(())` for every filesystem event burst — failed
39/// sends (channel full) are intentional coalescing.
40///
41/// The closure runs synchronously on the worker thread, so it may use
42/// blocking I/O freely. It returns `Ok(())` on success or `Err(msg)`
43/// with a user-facing message on failure; the worker handles printing
44/// and broadcasting based on the result.
45///
46/// The returned [`TickSender`] does not need to be held to keep the
47/// worker alive — the worker stops only when every clone of the
48/// sender is dropped.
49pub fn spawn_rebuild_worker<F>(livereload: LiveReload, mut rebuild: F) -> TickSender
50where
51    F: FnMut() -> Result<(), String> + Send + 'static,
52{
53    let (tx, rx) = sync_channel::<()>(TICK_CHANNEL_CAPACITY);
54    thread::spawn(move || {
55        let mut reporter = RebuildReporter::new();
56        let mut dirty = false;
57        loop {
58            if dirty {
59                // A rebuild we just finished was racing with new events.
60                // Skip the blocking recv so we rebuild immediately —
61                // but still drain any ticks that piled up so they
62                // collapse into this iteration.
63                while rx.try_recv().is_ok() {}
64            } else {
65                // Block for the first tick. Channel closed (every
66                // external sender dropped) → exit cleanly so the
67                // process can shut down without a zombie thread.
68                if rx.recv().is_err() {
69                    break;
70                }
71                // Drain extra ticks that piled up while we were
72                // waiting — they all collapse into this rebuild.
73                while rx.try_recv().is_ok() {}
74            }
75
76            let start = std::time::Instant::now();
77            let result = rebuild();
78
79            // Drain ticks that arrived during the build (= dirty events).
80            // If any showed up, the next iteration runs without
81            // blocking on `recv` so the user sees their change ASAP.
82            dirty = false;
83            while rx.try_recv().is_ok() {
84                dirty = true;
85            }
86
87            match result {
88                Ok(()) => {
89                    reporter.success(start.elapsed());
90                    livereload.broadcast_reload();
91                }
92                Err(msg) => {
93                    reporter.error(&msg);
94                    livereload.broadcast_error(msg);
95                }
96            }
97        }
98    });
99    tx
100}