@fullstackhouse/open-mercato-durable-work 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +136 -0
  2. package/dist/core/errors.js +81 -0
  3. package/dist/core/errors.js.map +7 -0
  4. package/dist/core/ids.js +30 -0
  5. package/dist/core/ids.js.map +7 -0
  6. package/dist/core/reconciler.js +149 -0
  7. package/dist/core/reconciler.js.map +7 -0
  8. package/dist/core/registry.js +72 -0
  9. package/dist/core/registry.js.map +7 -0
  10. package/dist/core/run-slice.js +210 -0
  11. package/dist/core/run-slice.js.map +7 -0
  12. package/dist/core/schema.js +100 -0
  13. package/dist/core/schema.js.map +7 -0
  14. package/dist/core/service.js +161 -0
  15. package/dist/core/service.js.map +7 -0
  16. package/dist/core/store.js +516 -0
  17. package/dist/core/store.js.map +7 -0
  18. package/dist/core/terminal.js +53 -0
  19. package/dist/core/terminal.js.map +7 -0
  20. package/dist/core/types.js +1 -0
  21. package/dist/core/types.js.map +7 -0
  22. package/dist/core/worker.js +111 -0
  23. package/dist/core/worker.js.map +7 -0
  24. package/dist/index.js +99 -0
  25. package/dist/index.js.map +7 -0
  26. package/dist/modules/durable_work/acl.js +10 -0
  27. package/dist/modules/durable_work/acl.js.map +7 -0
  28. package/dist/modules/durable_work/api/jobs/[id]/redrive.js +20 -0
  29. package/dist/modules/durable_work/api/jobs/[id]/redrive.js.map +7 -0
  30. package/dist/modules/durable_work/api/jobs/[id]/route.js +26 -0
  31. package/dist/modules/durable_work/api/jobs/[id]/route.js.map +7 -0
  32. package/dist/modules/durable_work/api/jobs/route.js +24 -0
  33. package/dist/modules/durable_work/api/jobs/route.js.map +7 -0
  34. package/dist/modules/durable_work/cli.js +72 -0
  35. package/dist/modules/durable_work/cli.js.map +7 -0
  36. package/dist/modules/durable_work/data/entities.js +163 -0
  37. package/dist/modules/durable_work/data/entities.js.map +7 -0
  38. package/dist/modules/durable_work/di.js +34 -0
  39. package/dist/modules/durable_work/di.js.map +7 -0
  40. package/dist/modules/durable_work/events.js +26 -0
  41. package/dist/modules/durable_work/events.js.map +7 -0
  42. package/dist/modules/durable_work/index.js +17 -0
  43. package/dist/modules/durable_work/index.js.map +7 -0
  44. package/dist/modules/durable_work/lib/route-helpers.js +66 -0
  45. package/dist/modules/durable_work/lib/route-helpers.js.map +7 -0
  46. package/dist/modules/durable_work/migrations/Migration20260908120000.js +17 -0
  47. package/dist/modules/durable_work/migrations/Migration20260908120000.js.map +7 -0
  48. package/dist/modules/durable_work/setup.js +12 -0
  49. package/dist/modules/durable_work/setup.js.map +7 -0
  50. package/dist/om/config.js +49 -0
  51. package/dist/om/config.js.map +7 -0
  52. package/dist/om/progress-mirror.js +49 -0
  53. package/dist/om/progress-mirror.js.map +7 -0
  54. package/dist/om/sql-executor-mikro.js +48 -0
  55. package/dist/om/sql-executor-mikro.js.map +7 -0
  56. package/dist/transport/bullmq.js +144 -0
  57. package/dist/transport/bullmq.js.map +7 -0
  58. package/dist/transport/conformance.js +177 -0
  59. package/dist/transport/conformance.js.map +7 -0
  60. package/dist/transport/memory.js +139 -0
  61. package/dist/transport/memory.js.map +7 -0
  62. package/dist/transport/pgboss.js +176 -0
  63. package/dist/transport/pgboss.js.map +7 -0
  64. package/dist/transport/types.js +1 -0
  65. package/dist/transport/types.js.map +7 -0
  66. package/generated/entities/durable_work_job/index.ts +42 -0
  67. package/generated/entities.ids.generated.ts +9 -0
  68. package/package.json +145 -0
  69. package/src/core/__tests__/registry.test.ts +43 -0
  70. package/src/core/errors.ts +104 -0
  71. package/src/core/ids.ts +58 -0
  72. package/src/core/reconciler.ts +242 -0
  73. package/src/core/registry.ts +199 -0
  74. package/src/core/run-slice.ts +343 -0
  75. package/src/core/schema.ts +114 -0
  76. package/src/core/service.ts +222 -0
  77. package/src/core/store.ts +786 -0
  78. package/src/core/terminal.ts +107 -0
  79. package/src/core/types.ts +120 -0
  80. package/src/core/worker.ts +169 -0
  81. package/src/index.ts +100 -0
  82. package/src/modules/durable_work/__integration__/TC-DW-001.spec.ts +51 -0
  83. package/src/modules/durable_work/__tests__/metadata.test.ts +13 -0
  84. package/src/modules/durable_work/__tests__/schema-agreement.test.ts +52 -0
  85. package/src/modules/durable_work/acl.ts +6 -0
  86. package/src/modules/durable_work/api/jobs/[id]/redrive.ts +27 -0
  87. package/src/modules/durable_work/api/jobs/[id]/route.ts +27 -0
  88. package/src/modules/durable_work/api/jobs/route.ts +26 -0
  89. package/src/modules/durable_work/cli.ts +91 -0
  90. package/src/modules/durable_work/data/entities.ts +158 -0
  91. package/src/modules/durable_work/di.ts +41 -0
  92. package/src/modules/durable_work/events.ts +30 -0
  93. package/src/modules/durable_work/index.ts +16 -0
  94. package/src/modules/durable_work/lib/route-helpers.ts +83 -0
  95. package/src/modules/durable_work/migrations/Migration20260908120000.ts +24 -0
  96. package/src/modules/durable_work/setup.ts +10 -0
  97. package/src/om/__tests__/sql-executor-mikro.test.ts +83 -0
  98. package/src/om/config.ts +65 -0
  99. package/src/om/progress-mirror.ts +80 -0
  100. package/src/om/sql-executor-mikro.ts +104 -0
  101. package/src/transport/bullmq.ts +213 -0
  102. package/src/transport/conformance.ts +218 -0
  103. package/src/transport/memory.ts +191 -0
  104. package/src/transport/pgboss.ts +250 -0
  105. package/src/transport/types.ts +81 -0
