@ultimat3/jobs 15.0.0 → 17.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CLAUDE.md CHANGED
@@ -107,6 +107,48 @@ Tier 3. The `job` + `task` primitives, durable steps, transactional outbox, queu
107
107
  `createWorker().start()` THROW `X_JOB_CONCURRENCY_UNENFORCEABLE` when a registered job declares
108
108
  `concurrency`: a documented guarantee that silently does nothing is the worst of the three
109
109
  options, and refusing is what axiom 3 asks for.
110
+ - **`WorkerOptions.concurrency` is read by OWN key, because a queue NAME is deployment data**
111
+ (`worker.ts`, `As of 2026-08-26`). `options.concurrency?.[queue]` answered
112
+ `Object.prototype.constructor` for a queue called `constructor`, so
113
+ `Math.max(0, <function> - inFlight)` was `NaN`, the `free === 0` guard did not catch it, and the
114
+ pass issued `driver.claim({ limit: NaN })`. `bun run proto-index` cannot reach this one — the
115
+ table is a **parameter**, not an object literal in the file — so `worker-slots.test.ts` is the
116
+ enforcement, over `constructor`, `__proto__` and `toString` at once.
117
+ - **Every numeric knob is refused when it is not a FINITE number** — `@ultimat3/core`'s
118
+ `finiteOption()` (a bound) and `finiteCount()` (a whole number of things, with the caller's
119
+ minimum) are the two refusals, and this package declares none of its own. `worker-options.ts` is
120
+ the one place `createWorker` reads them (`As of 2026-08-26`), and `createOutboxRelay` refuses its
121
+ own two.
122
+ Measured: `visibilityTimeoutMs: NaN` makes `visibleAt` `NaN`, the reclaim scan asks
123
+ `visibleAt <= now`, and a job whose worker DIED is never claimable again — at-least-once becomes
124
+ never, on a row `x jobs ls` still prints as `running`. `concurrency: NaN` slices `(0, NaN)`, so
125
+ the worker claims nothing and reports healthy; `pollIntervalMs: NaN` is `setTimeout(fn, 0)`, so
126
+ the claim loop spins on the database. `??` guards only nullish and `Math.max`/`Math.floor`
127
+ propagate `NaN`: `Number(process.env.X)` on an unset variable arrives intact. Same refusal
128
+ `createLimiter`'s `maxTenants` and `backfill()`'s `batch` already made.
129
+
130
+ **`bun run finite-bounds` is a floor, never the answer, and a pin of zero is not proof**
131
+ (`As of 2026-08-26`). It matches `x.y ?? CONST`, so it never saw `createLimiter`'s four
132
+ ceilings — read as `config.global !== undefined && global >= config.global`, a shape with no
133
+ `??` in it — and this package read as clean at **zero** while every one of them was off.
134
+ Measured: `createLimiter({ global: Number(process.env.WORKER_GLOBAL_CONCURRENCY) })` with the
135
+ variable unset granted **1000 of 1000** acquires where `global: 2` grants 2, and
136
+ `snapshot().config` still reported the ceiling to `/_x`. All five numbers (`perTenant`,
137
+ `perQueue`, `global`, `ratePerTenant.limit`, `ratePerTenant.windowMs` — the window is half the
138
+ same ceiling, since `stamp > at - NaN` empties it on every call) are screened at construction
139
+ beside `maxTenants`, `finiteCount` with **min 0**: zero is a HARD STOP here and one this repo's
140
+ own suite configures, never "unlimited", which is what omitting the option means.
141
+
142
+ **A row count is `finiteCount`, and the reason is driver parity** (`As of 2026-08-26`).
143
+ `finiteOption` accepts `-1` and `2.5`, and both diverge: `introspect.list({ limit: -1 })` sliced
144
+ every row BUT the newest on `driver-memory.ts` and Postgres answers `ERROR: LIMIT must not be
145
+ negative` (probed on pg18), while `2.5` keeps 2 rows here and **3** there with no error on either
146
+ side. `list`, `deadLetters`, the backfill ledger's `list` and `assertClaimBounds` (both drivers'
147
+ `claim`) take the count screen with **min 0** — `limit: 0` is zero rows on both — and
148
+ `claim`'s `visibilityTimeoutMs` takes `finiteOption`, because a lease window is a duration.
149
+ The memory driver's `list`/`deadLetters` are `async` for the reason `claim` is: a refusal must
150
+ REJECT on both, and a synchronous throw out of a method typed `Promise<…>` is itself the
151
+ divergence.
110
152
  - **`enqueuedBy` is ATTRIBUTION, never authority — decided 2026-08, do not re-litigate.** Both
111
153
  answers were defensible. Impersonating the enqueuer at claim time gives correct authz and is
112
154
  rejected because a job that sleeps three days, or dead-letters and is retried next quarter, then
@@ -850,7 +892,7 @@ picture from the other side.
850
892
  | `events-pg.ts` | `createPgEventBus` — `step.waitForEvent` across processes |
851
893
  | `driver.ts` | `JobDriver` contract + wire records |
852
894
  | `driver-pg.ts` | default driver, real SQL constants, and `createPgLeader` — the advisory-lock election that is **not** what a scheduler uses; `scheduler-pg.ts` above owns the lease-row one boot wires |
