UNPKG

@on-the-ground/daemonizer

Version:

A minimal async control flow framework for browser and Node.js daemons.

201 lines (139 loc) 5.61 kB
# 🌀 Daemonizer A minimal async control flow framework for writing browser and Node.js daemons. Daemonizer helps you manage long-running async event loops, safely respond to cancellation, and yield control to the macro task queue—without starving the event loop. --- ## 🚀 Installation ```bash yarn add daemonizer # or npm install daemonizer ``` --- ## ✨ Features - **Abort-aware event loop** - **Task group with lifecycle tracking** - **Yielding mechanism to avoid blocking** - **Bounded queue for backpressure** - **Partitioned processing for per-key ordering with cross-partition parallelism** - **Works in both Node.js and browser** --- ## 🧪 Try in Your Browser Run the daemon in your browser with no setup: 👉 [examples/example.html](./examples/example.html) Open your browser console and watch `tick:` messages stream in real time! ```html <!-- examples/example.html --> <!DOCTYPE html> <html> <body> <script type="module"> import { Daemon } from "https://esm.sh/@on-the-ground/daemonizer@latest"; ///////////// Daemon Example ///////////// let controller = new AbortController(); const daemon = new Daemon(controller.signal, async (_signal, event) => { await new Promise((r) => setTimeout(r, 1000)); console.log("tick:", event); }); for (let i = 1; i <= 5; i++) { await daemon.pushEvent(i); } setTimeout(() => controller.abort(), 3000); console.log("waiting the daemon down"); await daemon.close(); console.log("the daemon got down"); // Results: // waiting the daemon down // tick: 1 // tick: 2 // the daemon got down // tick: 3 <- long running task, use strictInterval to abort it </script> </body> </html> ``` --- ## 🧠 API Overview ### `Daemon` – The Core Abstraction ```ts import { Daemon } from "@on-the-ground/daemonizer"; const daemon = new Daemon(signal, async (msg) => { // Your background task handler console.log("received:", msg); }); // Push a task into the daemon's queue await daemon.pushEvent({ type: "log", content: "hello" }); // Or push without waiting for queue space—returns false if full or closed const accepted = daemon.tryPushEvent({ type: "log", content: "hello" }); // Gracefully shut down when you're done await daemon.close(); ``` #### ✅ Features - Runs background tasks with structured concurrency - Backpressure-safe via internal bounded queue - Auto-shuts down when `AbortSignal` is aborted - One-liner setup: no boilerplate, no ceremony --- ### `PartitionedDaemon` – Parallel Processing with Per-key Ordering Routes events to one of N `Daemon` instances by hashing a key extracted from each event, so events sharing a key always land on the same partition (preserving order) while different partitions process in parallel—analogous to Kafka's partition model. `PartitionedDaemon` builds every partition itself, by calling your `partitionFactory` once per index and since it built them, it's the one that closes them too. Because the factory runs fresh for each index, each call can close over its own local state. ```ts import { Daemon, PartitionedDaemon } from "@on-the-ground/daemonizer"; // Each partition owns an independent instance they never see each other's state. const daemon = new PartitionedDaemon( () => { const store = new Map<number, unknown>(); return new Daemon(signal, async (_signal, event) => { store.set(event.userId, event); console.log("received:", event); }, 10 /* bufferSize */); }, 4, // partitionCount (event) => event.userId // key extractor: decides the partition ); await daemon.pushEvent({ userId: 42, type: "log", content: "hello" }); // Closes all partitions in parallel and waits for every one to drain. await daemon.close(); ``` If every partition should just share one stateless handler, the factory just ignores its index and returns the same shape every time: ```ts const daemon = new PartitionedDaemon( () => new Daemon(signal, handleEvent, 10), 4, (event) => event.userId ); ``` #### ✅ Features - Per-key ordering, cross-partition parallelism - Same `pushEvent` / `tryPushEvent` / `close` surface as `Daemon` - Owns every partition it builds via `partitionFactory`, so per-partition local state is just a closure away and lifecycle (`close()`) stays unambiguous --- ### 🧰 Low-level Tools (also exported) ### `launchEventLoop(signal, taskGroup, events, handler)` Runs a long-lived, abortable event loop over an `AsyncIterable`. Automatically yields to the macro task queue to prevent starvation. ### `TaskGroup` Tracks the lifecycle of multiple concurrent async tasks. - `add(n = 1)` - `done()` - `wait(): Promise<void>` ### `MacroTaskYielder` Yields only if enough time has passed since the last yield. ### `BoundedQueue<T>` A fixed-capacity async queue. Backpressure-aware and safe for multiple consumers. --- ## 🌐 Compatibility Daemonizer is fully compatible with: - Node.js (v16+) - Modern browsers (via bundlers like Vite, Webpack, etc.) No external runtime dependencies. --- ## 📜 License MIT © 2025 Joohyung Park [github.com/on-the-ground/daemonizer](https://github.com/on-the-ground/daemonizer) --- > _"The name was subconsciously inspired by countless replays of Judas Priest’s 'Demonizer'."_