@@ -0,0 +1,516 @@
1
+ import {
2
+ LOCK_KEY_INDEX,
3
+ NO_ORG,
4
+ TABLE
5
+ } from "./schema.js";
6
+ import { LockKeyHeldError } from "./errors.js";
7
+ const COLUMNS = `
8
+ id, tenant_id, organization_id, kind, status, created_by, created_at, updated_at,
9
+ input, checkpoint, meta,
10
+ idempotency_key, lock_key, subject_type, subject_id, progress_job_id,
11
+ lease_owner, lease_epoch, lease_expires_at, heartbeat_at,
12
+ queue_name, queue_job_id, continuation_seq, redrives, next_run_at, pending_since,
13
+ redrives_since_commit, consecutive_failures, interruptions, mirror_attempts, last_committed_at,
14
+ started_at, finished_at, parked_at, cancel_requested_at, cancelled_by,
15
+ error_class, error_code, error_message, domain_mirrored_at,
16
+ processed_count, total_count`;
17
+ const num = (value) => typeof value === "number" ? value : Number(value ?? 0);
18
+ const date = (value) => value == null ? null : value instanceof Date ? value : new Date(String(value));
19
+ function mapRow(row) {
20
+ return {
21
+ id: String(row.id),
22
+ tenantId: String(row.tenant_id),
23
+ organizationId: row.organization_id == null ? null : String(row.organization_id),
24
+ kind: String(row.kind),
25
+ status: String(row.status),
26
+ createdBy: row.created_by == null ? null : String(row.created_by),
27
+ createdAt: date(row.created_at),
28
+ updatedAt: date(row.updated_at),
29
+ input: row.input ?? null,
30
+ checkpoint: row.checkpoint ?? null,
31
+ meta: row.meta ?? null,
32
+ idempotencyKey: row.idempotency_key == null ? null : String(row.idempotency_key),
33
+ lockKey: row.lock_key == null ? null : String(row.lock_key),
34
+ subjectType: row.subject_type == null ? null : String(row.subject_type),
35
+ subjectId: row.subject_id == null ? null : String(row.subject_id),
36
+ progressJobId: row.progress_job_id == null ? null : String(row.progress_job_id),
37
+ leaseOwner: row.lease_owner == null ? null : String(row.lease_owner),
38
+ leaseEpoch: num(row.lease_epoch),
39
+ leaseExpiresAt: date(row.lease_expires_at),
40
+ heartbeatAt: date(row.heartbeat_at),
41
+ queueName: row.queue_name == null ? null : String(row.queue_name),
42
+ queueJobId: row.queue_job_id == null ? null : String(row.queue_job_id),
43
+ continuationSeq: num(row.continuation_seq),
44
+ redrives: num(row.redrives),
45
+ nextRunAt: date(row.next_run_at),
46
+ pendingSince: date(row.pending_since),
47
+ redrivesSinceCommit: num(row.redrives_since_commit),
48
+ consecutiveFailures: num(row.consecutive_failures),
49
+ interruptions: num(row.interruptions),
50
+ mirrorAttempts: num(row.mirror_attempts),
51
+ lastCommittedAt: date(row.last_committed_at),
52
+ startedAt: date(row.started_at),
53
+ finishedAt: date(row.finished_at),
54
+ parkedAt: date(row.parked_at),
55
+ cancelRequestedAt: date(row.cancel_requested_at),
56
+ cancelledBy: row.cancelled_by == null ? null : String(row.cancelled_by),
57
+ errorClass: row.error_class == null ? null : String(row.error_class),
58
+ errorCode: row.error_code == null ? null : String(row.error_code),
59
+ errorMessage: row.error_message == null ? null : String(row.error_message),
60
+ domainMirroredAt: date(row.domain_mirrored_at),
61
+ processedCount: num(row.processed_count),
62
+ totalCount: row.total_count == null ? null : num(row.total_count)
63
+ };
64
+ }
65
+ const one = (result) => result.rows.length ? mapRow(result.rows[0]) : null;
66
+ const SCOPE = `tenant_id = $2 and (organization_id = $3 or ($3::uuid is null and organization_id is null))`;
67
+ function isUniqueViolation(error, index) {
68
+ const e = error;
69
+ if (!e || e.code !== "23505") return false;
70
+ if (typeof e.constraint === "string") return e.constraint === index;
71
+ return typeof e.message === "string" && e.message.includes(index);
72
+ }
73
+ async function insertJob(sql, id, scope, input, queueName) {
74
+ const params = [
75
+ id,
76
+ scope.tenantId,
77
+ scope.organizationId,
78
+ input.kind,
79
+ input.input ?? null,
80
+ input.meta ?? null,
81
+ input.idempotencyKey ?? null,
82
+ input.lockKey ?? null,
83
+ input.subject?.type ?? null,
84
+ input.subject?.id ?? null,
85
+ input.progressJobId ?? null,
86
+ input.createdBy ?? null,
87
+ queueName,
88
+ input.totalCount ?? null
89
+ ];
90
+ if (input.idempotencyKey) {
91
+ const existing = await findByIdempotencyKey(sql, scope, input.idempotencyKey);
92
+ if (existing) return { job: existing, created: false };
93
+ }
94
+ if (input.lockKey) {
95
+ const holder = await findLiveByLockKey(sql, scope, input.lockKey);
96
+ if (holder) throw new LockKeyHeldError(input.lockKey, holder.id);
97
+ }
98
+ try {
99
+ const inserted = await sql.query(
100
+ `insert into ${TABLE} (
101
+ id, tenant_id, organization_id, kind, status,
102
+ input, meta, idempotency_key, lock_key, subject_type, subject_id, progress_job_id,
103
+ created_by, queue_name, total_count, pending_since, created_at, updated_at
104
+ ) values (
105
+ $1, $2, $3, $4, 'pending',
106
+ $5::jsonb, $6::jsonb, $7, $8, $9, $10, $11,
107
+ $12, $13, $14, now(), now(), now()
108
+ ) returning ${COLUMNS}`,
109
+ params
110
+ );
111
+ return { job: mapRow(inserted.rows[0]), created: true };
112
+ } catch (error) {
113
+ if (input.lockKey && isUniqueViolation(error, LOCK_KEY_INDEX)) {
114
+ throw new LockKeyHeldError(input.lockKey);
115
+ }
116
+ throw error;
117
+ }
118
+ }
119
+ async function findByIdempotencyKey(sql, scope, key) {
120
+ return one(await sql.query(`select ${COLUMNS} from ${TABLE} where tenant_id = $1 and idempotency_key = $2`, [scope.tenantId, key]));
121
+ }
122
+ async function findLiveByLockKey(sql, scope, lockKey) {
123
+ return one(
124
+ await sql.query(
125
+ `select ${COLUMNS} from ${TABLE}
126
+ where lock_key = $1 and tenant_id = $2
127
+ and coalesce(organization_id, '${NO_ORG}'::uuid) = coalesce($3::uuid, '${NO_ORG}'::uuid)
128
+ and status in ('pending','running')
129
+ limit 1`,
130
+ [lockKey, scope.tenantId, scope.organizationId]
131
+ )
132
+ );
133
+ }
134
+ async function getJob(sql, id, scope) {
135
+ return one(await sql.query(`select ${COLUMNS} from ${TABLE} where id = $1 and ${SCOPE}`, [id, scope.tenantId, scope.organizationId]));
136
+ }
137
+ async function claim(sql, id, scope, delivery, owner, ttlMs) {
138
+ return one(
139
+ await sql.query(
140
+ `update ${TABLE}
141
+ set status = 'running',
142
+ lease_owner = $4,
143
+ lease_epoch = lease_epoch + 1,
144
+ lease_expires_at = now() + ($5::bigint * interval '1 millisecond'),
145
+ heartbeat_at = now(),
146
+ pending_since = null,
147
+ next_run_at = null,
148
+ started_at = coalesce(started_at, now()),
149
+ updated_at = now()
150
+ where id = $1 and ${SCOPE}
151
+ and status in ('pending','running')
152
+ and continuation_seq = $6 and redrives = $7
153
+ and (lease_expires_at is null or lease_expires_at < now())
154
+ returning ${COLUMNS}`,
155
+ [id, scope.tenantId, scope.organizationId, owner, ttlMs, delivery.seq, delivery.redrives]
156
+ )
157
+ );
158
+ }
159
+ async function heartbeat(sql, lease, patch = {}) {
160
+ const result = await sql.query(
161
+ `update ${TABLE}
162
+ set lease_expires_at = now() + ($4::bigint * interval '1 millisecond'),
163
+ heartbeat_at = now(),
164
+ processed_count = coalesce($5::int, processed_count),
165
+ total_count = case when $6::boolean then $7::int else total_count end,
166
+ consecutive_failures = case when $8::boolean then 0 else consecutive_failures end,
167
+ redrives_since_commit = case when $8::boolean then 0 else redrives_since_commit end,
168
+ last_committed_at = case when $8::boolean then now() else last_committed_at end
169
+ where id = $1 and status = 'running' and lease_owner = $2 and lease_epoch = $3
170
+ returning cancel_requested_at`,
171
+ [
172
+ lease.jobId,
173
+ lease.owner,
174
+ lease.epoch,
175
+ lease.ttlMs,
176
+ patch.processedCount ?? null,
177
+ // `totalCount: null` is a meaningful value ("unknown"), so presence and value are
178
+ // carried separately rather than collapsing both onto SQL NULL.
179
+ Object.prototype.hasOwnProperty.call(patch, "totalCount"),
180
+ patch.totalCount ?? null,
181
+ patch.committed === true
182
+ ]
183
+ );
184
+ if (!result.rows.length) return null;
185
+ return { cancelRequested: result.rows[0].cancel_requested_at != null };
186
+ }
187
+ async function assertLease(tx, lease) {
188
+ const result = await tx.query(
189
+ `select 1 from ${TABLE}
190
+ where id = $1 and status = 'running' and lease_owner = $2 and lease_epoch = $3
191
+ for update`,
192
+ [lease.jobId, lease.owner, lease.epoch]
193
+ );
194
+ return result.rows.length > 0;
195
+ }
196
+ async function writeCheckpoint(tx, lease, checkpoint, patch = {}) {
197
+ const result = await tx.query(
198
+ `update ${TABLE}
199
+ set checkpoint = $4::jsonb,
200
+ lease_expires_at = now() + ($5::bigint * interval '1 millisecond'),
201
+ heartbeat_at = now(),
202
+ processed_count = coalesce($6::int, processed_count),
203
+ total_count = case when $7::boolean then $8::int else total_count end,
204
+ consecutive_failures = 0,
205
+ redrives_since_commit = 0,
206
+ last_committed_at = now(),
207
+ updated_at = now()
208
+ where id = $1 and status = 'running' and lease_owner = $2 and lease_epoch = $3
209
+ returning id`,
210
+ [
211
+ lease.jobId,
212
+ lease.owner,
213
+ lease.epoch,
214
+ checkpoint ?? null,
215
+ lease.ttlMs,
216
+ patch.processedCount ?? null,
217
+ Object.prototype.hasOwnProperty.call(patch, "totalCount"),
218
+ patch.totalCount ?? null
219
+ ]
220
+ );
221
+ return result.rows.length > 0;
222
+ }
223
+ async function yieldSlice(sql, lease, opts) {
224
+ const result = await sql.query(
225
+ `update ${TABLE}
226
+ set status = 'pending',
227
+ continuation_seq = continuation_seq + 1,
228
+ interruptions = interruptions + case when $4::boolean then 1 else 0 end,
229
+ lease_owner = null,
230
+ lease_expires_at = now(),
231
+ pending_since = now(),
232
+ next_run_at = now(),
233
+ updated_at = now()
234
+ where id = $1 and status = 'running' and lease_owner = $2 and lease_epoch = $3
235
+ returning continuation_seq, redrives`,
236
+ [lease.jobId, lease.owner, lease.epoch, opts.interrupted]
237
+ );
238
+ if (!result.rows.length) return null;
239
+ return { seq: num(result.rows[0].continuation_seq), redrives: num(result.rows[0].redrives) };
240
+ }
241
+ async function failSlice(sql, lease, error, opts) {
242
+ const result = await sql.query(
243
+ `update ${TABLE}
244
+ set lease_owner = null,
245
+ lease_expires_at = now(),
246
+ next_run_at = case when $4::bigint is null then null else now() + ($4::bigint * interval '1 millisecond') end,
247
+ consecutive_failures = consecutive_failures + 1,
248
+ error_class = $5,
249
+ error_message = $6,
250
+ error_code = case
251
+ when $7::boolean then 'unrecoverable'
252
+ when consecutive_failures + 1 >= $8::int then 'retry_exhausted'
253
+ else $9 end,
254
+ updated_at = now()
255
+ where id = $1 and status = 'running' and lease_owner = $2 and lease_epoch = $3
256
+ returning consecutive_failures, error_code`,
257
+ [
258
+ lease.jobId,
259
+ lease.owner,
260
+ lease.epoch,
261
+ opts.nextAttemptDelayMs,
262
+ error.class,
263
+ error.message,
264
+ error.class === "unrecoverable",
265
+ opts.maxConsecutiveFailures,
266
+ error.code
267
+ ]
268
+ );
269
+ if (!result.rows.length) return null;
270
+ const code = result.rows[0].error_code;
271
+ const verdict = code === "unrecoverable" || code === "retry_exhausted" ? code : null;
272
+ return { consecutiveFailures: num(result.rows[0].consecutive_failures), verdict };
273
+ }
274
+ async function releaseLease(sql, lease, opts) {
275
+ return one(
276
+ await sql.query(
277
+ `update ${TABLE}
278
+ set lease_owner = null,
279
+ lease_expires_at = now(),
280
+ next_run_at = case when $4::bigint is null then null else now() + ($4::bigint * interval '1 millisecond') end,
281
+ updated_at = now()
282
+ where id = $1 and status = 'running' and lease_owner = $2 and lease_epoch = $3
283
+ returning ${COLUMNS}`,
284
+ [lease.jobId, lease.owner, lease.epoch, opts.nextAttemptDelayMs]
285
+ )
286
+ );
287
+ }
288
+ async function completeCas(tx, lease, patch = {}) {
289
+ return one(
290
+ await tx.query(
291
+ `update ${TABLE}
292
+ set status = 'completed', finished_at = now(), lease_owner = null, lease_expires_at = now(),
293
+ next_run_at = null, error_class = null, error_code = null, error_message = null,
294
+ processed_count = coalesce($3::int, processed_count),
295
+ total_count = coalesce($4::int, total_count),
296
+ updated_at = now()
297
+ where id = $1 and status = 'running' and lease_epoch = $2
298
+ returning ${COLUMNS}`,
299
+ [lease.jobId, lease.epoch, patch.processedCount ?? null, patch.totalCount ?? null]
300
+ )
301
+ );
302
+ }
303
+ async function failTerminalCas(tx, lease, verdict) {
304
+ return one(
305
+ await tx.query(
306
+ `update ${TABLE}
307
+ set status = 'failed', finished_at = now(), lease_owner = null, lease_expires_at = now(),
308
+ next_run_at = null, error_code = $3, error_class = $4,
309
+ error_message = coalesce($5, error_message), updated_at = now()
310
+ where id = $1 and status = 'running' and lease_epoch = $2
311
+ returning ${COLUMNS}`,
312
+ [lease.jobId, lease.epoch, verdict.code, verdict.class, verdict.message]
313
+ )
314
+ );
315
+ }
316
+ async function cancelCas(tx, lease) {
317
+ return one(
318
+ await tx.query(
319
+ `update ${TABLE}
320
+ set status = 'cancelled', finished_at = now(), lease_owner = null, lease_expires_at = now(),
321
+ next_run_at = null, updated_at = now()
322
+ where id = $1 and status = 'running' and lease_epoch = $2
323
+ returning ${COLUMNS}`,
324
+ [lease.jobId, lease.epoch]
325
+ )
326
+ );
327
+ }
328
+ async function markMirrored(tx, id) {
329
+ await tx.query(`update ${TABLE} set domain_mirrored_at = now() where id = $1`, [id]);
330
+ }
331
+ async function bumpMirrorAttempts(sql, id) {
332
+ const result = await sql.query(
333
+ `update ${TABLE} set mirror_attempts = mirror_attempts + 1, updated_at = now() where id = $1 returning mirror_attempts`,
334
+ [id]
335
+ );
336
+ return result.rows.length ? num(result.rows[0].mirror_attempts) : 0;
337
+ }
338
+ async function requestCancel(sql, id, scope, by) {
339
+ return one(
340
+ await sql.query(
341
+ `update ${TABLE}
342
+ set cancel_requested_at = coalesce(cancel_requested_at, now()), cancelled_by = coalesce(cancelled_by, $4), updated_at = now()
343
+ where id = $1 and ${SCOPE} and status in ('pending','running')
344
+ returning ${COLUMNS}`,
345
+ [id, scope.tenantId, scope.organizationId, by]
346
+ )
347
+ );
348
+ }
349
+ async function cancelPending(tx, id) {
350
+ return one(
351
+ await tx.query(
352
+ `update ${TABLE}
353
+ set status = 'cancelled', finished_at = now(), lease_owner = null, lease_expires_at = now(),
354
+ next_run_at = null, pending_since = null, updated_at = now()
355
+ where id = $1 and status = 'pending' and cancel_requested_at is not null
356
+ returning ${COLUMNS}`,
357
+ [id]
358
+ )
359
+ );
360
+ }
361
+ const REDRIVE_CORE = `status = 'pending', lease_owner = null, lease_epoch = lease_epoch + 1,
362
+ lease_expires_at = now(), redrives = redrives + 1, pending_since = now(), updated_at = now()`;
363
+ async function takeOrphan(sql, id, opts) {
364
+ return one(
365
+ await sql.query(
366
+ `update ${TABLE}
367
+ set ${REDRIVE_CORE}, redrives_since_commit = redrives_since_commit + 1,
368
+ next_run_at = now() + ($4::bigint * interval '1 millisecond')
369
+ where id = $1 and status = 'running'
370
+ and lease_expires_at < now() - ($2::bigint * interval '1 millisecond')
371
+ and (next_run_at is null or next_run_at < now() - ($3::bigint * interval '1 millisecond'))
372
+ and cancel_requested_at is null
373
+ and (error_code is null or error_code not in ('unrecoverable','retry_exhausted'))
374
+ returning ${COLUMNS}`,
375
+ [id, opts.graceMs, opts.pendingTtlMs, opts.backoffMs]
376
+ )
377
+ );
378
+ }
379
+ async function redrivePending(sql, id, opts) {
380
+ return one(
381
+ await sql.query(
382
+ `update ${TABLE}
383
+ set ${REDRIVE_CORE}, redrives_since_commit = redrives_since_commit + 1, next_run_at = now()
384
+ where id = $1 and status = 'pending'
385
+ and cancel_requested_at is null
386
+ and greatest(pending_since, coalesce(next_run_at, pending_since)) < now() - ($2::bigint * interval '1 millisecond')
387
+ returning ${COLUMNS}`,
388
+ [id, opts.pendingTtlMs]
389
+ )
390
+ );
391
+ }
392
+ async function park(sql, id, reason, message) {
393
+ return one(
394
+ await sql.query(
395
+ `update ${TABLE}
396
+ set status = 'failed', parked_at = now(), finished_at = now(),
397
+ lease_owner = null, lease_expires_at = now(), next_run_at = null, pending_since = null,
398
+ error_code = $2, error_class = coalesce(error_class, 'terminal'),
399
+ error_message = coalesce($3, error_message), updated_at = now()
400
+ where id = $1 and status in ('pending','running')
401
+ returning ${COLUMNS}`,
402
+ [id, reason, message]
403
+ )
404
+ );
405
+ }
406
+ async function operatorRedrive(tx, id, scope, opts) {
407
+ return one(
408
+ await tx.query(
409
+ `update ${TABLE}
410
+ set ${REDRIVE_CORE}, redrives_since_commit = 0, consecutive_failures = 0, mirror_attempts = 0,
411
+ parked_at = null, error_code = null, error_class = null, finished_at = null,
412
+ domain_mirrored_at = null, cancel_requested_at = null, cancelled_by = null, next_run_at = now()
413
+ where id = $1 and ${SCOPE}
414
+ and ($6::boolean or error_code is distinct from 'unrecoverable')
415
+ and (status = 'failed'
416
+ or (status = 'running'
417
+ and lease_expires_at < now() - ($4::bigint * interval '1 millisecond')
418
+ and (next_run_at is null or next_run_at < now() - ($5::bigint * interval '1 millisecond'))))
419
+ returning ${COLUMNS}`,
420
+ [id, scope.tenantId, scope.organizationId, opts.graceMs, opts.pendingTtlMs, opts.force]
421
+ )
422
+ );
423
+ }
424
+ async function recordEnqueue(sql, id, queueJobId, queueName) {
425
+ await sql.query(`update ${TABLE} set queue_job_id = $2, queue_name = $3 where id = $1`, [id, queueJobId, queueName]);
426
+ }
427
+ const SKIP_LOCKED = "for update skip locked";
428
+ async function selectCancelling(tx, limit, tenantId) {
429
+ const result = await tx.query(
430
+ `select ${COLUMNS} from ${TABLE}
431
+ where cancel_requested_at is not null and status in ('pending','running')
432
+ and ($2::uuid is null or tenant_id = $2)
433
+ order by cancel_requested_at asc limit $1 ${SKIP_LOCKED}`,
434
+ [limit, tenantId ?? null]
435
+ );
436
+ return result.rows.map(mapRow);
437
+ }
438
+ async function selectOrphans(tx, opts) {
439
+ const result = await tx.query(
440
+ `select ${COLUMNS} from ${TABLE}
441
+ where status = 'running'
442
+ and lease_expires_at < now() - ($1::bigint * interval '1 millisecond')
443
+ and (next_run_at is null or next_run_at < now() - ($2::bigint * interval '1 millisecond'))
444
+ and cancel_requested_at is null
445
+ and ($4::uuid is null or tenant_id = $4)
446
+ order by lease_expires_at asc limit $3 ${SKIP_LOCKED}`,
447
+ [opts.graceMs, opts.pendingTtlMs, opts.limit, opts.tenantId ?? null]
448
+ );
449
+ return result.rows.map(mapRow);
450
+ }
451
+ async function selectStalePending(tx, opts) {
452
+ const result = await tx.query(
453
+ `select ${COLUMNS} from ${TABLE}
454
+ where status = 'pending'
455
+ and cancel_requested_at is null
456
+ and greatest(pending_since, coalesce(next_run_at, pending_since)) < now() - ($1::bigint * interval '1 millisecond')
457
+ and ($3::uuid is null or tenant_id = $3)
458
+ order by pending_since asc limit $2 ${SKIP_LOCKED}`,
459
+ [opts.pendingTtlMs, opts.limit, opts.tenantId ?? null]
460
+ );
461
+ return result.rows.map(mapRow);
462
+ }
463
+ async function listJobs(sql, scope, filter = {}) {
464
+ const page = Math.max(1, filter.page ?? 1);
465
+ const pageSize = Math.min(200, Math.max(1, filter.pageSize ?? 20));
466
+ const where = ["tenant_id = $1 and (organization_id = $2 or ($2::uuid is null and organization_id is null))"];
467
+ const params = [scope.tenantId, scope.organizationId];
468
+ const push = (clause2, value) => {
469
+ params.push(value);
470
+ where.push(clause2.replace("$?", `$${params.length}`));
471
+ };
472
+ if (filter.kind) push("kind = $?", filter.kind);
473
+ if (filter.status) push("status = $?", filter.status);
474
+ if (filter.subject) {
475
+ push("subject_type = $?", filter.subject.type);
476
+ push("subject_id = $?", filter.subject.id);
477
+ }
478
+ const clause = where.join(" and ");
479
+ const total = await sql.query(`select count(*)::int as n from ${TABLE} where ${clause}`, params);
480
+ const items = await sql.query(
481
+ `select ${COLUMNS} from ${TABLE} where ${clause} order by created_at desc limit ${pageSize} offset ${(page - 1) * pageSize}`,
482
+ params
483
+ );
484
+ return { items: items.rows.map(mapRow), total: num(total.rows[0]?.n) };
485
+ }
486
+ export {
487
+ assertLease,
488
+ bumpMirrorAttempts,
489
+ cancelCas,
490
+ cancelPending,
491
+ claim,
492
+ completeCas,
493
+ failSlice,
494
+ failTerminalCas,
495
+ findByIdempotencyKey,
496
+ findLiveByLockKey,
497
+ getJob,
498
+ heartbeat,
499
+ insertJob,
500
+ listJobs,
501
+ mapRow,
502
+ markMirrored,
503
+ operatorRedrive,
504
+ park,
505
+ recordEnqueue,
506
+ redrivePending,
507
+ releaseLease,
508
+ requestCancel,
509
+ selectCancelling,
510
+ selectOrphans,
511
+ selectStalePending,
512
+ takeOrphan,
513
+ writeCheckpoint,
514
+ yieldSlice
515
+ };
516
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/core/store.ts"],
4
+ "sourcesContent": ["// Every statement that may write a job row lives here, and nowhere else.\n//\n// They are hand-written SQL rather than ORM calls because each one is a compare-and-set whose\n// WHERE clause *is* the guarantee \u2014 \"only the owner of epoch N may write this row\", \"only one\n// live job per lock key\". An ORM would put a translation layer between that predicate and the\n// person reviewing it, and these predicates are exactly what has to be reviewable.\n//\n// Two rules hold throughout:\n// 1. Time is the database's. A duration crosses the boundary; a worker-computed timestamp\n// never does. A worker five minutes ahead of Postgres would otherwise push every\n// reconciler predicate five minutes out.\n// 2. A verdict is decided inside the statement that has the row locked, from the row's\n// current counters \u2014 never from a value the caller read earlier.\n\nimport {\n LOCK_KEY_INDEX,\n NO_ORG,\n TABLE,\n} from './schema'\nimport { LockKeyHeldError } from './errors'\nimport type {\n DurableJob,\n DurableJobStatus,\n ErrorClass,\n Lease,\n ParkReason,\n Scope,\n SqlExecutor,\n SliceVerdict,\n StartJobInput,\n} from './types'\n\n/** Selected by every statement that returns a row, so the mapper always sees every column. */\nconst COLUMNS = `\n id, tenant_id, organization_id, kind, status, created_by, created_at, updated_at,\n input, checkpoint, meta,\n idempotency_key, lock_key, subject_type, subject_id, progress_job_id,\n lease_owner, lease_epoch, lease_expires_at, heartbeat_at,\n queue_name, queue_job_id, continuation_seq, redrives, next_run_at, pending_since,\n redrives_since_commit, consecutive_failures, interruptions, mirror_attempts, last_committed_at,\n started_at, finished_at, parked_at, cancel_requested_at, cancelled_by,\n error_class, error_code, error_message, domain_mirrored_at,\n processed_count, total_count`\n\ntype Row = Record<string, unknown>\n\nconst num = (value: unknown): number => (typeof value === 'number' ? value : Number(value ?? 0))\nconst date = (value: unknown): Date | null => (value == null ? null : value instanceof Date ? value : new Date(String(value)))\n\nexport function mapRow(row: Row): DurableJob {\n return {\n id: String(row.id),\n tenantId: String(row.tenant_id),\n organizationId: row.organization_id == null ? null : String(row.organization_id),\n kind: String(row.kind),\n status: String(row.status) as DurableJobStatus,\n createdBy: row.created_by == null ? null : String(row.created_by),\n createdAt: date(row.created_at)!,\n updatedAt: date(row.updated_at)!,\n\n input: row.input ?? null,\n checkpoint: row.checkpoint ?? null,\n meta: (row.meta ?? null) as Record<string, unknown> | null,\n\n idempotencyKey: row.idempotency_key == null ? null : String(row.idempotency_key),\n lockKey: row.lock_key == null ? null : String(row.lock_key),\n subjectType: row.subject_type == null ? null : String(row.subject_type),\n subjectId: row.subject_id == null ? null : String(row.subject_id),\n progressJobId: row.progress_job_id == null ? null : String(row.progress_job_id),\n\n leaseOwner: row.lease_owner == null ? null : String(row.lease_owner),\n leaseEpoch: num(row.lease_epoch),\n leaseExpiresAt: date(row.lease_expires_at),\n heartbeatAt: date(row.heartbeat_at),\n\n queueName: row.queue_name == null ? null : String(row.queue_name),\n queueJobId: row.queue_job_id == null ? null : String(row.queue_job_id),\n continuationSeq: num(row.continuation_seq),\n redrives: num(row.redrives),\n nextRunAt: date(row.next_run_at),\n pendingSince: date(row.pending_since),\n\n redrivesSinceCommit: num(row.redrives_since_commit),\n consecutiveFailures: num(row.consecutive_failures),\n interruptions: num(row.interruptions),\n mirrorAttempts: num(row.mirror_attempts),\n lastCommittedAt: date(row.last_committed_at),\n\n startedAt: date(row.started_at),\n finishedAt: date(row.finished_at),\n parkedAt: date(row.parked_at),\n cancelRequestedAt: date(row.cancel_requested_at),\n cancelledBy: row.cancelled_by == null ? null : String(row.cancelled_by),\n errorClass: row.error_class == null ? null : (String(row.error_class) as ErrorClass),\n errorCode: row.error_code == null ? null : String(row.error_code),\n errorMessage: row.error_message == null ? null : String(row.error_message),\n domainMirroredAt: date(row.domain_mirrored_at),\n\n processedCount: num(row.processed_count),\n totalCount: row.total_count == null ? null : num(row.total_count),\n }\n}\n\nconst one = (result: { rows: Row[] }): DurableJob | null => (result.rows.length ? mapRow(result.rows[0]!) : null)\n\n/** Scope predicate. A null organization matches only a null organization \u2014 it is a real value\n * (\"tenant-wide\"), not a wildcard, and treating it as one would leak jobs across orgs. */\nconst SCOPE = `tenant_id = $2 and (organization_id = $3 or ($3::uuid is null and organization_id is null))`\n\nfunction isUniqueViolation(error: unknown, index: string): boolean {\n const e = error as { code?: unknown; constraint?: unknown; message?: unknown } | null\n if (!e || e.code !== '23505') return false\n if (typeof e.constraint === 'string') return e.constraint === index\n return typeof e.message === 'string' && e.message.includes(index)\n}\n\n// ---------------------------------------------------------------------------------------\n// Creation\n// ---------------------------------------------------------------------------------------\n\nexport type InsertResult = { job: DurableJob; created: boolean }\n\n/**\n * Inserts a job, or returns the existing one when `idempotencyKey` has been used before.\n *\n * Runs on the caller's executor so it can be part of their transaction: creating the domain\n * row and the job row together is the entire point of the transactional-start guarantee, and\n * an enqueue that happens before that transaction commits is a delivery for a job that may\n * never exist.\n */\nexport async function insertJob(\n sql: SqlExecutor,\n id: string,\n scope: Scope,\n input: StartJobInput,\n queueName: string,\n): Promise<InsertResult> {\n const params = [\n id,\n scope.tenantId,\n scope.organizationId,\n input.kind,\n input.input ?? null,\n input.meta ?? null,\n input.idempotencyKey ?? null,\n input.lockKey ?? null,\n input.subject?.type ?? null,\n input.subject?.id ?? null,\n input.progressJobId ?? null,\n input.createdBy ?? null,\n queueName,\n input.totalCount ?? null,\n ]\n\n // Checked before the insert, not after the violation.\n //\n // A failed statement aborts the whole transaction in Postgres \u2014 every subsequent command is\n // refused until it ends. So a catch that queries for the existing job, or for the lock\n // holder, works on an autocommit connection and fails on the caller's transaction, which is\n // exactly where `start` is supposed to be called. The unique indexes are still the\n // authority: they close the race between this check and the insert, and a violation that\n // survives it is raised without touching the connection again.\n if (input.idempotencyKey) {\n const existing = await findByIdempotencyKey(sql, scope, input.idempotencyKey)\n if (existing) return { job: existing, created: false }\n }\n if (input.lockKey) {\n const holder = await findLiveByLockKey(sql, scope, input.lockKey)\n if (holder) throw new LockKeyHeldError(input.lockKey, holder.id)\n }\n\n try {\n const inserted = await sql.query<Row>(\n `insert into ${TABLE} (\n id, tenant_id, organization_id, kind, status,\n input, meta, idempotency_key, lock_key, subject_type, subject_id, progress_job_id,\n created_by, queue_name, total_count, pending_since, created_at, updated_at\n ) values (\n $1, $2, $3, $4, 'pending',\n $5::jsonb, $6::jsonb, $7, $8, $9, $10, $11,\n $12, $13, $14, now(), now(), now()\n ) returning ${COLUMNS}`,\n params,\n )\n return { job: mapRow(inserted.rows[0]!), created: true }\n } catch (error) {\n // Lost the race with a concurrent start. No further query is issued here \u2014 the transaction\n // is aborted, so any lookup would fail with \"current transaction is aborted\" and bury the\n // real cause. The holder's id is unavailable in this narrow case; the refusal is not.\n if (input.lockKey && isUniqueViolation(error, LOCK_KEY_INDEX)) {\n throw new LockKeyHeldError(input.lockKey)\n }\n throw error\n }\n}\n\nexport async function findByIdempotencyKey(sql: SqlExecutor, scope: Scope, key: string): Promise<DurableJob | null> {\n return one(await sql.query<Row>(`select ${COLUMNS} from ${TABLE} where tenant_id = $1 and idempotency_key = $2`, [scope.tenantId, key]))\n}\n\nexport async function findLiveByLockKey(sql: SqlExecutor, scope: Scope, lockKey: string): Promise<DurableJob | null> {\n return one(\n await sql.query<Row>(\n `select ${COLUMNS} from ${TABLE}\n where lock_key = $1 and tenant_id = $2\n and coalesce(organization_id, '${NO_ORG}'::uuid) = coalesce($3::uuid, '${NO_ORG}'::uuid)\n and status in ('pending','running')\n limit 1`,\n [lockKey, scope.tenantId, scope.organizationId],\n ),\n )\n}\n\nexport async function getJob(sql: SqlExecutor, id: string, scope: Scope): Promise<DurableJob | null> {\n return one(await sql.query<Row>(`select ${COLUMNS} from ${TABLE} where id = $1 and ${SCOPE}`, [id, scope.tenantId, scope.organizationId]))\n}\n\n// ---------------------------------------------------------------------------------------\n// The lease\n// ---------------------------------------------------------------------------------------\n\n/**\n * Accepts one delivery and takes the lease.\n *\n * Refuses unless `(continuation_seq, redrives)` still match what the delivery carries \u2014 that\n * pair is the fence against a straggling redelivery of a slice that has already moved on.\n *\n * Reads no scheduled time. `next_run_at` is written on the database's clock but every delivery\n * that carries one was scheduled by the *transport's* clock, so a `next_run_at <= now()` clause\n * would refuse a retry that arrives a few milliseconds early and lose it permanently. Stale\n * deliveries are refused by identity alone.\n *\n * Writes `next_run_at = null`: the scheduled delivery is now consumed. Without that, a worker\n * SIGKILLed mid-slice would leave a timestamp for a delivery no broker holds, and the\n * reconciler would wait out the whole pending TTL instead of the much shorter lease grace.\n */\nexport async function claim(\n sql: SqlExecutor,\n id: string,\n scope: Scope,\n delivery: { seq: number; redrives: number },\n owner: string,\n ttlMs: number,\n): Promise<DurableJob | null> {\n return one(\n await sql.query<Row>(\n `update ${TABLE}\n set status = 'running',\n lease_owner = $4,\n lease_epoch = lease_epoch + 1,\n lease_expires_at = now() + ($5::bigint * interval '1 millisecond'),\n heartbeat_at = now(),\n pending_since = null,\n next_run_at = null,\n started_at = coalesce(started_at, now()),\n updated_at = now()\n where id = $1 and ${SCOPE}\n and status in ('pending','running')\n and continuation_seq = $6 and redrives = $7\n and (lease_expires_at is null or lease_expires_at < now())\n returning ${COLUMNS}`,\n [id, scope.tenantId, scope.organizationId, owner, ttlMs, delivery.seq, delivery.redrives],\n ),\n )\n}\n\nexport type HeartbeatPatch = { processedCount?: number; totalCount?: number | null; committed?: boolean }\n\n/**\n * Extends the lease and optionally records progress. Returns null when the lease is gone \u2014\n * the slice must then abort, because someone else now owns the job.\n *\n * `committed: true` means the slice durably committed a unit of work. That resets both\n * budgets: a job that is making progress has not earned any of the suspicion those counters\n * represent, however many times it was interrupted getting there.\n *\n * Touches no indexed column, so it stays a HOT update. That matters at the cadence a\n * multi-day run heartbeats at.\n */\nexport async function heartbeat(\n sql: SqlExecutor,\n lease: Lease,\n patch: HeartbeatPatch = {},\n): Promise<{ cancelRequested: boolean } | null> {\n const result = await sql.query<Row>(\n `update ${TABLE}\n set lease_expires_at = now() + ($4::bigint * interval '1 millisecond'),\n heartbeat_at = now(),\n processed_count = coalesce($5::int, processed_count),\n total_count = case when $6::boolean then $7::int else total_count end,\n consecutive_failures = case when $8::boolean then 0 else consecutive_failures end,\n redrives_since_commit = case when $8::boolean then 0 else redrives_since_commit end,\n last_committed_at = case when $8::boolean then now() else last_committed_at end\n where id = $1 and status = 'running' and lease_owner = $2 and lease_epoch = $3\n returning cancel_requested_at`,\n [\n lease.jobId,\n lease.owner,\n lease.epoch,\n lease.ttlMs,\n patch.processedCount ?? null,\n // `totalCount: null` is a meaningful value (\"unknown\"), so presence and value are\n // carried separately rather than collapsing both onto SQL NULL.\n Object.prototype.hasOwnProperty.call(patch, 'totalCount'),\n patch.totalCount ?? null,\n patch.committed === true,\n ],\n )\n if (!result.rows.length) return null\n return { cancelRequested: result.rows[0]!.cancel_requested_at != null }\n}\n\n/**\n * Re-asserts the lease inside the caller's transaction.\n *\n * This is what makes `fencedWrite` a fence rather than a hope: the slice's domain writes and\n * this check commit or roll back together, so a worker whose lease expired mid-transaction\n * cannot land a write that outlives its right to make one.\n */\nexport async function assertLease(tx: SqlExecutor, lease: Lease): Promise<boolean> {\n const result = await tx.query<Row>(\n `select 1 from ${TABLE}\n where id = $1 and status = 'running' and lease_owner = $2 and lease_epoch = $3\n for update`,\n [lease.jobId, lease.owner, lease.epoch],\n )\n return result.rows.length > 0\n}\n\n/** Records a checkpoint under the fence, and counts as a committed unit. */\nexport async function writeCheckpoint(\n tx: SqlExecutor,\n lease: Lease,\n checkpoint: unknown,\n patch: HeartbeatPatch = {},\n): Promise<boolean> {\n const result = await tx.query<Row>(\n `update ${TABLE}\n set checkpoint = $4::jsonb,\n lease_expires_at = now() + ($5::bigint * interval '1 millisecond'),\n heartbeat_at = now(),\n processed_count = coalesce($6::int, processed_count),\n total_count = case when $7::boolean then $8::int else total_count end,\n consecutive_failures = 0,\n redrives_since_commit = 0,\n last_committed_at = now(),\n updated_at = now()\n where id = $1 and status = 'running' and lease_owner = $2 and lease_epoch = $3\n returning id`,\n [\n lease.jobId,\n lease.owner,\n lease.epoch,\n checkpoint ?? null,\n lease.ttlMs,\n patch.processedCount ?? null,\n Object.prototype.hasOwnProperty.call(patch, 'totalCount'),\n patch.totalCount ?? null,\n ],\n )\n return result.rows.length > 0\n}\n\n/**\n * Hands the remaining work back: the slice spent its budget or was asked to stop.\n *\n * Bumps `continuation_seq`, which invalidates the current delivery id and mints the next one.\n * Spends no retry attempt and touches no failure counter \u2014 yielding is the mechanism working,\n * not failing, and counting it would eventually park a perfectly healthy long job.\n */\nexport async function yieldSlice(\n sql: SqlExecutor,\n lease: Lease,\n opts: { interrupted: boolean },\n): Promise<{ seq: number; redrives: number } | null> {\n const result = await sql.query<Row>(\n `update ${TABLE}\n set status = 'pending',\n continuation_seq = continuation_seq + 1,\n interruptions = interruptions + case when $4::boolean then 1 else 0 end,\n lease_owner = null,\n lease_expires_at = now(),\n pending_since = now(),\n next_run_at = now(),\n updated_at = now()\n where id = $1 and status = 'running' and lease_owner = $2 and lease_epoch = $3\n returning continuation_seq, redrives`,\n [lease.jobId, lease.owner, lease.epoch, opts.interrupted],\n )\n if (!result.rows.length) return null\n return { seq: num(result.rows[0]!.continuation_seq), redrives: num(result.rows[0]!.redrives) }\n}\n\n/**\n * The slice threw. Stays `running` and releases the lease so the transport's next attempt can\n * claim the same `(seq, redrives)`.\n *\n * The verdict is decided *here*, from the counter this statement increments \u2014 never from a\n * value read at claim time. A committed heartbeat earlier in this same slice reset\n * `consecutive_failures` to zero, and the CASE below sees that reset, so a slice that made\n * progress cannot be failed terminally on a stale count.\n */\nexport async function failSlice(\n sql: SqlExecutor,\n lease: Lease,\n error: { message: string; code: string | null; class: ErrorClass },\n opts: { nextAttemptDelayMs: number | null; maxConsecutiveFailures: number },\n): Promise<{ consecutiveFailures: number; verdict: SliceVerdict | null } | null> {\n const result = await sql.query<Row>(\n `update ${TABLE}\n set lease_owner = null,\n lease_expires_at = now(),\n next_run_at = case when $4::bigint is null then null else now() + ($4::bigint * interval '1 millisecond') end,\n consecutive_failures = consecutive_failures + 1,\n error_class = $5,\n error_message = $6,\n error_code = case\n when $7::boolean then 'unrecoverable'\n when consecutive_failures + 1 >= $8::int then 'retry_exhausted'\n else $9 end,\n updated_at = now()\n where id = $1 and status = 'running' and lease_owner = $2 and lease_epoch = $3\n returning consecutive_failures, error_code`,\n [\n lease.jobId,\n lease.owner,\n lease.epoch,\n opts.nextAttemptDelayMs,\n error.class,\n error.message,\n error.class === 'unrecoverable',\n opts.maxConsecutiveFailures,\n error.code,\n ],\n )\n if (!result.rows.length) return null\n const code = result.rows[0]!.error_code\n const verdict = code === 'unrecoverable' || code === 'retry_exhausted' ? (code as SliceVerdict) : null\n return { consecutiveFailures: num(result.rows[0]!.consecutive_failures), verdict }\n}\n\n/**\n * Releases the lease without counting anything.\n *\n * Used when a terminal transaction rolled back while this delivery still held the lease. A\n * mirror failure is not a slice failure: counting it would burn the retry budget for something\n * the work itself did not do, and would clear a cancellation the operator is still waiting on.\n */\nexport async function releaseLease(\n sql: SqlExecutor,\n lease: Lease,\n opts: { nextAttemptDelayMs: number | null },\n): Promise<DurableJob | null> {\n return one(\n await sql.query<Row>(\n `update ${TABLE}\n set lease_owner = null,\n lease_expires_at = now(),\n next_run_at = case when $4::bigint is null then null else now() + ($4::bigint * interval '1 millisecond') end,\n updated_at = now()\n where id = $1 and status = 'running' and lease_owner = $2 and lease_epoch = $3\n returning ${COLUMNS}`,\n [lease.jobId, lease.owner, lease.epoch, opts.nextAttemptDelayMs],\n ),\n )\n}\n\n// ---------------------------------------------------------------------------------------\n// Terminal transitions \u2014 run inside the terminal transaction (see terminal.ts)\n// ---------------------------------------------------------------------------------------\n\n/**\n * Fenced on the epoch alone, not on the owner.\n *\n * The fence has to match both of its callers' lease states: on the ordinary failure path\n * `failSlice` has already nulled `lease_owner`, while on the retry-of-a-verdict path the\n * claim re-acquired it. The epoch is what both have in common and is what actually identifies\n * the generation of the lease.\n */\nexport async function completeCas(tx: SqlExecutor, lease: Lease, patch: { processedCount?: number; totalCount?: number | null } = {}): Promise<DurableJob | null> {\n return one(\n await tx.query<Row>(\n `update ${TABLE}\n set status = 'completed', finished_at = now(), lease_owner = null, lease_expires_at = now(),\n next_run_at = null, error_class = null, error_code = null, error_message = null,\n processed_count = coalesce($3::int, processed_count),\n total_count = coalesce($4::int, total_count),\n updated_at = now()\n where id = $1 and status = 'running' and lease_epoch = $2\n returning ${COLUMNS}`,\n [lease.jobId, lease.epoch, patch.processedCount ?? null, patch.totalCount ?? null],\n ),\n )\n}\n\nexport async function failTerminalCas(\n tx: SqlExecutor,\n lease: Lease,\n verdict: { code: string; class: ErrorClass; message: string | null },\n): Promise<DurableJob | null> {\n return one(\n await tx.query<Row>(\n `update ${TABLE}\n set status = 'failed', finished_at = now(), lease_owner = null, lease_expires_at = now(),\n next_run_at = null, error_code = $3, error_class = $4,\n error_message = coalesce($5, error_message), updated_at = now()\n where id = $1 and status = 'running' and lease_epoch = $2\n returning ${COLUMNS}`,\n [lease.jobId, lease.epoch, verdict.code, verdict.class, verdict.message],\n ),\n )\n}\n\nexport async function cancelCas(tx: SqlExecutor, lease: Lease): Promise<DurableJob | null> {\n return one(\n await tx.query<Row>(\n `update ${TABLE}\n set status = 'cancelled', finished_at = now(), lease_owner = null, lease_expires_at = now(),\n next_run_at = null, updated_at = now()\n where id = $1 and status = 'running' and lease_epoch = $2\n returning ${COLUMNS}`,\n [lease.jobId, lease.epoch],\n ),\n )\n}\n\n/** Marks the domain mirror as landed. Called inside the same transaction as the CAS above. */\nexport async function markMirrored(tx: SqlExecutor, id: string): Promise<void> {\n await tx.query(`update ${TABLE} set domain_mirrored_at = now() where id = $1`, [id])\n}\n\n/**\n * Counts a failed mirror attempt.\n *\n * Deliberately its own autocommit statement, never part of the transaction it is counting \u2014\n * that transaction rolled back, and a counter written inside it would roll back with it,\n * leaving a job that retries its mirror forever with nothing to show for it.\n */\nexport async function bumpMirrorAttempts(sql: SqlExecutor, id: string): Promise<number> {\n const result = await sql.query<Row>(\n `update ${TABLE} set mirror_attempts = mirror_attempts + 1, updated_at = now() where id = $1 returning mirror_attempts`,\n [id],\n )\n return result.rows.length ? num(result.rows[0]!.mirror_attempts) : 0\n}\n\n// ---------------------------------------------------------------------------------------\n// Cancellation\n// ---------------------------------------------------------------------------------------\n\n/** Records the intent. A running slice observes it at its next heartbeat; a pending job is\n * ended by the reconciler. Cancellation is never a write to `status` from outside. */\nexport async function requestCancel(sql: SqlExecutor, id: string, scope: Scope, by: string | null): Promise<DurableJob | null> {\n return one(\n await sql.query<Row>(\n `update ${TABLE}\n set cancel_requested_at = coalesce(cancel_requested_at, now()), cancelled_by = coalesce(cancelled_by, $4), updated_at = now()\n where id = $1 and ${SCOPE} and status in ('pending','running')\n returning ${COLUMNS}`,\n [id, scope.tenantId, scope.organizationId, by],\n ),\n )\n}\n\n/** Ends a job that was cancelled while it was not running. Used by the reconciler's cancel\n * query, where there is no lease to fence on \u2014 the `pending` status is the fence. */\nexport async function cancelPending(tx: SqlExecutor, id: string): Promise<DurableJob | null> {\n return one(\n await tx.query<Row>(\n `update ${TABLE}\n set status = 'cancelled', finished_at = now(), lease_owner = null, lease_expires_at = now(),\n next_run_at = null, pending_since = null, updated_at = now()\n where id = $1 and status = 'pending' and cancel_requested_at is not null\n returning ${COLUMNS}`,\n [id],\n ),\n )\n}\n\n// ---------------------------------------------------------------------------------------\n// The re-drive family. Three statements, one core; they differ only in predicate and budget.\n// ---------------------------------------------------------------------------------------\n\nconst REDRIVE_CORE = `status = 'pending', lease_owner = null, lease_epoch = lease_epoch + 1,\n lease_expires_at = now(), redrives = redrives + 1, pending_since = now(), updated_at = now()`\n\n/**\n * The reconciler takes an orphan: a running job whose driver stopped heartbeating.\n *\n * Two tolerances, deliberately different. `lease_expires_at` is a database-clock fact about a\n * driver, so a short grace suffices. `next_run_at` is when the *transport* makes a delivery\n * available, not when a worker picks it up \u2014 behind a busy queue that can be minutes \u2014 so a\n * scheduled delivery gets the same generous tolerance a pending job gets. Using the short\n * grace for both would take healthy jobs whose retry is merely queued, spend their orphan\n * budget, and park them as poison after a few busy periods.\n */\nexport async function takeOrphan(\n sql: SqlExecutor,\n id: string,\n opts: { graceMs: number; pendingTtlMs: number; backoffMs: number },\n): Promise<DurableJob | null> {\n return one(\n await sql.query<Row>(\n `update ${TABLE}\n set ${REDRIVE_CORE}, redrives_since_commit = redrives_since_commit + 1,\n next_run_at = now() + ($4::bigint * interval '1 millisecond')\n where id = $1 and status = 'running'\n and lease_expires_at < now() - ($2::bigint * interval '1 millisecond')\n and (next_run_at is null or next_run_at < now() - ($3::bigint * interval '1 millisecond'))\n and cancel_requested_at is null\n and (error_code is null or error_code not in ('unrecoverable','retry_exhausted'))\n returning ${COLUMNS}`,\n [id, opts.graceMs, opts.pendingTtlMs, opts.backoffMs],\n ),\n )\n}\n\n/** Re-drives a pending job whose delivery was never picked up \u2014 a lost hand-back or an\n * enqueue that never reached the broker. */\nexport async function redrivePending(sql: SqlExecutor, id: string, opts: { pendingTtlMs: number }): Promise<DurableJob | null> {\n return one(\n await sql.query<Row>(\n `update ${TABLE}\n set ${REDRIVE_CORE}, redrives_since_commit = redrives_since_commit + 1, next_run_at = now()\n where id = $1 and status = 'pending'\n and cancel_requested_at is null\n and greatest(pending_since, coalesce(next_run_at, pending_since)) < now() - ($2::bigint * interval '1 millisecond')\n returning ${COLUMNS}`,\n [id, opts.pendingTtlMs],\n ),\n )\n}\n\n/** Parks a job the reconciler will not re-drive again. `failed` with a reason, and re-drivable\n * only by an operator \u2014 which is the point: something needs a human before it runs again. */\nexport async function park(sql: SqlExecutor, id: string, reason: ParkReason, message: string | null): Promise<DurableJob | null> {\n return one(\n await sql.query<Row>(\n `update ${TABLE}\n set status = 'failed', parked_at = now(), finished_at = now(),\n lease_owner = null, lease_expires_at = now(), next_run_at = null, pending_since = null,\n error_code = $2, error_class = coalesce(error_class, 'terminal'),\n error_message = coalesce($3, error_message), updated_at = now()\n where id = $1 and status in ('pending','running')\n returning ${COLUMNS}`,\n [id, reason, message],\n ),\n )\n}\n\n/**\n * The operator's way out of `failed` \u2014 parked or terminal \u2014 and the manual take of an\n * expired lease.\n *\n * Resets both budgets, because an operator asking for a re-drive is explicitly asking for more\n * attempts. Does not reset `redrives`: that is identity, not budget, and a retained transport\n * job may still carry the old pair. Clears `cancel_requested_at` too, so a job that failed\n * before its slice ever observed the cancellation is not immediately cancelled again by the\n * re-driven slice's first heartbeat \u2014 the explicit re-drive supersedes the never-honoured\n * request.\n *\n * `completed` and `cancelled` are never re-drivable: the first is done, the second was asked\n * for. Start a new job instead.\n */\nexport async function operatorRedrive(\n tx: SqlExecutor,\n id: string,\n scope: Scope,\n opts: { graceMs: number; pendingTtlMs: number; force: boolean },\n): Promise<DurableJob | null> {\n return one(\n await tx.query<Row>(\n `update ${TABLE}\n set ${REDRIVE_CORE}, redrives_since_commit = 0, consecutive_failures = 0, mirror_attempts = 0,\n parked_at = null, error_code = null, error_class = null, finished_at = null,\n domain_mirrored_at = null, cancel_requested_at = null, cancelled_by = null, next_run_at = now()\n where id = $1 and ${SCOPE}\n and ($6::boolean or error_code is distinct from 'unrecoverable')\n and (status = 'failed'\n or (status = 'running'\n and lease_expires_at < now() - ($4::bigint * interval '1 millisecond')\n and (next_run_at is null or next_run_at < now() - ($5::bigint * interval '1 millisecond'))))\n returning ${COLUMNS}`,\n [id, scope.tenantId, scope.organizationId, opts.graceMs, opts.pendingTtlMs, opts.force],\n ),\n )\n}\n\n/** Records the delivery the transport accepted, so a later cancel can remove it. */\nexport async function recordEnqueue(sql: SqlExecutor, id: string, queueJobId: string, queueName: string): Promise<void> {\n await sql.query(`update ${TABLE} set queue_job_id = $2, queue_name = $3 where id = $1`, [id, queueJobId, queueName])\n}\n\n// ---------------------------------------------------------------------------------------\n// Reconciler candidate selection\n// ---------------------------------------------------------------------------------------\n\n/** `for update skip locked` is what lets two reconcilers run at once: each takes a disjoint\n * slice of the candidates rather than one blocking the other or both acting on the same row. */\nconst SKIP_LOCKED = 'for update skip locked'\n\n/** The reconciler is system-wide by default. The optional tenant filter exists so a fleet can\n * shard the loop \u2014 one worker per tenant group \u2014 rather than having every worker scan every\n * tenant's rows and skip-lock its way past them. */\nexport async function selectCancelling(tx: SqlExecutor, limit: number, tenantId?: string): Promise<DurableJob[]> {\n const result = await tx.query<Row>(\n `select ${COLUMNS} from ${TABLE}\n where cancel_requested_at is not null and status in ('pending','running')\n and ($2::uuid is null or tenant_id = $2)\n order by cancel_requested_at asc limit $1 ${SKIP_LOCKED}`,\n [limit, tenantId ?? null],\n )\n return result.rows.map(mapRow)\n}\n\nexport async function selectOrphans(\n tx: SqlExecutor,\n opts: { graceMs: number; pendingTtlMs: number; limit: number; tenantId?: string },\n): Promise<DurableJob[]> {\n const result = await tx.query<Row>(\n `select ${COLUMNS} from ${TABLE}\n where status = 'running'\n and lease_expires_at < now() - ($1::bigint * interval '1 millisecond')\n and (next_run_at is null or next_run_at < now() - ($2::bigint * interval '1 millisecond'))\n and cancel_requested_at is null\n and ($4::uuid is null or tenant_id = $4)\n order by lease_expires_at asc limit $3 ${SKIP_LOCKED}`,\n [opts.graceMs, opts.pendingTtlMs, opts.limit, opts.tenantId ?? null],\n )\n return result.rows.map(mapRow)\n}\n\nexport async function selectStalePending(\n tx: SqlExecutor,\n opts: { pendingTtlMs: number; limit: number; tenantId?: string },\n): Promise<DurableJob[]> {\n const result = await tx.query<Row>(\n `select ${COLUMNS} from ${TABLE}\n where status = 'pending'\n and cancel_requested_at is null\n and greatest(pending_since, coalesce(next_run_at, pending_since)) < now() - ($1::bigint * interval '1 millisecond')\n and ($3::uuid is null or tenant_id = $3)\n order by pending_since asc limit $2 ${SKIP_LOCKED}`,\n [opts.pendingTtlMs, opts.limit, opts.tenantId ?? null],\n )\n return result.rows.map(mapRow)\n}\n\n// ---------------------------------------------------------------------------------------\n// Read side\n// ---------------------------------------------------------------------------------------\n\nexport type ListFilter = {\n kind?: string\n status?: DurableJobStatus\n subject?: { type: string; id: string }\n page?: number\n pageSize?: number\n}\n\nexport async function listJobs(sql: SqlExecutor, scope: Scope, filter: ListFilter = {}): Promise<{ items: DurableJob[]; total: number }> {\n const page = Math.max(1, filter.page ?? 1)\n const pageSize = Math.min(200, Math.max(1, filter.pageSize ?? 20))\n // Written out rather than reusing SCOPE: that constant is numbered for statements whose\n // first parameter is the job id, and renumbering it by string replacement is precisely the\n // kind of cleverness that silently changes a predicate.\n const where: string[] = ['tenant_id = $1 and (organization_id = $2 or ($2::uuid is null and organization_id is null))']\n const params: unknown[] = [scope.tenantId, scope.organizationId]\n const push = (clause: string, value: unknown) => {\n params.push(value)\n where.push(clause.replace('$?', `$${params.length}`))\n }\n if (filter.kind) push('kind = $?', filter.kind)\n if (filter.status) push('status = $?', filter.status)\n if (filter.subject) {\n push('subject_type = $?', filter.subject.type)\n push('subject_id = $?', filter.subject.id)\n }\n const clause = where.join(' and ')\n const total = await sql.query<Row>(`select count(*)::int as n from ${TABLE} where ${clause}`, params)\n const items = await sql.query<Row>(\n `select ${COLUMNS} from ${TABLE} where ${clause} order by created_at desc limit ${pageSize} offset ${(page - 1) * pageSize}`,\n params,\n )\n return { items: items.rows.map(mapRow), total: num(total.rows[0]?.n) }\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["clause"]
7
+ }