853
- | `driver-pg-ddl.ts` | `SQL_JOBS_TABLE` + `SQL_OUTBOX_TABLE` — the schema the driver installs. Whichever file holds the DDL is the one whose comments may carry no `;` and no `'` |
895
+ | `driver-pg-ddl.ts` | `SQL_JOBS_TABLE` — the schema the driver installs, and the ONE install point: every durable table this package owns, `x_outbox` included, is declared in it. Whichever file holds the DDL is the one whose comments may carry no `;` and no `'` |
854
896
  | `driver-pg-jobs-sql.ts` | every statement returning a whole `x_jobs` row, and the `JOB_ROW_COLUMNS` projection they share. Split off at `driver-pg-sql.ts`'s size ceiling and re-exported from it |
855
897
  | `driver-pg-rows.ts` | a Postgres row → a wire record: `JobRow`/`StepRow`/`BackfillRow` and their mappings |
856
898
  | `driver-memory.ts` | `x dev` / tests |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/jobs",
3
- "version": "15.0.0",
3
+ "version": "17.0.0",
4
4
  "description": "Durable background work: steps, transactional outbox, cron tasks, one driver interface",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -32,9 +32,9 @@
32
32
  "test": "bun test"
33
33
  },
34
34
  "dependencies": {
35
- "@ultimat3/core": "15.0.0",
36
- "@ultimat3/entity": "15.0.0",
37
- "@ultimat3/schema": "15.0.0",
38
- "@ultimat3/time": "15.0.0"
35
+ "@ultimat3/core": "17.0.0",
36
+ "@ultimat3/entity": "17.0.0",
37
+ "@ultimat3/schema": "17.0.0",
38
+ "@ultimat3/time": "17.0.0"
39
39
  }
40
40
  }
@@ -10,7 +10,7 @@
10
10
  // become — the checkpoints are transactional with the work, and this row is not.
11
11
 
12
12
  import type { Clock } from '@ultimat3/core';
13
- import { systemClock } from '@ultimat3/core';
13
+ import { finiteCount, systemClock } from '@ultimat3/core';
14
14
  import { nowMs } from './clock';
15
15
 
16
16
  /**
@@ -167,7 +167,8 @@ export function createMemoryBackfillLedger(clock: Clock = systemClock): Backfill
167
167
  });
168
168
  return Promise.resolve();
169
169
  },
170
- list(filter = {}) {
170
+ // `async`, so a refused limit REJECTS here as it does on the pg ledger — see `driver-memory.ts`.
171
+ async list(filter = {}) {
171
172
  // Reversed BEFORE the sort: the test clock is frozen, so two rows share a `startedAt` and a
172
173
  // stable sort would hand back the oldest of them first under a "newest first" contract.
173
174
  const rows = [...runs.values()]
@@ -176,8 +177,8 @@ export function createMemoryBackfillLedger(clock: Clock = systemClock): Backfill
176
177
  .filter((run) => filter.name === undefined || run.name === filter.name)
177
178
  .filter((run) => filter.status === undefined || run.status === filter.status)
178
179
  .filter((run) => filter.runId === undefined || run.runId === filter.runId)
179
- .slice(0, filter.limit ?? 100);
180
- return Promise.resolve(rows);
180
+ .slice(0, finiteCount('the backfill ledger list', 'limit', filter.limit ?? 100));
181
+ return rows;
181
182
  },
182
183
  };
183
184
  }
@@ -3,7 +3,7 @@
3
3
  // real claim/ack/nack paths rather than a mock that always succeeds.
4
4
 
5
5
  import type { Clock } from '@ultimat3/core';
6
- import { assert, systemClock, uuid } from '@ultimat3/core';
6
+ import { assert, finiteCount, systemClock, uuid } from '@ultimat3/core';
7
7
  import type { BackfillLedger } from './backfill-ledger';
8
8
  import { createMemoryBackfillLedger } from './backfill-ledger';
9
9
  import { nowMs } from './clock';
@@ -20,7 +20,7 @@ import type {
20
20
  NackOptions,
21
21
  QueueStats,
22
22
  } from './driver';
23
- import { assertClaimQueues, DEFAULT_QUEUE } from './driver';
23
+ import { assertClaimBounds, assertClaimQueues, DEFAULT_QUEUE } from './driver';
24
24
  import { JobDuplicateError } from './errors';
25
25
  import type { LeaseStore } from './leases';
26
26
  import { createMemoryLeaseStore } from './leases';
@@ -103,7 +103,10 @@ export function createMemoryDriver(options: MemoryDriverOptions = {}): MemoryJob
103
103
  job(jobId) {
104
104
  return Promise.resolve(jobs.get(jobId));
105
105
  },
106
- list(filter: JobFilter = {}) {
106
+ // `async` for the reason `claim` is: a refused bound must REJECT here exactly as it does on the
107
+ // pg driver, and a synchronous throw out of a method typed `Promise<…>` is a second answer to
108
+ // one question.
109
+ async list(filter: JobFilter = {}) {
107
110
  const rows = [...jobs.values()]
108
111
  .filter((record) => filter.queue === undefined || record.queue === filter.queue)
109
112
  .filter((record) => filter.name === undefined || record.name === filter.name)
@@ -112,15 +115,15 @@ export function createMemoryDriver(options: MemoryDriverOptions = {}): MemoryJob
112
115
  // `x jobs ls` answered one thing against `x dev` and the opposite in production — and,
113
116
  // because the limit is applied after the sort, a default page of the hundred OLDEST rows.
114
117
  .sort((a, b) => b.createdAt - a.createdAt)
115
- .slice(0, filter.limit ?? 100);
116
- return Promise.resolve(rows);
118
+ .slice(0, finiteCount('the memory driver list', 'limit', filter.limit ?? 100));
119
+ return rows;
117
120
  },
118
- deadLetters(limit = 100) {
121
+ async deadLetters(limit = 100) {
119
122
  const rows = [...jobs.values()]
120
123
  .filter((record) => record.state === 'dead')
121
124
  .sort((a, b) => b.updatedAt - a.updatedAt)
122
- .slice(0, limit);
123
- return Promise.resolve(rows);
125
+ .slice(0, finiteCount('the memory driver dead letters', 'limit', limit));
126
+ return rows;
124
127
  },
125
128
  async requeue(jobId, requeueOptions) {
126
129
  const existing = jobs.get(jobId);
@@ -199,6 +202,7 @@ export function createMemoryDriver(options: MemoryDriverOptions = {}): MemoryJob
199
202
  // question, which is the class of divergence this pair is checked for.
200
203
  async claim(claimOptions: ClaimOptions): Promise<readonly ClaimedJob[]> {
201
204
  assertClaimQueues('memory', claimOptions);
205
+ assertClaimBounds('memory', claimOptions);
202
206
  const at = nowMs(clock);
203
207
  const wanted = new Set(claimOptions.queues);
204
208
  const claimable = [...jobs.values()]
@@ -177,29 +177,3 @@ create table if not exists x_job_events (
177
177
  create index if not exists x_job_events_lookup_idx
178
178
  on x_job_events (name, published_at);
179
179
  `.trim();
180
-
181
- /**
182
- * Kept as its own constant because it is a public export and `x_outbox` is a table an operator
183
- * may need to create alone. It is ALSO inside `SQL_JOBS_TABLE`, which is the one boot applies —
184
- * two install points for one table is how the outbox came to be documented and never created.
185
- */
186
- export const SQL_OUTBOX_TABLE = `
187
- create table if not exists x_outbox (
188
- id uuid primary key,
189
- job text not null,
190
- queue text not null default 'default',
191
- input jsonb not null,
192
- idempotency_key text not null,
193
- max_attempts int not null default 3,
194
- run_at timestamptz not null default now(),
195
- staged_at timestamptz not null default now(),
196
- tenant_id text,
197
- traceparent text,
198
- enqueued_by text,
199
- published_at timestamptz
200
- );
201
- create index if not exists x_outbox_unpublished_idx
202
- on x_outbox (staged_at) where published_at is null;
203
- alter table x_outbox add column if not exists claimed_at timestamptz;
204
- alter table x_outbox add column if not exists claimed_by text;
205
- `.trim();
@@ -7,7 +7,7 @@
7
7
  // it is applied once at boot and never by a driver method. Re-exported from here so the install
