Skip to main content

Crate coalesce_worker

Crate coalesce_worker 

Source
Expand description

A coalescing worker thread with generation-counter stale-result rejection — the discipline needed to run tree-sitter (or any expensive incremental parser) off the main thread without corrupting state with out-of-date results.

§The problem

Suppose a GUI editor offloads syntax highlighting to a background thread. The user types rapidly; the main thread fires off highlight requests, one per keystroke. The worker can only process one at a time. By the time request N-5 finishes, the source has moved to state N, and the result computed from the stale N-5 source is no longer valid — if the main thread applies it anyway, the UI shows spans pointing at byte offsets that no longer exist, causing rendering glitches or panics.

This class of bug is documented in goliajp/devops/dotclaude/common/classic-errors.md as “Stale async cache after mutation”.

§The fix

Two disciplines enforced by this crate:

  • Request coalescing — when the worker finishes one job, it drains any queued requests and processes only the latest; older requests that never started are silently discarded.
  • Generation counting — every submitted request gets a monotonic generation number. When the main thread polls for results, it drains the receive channel and keeps only the newest generation; older results are dropped without being applied.

§Example: tree-sitter highlighting

use coalesce_worker::{Worker, Coalescer};
use std::sync::Arc;

struct HighlightWorker {
    highlighter: tree_sitter_highlight::Highlighter,
}

struct HighlightRequest {
    source: Arc<Vec<u8>>,
    config: Arc<tree_sitter_highlight::HighlightConfiguration>,
}

struct HighlightResponse {
    events: Vec<tree_sitter_highlight::HighlightEvent>,
}

impl Worker for HighlightWorker {
    type Request = HighlightRequest;
    type Response = HighlightResponse;
    fn handle(&mut self, req: Self::Request) -> Self::Response {
        let events = self.highlighter
            .highlight(&req.config, &req.source, None, |_| None)
            .unwrap()
            .collect::<Result<Vec<_>, _>>()
            .unwrap();
        HighlightResponse { events }
    }
}

Then drive the coalescer from the main thread:

// main loop
coalescer.submit(current_source());
if let Some(Output { generation: _, value }) = coalescer.poll() {
    render_highlights(value);
}

§Not only for tree-sitter

Any long-running background computation that can be superseded — rebuilding a suggestion index, recompiling a preview, running a linter — fits the same pattern.

Structs§

Coalescer
Coalescing async dispatcher around a Worker.
Output
One response from the worker, tagged with the generation of the request that produced it.

Traits§

Worker
A worker that owns its processing state and handles one request at a time.