@ultimat3/jobs 1.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.
Files changed (95) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +202 -0
  3. package/package.json +38 -0
  4. package/src/clock.d.ts +7 -0
  5. package/src/clock.d.ts.map +1 -0
  6. package/src/clock.js +21 -0
  7. package/src/clock.js.map +1 -0
  8. package/src/clock.ts +23 -0
  9. package/src/describe.ts +61 -0
  10. package/src/driver-memory.d.ts +9 -0
  11. package/src/driver-memory.d.ts.map +1 -0
  12. package/src/driver-memory.js +189 -0
  13. package/src/driver-memory.js.map +1 -0
  14. package/src/driver-memory.ts +216 -0
  15. package/src/driver-nats.d.ts +7 -0
  16. package/src/driver-nats.d.ts.map +1 -0
  17. package/src/driver-nats.js +51 -0
  18. package/src/driver-nats.js.map +1 -0
  19. package/src/driver-nats.ts +73 -0
  20. package/src/driver-pg-sql.d.ts +19 -0
  21. package/src/driver-pg-sql.d.ts.map +1 -0
  22. package/src/driver-pg-sql.js +160 -0
  23. package/src/driver-pg-sql.js.map +1 -0
  24. package/src/driver-pg-sql.ts +170 -0
  25. package/src/driver-pg.d.ts +17 -0
  26. package/src/driver-pg.d.ts.map +1 -0
  27. package/src/driver-pg.js +246 -0
  28. package/src/driver-pg.js.map +1 -0
  29. package/src/driver-pg.ts +356 -0
  30. package/src/driver-redis.d.ts +7 -0
  31. package/src/driver-redis.d.ts.map +1 -0
  32. package/src/driver-redis.js +54 -0
  33. package/src/driver-redis.js.map +1 -0
  34. package/src/driver-redis.ts +76 -0
  35. package/src/driver.d.ts +114 -0
  36. package/src/driver.d.ts.map +1 -0
  37. package/src/driver.js +14 -0
  38. package/src/driver.js.map +1 -0
  39. package/src/driver.ts +143 -0
  40. package/src/errors.d.ts +63 -0
  41. package/src/errors.d.ts.map +1 -0
  42. package/src/errors.js +105 -0
  43. package/src/errors.js.map +1 -0
  44. package/src/errors.ts +165 -0
  45. package/src/events.d.ts +35 -0
  46. package/src/events.d.ts.map +1 -0
  47. package/src/events.js +92 -0
  48. package/src/events.js.map +1 -0
  49. package/src/events.ts +134 -0
  50. package/src/index.d.ts +32 -0
  51. package/src/index.d.ts.map +1 -0
  52. package/src/index.js +18 -0
  53. package/src/index.js.map +1 -0
  54. package/src/index.ts +172 -0
  55. package/src/inspect.d.ts +82 -0
  56. package/src/inspect.d.ts.map +1 -0
  57. package/src/inspect.js +113 -0
  58. package/src/inspect.js.map +1 -0
  59. package/src/inspect.ts +213 -0
  60. package/src/job.d.ts +71 -0
  61. package/src/job.d.ts.map +1 -0
  62. package/src/job.js +99 -0
  63. package/src/job.js.map +1 -0
  64. package/src/job.ts +261 -0
  65. package/src/limits.d.ts +47 -0
  66. package/src/limits.d.ts.map +1 -0
  67. package/src/limits.js +0 -0
  68. package/src/limits.js.map +1 -0
  69. package/src/limits.ts +0 -0
  70. package/src/outbox.d.ts +81 -0
  71. package/src/outbox.d.ts.map +1 -0
  72. package/src/outbox.js +202 -0
  73. package/src/outbox.js.map +1 -0
  74. package/src/outbox.ts +336 -0
  75. package/src/register.ts +40 -0
  76. package/src/retry.d.ts +40 -0
  77. package/src/retry.d.ts.map +1 -0
  78. package/src/retry.js +59 -0
  79. package/src/retry.js.map +1 -0
  80. package/src/retry.ts +90 -0
  81. package/src/scheduler.d.ts +79 -0
  82. package/src/scheduler.d.ts.map +1 -0
  83. package/src/scheduler.js +183 -0
  84. package/src/scheduler.js.map +1 -0
  85. package/src/scheduler.ts +417 -0
  86. package/src/steps.d.ts +86 -0
  87. package/src/steps.d.ts.map +1 -0
  88. package/src/steps.js +227 -0
  89. package/src/steps.js.map +1 -0
  90. package/src/steps.ts +339 -0
  91. package/src/worker.d.ts +68 -0
  92. package/src/worker.d.ts.map +1 -0
  93. package/src/worker.js +273 -0
  94. package/src/worker.js.map +1 -0
  95. package/src/worker.ts +356 -0