8
8
  // point keeps the ONE import path every caller already uses.
9
9
 
10
- export { SQL_JOBS_TABLE, SQL_OUTBOX_TABLE } from './driver-pg-ddl';
10
+ export { SQL_JOBS_TABLE } from './driver-pg-ddl';
11
11
  // The whole-`x_jobs`-row reads, split off at this file's size ceiling and re-exported for the
12
12
  // same reason the DDL is. `JOB_ROW_COLUMNS` is imported as a value because `SQL_CANCEL` also
13
13
  // returns a whole row and must project it identically — two spellings of one row shape is how
package/src/driver-pg.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  // they run against in `driver-pg-ddl.ts`, and the row-to-record decoding in `driver-pg-rows.ts`.
6
6
 
7
7
  import type { Clock } from '@ultimat3/core';
8
- import { systemClock, uuid } from '@ultimat3/core';
8
+ import { finiteCount, systemClock, uuid } from '@ultimat3/core';
9
9
  import type { BackfillLedger } from './backfill-ledger';
10
10
  import { nowMs } from './clock';
11
11
  import type {
@@ -20,7 +20,7 @@ import type {
20
20
  NackOptions,
21
21
  QueueStats,
22
22
  } from './driver';
23
- import { assertClaimQueues, DEFAULT_QUEUE } from './driver';
23
+ import { assertClaimBounds, assertClaimQueues, DEFAULT_QUEUE } from './driver';
24
24
  import type { BackfillRow, JobRow, StepRow } from './driver-pg-rows';
25
25
  import { num, toBackfillRun, toJobRecord, toStepRecord } from './driver-pg-rows';
26
26
  import {
@@ -135,7 +135,7 @@ function pgBackfillLedger(exec: () => PgExecutor): BackfillLedger {
135
135
  filter.name ?? null,
136
136
  filter.status ?? null,
137
137
  filter.runId ?? null,
138
- filter.limit ?? 100,
138
+ finiteCount('the pg driver list', 'limit', filter.limit ?? 100),
139
139
  ]);
140
140
  return rows.map(toBackfillRun);
141
141
  },
@@ -200,12 +200,14 @@ export function createPgDriver(options: PgDriverOptions = {}): JobDriver {
200
200
  filter.queue ?? null,
201
201
  filter.name ?? null,
202
202
  filter.state ?? null,
203
- filter.limit ?? 100,
203
+ finiteCount('the pg driver list', 'limit', filter.limit ?? 100),
204
204
  ]);
