1.0.0 • Published 5 months ago

@silyze/background-runner v1.0.0

Weekly downloads
-
License
MIT
Repository
-
Last release
5 months ago

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:

PrimitivePurpose
handler(...)Declare a job with a name and an async function.
registry(...)Collect many job handlers into a typed registry.
BackgroundRunnerWire 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 ExecuteInstantly for 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-runner

Requires 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

SymbolDescription
SerializableValueJSON‑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.
MaybeRegistryJobHandlerSafely looks up a handler by name, returns undefined if absent.
RegistryJobHandlerSame 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 of registry(...).
  • transportFactory(getJobHandler) – Function that returns a JobTransport. You receive a safe getJobHandler helper for worker‑side lookup.

The runner exposes:

run(name, ...params): void

Type 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);
}
1.0.0

5 months ago