package/src/driver.ts ADDED
@@ -0,0 +1,143 @@
1
+ // The queue contract. Every driver (pg, memory, redis, nats) implements exactly this, so
2
+ // switching backends is a config line and ZERO job-code change. Six methods and no more:
3
+ // claim/ack/nack with a visibility timeout is the smallest set that survives a worker crash.
4
+
5
+ import type { StepStore } from './steps';
6
+
7
+ export type JobState = 'ready' | 'delayed' | 'running' | 'suspended' | 'done' | 'failed' | 'dead';
8
+
9
+ export interface JobRecord {
10
+ readonly id: string;
11
+ readonly name: string;
12
+ readonly queue: string;
13
+ readonly input: unknown;
14
+ /** Dedupe key from the job definition. At-least-once delivery leans on this. */
15
+ readonly idempotencyKey: string;
16
+ /** Stable across retries and suspensions — the key every step record hangs off. */
17
+ readonly runId: string;
18
+ readonly attempt: number;
19
+ readonly maxAttempts: number;
20
+ readonly state: JobState;
21
+ /** Epoch ms; the job is invisible until then. */
22
+ readonly runAt: number;
23
+ readonly createdAt: number;
24
+ readonly updatedAt: number;
25
+ /** Actor's orgId, for per-tenant limits. */
26
+ readonly tenantId?: string;
27
+ readonly lastError?: string;
28
+ readonly claimedBy?: string;
29
+ readonly visibleAt?: number;
30
+ }
31
+
32
+ export type ConflictPolicy = 'dedupe' | 'error';
33
+
34
+ export interface EnqueueRequest {
35
+ readonly name: string;
36
+ readonly queue: string;
37
+ readonly input: unknown;
38
+ readonly idempotencyKey: string;
39
+ readonly maxAttempts: number;
40
+ /** Epoch ms. Omit for "now". */
41
+ readonly runAt?: number;
42
+ readonly tenantId?: string;
43
+ /** Reuse an existing run id when resuming, so step history is preserved. */
44
+ readonly runId?: string;
45
+ readonly onConflict?: ConflictPolicy;
46
+ }
47
+
48
+ export interface EnqueueResult {
49
+ readonly id: string;
50
+ readonly runId: string;
51
+ /** True when an in-flight job already held this idempotency key. */
52
+ readonly deduped: boolean;
53
+ }
54
+
55
+ export interface ClaimOptions {
56
+ readonly queues: readonly string[];
57
+ readonly limit: number;
58
+ /** Lease length. A worker that dies without ack makes the job claimable again after this. */
59
+ readonly visibilityTimeoutMs: number;
60
+ readonly workerId: string;
61
+ }
62
+
63
+ export interface ClaimedJob extends JobRecord {
64
+ readonly claimedAt: number;
65
+ readonly visibleAt: number;
66
+ }
67
+
68
+ export interface NackOptions {
69
+ /** Delay before the job becomes claimable again. */
70
+ readonly delayMs: number;
71
+ readonly error?: string;
72
+ /**
73
+ * False for a suspension (`step.sleep`): parking a run is not a failure and must not burn
74
+ * a retry attempt, or a 3-day sleep would dead-letter the job.
75
+ */
76
+ readonly countsAsAttempt?: boolean;
77
+ readonly deadLetter?: boolean;
78
+ }
79
+
80
+ export interface QueueStats {
81
+ readonly queue: string;
82
+ readonly ready: number;
83
+ readonly delayed: number;
84
+ readonly running: number;
85
+ readonly suspended: number;
86
+ readonly dead: number;
87
+ /** Age in ms of the oldest claimable job — the number that decides autoscaling. */
88
+ readonly oldestReadyMs: number;
89
+ }
90
+
91
+ export interface JobFilter {
92
+ readonly queue?: string;
93
+ readonly name?: string;
94
+ readonly state?: JobState;
95
+ readonly limit?: number;
96
+ }
97
+
98
+ /** Optional: powers `/_x` and the MCP tools. A minimal driver may omit it. */
99
+ export interface JobIntrospection {
100
+ job(jobId: string): Promise<JobRecord | undefined>;
101
+ list(filter?: JobFilter): Promise<readonly JobRecord[]>;
102
+ deadLetters(limit?: number): Promise<readonly JobRecord[]>;
103
+ /** Re-queue a dead/failed job. `fromStep` drops step records from that step onward. */
104
+ requeue(jobId: string, options?: { readonly fromStep?: string }): Promise<JobRecord>;
105
+ }
106
+
107
+ export interface JobDriver {
108
+ readonly name: string;
109
+ /** Step persistence lives with the queue: one store, one transaction boundary. */
110
+ readonly steps: StepStore;
111
+ enqueue(request: EnqueueRequest): Promise<EnqueueResult>;
112
+ claim(options: ClaimOptions): Promise<readonly ClaimedJob[]>;
113
+ ack(jobId: string): Promise<void>;
114
+ nack(jobId: string, options: NackOptions): Promise<void>;
115
+ /** Extends the lease of a long-running job so it is not double-claimed. */
116
+ heartbeat(jobId: string, options: { readonly visibilityTimeoutMs: number }): Promise<void>;
117
+ stats(): Promise<readonly QueueStats[]>;
118
+ readonly introspect?: JobIntrospection;
119
+ close?(): Promise<void>;
120
+ }
121
+
122
+ export const DEFAULT_QUEUE = 'default';
123
+ export const DEFAULT_VISIBILITY_TIMEOUT_MS = 30_000;
124
+
125
+ let ambient: JobDriver | undefined;
126
+
127
+ /** Set once at boot from `app.config.ts`. Roles share one driver instance per process. */
128
+ export function setJobDriver(driver: JobDriver): void {
129
+ ambient = driver;
130
+ }
131
+
132
+ export function jobDriver(): JobDriver | undefined {
133
+ return ambient;
134
+ }
135
+
136
+ /**
137
+ * Test/CLI seam: forget the ambient driver. The counterpart to `resetMailDriver()` — a test
138
+ * that installs a queue has to be able to put the process back, or every later file in the
139
+ * same bun process silently enqueues where it meant to run inline.
140
+ */
141
+ export function resetJobDriver(): void {
142
+ ambient = undefined;
143
+ }
@@ -0,0 +1,63 @@
1
+ import { UltimateError } from '@ultimat3/core';
2
+ export declare const JOB_ERROR_CODES: readonly ['X_JOB_DUPLICATE', 'X_STEP_DUPLICATE', 'X_JOB_TIMEOUT', 'X_JOB_MAX_ATTEMPTS', 'X_DRIVER_UNAVAILABLE', 'X_IDEMPOTENCY_REQUIRED', 'X_OUTBOX_NO_TX', 'X_NOT_IMPLEMENTED'];
3
+ export type JobErrorCode = (typeof JOB_ERROR_CODES)[number];
4
+ /** An enqueue collided with a live job holding the same idempotency key under `onConflict: 'error'`. */
5
+ export declare class JobDuplicateError extends UltimateError {
6
+ constructor(input: {
7
+ job: string;
8
+ idempotencyKey: string;
9
+ existingId: string;
10
+ });
11
+ }
12
+ /** Two steps in one run share a name, so replay cannot tell their persisted results apart. */
13
+ export declare class StepDuplicateError extends UltimateError {
14
+ constructor(input: {
15
+ job: string;
16
+ step: string;
17
+ });
18
+ }
19
+ export declare class JobTimeoutError extends UltimateError {
20
+ constructor(input: {
21
+ job: string;
22
+ timeoutMs: number;
23
+ step?: string;
24
+ });
25
+ }
26
+ /** Retries exhausted. The job is in the dead-letter queue, not lost. */
27
+ export declare class JobMaxAttemptsError extends UltimateError {
28
+ constructor(input: {
29
+ job: string;
30
+ jobId: string;
31
+ attempts: number;
32
+ lastError: string;
33
+ });
34
+ }
35
+ export declare class DriverUnavailableError extends UltimateError {
36
+ constructor(input: {
37
+ driver: string;
38
+ cause: string;
39
+ fix: string;
40
+ });
41
+ }
42
+ /**
43
+ * The type signature already requires `idempotencyKey`; this is the runtime backstop for
44
+ * generated code and JS callers, so the guarantee holds at both ends.
45
+ */
46
+ export declare class IdempotencyRequiredError extends UltimateError {
47
+ constructor(input: {
48
+ job: string;
49
+ });
50
+ }
51
+ /** An outbox enqueue happened with no ambient transaction to join. */
52
+ export declare class OutboxNoTxError extends UltimateError {
53
+ constructor(input: {
54
+ job: string;
55
+ });
56
+ }
57
+ export declare class JobsNotImplementedError extends UltimateError {
58
+ constructor(input: {
59
+ feature: string;
60
+ fix: string;
61
+ });
62
+ }
63
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["errors.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAE/C,eAAO,MAAM,eAAe,YAC1B,iBAAiB,EACjB,kBAAkB,EAClB,eAAe,EACf,oBAAoB,EACpB,sBAAsB,EACtB,wBAAwB,EACxB,gBAAgB,EAChB,mBAAmB,CACX,CAAC;AAEX,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAC;AAI5D,wGAAwG;AACxG,qBAAa,iBAAkB,SAAQ,aAAa;IAClD,YAAY,KAAK,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,EAO7E;CACF;AAED,8FAA8F;AAC9F,qBAAa,kBAAmB,SAAQ,aAAa;IACnD,YAAY,KAAK,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAO/C;CACF;AAED,qBAAa,eAAgB,SAAQ,aAAa;IAChD,YAAY,KAAK,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,EAUnE;CACF;AAED,wEAAwE;AACxE,qBAAa,mBAAoB,SAAQ,aAAa;IACpD,YAAY,KAAK,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,EAOrF;CACF;AAED,qBAAa,sBAAuB,SAAQ,aAAa;IACvD,YAAY,KAAK,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,EAOhE;CACF;AAED;;;GAGG;AACH,qBAAa,wBAAyB,SAAQ,aAAa;IACzD,YAAY,KAAK,EAAE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,EAOjC;CACF;AAED,sEAAsE;AACtE,qBAAa,eAAgB,SAAQ,aAAa;IAChD,YAAY,KAAK,EAAE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,EAOjC;CACF;AAED,qBAAa,uBAAwB,SAAQ,aAAa;IACxD,YAAY,KAAK,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,EAOlD;CACF"}
package/src/errors.js ADDED
@@ -0,0 +1,105 @@
1
+ // The X_* codes owned by @ultimat3/jobs. Every one names the command or code change that
2
+ // fixes it — a job failure an agent cannot act on is a job failure that gets retried forever.
3
+ import { UltimateError } from '@ultimat3/core';
4
+ export const JOB_ERROR_CODES = [
5
+ 'X_JOB_DUPLICATE',
6
+ 'X_STEP_DUPLICATE',
7
+ 'X_JOB_TIMEOUT',
8
+ 'X_JOB_MAX_ATTEMPTS',
9
+ 'X_DRIVER_UNAVAILABLE',
10
+ 'X_IDEMPOTENCY_REQUIRED',
11
+ 'X_OUTBOX_NO_TX',
12
+ 'X_NOT_IMPLEMENTED',
13
+ ];
14
+ const docsFor = (code) => `https://ultimate.dev/errors/${code}`;
15
+ /** An enqueue collided with a live job holding the same idempotency key under `onConflict: 'error'`. */
16
+ export class JobDuplicateError extends UltimateError {
17
+ constructor(input) {
18
+ super({
19
+ code: 'X_JOB_DUPLICATE',
20
+ cause: `job "${input.job}" already queued as ${input.existingId} with idempotencyKey "${input.idempotencyKey}"`,
21
+ fix: 'pass onConflict: "dedupe" to enqueue, or make idempotencyKey narrower',
22
+ docs: docsFor('X_JOB_DUPLICATE'),
23
+ });
24
+ }
25
+ }
26
+ /** Two steps in one run share a name, so replay cannot tell their persisted results apart. */
27
+ export class StepDuplicateError extends UltimateError {
28
+ constructor(input) {
29
+ super({
30
+ code: 'X_STEP_DUPLICATE',
31
+ cause: `job "${input.job}" used step name "${input.step}" twice in one run`,
32
+ fix: `rename one of them, e.g. step.run('${input.step}-2', ...) — step names are the replay key`,
33
+ docs: docsFor('X_STEP_DUPLICATE'),
34
+ });
35
+ }
36
+ }
37
+ export class JobTimeoutError extends UltimateError {
38
+ constructor(input) {
39
+ super({
40
+ code: 'X_JOB_TIMEOUT',
41
+ cause: input.step === undefined
42
+ ? `job "${input.job}" exceeded its ${input.timeoutMs}ms timeout`
43
+ : `job "${input.job}" step "${input.step}" exceeded its ${input.timeoutMs}ms timeout`,
44
+ fix: `raise timeout on the job definition, or split the work into step.run() calls`,
45
+ docs: docsFor('X_JOB_TIMEOUT'),
46
+ });
47
+ }
48
+ }
49
+ /** Retries exhausted. The job is in the dead-letter queue, not lost. */
50
+ export class JobMaxAttemptsError extends UltimateError {
51
+ constructor(input) {
52
+ super({
53
+ code: 'X_JOB_MAX_ATTEMPTS',
54
+ cause: `job "${input.job}" failed ${input.attempts} times, last error: ${input.lastError}`,
55
+ fix: `x jobs retry ${input.jobId}`,
56
+ docs: docsFor('X_JOB_MAX_ATTEMPTS'),
57
+ });
58
+ }
59
+ }
60
+ export class DriverUnavailableError extends UltimateError {
61
+ constructor(input) {
62
+ super({
63
+ code: 'X_DRIVER_UNAVAILABLE',
64
+ cause: `jobs driver "${input.driver}" is unavailable: ${input.cause}`,
65
+ fix: input.fix,
66
+ docs: docsFor('X_DRIVER_UNAVAILABLE'),
67
+ });
68
+ }
69
+ }
70
+ /**
71
+ * The type signature already requires `idempotencyKey`; this is the runtime backstop for
72
+ * generated code and JS callers, so the guarantee holds at both ends.
73
+ */
74
+ export class IdempotencyRequiredError extends UltimateError {
75
+ constructor(input) {
76
+ super({
77
+ code: 'X_IDEMPOTENCY_REQUIRED',
78
+ cause: `job "${input.job}" has no idempotencyKey — at-least-once delivery would run it twice`,
79
+ fix: `add idempotencyKey: (input) => \`${input.job}:\${input.id}\` to the job definition`,
80
+ docs: docsFor('X_IDEMPOTENCY_REQUIRED'),
81
+ });
82
+ }
83
+ }
84
+ /** An outbox enqueue happened with no ambient transaction to join. */
85
+ export class OutboxNoTxError extends UltimateError {
86
+ constructor(input) {
87
+ super({
88
+ code: 'X_OUTBOX_NO_TX',
89
+ cause: `ctx.jobs.enqueue(${input.job}) ran outside a transaction with outbox: 'required'`,
90
+ fix: 'wrap the call in ctx.tx(async (tx) => ...), or enqueue with { outbox: false }',
91
+ docs: docsFor('X_OUTBOX_NO_TX'),
92
+ });
93
+ }
94
+ }
95
+ export class JobsNotImplementedError extends UltimateError {
96
+ constructor(input) {
97
+ super({
98
+ code: 'X_NOT_IMPLEMENTED',
99
+ cause: `${input.feature} is declared but not implemented in @ultimat3/jobs`,
100
+ fix: input.fix,
101
+ docs: docsFor('X_NOT_IMPLEMENTED'),
102
+ });
103
+ }
104
+ }
105
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["errors.ts"],"names":[],"mappings":"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"}
package/src/errors.ts ADDED
@@ -0,0 +1,165 @@
1
+ // The X_* codes owned by @ultimat3/jobs. Every one names the command or code change that
2
+ // fixes it — a job failure an agent cannot act on is a job failure that gets retried forever.
3
+ import { registerErrorCodes, UltimateError } from '@ultimat3/core';
4
+
5
+ /** Codes this package declares and owns. */
6
+ export const JOB_OWNED_ERROR_CODES = [
7
+ 'X_JOB_DUPLICATE',
8
+ 'X_STEP_DUPLICATE',
9
+ 'X_JOB_TIMEOUT',
10
+ 'X_JOB_MAX_ATTEMPTS',
11
+ 'X_DRIVER_UNAVAILABLE',
12
+ 'X_IDEMPOTENCY_REQUIRED',
13
+ 'X_OUTBOX_NO_TX',
14
+ ] as const;
15
+
16
+ /**
17
+ * `X_NOT_IMPLEMENTED` is `@ultimat3/core`'s. `JobsNotImplementedError` below throws it; jobs keeps
18
+ * no title for it, because the copy this file used to hold was a second title that nothing would
19
+ * have failed on once core's changed.
20
+ */
21
+ export const JOB_BORROWED_ERROR_CODES = ['X_NOT_IMPLEMENTED'] as const;
22
+
23
+ /** Every code jobs can throw: the ones it owns plus the one it borrows. */
24
+ export const JOB_ERROR_CODES = [...JOB_OWNED_ERROR_CODES, ...JOB_BORROWED_ERROR_CODES] as const;
25
+
26
+ export type JobOwnedErrorCode = (typeof JOB_OWNED_ERROR_CODES)[number];
27
+ export type JobErrorCode = (typeof JOB_ERROR_CODES)[number];
28
+
29
+ export const JOB_ERROR_TITLES: Readonly<Record<JobOwnedErrorCode, string>> = {
30
+ X_JOB_DUPLICATE: 'a live key already has a job',
31
+ X_STEP_DUPLICATE: 'two step.run calls share a name',
32
+ X_JOB_TIMEOUT: 'a job exceeded its wall-clock limit',
33
+ X_JOB_MAX_ATTEMPTS: 'the job exhausted its retries',
34
+ X_DRIVER_UNAVAILABLE: 'the queue driver is unreachable',
35
+ X_IDEMPOTENCY_REQUIRED: 'the job has no idempotencyKey',
36
+ X_OUTBOX_NO_TX: 'enqueue outside a transaction',
37
+ };
38
+
39
+ // One unconditional call, so a second package claiming one of jobs' codes throws
40
+ // X_ERROR_CODE_DUPLICATE instead of losing silently to whichever module imported first.
41
+ registerErrorCodes(
42
+ Object.fromEntries(Object.entries(JOB_ERROR_TITLES).map(([code, title]) => [code, { title }])),
43
+ );
44
+
45
+ const docsFor = (code: JobErrorCode): string => `https://ultimate.dev/errors/${code}`;
46
+
47
+ /** An enqueue collided with a live job holding the same idempotency key under `onConflict: 'error'`. */
48
+ export class JobDuplicateError extends UltimateError {
49
+ constructor(input: { job: string; idempotencyKey: string; existingId: string }) {
50
+ super({
51
+ code: 'X_JOB_DUPLICATE',
52
+ cause: `job "${input.job}" already queued as ${input.existingId} with idempotencyKey "${input.idempotencyKey}"`,
53
+ fix: 'pass onConflict: "dedupe" to enqueue, or make idempotencyKey narrower',
54
+ docs: docsFor('X_JOB_DUPLICATE'),
55
+ });
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Two things claim one durable name — two definitions setting the same `name:`, two handles under
61
+ * one export name, or one handle exported twice. Shares `X_JOB_DUPLICATE` with the enqueue-time
62
+ * collision because it is the same statement — this key already belongs to another job — caught
63
+ * one stage earlier. Left unrefused, the second claim would silently take over delivery of every
64
+ * row already queued under the first.
65
+ *
66
+ * The `fix` names the edit and the command that shows what is already seated. It cannot name a
67
+ * file: the registry holds handles, not source locations, and `name` is all a handle carries.
68
+ */
69
+ export class JobNameTakenError extends UltimateError {
70
+ constructor(input: { kind: 'job' | 'task'; name: string }) {
71
+ super({
72
+ code: 'X_JOB_DUPLICATE',
73
+ cause: `two ${input.kind}s claim the name "${input.name}"`,
74
+ fix: `x jobs ls --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`,
75
+ docs: docsFor('X_JOB_DUPLICATE'),
76
+ });
77
+ }
78
+ }
79
+
80
+ /** Two steps in one run share a name, so replay cannot tell their persisted results apart. */
81
+ export class StepDuplicateError extends UltimateError {
82
+ constructor(input: { job: string; step: string }) {
83
+ super({
84
+ code: 'X_STEP_DUPLICATE',
85
+ cause: `job "${input.job}" used step name "${input.step}" twice in one run`,
86
+ fix: `rename one of them, e.g. step.run('${input.step}-2', ...) — step names are the replay key`,
87
+ docs: docsFor('X_STEP_DUPLICATE'),
88
+ });
89
+ }
90
+ }
91
+
92
+ export class JobTimeoutError extends UltimateError {
93
+ constructor(input: { job: string; timeoutMs: number; step?: string }) {
94
+ super({
95
+ code: 'X_JOB_TIMEOUT',
96
+ cause:
97
+ input.step === undefined
98
+ ? `job "${input.job}" exceeded its ${input.timeoutMs}ms timeout`
99
+ : `job "${input.job}" step "${input.step}" exceeded its ${input.timeoutMs}ms timeout`,
100
+ fix: `raise timeout on the job definition, or split the work into step.run() calls`,
101
+ docs: docsFor('X_JOB_TIMEOUT'),
102
+ });
103
+ }
104
+ }
105
+
106
+ /** Retries exhausted. The job is in the dead-letter queue, not lost. */
107
+ export class JobMaxAttemptsError extends UltimateError {
108
+ constructor(input: { job: string; jobId: string; attempts: number; lastError: string }) {
109
+ super({
110
+ code: 'X_JOB_MAX_ATTEMPTS',
111
+ cause: `job "${input.job}" failed ${input.attempts} times, last error: ${input.lastError}`,
112
+ fix: `x jobs retry ${input.jobId}`,
113
+ docs: docsFor('X_JOB_MAX_ATTEMPTS'),
114
+ });
115
+ }
116
+ }
117
+
118
+ export class DriverUnavailableError extends UltimateError {
119
+ constructor(input: { driver: string; cause: string; fix: string }) {
120
+ super({
121
+ code: 'X_DRIVER_UNAVAILABLE',
122
+ cause: `jobs driver "${input.driver}" is unavailable: ${input.cause}`,
123
+ fix: input.fix,
124
+ docs: docsFor('X_DRIVER_UNAVAILABLE'),
125
+ });
126
+ }
127
+ }
128
+
129
+ /**
130
+ * The type signature already requires `idempotencyKey`; this is the runtime backstop for
131
+ * generated code and JS callers, so the guarantee holds at both ends.
132
+ */
133
+ export class IdempotencyRequiredError extends UltimateError {
134
+ constructor(input: { job: string }) {
135
+ super({
136
+ code: 'X_IDEMPOTENCY_REQUIRED',
137
+ cause: `job "${input.job}" has no idempotencyKey — at-least-once delivery would run it twice`,
138
+ fix: `add idempotencyKey: (input) => \`${input.job}:\${input.id}\` to the job definition`,
139
+ docs: docsFor('X_IDEMPOTENCY_REQUIRED'),
140
+ });
141
+ }
142
+ }
143
+
144
+ /** An outbox enqueue happened with no ambient transaction to join. */
145
+ export class OutboxNoTxError extends UltimateError {
146
+ constructor(input: { job: string }) {
147
+ super({
148
+ code: 'X_OUTBOX_NO_TX',
149
+ cause: `ctx.jobs.enqueue(${input.job}) ran outside a transaction with outbox: 'required'`,
150
+ fix: 'wrap the call in ctx.tx(async (tx) => ...), or enqueue with { outbox: false }',
151
+ docs: docsFor('X_OUTBOX_NO_TX'),
152
+ });
153
+ }
154
+ }
155
+
156
+ export class JobsNotImplementedError extends UltimateError {
157
+ constructor(input: { feature: string; fix: string }) {
158
+ super({
159
+ code: 'X_NOT_IMPLEMENTED',
160
+ cause: `${input.feature} is declared but not implemented in @ultimat3/jobs`,
161
+ fix: input.fix,
162
+ docs: docsFor('X_NOT_IMPLEMENTED'),
163
+ });
164
+ }
165
+ }
@@ -0,0 +1,35 @@
1
+ import type { Clock } from '@ultimat3/core';
2
+ import type { DurationInput } from './clock';
3
+ import type { EventLookup } from './steps';
4
+ export interface JobEvent {
5
+ readonly id: string;
6
+ readonly name: string;
7
+ readonly payload: unknown;
8
+ /** Ties the event to one waiting run — usually an entity id. */
9
+ readonly correlationKey?: string;
10
+ readonly publishedAt: number;
11
+ readonly expiresAt: number;
12
+ }
13
+ export interface PublishOptions {
14
+ readonly correlationKey?: string;
15
+ /** How long the event stays matchable. Default 7d — longer than any sane wait. */
16
+ readonly ttl?: DurationInput;
17
+ }
18
+ export interface EventBus extends EventLookup {
19
+ publish(name: string, payload: unknown, options?: PublishOptions): Promise<JobEvent>;
20
+ list(name?: string): Promise<readonly JobEvent[]>;
21
+ purgeExpired(): number;
22
+ size(): number;
23
+ }
24
+ export interface MemoryEventBusOptions {
25
+ readonly clock?: Clock;
26
+ readonly defaultTtl?: DurationInput;
27
+ readonly maxEvents?: number;
28
+ }
29
+ export declare function createMemoryEventBus(options?: MemoryEventBusOptions): EventBus;
30
+ /** Swapped at boot for the NATS/Redis-streams bus in a multi-node deployment. */
31
+ export declare function setEventBus(bus: EventBus): void;
32
+ export declare function eventBus(): EventBus;
33
+ /** The one function app code calls to unblock a waiting step. */
34
+ export declare function publishEvent(name: string, payload: unknown, options?: PublishOptions): Promise<JobEvent>;
35
+ //# sourceMappingURL=events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["events.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAE5C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAE7C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAE3C,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,gEAAgE;IAChE,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,kFAAkF;IAClF,QAAQ,CAAC,GAAG,CAAC,EAAE,aAAa,CAAC;CAC9B;AAED,MAAM,WAAW,QAAS,SAAQ,WAAW;IAC3C,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IACrF,IAAI,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,QAAQ,EAAE,CAAC,CAAC;IAClD,YAAY,IAAI,MAAM,CAAC;IACvB,IAAI,IAAI,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC;IACvB,QAAQ,CAAC,UAAU,CAAC,EAAE,aAAa,CAAC;IACpC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,wBAAgB,oBAAoB,CAAC,OAAO,GAAE,qBAA0B,GAAG,QAAQ,CA0ElF;AAID,iFAAiF;AACjF,wBAAgB,WAAW,CAAC,GAAG,EAAE,QAAQ,GAAG,IAAI,CAE/C;AAED,wBAAgB,QAAQ,IAAI,QAAQ,CAEnC;AAED,iEAAiE;AACjE,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,QAAQ,CAAC,CAEnB"}
package/src/events.js ADDED
@@ -0,0 +1,92 @@
1
+ // The event bus `step.waitForEvent` consumes. Events are stored, not just broadcast: a step
2
+ // that suspends at 12:00 and resumes at 12:00:30 must still see an event published at
3
+ // 12:00:10, so a fire-and-forget emitter would silently strand every waiting run.
4
+ import { logger, systemClock, uuid } from '@ultimat3/core';
5
+ import { nowMs, toMs } from './clock';
6
+ export function createMemoryEventBus(options = {}) {
7
+ const clock = options.clock ?? systemClock;
8
+ const defaultTtl = options.defaultTtl ?? 604_800_000;
9
+ const maxEvents = options.maxEvents ?? 10_000;
10
+ const events = new Map();
11
+ const purgeExpired = () => {
12
+ const at = nowMs(clock);
13
+ let removed = 0;
14
+ for (const [id, event] of events) {
15
+ if (event.expiresAt <= at) {
16
+ events.delete(id);
17
+ removed += 1;
18
+ }
19
+ }
20
+ return removed;
21
+ };
22
+ return {
23
+ publish(name, payload, publishOptions = {}) {
24
+ purgeExpired();
25
+ const at = nowMs(clock);
26
+ const event = {
27
+ id: uuid(),
28
+ name,
29
+ payload,
30
+ publishedAt: at,
31
+ expiresAt: at + toMs(publishOptions.ttl ?? defaultTtl),
32
+ ...(publishOptions.correlationKey === undefined
33
+ ? {}
34
+ : { correlationKey: publishOptions.correlationKey }),
35
+ };
36
+ events.set(event.id, event);
37
+ while (events.size > maxEvents) {
38
+ const oldest = events.keys().next();
39
+ if (oldest.done === true)
40
+ break;
41
+ events.delete(oldest.value);
42
+ }
43
+ logger.debug('jobs.event.published', {
44
+ event: name,
45
+ correlationKey: publishOptions.correlationKey ?? null,
46
+ });
47
+ return Promise.resolve(event);
48
+ },
49
+ /**
50
+ * Earliest matching event at or after `afterMs`, so a resumed step consumes events in
51
+ * publication order rather than jumping to the newest one.
52
+ */
53
+ find(name, correlationKey, afterMs) {
54
+ const at = nowMs(clock);
55
+ let best;
56
+ for (const event of events.values()) {
57
+ if (event.name !== name)
58
+ continue;
59
+ if (event.expiresAt <= at)
60
+ continue;
61
+ if (event.publishedAt < afterMs)
62
+ continue;
63
+ if (correlationKey !== undefined && event.correlationKey !== correlationKey)
64
+ continue;
65
+ if (best === undefined || event.publishedAt < best.publishedAt)
66
+ best = event;
67
+ }
68
+ return Promise.resolve(best === undefined ? undefined : { payload: best.payload, publishedAt: best.publishedAt });
69
+ },
70
+ list(name) {
71
+ const all = [...events.values()]
72
+ .filter((event) => name === undefined || event.name === name)
73
+ .sort((a, b) => a.publishedAt - b.publishedAt);
74
+ return Promise.resolve(all);
75
+ },
76
+ purgeExpired,
77
+ size: () => events.size,
78
+ };
79
+ }
80
+ let ambientBus = createMemoryEventBus();
81
+ /** Swapped at boot for the NATS/Redis-streams bus in a multi-node deployment. */
82
+ export function setEventBus(bus) {
83
+ ambientBus = bus;
84
+ }
85
+ export function eventBus() {
86
+ return ambientBus;
87
+ }
88
+ /** The one function app code calls to unblock a waiting step. */
89
+ export function publishEvent(name, payload, options) {
90
+ return ambientBus.publish(name, payload, options);
91
+ }
92
+ //# sourceMappingURL=events.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.js","sourceRoot":"","sources":["events.ts"],"names":[],"mappings":"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"}