Core@rhythmjs/rhythm
The core pipeline
Compose behavior around a typed input and a shared context.
Create an instance#
Rhythm<TInput, TStartup> declares the input required by each run and, optionally, the shape of the startup values the app holds in context. Optional name and type labels make errors from registered modules easier to identify.
import { Rhythm } from "@rhythmjs/rhythm";
const app = new Rhythm<{ jobId: string }, { service: string }>({ name: "jobs" }).use((ctx) =>
console.log(ctx.service, ctx.jobId),
);
app.context.service = "worker";Run and reuse#
run(input) creates a shallow context from { ...app.context, ...input }, dispatches it through the chain, and returns the final context. Startup values are on the context from the start, and an input field with the same name wins. See Startup context.
callback() exposes the reusable async handler. Finish configuring the pipeline before obtaining a handler.
const handle = app.callback();
await handle({ jobId: "job-1" });
const result = await app.run({ jobId: "job-2" });
console.log(result.service); // workerUnderstand state#
Every run gets a new top-level context object, but startup values are shared references. DeepReadonly is a TypeScript constraint, not a runtime freeze. Avoid mutable shared request state in context.
Choose a composition boundary#
Use register(child, exportValue) when a module needs a separate top-level context. Use use(child.middleware()) for flat composition and fallthrough into the parent.
A flat-mounted child works on the parent's context object, so its startup values are merged into it and stay visible to the parent's later middleware. Use register() when the child's values must stay inside it.