@silyze/background-runner v1.0.0
Background Runner
Background Runner is a tiny, type‑safe framework for executing tasks off the main call‑site – whether that means a local queue, a TCP worker, or a fully remote job system.
It gives you three primitives:
| Primitive | Purpose |
|---|---|
handler(...) | Declare a job with a name and an async function. |
registry(...) | Collect many job handlers into a typed registry. |
BackgroundRunner | Wire a transport (how jobs are delivered) to a registry and expose run(name, ...params). |
Everything is inferred so you can only call jobs that exist, with the right parameter shapes – no schema files or codegen needed.
Features
- End‑to‑end type‑safety – Job names and parameter tuples are checked at compile time.
- Pluggable transport – Swap
ExecuteInstantlyfor a queue, WebSocket, or message broker without touching caller code. - Zero dependencies – Ships as pure TypeScript typings + ~1 kB runtime.
- Structured clone example shows deep‑copy to keep workers pure.
Installation
npm install @silyze/background-runnerRequires TypeScript 5.2+ and Node 18+ (tested on 20 LTS).
Quick start
import {
handler,
registry,
BackgroundRunner,
JobHandler,
RegistryName,
MaybeRegistryJobHandler,
JobTransport,
} from "@silyze/background-runner";
/**
* A naïve transport that just calls the handler immediately.
* Replace this with a RabbitMQ or Redis implementation later.
*/
function ExecuteInstantly<
R extends JobHandler<string, any>[],
N extends RegistryName<R>
>(
getJobHandler: <F extends N>(name: F) => MaybeRegistryJobHandler<F, R>
): JobTransport<R, N> {
return {
handleJob(name, params) {
const jobHandler = getJobHandler(name);
// optional: network hop, retry logic, etc.
// @ts-ignore – deep copy keeps job code pure
jobHandler?.handler(...structuredClone(params));
},
};
}
/** Job definitions */
const consoleRegistry = registry(
handler("log", async (msg: string) => console.log(msg)),
handler("error", async (msg: string) => console.error(msg)),
handler("warn", async (msg: string) => console.warn(msg))
);
/** Runner bound to our transport */
const consoleRunner = new BackgroundRunner(consoleRegistry, ExecuteInstantly);
/** Fire‑and‑forget jobs */
consoleRunner.run("log", "Hello background world!");
consoleRunner.run("error", "Something went wrong…");
consoleRunner.run("warn", "Heads‑up!");API Reference
Types & Helpers
| Symbol | Description |
|---|---|
SerializableValue | JSON‑friendly primitives plus nested arrays/objects. |
JobHandler<N,P> | { name: N; handler: (...params: P) => void\|Promise }. |
ParamsOf<JobHandler> | Extracts the parameter tuple P. |
RegistryName<Registry> | Union of job names inside a registry. |
MaybeRegistryJobHandler | Safely looks up a handler by name, returns undefined if absent. |
RegistryJobHandler | Same lookup but never when not found (internal). |
handler(name, fn)
Create a job handler.
const sendEmail = handler("send-email", async (to: string, body: string) => {
/* … */
});registry(...handlers)
Bundle handlers so they can share a runner.
const mailRegistry = registry(
sendEmail,
handler("ping", () => {})
);JobTransport
Interface a transport must fulfil:
interface JobTransport<Registry, Name> {
handleJob(name: Name, params: ParamsTuple): void;
}Implementations typically serialize & enqueue the call, then a worker dequeues and executes.
BackgroundRunner
new BackgroundRunner(registry, transportFactory);registry– Output ofregistry(...).transportFactory(getJobHandler)– Function that returns aJobTransport. You receive a safegetJobHandlerhelper for worker‑side lookup.
The runner exposes:
run(name, ...params): voidType checking guarantees name exists and params match the handler signature.
Building custom transports
Because BackgroundRunner provides a registry‑aware getJobHandler, your transport doesn’t need reflection. A minimal queue example:
function ViaInMemoryQueue<
R extends JobHandler<string, any>[],
N extends RegistryName<R>
>(
getJobHandler: <F extends N>(name: F) => MaybeRegistryJobHandler<F, R>
): JobTransport<R, N> {
const queue: { name: N; params: SerializableValue[] }[] = [];
return {
handleJob(name, params) {
queue.push({ name, params });
},
};
// somewhere else (worker loop)
setInterval(() => {
const job = queue.shift();
if (!job) return;
getJobHandler(job.name)?.handler(...job.params);
}, 100);
}5 months ago