Docs
/

@leumas/jobs

Leumas Jobs — the one durable, connector-backed job engine every long-running domain shares. Persists each job in the dynamic jobs collection (survives restart), runs kind-keyed runners with a...

@leumas/jobs

The one durable job engine, and the queue that is Leumas Fabric's control plane. A job is a row in the dynamic jobs collection, so it survives a restart; a runner registered for its kind does the work; progress rides the automation SSE bus.

import { createJobEngine } from '@leumas/jobs';
import { createJobsStore, createJobsRouter } from '@leumas/jobs/surface';

const jobs = createJobEngine({ connector, stream, name: 'default', concurrency: 1 });
jobs.registerRunner('render', async (job, helpers) => {
  helpers.log('starting');                       // durable — kept on the row, capped
  await helpers.progress('encoding', 40);
  return { url: '…' };                           // returning marks it done; throwing marks it failed
}, { label: 'Render', group: 'Video', memberSafe: true, schedulable: true });

app.use('/api/jobs', createJobsRouter({ store: createJobsStore({ connector }), engines: () => [jobs], gate, isAdmin }));

The five things worth knowing before you touch it

1. Submitting is EXECUTION, and it is authorized per kind. POST /api/jobs denies by default: a kind is operator-only unless its manifest says memberSafe: true. capability reaches the whole ecosystem-cell registry, so it deliberately is not. jobs is also in protectedCollections — the router is the only write door, because resumePending() executes whatever it finds at the next boot.

2. waiting is a state, not a held slot. A runner that cannot start yet returns helpers.requeue(reason, afterMs): the row goes to waiting with the reason in words, the concurrency slot is released, and it is re-asked later. Blocking instead would deadlock the queue — at the default concurrency of 1, one unplaceable job would starve every placeable job behind it. wakeWaiting() re-asks everything immediately, which is what makes plugging a machine in drain the backlog in front of the person who plugged it in.

3. Events are ADDRESSED. push({ userId: owner, andOperators: true }) — the owner sees their own job and operators still see the whole queue. Unaddressed events on that bus reach operators ONLY, so for a while the one person who could not watch a job was the person who started it.

4. Two engines share this collection. The default one and the fleet's (FLEET_CONCURRENCY), kept apart only by kind-scoped resumePending. Registering the same kind on both makes them fight over the same row. describeKinds() reports which engine owns a kind, and at what width.

5. A row is normalized on read. normalizeJob() backfills startedAt/endedAt for rows written before those existed and marks them derived — so a surface renders "≈" and a percentile can exclude them, rather than reporting a queue latency of zero that never happened.

Guards

pnpm smoke:jobs runs the real engine over an in-memory connector: timestamps, the durable tail, the deadlock canary (a waiting job must not hold its slot), cancel disarming a pending re-ask, SSE addressing, and the four authorization refusals. pnpm smoke:fabric covers the fleet path.

Generated from the Leumas repository. Every page cites the file it came from.leumas.techllms.txt