205
205
  return rows.map(toJobRecord);
206
206
  },
207
207
  async deadLetters(limit = 100) {
208
- const rows = await exec().query<JobRow>(SQL_JOB_DEAD_LETTERS, [limit]);
208
+ const rows = await exec().query<JobRow>(SQL_JOB_DEAD_LETTERS, [
209
+ finiteCount('the pg driver dead letters', 'limit', limit),
210
+ ]);
209
211
  return rows.map(toJobRecord);
210
212
  },
211
213
  async requeue(jobId, requeueOptions) {
@@ -293,6 +295,7 @@ export function createPgDriver(options: PgDriverOptions = {}): JobDriver {
293
295
 
294
296
  async claim(claimOptions: ClaimOptions): Promise<readonly ClaimedJob[]> {
295
297
  assertClaimQueues('pg', claimOptions);
298
+ assertClaimBounds('pg', claimOptions);
296
299
  const rows = await exec().query<JobRow>(SQL_CLAIM, [
297
300
  claimOptions.queues,
298
301
  claimOptions.limit,
package/src/driver.ts CHANGED
@@ -8,6 +8,7 @@
8
8
  // `X_NOT_IMPLEMENTED` stubs. What IS true is the second half — swapping the driver is
9
9
  // `setJobDriver(other)` and ZERO job-code change — and that is what the interface buys.
10
10
 
11
+ import { finiteCount, finiteOption } from '@ultimat3/core';
11
12
  import type { BackfillLedger } from './backfill-ledger';
12
13
  import { ClaimQueuesEmptyError } from './errors';
13
14
  import type { LeaseStore } from './leases';
@@ -270,3 +271,20 @@ export function resetJobDriver(): void {
270
271
  export const assertClaimQueues = (driver: string, options: ClaimOptions): void => {
271
272
  if (options.queues.length === 0) throw new ClaimQueuesEmptyError(driver);
272
273
  };
274
+
275
+ /**
276
+ * The two NUMBERS of a claim, screened for both drivers in one place for the reason above: a value
277
+ * neither of them refuses is answered two ways. `limit: -1` sliced every ready row but the newest
278
+ * into a lease on the memory driver, where Postgres answers `LIMIT must not be negative`; `2.5`
279
+ * claims 2 rows here and 3 there, with no error on either side.
280
+ *
281
+ * `limit` is a COUNT of rows and takes zero — claiming nothing is what a full worker asks for, and
282
+ * both drivers already answer it identically. `visibilityTimeoutMs` is a DURATION, so it is
283
+ * screened for finiteness alone, the same rule `worker-options.ts` applies to the same knob: it is
284
+ * on this list because `visibleAt = at + NaN` is never `<= now`, which turns at-least-once into
285
+ * never on a row `x jobs ls` still prints as `running`.
286
+ */
287
+ export const assertClaimBounds = (driver: string, options: ClaimOptions): void => {
288
+ finiteCount(`the ${driver} driver claim`, 'limit', options.limit);
289
+ finiteOption(`the ${driver} driver claim`, 'visibilityTimeoutMs', options.visibilityTimeoutMs);
290
+ };
package/src/events-pg.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  // resumes at 12:00:30 must still see an event published at 12:00:10.
8
8
 
9
9
  import type { Clock } from '@ultimat3/core';
10
- import { logger, renderThrowable, systemClock, uuid } from '@ultimat3/core';
10
+ import { finiteOption, logger, renderThrowable, systemClock, uuid } from '@ultimat3/core';
11
11
  import type { DurationInput } from './clock';
12
12
  import { nowMs, toMs } from './clock';
13
13
  import type { PgExecutor } from './driver-pg';
@@ -46,8 +46,15 @@ export interface PgEventBusOptions {
46
46
  */
47
47
  export function createPgEventBus(options: PgEventBusOptions): EventBus {
48
48
  const clock = options.clock ?? systemClock;
49
- const defaultTtl = options.defaultTtl ?? 604_800_000;
50
- const listLimit = options.listLimit ?? 1_000;
49
+ // TWO screens, for the reason `events.ts` states: `defaultTtl` is the constructor's knob and
50
+ // `ttl` is the publish call's, so one screen over `ttl ?? defaultTtl` names the wrong one for
51
+ // whichever value actually arrived.
52
+ const defaultTtlMs = finiteOption(
53
+ 'the pg event bus',
54
+ 'defaultTtl',
55
+ toMs(options.defaultTtl ?? 604_800_000),
56
+ );
57
+ const listLimit = finiteOption('the pg event bus', 'listLimit', options.listLimit ?? 1_000);
51
58
  const exec = options.executor;
52
59
 
53
60
  const purgeExpired = (): number => {
@@ -68,7 +75,11 @@ export function createPgEventBus(options: PgEventBusOptions): EventBus {
68
75
  name,
69
76
  payload,
70
77
  publishedAt: at,
71
- expiresAt: at + toMs(publishOptions.ttl ?? defaultTtl),
78
+ expiresAt:
79
+ at +
80
+ (publishOptions.ttl === undefined
81
+ ? defaultTtlMs
82
+ : finiteOption('the pg event bus', 'ttl', toMs(publishOptions.ttl))),
72
83
  ...(publishOptions.correlationKey === undefined
73
84
  ? {}
74
85
  : { correlationKey: publishOptions.correlationKey }),
package/src/events.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  // 12:00:10, so a fire-and-forget emitter would silently strand every waiting run.
4
4
 
5
5
  import type { Clock } from '@ultimat3/core';
6
- import { logger, systemClock, uuid } from '@ultimat3/core';
6
+ import { finiteOption, logger, systemClock, uuid } from '@ultimat3/core';
7
7
  import type { DurationInput } from './clock';
8
8
  import { nowMs, toMs } from './clock';
9
9
  import type { EventLookup } from './steps';
@@ -39,8 +39,17 @@ export interface MemoryEventBusOptions {
39
39
 
40
40
  export function createMemoryEventBus(options: MemoryEventBusOptions = {}): EventBus {
41
41
  const clock = options.clock ?? systemClock;
42
- const defaultTtl = options.defaultTtl ?? 604_800_000;
43
- const maxEvents = options.maxEvents ?? 10_000;
42
+ // TWO screens, because these are two knobs: the default is declared at construction and belongs
43
+ // to whoever built the bus, `ttl` rides the publish CALL. One screen over `ttl ?? defaultTtl`
44
+ // told a caller who wrote `{ ttl: NaN }` to "pass a finite defaultTtl" — an instruction naming an
45
+ // option they never set, on a constructor usually in another file. `steps.ts` names `timeout` for
46
+ // the same value shape.
47
+ const defaultTtlMs = finiteOption(
48
+ 'the memory event bus',
49
+ 'defaultTtl',
50
+ toMs(options.defaultTtl ?? 604_800_000),
51
+ );
52
+ const maxEvents = finiteOption('the memory event bus', 'maxEvents', options.maxEvents ?? 10_000);
44
53
  const events = new Map<string, JobEvent>();
45
54
 
46
55
  const purgeExpired = (): number => {
@@ -64,7 +73,11 @@ export function createMemoryEventBus(options: MemoryEventBusOptions = {}): Event
64
73
  name,
65
74
  payload,
66
75
  publishedAt: at,
67
- expiresAt: at + toMs(publishOptions.ttl ?? defaultTtl),
76
+ expiresAt:
77
+ at +
78
+ (publishOptions.ttl === undefined
79
+ ? defaultTtlMs
80
+ : finiteOption('the memory event bus', 'ttl', toMs(publishOptions.ttl))),
68
81
  ...(publishOptions.correlationKey === undefined
69
82
  ? {}
70
83
  : { correlationKey: publishOptions.correlationKey }),
package/src/index.ts CHANGED
@@ -101,6 +101,7 @@ export type {
101
101
  QueueStats,
102
102
  } from './driver';
103
103
  export {
104
+ assertClaimBounds,
104
105
  assertClaimQueues,
105
106
  DEFAULT_QUEUE,
106
107
  DEFAULT_VISIBILITY_TIMEOUT_MS,
@@ -138,7 +139,6 @@ export {
138
139
  SQL_OUTBOX_MARK_PUBLISHED,
139
140
  SQL_OUTBOX_RELEASE,
140
141
  SQL_OUTBOX_STAGE,
141
- SQL_OUTBOX_TABLE,
142
142
  SQL_SCHEDULER_STATE_GET,
143
143
  SQL_SCHEDULER_STATE_MARK,
144
144
  SQL_STATS,
package/src/limits.ts CHANGED
@@ -12,7 +12,7 @@
12
12
  // what `job.concurrency` is enforced with.
13
13
 
14
14
  import type { Clock } from '@ultimat3/core';
15
- import { assert, systemClock } from '@ultimat3/core';
15
+ import { assert, finiteCount, systemClock } from '@ultimat3/core';
16
16
  import { nowMs } from './clock';
17
17
 
18
18
  export interface RateLimit {
@@ -126,6 +126,27 @@ export function createLimiter(
126
126
  );
127
127
  const maxTenants = Math.max(1, Math.floor(requested));
128
128
  const evictTo = Math.max(1, Math.floor(maxTenants * 0.9));
129
+ // The ceilings this limiter ENFORCES, screened where they are declared — the refusal `maxTenants`
130
+ // above already makes, for a sharper reason: every one of them is read as
131
+ // `config.x !== undefined && count >= config.x`, so a `NaN` leaves the option PRESENT and the
132
+ // comparison false forever. Measured: `global: Number(process.env.WORKER_GLOBAL_CONCURRENCY)`
133
+ // with the variable unset granted 1000 of 1000 acquires while `snapshot().config` still reported
134
+ // a configured ceiling. `ratePerTenant.windowMs` is on the list because it is half the same
135
+ // ceiling: `stamp > at - NaN` is false for every stamp, so the window reads empty on every call.
136
+ //
137
+ // `min` is 0 on all five, deliberately: zero is a HARD STOP here and one this repo's own suite
138
+ // configures (`limits-bound.test.ts`'s `{ perTenant: 0 }`), never "unlimited" — omitting the
139
+ // option is what means that. A count is whole because these are SLOTS: `global: 2.5` granted 3,
140
+ // which is a ceiling nobody wrote.
141
+ for (const [option, value] of [
142
+ ['perTenant', config.perTenant],
143
+ ['perQueue', config.perQueue],
144
+ ['global', config.global],
145
+ ['ratePerTenant.limit', config.ratePerTenant?.limit],
146
+ ['ratePerTenant.windowMs', config.ratePerTenant?.windowMs],
147
+ ] as const) {
148
+ if (value !== undefined) finiteCount('createLimiter', option, value);
149
+ }
129
150
  const byQueue = new Map<string, number>();
130
151
  const byTenant = new Map<string, number>();
131
152
  // `{queue, tenantId}` is ONE key — `blockedBy` has always read it that way. Without this counter
package/src/outbox.ts CHANGED
@@ -24,7 +24,14 @@
24
24
  // test and `x dev` must enqueue with nothing wired — but it is a fallback, not the guarantee.
25
25
 
26
26
  import type { Clock } from '@ultimat3/core';
27
- import { currentSpanContext, logger, renderThrowable, traceparent, uuid } from '@ultimat3/core';
27
+ import {
28
+ assert,
29
+ currentSpanContext,
30
+ logger,
31
+ renderThrowable,
32
+ traceparent,
33
+ uuid,
34
+ } from '@ultimat3/core';
28
35
  import type { Tx } from '@ultimat3/entity';
29
36
  import { nowMs } from './clock';
30
37
  import type { EnqueueResult, JobDriver } from './driver';
@@ -383,6 +390,18 @@ export interface OutboxRelay {
383
390
  export function createOutboxRelay(options: RelayOptions): OutboxRelay {
384
391
  const batchSize = options.batchSize ?? 100;
385
392
  const intervalMs = options.intervalMs ?? 200;
393
+ // Refused, never clamped — `worker-options.ts` carries the reason. `setInterval(fn, NaN)` reads
394
+ // the delay as 0 and `claim(NaN)` slices `(0, NaN)`: a relay that spins and publishes nothing.
395
+ assert(
396
+ Number.isSafeInteger(batchSize) && batchSize >= 1,
397
+ `createOutboxRelay batchSize is ${String(batchSize)} — a batch is a whole number of staged rows, at least one`,
398
+ 'pass a finite batchSize to createOutboxRelay(...), or omit it for the default 100',
399
+ );
400
+ assert(
401
+ Number.isFinite(intervalMs) && intervalMs >= 0,
402
+ `createOutboxRelay intervalMs is ${String(intervalMs)}, which setInterval reads as 0 — the relay would spin, not poll`,
403
+ 'pass a finite intervalMs to createOutboxRelay(...), or omit it for the default 200',
404
+ );
386
405
  let timer: ReturnType<typeof setInterval> | undefined;
387
406
  let running = false;
388
407
  /** The pass in flight, so `stop()` joins it instead of returning underneath it. */
@@ -4,7 +4,7 @@
4
4
  // pod it replaced dropped, and two pods in a rolling update both dispatch every task.
5
5
 
6
6
  import type { Clock } from '@ultimat3/core';
7
- import { uuid } from '@ultimat3/core';
7
+ import { finiteOption, uuid } from '@ultimat3/core';
8
8
  import { nowMs } from './clock';
9
9
  import type { PgExecutor } from './driver-pg';
10
10
  import {
@@ -70,7 +70,11 @@ export const DEFAULT_LEADER_TTL_MS = 30_000;
70
70
  export function createPgLeaseLeader(options: PgLeaseLeaderOptions): LeaderElection {
71
71
  const lockKey = options.lockKey ?? 'scheduler';
72
72
  const holder = options.holder ?? `scheduler-${uuid()}`;
73
- const ttlMs = options.ttlMs ?? DEFAULT_LEADER_TTL_MS;
73
+ const ttlMs = finiteOption(
74
+ 'the pg scheduler lease',
75
+ 'ttlMs',
76
+ options.ttlMs ?? DEFAULT_LEADER_TTL_MS,
77
+ );
74
78
  return {
75
79
  async acquire() {
76
80
  const rows = await options.executor.query<{ holder: string }>(SQL_LEADER_ACQUIRE, [
package/src/scheduler.ts CHANGED
@@ -21,7 +21,7 @@
21
21
  // over the same `lastFiredAt`.
22
22
 
23
23
  import type { Clock } from '@ultimat3/core';
24
- import { isUltimateError, logger, onShutdown, renderThrowable } from '@ultimat3/core';
24
+ import { finiteOption, isUltimateError, logger, onShutdown, renderThrowable } from '@ultimat3/core';
25
25
  import { instant, nextCronOccurrence } from '@ultimat3/time';
26
26
  import { nowMs } from './clock';
27
27
  import { settleAllBy } from './drain-wait';
@@ -115,7 +115,11 @@ export interface Scheduler {
115
115
  export function createScheduler(options: SchedulerOptions): Scheduler {
116
116
  const schedulerState = options.state ?? createMemorySchedulerState();
117
117
  const resolveCron = options.cron ?? defaultCronResolver;
118
- const tickIntervalMs = options.tickIntervalMs ?? 1_000;
118
+ const tickIntervalMs = finiteOption(
119
+ 'createScheduler',
120
+ 'tickIntervalMs',
121
+ options.tickIntervalMs ?? 1_000,
122
+ );
119
123
  const leader = options.leader ?? soleLeader();
120
124
  let timer: ReturnType<typeof setTimeout> | undefined;
121
125
  let isLeader = false;
package/src/steps.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  // catches it and re-queues the job for `resumeAt` instead of holding a process for three days.
8
8
 
9
9
  import type { Clock } from '@ultimat3/core';
10
- import { logger, renderThrowable } from '@ultimat3/core';
10
+ import { finiteOption, logger, renderThrowable } from '@ultimat3/core';
11
11
  import type { DurationInput } from './clock';
12
12
  import { nowMs, toMs } from './clock';
13
13
  import { JobAbortedError, JobTimeoutError, StepDuplicateError } from './errors';
@@ -199,7 +199,7 @@ export function createStepRunner(options: StepRunnerOptions): StepRunner {
199
199
  const used: string[] = [];
200
200
  const replayed: string[] = [];
201
201
  const clock = options.clock;
202
- const pollMs = options.eventPollMs ?? 30_000;
202
+ const pollMs = finiteOption('step.waitForEvent', 'eventPollMs', options.eventPollMs ?? 30_000);
203
203
  const runSignal = options.signal ?? NEVER_ABORTED;
204
204
 
205
205
  /**
@@ -387,7 +387,9 @@ export function createStepRunner(options: StepRunnerOptions): StepRunner {
387
387
 
388
388
  const at = now();
389
389
  const startedAt = existing?.startedAt ?? at;
390
- const deadline = startedAt + toMs(waitOptions.timeout ?? 86_400_000);
390
+ const deadline =
391
+ startedAt +
392
+ finiteOption('step.waitForEvent', 'timeout', toMs(waitOptions.timeout ?? 86_400_000));
391
393
  const correlationKey = waitOptions.match;
392
394
 
393
395
  const hit = await options.events?.find(event, correlationKey, startedAt);
@@ -411,7 +413,11 @@ export function createStepRunner(options: StepRunnerOptions): StepRunner {
411
413
  throw new JobTimeoutError({
412
414
  job: jobName,
413
415
  step: name,
414
- timeoutMs: toMs(waitOptions.timeout ?? 86_400_000),
416
+ timeoutMs: finiteOption(
417
+ 'step.waitForEvent',
418
+ 'timeout',
419
+ toMs(waitOptions.timeout ?? 86_400_000),
420
+ ),
415
421
  });
416
422
  }
417
423
  logger.warn('jobs.step.wait-timeout', { job: jobName, step: name, event });
package/src/webhook.ts CHANGED
@@ -17,6 +17,7 @@
17
17
 
18
18
  import type { Clock, Ctx } from '@ultimat3/core';
19
19
  import {
20
+ finiteOption,
20
21
  isCanonicalWebhookField,
21
22
  renderThrowable,
22
23
  systemClock,
@@ -169,7 +170,11 @@ const isRetryableStatus = (status: number): boolean =>
169
170
 
170
171
  export function webhook(definition: WebhookDefinition): JobHandle<WebhookDeliveryInput> {
171
172
  const clock = definition.clock ?? systemClock;
172
- const disableAfter = definition.disableAfter ?? DEFAULT_WEBHOOK_DISABLE_AFTER;
173
+ const disableAfter = finiteOption(
174
+ 'webhook()',
175
+ 'disableAfter',
176
+ definition.disableAfter ?? DEFAULT_WEBHOOK_DISABLE_AFTER,
177
+ );
173
178
  const send = definition.fetch ?? ((url, init) => fetch(url, init));
174
179
 
175
180
  return job<WebhookDeliveryInput>({
@@ -0,0 +1,73 @@
1
+ // Every numeric knob `createWorker` accepts, read and REFUSED in one place — the slot table
2
+ // included, because a queue name is data and a slot count is a bound, and both arrive from the
3
+ // same deployment config.
4
+ //
5
+ // WHY A REFUSAL AND NOT A CLAMP. `Number(process.env.JOB_VISIBILITY_MS)` on an unset variable is
6
+ // `NaN`; `??` guards only nullish, and `Math.max`/`Math.min`/`Math.floor` PROPAGATE it. So the
7
+ // value arrives at a lease deadline, a claim limit and a timer interval intact, and every
8
+ // comparison against it reads FALSE — measured on `createMemoryDriver`: `visibleAt = at + NaN`,
9
+ // the reclaim scan asks `visibleAt <= at`, and a job whose worker died is never claimable again.
10
+ // At-least-once becomes never, with no error and a row `x jobs ls` still prints as `running`.
11
+ // `slice(0, NaN)` is `[]`, so a `concurrency: NaN` worker claims nothing and reports healthy.
12
+ // Same shape as `createLimiter`'s `maxTenants` and `backfill()`'s `batch`, refused the same way.
13
+
14
+ import { finiteOption } from '@ultimat3/core';
15
+ import { DEFAULT_VISIBILITY_TIMEOUT_MS } from './driver';
16
+
17
+ /** Slots a queue gets when `concurrency` names no number for it. */
18
+ const DEFAULT_SLOTS = 5;
19
+
20
+ /** The subset of `WorkerOptions` this module reads. Structural, so `WorkerOptions` satisfies it. */
21
+ export interface WorkerNumericOptions {
22
+ readonly concurrency?: number | Readonly<Record<string, number>> | undefined;
23
+ readonly visibilityTimeoutMs?: number | undefined;
24
+ readonly pollIntervalMs?: number | undefined;
25
+ readonly heartbeatIntervalMs?: number | undefined;
26
+ }
27
+
28
+ export interface WorkerTimings {
29
+ readonly visibilityTimeoutMs: number;
30
+ readonly pollIntervalMs: number;
31
+ readonly heartbeatIntervalMs: number;
32
+ /** Slots for one queue, by OWN key — see `slotsFor` below. */
33
+ readonly slotsFor: (queue: string) => number;
34
+ }
35
+
36
+ /**
37
+ * Slots for one queue. A queue NAME is deployment data, so the table is read by OWN keys:
38
+ * `concurrency['constructor']` answers `Object.prototype.constructor`, and
39
+ * `Math.max(0, <function> - inFlight)` is `NaN`, which the `free === 0` guard does not catch.
40
+ * `bun run proto-index` cannot see this one — the table is a parameter, not a literal in a file.
41
+ */
42
+ const slotTable =
43
+ (declared: number | Readonly<Record<string, number>> | undefined) =>
44
+ (queue: string): number => {
45
+ if (typeof declared === 'number') return declared;
46
+ if (declared === undefined || !Object.hasOwn(declared, queue)) return DEFAULT_SLOTS;
47
+ return declared[queue] ?? DEFAULT_SLOTS;
48
+ };
49
+
50
+ export function resolveWorkerTimings(options: WorkerNumericOptions): WorkerTimings {
51
+ const visibilityTimeoutMs = options.visibilityTimeoutMs ?? DEFAULT_VISIBILITY_TIMEOUT_MS;
52
+ finiteOption('createWorker', 'visibilityTimeoutMs', visibilityTimeoutMs);
53
+ const pollIntervalMs = options.pollIntervalMs ?? 250;
54
+ // `setTimeout(fn, NaN)` coerces the delay to 0, so the claim loop stops being a poll and becomes
55
+ // a spin: one round trip to Postgres per event-loop turn, from every worker replica.
56
+ finiteOption('createWorker', 'pollIntervalMs', pollIntervalMs);
57
+ const heartbeatIntervalMs = options.heartbeatIntervalMs ?? Math.floor(visibilityTimeoutMs / 3);
58
+ finiteOption('createWorker', 'heartbeatIntervalMs', heartbeatIntervalMs);
59
+ const declared = options.concurrency;
60
+ if (typeof declared === 'number') finiteOption('createWorker', 'concurrency', declared);
61
+ else if (declared !== undefined) {
62
+ // Per queue, by own key: an inherited member is not this table's to answer with either.
63
+ for (const queue of Object.keys(declared)) {
64
+ finiteOption('createWorker', `concurrency.${queue}`, declared[queue] ?? DEFAULT_SLOTS);
65
+ }
66
+ }
67
+ return {
68
+ visibilityTimeoutMs,
69
+ pollIntervalMs,
70
+ heartbeatIntervalMs,
71
+ slotsFor: slotTable(declared),
72
+ };
73
+ }
package/src/worker.ts CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  import { nowMs } from './clock';
17
17
  import { settleAllBy } from './drain-wait';
18
18
  import type { ClaimedJob, JobDriver, QueueStats } from './driver';
19
- import { DEFAULT_QUEUE, DEFAULT_VISIBILITY_TIMEOUT_MS } from './driver';
19
+ import { DEFAULT_QUEUE } from './driver';
20
20
  import { ConcurrencyUnenforceableError } from './errors';
21
21
  import type { JobExecution, JobOutcome } from './execute';
22
22
  import { getJob, registeredJobs } from './job';
@@ -25,6 +25,7 @@ import { createLimiter } from './limits';
25
25
  import { recordQueueDeadJobs, recordQueueOldestReady } from './metrics';
26
26
  import type { EventLookup } from './steps';
27
27
  import { createFleetSlots } from './worker-fleet-slots';
28
+ import { resolveWorkerTimings } from './worker-options';
28
29
  import { runClaimedJob } from './worker-run';
29
30
 
30
31
  /**
@@ -89,13 +90,10 @@ export interface Worker {
89
90
  export function createWorker(options: WorkerOptions): Worker {
90
91
  const workerId = options.workerId ?? `worker-${uuid()}`;
91
92
  const queues = options.queues ?? [DEFAULT_QUEUE];
92
- const visibilityTimeoutMs = options.visibilityTimeoutMs ?? DEFAULT_VISIBILITY_TIMEOUT_MS;
93
- const pollIntervalMs = options.pollIntervalMs ?? 250;
94
- const heartbeatIntervalMs = options.heartbeatIntervalMs ?? Math.floor(visibilityTimeoutMs / 3);
95
- const slotsFor = (queue: string): number =>
96
- typeof options.concurrency === 'number'
97
- ? options.concurrency
98
- : (options.concurrency?.[queue] ?? 5);
93
+ // Every numeric knob, read and refused in one place — `worker-options.ts` says why a non-finite
94
+ // one is a refusal rather than a clamp, and carries the slot table's own-key read with it.
95
+ const { visibilityTimeoutMs, pollIntervalMs, heartbeatIntervalMs, slotsFor } =
96
+ resolveWorkerTimings(options);
99
97
  const limiter = options.limiter ?? createLimiter({});
100
98
  const driverLeases = options.driver.leases;
101
99
  // `job.concurrency`, held as a row every replica sees. The TTL is the visibility timeout and the