@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.
- package/LICENSE +21 -0
- package/README.md +202 -0
- package/package.json +38 -0
- package/src/clock.d.ts +7 -0
- package/src/clock.d.ts.map +1 -0
- package/src/clock.js +21 -0
- package/src/clock.js.map +1 -0
- package/src/clock.ts +23 -0
- package/src/describe.ts +61 -0
- package/src/driver-memory.d.ts +9 -0
- package/src/driver-memory.d.ts.map +1 -0
- package/src/driver-memory.js +189 -0
- package/src/driver-memory.js.map +1 -0
- package/src/driver-memory.ts +216 -0
- package/src/driver-nats.d.ts +7 -0
- package/src/driver-nats.d.ts.map +1 -0
- package/src/driver-nats.js +51 -0
- package/src/driver-nats.js.map +1 -0
- package/src/driver-nats.ts +73 -0
- package/src/driver-pg-sql.d.ts +19 -0
- package/src/driver-pg-sql.d.ts.map +1 -0
- package/src/driver-pg-sql.js +160 -0
- package/src/driver-pg-sql.js.map +1 -0
- package/src/driver-pg-sql.ts +170 -0
- package/src/driver-pg.d.ts +17 -0
- package/src/driver-pg.d.ts.map +1 -0
- package/src/driver-pg.js +246 -0
- package/src/driver-pg.js.map +1 -0
- package/src/driver-pg.ts +356 -0
- package/src/driver-redis.d.ts +7 -0
- package/src/driver-redis.d.ts.map +1 -0
- package/src/driver-redis.js +54 -0
- package/src/driver-redis.js.map +1 -0
- package/src/driver-redis.ts +76 -0
- package/src/driver.d.ts +114 -0
- package/src/driver.d.ts.map +1 -0
- package/src/driver.js +14 -0
- package/src/driver.js.map +1 -0
- package/src/driver.ts +143 -0
- package/src/errors.d.ts +63 -0
- package/src/errors.d.ts.map +1 -0
- package/src/errors.js +105 -0
- package/src/errors.js.map +1 -0
- package/src/errors.ts +165 -0
- package/src/events.d.ts +35 -0
- package/src/events.d.ts.map +1 -0
- package/src/events.js +92 -0
- package/src/events.js.map +1 -0
- package/src/events.ts +134 -0
- package/src/index.d.ts +32 -0
- package/src/index.d.ts.map +1 -0
- package/src/index.js +18 -0
- package/src/index.js.map +1 -0
- package/src/index.ts +172 -0
- package/src/inspect.d.ts +82 -0
- package/src/inspect.d.ts.map +1 -0
- package/src/inspect.js +113 -0
- package/src/inspect.js.map +1 -0
- package/src/inspect.ts +213 -0
- package/src/job.d.ts +71 -0
- package/src/job.d.ts.map +1 -0
- package/src/job.js +99 -0
- package/src/job.js.map +1 -0
- package/src/job.ts +261 -0
- package/src/limits.d.ts +47 -0
- package/src/limits.d.ts.map +1 -0
- package/src/limits.js +0 -0
- package/src/limits.js.map +1 -0
- package/src/limits.ts +0 -0
- package/src/outbox.d.ts +81 -0
- package/src/outbox.d.ts.map +1 -0
- package/src/outbox.js +202 -0
- package/src/outbox.js.map +1 -0
- package/src/outbox.ts +336 -0
- package/src/register.ts +40 -0
- package/src/retry.d.ts +40 -0
- package/src/retry.d.ts.map +1 -0
- package/src/retry.js +59 -0
- package/src/retry.js.map +1 -0
- package/src/retry.ts +90 -0
- package/src/scheduler.d.ts +79 -0
- package/src/scheduler.d.ts.map +1 -0
- package/src/scheduler.js +183 -0
- package/src/scheduler.js.map +1 -0
- package/src/scheduler.ts +417 -0
- package/src/steps.d.ts +86 -0
- package/src/steps.d.ts.map +1 -0
- package/src/steps.js +227 -0
- package/src/steps.js.map +1 -0
- package/src/steps.ts +339 -0
- package/src/worker.d.ts +68 -0
- package/src/worker.d.ts.map +1 -0
- package/src/worker.js +273 -0
- package/src/worker.js.map +1 -0
- package/src/worker.ts +356 -0
package/src/driver-pg.ts
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
// The DEFAULT driver: a Postgres queue. Zero infra to start — the database you already have
|
|
2
|
+
// is the queue. `SELECT ... FOR UPDATE SKIP LOCKED` lets N workers claim disjoint batches
|
|
3
|
+
// without a coordinator, and the visibility timeout (`visible_at`) makes a worker crash cost
|
|
4
|
+
// one lease instead of one job. The statements themselves live in `driver-pg-sql.ts`.
|
|
5
|
+
|
|
6
|
+
import type { Clock } from '@ultimat3/core';
|
|
7
|
+
import { systemClock, uuid } from '@ultimat3/core';
|
|
8
|
+
import { nowMs } from './clock';
|
|
9
|
+
import type {
|
|
10
|
+
ClaimedJob,
|
|
11
|
+
ClaimOptions,
|
|
12
|
+
EnqueueRequest,
|
|
13
|
+
EnqueueResult,
|
|
14
|
+
JobDriver,
|
|
15
|
+
JobFilter,
|
|
16
|
+
JobIntrospection,
|
|
17
|
+
JobRecord,
|
|
18
|
+
NackOptions,
|
|
19
|
+
QueueStats,
|
|
20
|
+
} from './driver';
|
|
21
|
+
import { DEFAULT_QUEUE } from './driver';
|
|
22
|
+
import {
|
|
23
|
+
SQL_ACK,
|
|
24
|
+
SQL_ADVISORY_UNLOCK,
|
|
25
|
+
SQL_CLAIM,
|
|
26
|
+
SQL_ENQUEUE,
|
|
27
|
+
SQL_FIND_LIVE_BY_KEY,
|
|
28
|
+
SQL_HEARTBEAT,
|
|
29
|
+
SQL_NACK,
|
|
30
|
+
SQL_STATS,
|
|
31
|
+
SQL_STEP_GET,
|
|
32
|
+
SQL_STEP_PUT,
|
|
33
|
+
SQL_TRY_ADVISORY_LOCK,
|
|
34
|
+
} from './driver-pg-sql';
|
|
35
|
+
import { DriverUnavailableError, JobDuplicateError } from './errors';
|
|
36
|
+
import type { StepRecord, StepStore } from './steps';
|
|
37
|
+
|
|
38
|
+
/** The one thing this driver needs from the DB layer. Satisfied by `Bun.sql` and by a Tx. */
|
|
39
|
+
export interface PgExecutor {
|
|
40
|
+
query<R>(sql: string, params: readonly unknown[]): Promise<readonly R[]>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface PgDriverOptions {
|
|
44
|
+
readonly executor?: PgExecutor;
|
|
45
|
+
readonly clock?: Clock;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface StepRow {
|
|
49
|
+
readonly run_id: string;
|
|
50
|
+
readonly name: string;
|
|
51
|
+
readonly status: string;
|
|
52
|
+
readonly output: unknown;
|
|
53
|
+
readonly started_at: number | string;
|
|
54
|
+
readonly completed_at: number | string | null;
|
|
55
|
+
readonly wake_at: number | string | null;
|
|
56
|
+
readonly event: string | null;
|
|
57
|
+
readonly correlation_key: string | null;
|
|
58
|
+
readonly attempts: number;
|
|
59
|
+
readonly error: string | null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface JobRow {
|
|
63
|
+
readonly id: string;
|
|
64
|
+
readonly name: string;
|
|
65
|
+
readonly queue: string;
|
|
66
|
+
readonly input: unknown;
|
|
67
|
+
readonly idempotency_key: string;
|
|
68
|
+
readonly run_id: string;
|
|
69
|
+
readonly attempt: number;
|
|
70
|
+
readonly max_attempts: number;
|
|
71
|
+
readonly state: string;
|
|
72
|
+
readonly tenant_id: string | null;
|
|
73
|
+
readonly last_error: string | null;
|
|
74
|
+
readonly claimed_by: string | null;
|
|
75
|
+
readonly run_at: number | string;
|
|
76
|
+
readonly visible_at: number | string | null;
|
|
77
|
+
readonly created_at: number | string;
|
|
78
|
+
readonly updated_at: number | string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const num = (value: number | string | null | undefined): number =>
|
|
82
|
+
value === null || value === undefined ? 0 : Number(value);
|
|
83
|
+
|
|
84
|
+
const optionalNum = (value: number | string | null | undefined): number | undefined =>
|
|
85
|
+
value === null || value === undefined ? undefined : Number(value);
|
|
86
|
+
|
|
87
|
+
function toJobRecord(row: JobRow): JobRecord {
|
|
88
|
+
const visibleAt = optionalNum(row.visible_at);
|
|
89
|
+
return {
|
|
90
|
+
id: row.id,
|
|
91
|
+
name: row.name,
|
|
92
|
+
queue: row.queue,
|
|
93
|
+
input: row.input,
|
|
94
|
+
idempotencyKey: row.idempotency_key,
|
|
95
|
+
runId: row.run_id,
|
|
96
|
+
attempt: row.attempt,
|
|
97
|
+
maxAttempts: row.max_attempts,
|
|
98
|
+
state: row.state as JobRecord['state'],
|
|
99
|
+
runAt: num(row.run_at),
|
|
100
|
+
createdAt: num(row.created_at),
|
|
101
|
+
updatedAt: num(row.updated_at),
|
|
102
|
+
...(row.tenant_id === null ? {} : { tenantId: row.tenant_id }),
|
|
103
|
+
...(row.last_error === null ? {} : { lastError: row.last_error }),
|
|
104
|
+
...(row.claimed_by === null ? {} : { claimedBy: row.claimed_by }),
|
|
105
|
+
...(visibleAt === undefined ? {} : { visibleAt }),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function toStepRecord(row: StepRow): StepRecord {
|
|
110
|
+
const completedAt = optionalNum(row.completed_at);
|
|
111
|
+
const wakeAt = optionalNum(row.wake_at);
|
|
112
|
+
return {
|
|
113
|
+
runId: row.run_id,
|
|
114
|
+
name: row.name,
|
|
115
|
+
status: row.status as StepRecord['status'],
|
|
116
|
+
output: row.output,
|
|
117
|
+
startedAt: num(row.started_at),
|
|
118
|
+
attempts: row.attempts,
|
|
119
|
+
...(completedAt === undefined ? {} : { completedAt }),
|
|
120
|
+
...(wakeAt === undefined ? {} : { wakeAt }),
|
|
121
|
+
...(row.event === null ? {} : { event: row.event }),
|
|
122
|
+
...(row.correlation_key === null ? {} : { correlationKey: row.correlation_key }),
|
|
123
|
+
...(row.error === null ? {} : { error: row.error }),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function resolveExecutor(injected: PgExecutor | undefined): PgExecutor {
|
|
128
|
+
if (injected !== undefined) return injected;
|
|
129
|
+
throw new DriverUnavailableError({
|
|
130
|
+
driver: 'pg',
|
|
131
|
+
cause: 'no PgExecutor was provided and Bun.sql is not configured',
|
|
132
|
+
fix: 'set DATABASE_URL in .env then run `x db up`, or use driver: "memory" in app.config.ts',
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function pgStepStore(exec: () => PgExecutor): StepStore {
|
|
137
|
+
return {
|
|
138
|
+
async get(runId, name) {
|
|
139
|
+
const rows = await exec().query<StepRow>(SQL_STEP_GET, [runId, name]);
|
|
140
|
+
const row = rows[0];
|
|
141
|
+
return row === undefined ? undefined : toStepRecord(row);
|
|
142
|
+
},
|
|
143
|
+
async put(record) {
|
|
144
|
+
await exec().query(SQL_STEP_PUT, [
|
|
145
|
+
record.runId,
|
|
146
|
+
record.name,
|
|
147
|
+
record.status,
|
|
148
|
+
JSON.stringify(record.output ?? null),
|
|
149
|
+
record.startedAt,
|
|
150
|
+
record.completedAt ?? null,
|
|
151
|
+
record.wakeAt ?? null,
|
|
152
|
+
record.event ?? null,
|
|
153
|
+
record.correlationKey ?? null,
|
|
154
|
+
record.attempts,
|
|
155
|
+
record.error ?? null,
|
|
156
|
+
]);
|
|
157
|
+
},
|
|
158
|
+
async list(runId) {
|
|
159
|
+
const rows = await exec().query<StepRow>(
|
|
160
|
+
`select * from x_job_steps where run_id = $1 order by started_at`,
|
|
161
|
+
[runId],
|
|
162
|
+
);
|
|
163
|
+
return rows.map(toStepRecord);
|
|
164
|
+
},
|
|
165
|
+
async del(runId, name) {
|
|
166
|
+
await exec().query(`delete from x_job_steps where run_id = $1 and name = $2`, [runId, name]);
|
|
167
|
+
},
|
|
168
|
+
async clear(runId) {
|
|
169
|
+
await exec().query(`delete from x_job_steps where run_id = $1`, [runId]);
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function createPgDriver(options: PgDriverOptions = {}): JobDriver {
|
|
175
|
+
const clock = options.clock ?? systemClock;
|
|
176
|
+
let executor: PgExecutor | undefined;
|
|
177
|
+
const exec = (): PgExecutor => {
|
|
178
|
+
executor ??= resolveExecutor(options.executor);
|
|
179
|
+
return executor;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const introspect: JobIntrospection = {
|
|
183
|
+
async job(jobId) {
|
|
184
|
+
const rows = await exec().query<JobRow>(`select * from x_jobs where id = $1`, [jobId]);
|
|
185
|
+
const row = rows[0];
|
|
186
|
+
return row === undefined ? undefined : toJobRecord(row);
|
|
187
|
+
},
|
|
188
|
+
async list(filter: JobFilter = {}) {
|
|
189
|
+
const rows = await exec().query<JobRow>(
|
|
190
|
+
`select * from x_jobs
|
|
191
|
+
where ($1::text is null or queue = $1)
|
|
192
|
+
and ($2::text is null or name = $2)
|
|
193
|
+
and ($3::text is null or state = $3)
|
|
194
|
+
order by created_at desc
|
|
195
|
+
limit $4`,
|
|
196
|
+
[filter.queue ?? null, filter.name ?? null, filter.state ?? null, filter.limit ?? 100],
|
|
197
|
+
);
|
|
198
|
+
return rows.map(toJobRecord);
|
|
199
|
+
},
|
|
200
|
+
async deadLetters(limit = 100) {
|
|
201
|
+
const rows = await exec().query<JobRow>(
|
|
202
|
+
`select * from x_jobs where state = 'dead' order by updated_at desc limit $1`,
|
|
203
|
+
[limit],
|
|
204
|
+
);
|
|
205
|
+
return rows.map(toJobRecord);
|
|
206
|
+
},
|
|
207
|
+
async requeue(jobId, requeueOptions) {
|
|
208
|
+
if (requeueOptions?.fromStep !== undefined) {
|
|
209
|
+
const current = await this.job(jobId);
|
|
210
|
+
if (current !== undefined) {
|
|
211
|
+
await exec().query(`delete from x_job_steps where run_id = $1 and name = $2`, [
|
|
212
|
+
current.runId,
|
|
213
|
+
requeueOptions.fromStep,
|
|
214
|
+
]);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const rows = await exec().query<JobRow>(
|
|
218
|
+
`update x_jobs set state = 'ready', attempt = 0, run_at = now(), updated_at = now()
|
|
219
|
+
where id = $1 returning *`,
|
|
220
|
+
[jobId],
|
|
221
|
+
);
|
|
222
|
+
const row = rows[0];
|
|
223
|
+
if (row === undefined) {
|
|
224
|
+
throw new DriverUnavailableError({
|
|
225
|
+
driver: 'pg',
|
|
226
|
+
cause: `job ${jobId} does not exist`,
|
|
227
|
+
fix: 'x jobs list --state dead --json',
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
return toJobRecord(row);
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
name: 'pg',
|
|
236
|
+
steps: pgStepStore(exec),
|
|
237
|
+
introspect,
|
|
238
|
+
|
|
239
|
+
async enqueue(request: EnqueueRequest): Promise<EnqueueResult> {
|
|
240
|
+
const runAt = request.runAt ?? nowMs(clock);
|
|
241
|
+
const rows = await exec().query<{ id: string; run_id: string }>(SQL_ENQUEUE, [
|
|
242
|
+
uuid(),
|
|
243
|
+
request.name,
|
|
244
|
+
request.queue || DEFAULT_QUEUE,
|
|
245
|
+
JSON.stringify(request.input ?? null),
|
|
246
|
+
request.idempotencyKey,
|
|
247
|
+
request.runId ?? uuid(),
|
|
248
|
+
request.maxAttempts,
|
|
249
|
+
runAt,
|
|
250
|
+
request.tenantId ?? null,
|
|
251
|
+
]);
|
|
252
|
+
const inserted = rows[0];
|
|
253
|
+
if (inserted !== undefined) {
|
|
254
|
+
return { id: inserted.id, runId: inserted.run_id, deduped: false };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// `do nothing` fired: a live job already owns this idempotency key.
|
|
258
|
+
const existing = await exec().query<{ id: string; run_id: string }>(SQL_FIND_LIVE_BY_KEY, [
|
|
259
|
+
request.idempotencyKey,
|
|
260
|
+
]);
|
|
261
|
+
const found = existing[0];
|
|
262
|
+
if (found === undefined) {
|
|
263
|
+
throw new DriverUnavailableError({
|
|
264
|
+
driver: 'pg',
|
|
265
|
+
cause: `enqueue of "${request.name}" was rejected but no live row holds its idempotency key`,
|
|
266
|
+
fix: 'x db check — the x_jobs_idempotency_live_idx index is missing or stale',
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
if (request.onConflict === 'error') {
|
|
270
|
+
throw new JobDuplicateError({
|
|
271
|
+
job: request.name,
|
|
272
|
+
idempotencyKey: request.idempotencyKey,
|
|
273
|
+
existingId: found.id,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
return { id: found.id, runId: found.run_id, deduped: true };
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
async claim(claimOptions: ClaimOptions): Promise<readonly ClaimedJob[]> {
|
|
280
|
+
const queues = claimOptions.queues.length > 0 ? claimOptions.queues : [DEFAULT_QUEUE];
|
|
281
|
+
const rows = await exec().query<JobRow>(SQL_CLAIM, [
|
|
282
|
+
queues,
|
|
283
|
+
claimOptions.limit,
|
|
284
|
+
claimOptions.workerId,
|
|
285
|
+
claimOptions.visibilityTimeoutMs,
|
|
286
|
+
]);
|
|
287
|
+
const at = nowMs(clock);
|
|
288
|
+
return rows.map((row) => {
|
|
289
|
+
const record = toJobRecord(row);
|
|
290
|
+
return {
|
|
291
|
+
...record,
|
|
292
|
+
claimedAt: at,
|
|
293
|
+
visibleAt: record.visibleAt ?? at + claimOptions.visibilityTimeoutMs,
|
|
294
|
+
};
|
|
295
|
+
});
|
|
296
|
+
},
|
|
297
|
+
|
|
298
|
+
async ack(jobId: string): Promise<void> {
|
|
299
|
+
await exec().query(SQL_ACK, [jobId]);
|
|
300
|
+
},
|
|
301
|
+
|
|
302
|
+
async nack(jobId: string, nackOptions: NackOptions): Promise<void> {
|
|
303
|
+
const counts = nackOptions.countsAsAttempt !== false;
|
|
304
|
+
const state = nackOptions.deadLetter === true ? 'dead' : counts ? 'ready' : 'suspended';
|
|
305
|
+
await exec().query(SQL_NACK, [
|
|
306
|
+
jobId,
|
|
307
|
+
state,
|
|
308
|
+
counts,
|
|
309
|
+
nackOptions.delayMs,
|
|
310
|
+
nackOptions.error ?? null,
|
|
311
|
+
]);
|
|
312
|
+
},
|
|
313
|
+
|
|
314
|
+
async heartbeat(jobId: string, heartbeatOptions): Promise<void> {
|
|
315
|
+
await exec().query(SQL_HEARTBEAT, [jobId, heartbeatOptions.visibilityTimeoutMs]);
|
|
316
|
+
},
|
|
317
|
+
|
|
318
|
+
async stats(): Promise<readonly QueueStats[]> {
|
|
319
|
+
const rows = await exec().query<{
|
|
320
|
+
queue: string;
|
|
321
|
+
ready: number | string;
|
|
322
|
+
delayed: number | string;
|
|
323
|
+
running: number | string;
|
|
324
|
+
suspended: number | string;
|
|
325
|
+
dead: number | string;
|
|
326
|
+
oldest_ready_ms: number | string;
|
|
327
|
+
}>(SQL_STATS, []);
|
|
328
|
+
return rows.map((row) => ({
|
|
329
|
+
queue: row.queue,
|
|
330
|
+
ready: num(row.ready),
|
|
331
|
+
delayed: num(row.delayed),
|
|
332
|
+
running: num(row.running),
|
|
333
|
+
suspended: num(row.suspended),
|
|
334
|
+
dead: num(row.dead),
|
|
335
|
+
oldestReadyMs: Math.round(num(row.oldest_ready_ms)),
|
|
336
|
+
}));
|
|
337
|
+
},
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Advisory-lock leader election, used by the `scheduler` role. */
|
|
342
|
+
export function createPgLeader(
|
|
343
|
+
lockKey: number,
|
|
344
|
+
options: PgDriverOptions = {},
|
|
345
|
+
): { acquire(): Promise<boolean>; release(): Promise<void> } {
|
|
346
|
+
const exec = (): PgExecutor => resolveExecutor(options.executor);
|
|
347
|
+
return {
|
|
348
|
+
async acquire() {
|
|
349
|
+
const rows = await exec().query<{ locked: boolean }>(SQL_TRY_ADVISORY_LOCK, [lockKey]);
|
|
350
|
+
return rows[0]?.locked === true;
|
|
351
|
+
},
|
|
352
|
+
async release() {
|
|
353
|
+
await exec().query(SQL_ADVISORY_UNLOCK, [lockKey]);
|
|
354
|
+
},
|
|
355
|
+
};
|
|
356
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { JobDriver } from './driver';
|
|
2
|
+
export interface RedisDriverOptions {
|
|
3
|
+
readonly url?: string;
|
|
4
|
+
readonly consumerGroup?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function createRedisDriver(_options?: RedisDriverOptions): JobDriver;
|
|
7
|
+
//# sourceMappingURL=driver-redis.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"driver-redis.d.ts","sourceRoot":"","sources":["driver-redis.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAKV,SAAS,EAGV,MAAM,UAAU,CAAC;AA4BlB,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;CACjC;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,GAAE,kBAAuB,GAAG,SAAS,CAuB9E"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Redis driver — interface-complete, not implemented. Honest stub: correct types so an app
|
|
2
|
+
// can be written against it and `x verify` typechecks, with one labelled throw per method so
|
|
3
|
+
// nobody discovers the gap from a silently-dropped job.
|
|
4
|
+
//
|
|
5
|
+
// Design it will implement when written: a per-queue Redis Stream with a consumer group
|
|
6
|
+
// (XADD / XREADGROUP / XACK), `XAUTOCLAIM` for the visibility timeout, a ZSET for delayed
|
|
7
|
+
// and suspended runs, and step records in a hash keyed by run id.
|
|
8
|
+
import { JobsNotImplementedError } from './errors';
|
|
9
|
+
const FIX = 'use driver: "pg" (default) or "memory" — see docs/jobs/drivers.md#redis';
|
|
10
|
+
const unavailable = (method) => {
|
|
11
|
+
throw new JobsNotImplementedError({ feature: `redis jobs driver (${method})`, fix: FIX });
|
|
12
|
+
};
|
|
13
|
+
const redisStepStore = () => ({
|
|
14
|
+
get(_runId, _name) {
|
|
15
|
+
return unavailable('steps.get');
|
|
16
|
+
},
|
|
17
|
+
put(_record) {
|
|
18
|
+
return unavailable('steps.put');
|
|
19
|
+
},
|
|
20
|
+
list(_runId) {
|
|
21
|
+
return unavailable('steps.list');
|
|
22
|
+
},
|
|
23
|
+
del(_runId, _name) {
|
|
24
|
+
return unavailable('steps.del');
|
|
25
|
+
},
|
|
26
|
+
clear(_runId) {
|
|
27
|
+
return unavailable('steps.clear');
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
export function createRedisDriver(_options = {}) {
|
|
31
|
+
return {
|
|
32
|
+
name: 'redis',
|
|
33
|
+
steps: redisStepStore(),
|
|
34
|
+
enqueue(_request) {
|
|
35
|
+
return unavailable('enqueue');
|
|
36
|
+
},
|
|
37
|
+
claim(_options) {
|
|
38
|
+
return unavailable('claim');
|
|
39
|
+
},
|
|
40
|
+
ack(_jobId) {
|
|
41
|
+
return unavailable('ack');
|
|
42
|
+
},
|
|
43
|
+
nack(_jobId, _options) {
|
|
44
|
+
return unavailable('nack');
|
|
45
|
+
},
|
|
46
|
+
heartbeat(_jobId, _options) {
|
|
47
|
+
return unavailable('heartbeat');
|
|
48
|
+
},
|
|
49
|
+
stats() {
|
|
50
|
+
return unavailable('stats');
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=driver-redis.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"driver-redis.js","sourceRoot":"","sources":["driver-redis.ts"],"names":[],"mappings":"AAAA,2FAA2F;AAC3F,6FAA6F;AAC7F,wDAAwD;AACxD,EAAE;AACF,wFAAwF;AACxF,0FAA0F;AAC1F,kEAAkE;AAWlE,OAAO,EAAE,uBAAuB,EAAE,MAAM,UAAU,CAAC;AAGnD,MAAM,GAAG,GAAG,yEAAyE,CAAC;AAEtF,MAAM,WAAW,GAAG,CAAC,MAAc,EAAS,EAAE;IAC5C,MAAM,IAAI,uBAAuB,CAAC,EAAE,OAAO,EAAE,sBAAsB,MAAM,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC;AAC5F,CAAC,CAAC;AAEF,MAAM,cAAc,GAAG,GAAc,EAAE,CAAC,CAAC;IACvC,GAAG,CAAC,MAAc,EAAE,KAAa;QAC/B,OAAO,WAAW,CAAC,WAAW,CAAC,CAAC;IAClC,CAAC;IACD,GAAG,CAAC,OAAmB;QACrB,OAAO,WAAW,CAAC,WAAW,CAAC,CAAC;IAClC,CAAC;IACD,IAAI,CAAC,MAAc;QACjB,OAAO,WAAW,CAAC,YAAY,CAAC,CAAC;IACnC,CAAC;IACD,GAAG,CAAC,MAAc,EAAE,KAAa;QAC/B,OAAO,WAAW,CAAC,WAAW,CAAC,CAAC;IAClC,CAAC;IACD,KAAK,CAAC,MAAc;QAClB,OAAO,WAAW,CAAC,aAAa,CAAC,CAAC;IACpC,CAAC;CACF,CAAC,CAAC;AAOH,MAAM,UAAU,iBAAiB,CAAC,QAAQ,GAAuB,EAAE;IACjE,OAAO;QACL,IAAI,EAAE,OAAO;QACb,KAAK,EAAE,cAAc,EAAE;QACvB,OAAO,CAAC,QAAwB;YAC9B,OAAO,WAAW,CAAC,SAAS,CAAC,CAAC;QAChC,CAAC;QACD,KAAK,CAAC,QAAsB;YAC1B,OAAO,WAAW,CAAC,OAAO,CAAC,CAAC;QAC9B,CAAC;QACD,GAAG,CAAC,MAAc;YAChB,OAAO,WAAW,CAAC,KAAK,CAAC,CAAC;QAC5B,CAAC;QACD,IAAI,CAAC,MAAc,EAAE,QAAqB;YACxC,OAAO,WAAW,CAAC,MAAM,CAAC,CAAC;QAC7B,CAAC;QACD,SAAS,CAAC,MAAc,EAAE,QAAkD;YAC1E,OAAO,WAAW,CAAC,WAAW,CAAC,CAAC;QAClC,CAAC;QACD,KAAK;YACH,OAAO,WAAW,CAAC,OAAO,CAAC,CAAC;QAC9B,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Redis driver — interface-complete, not implemented. Honest stub: correct types so an app
|
|
2
|
+
// can be written against it and `x verify` typechecks, with one labelled throw per method so
|
|
3
|
+
// nobody discovers the gap from a silently-dropped job.
|
|
4
|
+
//
|
|
5
|
+
// Design it will implement when written: a per-queue Redis Stream with a consumer group
|
|
6
|
+
// (XADD / XREADGROUP / XACK), `XAUTOCLAIM` for the visibility timeout, a ZSET for delayed
|
|
7
|
+
// and suspended runs, and step records in a hash keyed by run id.
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
ClaimedJob,
|
|
11
|
+
ClaimOptions,
|
|
12
|
+
EnqueueRequest,
|
|
13
|
+
EnqueueResult,
|
|
14
|
+
JobDriver,
|
|
15
|
+
NackOptions,
|
|
16
|
+
QueueStats,
|
|
17
|
+
} from './driver';
|
|
18
|
+
import { JobsNotImplementedError } from './errors';
|
|
19
|
+
import type { StepRecord, StepStore } from './steps';
|
|
20
|
+
|
|
21
|
+
// Names the config edit that actually removes the stub, plus the runnable command for whatever
|
|
22
|
+
// is already queued. The redis driver lands in v2; there is no flag that turns this one on.
|
|
23
|
+
const FIX =
|
|
24
|
+
"set jobs: { driver: 'postgres' } in app.config.ts, then: x jobs drain --to memory --json";
|
|
25
|
+
|
|
26
|
+
const unavailable = (method: string): never => {
|
|
27
|
+
throw new JobsNotImplementedError({ feature: `redis jobs driver (${method})`, fix: FIX });
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const redisStepStore = (): StepStore => ({
|
|
31
|
+
get(_runId: string, _name: string): Promise<StepRecord | undefined> {
|
|
32
|
+
return unavailable('steps.get');
|
|
33
|
+
},
|
|
34
|
+
put(_record: StepRecord): Promise<void> {
|
|
35
|
+
return unavailable('steps.put');
|
|
36
|
+
},
|
|
37
|
+
list(_runId: string): Promise<readonly StepRecord[]> {
|
|
38
|
+
return unavailable('steps.list');
|
|
39
|
+
},
|
|
40
|
+
del(_runId: string, _name: string): Promise<void> {
|
|
41
|
+
return unavailable('steps.del');
|
|
42
|
+
},
|
|
43
|
+
clear(_runId: string): Promise<void> {
|
|
44
|
+
return unavailable('steps.clear');
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export interface RedisDriverOptions {
|
|
49
|
+
readonly url?: string;
|
|
50
|
+
readonly consumerGroup?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function createRedisDriver(_options: RedisDriverOptions = {}): JobDriver {
|
|
54
|
+
return {
|
|
55
|
+
name: 'redis',
|
|
56
|
+
steps: redisStepStore(),
|
|
57
|
+
enqueue(_request: EnqueueRequest): Promise<EnqueueResult> {
|
|
58
|
+
return unavailable('enqueue');
|
|
59
|
+
},
|
|
60
|
+
claim(_options: ClaimOptions): Promise<readonly ClaimedJob[]> {
|
|
61
|
+
return unavailable('claim');
|
|
62
|
+
},
|
|
63
|
+
ack(_jobId: string): Promise<void> {
|
|
64
|
+
return unavailable('ack');
|
|
65
|
+
},
|
|
66
|
+
nack(_jobId: string, _options: NackOptions): Promise<void> {
|
|
67
|
+
return unavailable('nack');
|
|
68
|
+
},
|
|
69
|
+
heartbeat(_jobId: string, _options: { readonly visibilityTimeoutMs: number }): Promise<void> {
|
|
70
|
+
return unavailable('heartbeat');
|
|
71
|
+
},
|
|
72
|
+
stats(): Promise<readonly QueueStats[]> {
|
|
73
|
+
return unavailable('stats');
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
package/src/driver.d.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { StepStore } from './steps';
|
|
2
|
+
export type JobState = 'ready' | 'delayed' | 'running' | 'suspended' | 'done' | 'failed' | 'dead';
|
|
3
|
+
export interface JobRecord {
|
|
4
|
+
readonly id: string;
|
|
5
|
+
readonly name: string;
|
|
6
|
+
readonly queue: string;
|
|
7
|
+
readonly input: unknown;
|
|
8
|
+
/** Dedupe key from the job definition. At-least-once delivery leans on this. */
|
|
9
|
+
readonly idempotencyKey: string;
|
|
10
|
+
/** Stable across retries and suspensions — the key every step record hangs off. */
|
|
11
|
+
readonly runId: string;
|
|
12
|
+
readonly attempt: number;
|
|
13
|
+
readonly maxAttempts: number;
|
|
14
|
+
readonly state: JobState;
|
|
15
|
+
/** Epoch ms; the job is invisible until then. */
|
|
16
|
+
readonly runAt: number;
|
|
17
|
+
readonly createdAt: number;
|
|
18
|
+
readonly updatedAt: number;
|
|
19
|
+
/** Actor's orgId, for per-tenant limits. */
|
|
20
|
+
readonly tenantId?: string;
|
|
21
|
+
readonly lastError?: string;
|
|
22
|
+
readonly claimedBy?: string;
|
|
23
|
+
readonly visibleAt?: number;
|
|
24
|
+
}
|
|
25
|
+
export type ConflictPolicy = 'dedupe' | 'error';
|
|
26
|
+
export interface EnqueueRequest {
|
|
27
|
+
readonly name: string;
|
|
28
|
+
readonly queue: string;
|
|
29
|
+
readonly input: unknown;
|
|
30
|
+
readonly idempotencyKey: string;
|
|
31
|
+
readonly maxAttempts: number;
|
|
32
|
+
/** Epoch ms. Omit for "now". */
|
|
33
|
+
readonly runAt?: number;
|
|
34
|
+
readonly tenantId?: string;
|
|
35
|
+
/** Reuse an existing run id when resuming, so step history is preserved. */
|
|
36
|
+
readonly runId?: string;
|
|
37
|
+
readonly onConflict?: ConflictPolicy;
|
|
38
|
+
}
|
|
39
|
+
export interface EnqueueResult {
|
|
40
|
+
readonly id: string;
|
|
41
|
+
readonly runId: string;
|
|
42
|
+
/** True when an in-flight job already held this idempotency key. */
|
|
43
|
+
readonly deduped: boolean;
|
|
44
|
+
}
|
|
45
|
+
export interface ClaimOptions {
|
|
46
|
+
readonly queues: readonly string[];
|
|
47
|
+
readonly limit: number;
|
|
48
|
+
/** Lease length. A worker that dies without ack makes the job claimable again after this. */
|
|
49
|
+
readonly visibilityTimeoutMs: number;
|
|
50
|
+
readonly workerId: string;
|
|
51
|
+
}
|
|
52
|
+
export interface ClaimedJob extends JobRecord {
|
|
53
|
+
readonly claimedAt: number;
|
|
54
|
+
readonly visibleAt: number;
|
|
55
|
+
}
|
|
56
|
+
export interface NackOptions {
|
|
57
|
+
/** Delay before the job becomes claimable again. */
|
|
58
|
+
readonly delayMs: number;
|
|
59
|
+
readonly error?: string;
|
|
60
|
+
/**
|
|
61
|
+
* False for a suspension (`step.sleep`): parking a run is not a failure and must not burn
|
|
62
|
+
* a retry attempt, or a 3-day sleep would dead-letter the job.
|
|
63
|
+
*/
|
|
64
|
+
readonly countsAsAttempt?: boolean;
|
|
65
|
+
readonly deadLetter?: boolean;
|
|
66
|
+
}
|
|
67
|
+
export interface QueueStats {
|
|
68
|
+
readonly queue: string;
|
|
69
|
+
readonly ready: number;
|
|
70
|
+
readonly delayed: number;
|
|
71
|
+
readonly running: number;
|
|
72
|
+
readonly suspended: number;
|
|
73
|
+
readonly dead: number;
|
|
74
|
+
/** Age in ms of the oldest claimable job — the number that decides autoscaling. */
|
|
75
|
+
readonly oldestReadyMs: number;
|
|
76
|
+
}
|
|
77
|
+
export interface JobFilter {
|
|
78
|
+
readonly queue?: string;
|
|
79
|
+
readonly name?: string;
|
|
80
|
+
readonly state?: JobState;
|
|
81
|
+
readonly limit?: number;
|
|
82
|
+
}
|
|
83
|
+
/** Optional: powers `/_x` and the MCP tools. A minimal driver may omit it. */
|
|
84
|
+
export interface JobIntrospection {
|
|
85
|
+
job(jobId: string): Promise<JobRecord | undefined>;
|
|
86
|
+
list(filter?: JobFilter): Promise<readonly JobRecord[]>;
|
|
87
|
+
deadLetters(limit?: number): Promise<readonly JobRecord[]>;
|
|
88
|
+
/** Re-queue a dead/failed job. `fromStep` drops step records from that step onward. */
|
|
89
|
+
requeue(jobId: string, options?: {
|
|
90
|
+
readonly fromStep?: string;
|
|
91
|
+
}): Promise<JobRecord>;
|
|
92
|
+
}
|
|
93
|
+
export interface JobDriver {
|
|
94
|
+
readonly name: string;
|
|
95
|
+
/** Step persistence lives with the queue: one store, one transaction boundary. */
|
|
96
|
+
readonly steps: StepStore;
|
|
97
|
+
enqueue(request: EnqueueRequest): Promise<EnqueueResult>;
|
|
98
|
+
claim(options: ClaimOptions): Promise<readonly ClaimedJob[]>;
|
|
99
|
+
ack(jobId: string): Promise<void>;
|
|
100
|
+
nack(jobId: string, options: NackOptions): Promise<void>;
|
|
101
|
+
/** Extends the lease of a long-running job so it is not double-claimed. */
|
|
102
|
+
heartbeat(jobId: string, options: {
|
|
103
|
+
readonly visibilityTimeoutMs: number;
|
|
104
|
+
}): Promise<void>;
|
|
105
|
+
stats(): Promise<readonly QueueStats[]>;
|
|
106
|
+
readonly introspect?: JobIntrospection;
|
|
107
|
+
close?(): Promise<void>;
|
|
108
|
+
}
|
|
109
|
+
export declare const DEFAULT_QUEUE = "default";
|
|
110
|
+
export declare const DEFAULT_VISIBILITY_TIMEOUT_MS = 30000;
|
|
111
|
+
/** Set once at boot from `app.config.ts`. Roles share one driver instance per process. */
|
|
112
|
+
export declare function setJobDriver(driver: JobDriver): void;
|
|
113
|
+
export declare function jobDriver(): JobDriver | undefined;
|
|
114
|
+
//# sourceMappingURL=driver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"driver.d.ts","sourceRoot":"","sources":["driver.ts"],"names":[],"mappings":"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"}
|
package/src/driver.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
export const DEFAULT_QUEUE = 'default';
|
|
5
|
+
export const DEFAULT_VISIBILITY_TIMEOUT_MS = 30_000;
|
|
6
|
+
let ambient;
|
|
7
|
+
/** Set once at boot from `app.config.ts`. Roles share one driver instance per process. */
|
|
8
|
+
export function setJobDriver(driver) {
|
|
9
|
+
ambient = driver;
|
|
10
|
+
}
|
|
11
|
+
export function jobDriver() {
|
|
12
|
+
return ambient;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=driver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"driver.js","sourceRoot":"","sources":["driver.ts"],"names":[],"mappings":"AAAA,yFAAyF;AACzF,yFAAyF;AACzF,6FAA6F;AAuH7F,MAAM,CAAC,MAAM,aAAa,GAAG,SAAS,CAAC;AACvC,MAAM,CAAC,MAAM,6BAA6B,GAAG,MAAM,CAAC;AAEpD,IAAI,OAA8B,CAAC;AAEnC,0FAA0F;AAC1F,MAAM,UAAU,YAAY,CAAC,MAAiB;IAC5C,OAAO,GAAG,MAAM,CAAC;AACnB,CAAC;AAED,MAAM,UAAU,SAAS;IACvB,OAAO,OAAO,CAAC;AACjB,CAAC"}
|