@zucker-framework/task 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/dist/index.js ADDED
@@ -0,0 +1,1933 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/index.ts
22
+ var index_exports = {};
23
+ __export(index_exports, {
24
+ DurableTaskLeaseLostError: () => DurableTaskLeaseLostError,
25
+ DurableTaskNotClaimableError: () => DurableTaskNotClaimableError,
26
+ DurableTaskNotFoundError: () => DurableTaskNotFoundError,
27
+ DurableTaskRetryScheduledError: () => DurableTaskRetryScheduledError,
28
+ DurableWorkerRuntime: () => DurableWorkerRuntime,
29
+ ExponentialTaskRetryPolicy: () => ExponentialTaskRetryPolicy,
30
+ InMemoryTaskOutboxRepository: () => InMemoryTaskOutboxRepository,
31
+ InMemoryTaskRepository: () => InMemoryTaskRepository,
32
+ InvalidTaskTransitionError: () => InvalidTaskTransitionError,
33
+ PostgresTaskOutboxRepository: () => PostgresTaskOutboxRepository,
34
+ PostgresTaskRedriver: () => PostgresTaskRedriver,
35
+ PostgresTaskRepository: () => PostgresTaskRepository,
36
+ PostgresTaskSubmitter: () => PostgresTaskSubmitter,
37
+ PostgresTaskSummaryReader: () => PostgresTaskSummaryReader,
38
+ PostgresTaskTimelineReader: () => PostgresTaskTimelineReader,
39
+ TASK_STATUSES: () => TASK_STATUSES,
40
+ TASK_TIMELINE_EVENT_TYPES: () => TASK_TIMELINE_EVENT_TYPES,
41
+ TaskIdempotencyConflictError: () => TaskIdempotencyConflictError,
42
+ TaskLeaseReconciler: () => TaskLeaseReconciler,
43
+ TaskOutboxConflictError: () => TaskOutboxConflictError,
44
+ TaskOutboxDispatcher: () => TaskOutboxDispatcher,
45
+ TaskQueueReconciler: () => TaskQueueReconciler,
46
+ UnsupportedDurableTaskKindError: () => UnsupportedDurableTaskKindError,
47
+ assertTaskTransition: () => assertTaskTransition,
48
+ canTransitionTask: () => canTransitionTask,
49
+ createTaskFingerprint: () => createTaskFingerprint,
50
+ isTerminalTaskStatus: () => isTerminalTaskStatus,
51
+ requiresTaskLease: () => requiresTaskLease
52
+ });
53
+ module.exports = __toCommonJS(index_exports);
54
+
55
+ // src/task-state-machine.ts
56
+ var TASK_STATUSES = [
57
+ "QUEUED",
58
+ "PROCESSING",
59
+ "WAITING_PROVIDER",
60
+ "RETRYING",
61
+ "COMPLETED",
62
+ "FAILED",
63
+ "CANCELLED"
64
+ ];
65
+ var transitions = {
66
+ QUEUED: [
67
+ "PROCESSING",
68
+ "CANCELLED"
69
+ ],
70
+ PROCESSING: [
71
+ "WAITING_PROVIDER",
72
+ "RETRYING",
73
+ "COMPLETED",
74
+ "FAILED",
75
+ "CANCELLED"
76
+ ],
77
+ WAITING_PROVIDER: [
78
+ "PROCESSING",
79
+ "RETRYING",
80
+ "FAILED",
81
+ "CANCELLED"
82
+ ],
83
+ RETRYING: [
84
+ "QUEUED",
85
+ "FAILED",
86
+ "CANCELLED"
87
+ ],
88
+ COMPLETED: [],
89
+ FAILED: [],
90
+ CANCELLED: []
91
+ };
92
+ var InvalidTaskTransitionError = class extends Error {
93
+ static {
94
+ __name(this, "InvalidTaskTransitionError");
95
+ }
96
+ from;
97
+ to;
98
+ constructor(from, to) {
99
+ super(`Invalid task transition: ${from} -> ${to}`), this.from = from, this.to = to;
100
+ this.name = "InvalidTaskTransitionError";
101
+ }
102
+ };
103
+ function canTransitionTask(from, to) {
104
+ return transitions[from].includes(to);
105
+ }
106
+ __name(canTransitionTask, "canTransitionTask");
107
+ function assertTaskTransition(from, to) {
108
+ if (!canTransitionTask(from, to)) {
109
+ throw new InvalidTaskTransitionError(from, to);
110
+ }
111
+ }
112
+ __name(assertTaskTransition, "assertTaskTransition");
113
+ function isTerminalTaskStatus(status2) {
114
+ return status2 === "COMPLETED" || status2 === "FAILED" || status2 === "CANCELLED";
115
+ }
116
+ __name(isTerminalTaskStatus, "isTerminalTaskStatus");
117
+ function requiresTaskLease(status2) {
118
+ return status2 === "PROCESSING" || status2 === "WAITING_PROVIDER";
119
+ }
120
+ __name(requiresTaskLease, "requiresTaskLease");
121
+
122
+ // src/task-fingerprint.ts
123
+ var import_node_crypto = require("crypto");
124
+ function serialize(value, seen) {
125
+ if (value === null) return "null";
126
+ switch (typeof value) {
127
+ case "string":
128
+ case "boolean":
129
+ return JSON.stringify(value);
130
+ case "number":
131
+ if (!Number.isFinite(value)) {
132
+ throw new TypeError("Task fingerprint input cannot contain non-finite numbers");
133
+ }
134
+ return JSON.stringify(value);
135
+ case "undefined":
136
+ return void 0;
137
+ case "bigint":
138
+ case "function":
139
+ case "symbol":
140
+ throw new TypeError(`Task fingerprint input cannot contain ${typeof value}`);
141
+ case "object":
142
+ break;
143
+ }
144
+ if (value instanceof Date) return JSON.stringify(value.toISOString());
145
+ if (seen.has(value)) throw new TypeError("Task fingerprint input cannot contain circular references");
146
+ seen.add(value);
147
+ try {
148
+ if (Array.isArray(value)) {
149
+ return `[${value.map((entry) => serialize(entry, seen) ?? "null").join(",")}]`;
150
+ }
151
+ const prototype = Object.getPrototypeOf(value);
152
+ if (prototype !== Object.prototype && prototype !== null) {
153
+ throw new TypeError("Task fingerprint input must contain only JSON-compatible objects");
154
+ }
155
+ const entries = Object.keys(value).sort().flatMap((key) => {
156
+ const serialized = serialize(value[key], seen);
157
+ return serialized === void 0 ? [] : [
158
+ `${JSON.stringify(key)}:${serialized}`
159
+ ];
160
+ });
161
+ return `{${entries.join(",")}}`;
162
+ } finally {
163
+ seen.delete(value);
164
+ }
165
+ }
166
+ __name(serialize, "serialize");
167
+ function createTaskFingerprint(value) {
168
+ const canonical = serialize(value, /* @__PURE__ */ new Set());
169
+ if (canonical === void 0) {
170
+ throw new TypeError("Task fingerprint input cannot be undefined");
171
+ }
172
+ return `sha256:${(0, import_node_crypto.createHash)("sha256").update(canonical).digest("hex")}`;
173
+ }
174
+ __name(createTaskFingerprint, "createTaskFingerprint");
175
+
176
+ // src/task-repository.ts
177
+ var TaskIdempotencyConflictError = class extends Error {
178
+ static {
179
+ __name(this, "TaskIdempotencyConflictError");
180
+ }
181
+ idempotencyKey;
182
+ constructor(idempotencyKey) {
183
+ super(`Idempotency key was reused with a different request: ${idempotencyKey}`), this.idempotencyKey = idempotencyKey;
184
+ this.name = "TaskIdempotencyConflictError";
185
+ }
186
+ };
187
+
188
+ // src/in-memory-task-repository.ts
189
+ function cloneTask(task) {
190
+ return {
191
+ ...task,
192
+ resource: {
193
+ ...task.resource
194
+ },
195
+ error: task.error ? {
196
+ ...task.error,
197
+ metadata: task.error.metadata ? {
198
+ ...task.error.metadata
199
+ } : void 0
200
+ } : void 0,
201
+ availableAt: new Date(task.availableAt),
202
+ createdAt: new Date(task.createdAt),
203
+ updatedAt: new Date(task.updatedAt),
204
+ startedAt: task.startedAt ? new Date(task.startedAt) : void 0,
205
+ completedAt: task.completedAt ? new Date(task.completedAt) : void 0,
206
+ leaseExpiresAt: task.leaseExpiresAt ? new Date(task.leaseExpiresAt) : void 0,
207
+ heartbeatAt: task.heartbeatAt ? new Date(task.heartbeatAt) : void 0
208
+ };
209
+ }
210
+ __name(cloneTask, "cloneTask");
211
+ function clampProgress(progress) {
212
+ return Math.max(0, Math.min(100, progress));
213
+ }
214
+ __name(clampProgress, "clampProgress");
215
+ var InMemoryTaskRepository = class {
216
+ static {
217
+ __name(this, "InMemoryTaskRepository");
218
+ }
219
+ tasks = /* @__PURE__ */ new Map();
220
+ idempotencyIndex = /* @__PURE__ */ new Map();
221
+ async create(input) {
222
+ const existingId = this.idempotencyIndex.get(input.idempotencyKey);
223
+ if (existingId) {
224
+ const existing = this.tasks.get(existingId);
225
+ if (!existing) throw new Error(`Task index is corrupted for key ${input.idempotencyKey}`);
226
+ if (existing.idempotencyFingerprint !== input.idempotencyFingerprint) {
227
+ throw new TaskIdempotencyConflictError(input.idempotencyKey);
228
+ }
229
+ return {
230
+ task: cloneTask(existing),
231
+ created: false
232
+ };
233
+ }
234
+ if (this.tasks.has(input.id)) throw new Error(`Task already exists: ${input.id}`);
235
+ if (input.maxAttempts < 1) throw new RangeError("maxAttempts must be at least 1");
236
+ const task = {
237
+ id: input.id,
238
+ kind: input.kind,
239
+ resource: {
240
+ ...input.resource
241
+ },
242
+ userId: input.userId,
243
+ orderId: input.orderId,
244
+ status: "QUEUED",
245
+ progress: 0,
246
+ attempt: 0,
247
+ maxAttempts: input.maxAttempts,
248
+ idempotencyKey: input.idempotencyKey,
249
+ idempotencyFingerprint: input.idempotencyFingerprint,
250
+ handlerVersion: input.handlerVersion,
251
+ payload: input.payload,
252
+ availableAt: new Date(input.availableAt ?? input.now),
253
+ createdAt: new Date(input.now),
254
+ updatedAt: new Date(input.now),
255
+ version: 0
256
+ };
257
+ this.tasks.set(task.id, task);
258
+ this.idempotencyIndex.set(task.idempotencyKey, task.id);
259
+ return {
260
+ task: cloneTask(task),
261
+ created: true
262
+ };
263
+ }
264
+ async findById(id) {
265
+ const task = this.tasks.get(id);
266
+ return task ? cloneTask(task) : null;
267
+ }
268
+ async findByIdempotencyKey(key) {
269
+ const id = this.idempotencyIndex.get(key);
270
+ return id ? this.findById(id) : null;
271
+ }
272
+ async claim(input) {
273
+ const task = this.tasks.get(input.id);
274
+ if (!task || task.status !== "QUEUED" || task.availableAt.getTime() > input.now.getTime() || task.attempt >= task.maxAttempts || input.leaseExpiresAt.getTime() <= input.now.getTime()) {
275
+ return null;
276
+ }
277
+ task.status = "PROCESSING";
278
+ task.workerId = input.workerId;
279
+ task.leaseToken = input.leaseToken;
280
+ task.leaseExpiresAt = new Date(input.leaseExpiresAt);
281
+ task.heartbeatAt = new Date(input.now);
282
+ task.startedAt ??= new Date(input.now);
283
+ task.attempt += 1;
284
+ task.updatedAt = new Date(input.now);
285
+ task.version += 1;
286
+ return cloneTask(task);
287
+ }
288
+ async heartbeat(input) {
289
+ const task = this.tasks.get(input.id);
290
+ if (!task || !requiresTaskLease(task.status) || task.leaseToken !== input.leaseToken || !task.leaseExpiresAt || task.leaseExpiresAt.getTime() <= input.now.getTime() || input.leaseExpiresAt.getTime() <= input.now.getTime()) {
291
+ return false;
292
+ }
293
+ task.heartbeatAt = new Date(input.now);
294
+ task.leaseExpiresAt = new Date(input.leaseExpiresAt);
295
+ task.updatedAt = new Date(input.now);
296
+ task.version += 1;
297
+ return true;
298
+ }
299
+ async reportProgress(input) {
300
+ const task = this.tasks.get(input.id);
301
+ if (!task || !requiresTaskLease(task.status) || task.leaseToken !== input.leaseToken || !task.leaseExpiresAt || task.leaseExpiresAt.getTime() <= input.now.getTime()) {
302
+ return null;
303
+ }
304
+ task.stage = input.stage ?? task.stage;
305
+ task.progress = Math.max(task.progress, clampProgress(input.progress));
306
+ task.updatedAt = new Date(input.now);
307
+ task.version += 1;
308
+ return cloneTask(task);
309
+ }
310
+ async transition(input) {
311
+ const task = this.tasks.get(input.id);
312
+ if (!task || task.status !== input.from) return null;
313
+ assertTaskTransition(input.from, input.to);
314
+ if (input.from === "QUEUED" && input.to === "PROCESSING") return null;
315
+ if (requiresTaskLease(input.from)) {
316
+ if (!input.leaseToken || task.leaseToken !== input.leaseToken || !task.leaseExpiresAt || task.leaseExpiresAt.getTime() <= input.now.getTime()) {
317
+ return null;
318
+ }
319
+ }
320
+ task.status = input.to;
321
+ task.stage = input.stage ?? task.stage;
322
+ task.progress = input.to === "COMPLETED" ? 100 : clampProgress(input.progress ?? task.progress);
323
+ task.result = input.result ?? task.result;
324
+ task.error = input.error;
325
+ task.providerRequestId = input.providerRequestId ?? task.providerRequestId;
326
+ task.availableAt = new Date(input.availableAt ?? task.availableAt);
327
+ task.updatedAt = new Date(input.now);
328
+ task.version += 1;
329
+ if (input.to === "QUEUED" || input.to === "RETRYING" || isTerminalTaskStatus(input.to)) {
330
+ task.workerId = void 0;
331
+ task.leaseToken = void 0;
332
+ task.leaseExpiresAt = void 0;
333
+ task.heartbeatAt = void 0;
334
+ }
335
+ if (isTerminalTaskStatus(input.to)) task.completedAt = new Date(input.now);
336
+ return cloneTask(task);
337
+ }
338
+ async findExpiredLeases(input) {
339
+ if (input.limit < 1) return [];
340
+ return [
341
+ ...this.tasks.values()
342
+ ].filter((task) => requiresTaskLease(task.status) && Boolean(task.leaseExpiresAt && task.leaseExpiresAt.getTime() <= input.now.getTime())).sort((a, b) => (a.leaseExpiresAt?.getTime() ?? 0) - (b.leaseExpiresAt?.getTime() ?? 0)).slice(0, input.limit).map(cloneTask);
343
+ }
344
+ async findReadyQueued(input) {
345
+ if (input.limit < 1) return [];
346
+ return [
347
+ ...this.tasks.values()
348
+ ].filter((task) => task.status === "QUEUED" && task.availableAt.getTime() <= input.now.getTime() && task.attempt < task.maxAttempts).sort((a, b) => a.availableAt.getTime() - b.availableAt.getTime()).slice(input.offset ?? 0, (input.offset ?? 0) + input.limit).map(cloneTask);
349
+ }
350
+ async recoverExpiredLease(input) {
351
+ const task = this.tasks.get(input.id);
352
+ if (!task || !requiresTaskLease(task.status) || task.leaseToken !== input.leaseToken || !task.leaseExpiresAt || task.leaseExpiresAt.getTime() > input.now.getTime()) {
353
+ return null;
354
+ }
355
+ const attemptsExhausted = task.attempt >= task.maxAttempts;
356
+ task.status = attemptsExhausted ? "FAILED" : "QUEUED";
357
+ task.error = input.error;
358
+ task.availableAt = new Date(input.availableAt ?? input.now);
359
+ task.workerId = void 0;
360
+ task.leaseToken = void 0;
361
+ task.leaseExpiresAt = void 0;
362
+ task.heartbeatAt = void 0;
363
+ task.updatedAt = new Date(input.now);
364
+ task.version += 1;
365
+ if (attemptsExhausted) task.completedAt = new Date(input.now);
366
+ return cloneTask(task);
367
+ }
368
+ async cancel(id, now) {
369
+ const task = this.tasks.get(id);
370
+ if (!task || isTerminalTaskStatus(task.status)) return null;
371
+ task.status = "CANCELLED";
372
+ task.workerId = void 0;
373
+ task.leaseToken = void 0;
374
+ task.leaseExpiresAt = void 0;
375
+ task.heartbeatAt = void 0;
376
+ task.completedAt = new Date(now);
377
+ task.updatedAt = new Date(now);
378
+ task.version += 1;
379
+ return cloneTask(task);
380
+ }
381
+ };
382
+
383
+ // src/postgres-task-repository.ts
384
+ var CREATE_TASK_SQL = `
385
+ WITH inserted AS (
386
+ INSERT INTO async_jobs (
387
+ "id", "kind", "resourceType", "resourceId", "userId", "orderId",
388
+ "status", "progress", "attempt", "maxAttempts", "idempotencyKey",
389
+ "idempotencyFingerprint", "handlerVersion", "payload", "availableAt",
390
+ "createdAt", "updatedAt", "version"
391
+ ) VALUES (
392
+ $1, $2, $3, $4, $5, $6,
393
+ 'QUEUED', 0, 0, $7, $8,
394
+ $9, $10, $11::jsonb, $12,
395
+ $13, $13, 0
396
+ )
397
+ ON CONFLICT ("idempotencyKey") DO NOTHING
398
+ RETURNING *
399
+ )
400
+ SELECT inserted.*, TRUE AS "__created"
401
+ FROM inserted
402
+ UNION ALL
403
+ SELECT existing.*, FALSE AS "__created"
404
+ FROM async_jobs AS existing
405
+ WHERE existing."idempotencyKey" = $8
406
+ AND NOT EXISTS (SELECT 1 FROM inserted)
407
+ LIMIT 1
408
+ `;
409
+ var CLAIM_TASK_SQL = `
410
+ UPDATE async_jobs
411
+ SET
412
+ "status" = 'PROCESSING',
413
+ "workerId" = $2,
414
+ "leaseToken" = $3,
415
+ "leaseExpiresAt" = $4,
416
+ "heartbeatAt" = $5,
417
+ "startedAt" = COALESCE("startedAt", $5),
418
+ "attempt" = "attempt" + 1,
419
+ "updatedAt" = $5,
420
+ "version" = "version" + 1
421
+ WHERE "id" = $1
422
+ AND "status" = 'QUEUED'
423
+ AND "availableAt" <= $5
424
+ AND "attempt" < "maxAttempts"
425
+ AND $4 > $5
426
+ RETURNING *
427
+ `;
428
+ var HEARTBEAT_TASK_SQL = `
429
+ UPDATE async_jobs
430
+ SET
431
+ "heartbeatAt" = $4,
432
+ "leaseExpiresAt" = $3,
433
+ "updatedAt" = $4,
434
+ "version" = "version" + 1
435
+ WHERE "id" = $1
436
+ AND "status" IN ('PROCESSING', 'WAITING_PROVIDER')
437
+ AND "leaseToken" = $2
438
+ AND "leaseExpiresAt" > $4
439
+ AND $3 > $4
440
+ RETURNING "id"
441
+ `;
442
+ var REPORT_TASK_PROGRESS_SQL = `
443
+ UPDATE async_jobs
444
+ SET
445
+ "stage" = COALESCE($3::text, "stage"),
446
+ "progress" = GREATEST("progress", GREATEST(0, LEAST(100, $4::integer))),
447
+ "updatedAt" = $5,
448
+ "version" = "version" + 1
449
+ WHERE "id" = $1
450
+ AND "status" IN ('PROCESSING', 'WAITING_PROVIDER')
451
+ AND "leaseToken" = $2
452
+ AND "leaseExpiresAt" > $5
453
+ RETURNING *
454
+ `;
455
+ var TRANSITION_TASK_SQL = `
456
+ UPDATE async_jobs
457
+ SET
458
+ "status" = $3::async_job_status,
459
+ "stage" = COALESCE($5::text, "stage"),
460
+ "progress" = CASE
461
+ WHEN $3 = 'COMPLETED' THEN 100
462
+ WHEN $6::integer IS NULL THEN "progress"
463
+ ELSE GREATEST(0, LEAST(100, $6::integer))
464
+ END,
465
+ "result" = COALESCE($7::jsonb, "result"),
466
+ "errorCode" = $8::text,
467
+ "errorCategory" = $9::text,
468
+ "errorSummary" = $10::text,
469
+ "errorRetryable" = $11::boolean,
470
+ "errorMetadata" = $12::jsonb,
471
+ "providerRequestId" = COALESCE($13::text, "providerRequestId"),
472
+ "availableAt" = COALESCE($14::timestamptz, "availableAt"),
473
+ "workerId" = CASE WHEN $16::boolean THEN NULL ELSE "workerId" END,
474
+ "leaseToken" = CASE WHEN $16::boolean THEN NULL ELSE "leaseToken" END,
475
+ "leaseExpiresAt" = CASE WHEN $16::boolean THEN NULL ELSE "leaseExpiresAt" END,
476
+ "heartbeatAt" = CASE WHEN $16::boolean THEN NULL ELSE "heartbeatAt" END,
477
+ "completedAt" = CASE WHEN $17::boolean THEN $15::timestamptz ELSE "completedAt" END,
478
+ "updatedAt" = $15::timestamptz,
479
+ "version" = "version" + 1
480
+ WHERE "id" = $1
481
+ AND "status" = $2::async_job_status
482
+ AND (
483
+ NOT $18::boolean
484
+ OR (
485
+ $4::text IS NOT NULL
486
+ AND "leaseToken" = $4::text
487
+ AND "leaseExpiresAt" > $15::timestamptz
488
+ )
489
+ )
490
+ RETURNING *
491
+ `;
492
+ var FIND_EXPIRED_LEASES_SQL = `
493
+ SELECT *
494
+ FROM async_jobs
495
+ WHERE "status" IN ('PROCESSING', 'WAITING_PROVIDER')
496
+ AND "leaseExpiresAt" <= $1
497
+ ORDER BY "leaseExpiresAt" ASC
498
+ LIMIT $2
499
+ `;
500
+ var FIND_READY_QUEUED_TASKS_SQL = `
501
+ SELECT *
502
+ FROM async_jobs
503
+ WHERE "status" = 'QUEUED'
504
+ AND "availableAt" <= $1
505
+ AND "attempt" < "maxAttempts"
506
+ ORDER BY "availableAt" ASC, "createdAt" ASC
507
+ LIMIT $2
508
+ OFFSET $3
509
+ `;
510
+ var RECOVER_EXPIRED_LEASE_SQL = `
511
+ UPDATE async_jobs
512
+ SET
513
+ "status" = CASE
514
+ WHEN "attempt" >= "maxAttempts" THEN 'FAILED'::async_job_status
515
+ ELSE 'QUEUED'::async_job_status
516
+ END,
517
+ "errorCode" = $4,
518
+ "errorCategory" = $5,
519
+ "errorSummary" = $6,
520
+ "errorRetryable" = $7,
521
+ "errorMetadata" = $8::jsonb,
522
+ "availableAt" = COALESCE($9, $3),
523
+ "workerId" = NULL,
524
+ "leaseToken" = NULL,
525
+ "leaseExpiresAt" = NULL,
526
+ "heartbeatAt" = NULL,
527
+ "completedAt" = CASE
528
+ WHEN "attempt" >= "maxAttempts" THEN $3
529
+ ELSE "completedAt"
530
+ END,
531
+ "updatedAt" = $3,
532
+ "version" = "version" + 1
533
+ WHERE "id" = $1
534
+ AND "status" IN ('PROCESSING', 'WAITING_PROVIDER')
535
+ AND "leaseToken" = $2
536
+ AND "leaseExpiresAt" <= $3
537
+ RETURNING *
538
+ `;
539
+ var CANCEL_TASK_SQL = `
540
+ UPDATE async_jobs
541
+ SET
542
+ "status" = 'CANCELLED',
543
+ "workerId" = NULL,
544
+ "leaseToken" = NULL,
545
+ "leaseExpiresAt" = NULL,
546
+ "heartbeatAt" = NULL,
547
+ "completedAt" = $2,
548
+ "updatedAt" = $2,
549
+ "version" = "version" + 1
550
+ WHERE "id" = $1
551
+ AND "status" NOT IN ('COMPLETED', 'FAILED', 'CANCELLED')
552
+ RETURNING *
553
+ `;
554
+ function jsonParameter(value) {
555
+ return value === void 0 ? null : JSON.stringify(value);
556
+ }
557
+ __name(jsonParameter, "jsonParameter");
558
+ function parseJson(value) {
559
+ if (typeof value !== "string") return value;
560
+ return JSON.parse(value);
561
+ }
562
+ __name(parseJson, "parseJson");
563
+ function optionalDate(value) {
564
+ return value === null ? void 0 : new Date(value);
565
+ }
566
+ __name(optionalDate, "optionalDate");
567
+ function taskStatus(value) {
568
+ if (TASK_STATUSES.includes(value)) return value;
569
+ throw new Error(`Unknown task status returned by PostgreSQL: ${value}`);
570
+ }
571
+ __name(taskStatus, "taskStatus");
572
+ function taskError(row) {
573
+ if (!row.errorCode) return void 0;
574
+ return {
575
+ code: row.errorCode,
576
+ category: row.errorCategory ?? "UNKNOWN",
577
+ summary: row.errorSummary ?? row.errorCode,
578
+ retryable: row.errorRetryable ?? false,
579
+ metadata: row.errorMetadata ? parseJson(row.errorMetadata) : void 0
580
+ };
581
+ }
582
+ __name(taskError, "taskError");
583
+ var PostgresTaskRepository = class {
584
+ static {
585
+ __name(this, "PostgresTaskRepository");
586
+ }
587
+ client;
588
+ constructor(client) {
589
+ this.client = client;
590
+ }
591
+ async create(input) {
592
+ if (input.maxAttempts < 1) throw new RangeError("maxAttempts must be at least 1");
593
+ const result = await this.client.query(CREATE_TASK_SQL, [
594
+ input.id,
595
+ input.kind,
596
+ input.resource.type,
597
+ input.resource.id,
598
+ input.userId ?? null,
599
+ input.orderId ?? null,
600
+ input.maxAttempts,
601
+ input.idempotencyKey,
602
+ input.idempotencyFingerprint,
603
+ input.handlerVersion,
604
+ jsonParameter(input.payload),
605
+ input.availableAt ?? input.now,
606
+ input.now
607
+ ]);
608
+ const row = result.rows[0];
609
+ if (!row) {
610
+ const existing = await this.findByIdempotencyKey(input.idempotencyKey);
611
+ if (!existing) throw new Error(`Task already exists with a different id: ${input.id}`);
612
+ if (existing.idempotencyFingerprint !== input.idempotencyFingerprint) {
613
+ throw new TaskIdempotencyConflictError(input.idempotencyKey);
614
+ }
615
+ return {
616
+ task: existing,
617
+ created: false
618
+ };
619
+ }
620
+ if (row.idempotencyFingerprint !== input.idempotencyFingerprint) {
621
+ throw new TaskIdempotencyConflictError(input.idempotencyKey);
622
+ }
623
+ return {
624
+ task: this.toTask(row),
625
+ created: row.__created
626
+ };
627
+ }
628
+ async findById(id) {
629
+ const result = await this.client.query('SELECT * FROM async_jobs WHERE "id" = $1 LIMIT 1', [
630
+ id
631
+ ]);
632
+ return result.rows[0] ? this.toTask(result.rows[0]) : null;
633
+ }
634
+ async findByIdempotencyKey(key) {
635
+ const result = await this.client.query('SELECT * FROM async_jobs WHERE "idempotencyKey" = $1 LIMIT 1', [
636
+ key
637
+ ]);
638
+ return result.rows[0] ? this.toTask(result.rows[0]) : null;
639
+ }
640
+ async claim(input) {
641
+ const result = await this.client.query(CLAIM_TASK_SQL, [
642
+ input.id,
643
+ input.workerId,
644
+ input.leaseToken,
645
+ input.leaseExpiresAt,
646
+ input.now
647
+ ]);
648
+ return result.rows[0] ? this.toTask(result.rows[0]) : null;
649
+ }
650
+ async heartbeat(input) {
651
+ const result = await this.client.query(HEARTBEAT_TASK_SQL, [
652
+ input.id,
653
+ input.leaseToken,
654
+ input.leaseExpiresAt,
655
+ input.now
656
+ ]);
657
+ return result.rows.length === 1;
658
+ }
659
+ async reportProgress(input) {
660
+ const result = await this.client.query(REPORT_TASK_PROGRESS_SQL, [
661
+ input.id,
662
+ input.leaseToken,
663
+ input.stage ?? null,
664
+ input.progress,
665
+ input.now
666
+ ]);
667
+ return result.rows[0] ? this.toTask(result.rows[0]) : null;
668
+ }
669
+ async transition(input) {
670
+ assertTaskTransition(input.from, input.to);
671
+ if (input.from === "QUEUED" && input.to === "PROCESSING") return null;
672
+ const clearsLease = input.to === "QUEUED" || input.to === "RETRYING" || isTerminalTaskStatus(input.to);
673
+ const error = input.error;
674
+ const result = await this.client.query(TRANSITION_TASK_SQL, [
675
+ input.id,
676
+ input.from,
677
+ input.to,
678
+ input.leaseToken ?? null,
679
+ input.stage ?? null,
680
+ input.progress ?? null,
681
+ jsonParameter(input.result),
682
+ error?.code ?? null,
683
+ error?.category ?? null,
684
+ error?.summary ?? null,
685
+ error?.retryable ?? null,
686
+ jsonParameter(error?.metadata),
687
+ input.providerRequestId ?? null,
688
+ input.availableAt ?? null,
689
+ input.now,
690
+ clearsLease,
691
+ isTerminalTaskStatus(input.to),
692
+ requiresTaskLease(input.from)
693
+ ]);
694
+ return result.rows[0] ? this.toTask(result.rows[0]) : null;
695
+ }
696
+ async findExpiredLeases(input) {
697
+ if (input.limit < 1) return [];
698
+ const result = await this.client.query(FIND_EXPIRED_LEASES_SQL, [
699
+ input.now,
700
+ input.limit
701
+ ]);
702
+ return result.rows.map((row) => this.toTask(row));
703
+ }
704
+ async findReadyQueued(input) {
705
+ if (input.limit < 1) return [];
706
+ const result = await this.client.query(FIND_READY_QUEUED_TASKS_SQL, [
707
+ input.now,
708
+ input.limit,
709
+ input.offset ?? 0
710
+ ]);
711
+ return result.rows.map((row) => this.toTask(row));
712
+ }
713
+ async recoverExpiredLease(input) {
714
+ const result = await this.client.query(RECOVER_EXPIRED_LEASE_SQL, [
715
+ input.id,
716
+ input.leaseToken,
717
+ input.now,
718
+ input.error.code,
719
+ input.error.category,
720
+ input.error.summary,
721
+ input.error.retryable,
722
+ jsonParameter(input.error.metadata),
723
+ input.availableAt ?? null
724
+ ]);
725
+ return result.rows[0] ? this.toTask(result.rows[0]) : null;
726
+ }
727
+ async cancel(id, now) {
728
+ const result = await this.client.query(CANCEL_TASK_SQL, [
729
+ id,
730
+ now
731
+ ]);
732
+ return result.rows[0] ? this.toTask(result.rows[0]) : null;
733
+ }
734
+ toTask(row) {
735
+ return {
736
+ id: row.id,
737
+ kind: row.kind,
738
+ resource: {
739
+ type: row.resourceType,
740
+ id: row.resourceId
741
+ },
742
+ userId: row.userId ?? void 0,
743
+ orderId: row.orderId ?? void 0,
744
+ status: taskStatus(row.status),
745
+ stage: row.stage ?? void 0,
746
+ progress: row.progress,
747
+ attempt: row.attempt,
748
+ maxAttempts: row.maxAttempts,
749
+ idempotencyKey: row.idempotencyKey,
750
+ idempotencyFingerprint: row.idempotencyFingerprint,
751
+ handlerVersion: row.handlerVersion,
752
+ payload: parseJson(row.payload),
753
+ result: row.result === null ? void 0 : parseJson(row.result),
754
+ error: taskError(row),
755
+ providerRequestId: row.providerRequestId ?? void 0,
756
+ workerId: row.workerId ?? void 0,
757
+ leaseToken: row.leaseToken ?? void 0,
758
+ leaseExpiresAt: optionalDate(row.leaseExpiresAt),
759
+ heartbeatAt: optionalDate(row.heartbeatAt),
760
+ availableAt: new Date(row.availableAt),
761
+ createdAt: new Date(row.createdAt),
762
+ startedAt: optionalDate(row.startedAt),
763
+ completedAt: optionalDate(row.completedAt),
764
+ updatedAt: new Date(row.updatedAt),
765
+ version: row.version
766
+ };
767
+ }
768
+ };
769
+
770
+ // src/postgres-task-summary-reader.ts
771
+ var SAFE_SUMMARY_COLUMNS = `
772
+ "id", "kind", "resourceType", "resourceId", "status"::text AS "status",
773
+ "stage", "progress", "attempt", "maxAttempts", "errorCode",
774
+ "errorCategory", "errorRetryable", "createdAt",
775
+ "startedAt", "completedAt", "updatedAt", "version"
776
+ `;
777
+ function optionalDate2(value) {
778
+ return value === null ? void 0 : new Date(value);
779
+ }
780
+ __name(optionalDate2, "optionalDate");
781
+ function taskStatus2(value) {
782
+ if (TASK_STATUSES.includes(value)) return value;
783
+ throw new Error(`Unknown task status returned by PostgreSQL: ${value}`);
784
+ }
785
+ __name(taskStatus2, "taskStatus");
786
+ function toSummary(row) {
787
+ const error = row.errorCode ? {
788
+ code: row.errorCode,
789
+ category: row.errorCategory ?? "UNKNOWN",
790
+ summary: row.errorCode,
791
+ retryable: row.errorRetryable ?? false
792
+ } : void 0;
793
+ return {
794
+ id: row.id,
795
+ kind: row.kind,
796
+ resource: {
797
+ type: row.resourceType,
798
+ id: row.resourceId
799
+ },
800
+ status: taskStatus2(row.status),
801
+ stage: row.stage ?? void 0,
802
+ progress: row.progress,
803
+ attempt: row.attempt,
804
+ maxAttempts: row.maxAttempts,
805
+ error,
806
+ createdAt: new Date(row.createdAt),
807
+ startedAt: optionalDate2(row.startedAt),
808
+ completedAt: optionalDate2(row.completedAt),
809
+ updatedAt: new Date(row.updatedAt),
810
+ version: row.version
811
+ };
812
+ }
813
+ __name(toSummary, "toSummary");
814
+ var PostgresTaskSummaryReader = class {
815
+ static {
816
+ __name(this, "PostgresTaskSummaryReader");
817
+ }
818
+ client;
819
+ constructor(client) {
820
+ this.client = client;
821
+ }
822
+ async findOwnedById(taskId, userId) {
823
+ const result = await this.client.query(`SELECT ${SAFE_SUMMARY_COLUMNS}
824
+ FROM async_jobs
825
+ WHERE "id" = $1 AND "userId" = $2
826
+ LIMIT 1`, [
827
+ taskId,
828
+ userId
829
+ ]);
830
+ return result.rows[0] ? toSummary(result.rows[0]) : null;
831
+ }
832
+ async listOwned(input) {
833
+ if (!input.userId) return [];
834
+ const limit = Math.max(1, Math.min(input.limit ?? 50, 100));
835
+ const statuses = input.statuses?.length ? [
836
+ ...new Set(input.statuses)
837
+ ] : void 0;
838
+ if (statuses?.some((status2) => !TASK_STATUSES.includes(status2))) {
839
+ throw new RangeError("statuses contains an unknown task status");
840
+ }
841
+ const statusPlaceholders = statuses?.map((_, index) => `$${index + 2}::async_job_status`).join(", ");
842
+ const result = statuses ? await this.client.query(`SELECT ${SAFE_SUMMARY_COLUMNS}
843
+ FROM async_jobs
844
+ WHERE "userId" = $1
845
+ AND "status" IN (${statusPlaceholders})
846
+ ORDER BY "createdAt" DESC, "id" DESC
847
+ LIMIT $${statuses.length + 2}`, [
848
+ input.userId,
849
+ ...statuses,
850
+ limit
851
+ ]) : await this.client.query(`SELECT ${SAFE_SUMMARY_COLUMNS}
852
+ FROM async_jobs
853
+ WHERE "userId" = $1
854
+ ORDER BY "createdAt" DESC, "id" DESC
855
+ LIMIT $2`, [
856
+ input.userId,
857
+ limit
858
+ ]);
859
+ return result.rows.map(toSummary);
860
+ }
861
+ };
862
+
863
+ // src/task-timeline.ts
864
+ var TASK_TIMELINE_EVENT_TYPES = [
865
+ "SNAPSHOT",
866
+ "CREATED",
867
+ "CLAIMED",
868
+ "PROGRESS",
869
+ "RETRY_SCHEDULED",
870
+ "REQUEUED",
871
+ "LEASE_RECOVERED",
872
+ "MANUAL_REDRIVE",
873
+ "COMPLETED",
874
+ "FAILED",
875
+ "CANCELLED",
876
+ "STATUS_CHANGED"
877
+ ];
878
+ var PUBLIC_COLUMNS = `events."id", events."taskId", events."sequence",
879
+ events."eventType", events."fromStatus"::text AS "fromStatus",
880
+ events."toStatus"::text AS "toStatus", events."stage", events."progress",
881
+ events."attempt", events."maxAttempts", events."errorCode",
882
+ events."errorCategory", events."errorRetryable",
883
+ events."createdAt"`;
884
+ function status(value) {
885
+ if (value === null) return void 0;
886
+ if (TASK_STATUSES.includes(value)) return value;
887
+ throw new Error(`Unknown task status returned by PostgreSQL: ${value}`);
888
+ }
889
+ __name(status, "status");
890
+ function requiredStatus(value) {
891
+ const parsed = status(value);
892
+ if (parsed) return parsed;
893
+ throw new Error("Task timeline event is missing its destination status");
894
+ }
895
+ __name(requiredStatus, "requiredStatus");
896
+ function eventType(value) {
897
+ if (TASK_TIMELINE_EVENT_TYPES.includes(value)) {
898
+ return value;
899
+ }
900
+ throw new Error(`Unknown task timeline event type returned by PostgreSQL: ${value}`);
901
+ }
902
+ __name(eventType, "eventType");
903
+ function toOwnedEvent(row) {
904
+ const error = row.errorCode ? {
905
+ code: row.errorCode,
906
+ category: row.errorCategory ?? "UNKNOWN",
907
+ summary: row.errorCode,
908
+ retryable: row.errorRetryable ?? false
909
+ } : void 0;
910
+ return {
911
+ id: row.id,
912
+ taskId: row.taskId,
913
+ sequence: row.sequence,
914
+ type: eventType(row.eventType),
915
+ fromStatus: status(row.fromStatus),
916
+ toStatus: requiredStatus(row.toStatus),
917
+ stage: row.stage ?? void 0,
918
+ progress: row.progress,
919
+ attempt: row.attempt,
920
+ maxAttempts: row.maxAttempts,
921
+ error,
922
+ createdAt: new Date(row.createdAt)
923
+ };
924
+ }
925
+ __name(toOwnedEvent, "toOwnedEvent");
926
+ var PostgresTaskTimelineReader = class {
927
+ static {
928
+ __name(this, "PostgresTaskTimelineReader");
929
+ }
930
+ client;
931
+ constructor(client) {
932
+ this.client = client;
933
+ }
934
+ async listOwned(taskId, userId, limit = 200) {
935
+ if (!userId) return [];
936
+ const result = await this.client.query(`SELECT ${PUBLIC_COLUMNS}
937
+ FROM async_job_events AS events
938
+ INNER JOIN async_jobs AS tasks ON tasks."id" = events."taskId"
939
+ WHERE events."taskId" = $1 AND tasks."userId" = $2
940
+ ORDER BY events."sequence" ASC
941
+ LIMIT $3`, [
942
+ taskId,
943
+ userId,
944
+ this.normalizeLimit(limit)
945
+ ]);
946
+ return result.rows.map(toOwnedEvent);
947
+ }
948
+ async listByTaskId(taskId, limit = 500) {
949
+ const result = await this.client.query(`SELECT ${PUBLIC_COLUMNS}, events."workerId", events."providerRequestId"
950
+ FROM async_job_events AS events
951
+ WHERE events."taskId" = $1
952
+ ORDER BY events."sequence" ASC
953
+ LIMIT $2`, [
954
+ taskId,
955
+ this.normalizeLimit(limit)
956
+ ]);
957
+ return result.rows.map((row) => ({
958
+ ...toOwnedEvent(row),
959
+ workerId: row.workerId ?? void 0,
960
+ providerRequestId: row.providerRequestId ?? void 0
961
+ }));
962
+ }
963
+ normalizeLimit(limit) {
964
+ return Math.max(1, Math.min(Math.trunc(limit), 1e3));
965
+ }
966
+ };
967
+
968
+ // src/task-redriver.ts
969
+ var PostgresTaskRedriver = class {
970
+ static {
971
+ __name(this, "PostgresTaskRedriver");
972
+ }
973
+ client;
974
+ constructor(client) {
975
+ this.client = client;
976
+ }
977
+ async redriveFailed(input) {
978
+ const additionalAttempts = input.additionalAttempts ?? 1;
979
+ if (!Number.isInteger(additionalAttempts) || additionalAttempts < 1 || additionalAttempts > 10) {
980
+ throw new RangeError("additionalAttempts must be an integer between 1 and 10");
981
+ }
982
+ const result = await this.client.query(`UPDATE async_jobs
983
+ SET "status" = 'QUEUED'::async_job_status,
984
+ "stage" = $2,
985
+ "maxAttempts" = GREATEST("maxAttempts", "attempt" + $3),
986
+ "result" = NULL,
987
+ "errorCode" = NULL,
988
+ "errorCategory" = NULL,
989
+ "errorSummary" = NULL,
990
+ "errorRetryable" = NULL,
991
+ "errorMetadata" = NULL,
992
+ "providerRequestId" = NULL,
993
+ "workerId" = NULL,
994
+ "leaseToken" = NULL,
995
+ "leaseExpiresAt" = NULL,
996
+ "heartbeatAt" = NULL,
997
+ "availableAt" = $4,
998
+ "completedAt" = NULL,
999
+ "updatedAt" = $4,
1000
+ "version" = "version" + 1
1001
+ WHERE "id" = $1 AND "status" = 'FAILED'::async_job_status
1002
+ RETURNING "id", "attempt", "maxAttempts", "version", "availableAt"`, [
1003
+ input.taskId,
1004
+ input.stage ?? "manual-redrive",
1005
+ additionalAttempts,
1006
+ input.now
1007
+ ]);
1008
+ const row = result.rows[0];
1009
+ return row ? {
1010
+ taskId: row.id,
1011
+ attempt: row.attempt,
1012
+ maxAttempts: row.maxAttempts,
1013
+ version: row.version,
1014
+ availableAt: new Date(row.availableAt)
1015
+ } : null;
1016
+ }
1017
+ };
1018
+
1019
+ // src/postgres-task-outbox.ts
1020
+ var import_node_util = require("util");
1021
+
1022
+ // src/task-outbox.ts
1023
+ var TaskOutboxConflictError = class extends Error {
1024
+ static {
1025
+ __name(this, "TaskOutboxConflictError");
1026
+ }
1027
+ eventId;
1028
+ constructor(eventId) {
1029
+ super(`Outbox event id was reused with different content: ${eventId}`), this.eventId = eventId;
1030
+ this.name = "TaskOutboxConflictError";
1031
+ }
1032
+ };
1033
+
1034
+ // src/postgres-task-outbox.ts
1035
+ var APPEND_OUTBOX_SQL = `
1036
+ WITH inserted AS (
1037
+ INSERT INTO async_job_outbox (
1038
+ "id", "taskId", "topic", "payload", "status", "attempt",
1039
+ "availableAt", "createdAt", "updatedAt"
1040
+ ) VALUES (
1041
+ $1, $2, $3, $4::jsonb, 'PENDING', 0,
1042
+ $5, $6, $6
1043
+ )
1044
+ ON CONFLICT ("id") DO NOTHING
1045
+ RETURNING *
1046
+ )
1047
+ SELECT inserted.*, TRUE AS "__created"
1048
+ FROM inserted
1049
+ UNION ALL
1050
+ SELECT existing.*, FALSE AS "__created"
1051
+ FROM async_job_outbox AS existing
1052
+ WHERE existing."id" = $1
1053
+ AND NOT EXISTS (SELECT 1 FROM inserted)
1054
+ LIMIT 1
1055
+ `;
1056
+ var CLAIM_OUTBOX_SQL = `
1057
+ WITH candidates AS (
1058
+ SELECT "id"
1059
+ FROM async_job_outbox
1060
+ WHERE "availableAt" <= $4
1061
+ AND (
1062
+ "status" = 'PENDING'
1063
+ OR (
1064
+ "status" = 'PUBLISHING'
1065
+ AND "leaseExpiresAt" <= $4
1066
+ )
1067
+ )
1068
+ ORDER BY "createdAt" ASC
1069
+ LIMIT $5
1070
+ FOR UPDATE SKIP LOCKED
1071
+ )
1072
+ UPDATE async_job_outbox AS events
1073
+ SET
1074
+ "status" = 'PUBLISHING',
1075
+ "workerId" = $1,
1076
+ "leaseToken" = $2,
1077
+ "leaseExpiresAt" = $3,
1078
+ "attempt" = events."attempt" + 1,
1079
+ "updatedAt" = $4
1080
+ FROM candidates
1081
+ WHERE events."id" = candidates."id"
1082
+ AND $3 > $4
1083
+ RETURNING events.*
1084
+ `;
1085
+ var MARK_OUTBOX_PUBLISHED_SQL = `
1086
+ UPDATE async_job_outbox
1087
+ SET
1088
+ "status" = 'PUBLISHED',
1089
+ "workerId" = NULL,
1090
+ "leaseToken" = NULL,
1091
+ "leaseExpiresAt" = NULL,
1092
+ "publishedAt" = $3,
1093
+ "updatedAt" = $3,
1094
+ "lastError" = NULL
1095
+ WHERE "id" = $1
1096
+ AND "status" = 'PUBLISHING'
1097
+ AND "leaseToken" = $2
1098
+ AND "leaseExpiresAt" > $3
1099
+ RETURNING "id"
1100
+ `;
1101
+ var RESCHEDULE_OUTBOX_SQL = `
1102
+ UPDATE async_job_outbox
1103
+ SET
1104
+ "status" = 'PENDING',
1105
+ "workerId" = NULL,
1106
+ "leaseToken" = NULL,
1107
+ "leaseExpiresAt" = NULL,
1108
+ "availableAt" = $3,
1109
+ "lastError" = $4,
1110
+ "updatedAt" = $5
1111
+ WHERE "id" = $1
1112
+ AND "status" = 'PUBLISHING'
1113
+ AND "leaseToken" = $2
1114
+ AND "leaseExpiresAt" > $5
1115
+ RETURNING "id"
1116
+ `;
1117
+ function parseJson2(value) {
1118
+ if (typeof value !== "string") return value;
1119
+ return JSON.parse(value);
1120
+ }
1121
+ __name(parseJson2, "parseJson");
1122
+ function optionalDate3(value) {
1123
+ return value === null ? void 0 : new Date(value);
1124
+ }
1125
+ __name(optionalDate3, "optionalDate");
1126
+ var PostgresTaskOutboxRepository = class {
1127
+ static {
1128
+ __name(this, "PostgresTaskOutboxRepository");
1129
+ }
1130
+ client;
1131
+ constructor(client) {
1132
+ this.client = client;
1133
+ }
1134
+ async append(input) {
1135
+ const result = await this.client.query(APPEND_OUTBOX_SQL, [
1136
+ input.id,
1137
+ input.taskId,
1138
+ input.topic,
1139
+ JSON.stringify(input.payload),
1140
+ input.availableAt ?? input.now,
1141
+ input.now
1142
+ ]);
1143
+ let row = result.rows[0];
1144
+ if (!row) {
1145
+ const existing = await this.client.query('SELECT * FROM async_job_outbox WHERE "id" = $1 LIMIT 1', [
1146
+ input.id
1147
+ ]);
1148
+ row = existing.rows[0];
1149
+ if (row) row.__created = false;
1150
+ }
1151
+ if (!row) throw new Error(`Outbox event could not be created: ${input.id}`);
1152
+ if (row.taskId !== input.taskId || row.topic !== input.topic || !(0, import_node_util.isDeepStrictEqual)(parseJson2(row.payload), input.payload)) {
1153
+ throw new TaskOutboxConflictError(input.id);
1154
+ }
1155
+ return {
1156
+ record: this.toRecord(row),
1157
+ created: row.__created
1158
+ };
1159
+ }
1160
+ async claimBatch(input) {
1161
+ if (input.limit < 1 || input.leaseExpiresAt.getTime() <= input.now.getTime()) return [];
1162
+ const result = await this.client.query(CLAIM_OUTBOX_SQL, [
1163
+ input.workerId,
1164
+ input.leaseToken,
1165
+ input.leaseExpiresAt,
1166
+ input.now,
1167
+ input.limit
1168
+ ]);
1169
+ return result.rows.map((row) => this.toRecord(row));
1170
+ }
1171
+ async markPublished(id, leaseToken, now) {
1172
+ const result = await this.client.query(MARK_OUTBOX_PUBLISHED_SQL, [
1173
+ id,
1174
+ leaseToken,
1175
+ now
1176
+ ]);
1177
+ return result.rows.length === 1;
1178
+ }
1179
+ async reschedule(id, leaseToken, availableAt, errorSummary, now) {
1180
+ const result = await this.client.query(RESCHEDULE_OUTBOX_SQL, [
1181
+ id,
1182
+ leaseToken,
1183
+ availableAt,
1184
+ errorSummary,
1185
+ now
1186
+ ]);
1187
+ return result.rows.length === 1;
1188
+ }
1189
+ toRecord(row) {
1190
+ return {
1191
+ id: row.id,
1192
+ taskId: row.taskId,
1193
+ topic: row.topic,
1194
+ payload: parseJson2(row.payload),
1195
+ status: row.status,
1196
+ attempt: row.attempt,
1197
+ availableAt: new Date(row.availableAt),
1198
+ workerId: row.workerId ?? void 0,
1199
+ leaseToken: row.leaseToken ?? void 0,
1200
+ leaseExpiresAt: optionalDate3(row.leaseExpiresAt),
1201
+ publishedAt: optionalDate3(row.publishedAt),
1202
+ lastError: row.lastError ?? void 0,
1203
+ createdAt: new Date(row.createdAt),
1204
+ updatedAt: new Date(row.updatedAt)
1205
+ };
1206
+ }
1207
+ };
1208
+
1209
+ // src/postgres-task-submitter.ts
1210
+ var PostgresTaskSubmitter = class {
1211
+ static {
1212
+ __name(this, "PostgresTaskSubmitter");
1213
+ }
1214
+ transactions;
1215
+ constructor(transactions) {
1216
+ this.transactions = transactions;
1217
+ }
1218
+ async submit(input, options = {}) {
1219
+ return this.transactions.runInTransaction(async (client) => {
1220
+ const result = await this.submitWithClient(client, input);
1221
+ await options.transactionalWork?.(client, result);
1222
+ return result;
1223
+ });
1224
+ }
1225
+ async submitBatch(inputs, options = {}) {
1226
+ return this.transactions.runInTransaction(async (client) => {
1227
+ const results = [];
1228
+ for (const input of inputs) {
1229
+ results.push(await this.submitWithClient(client, input));
1230
+ }
1231
+ await options.transactionalWork?.(client, results);
1232
+ return results;
1233
+ });
1234
+ }
1235
+ async submitWithClient(client, input) {
1236
+ const tasks = new PostgresTaskRepository(client);
1237
+ const outbox = new PostgresTaskOutboxRepository(client);
1238
+ const task = await tasks.create(input.task);
1239
+ const event = await outbox.append({
1240
+ ...input.outbox,
1241
+ taskId: task.task.id,
1242
+ now: input.task.now
1243
+ });
1244
+ return {
1245
+ task,
1246
+ outbox: event
1247
+ };
1248
+ }
1249
+ };
1250
+
1251
+ // src/task-lease-reconciler.ts
1252
+ var leaseExpiredError = {
1253
+ code: "WORKER_LEASE_EXPIRED",
1254
+ category: "INTERNAL",
1255
+ summary: "Worker heartbeat expired before the task reached a terminal state",
1256
+ retryable: true
1257
+ };
1258
+ var TaskLeaseReconciler = class {
1259
+ static {
1260
+ __name(this, "TaskLeaseReconciler");
1261
+ }
1262
+ repository;
1263
+ batchSize;
1264
+ retryDelayMs;
1265
+ now;
1266
+ constructor(repository, options = {}) {
1267
+ this.repository = repository;
1268
+ this.batchSize = options.batchSize ?? 100;
1269
+ this.retryDelayMs = options.retryDelayMs ?? 0;
1270
+ this.now = options.now ?? (() => /* @__PURE__ */ new Date());
1271
+ if (this.batchSize < 1) throw new RangeError("batchSize must be at least 1");
1272
+ if (this.retryDelayMs < 0) throw new RangeError("retryDelayMs cannot be negative");
1273
+ }
1274
+ async runOnce() {
1275
+ const now = this.now();
1276
+ const expired = await this.repository.findExpiredLeases({
1277
+ now,
1278
+ limit: this.batchSize
1279
+ });
1280
+ const result = {
1281
+ found: expired.length,
1282
+ requeued: 0,
1283
+ failed: 0,
1284
+ conflicts: 0
1285
+ };
1286
+ for (const task of expired) {
1287
+ if (!task.leaseToken) {
1288
+ result.conflicts += 1;
1289
+ continue;
1290
+ }
1291
+ const recovered = await this.repository.recoverExpiredLease({
1292
+ id: task.id,
1293
+ leaseToken: task.leaseToken,
1294
+ now,
1295
+ availableAt: new Date(now.getTime() + this.retryDelayMs),
1296
+ error: leaseExpiredError
1297
+ });
1298
+ if (!recovered) result.conflicts += 1;
1299
+ else if (recovered.status === "FAILED") result.failed += 1;
1300
+ else result.requeued += 1;
1301
+ }
1302
+ return result;
1303
+ }
1304
+ };
1305
+
1306
+ // src/task-queue-reconciler.ts
1307
+ var TaskQueueReconciler = class {
1308
+ static {
1309
+ __name(this, "TaskQueueReconciler");
1310
+ }
1311
+ repository;
1312
+ publisher;
1313
+ batchSize;
1314
+ now;
1315
+ offset = 0;
1316
+ constructor(repository, publisher, options = {}) {
1317
+ this.repository = repository;
1318
+ this.publisher = publisher;
1319
+ this.batchSize = options.batchSize ?? 100;
1320
+ this.now = options.now ?? (() => /* @__PURE__ */ new Date());
1321
+ if (this.batchSize < 1) throw new RangeError("batchSize must be at least 1");
1322
+ }
1323
+ async runOnce() {
1324
+ let tasks = await this.repository.findReadyQueued({
1325
+ now: this.now(),
1326
+ limit: this.batchSize,
1327
+ offset: this.offset
1328
+ });
1329
+ if (tasks.length === 0 && this.offset > 0) {
1330
+ this.offset = 0;
1331
+ tasks = await this.repository.findReadyQueued({
1332
+ now: this.now(),
1333
+ limit: this.batchSize,
1334
+ offset: 0
1335
+ });
1336
+ }
1337
+ this.offset = tasks.length < this.batchSize ? 0 : this.offset + tasks.length;
1338
+ const result = {
1339
+ found: tasks.length,
1340
+ ensured: 0,
1341
+ failed: 0
1342
+ };
1343
+ for (const task of tasks) {
1344
+ try {
1345
+ await this.publisher.ensure(task);
1346
+ result.ensured += 1;
1347
+ } catch {
1348
+ result.failed += 1;
1349
+ }
1350
+ }
1351
+ return result;
1352
+ }
1353
+ };
1354
+
1355
+ // src/retry-policy.ts
1356
+ var defaultRetryableCategories = [
1357
+ "RATE_LIMIT",
1358
+ "NETWORK",
1359
+ "PROVIDER_5XX",
1360
+ "PROVIDER_TIMEOUT",
1361
+ "INTERNAL"
1362
+ ];
1363
+ var ExponentialTaskRetryPolicy = class {
1364
+ static {
1365
+ __name(this, "ExponentialTaskRetryPolicy");
1366
+ }
1367
+ baseDelayMs;
1368
+ maxDelayMs;
1369
+ jitterRatio;
1370
+ retryableCategories;
1371
+ random;
1372
+ constructor(options = {}) {
1373
+ this.baseDelayMs = options.baseDelayMs ?? 1e3;
1374
+ this.maxDelayMs = options.maxDelayMs ?? 6e4;
1375
+ this.jitterRatio = options.jitterRatio ?? 0.2;
1376
+ this.retryableCategories = new Set(options.retryableCategories ?? defaultRetryableCategories);
1377
+ this.random = options.random ?? Math.random;
1378
+ if (this.baseDelayMs < 0) throw new RangeError("baseDelayMs cannot be negative");
1379
+ if (this.maxDelayMs < this.baseDelayMs) {
1380
+ throw new RangeError("maxDelayMs cannot be lower than baseDelayMs");
1381
+ }
1382
+ if (this.jitterRatio < 0 || this.jitterRatio > 1) {
1383
+ throw new RangeError("jitterRatio must be between 0 and 1");
1384
+ }
1385
+ }
1386
+ decide(context) {
1387
+ if (context.attempt >= context.maxAttempts) {
1388
+ return {
1389
+ retry: false,
1390
+ reason: "ATTEMPTS_EXHAUSTED"
1391
+ };
1392
+ }
1393
+ if (!context.error.retryable || !this.retryableCategories.has(context.error.category)) {
1394
+ return {
1395
+ retry: false,
1396
+ reason: "NON_RETRYABLE"
1397
+ };
1398
+ }
1399
+ const exponential = Math.min(this.maxDelayMs, this.baseDelayMs * 2 ** Math.max(0, context.attempt - 1));
1400
+ const retryAfter = Math.max(0, context.retryAfterMs ?? 0);
1401
+ const base = Math.max(exponential, retryAfter);
1402
+ const spread = Math.min(this.maxDelayMs, base) * this.jitterRatio;
1403
+ const jittered = base - spread + this.random() * spread * 2;
1404
+ return {
1405
+ retry: true,
1406
+ delayMs: Math.round(Math.min(this.maxDelayMs, jittered))
1407
+ };
1408
+ }
1409
+ };
1410
+
1411
+ // src/durable-worker-runtime.ts
1412
+ var import_node_crypto2 = require("crypto");
1413
+ var DurableTaskNotFoundError = class extends Error {
1414
+ static {
1415
+ __name(this, "DurableTaskNotFoundError");
1416
+ }
1417
+ taskId;
1418
+ constructor(taskId) {
1419
+ super(`Durable task was not found: ${taskId}`), this.taskId = taskId;
1420
+ this.name = "DurableTaskNotFoundError";
1421
+ }
1422
+ };
1423
+ var UnsupportedDurableTaskKindError = class extends Error {
1424
+ static {
1425
+ __name(this, "UnsupportedDurableTaskKindError");
1426
+ }
1427
+ taskId;
1428
+ kind;
1429
+ constructor(taskId, kind) {
1430
+ super(`No durable task handler is registered for kind: ${kind}`), this.taskId = taskId, this.kind = kind;
1431
+ this.name = "UnsupportedDurableTaskKindError";
1432
+ }
1433
+ };
1434
+ var DurableTaskNotClaimableError = class extends Error {
1435
+ static {
1436
+ __name(this, "DurableTaskNotClaimableError");
1437
+ }
1438
+ taskId;
1439
+ status;
1440
+ constructor(taskId, status2) {
1441
+ super(`Durable task is not claimable: ${taskId} (${status2})`), this.taskId = taskId, this.status = status2;
1442
+ this.name = "DurableTaskNotClaimableError";
1443
+ }
1444
+ };
1445
+ var DurableTaskLeaseLostError = class extends Error {
1446
+ static {
1447
+ __name(this, "DurableTaskLeaseLostError");
1448
+ }
1449
+ taskId;
1450
+ operation;
1451
+ constructor(taskId, operation) {
1452
+ super(`Durable task lease no longer owns ${operation}: ${taskId}`), this.taskId = taskId, this.operation = operation;
1453
+ this.name = "DurableTaskLeaseLostError";
1454
+ }
1455
+ };
1456
+ var DurableTaskRetryScheduledError = class extends Error {
1457
+ static {
1458
+ __name(this, "DurableTaskRetryScheduledError");
1459
+ }
1460
+ taskId;
1461
+ taskError;
1462
+ retryAt;
1463
+ constructor(taskId, taskError2, retryAt) {
1464
+ super(`Durable task retry was scheduled: ${taskId} (${taskError2.code})`), this.taskId = taskId, this.taskError = taskError2, this.retryAt = retryAt;
1465
+ this.name = "DurableTaskRetryScheduledError";
1466
+ }
1467
+ };
1468
+ var defaultHeartbeatScheduler = {
1469
+ start(tick, intervalMs) {
1470
+ const handle = setInterval(() => void tick(), intervalMs);
1471
+ handle.unref?.();
1472
+ return handle;
1473
+ },
1474
+ stop(handle) {
1475
+ clearInterval(handle);
1476
+ }
1477
+ };
1478
+ function terminalResult(task) {
1479
+ return {
1480
+ taskId: task.id,
1481
+ kind: task.kind,
1482
+ status: task.status
1483
+ };
1484
+ }
1485
+ __name(terminalResult, "terminalResult");
1486
+ var DurableWorkerRuntime = class {
1487
+ static {
1488
+ __name(this, "DurableWorkerRuntime");
1489
+ }
1490
+ options;
1491
+ repository;
1492
+ retryPolicy;
1493
+ workerId;
1494
+ leaseDurationMs;
1495
+ heartbeatIntervalMs;
1496
+ maxConsecutiveHeartbeatFailures;
1497
+ now;
1498
+ createLeaseToken;
1499
+ heartbeatScheduler;
1500
+ constructor(options) {
1501
+ this.options = options;
1502
+ this.repository = options.repository;
1503
+ this.retryPolicy = options.retryPolicy;
1504
+ this.workerId = options.workerId;
1505
+ this.leaseDurationMs = options.leaseDurationMs ?? 9e4;
1506
+ this.heartbeatIntervalMs = options.heartbeatIntervalMs ?? 2e4;
1507
+ this.maxConsecutiveHeartbeatFailures = options.maxConsecutiveHeartbeatFailures ?? 3;
1508
+ this.now = options.now ?? (() => /* @__PURE__ */ new Date());
1509
+ this.createLeaseToken = options.createLeaseToken ?? import_node_crypto2.randomUUID;
1510
+ this.heartbeatScheduler = options.heartbeatScheduler ?? defaultHeartbeatScheduler;
1511
+ if (!this.workerId.trim()) throw new RangeError("workerId cannot be empty");
1512
+ if (this.leaseDurationMs < 1) throw new RangeError("leaseDurationMs must be positive");
1513
+ if (this.heartbeatIntervalMs < 1) {
1514
+ throw new RangeError("heartbeatIntervalMs must be positive");
1515
+ }
1516
+ if (this.heartbeatIntervalMs >= this.leaseDurationMs) {
1517
+ throw new RangeError("heartbeatIntervalMs must be lower than leaseDurationMs");
1518
+ }
1519
+ if (this.maxConsecutiveHeartbeatFailures < 1) {
1520
+ throw new RangeError("maxConsecutiveHeartbeatFailures must be at least 1");
1521
+ }
1522
+ }
1523
+ async run(taskId) {
1524
+ let task = await this.repository.findById(taskId);
1525
+ if (!task) throw new DurableTaskNotFoundError(taskId);
1526
+ if (isTerminalTaskStatus(task.status)) return terminalResult(task);
1527
+ if (task.status === "RETRYING") {
1528
+ const queued = await this.repository.transition({
1529
+ id: task.id,
1530
+ from: "RETRYING",
1531
+ to: "QUEUED",
1532
+ now: this.now()
1533
+ });
1534
+ if (!queued) {
1535
+ const latest = await this.repository.findById(task.id);
1536
+ if (latest && isTerminalTaskStatus(latest.status)) return terminalResult(latest);
1537
+ throw new DurableTaskNotClaimableError(task.id, latest?.status ?? task.status);
1538
+ }
1539
+ task = queued;
1540
+ }
1541
+ if (task.status !== "QUEUED") {
1542
+ throw new DurableTaskNotClaimableError(task.id, task.status);
1543
+ }
1544
+ const handler = this.options.resolveHandler(task);
1545
+ if (!handler) throw new UnsupportedDurableTaskKindError(task.id, task.kind);
1546
+ const leaseToken = this.createLeaseToken();
1547
+ const claimedAt = this.now();
1548
+ const claimed = await this.repository.claim({
1549
+ id: task.id,
1550
+ workerId: this.workerId,
1551
+ leaseToken,
1552
+ leaseExpiresAt: this.leaseExpiry(claimedAt),
1553
+ now: claimedAt
1554
+ });
1555
+ if (!claimed) throw new DurableTaskNotClaimableError(task.id, task.status);
1556
+ const controller = new AbortController();
1557
+ let abortNotified = false;
1558
+ let heartbeatFailures = 0;
1559
+ let heartbeatInFlight = false;
1560
+ const abortAttempt = /* @__PURE__ */ __name(async (operation) => {
1561
+ if (controller.signal.aborted) return;
1562
+ const reason = new DurableTaskLeaseLostError(claimed.id, operation);
1563
+ controller.abort(reason);
1564
+ if (abortNotified) return;
1565
+ abortNotified = true;
1566
+ try {
1567
+ await this.options.onAbort?.(claimed, reason);
1568
+ } catch {
1569
+ this.options.logger?.warn?.("durable_task_abort_callback_failed", {
1570
+ taskId: claimed.id,
1571
+ kind: claimed.kind
1572
+ });
1573
+ }
1574
+ }, "abortAttempt");
1575
+ const renewLease = /* @__PURE__ */ __name(async () => {
1576
+ const now = this.now();
1577
+ return this.repository.heartbeat({
1578
+ id: claimed.id,
1579
+ leaseToken,
1580
+ leaseExpiresAt: this.leaseExpiry(now),
1581
+ now
1582
+ });
1583
+ }, "renewLease");
1584
+ const heartbeat = /* @__PURE__ */ __name(async () => {
1585
+ if (heartbeatInFlight || controller.signal.aborted) return;
1586
+ heartbeatInFlight = true;
1587
+ try {
1588
+ const renewed = await renewLease();
1589
+ if (!renewed) {
1590
+ this.options.logger?.error?.("durable_task_lease_lost", {
1591
+ taskId: claimed.id,
1592
+ kind: claimed.kind
1593
+ });
1594
+ await abortAttempt("heartbeat");
1595
+ return;
1596
+ }
1597
+ heartbeatFailures = 0;
1598
+ } catch {
1599
+ heartbeatFailures += 1;
1600
+ this.options.logger?.warn?.("durable_task_heartbeat_failed", {
1601
+ taskId: claimed.id,
1602
+ kind: claimed.kind,
1603
+ consecutiveFailures: heartbeatFailures
1604
+ });
1605
+ if (heartbeatFailures >= this.maxConsecutiveHeartbeatFailures) {
1606
+ await abortAttempt("heartbeat");
1607
+ }
1608
+ } finally {
1609
+ heartbeatInFlight = false;
1610
+ }
1611
+ }, "heartbeat");
1612
+ const context = {
1613
+ task: claimed,
1614
+ leaseToken,
1615
+ signal: controller.signal,
1616
+ now: this.now,
1617
+ assertActive: /* @__PURE__ */ __name(async () => {
1618
+ let active;
1619
+ try {
1620
+ active = await renewLease();
1621
+ } catch (error) {
1622
+ await abortAttempt("active check");
1623
+ throw error;
1624
+ }
1625
+ if (active) return;
1626
+ await abortAttempt("active check");
1627
+ throw new DurableTaskLeaseLostError(claimed.id, "active check");
1628
+ }, "assertActive"),
1629
+ reportProgress: /* @__PURE__ */ __name(async (stage, progress) => {
1630
+ const updated = await this.repository.reportProgress({
1631
+ id: claimed.id,
1632
+ leaseToken,
1633
+ stage,
1634
+ progress,
1635
+ now: this.now()
1636
+ });
1637
+ if (updated) return updated;
1638
+ await abortAttempt("progress update");
1639
+ throw new DurableTaskLeaseLostError(claimed.id, "progress update");
1640
+ }, "reportProgress")
1641
+ };
1642
+ const heartbeatHandle = this.heartbeatScheduler.start(heartbeat, this.heartbeatIntervalMs);
1643
+ try {
1644
+ const result = await handler.execute(context);
1645
+ if (controller.signal.aborted) {
1646
+ throw controller.signal.reason instanceof Error ? controller.signal.reason : new DurableTaskLeaseLostError(claimed.id, "completion");
1647
+ }
1648
+ const completed = handler.complete ? await handler.complete(context, result) : Boolean(await this.repository.transition({
1649
+ id: claimed.id,
1650
+ from: "PROCESSING",
1651
+ to: "COMPLETED",
1652
+ leaseToken,
1653
+ result,
1654
+ now: this.now()
1655
+ }));
1656
+ if (!completed) return this.resolveRejectedCommit(claimed, "completion");
1657
+ return {
1658
+ taskId: claimed.id,
1659
+ kind: claimed.kind,
1660
+ status: "COMPLETED"
1661
+ };
1662
+ } catch (executionError) {
1663
+ const latest = await this.findLatestBestEffort(claimed);
1664
+ if (latest && isTerminalTaskStatus(latest.status)) {
1665
+ if (latest.status === "CANCELLED") await handler.onCancelled?.(context);
1666
+ return terminalResult(latest);
1667
+ }
1668
+ if (executionError instanceof DurableTaskRetryScheduledError) throw executionError;
1669
+ if (controller.signal.aborted) {
1670
+ throw controller.signal.reason instanceof Error ? controller.signal.reason : new DurableTaskLeaseLostError(claimed.id, "execution");
1671
+ }
1672
+ if (executionError instanceof DurableTaskLeaseLostError) throw executionError;
1673
+ const taskError2 = this.options.classifyError(executionError, claimed);
1674
+ const decision = this.retryPolicy.decide({
1675
+ error: taskError2,
1676
+ attempt: claimed.attempt,
1677
+ maxAttempts: claimed.maxAttempts,
1678
+ retryAfterMs: this.options.retryAfterMs?.(executionError, claimed)
1679
+ });
1680
+ if (decision.retry) {
1681
+ const retryAt = new Date(this.now().getTime() + decision.delayMs);
1682
+ const retried = handler.retry ? await handler.retry(context, taskError2, retryAt) : await this.retry(context, taskError2, retryAt);
1683
+ if (!retried) return this.resolveRejectedCommit(claimed, "retry");
1684
+ throw new DurableTaskRetryScheduledError(claimed.id, taskError2, retryAt);
1685
+ }
1686
+ const failed = handler.fail ? await handler.fail(context, taskError2) : Boolean(await this.repository.transition({
1687
+ id: claimed.id,
1688
+ from: "PROCESSING",
1689
+ to: "FAILED",
1690
+ leaseToken,
1691
+ error: taskError2,
1692
+ now: this.now()
1693
+ }));
1694
+ if (!failed) return this.resolveRejectedCommit(claimed, "failure");
1695
+ return {
1696
+ taskId: claimed.id,
1697
+ kind: claimed.kind,
1698
+ status: "FAILED"
1699
+ };
1700
+ } finally {
1701
+ this.heartbeatScheduler.stop(heartbeatHandle);
1702
+ try {
1703
+ await handler.cleanup?.(context);
1704
+ } catch {
1705
+ this.options.logger?.warn?.("durable_task_cleanup_failed", {
1706
+ taskId: claimed.id,
1707
+ kind: claimed.kind
1708
+ });
1709
+ }
1710
+ }
1711
+ }
1712
+ leaseExpiry(now) {
1713
+ return new Date(now.getTime() + this.leaseDurationMs);
1714
+ }
1715
+ async retry(context, error, retryAt) {
1716
+ const retrying = await this.repository.transition({
1717
+ id: context.task.id,
1718
+ from: "PROCESSING",
1719
+ to: "RETRYING",
1720
+ leaseToken: context.leaseToken,
1721
+ error,
1722
+ availableAt: retryAt,
1723
+ now: this.now()
1724
+ });
1725
+ if (!retrying) return false;
1726
+ return Boolean(await this.repository.transition({
1727
+ id: context.task.id,
1728
+ from: "RETRYING",
1729
+ to: "QUEUED",
1730
+ error,
1731
+ availableAt: retryAt,
1732
+ now: this.now()
1733
+ }));
1734
+ }
1735
+ async resolveRejectedCommit(claimed, operation) {
1736
+ const latest = await this.repository.findById(claimed.id);
1737
+ if (latest && isTerminalTaskStatus(latest.status)) return terminalResult(latest);
1738
+ throw new DurableTaskLeaseLostError(claimed.id, operation);
1739
+ }
1740
+ async findLatestBestEffort(claimed) {
1741
+ try {
1742
+ return await this.repository.findById(claimed.id);
1743
+ } catch {
1744
+ this.options.logger?.warn?.("durable_task_status_refresh_failed", {
1745
+ taskId: claimed.id,
1746
+ kind: claimed.kind
1747
+ });
1748
+ return null;
1749
+ }
1750
+ }
1751
+ };
1752
+
1753
+ // src/in-memory-task-outbox.ts
1754
+ var import_node_util2 = require("util");
1755
+ function cloneRecord(record) {
1756
+ return {
1757
+ ...record,
1758
+ availableAt: new Date(record.availableAt),
1759
+ createdAt: new Date(record.createdAt),
1760
+ updatedAt: new Date(record.updatedAt),
1761
+ leaseExpiresAt: record.leaseExpiresAt ? new Date(record.leaseExpiresAt) : void 0,
1762
+ publishedAt: record.publishedAt ? new Date(record.publishedAt) : void 0
1763
+ };
1764
+ }
1765
+ __name(cloneRecord, "cloneRecord");
1766
+ var InMemoryTaskOutboxRepository = class {
1767
+ static {
1768
+ __name(this, "InMemoryTaskOutboxRepository");
1769
+ }
1770
+ records = /* @__PURE__ */ new Map();
1771
+ async append(input) {
1772
+ const existing = this.records.get(input.id);
1773
+ if (existing) {
1774
+ if (existing.taskId !== input.taskId || existing.topic !== input.topic || !(0, import_node_util2.isDeepStrictEqual)(existing.payload, input.payload)) {
1775
+ throw new TaskOutboxConflictError(input.id);
1776
+ }
1777
+ return {
1778
+ record: cloneRecord(existing),
1779
+ created: false
1780
+ };
1781
+ }
1782
+ const record = {
1783
+ id: input.id,
1784
+ taskId: input.taskId,
1785
+ topic: input.topic,
1786
+ payload: input.payload,
1787
+ status: "PENDING",
1788
+ attempt: 0,
1789
+ availableAt: new Date(input.availableAt ?? input.now),
1790
+ createdAt: new Date(input.now),
1791
+ updatedAt: new Date(input.now)
1792
+ };
1793
+ this.records.set(record.id, record);
1794
+ return {
1795
+ record: cloneRecord(record),
1796
+ created: true
1797
+ };
1798
+ }
1799
+ async claimBatch(input) {
1800
+ if (input.limit < 1) return [];
1801
+ if (input.leaseExpiresAt.getTime() <= input.now.getTime()) return [];
1802
+ const claimable = [
1803
+ ...this.records.values()
1804
+ ].filter((record) => {
1805
+ if (record.availableAt.getTime() > input.now.getTime()) return false;
1806
+ if (record.status === "PENDING") return true;
1807
+ return record.status === "PUBLISHING" && Boolean(record.leaseExpiresAt && record.leaseExpiresAt.getTime() <= input.now.getTime());
1808
+ }).sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()).slice(0, input.limit);
1809
+ return claimable.map((record) => {
1810
+ record.status = "PUBLISHING";
1811
+ record.workerId = input.workerId;
1812
+ record.leaseToken = input.leaseToken;
1813
+ record.leaseExpiresAt = new Date(input.leaseExpiresAt);
1814
+ record.attempt += 1;
1815
+ record.updatedAt = new Date(input.now);
1816
+ return cloneRecord(record);
1817
+ });
1818
+ }
1819
+ async markPublished(id, leaseToken, now) {
1820
+ const record = this.records.get(id);
1821
+ if (!record || record.status !== "PUBLISHING" || record.leaseToken !== leaseToken || !record.leaseExpiresAt || record.leaseExpiresAt.getTime() <= now.getTime()) return false;
1822
+ record.status = "PUBLISHED";
1823
+ record.workerId = void 0;
1824
+ record.leaseToken = void 0;
1825
+ record.leaseExpiresAt = void 0;
1826
+ record.publishedAt = new Date(now);
1827
+ record.updatedAt = new Date(now);
1828
+ record.lastError = void 0;
1829
+ return true;
1830
+ }
1831
+ async reschedule(id, leaseToken, availableAt, errorSummary, now) {
1832
+ const record = this.records.get(id);
1833
+ if (!record || record.status !== "PUBLISHING" || record.leaseToken !== leaseToken || !record.leaseExpiresAt || record.leaseExpiresAt.getTime() <= now.getTime()) return false;
1834
+ record.status = "PENDING";
1835
+ record.workerId = void 0;
1836
+ record.leaseToken = void 0;
1837
+ record.leaseExpiresAt = void 0;
1838
+ record.availableAt = new Date(availableAt);
1839
+ record.updatedAt = new Date(now);
1840
+ record.lastError = errorSummary;
1841
+ return true;
1842
+ }
1843
+ };
1844
+
1845
+ // src/task-outbox-dispatcher.ts
1846
+ var TaskOutboxDispatcher = class {
1847
+ static {
1848
+ __name(this, "TaskOutboxDispatcher");
1849
+ }
1850
+ repository;
1851
+ publisher;
1852
+ options;
1853
+ batchSize;
1854
+ leaseDurationMs;
1855
+ retryDelayMs;
1856
+ now;
1857
+ constructor(repository, publisher, options) {
1858
+ this.repository = repository;
1859
+ this.publisher = publisher;
1860
+ this.options = options;
1861
+ this.batchSize = options.batchSize ?? 100;
1862
+ this.leaseDurationMs = options.leaseDurationMs ?? 3e4;
1863
+ this.retryDelayMs = options.retryDelayMs ?? ((attempt) => Math.min(6e4, 1e3 * 2 ** (attempt - 1)));
1864
+ this.now = options.now ?? (() => /* @__PURE__ */ new Date());
1865
+ if (!options.workerId) throw new Error("workerId is required");
1866
+ if (this.batchSize < 1) throw new RangeError("batchSize must be at least 1");
1867
+ if (this.leaseDurationMs < 1) throw new RangeError("leaseDurationMs must be at least 1");
1868
+ }
1869
+ async runOnce() {
1870
+ const claimedAt = this.now();
1871
+ const leaseToken = this.options.createLeaseToken();
1872
+ const messages = await this.repository.claimBatch({
1873
+ workerId: this.options.workerId,
1874
+ leaseToken,
1875
+ leaseExpiresAt: new Date(claimedAt.getTime() + this.leaseDurationMs),
1876
+ limit: this.batchSize,
1877
+ now: claimedAt
1878
+ });
1879
+ const result = {
1880
+ claimed: messages.length,
1881
+ published: 0,
1882
+ rescheduled: 0,
1883
+ conflicts: 0
1884
+ };
1885
+ for (const message of messages) {
1886
+ try {
1887
+ await this.publisher.publish(message);
1888
+ const marked = await this.repository.markPublished(message.id, leaseToken, this.now());
1889
+ if (marked) result.published += 1;
1890
+ else result.conflicts += 1;
1891
+ } catch (error) {
1892
+ const now = this.now();
1893
+ const delayMs = Math.max(0, this.retryDelayMs(message.attempt));
1894
+ const summary = error instanceof Error ? error.message : String(error);
1895
+ const rescheduled = await this.repository.reschedule(message.id, leaseToken, new Date(now.getTime() + delayMs), summary.slice(0, 280), now);
1896
+ if (rescheduled) result.rescheduled += 1;
1897
+ else result.conflicts += 1;
1898
+ }
1899
+ }
1900
+ return result;
1901
+ }
1902
+ };
1903
+ // Annotate the CommonJS export names for ESM import in node:
1904
+ 0 && (module.exports = {
1905
+ DurableTaskLeaseLostError,
1906
+ DurableTaskNotClaimableError,
1907
+ DurableTaskNotFoundError,
1908
+ DurableTaskRetryScheduledError,
1909
+ DurableWorkerRuntime,
1910
+ ExponentialTaskRetryPolicy,
1911
+ InMemoryTaskOutboxRepository,
1912
+ InMemoryTaskRepository,
1913
+ InvalidTaskTransitionError,
1914
+ PostgresTaskOutboxRepository,
1915
+ PostgresTaskRedriver,
1916
+ PostgresTaskRepository,
1917
+ PostgresTaskSubmitter,
1918
+ PostgresTaskSummaryReader,
1919
+ PostgresTaskTimelineReader,
1920
+ TASK_STATUSES,
1921
+ TASK_TIMELINE_EVENT_TYPES,
1922
+ TaskIdempotencyConflictError,
1923
+ TaskLeaseReconciler,
1924
+ TaskOutboxConflictError,
1925
+ TaskOutboxDispatcher,
1926
+ TaskQueueReconciler,
1927
+ UnsupportedDurableTaskKindError,
1928
+ assertTaskTransition,
1929
+ canTransitionTask,
1930
+ createTaskFingerprint,
1931
+ isTerminalTaskStatus,
1932
+ requiresTaskLease
1933
+ });