@workglow/postgres 0.2.37 → 0.3.1

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.
@@ -299,6 +299,42 @@ function postgresQueueMigrations(tableName, prefixes) {
299
299
  ON ${tableName} (${prefixIndexPrefix}queue, status, visible_at)
300
300
  `);
301
301
  }
302
+ },
303
+ {
304
+ component,
305
+ version: 4,
306
+ description: "Add UNIQUE partial index for findActiveByFingerprint O(1) lookup + fingerprint dedup at the DB layer (H2)",
307
+ async up(db) {
308
+ await db.query(`
309
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_${tableName}_fingerprint_active
310
+ ON ${tableName}(${prefixIndexPrefix}queue, fingerprint)
311
+ WHERE status IN ('PENDING','PROCESSING')
312
+ `);
313
+ }
314
+ },
315
+ {
316
+ component,
317
+ version: 5,
318
+ description: "Converge idx_<table>_fingerprint_active to UNIQUE for DBs that applied the pre-edit v4 (non-unique) variant",
319
+ async up(db) {
320
+ const indexName = `idx_${tableName}_fingerprint_active`;
321
+ const result = await db.query(`SELECT i.indisunique
322
+ FROM pg_class c
323
+ JOIN pg_index i ON i.indexrelid = c.oid
324
+ JOIN pg_namespace n ON n.oid = c.relnamespace
325
+ WHERE c.relname = $1
326
+ AND n.nspname = current_schema()`, [indexName]);
327
+ const existing = result.rows[0];
328
+ if (existing && existing.indisunique) {
329
+ return;
330
+ }
331
+ await db.query(`DROP INDEX IF EXISTS ${indexName}`);
332
+ await db.query(`
333
+ CREATE UNIQUE INDEX IF NOT EXISTS ${indexName}
334
+ ON ${tableName}(${prefixIndexPrefix}queue, fingerprint)
335
+ WHERE status IN ('PENDING','PROCESSING')
336
+ `);
337
+ }
302
338
  }
303
339
  ];
304
340
  }
@@ -347,7 +383,7 @@ class PostgresQueueStorage {
347
383
  const now = new Date().toISOString();
348
384
  job.queue = this.queueName;
349
385
  job.job_run_id = job.job_run_id ?? uuid4();
350
- job.fingerprint = await makeFingerprint(job.input);
386
+ job.fingerprint = job.fingerprint ?? await makeFingerprint(job.input);
351
387
  job.status = JobStatus2.PENDING;
352
388
  job.progress = 0;
353
389
  job.progress_message = "";
@@ -389,7 +425,22 @@ class PostgresQueueStorage {
389
425
  job.progress_message,
390
426
  job.progress_details ? JSON.stringify(job.progress_details) : null
391
427
  ];
392
- const result = await this.db.query(sql, params);
428
+ let result;
429
+ try {
430
+ result = await this.db.query(sql, params);
431
+ } catch (err) {
432
+ const e = err;
433
+ const isUniqueViolation = e?.code === "23505";
434
+ const involvesFingerprint = typeof e?.constraint === "string" ? e.constraint.includes("fingerprint") : typeof e?.message === "string" ? e.message.includes("fingerprint") : false;
435
+ if (isUniqueViolation && involvesFingerprint && job.fingerprint) {
436
+ const winner = await this.findActiveByFingerprint(job.fingerprint, this.queueName);
437
+ if (winner?.id != null) {
438
+ job.id = winner.id;
439
+ return winner.id;
440
+ }
441
+ }
442
+ throw err;
443
+ }
393
444
  if (!result)
394
445
  throw new Error("Failed to add to queue");
395
446
  job.id = result.rows[0].id;
@@ -580,6 +631,8 @@ class PostgresQueueStorage {
580
631
  if ("progress_details" in fields) {
581
632
  push("progress_details", fields.progress_details != null ? JSON.stringify(fields.progress_details) : null);
582
633
  }
634
+ if ("visible_at" in fields)
635
+ push("visible_at", fields.visible_at ?? null);
583
636
  if (sets.length === 0)
584
637
  return;
585
638
  const idParam = nextParam;
@@ -668,6 +721,66 @@ class PostgresQueueStorage {
668
721
  const { conditions: prefixConditions, params: prefixParams } = this.buildPrefixWhereClause(3);
669
722
  await this.db.query(`DELETE FROM ${this.tableName} WHERE id = $1 AND queue = $2${prefixConditions}`, [jobId, this.queueName, ...prefixParams]);
670
723
  }
724
+ async findActiveByFingerprint(fingerprint, queueName) {
725
+ const { conditions: prefixConditions, params: prefixParams } = this.buildPrefixWhereClause(3);
726
+ const result = await this.db.query(`SELECT * FROM ${this.tableName}
727
+ WHERE fingerprint = $1 AND queue = $2
728
+ AND status IN ('PENDING','PROCESSING')${prefixConditions}
729
+ ORDER BY created_at DESC
730
+ LIMIT 1`, [fingerprint, queueName, ...prefixParams]);
731
+ if (!result || result.rows.length === 0)
732
+ return;
733
+ return result.rows[0];
734
+ }
735
+ async getMany(ids) {
736
+ if (ids.length === 0)
737
+ return [];
738
+ const numericIds = ids.map((id) => Number(id)).filter((n) => Number.isInteger(n) && n > 0);
739
+ if (numericIds.length === 0)
740
+ return ids.map(() => {
741
+ return;
742
+ });
743
+ const { conditions: prefixConditions, params: prefixParams } = this.buildPrefixWhereClause(3);
744
+ const result = await this.db.query(`SELECT * FROM ${this.tableName}
745
+ WHERE id = ANY($1::int[]) AND queue = $2${prefixConditions}`, [numericIds, this.queueName, ...prefixParams]);
746
+ if (!result)
747
+ return ids.map(() => {
748
+ return;
749
+ });
750
+ const map = new Map;
751
+ for (const row of result.rows) {
752
+ map.set(Number(row.id), row);
753
+ }
754
+ return ids.map((id) => map.get(Number(id)));
755
+ }
756
+ async completeWithResult(id, result) {
757
+ const { conditions: prefixConditions, params: prefixParams } = this.buildPrefixWhereClause(3);
758
+ await this.db.query(`UPDATE ${this.tableName}
759
+ SET output = $1,
760
+ status = 'COMPLETED',
761
+ progress = 100,
762
+ progress_message = '',
763
+ progress_details = NULL,
764
+ completed_at = NOW() AT TIME ZONE 'UTC'
765
+ WHERE id = $2 AND queue = $3${prefixConditions}`, [result != null ? JSON.stringify(result) : null, id, this.queueName, ...prefixParams]);
766
+ }
767
+ async failWithError(id, opts) {
768
+ const { conditions: prefixConditions, params: prefixParams } = this.buildPrefixWhereClause(5);
769
+ await this.db.query(`UPDATE ${this.tableName}
770
+ SET error = COALESCE($1, error),
771
+ error_code = COALESCE($2, error_code),
772
+ abort_requested_at = CASE WHEN $3 THEN NOW() AT TIME ZONE 'UTC' ELSE abort_requested_at END,
773
+ status = 'FAILED',
774
+ completed_at = COALESCE(completed_at, NOW() AT TIME ZONE 'UTC')
775
+ WHERE id = $4 AND queue = $5${prefixConditions}`, [
776
+ "error" in opts ? opts.error ?? null : null,
777
+ "errorCode" in opts ? opts.errorCode ?? null : null,
778
+ opts.abortRequested === true,
779
+ id,
780
+ this.queueName,
781
+ ...prefixParams
782
+ ]);
783
+ }
671
784
  async deleteJobsByStatusAndAge(status, olderThanMs) {
672
785
  const cutoffDate = new Date(Date.now() - olderThanMs).toISOString();
673
786
  const { conditions: prefixConditions, params: prefixParams } = this.buildPrefixWhereClause(4);
@@ -1229,6 +1342,36 @@ class PostgresJobStore {
1229
1342
  async saveStatus(id, status) {
1230
1343
  await this.core.saveStatus(id, status);
1231
1344
  }
1345
+ async create(body, opts) {
1346
+ const enriched = {
1347
+ ...body,
1348
+ fingerprint: opts.fingerprint ?? body.fingerprint,
1349
+ job_run_id: opts.jobRunId ?? body.job_run_id,
1350
+ max_attempts: opts.maxAttempts ?? body.max_attempts,
1351
+ deadline_at: opts.timeoutSeconds != null ? new Date(Date.now() + opts.timeoutSeconds * 1000).toISOString() : body.deadline_at
1352
+ };
1353
+ return this.core.add(enriched);
1354
+ }
1355
+ async findActiveByFingerprint(fingerprint, queueName) {
1356
+ return this.core.findActiveByFingerprint(fingerprint, queueName);
1357
+ }
1358
+ async getMany(ids) {
1359
+ return this.core.getMany(ids);
1360
+ }
1361
+ async completeWithResult(id, result) {
1362
+ this.pending.delete(id);
1363
+ await this.core.completeWithResult(id, result);
1364
+ }
1365
+ async failWithError(id, opts) {
1366
+ this.pending.delete(id);
1367
+ await this.core.failWithError(id, opts);
1368
+ }
1369
+ async markEnqueueDeferred(id, opts) {
1370
+ await this.core.finalize(id, {
1371
+ visible_at: opts.visible_at.toISOString(),
1372
+ error_code: opts.errorCode
1373
+ });
1374
+ }
1232
1375
  }
1233
1376
  // src/job-queue/createPostgresQueue.ts
1234
1377
  function createPostgresQueue(queueName, pool, opts) {
@@ -1253,4 +1396,4 @@ export {
1253
1396
  POSTGRES_QUEUE_STORAGE
1254
1397
  };
1255
1398
 
1256
- //# debugId=A893F6ECCE25069B64756E2164756E21
1399
+ //# debugId=D9E27F175B26F25364756E2164756E21