@on-the-ground/daemonizer
v0.2.2
Published
A minimal async control flow framework for browser and Node.js daemons.
Downloads
188
Maintainers
Readme
🌀 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
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
Open your browser console and watch tick: messages stream in real time!
<!-- 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
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
AbortSignalis 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.
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:
const daemon = new PartitionedDaemon(
() => new Daemon(signal, handleEvent, 10),
4,
(event) => event.userId
);✅ Features
- Per-key ordering, cross-partition parallelism
- Same
pushEvent/tryPushEvent/closesurface asDaemon - 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
"The name was subconsciously inspired by countless replays of Judas Priest’s 'Demonizer'."
