@ultimat3/jobs 25.2.0 → 26.1.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/README.md +6 -6
- package/package.json +6 -6
- package/src/backfill-inspect.ts +1 -1
- package/src/driver-pg-jobs-sql.ts +1 -1
- package/src/driver.ts +1 -1
- package/src/errors-concurrency.ts +2 -2
- package/src/errors-operator.ts +3 -3
- package/src/errors-requeue.ts +1 -1
- package/src/errors.ts +2 -2
- package/src/inspect-operator.ts +1 -1
- package/src/inspect.ts +1 -1
- package/src/metrics.ts +1 -1
- package/src/scheduler-pg.ts +1 -1
- package/src/webhook-errors.ts +1 -1
- package/src/worker-options.ts +1 -1
- package/src/worker-registry.ts +1 -1
package/README.md
CHANGED
|
@@ -293,7 +293,7 @@ report a different number:
|
|
|
293
293
|
| Surface | Shows |
|
|
294
294
|
|---|---|
|
|
295
295
|
| `x db backfill --list` | the whole ledger, `--name` / `--status` / `--limit`, and `--json` |
|
|
296
|
-
| `x jobs
|
|
296
|
+
| `x jobs list` | the passes **in flight** — rows so far and cursor, beside the queue depth |
|
|
297
297
|
| `x jobs show <id>` | the ledger row for that run, under `backfill` (`null` for any other job) |
|
|
298
298
|
| `/_x` → jobs | the whole ledger plus a live count, alongside the queues and the step traces |
|
|
299
299
|
|
|
@@ -568,7 +568,7 @@ re-exports (`X_HELPER_COPY`). Same argument `timing-safe-equal.ts` makes for its
|
|
|
568
568
|
**Upgrading a webhook to `tenant: ({ orgId }) => orgId`.** Deliveries already queued without an
|
|
569
569
|
`orgId` dead-letter on their next attempt with `X_JOB_TENANT_MISMATCH`, and a retry cannot add the
|
|
570
570
|
field. Drain the queue before deploying the new declaration, or re-enqueue what dead-letters with its
|
|
571
|
-
org: `x jobs
|
|
571
|
+
org: `x jobs list --state dead --name partner.webhooks --json`, then `deliver.enqueue({ endpointId, eventId, orgId })` for each.
|
|
572
572
|
|
|
573
573
|
**Set `timeout` below the drain budget.** A webhook has no timeout unless you declare one, and a
|
|
574
574
|
POST on the wire when SIGTERM lands runs until the receiver answers. With a `timeout`, a drained
|
|
@@ -1195,7 +1195,7 @@ export const syncAccountWatched = job({
|
|
|
1195
1195
|
|---|---|
|
|
1196
1196
|
| a retry, a `step.sleep` / `waitForEvent`, a drain | nobody: the run has not ended |
|
|
1197
1197
|
| `cancelJob()` / `x jobs cancel` on a queued or running row | the caller of the cancel — the worker's own settle then matches nothing |
|
|
1198
|
-
| `x jobs retry`, `
|
|
1198
|
+
| `x jobs retry`, `delete`, `promote`, `drain` | the operator; a retried run settles again, and is told again |
|
|
1199
1199
|
| a settle that did not land — the lease lapsed and the queue re-delivered | the claim that holds the row now |
|
|
1200
1200
|
|
|
1201
1201
|
| Guarantee | Stated plainly |
|
|
@@ -1254,13 +1254,13 @@ FOR a user takes that user's id in its input and re-authorises it in the body.
|
|
|
1254
1254
|
| `X_JOB_LEASE_LOST` | the job was cancelled, or its lease lapsed and the queue re-delivered it, while this worker was still running it |
|
|
1255
1255
|
| `X_JOB_SLOT_LOST` | the fleet `concurrency` slot this run held was taken by another worker — a different row on a different clock from the lease above |
|
|
1256
1256
|
| `X_JOB_NOT_CANCELLABLE` | `cancelJob` reached a job that is not live — done, failed, dead or already cancelled — an id no queue holds, or a driver with no `cancel` |
|
|
1257
|
-
| `X_JOB_NOT_FOUND` | `requeue` was asked for an id the queue does not hold — one answer on both drivers. Fix: `x jobs
|
|
1257
|
+
| `X_JOB_NOT_FOUND` | `requeue` was asked for an id the queue does not hold — one answer on both drivers. Fix: `x jobs list --json` |
|
|
1258
1258
|
| `X_JOB_CONCURRENCY_UNENFORCEABLE` | a registered job declares `concurrency`, plain or keyed, and the driver has no lease store |
|
|
1259
|
-
| `X_JOB_KEY_BUSY` | a run claimed under `whenBusy: 'fail'` while its key already held `limit` runs. Classified `terminal`. Fix: `x jobs
|
|
1259
|
+
| `X_JOB_KEY_BUSY` | a run claimed under `whenBusy: 'fail'` while its key already held `limit` runs. Classified `terminal`. Fix: `x jobs list --name <job> --state running --json` |
|
|
1260
1260
|
| `X_JOB_DECLARATION_INVALID` | `job()` is missing a required field, declares a `concurrency` no worker can honour (plain or keyed), or its `concurrency.key` answered an empty, non-string or over-long key at enqueue |
|
|
1261
1261
|
| `X_JOB_NOT_REMOVABLE` | `remove` / `removeMany` reached a running job. Fix: `x jobs cancel <id> --json` |
|
|
1262
1262
|
| `X_JOB_NOT_PROMOTABLE` | `promote` reached a job not waiting on its run time |
|
|
1263
|
-
| `X_JOB_PAGE_INVALID` | `list()` was asked for more than `MAX_JOB_PAGE` rows, or handed a cursor no page produced. The cause names which cursor (`after` / `before`). Fix: `x jobs
|
|
1263
|
+
| `X_JOB_PAGE_INVALID` | `list()` was asked for more than `MAX_JOB_PAGE` rows, or handed a cursor no page produced. The cause names which cursor (`after` / `before`). Fix: `x jobs list --limit 200 --json` |
|
|
1264
1264
|
| `X_JOB_ON_SETTLED_FAILED` | a declared `onSettled` threw on every one of its tries; logged and reported, never thrown |
|
|
1265
1265
|
| `X_NOT_IMPLEMENTED` | introspection or the operator surface on a driver with no `introspect` (a hand-rolled one) |
|
|
1266
1266
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/jobs",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "26.1.0",
|
|
4
4
|
"description": "Durable background work: steps, transactional outbox, cron tasks, one driver interface",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -32,10 +32,10 @@
|
|
|
32
32
|
"test": "bun test"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@ultimat3/core": "
|
|
36
|
-
"@ultimat3/db": "
|
|
37
|
-
"@ultimat3/entity": "
|
|
38
|
-
"@ultimat3/schema": "
|
|
39
|
-
"@ultimat3/time": "
|
|
35
|
+
"@ultimat3/core": "26.1.0",
|
|
36
|
+
"@ultimat3/db": "26.1.0",
|
|
37
|
+
"@ultimat3/entity": "26.1.0",
|
|
38
|
+
"@ultimat3/schema": "26.1.0",
|
|
39
|
+
"@ultimat3/time": "26.1.0"
|
|
40
40
|
}
|
|
41
41
|
}
|
package/src/backfill-inspect.ts
CHANGED
|
@@ -44,7 +44,7 @@ export function toBackfillProgress(run: BackfillRun): BackfillProgress {
|
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
46
|
* Every pass the ledger holds, newest first. An EMPTY list on a driver that ships no ledger, and
|
|
47
|
-
* deliberately not a throw: `x jobs
|
|
47
|
+
* deliberately not a throw: `x jobs list` and the jobs panel report the queue, and a queue that
|
|
48
48
|
* answers everything except "no backfills recorded" is a broken command for a fact nobody asked
|
|
49
49
|
* about. `x db backfill --list` says so in its own summary instead, where it IS the question.
|
|
50
50
|
*/
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Every statement returning a WHOLE `x_jobs` row, and the one column list they share. They ask
|
|
2
2
|
// Postgres for epoch ms because `select *` left the decoding to the client's type map: one with
|
|
3
3
|
// none decodes `timestamptz` as TEXT, so `toJobRecord` read `Number('2026-01-01 00:00:00+00')`
|
|
4
|
-
// and `x jobs
|
|
4
|
+
// and `x jobs list` / `show` / `cancel` printed `NaN` for every timestamp.
|
|
5
5
|
|
|
6
6
|
import { JOBS_WAKE_CHANNEL } from './driver-pg-wake-sql';
|
|
7
7
|
|
package/src/driver.ts
CHANGED
|
@@ -389,7 +389,7 @@ export const assertClaimQueues = (driver: string, options: ClaimOptions): void =
|
|
|
389
389
|
* both drivers already answer it identically. `visibilityTimeoutMs` is a DURATION, so it is
|
|
390
390
|
* screened for finiteness alone, the same rule `worker-options.ts` applies to the same knob: it is
|
|
391
391
|
* on this list because `visibleAt = at + NaN` is never `<= now`, which turns at-least-once into
|
|
392
|
-
* never on a row `x jobs
|
|
392
|
+
* never on a row `x jobs list` still prints as `running`.
|
|
393
393
|
*/
|
|
394
394
|
export const assertClaimBounds = (driver: string, options: ClaimOptions): void => {
|
|
395
395
|
finiteCount(`the ${driver} driver claim`, 'limit', options.limit);
|
|
@@ -67,8 +67,8 @@ export class JobKeyBusyError extends UltimateError {
|
|
|
67
67
|
// still goes through `renderFixShellArg` — the screen `bun run error-render` looks for —
|
|
68
68
|
// whose placeholder the guard above makes unreachable.
|
|
69
69
|
fix: isFixShellSafe(input.job)
|
|
70
|
-
? `x jobs
|
|
71
|
-
: 'x jobs
|
|
70
|
+
? `x jobs list --name ${renderFixShellArg(input.job, 'job')} --state running --json`
|
|
71
|
+
: 'x jobs list --state running --json',
|
|
72
72
|
meta: { job: input.job, key: input.key, limit: input.limit },
|
|
73
73
|
});
|
|
74
74
|
}
|
package/src/errors-operator.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { isFixShellSafe, renderFixShellArg, renderThrowable, UltimateError } fro
|
|
|
9
9
|
const jobCommand = (verb: string, jobId: string): string =>
|
|
10
10
|
isFixShellSafe(jobId)
|
|
11
11
|
? `x jobs ${verb} ${renderFixShellArg(jobId, 'id')} --json`
|
|
12
|
-
: 'x jobs
|
|
12
|
+
: 'x jobs list --state running --json';
|
|
13
13
|
|
|
14
14
|
/** `remove()` / `removeMany()` reached a job a worker holds. Cancel stops the body; then remove. */
|
|
15
15
|
export class JobNotRemovableError extends UltimateError {
|
|
@@ -22,7 +22,7 @@ export class JobNotRemovableError extends UltimateError {
|
|
|
22
22
|
: `job ${input.jobId} is running, and deleting the row under its body would not stop the body`,
|
|
23
23
|
fix:
|
|
24
24
|
input.jobId === undefined
|
|
25
|
-
? 'x jobs
|
|
25
|
+
? 'x jobs list --state running --json'
|
|
26
26
|
: jobCommand('cancel', input.jobId),
|
|
27
27
|
meta: { ...(input.jobId === undefined ? {} : { jobId: input.jobId }) },
|
|
28
28
|
});
|
|
@@ -92,7 +92,7 @@ export class JobPageInvalidError extends UltimateError {
|
|
|
92
92
|
input.limit === undefined
|
|
93
93
|
? `${input.subject} was handed ${cursor === 'after' ? 'an after' : 'a before'} cursor that no page produced — ${cursor}: ${source} of a page this driver answered is the only one it reads; omit it for the first page`
|
|
94
94
|
: `${input.subject} limit is ${input.limit}, over MAX_JOB_PAGE (${input.maxPage}) — the rows one page may answer; walk further with after: jobCursor(lastRow), and stop at a page shorter than its limit`,
|
|
95
|
-
fix: `x jobs
|
|
95
|
+
fix: `x jobs list --limit ${renderFixShellArg(String(input.maxPage), '200')} --json`,
|
|
96
96
|
meta: {
|
|
97
97
|
maxPage: input.maxPage,
|
|
98
98
|
...(input.limit === undefined ? {} : { limit: input.limit }),
|
package/src/errors-requeue.ts
CHANGED
|
@@ -31,7 +31,7 @@ export class JobNotFoundError extends UltimateError {
|
|
|
31
31
|
super({
|
|
32
32
|
code: 'X_JOB_NOT_FOUND',
|
|
33
33
|
cause: `the "${input.driver}" queue holds no job with id ${renderCauseValue(input.jobId)} — a mistyped id, a job removed since, or an id from another environment`,
|
|
34
|
-
fix: 'x jobs
|
|
34
|
+
fix: 'x jobs list --json',
|
|
35
35
|
meta: { jobId: input.jobId },
|
|
36
36
|
});
|
|
37
37
|
}
|
package/src/errors.ts
CHANGED
|
@@ -196,7 +196,7 @@ export class JobNameTakenError extends UltimateError {
|
|
|
196
196
|
super({
|
|
197
197
|
code: 'X_JOB_DUPLICATE',
|
|
198
198
|
cause: `two ${input.kind}s claim the name "${input.name}"`,
|
|
199
|
-
fix: `x jobs
|
|
199
|
+
fix: `x jobs list --json names the one already seated; rename the other's export, or its "name:" if it declares one — a ${input.kind} name is a durable queue key and is globally unique`,
|
|
200
200
|
});
|
|
201
201
|
}
|
|
202
202
|
}
|
|
@@ -434,7 +434,7 @@ export class JobNotCancellableError extends UltimateError {
|
|
|
434
434
|
input.state === 'missing'
|
|
435
435
|
? `no job ${input.jobId} exists in this queue`
|
|
436
436
|
: `job ${input.jobId} is "${input.state}" and only a job that has not finished can be cancelled`,
|
|
437
|
-
fix: `x jobs
|
|
437
|
+
fix: `x jobs list --state running --json`,
|
|
438
438
|
});
|
|
439
439
|
}
|
|
440
440
|
}
|
package/src/inspect-operator.ts
CHANGED
|
@@ -12,7 +12,7 @@ function operatorOf(driver: JobDriver): JobIntrospection {
|
|
|
12
12
|
if (driver.introspect === undefined) {
|
|
13
13
|
throw new NotImplementedError({
|
|
14
14
|
cause: `the operator surface of the "${driver.name}" jobs driver is not implemented: the driver has no introspect`,
|
|
15
|
-
fix: 'call setJobDriver(postgresJobDriver()) at boot — the pg driver implements introspect — then: x jobs
|
|
15
|
+
fix: 'call setJobDriver(postgresJobDriver()) at boot — the pg driver implements introspect — then: x jobs list --json',
|
|
16
16
|
});
|
|
17
17
|
}
|
|
18
18
|
return driver.introspect;
|
package/src/inspect.ts
CHANGED
|
@@ -114,7 +114,7 @@ function requireIntrospection(driver: JobDriver): NonNullable<JobDriver['introsp
|
|
|
114
114
|
if (driver.introspect === undefined) {
|
|
115
115
|
throw new NotImplementedError({
|
|
116
116
|
cause: `introspection for the "${driver.name}" jobs driver is not implemented: the driver has no introspect`,
|
|
117
|
-
fix: 'call setJobDriver(postgresJobDriver()) at boot — only the pg driver implements introspect — then: x jobs
|
|
117
|
+
fix: 'call setJobDriver(postgresJobDriver()) at boot — only the pg driver implements introspect — then: x jobs list --json',
|
|
118
118
|
});
|
|
119
119
|
}
|
|
120
120
|
return driver.introspect;
|
package/src/metrics.ts
CHANGED
|
@@ -22,7 +22,7 @@ import type { JobOutcome } from './execute';
|
|
|
22
22
|
* `step.sleep` read as a finished job and make the failure ratio meaningless. `interrupted` for
|
|
23
23
|
* the same reason: a deploy cutting a job short is the process's doing, and a failure ratio that
|
|
24
24
|
* spikes on every rollout is a page nobody answers. `refused` is `failed` because the ROW is: the
|
|
25
|
-
* counter says what `x jobs
|
|
25
|
+
* counter says what `x jobs list --state failed` says, and never `dead` — a key refusal is not in
|
|
26
26
|
* the dead-letter queue, so it must not move the gauge beside this one. Read by the worker's
|
|
27
27
|
* `recordJob` sites.
|
|
28
28
|
*/
|
package/src/scheduler-pg.ts
CHANGED
|
@@ -146,7 +146,7 @@ export function postgresLeaseLeader(options: PostgresLeaseLeaderOptions): Leader
|
|
|
146
146
|
};
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
/** Exposed so a test — and `x jobs
|
|
149
|
+
/** Exposed so a test — and `x jobs list` — can say which node currently holds the lease. */
|
|
150
150
|
export async function currentLeader(
|
|
151
151
|
executor: PgExecutor,
|
|
152
152
|
lockKey = 'scheduler',
|
package/src/webhook-errors.ts
CHANGED
|
@@ -122,7 +122,7 @@ export class WebhookDeliveryFailedError extends UltimateError {
|
|
|
122
122
|
super({
|
|
123
123
|
code: 'X_WEBHOOK_DELIVERY_FAILED',
|
|
124
124
|
cause: `webhook "${input.webhook}" delivery to ${webhookTarget(input.url)} ${answer}: ${input.detail}`,
|
|
125
|
-
fix: 'no edit here — the job retries on its declared backoff; watch the endpoint with x jobs
|
|
125
|
+
fix: 'no edit here — the job retries on its declared backoff; watch the endpoint with x jobs list --json and fix the receiver before it reaches disableAfter',
|
|
126
126
|
meta: { endpointId: input.endpointId, status: input.status },
|
|
127
127
|
});
|
|
128
128
|
}
|
package/src/worker-options.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// value arrives at a lease deadline, a claim limit and a timer interval intact, and every
|
|
8
8
|
// comparison against it reads FALSE — measured on `memoryJobDriver`: `visibleAt = at + NaN`,
|
|
9
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
|
|
10
|
+
// At-least-once becomes never, with no error and a row `x jobs list` still prints as `running`.
|
|
11
11
|
// `slice(0, NaN)` is `[]`, so a `concurrency: NaN` worker claims nothing and reports healthy.
|
|
12
12
|
// Same shape as `concurrencyLimiter`'s `maxTenants` and `backfill()`'s `batch`, refused the same way.
|
|
13
13
|
|
package/src/worker-registry.ts
CHANGED
|
@@ -113,6 +113,6 @@ export function reportUnregisteredQueues(workerId: string, queues: readonly stri
|
|
|
113
113
|
logger.info('jobs.worker.queue-unregistered', {
|
|
114
114
|
workerId,
|
|
115
115
|
queues: unnamed,
|
|
116
|
-
fix: 'remove the queue from jobs.queues in app.config.ts, or set queue on the job that should run there — x jobs
|
|
116
|
+
fix: 'remove the queue from jobs.queues in app.config.ts, or set queue on the job that should run there — x jobs list --json shows what each queue holds',
|
|
117
117
|
});
|
|
118
118
|
}
|