@pg-boss/dashboard 1.2.1 → 1.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.
Files changed (52) hide show
  1. package/build/client/assets/MenuTrigger-BNvpjhsQ.js +1 -0
  2. package/build/client/assets/_index-DqpFaaQw.js +1 -0
  3. package/build/client/assets/{badge-DCQvSdiR.js → badge-CMnQO7Lq.js} +1 -1
  4. package/build/client/assets/{button-BxLcuaPM.js → button-9NpSS9Ow.js} +1 -1
  5. package/build/client/assets/check-7jwc5sb1.js +1 -0
  6. package/build/client/assets/{chevron-down-Byq-CYG9.js → chevron-down-BFFjfYD4.js} +1 -1
  7. package/build/client/assets/chevron-right-DGk5QFJF.js +1 -0
  8. package/build/client/assets/{createLucideIcon-DVP_i62f.js → createLucideIcon-C-LI4enx.js} +1 -1
  9. package/build/client/assets/db-link-BajQ1v8I.js +1 -0
  10. package/build/client/assets/dialog-D-oczDM2.js +1 -0
  11. package/build/client/assets/{entry.client-DL_oPh96.js → entry.client-CqyjuPDB.js} +3 -3
  12. package/build/client/assets/{error-card-B0ANyjh3.js → error-card-BH7i86fH.js} +1 -1
  13. package/build/client/assets/{filter-select--qLjbs9m.js → filter-select-Bn_oSiip.js} +1 -1
  14. package/build/client/assets/jobs-CAd_qqLH.js +1 -0
  15. package/build/client/assets/jsx-runtime-RQyiN6Nr.js +16 -0
  16. package/build/client/assets/manifest-27e8e133.js +1 -0
  17. package/build/client/assets/migrations-D5l0n4Jn.js +1 -0
  18. package/build/client/assets/{pagination-Bzx8wbXG.js → pagination-C-ohiBmY.js} +1 -1
  19. package/build/client/assets/queues._index-8YriSqbQ.js +1 -0
  20. package/build/client/assets/queues._name-Cb17IB2u.js +1 -0
  21. package/build/client/assets/{queues._name.jobs._jobId-BkG9y75k.js → queues._name.jobs._jobId-Bkv8POBj.js} +1 -1
  22. package/build/client/assets/{queues.create-CMqQVLup.js → queues.create-DsY0Sc19.js} +1 -1
  23. package/build/client/assets/{react-dom-QnGHOQwT.js → react-dom-D_m_Zgd3.js} +1 -1
  24. package/build/client/assets/root-B0MB8jZH.css +2 -0
  25. package/build/client/assets/root-qxoeL6W3.js +40 -0
  26. package/build/client/assets/{schedules-DPXQoaEE.js → schedules-iYfIJxOD.js} +1 -1
  27. package/build/client/assets/{schedules._name._key-B_luxy1w.js → schedules._name._key-CJVu73XY.js} +1 -1
  28. package/build/client/assets/{schedules.new-BQV7GWzs.js → schedules.new-Cq0Mxa7G.js} +1 -1
  29. package/build/client/assets/send-8X9ZisG-.js +1 -0
  30. package/build/client/assets/{stat-card-DLtQnscf.js → stat-card-dyg1wY5p.js} +1 -1
  31. package/build/client/assets/{table-DqqzSNik.js → table-Cz7ujmH_.js} +1 -1
  32. package/build/client/assets/useOpenInteractionType-BQ1arb0B.js +1 -0
  33. package/build/client/assets/{warnings-CHKaRfIW.js → warnings-C1R_RzIe.js} +1 -1
  34. package/build/client/assets/x-AhXI_F1j.js +1 -0
  35. package/build/server/index.js +2053 -1114
  36. package/package.json +11 -8
  37. package/build/client/assets/MenuTrigger-BhalG0aG.js +0 -1
  38. package/build/client/assets/_index-D1-nZ7Th.js +0 -1
  39. package/build/client/assets/check-Ch42cXMT.js +0 -1
  40. package/build/client/assets/chevron-right-CKAGD7DJ.js +0 -1
  41. package/build/client/assets/db-link-BWWnHM0k.js +0 -1
  42. package/build/client/assets/dialog-Bik519zD.js +0 -1
  43. package/build/client/assets/jobs-D0a6Lwq0.js +0 -1
  44. package/build/client/assets/jsx-runtime-BgbGXvsu.js +0 -16
  45. package/build/client/assets/manifest-ef81a0f9.js +0 -1
  46. package/build/client/assets/queues._index-D8903DTa.js +0 -1
  47. package/build/client/assets/queues._name-BVt_4pav.js +0 -1
  48. package/build/client/assets/root-C0MdPLOa.css +0 -2
  49. package/build/client/assets/root-Df70GAY3.js +0 -40
  50. package/build/client/assets/send-DJBsfnx_.js +0 -1
  51. package/build/client/assets/useOpenInteractionType-D3JsvupP.js +0 -1
  52. package/build/client/assets/x-BPKZwOn9.js +0 -1
@@ -347,6 +347,38 @@ var WARNING_TYPE_OPTIONS = [{
347
347
  value: type,
348
348
  label: WARNING_TYPE_LABELS[type]
349
349
  }))];
350
+ var BAM_STATUSES = [
351
+ "pending",
352
+ "in_progress",
353
+ "completed",
354
+ "failed"
355
+ ];
356
+ /**
357
+ * Validate a BAM status filter value
358
+ */
359
+ function isValidBamStatus(value) {
360
+ if (value === null) return true;
361
+ return BAM_STATUSES.includes(value);
362
+ }
363
+ var BAM_STATUS_VARIANTS = {
364
+ pending: "gray",
365
+ in_progress: "primary",
366
+ completed: "success",
367
+ failed: "error"
368
+ };
369
+ var BAM_STATUS_LABELS = {
370
+ pending: "Pending",
371
+ in_progress: "In Progress",
372
+ completed: "Completed",
373
+ failed: "Failed"
374
+ };
375
+ var BAM_STATUS_OPTIONS = [{
376
+ value: null,
377
+ label: "All Statuses"
378
+ }, ...BAM_STATUSES.map((status) => ({
379
+ value: status,
380
+ label: BAM_STATUS_LABELS[status]
381
+ }))];
350
382
  /**
351
383
  * Format warning data for display
352
384
  */
@@ -840,6 +872,11 @@ var navigation = [
840
872
  href: "/schedules",
841
873
  icon: SchedulesIcon
842
874
  },
875
+ {
876
+ name: "Migrations",
877
+ href: "/migrations",
878
+ icon: MigrationsIcon
879
+ },
843
880
  {
844
881
  name: "Warnings",
845
882
  href: "/warnings",
@@ -916,6 +953,20 @@ function WarningIcon$1({ className }) {
916
953
  })
917
954
  });
918
955
  }
956
+ function MigrationsIcon({ className }) {
957
+ return /* @__PURE__ */ jsx("svg", {
958
+ className,
959
+ fill: "none",
960
+ viewBox: "0 0 24 24",
961
+ strokeWidth: 1.5,
962
+ stroke: "currentColor",
963
+ children: /* @__PURE__ */ jsx("path", {
964
+ strokeLinecap: "round",
965
+ strokeLinejoin: "round",
966
+ d: "M7.5 21 3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5"
967
+ })
968
+ });
969
+ }
919
970
  function DatabaseIcon({ className }) {
920
971
  return /* @__PURE__ */ jsx("svg", {
921
972
  className,
@@ -1208,11 +1259,11 @@ var dbContext = globalStore[TOKEN_KEY] ??= createContext();
1208
1259
  //#endregion
1209
1260
  //#region app/root.tsx
1210
1261
  var root_exports = /* @__PURE__ */ __exportAll({
1211
- ErrorBoundary: () => ErrorBoundary$11,
1262
+ ErrorBoundary: () => ErrorBoundary$12,
1212
1263
  Layout: () => Layout,
1213
1264
  default: () => root_default,
1214
1265
  links: () => links,
1215
- loader: () => loader$11,
1266
+ loader: () => loader$12,
1216
1267
  meta: () => meta
1217
1268
  });
1218
1269
  function MainContent({ children }) {
@@ -1288,7 +1339,7 @@ var themeScript = `
1288
1339
  link.href = 'data:image/svg+xml,' + encodeURIComponent(svg);
1289
1340
  })();
1290
1341
  `;
1291
- async function loader$11({ context }) {
1342
+ async function loader$12({ context }) {
1292
1343
  const { databases, currentDb } = context.get(dbContext);
1293
1344
  return {
1294
1345
  databases,
@@ -1321,7 +1372,7 @@ function Layout({ children }) {
1321
1372
  var root_default = UNSAFE_withComponentProps(function App() {
1322
1373
  return /* @__PURE__ */ jsx(Outlet, {});
1323
1374
  });
1324
- var ErrorBoundary$11 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary({ error }) {
1375
+ var ErrorBoundary$12 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary({ error }) {
1325
1376
  let message = "Oops!";
1326
1377
  let details = "An unexpected error occurred.";
1327
1378
  let stack;
@@ -1517,7 +1568,8 @@ function createTableVersion(schema) {
1517
1568
  CREATE TABLE ${schema}.version (
1518
1569
  version int primary key,
1519
1570
  cron_on timestamp with time zone,
1520
- bam_on timestamp with time zone
1571
+ bam_on timestamp with time zone,
1572
+ flow_on timestamp with time zone
1521
1573
  )
1522
1574
  `;
1523
1575
  }
@@ -1538,10 +1590,13 @@ function createTableQueue(schema) {
1538
1590
  table_name text NOT NULL,
1539
1591
  deferred_count int NOT NULL default 0,
1540
1592
  queued_count int NOT NULL default 0,
1593
+ ready_count int NOT NULL default 0,
1541
1594
  warning_queued int NOT NULL default 0,
1542
1595
  active_count int NOT NULL default 0,
1596
+ failed_count int NOT NULL default 0,
1543
1597
  total_count int NOT NULL default 0,
1544
1598
  heartbeat_seconds int,
1599
+ notify bool NOT NULL DEFAULT false,
1545
1600
  singletons_active text[],
1546
1601
  monitor_on timestamp with time zone,
1547
1602
  maintain_on timestamp with time zone,
@@ -1789,6 +1844,7 @@ function createTableJobCommon(schema) {
1789
1844
  SELECT ${schema}.job_table_run($cmd$${createIndexJobThrottle(schema)}$cmd$, '${COMMON_JOB_TABLE}');
1790
1845
  SELECT ${schema}.job_table_run($cmd$${createIndexJobFetch(schema)}$cmd$, '${COMMON_JOB_TABLE}');
1791
1846
  SELECT ${schema}.job_table_run($cmd$${createIndexJobGroupConcurrency(schema)}$cmd$, '${COMMON_JOB_TABLE}');
1847
+ SELECT ${schema}.job_table_run($cmd$${createIndexJobBlocking(schema)}$cmd$, '${COMMON_JOB_TABLE}');
1792
1848
 
1793
1849
  ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.${COMMON_JOB_TABLE} DEFAULT;
1794
1850
  `;
@@ -1806,6 +1862,7 @@ function createTableJobIndexes(schema, noDeferrableConstraints = false, noCoveri
1806
1862
  ${createIndexJobThrottle(schema)};
1807
1863
  ${createIndexJobFetch(schema, noCoveringIndex)};
1808
1864
  ${createIndexJobGroupConcurrency(schema)};
1865
+ ${createIndexJobBlocking(schema)};
1809
1866
  `;
1810
1867
  }
1811
1868
  function createQueueFunction(schema, noPartitioning = false) {
@@ -1878,7 +1935,8 @@ function createQueueFunction(schema, noPartitioning = false) {
1878
1935
  dead_letter,
1879
1936
  partition,
1880
1937
  table_name,
1881
- heartbeat_seconds
1938
+ heartbeat_seconds,
1939
+ notify
1882
1940
  )
1883
1941
  VALUES (
1884
1942
  queue_name,
@@ -1894,7 +1952,8 @@ function createQueueFunction(schema, noPartitioning = false) {
1894
1952
  options->>'deadLetter',
1895
1953
  COALESCE((options->>'partition')::bool, ${QUEUE_DEFAULTS.partition}),
1896
1954
  tablename,
1897
- (options->>'heartbeatSeconds')::int
1955
+ (options->>'heartbeatSeconds')::int,
1956
+ COALESCE((options->>'notify')::bool, false)
1898
1957
  )
1899
1958
  ON CONFLICT DO NOTHING
1900
1959
  RETURNING created_on
@@ -1914,6 +1973,7 @@ function createQueueFunction(schema, noPartitioning = false) {
1914
1973
  EXECUTE ${schema}.job_table_format($cmd$${createIndexJobFetch(schema)}$cmd$, tablename);
1915
1974
  EXECUTE ${schema}.job_table_format($cmd$${createIndexJobThrottle(schema)}$cmd$, tablename);
1916
1975
  EXECUTE ${schema}.job_table_format($cmd$${createIndexJobGroupConcurrency(schema)}$cmd$, tablename);
1976
+ EXECUTE ${schema}.job_table_format($cmd$${createIndexJobBlocking(schema)}$cmd$, tablename);
1917
1977
 
1918
1978
  IF options->>'policy' = 'short' THEN
1919
1979
  EXECUTE ${schema}.job_table_format($cmd$${createIndexJobPolicyShort(schema)}$cmd$, tablename);
@@ -1966,6 +2026,12 @@ function deleteQueueFunction(schema, noPartitioning = false) {
1966
2026
  function createQueue$1(schema, name, options, noAdvisoryLocks) {
1967
2027
  return locked(schema, `SELECT ${schema}.create_queue('${name}', '${JSON.stringify(options)}'::jsonb)`, "create-queue", noAdvisoryLocks);
1968
2028
  }
2029
+ function notifyChannelSql(schema) {
2030
+ return `('pgboss_' || left(encode(sha224('${schema}'::bytea), 'hex'), 24))`;
2031
+ }
2032
+ function notifyQueue(schema, name) {
2033
+ return `SELECT pg_notify(${notifyChannelSql(schema)}, '${name}')`;
2034
+ }
1969
2035
  function deleteQueue(schema, name, noAdvisoryLocks) {
1970
2036
  return locked(schema, `SELECT ${schema}.delete_queue('${name}')`, "delete-queue", noAdvisoryLocks);
1971
2037
  }
@@ -1991,7 +2057,7 @@ function createIndexJobThrottle(schema) {
1991
2057
  return `CREATE UNIQUE INDEX job_i4 ON ${schema}.job (name, singleton_on, COALESCE(singleton_key, '')) WHERE state <> '${JOB_STATES.cancelled}' AND singleton_on IS NOT NULL`;
1992
2058
  }
1993
2059
  function createIndexJobFetch(schema, noCoveringIndex = false) {
1994
- return `CREATE INDEX job_i5 ON ${schema}.job (name, start_after) ${noCoveringIndex ? "" : "INCLUDE (priority, created_on, id) "}WHERE state < '${JOB_STATES.active}' AND NOT blocked`;
2060
+ return `CREATE INDEX job_i5 ON ${schema}.job (name, start_after) WHERE state < '${JOB_STATES.active}' AND NOT blocked`;
1995
2061
  }
1996
2062
  function createIndexJobPolicyExclusive(schema) {
1997
2063
  return `CREATE UNIQUE INDEX job_i6 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state <= '${JOB_STATES.active}' AND policy = '${QUEUE_POLICIES.exclusive}'`;
@@ -2005,6 +2071,9 @@ function createCheckConstraintKeyStrictFifo(schema) {
2005
2071
  function createIndexJobGroupConcurrency(schema) {
2006
2072
  return `CREATE INDEX job_i7 ON ${schema}.job (name, group_id) WHERE state = '${JOB_STATES.active}' AND group_id IS NOT NULL`;
2007
2073
  }
2074
+ function createIndexJobBlocking(schema) {
2075
+ return `CREATE INDEX job_i9 ON ${schema}.job (name, id) WHERE blocking AND state = '${JOB_STATES.completed}'`;
2076
+ }
2008
2077
  function trySetQueueMonitorTime(schema, queues, seconds) {
2009
2078
  return trySetQueueTimestamp(schema, queues, "monitor_on", seconds);
2010
2079
  }
@@ -2017,6 +2086,9 @@ function trySetCronTime(schema, seconds) {
2017
2086
  function trySetBamTime(schema, seconds) {
2018
2087
  return trySetTimestamp(schema, "bam_on", seconds);
2019
2088
  }
2089
+ function trySetFlowTime(schema, seconds) {
2090
+ return trySetTimestamp(schema, "flow_on", seconds);
2091
+ }
2020
2092
  function trySetTimestamp(schema, column, seconds) {
2021
2093
  return `
2022
2094
  UPDATE ${schema}.version
@@ -2054,6 +2126,7 @@ function updateQueue(schema, { deadLetter } = {}) {
2054
2126
  heartbeat_seconds = CASE WHEN o.data ? 'heartbeatSeconds'
2055
2127
  THEN (o.data->>'heartbeatSeconds')::int
2056
2128
  ELSE heartbeat_seconds END,
2129
+ notify = COALESCE((o.data->>'notify')::bool, notify),
2057
2130
  ${deadLetter === void 0 ? "" : `dead_letter = CASE WHEN '${deadLetter}' IS DISTINCT FROM dead_letter THEN '${deadLetter}' ELSE dead_letter END,`}
2058
2131
  updated_on = now()
2059
2132
  FROM options o
@@ -2076,11 +2149,14 @@ function getQueues$1(schema, names) {
2076
2149
  q.deletion_seconds as "deleteAfterSeconds",
2077
2150
  q.partition,
2078
2151
  q.heartbeat_seconds as "heartbeatSeconds",
2152
+ q.notify,
2079
2153
  q.dead_letter as "deadLetter",
2080
2154
  q.deferred_count as "deferredCount",
2081
2155
  q.warning_queued as "warningQueueSize",
2082
2156
  q.queued_count as "queuedCount",
2157
+ q.ready_count as "readyCount",
2083
2158
  q.active_count as "activeCount",
2159
+ q.failed_count as "failedCount",
2084
2160
  q.total_count as "totalCount",
2085
2161
  q.singletons_active as "singletonsActive",
2086
2162
  q.table_name as "table",
@@ -2097,7 +2173,7 @@ function deleteJobsById(schema, table) {
2097
2173
  WITH results as (
2098
2174
  DELETE FROM ${schema}.${table}
2099
2175
  WHERE name = $1
2100
- AND id IN (SELECT UNNEST($2::uuid[]))
2176
+ AND id = ANY($2::uuid[])
2101
2177
  RETURNING 1
2102
2178
  )
2103
2179
  SELECT COUNT(*) from results
@@ -2360,7 +2436,7 @@ function completeJobsUpdate(schema, table, includeQueued) {
2360
2436
  blocked = ${includeQueued ? "false" : "blocked"},
2361
2437
  pending_dependencies = ${includeQueued ? "0" : "pending_dependencies"}
2362
2438
  WHERE name = $1
2363
- AND id IN (SELECT UNNEST($2::uuid[]))
2439
+ AND id = ANY($2::uuid[])
2364
2440
  AND ${includeQueued ? `state < '${JOB_STATES.completed}'` : `state = '${JOB_STATES.active}'`}`;
2365
2441
  }
2366
2442
  function lockedChildrenCte(schema) {
@@ -2384,24 +2460,46 @@ function unblockChildrenUpdate(schema) {
2384
2460
  }
2385
2461
  function completeJobs(schema, table, includeQueued) {
2386
2462
  return `
2387
- WITH completed AS (
2463
+ WITH results AS (
2388
2464
  ${completeJobsUpdate(schema, table, includeQueued)}
2389
- RETURNING name, id, blocking
2390
- ),
2391
- decremented AS (
2392
- SELECT d.child_name, d.child_id, COUNT(*)::int AS n
2393
- FROM ${schema}.job_dependency d
2394
- JOIN completed c ON c.blocking
2395
- AND d.parent_name = c.name
2396
- AND d.parent_id = c.id
2397
- GROUP BY d.child_name, d.child_id
2465
+ RETURNING 1
2466
+ )
2467
+ SELECT COUNT(*) FROM results
2468
+ `;
2469
+ }
2470
+ function completeJobsWithOutputs(schema, table) {
2471
+ return `
2472
+ WITH input AS (
2473
+ SELECT * FROM json_to_recordset($2::json) AS x (id uuid, output jsonb)
2398
2474
  ),
2399
- ${lockedChildrenCte(schema)},
2400
- unblocked AS (
2401
- ${unblockChildrenUpdate(schema)}
2475
+ results AS (
2476
+ UPDATE ${schema}.${table} j
2477
+ SET completed_on = now(),
2478
+ state = '${JOB_STATES.completed}',
2479
+ output = i.output
2480
+ FROM input i
2481
+ WHERE j.name = $1
2482
+ AND j.id = i.id
2483
+ AND j.state = '${JOB_STATES.active}'
2402
2484
  RETURNING 1
2403
2485
  )
2404
- SELECT COUNT(*) FROM completed
2486
+ SELECT COUNT(*) FROM results
2487
+ `;
2488
+ }
2489
+ function completeJobsWithOutputsDistributed(schema, table) {
2490
+ return `
2491
+ WITH input AS (
2492
+ SELECT * FROM json_to_recordset($2::json) AS x (id uuid, output jsonb)
2493
+ )
2494
+ UPDATE ${schema}.${table} j
2495
+ SET completed_on = now(),
2496
+ state = '${JOB_STATES.completed}',
2497
+ output = i.output
2498
+ FROM input i
2499
+ WHERE j.name = $1
2500
+ AND j.id = i.id
2501
+ AND j.state = '${JOB_STATES.active}'
2502
+ RETURNING j.id
2405
2503
  `;
2406
2504
  }
2407
2505
  function cancelJobs(schema, table) {
@@ -2411,7 +2509,7 @@ function cancelJobs(schema, table) {
2411
2509
  SET completed_on = now(),
2412
2510
  state = '${JOB_STATES.cancelled}'
2413
2511
  WHERE name = $1
2414
- AND id IN (SELECT UNNEST($2::uuid[]))
2512
+ AND id = ANY($2::uuid[])
2415
2513
  AND state < '${JOB_STATES.completed}'
2416
2514
  RETURNING 1
2417
2515
  )
@@ -2425,7 +2523,7 @@ function resumeJobs(schema, table) {
2425
2523
  SET completed_on = NULL,
2426
2524
  state = '${JOB_STATES.created}'
2427
2525
  WHERE name = $1
2428
- AND id IN (SELECT UNNEST($2::uuid[]))
2526
+ AND id = ANY($2::uuid[])
2429
2527
  AND state = '${JOB_STATES.cancelled}'
2430
2528
  RETURNING 1
2431
2529
  )
@@ -2439,11 +2537,11 @@ function restoreJobs(schema, table) {
2439
2537
  started_on = NULL,
2440
2538
  heartbeat_on = NULL
2441
2539
  WHERE name = $1
2442
- AND id IN (SELECT UNNEST($2::uuid[]))
2540
+ AND id = ANY($2::uuid[])
2443
2541
  `;
2444
2542
  }
2445
- function insertJobs(schema, { table, name, returnId = true }) {
2446
- return `
2543
+ function insertJobs(schema, { table, name, returnId = true, notify = false }) {
2544
+ const insert = `
2447
2545
  INSERT INTO ${schema}.${table} (
2448
2546
  id,
2449
2547
  name,
@@ -2526,11 +2624,35 @@ function insertJobs(schema, { table, name, returnId = true }) {
2526
2624
  ) j
2527
2625
  JOIN ${schema}.queue q ON q.name = '${name}'
2528
2626
  ON CONFLICT DO NOTHING
2529
- ${returnId ? "RETURNING id" : ""}
2627
+ ${notify ? "RETURNING id, start_after" : returnId ? "RETURNING id" : ""}
2628
+ `;
2629
+ if (!notify) return insert;
2630
+ const comparator = returnId ? ">= 0" : "< 0";
2631
+ return `
2632
+ WITH ins AS (
2633
+ ${insert}
2634
+ ),
2635
+ notified AS (
2636
+ SELECT pg_notify(${notifyChannelSql(schema)}, '${name}')
2637
+ FROM ins WHERE start_after <= now() LIMIT 1
2638
+ )
2639
+ SELECT id FROM ins WHERE (SELECT count(*) FROM notified) ${comparator}
2640
+ `;
2641
+ }
2642
+ function insertFlowJobs(schema, { table, name }, jobs) {
2643
+ return `
2644
+ WITH ins AS (
2645
+ ${insertJobs(schema, {
2646
+ table,
2647
+ name,
2648
+ returnId: true
2649
+ }).replace("$1", () => serializeJsonParam(jobs))}
2650
+ )
2651
+ SELECT 1 / (CASE WHEN (SELECT count(*) FROM ins) = ${jobs.length} THEN 1 ELSE 0 END)
2530
2652
  `;
2531
2653
  }
2532
2654
  function failJobsById(schema, table) {
2533
- return failJobs(schema, table, `name = $1 AND id IN (SELECT UNNEST($2::uuid[])) AND state < '${JOB_STATES.completed}'`, "$3::jsonb");
2655
+ return failJobs(schema, table, `name = $1 AND id = ANY($2::uuid[]) AND state < '${JOB_STATES.completed}'`, "$3::jsonb");
2534
2656
  }
2535
2657
  function failJobsByTimeout(schema, table, queues, noAdvisoryLocks) {
2536
2658
  return locked(schema, failJobs(schema, table, `state = '${JOB_STATES.active}'
@@ -2549,7 +2671,7 @@ function touchJobs(schema, table) {
2549
2671
  UPDATE ${schema}.${table}
2550
2672
  SET heartbeat_on = now()
2551
2673
  WHERE name = $1
2552
- AND id IN (SELECT UNNEST($2::uuid[]))
2674
+ AND id = ANY($2::uuid[])
2553
2675
  AND state = '${JOB_STATES.active}'
2554
2676
  RETURNING 1
2555
2677
  )
@@ -2558,7 +2680,12 @@ function touchJobs(schema, table) {
2558
2680
  }
2559
2681
  function failJobs(schema, table, where, output) {
2560
2682
  return `
2561
- WITH deleted_jobs AS (
2683
+ WITH ${failJobsBody(schema, table, where, output)}
2684
+ SELECT COUNT(*) FROM results
2685
+ `;
2686
+ }
2687
+ function failJobsBody(schema, table, where, output, forceTerminal = false) {
2688
+ return `deleted_jobs AS (
2562
2689
  DELETE FROM ${schema}.${table}
2563
2690
  WHERE ${where}
2564
2691
  RETURNING *
@@ -2600,10 +2727,10 @@ function failJobs(schema, table, where, output) {
2600
2727
  name,
2601
2728
  priority,
2602
2729
  data,
2603
- CASE
2730
+ ${forceTerminal ? `'${JOB_STATES.failed}'::${schema}.job_state` : `CASE
2604
2731
  WHEN retry_count < retry_limit THEN '${JOB_STATES.retry}'::${schema}.job_state
2605
2732
  ELSE '${JOB_STATES.failed}'::${schema}.job_state
2606
- END as state,
2733
+ END`} as state,
2607
2734
  retry_limit,
2608
2735
  retry_count,
2609
2736
  retry_delay,
@@ -2627,7 +2754,7 @@ function failJobs(schema, table, where, output) {
2627
2754
  expire_seconds,
2628
2755
  deletion_seconds,
2629
2756
  created_on,
2630
- CASE WHEN retry_count < retry_limit THEN NULL ELSE now() END as completed_on,
2757
+ ${forceTerminal ? "now()" : "CASE WHEN retry_count < retry_limit THEN NULL ELSE now() END"} as completed_on,
2631
2758
  keep_until,
2632
2759
  policy,
2633
2760
  ${output},
@@ -2726,19 +2853,35 @@ function failJobs(schema, table, where, output) {
2726
2853
  FROM results r
2727
2854
  JOIN ${schema}.queue q ON q.name = r.dead_letter
2728
2855
  WHERE state = '${JOB_STATES.failed}'
2729
- )
2856
+ )`;
2857
+ }
2858
+ function failJobsByIdWithOutputs(schema, table) {
2859
+ return `
2860
+ WITH output_map AS (
2861
+ SELECT * FROM json_to_recordset($2::json) AS x (id uuid, output jsonb)
2862
+ ),
2863
+ ${failJobsBody(schema, table, `name = $1 AND id IN (SELECT id FROM output_map) AND state < '${JOB_STATES.completed}'`, "(SELECT om.output FROM output_map om WHERE om.id = deleted_jobs.id)")}
2864
+ SELECT COUNT(*) FROM results
2865
+ `;
2866
+ }
2867
+ function deadLetterJobsByIdWithOutputs(schema, table) {
2868
+ return `
2869
+ WITH output_map AS (
2870
+ SELECT * FROM json_to_recordset($2::json) AS x (id uuid, output jsonb)
2871
+ ),
2872
+ ${failJobsBody(schema, table, `name = $1 AND id IN (SELECT id FROM output_map) AND state < '${JOB_STATES.completed}'`, "(SELECT om.output FROM output_map om WHERE om.id = deleted_jobs.id)", true)}
2730
2873
  SELECT COUNT(*) FROM results
2731
2874
  `;
2732
2875
  }
2733
2876
  function selectJobsToFailById(schema, table) {
2734
2877
  return {
2735
- text: `SELECT * FROM ${schema}.${table} WHERE name = $1 AND id IN (SELECT UNNEST($2::uuid[])) AND state < '${JOB_STATES.completed}'`,
2878
+ text: `SELECT * FROM ${schema}.${table} WHERE name = $1 AND id = ANY($2::uuid[]) AND state < '${JOB_STATES.completed}'`,
2736
2879
  values: []
2737
2880
  };
2738
2881
  }
2739
2882
  function deleteJobsToFail(schema, table) {
2740
2883
  return {
2741
- text: `DELETE FROM ${schema}.${table} WHERE name = $1 AND id IN (SELECT UNNEST($2::uuid[]))`,
2884
+ text: `DELETE FROM ${schema}.${table} WHERE name = $1 AND id = ANY($2::uuid[])`,
2742
2885
  values: []
2743
2886
  };
2744
2887
  }
@@ -2763,14 +2906,14 @@ function selectJobsToFailByHeartbeat(schema, table, queues) {
2763
2906
  }
2764
2907
  function deleteJobsByIds(schema, table) {
2765
2908
  return {
2766
- text: `DELETE FROM ${schema}.${table} WHERE id IN (SELECT UNNEST($1::uuid[]))`,
2909
+ text: `DELETE FROM ${schema}.${table} WHERE id = ANY($1::uuid[])`,
2767
2910
  values: []
2768
2911
  };
2769
2912
  }
2770
2913
  function completeJobsDistributed(schema, table, includeQueued) {
2771
2914
  return `
2772
2915
  ${completeJobsUpdate(schema, table, includeQueued)}
2773
- RETURNING id, blocking
2916
+ RETURNING id
2774
2917
  `;
2775
2918
  }
2776
2919
  function decrementDependents(schema) {
@@ -2779,13 +2922,75 @@ function decrementDependents(schema) {
2779
2922
  SELECT d.child_name, d.child_id, COUNT(*)::int AS n
2780
2923
  FROM ${schema}.job_dependency d
2781
2924
  WHERE d.parent_name = $1
2782
- AND d.parent_id IN (SELECT UNNEST($2::uuid[]))
2925
+ AND d.parent_id = ANY($2::uuid[])
2783
2926
  GROUP BY d.child_name, d.child_id
2784
2927
  ),
2785
2928
  ${lockedChildrenCte(schema)}
2786
2929
  ${unblockChildrenUpdate(schema)}
2787
2930
  `;
2788
2931
  }
2932
+ var FLOW_BATCH_SIZE = 1e3;
2933
+ function resolveFlowJobs(schema, table, names) {
2934
+ return {
2935
+ text: `
2936
+ WITH locked_parents AS (
2937
+ SELECT j.name, j.id
2938
+ FROM ${schema}.${table} j
2939
+ WHERE j.blocking
2940
+ AND j.state = '${JOB_STATES.completed}'
2941
+ AND j.name = ANY($1::text[])
2942
+ ORDER BY j.name, j.id
2943
+ FOR UPDATE OF j SKIP LOCKED
2944
+ LIMIT ${FLOW_BATCH_SIZE}
2945
+ ),
2946
+ decremented AS (
2947
+ SELECT d.child_name, d.child_id, COUNT(*)::int AS n
2948
+ FROM ${schema}.job_dependency d
2949
+ JOIN locked_parents p ON d.parent_name = p.name
2950
+ AND d.parent_id = p.id
2951
+ GROUP BY d.child_name, d.child_id
2952
+ ),
2953
+ ${lockedChildrenCte(schema)},
2954
+ unblocked AS (
2955
+ ${unblockChildrenUpdate(schema)}
2956
+ RETURNING 1
2957
+ ),
2958
+ cleared AS (
2959
+ UPDATE ${schema}.${table} j
2960
+ SET blocking = false
2961
+ FROM locked_parents p
2962
+ WHERE j.name = p.name
2963
+ AND j.id = p.id
2964
+ RETURNING 1
2965
+ )
2966
+ SELECT COUNT(*)::int AS resolved FROM cleared
2967
+ `,
2968
+ values: [names]
2969
+ };
2970
+ }
2971
+ function selectBlockingParents(schema, table, names, noSkipLocked) {
2972
+ return {
2973
+ text: `
2974
+ SELECT name, id
2975
+ FROM ${schema}.${table}
2976
+ WHERE blocking
2977
+ AND state = '${JOB_STATES.completed}'
2978
+ AND name = ANY($1::text[])
2979
+ ORDER BY name, id
2980
+ FOR UPDATE${noSkipLocked ? "" : " SKIP LOCKED"}
2981
+ LIMIT ${FLOW_BATCH_SIZE}
2982
+ `,
2983
+ values: [names]
2984
+ };
2985
+ }
2986
+ function clearBlocking(schema) {
2987
+ return `
2988
+ UPDATE ${schema}.job
2989
+ SET blocking = false
2990
+ WHERE name = $1
2991
+ AND id = ANY($2::uuid[])
2992
+ `;
2993
+ }
2789
2994
  function insertRetryJob(schema, table) {
2790
2995
  return `
2791
2996
  INSERT INTO ${schema}.${table} (
@@ -2827,7 +3032,7 @@ function retryJobs(schema, table) {
2827
3032
  SET state = '${JOB_STATES.retry}',
2828
3033
  retry_limit = retry_limit + 1
2829
3034
  WHERE name = $1
2830
- AND id IN (SELECT UNNEST($2::uuid[]))
3035
+ AND id = ANY($2::uuid[])
2831
3036
  AND state = '${JOB_STATES.failed}'
2832
3037
  RETURNING 1
2833
3038
  )
@@ -2839,14 +3044,26 @@ function getQueueStats$1(schema, table, queues) {
2839
3044
  text: `
2840
3045
  SELECT
2841
3046
  name,
2842
- (count(*) FILTER (WHERE start_after > now()))::int as "deferredCount",
2843
- (count(*) FILTER (WHERE state < '${JOB_STATES.active}'))::int as "queuedCount",
2844
- (count(*) FILTER (WHERE state = '${JOB_STATES.active}'))::int as "activeCount",
2845
- count(*)::int as "totalCount",
2846
- array_agg(singleton_key) FILTER (WHERE policy IN ('${QUEUE_POLICIES.singleton}','${QUEUE_POLICIES.stately}') AND state = '${JOB_STATES.active}') as "singletonsActive"
2847
- FROM ${schema}.${table}
2848
- WHERE name = ANY($1::text[])
2849
- GROUP BY 1
3047
+ "deferredCount",
3048
+ "queuedCount",
3049
+ GREATEST("queuedCount" - "deferredCount", 0) as "readyCount",
3050
+ "activeCount",
3051
+ "failedCount",
3052
+ "totalCount",
3053
+ "singletonsActive"
3054
+ FROM (
3055
+ SELECT
3056
+ name,
3057
+ (count(*) FILTER (WHERE start_after > now()))::int as "deferredCount",
3058
+ (count(*) FILTER (WHERE state < '${JOB_STATES.active}'))::int as "queuedCount",
3059
+ (count(*) FILTER (WHERE state = '${JOB_STATES.active}'))::int as "activeCount",
3060
+ (count(*) FILTER (WHERE state = '${JOB_STATES.failed}'))::int as "failedCount",
3061
+ count(*)::int as "totalCount",
3062
+ array_agg(singleton_key) FILTER (WHERE policy IN ('${QUEUE_POLICIES.singleton}','${QUEUE_POLICIES.stately}') AND state = '${JOB_STATES.active}') as "singletonsActive"
3063
+ FROM ${schema}.${table}
3064
+ WHERE name = ANY($1::text[])
3065
+ GROUP BY 1
3066
+ ) stats
2850
3067
  `,
2851
3068
  values: [queues]
2852
3069
  };
@@ -2857,7 +3074,9 @@ function cacheQueueStats(schema, table, queues, noAdvisoryLocks) {
2857
3074
  UPDATE ${schema}.queue SET
2858
3075
  deferred_count = COALESCE(stats."deferredCount", 0),
2859
3076
  queued_count = COALESCE(stats."queuedCount", 0),
3077
+ ready_count = COALESCE(stats."readyCount", 0),
2860
3078
  active_count = COALESCE(stats."activeCount", 0),
3079
+ failed_count = COALESCE(stats."failedCount", 0),
2861
3080
  total_count = COALESCE(stats."totalCount", 0),
2862
3081
  singletons_active = stats."singletonsActive"
2863
3082
  FROM (
@@ -2931,8 +3150,8 @@ function getJobById$1(schema, table) {
2931
3150
  AND id = $2
2932
3151
  `;
2933
3152
  }
2934
- function insertDependencies(schema) {
2935
- return `
3153
+ function insertDependencies(schema, deps) {
3154
+ const sql = `
2936
3155
  INSERT INTO ${schema}.job_dependency (child_name, child_id, parent_name, parent_id)
2937
3156
  SELECT child_name, child_id, parent_name, parent_id
2938
3157
  FROM json_to_recordset($1::json) AS x (
@@ -2943,6 +3162,7 @@ function insertDependencies(schema) {
2943
3162
  )
2944
3163
  ON CONFLICT DO NOTHING
2945
3164
  `;
3165
+ return deps ? sql.replace("$1", () => serializeJsonParam(deps)) : sql;
2946
3166
  }
2947
3167
  function getDependencies(schema) {
2948
3168
  return `
@@ -3018,7 +3238,7 @@ function getBamStatus(schema) {
3018
3238
  GROUP BY status
3019
3239
  `;
3020
3240
  }
3021
- function getBamEntries(schema) {
3241
+ function getBamEntries$1(schema) {
3022
3242
  return `
3023
3243
  SELECT id, name, version, status, queue, table_name as "table", command, error,
3024
3244
  created_on as "createdOn", started_on as "startedOn", completed_on as "completedOn"
@@ -3039,7 +3259,8 @@ var COMPATIBILITY_FLAGS = [
3039
3259
  "noTablePartitioning",
3040
3260
  "noDeferrableConstraints",
3041
3261
  "noAdvisoryLocks",
3042
- "noCoveringIndexes"
3262
+ "noCoveringIndexes",
3263
+ "noListenNotify"
3043
3264
  ];
3044
3265
  var BACKEND_PROFILES = {
3045
3266
  postgres: {
@@ -3054,7 +3275,8 @@ var BACKEND_PROFILES = {
3054
3275
  noTablePartitioning: true,
3055
3276
  noDeferrableConstraints: true,
3056
3277
  noAdvisoryLocks: true,
3057
- noCoveringIndexes: true
3278
+ noCoveringIndexes: true,
3279
+ noListenNotify: true
3058
3280
  }
3059
3281
  },
3060
3282
  yugabytedb: {
@@ -3078,6 +3300,7 @@ function assertObjectName(value, name = "Name") {
3078
3300
  }
3079
3301
  function validateQueueArgs(config = {}) {
3080
3302
  assert(!("deadLetter" in config) || config.deadLetter === null || typeof config.deadLetter === "string", "deadLetter must be a string");
3303
+ assert(!("notify" in config) || typeof config.notify === "boolean", "notify must be a boolean");
3081
3304
  if (config.deadLetter) assertObjectName(config.deadLetter, "deadLetter");
3082
3305
  validateRetryConfig(config);
3083
3306
  validateExpirationConfig(config);
@@ -3257,6 +3480,7 @@ function checkWorkArgs(name, args) {
3257
3480
  assert(!("includeMetadata" in options) || typeof options.includeMetadata === "boolean", "includeMetadata must be a boolean");
3258
3481
  assert(!("priority" in options) || typeof options.priority === "boolean", "priority must be a boolean");
3259
3482
  assert(!("localConcurrency" in options) || Number.isInteger(options.localConcurrency) && options.localConcurrency >= 1, "localConcurrency must be an integer >= 1");
3483
+ assert(!("perJobResults" in options) || typeof options.perJobResults === "boolean", "perJobResults must be a boolean");
3260
3484
  validatePriorityRangeConfig(options);
3261
3485
  validateGroupConcurrencyConfig(options);
3262
3486
  validateHeartbeatRefreshConfig(options);
@@ -3280,11 +3504,13 @@ function getConfig(value) {
3280
3504
  config.supervise = "supervise" in config ? config.supervise : true;
3281
3505
  config.migrate = "migrate" in config ? config.migrate : true;
3282
3506
  config.createSchema = "createSchema" in config ? config.createSchema : true;
3507
+ config.useListenNotify = "useListenNotify" in config ? config.useListenNotify : false;
3283
3508
  resolveBackend(config);
3284
3509
  applySchemaConfig(config);
3285
3510
  applyOpsConfig(config);
3286
3511
  applyScheduleConfig(config);
3287
3512
  applyBamConfig(config);
3513
+ applyFlowConfig(config);
3288
3514
  validateWarningConfig(config);
3289
3515
  return config;
3290
3516
  }
@@ -3349,6 +3575,13 @@ function validateHeartbeatRefreshConfig(config) {
3349
3575
  function applyPollingInterval(config) {
3350
3576
  assert(!("pollingIntervalSeconds" in config) || config.pollingIntervalSeconds >= POLICY.MIN_POLLING_INTERVAL_MS / 1e3, `configuration assert: pollingIntervalSeconds must be at least every ${POLICY.MIN_POLLING_INTERVAL_MS}ms`);
3351
3577
  config.pollingInterval = "pollingIntervalSeconds" in config ? config.pollingIntervalSeconds * 1e3 : 2e3;
3578
+ assert(!("notifyPollingIntervalSeconds" in config) || config.notifyPollingIntervalSeconds >= POLICY.MIN_POLLING_INTERVAL_MS / 1e3, `configuration assert: notifyPollingIntervalSeconds must be at least every ${POLICY.MIN_POLLING_INTERVAL_MS}ms`);
3579
+ if ("notifyPollingIntervalSeconds" in config) {
3580
+ config.notifyPollingInterval = config.notifyPollingIntervalSeconds * 1e3;
3581
+ assert(config.notifyPollingInterval >= config.pollingInterval, "configuration assert: notifyPollingIntervalSeconds must be at least pollingIntervalSeconds");
3582
+ } else config.notifyPollingInterval = Math.max(3e4, config.pollingInterval);
3583
+ assert(!("burstWhenReadyExceeds" in config) || Number.isInteger(config.burstWhenReadyExceeds) && config.burstWhenReadyExceeds >= 1, "configuration assert: burstWhenReadyExceeds must be an integer >= 1");
3584
+ assert(!("burstWhenBatchFull" in config) || typeof config.burstWhenBatchFull === "boolean", "configuration assert: burstWhenBatchFull must be a boolean");
3352
3585
  }
3353
3586
  function applyOpsConfig(config) {
3354
3587
  assert(!("superviseIntervalSeconds" in config) || config.superviseIntervalSeconds >= 1, "configuration assert: superviseIntervalSeconds must be at least every second");
@@ -3380,8 +3613,28 @@ function applyBamConfig(config) {
3380
3613
  assert(!("bamIntervalSeconds" in config) || config.bamIntervalSeconds >= minInterval, `configuration assert: bamIntervalSeconds must be at least ${minInterval} seconds`);
3381
3614
  config.bamIntervalSeconds = config.bamIntervalSeconds || 60;
3382
3615
  }
3616
+ function applyFlowConfig(config) {
3617
+ const minInterval = config.__test__bypass_flow_interval_check ? .5 : 1;
3618
+ assert(!("flowIntervalSeconds" in config) || config.flowIntervalSeconds >= minInterval, `configuration assert: flowIntervalSeconds must be at least ${minInterval} seconds`);
3619
+ config.flowIntervalSeconds = config.flowIntervalSeconds || 5;
3620
+ }
3383
3621
  //#endregion
3384
3622
  //#region ../../src/migrationStore.ts
3623
+ function formatJobTable(command, table) {
3624
+ return command.replaceAll(".job", `.${table}`).replaceAll("job_i", `${table}_i`);
3625
+ }
3626
+ function inlineAsyncCommand(schema, asyncCommand, version, partitionTables) {
3627
+ const nameMatch = asyncCommand.match(/job_table_run_async\(\s*'([^']+)'/);
3628
+ const bodyMatch = asyncCommand.match(/\$\$([\s\S]*?)\$\$/);
3629
+ const tableMatch = asyncCommand.match(/\$\$\s*,\s*'([^']+)'/);
3630
+ assert(nameMatch && bodyMatch, `Unable to inline async migration command: ${asyncCommand}`);
3631
+ const commandName = nameMatch[1];
3632
+ const body = bodyMatch[1].trim();
3633
+ return (tableMatch ? [tableMatch[1]] : ["job_common", ...partitionTables]).map((table) => {
3634
+ const ddl = formatJobTable(body, table).replace(/(CREATE (?:UNIQUE )?INDEX CONCURRENTLY) /, "$1 IF NOT EXISTS ");
3635
+ return `${`-- inlined from ${schema}.job_table_run_async (migration v${version}, command: ${commandName})`}\n${ddl}`;
3636
+ });
3637
+ }
3385
3638
  function flatten(schema, commands, version, noAdvisoryLocks) {
3386
3639
  commands.unshift(assertMigration(schema, version));
3387
3640
  commands.push(setVersion(schema, version));
@@ -3399,11 +3652,13 @@ function next(schema, version, migrations, noAdvisoryLocks) {
3399
3652
  assert(result, `Version ${version} not found.`);
3400
3653
  return flatten(schema, result.install, result.version, noAdvisoryLocks);
3401
3654
  }
3402
- function migrate(schema, version, migrations, noAdvisoryLocks) {
3655
+ function migrateCommands(schema, version, migrations, noAdvisoryLocks, options = {}) {
3403
3656
  migrations = migrations || getAll(schema);
3657
+ const concurrent = [];
3404
3658
  const result = migrations.filter((i) => i.previous >= version).sort((a, b) => a.version - b.version).reduce((acc, migration) => {
3405
3659
  acc.install = acc.install.concat(migration.install);
3406
- if (migration.async) {
3660
+ if (migration.async) if (options.inlineAsync) for (const cmd of migration.async) concurrent.push(...inlineAsyncCommand(schema, cmd, migration.version, options.partitionTables || []));
3661
+ else {
3407
3662
  const bamCommands = migration.async.map((cmd) => cmd.replace(/\$VERSION\$/g, String(migration.version)));
3408
3663
  acc.install = acc.install.concat(bamCommands);
3409
3664
  }
@@ -3414,151 +3669,657 @@ function migrate(schema, version, migrations, noAdvisoryLocks) {
3414
3669
  version
3415
3670
  });
3416
3671
  assert(result.install.length > 0, `Version ${version} not found.`);
3417
- return flatten(schema, result.install, result.version, noAdvisoryLocks);
3672
+ return {
3673
+ sql: flatten(schema, result.install, result.version, noAdvisoryLocks),
3674
+ concurrent
3675
+ };
3418
3676
  }
3419
- function getAll(schema) {
3420
- return [
3421
- {
3422
- release: "11.1.0",
3423
- version: 26,
3424
- previous: 25,
3425
- install: [`
3426
- CREATE OR REPLACE FUNCTION ${schema}.create_queue(queue_name text, options jsonb)
3427
- RETURNS VOID AS
3428
- $$
3429
- DECLARE
3430
- tablename varchar := CASE WHEN options->>'partition' = 'true'
3431
- THEN 'j' || encode(sha224(queue_name::bytea), 'hex')
3432
- ELSE 'job_common'
3433
- END;
3434
- queue_created_on timestamptz;
3435
- BEGIN
3436
-
3437
- WITH q as (
3438
- INSERT INTO ${schema}.queue (
3439
- name,
3440
- policy,
3441
- retry_limit,
3442
- retry_delay,
3443
- retry_backoff,
3444
- retry_delay_max,
3445
- expire_seconds,
3446
- retention_seconds,
3447
- deletion_seconds,
3448
- warning_queued,
3449
- dead_letter,
3450
- partition,
3451
- table_name
3452
- )
3453
- VALUES (
3454
- queue_name,
3455
- options->>'policy',
3456
- COALESCE((options->>'retryLimit')::int, 2),
3457
- COALESCE((options->>'retryDelay')::int, 0),
3458
- COALESCE((options->>'retryBackoff')::bool, false),
3459
- (options->>'retryDelayMax')::int,
3460
- COALESCE((options->>'expireInSeconds')::int, 900),
3461
- COALESCE((options->>'retentionSeconds')::int, 1209600),
3462
- COALESCE((options->>'deleteAfterSeconds')::int, 604800),
3463
- COALESCE((options->>'warningQueueSize')::int, 0),
3464
- options->>'deadLetter',
3465
- COALESCE((options->>'partition')::bool, false),
3466
- tablename
3467
- )
3468
- ON CONFLICT DO NOTHING
3469
- RETURNING created_on
3470
- )
3471
- SELECT created_on into queue_created_on from q;
3677
+ function migrate(schema, version, migrations, noAdvisoryLocks, options = {}) {
3678
+ const { sql, concurrent } = migrateCommands(schema, version, migrations, noAdvisoryLocks, options);
3679
+ return concurrent.length ? `${sql}\n${concurrent.join(";\n")};` : sql;
3680
+ }
3681
+ var createQueueFn = {
3682
+ 26: (schema) => `
3683
+ CREATE OR REPLACE FUNCTION ${schema}.create_queue(queue_name text, options jsonb)
3684
+ RETURNS VOID AS
3685
+ $$
3686
+ DECLARE
3687
+ tablename varchar := CASE WHEN options->>'partition' = 'true'
3688
+ THEN 'j' || encode(sha224(queue_name::bytea), 'hex')
3689
+ ELSE 'job_common'
3690
+ END;
3691
+ queue_created_on timestamptz;
3692
+ BEGIN
3472
3693
 
3473
- IF queue_created_on IS NULL OR options->>'partition' IS DISTINCT FROM 'true' THEN
3474
- RETURN;
3475
- END IF;
3694
+ WITH q as (
3695
+ INSERT INTO ${schema}.queue (
3696
+ name,
3697
+ policy,
3698
+ retry_limit,
3699
+ retry_delay,
3700
+ retry_backoff,
3701
+ retry_delay_max,
3702
+ expire_seconds,
3703
+ retention_seconds,
3704
+ deletion_seconds,
3705
+ warning_queued,
3706
+ dead_letter,
3707
+ partition,
3708
+ table_name
3709
+ )
3710
+ VALUES (
3711
+ queue_name,
3712
+ options->>'policy',
3713
+ COALESCE((options->>'retryLimit')::int, 2),
3714
+ COALESCE((options->>'retryDelay')::int, 0),
3715
+ COALESCE((options->>'retryBackoff')::bool, false),
3716
+ (options->>'retryDelayMax')::int,
3717
+ COALESCE((options->>'expireInSeconds')::int, 900),
3718
+ COALESCE((options->>'retentionSeconds')::int, 1209600),
3719
+ COALESCE((options->>'deleteAfterSeconds')::int, 604800),
3720
+ COALESCE((options->>'warningQueueSize')::int, 0),
3721
+ options->>'deadLetter',
3722
+ COALESCE((options->>'partition')::bool, false),
3723
+ tablename
3724
+ )
3725
+ ON CONFLICT DO NOTHING
3726
+ RETURNING created_on
3727
+ )
3728
+ SELECT created_on into queue_created_on from q;
3476
3729
 
3477
- EXECUTE format('CREATE TABLE ${schema}.%I (LIKE ${schema}.job INCLUDING DEFAULTS)', tablename);
3730
+ IF queue_created_on IS NULL OR options->>'partition' IS DISTINCT FROM 'true' THEN
3731
+ RETURN;
3732
+ END IF;
3478
3733
 
3479
- EXECUTE format('ALTER TABLE ${schema}.%1$I ADD PRIMARY KEY (name, id)', tablename);
3480
- EXECUTE format('ALTER TABLE ${schema}.%1$I ADD CONSTRAINT q_fkey FOREIGN KEY (name) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED', tablename);
3481
- EXECUTE format('ALTER TABLE ${schema}.%1$I ADD CONSTRAINT dlq_fkey FOREIGN KEY (dead_letter) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED', tablename);
3734
+ EXECUTE format('CREATE TABLE ${schema}.%I (LIKE ${schema}.job INCLUDING DEFAULTS)', tablename);
3482
3735
 
3483
- EXECUTE format('CREATE INDEX %1$s_i5 ON ${schema}.%1$I (name, start_after) INCLUDE (priority, created_on, id) WHERE state < ''active''', tablename);
3484
- EXECUTE format('CREATE UNIQUE INDEX %1$s_i4 ON ${schema}.%1$I (name, singleton_on, COALESCE(singleton_key, '''')) WHERE state <> ''cancelled'' AND singleton_on IS NOT NULL', tablename);
3736
+ EXECUTE format('ALTER TABLE ${schema}.%1$I ADD PRIMARY KEY (name, id)', tablename);
3737
+ EXECUTE format('ALTER TABLE ${schema}.%1$I ADD CONSTRAINT q_fkey FOREIGN KEY (name) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED', tablename);
3738
+ EXECUTE format('ALTER TABLE ${schema}.%1$I ADD CONSTRAINT dlq_fkey FOREIGN KEY (dead_letter) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED', tablename);
3485
3739
 
3486
- IF options->>'policy' = 'short' THEN
3487
- EXECUTE format('CREATE UNIQUE INDEX %1$s_i1 ON ${schema}.%1$I (name, COALESCE(singleton_key, '''')) WHERE state = ''created'' AND policy = ''short''', tablename);
3488
- ELSIF options->>'policy' = 'singleton' THEN
3489
- EXECUTE format('CREATE UNIQUE INDEX %1$s_i2 ON ${schema}.%1$I (name, COALESCE(singleton_key, '''')) WHERE state = ''active'' AND policy = ''singleton''', tablename);
3490
- ELSIF options->>'policy' = 'stately' THEN
3491
- EXECUTE format('CREATE UNIQUE INDEX %1$s_i3 ON ${schema}.%1$I (name, state, COALESCE(singleton_key, '''')) WHERE state <= ''active'' AND policy = ''stately''', tablename);
3492
- ELSIF options->>'policy' = 'exclusive' THEN
3493
- EXECUTE format('CREATE UNIQUE INDEX %1$s_i6 ON ${schema}.%1$I (name, COALESCE(singleton_key, '''')) WHERE state <= ''active'' AND policy = ''exclusive''', tablename);
3494
- END IF;
3740
+ EXECUTE format('CREATE INDEX %1$s_i5 ON ${schema}.%1$I (name, start_after) INCLUDE (priority, created_on, id) WHERE state < ''active''', tablename);
3741
+ EXECUTE format('CREATE UNIQUE INDEX %1$s_i4 ON ${schema}.%1$I (name, singleton_on, COALESCE(singleton_key, '''')) WHERE state <> ''cancelled'' AND singleton_on IS NOT NULL', tablename);
3495
3742
 
3496
- EXECUTE format('ALTER TABLE ${schema}.%I ADD CONSTRAINT cjc CHECK (name=%L)', tablename, queue_name);
3497
- EXECUTE format('ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.%I FOR VALUES IN (%L)', tablename, queue_name);
3498
- END;
3499
- $$
3500
- LANGUAGE plpgsql;
3501
- `, `CREATE UNIQUE INDEX job_i6 ON ${schema}.job_common (name, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'exclusive'`],
3502
- uninstall: [`DROP INDEX ${schema}.job_i6`]
3503
- },
3504
- {
3505
- release: "12.6.0",
3506
- version: 27,
3507
- previous: 26,
3508
- install: [
3509
- `ALTER TABLE ${schema}.version ADD COLUMN IF NOT EXISTS bam_on timestamp with time zone`,
3510
- `
3511
- CREATE TABLE IF NOT EXISTS ${schema}.bam (
3512
- id uuid PRIMARY KEY default gen_random_uuid(),
3513
- name text NOT NULL,
3514
- version int NOT NULL,
3515
- status text NOT NULL DEFAULT 'pending',
3516
- queue text,
3517
- table_name text NOT NULL,
3518
- command text NOT NULL,
3519
- error text,
3520
- created_on timestamp with time zone NOT NULL DEFAULT now(),
3521
- started_on timestamp with time zone,
3522
- completed_on timestamp with time zone
3523
- )
3524
- `,
3525
- `CREATE FUNCTION ${schema}.job_table_format(command text, table_name text)
3526
- RETURNS text AS
3527
- $$
3528
- SELECT format(
3529
- replace(
3530
- replace(command, '.job', '.%1$I'),
3531
- 'job_i', '%1$s_i'
3532
- ),
3533
- table_name
3534
- );
3535
- $$
3536
- LANGUAGE sql IMMUTABLE;
3537
- `,
3538
- `
3539
- CREATE OR REPLACE FUNCTION ${schema}.job_table_run_async(command_name text, version int, command text, tbl_name text DEFAULT NULL, queue_name text DEFAULT NULL)
3540
- RETURNS VOID AS
3541
- $$
3542
- BEGIN
3543
- IF queue_name IS NOT NULL THEN
3544
- SELECT table_name INTO tbl_name FROM ${schema}.queue WHERE name = queue_name;
3545
- END IF;
3743
+ IF options->>'policy' = 'short' THEN
3744
+ EXECUTE format('CREATE UNIQUE INDEX %1$s_i1 ON ${schema}.%1$I (name, COALESCE(singleton_key, '''')) WHERE state = ''created'' AND policy = ''short''', tablename);
3745
+ ELSIF options->>'policy' = 'singleton' THEN
3746
+ EXECUTE format('CREATE UNIQUE INDEX %1$s_i2 ON ${schema}.%1$I (name, COALESCE(singleton_key, '''')) WHERE state = ''active'' AND policy = ''singleton''', tablename);
3747
+ ELSIF options->>'policy' = 'stately' THEN
3748
+ EXECUTE format('CREATE UNIQUE INDEX %1$s_i3 ON ${schema}.%1$I (name, state, COALESCE(singleton_key, '''')) WHERE state <= ''active'' AND policy = ''stately''', tablename);
3749
+ ELSIF options->>'policy' = 'exclusive' THEN
3750
+ EXECUTE format('CREATE UNIQUE INDEX %1$s_i6 ON ${schema}.%1$I (name, COALESCE(singleton_key, '''')) WHERE state <= ''active'' AND policy = ''exclusive''', tablename);
3751
+ END IF;
3546
3752
 
3547
- IF tbl_name IS NOT NULL THEN
3548
- INSERT INTO ${schema}.bam (name, version, status, queue, table_name, command)
3549
- VALUES (
3550
- command_name,
3551
- version,
3552
- 'pending',
3553
- queue_name,
3554
- tbl_name,
3555
- ${schema}.job_table_format(command, tbl_name)
3556
- );
3557
- RETURN;
3558
- END IF;
3753
+ EXECUTE format('ALTER TABLE ${schema}.%I ADD CONSTRAINT cjc CHECK (name=%L)', tablename, queue_name);
3754
+ EXECUTE format('ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.%I FOR VALUES IN (%L)', tablename, queue_name);
3755
+ END;
3756
+ $$
3757
+ LANGUAGE plpgsql;
3758
+ `,
3759
+ 27: (schema) => `
3760
+ CREATE OR REPLACE FUNCTION ${schema}.create_queue(queue_name text, options jsonb)
3761
+ RETURNS VOID AS
3762
+ $$
3763
+ DECLARE
3764
+ tablename varchar := CASE WHEN options->>'partition' = 'true'
3765
+ THEN 'j' || encode(sha224(queue_name::bytea), 'hex')
3766
+ ELSE 'job_common'
3767
+ END;
3768
+ queue_created_on timestamptz;
3769
+ BEGIN
3559
3770
 
3560
- INSERT INTO ${schema}.bam (name, version, status, queue, table_name, command)
3561
- SELECT
3771
+ WITH q as (
3772
+ INSERT INTO ${schema}.queue (
3773
+ name,
3774
+ policy,
3775
+ retry_limit,
3776
+ retry_delay,
3777
+ retry_backoff,
3778
+ retry_delay_max,
3779
+ expire_seconds,
3780
+ retention_seconds,
3781
+ deletion_seconds,
3782
+ warning_queued,
3783
+ dead_letter,
3784
+ partition,
3785
+ table_name
3786
+ )
3787
+ VALUES (
3788
+ queue_name,
3789
+ options->>'policy',
3790
+ COALESCE((options->>'retryLimit')::int, 2),
3791
+ COALESCE((options->>'retryDelay')::int, 0),
3792
+ COALESCE((options->>'retryBackoff')::bool, false),
3793
+ (options->>'retryDelayMax')::int,
3794
+ COALESCE((options->>'expireInSeconds')::int, 900),
3795
+ COALESCE((options->>'retentionSeconds')::int, 1209600),
3796
+ COALESCE((options->>'deleteAfterSeconds')::int, 604800),
3797
+ COALESCE((options->>'warningQueueSize')::int, 0),
3798
+ options->>'deadLetter',
3799
+ COALESCE((options->>'partition')::bool, false),
3800
+ tablename
3801
+ )
3802
+ ON CONFLICT DO NOTHING
3803
+ RETURNING created_on
3804
+ )
3805
+ SELECT created_on into queue_created_on from q;
3806
+
3807
+ IF queue_created_on IS NULL OR options->>'partition' IS DISTINCT FROM 'true' THEN
3808
+ RETURN;
3809
+ END IF;
3810
+
3811
+ EXECUTE format('CREATE TABLE ${schema}.%I (LIKE ${schema}.job INCLUDING DEFAULTS)', tablename);
3812
+
3813
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD PRIMARY KEY (name, id)$cmd$, tablename);
3814
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT q_fkey FOREIGN KEY (name) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
3815
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT dlq_fkey FOREIGN KEY (dead_letter) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
3816
+
3817
+ EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i5 ON ${schema}.job (name, start_after) INCLUDE (priority, created_on, id) WHERE state < 'active'$cmd$, tablename);
3818
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i4 ON ${schema}.job (name, singleton_on, COALESCE(singleton_key, '')) WHERE state <> 'cancelled' AND singleton_on IS NOT NULL$cmd$, tablename);
3819
+ EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i7 ON ${schema}.job (name, group_id) WHERE state = 'active' AND group_id IS NOT NULL$cmd$, tablename);
3820
+
3821
+ IF options->>'policy' = 'short' THEN
3822
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i1 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = 'created' AND policy = 'short'$cmd$, tablename);
3823
+ ELSIF options->>'policy' = 'singleton' THEN
3824
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i2 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = 'active' AND policy = 'singleton'$cmd$, tablename);
3825
+ ELSIF options->>'policy' = 'stately' THEN
3826
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i3 ON ${schema}.job (name, state, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'stately'$cmd$, tablename);
3827
+ ELSIF options->>'policy' = 'exclusive' THEN
3828
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i6 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'exclusive'$cmd$, tablename);
3829
+ END IF;
3830
+
3831
+ EXECUTE format('ALTER TABLE ${schema}.%I ADD CONSTRAINT cjc CHECK (name=%L)', tablename, queue_name);
3832
+ EXECUTE format('ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.%I FOR VALUES IN (%L)', tablename, queue_name);
3833
+ END;
3834
+ $$
3835
+ LANGUAGE plpgsql;
3836
+ `,
3837
+ 28: (schema) => `
3838
+ CREATE OR REPLACE FUNCTION ${schema}.create_queue(queue_name text, options jsonb)
3839
+ RETURNS VOID AS
3840
+ $$
3841
+ DECLARE
3842
+ tablename varchar := CASE WHEN options->>'partition' = 'true'
3843
+ THEN 'j' || encode(sha224(queue_name::bytea), 'hex')
3844
+ ELSE 'job_common'
3845
+ END;
3846
+ queue_created_on timestamptz;
3847
+ BEGIN
3848
+
3849
+ WITH q as (
3850
+ INSERT INTO ${schema}.queue (
3851
+ name,
3852
+ policy,
3853
+ retry_limit,
3854
+ retry_delay,
3855
+ retry_backoff,
3856
+ retry_delay_max,
3857
+ expire_seconds,
3858
+ retention_seconds,
3859
+ deletion_seconds,
3860
+ warning_queued,
3861
+ dead_letter,
3862
+ partition,
3863
+ table_name
3864
+ )
3865
+ VALUES (
3866
+ queue_name,
3867
+ options->>'policy',
3868
+ COALESCE((options->>'retryLimit')::int, 2),
3869
+ COALESCE((options->>'retryDelay')::int, 0),
3870
+ COALESCE((options->>'retryBackoff')::bool, false),
3871
+ (options->>'retryDelayMax')::int,
3872
+ COALESCE((options->>'expireInSeconds')::int, 900),
3873
+ COALESCE((options->>'retentionSeconds')::int, 1209600),
3874
+ COALESCE((options->>'deleteAfterSeconds')::int, 604800),
3875
+ COALESCE((options->>'warningQueueSize')::int, 0),
3876
+ options->>'deadLetter',
3877
+ COALESCE((options->>'partition')::bool, false),
3878
+ tablename
3879
+ )
3880
+ ON CONFLICT DO NOTHING
3881
+ RETURNING created_on
3882
+ )
3883
+ SELECT created_on into queue_created_on from q;
3884
+
3885
+ IF queue_created_on IS NULL OR options->>'partition' IS DISTINCT FROM 'true' THEN
3886
+ RETURN;
3887
+ END IF;
3888
+
3889
+ EXECUTE format('CREATE TABLE ${schema}.%I (LIKE ${schema}.job INCLUDING DEFAULTS)', tablename);
3890
+
3891
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD PRIMARY KEY (name, id)$cmd$, tablename);
3892
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT q_fkey FOREIGN KEY (name) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
3893
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT dlq_fkey FOREIGN KEY (dead_letter) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
3894
+
3895
+ EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i5 ON ${schema}.job (name, start_after) INCLUDE (priority, created_on, id) WHERE state < 'active'$cmd$, tablename);
3896
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i4 ON ${schema}.job (name, singleton_on, COALESCE(singleton_key, '')) WHERE state <> 'cancelled' AND singleton_on IS NOT NULL$cmd$, tablename);
3897
+ EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i7 ON ${schema}.job (name, group_id) WHERE state = 'active' AND group_id IS NOT NULL$cmd$, tablename);
3898
+
3899
+ IF options->>'policy' = 'short' THEN
3900
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i1 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = 'created' AND policy = 'short'$cmd$, tablename);
3901
+ ELSIF options->>'policy' = 'singleton' THEN
3902
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i2 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = 'active' AND policy = 'singleton'$cmd$, tablename);
3903
+ ELSIF options->>'policy' = 'stately' THEN
3904
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i3 ON ${schema}.job (name, state, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'stately'$cmd$, tablename);
3905
+ ELSIF options->>'policy' = 'exclusive' THEN
3906
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i6 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'exclusive'$cmd$, tablename);
3907
+ ELSIF options->>'policy' = 'key_strict_fifo' THEN
3908
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i8 ON ${schema}.job (name, singleton_key) WHERE state IN ('active', 'retry', 'failed') AND policy = 'key_strict_fifo'$cmd$, tablename);
3909
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT job_key_strict_fifo_singleton_key_check CHECK (NOT (policy = 'key_strict_fifo' AND singleton_key IS NULL))$cmd$, tablename);
3910
+ END IF;
3911
+
3912
+ EXECUTE format('ALTER TABLE ${schema}.%I ADD CONSTRAINT cjc CHECK (name=%L)', tablename, queue_name);
3913
+ EXECUTE format('ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.%I FOR VALUES IN (%L)', tablename, queue_name);
3914
+ END;
3915
+ $$
3916
+ LANGUAGE plpgsql;
3917
+ `,
3918
+ 30: (schema) => `
3919
+ CREATE OR REPLACE FUNCTION ${schema}.create_queue(queue_name text, options jsonb)
3920
+ RETURNS VOID AS
3921
+ $$
3922
+ DECLARE
3923
+ tablename varchar := CASE WHEN options->>'partition' = 'true'
3924
+ THEN 'j' || encode(sha224(queue_name::bytea), 'hex')
3925
+ ELSE 'job_common'
3926
+ END;
3927
+ queue_created_on timestamptz;
3928
+ BEGIN
3929
+
3930
+ WITH q as (
3931
+ INSERT INTO ${schema}.queue (
3932
+ name,
3933
+ policy,
3934
+ retry_limit,
3935
+ retry_delay,
3936
+ retry_backoff,
3937
+ retry_delay_max,
3938
+ expire_seconds,
3939
+ retention_seconds,
3940
+ deletion_seconds,
3941
+ warning_queued,
3942
+ dead_letter,
3943
+ partition,
3944
+ table_name,
3945
+ heartbeat_seconds
3946
+ )
3947
+ VALUES (
3948
+ queue_name,
3949
+ options->>'policy',
3950
+ COALESCE((options->>'retryLimit')::int, 2),
3951
+ COALESCE((options->>'retryDelay')::int, 0),
3952
+ COALESCE((options->>'retryBackoff')::bool, false),
3953
+ (options->>'retryDelayMax')::int,
3954
+ COALESCE((options->>'expireInSeconds')::int, 900),
3955
+ COALESCE((options->>'retentionSeconds')::int, 1209600),
3956
+ COALESCE((options->>'deleteAfterSeconds')::int, 604800),
3957
+ COALESCE((options->>'warningQueueSize')::int, 0),
3958
+ options->>'deadLetter',
3959
+ COALESCE((options->>'partition')::bool, false),
3960
+ tablename,
3961
+ (options->>'heartbeatSeconds')::int
3962
+ )
3963
+ ON CONFLICT DO NOTHING
3964
+ RETURNING created_on
3965
+ )
3966
+ SELECT created_on into queue_created_on from q;
3967
+
3968
+ IF queue_created_on IS NULL OR options->>'partition' IS DISTINCT FROM 'true' THEN
3969
+ RETURN;
3970
+ END IF;
3971
+
3972
+ EXECUTE format('CREATE TABLE ${schema}.%I (LIKE ${schema}.job INCLUDING DEFAULTS)', tablename);
3973
+
3974
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD PRIMARY KEY (name, id)$cmd$, tablename);
3975
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT q_fkey FOREIGN KEY (name) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
3976
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT dlq_fkey FOREIGN KEY (dead_letter) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
3977
+
3978
+ EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i5 ON ${schema}.job (name, start_after) INCLUDE (priority, created_on, id) WHERE state < 'active'$cmd$, tablename);
3979
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i4 ON ${schema}.job (name, singleton_on, COALESCE(singleton_key, '')) WHERE state <> 'cancelled' AND singleton_on IS NOT NULL$cmd$, tablename);
3980
+ EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i7 ON ${schema}.job (name, group_id) WHERE state = 'active' AND group_id IS NOT NULL$cmd$, tablename);
3981
+
3982
+ IF options->>'policy' = 'short' THEN
3983
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i1 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = 'created' AND policy = 'short'$cmd$, tablename);
3984
+ ELSIF options->>'policy' = 'singleton' THEN
3985
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i2 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = 'active' AND policy = 'singleton'$cmd$, tablename);
3986
+ ELSIF options->>'policy' = 'stately' THEN
3987
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i3 ON ${schema}.job (name, state, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'stately'$cmd$, tablename);
3988
+ ELSIF options->>'policy' = 'exclusive' THEN
3989
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i6 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'exclusive'$cmd$, tablename);
3990
+ ELSIF options->>'policy' = 'key_strict_fifo' THEN
3991
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i8 ON ${schema}.job (name, singleton_key) WHERE state IN ('active', 'retry', 'failed') AND policy = 'key_strict_fifo'$cmd$, tablename);
3992
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT job_key_strict_fifo_singleton_key_check CHECK (NOT (policy = 'key_strict_fifo' AND singleton_key IS NULL))$cmd$, tablename);
3993
+ END IF;
3994
+
3995
+ EXECUTE format('ALTER TABLE ${schema}.%I ADD CONSTRAINT cjc CHECK (name=%L)', tablename, queue_name);
3996
+ EXECUTE format('ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.%I FOR VALUES IN (%L)', tablename, queue_name);
3997
+ END;
3998
+ $$
3999
+ LANGUAGE plpgsql;
4000
+ `,
4001
+ 31: (schema) => `
4002
+ CREATE OR REPLACE FUNCTION ${schema}.create_queue(queue_name text, options jsonb)
4003
+ RETURNS VOID AS
4004
+ $$
4005
+ DECLARE
4006
+ tablename varchar := CASE WHEN options->>'partition' = 'true'
4007
+ THEN 'j' || encode(sha224(queue_name::bytea), 'hex')
4008
+ ELSE 'job_common'
4009
+ END;
4010
+ queue_created_on timestamptz;
4011
+ BEGIN
4012
+
4013
+ WITH q as (
4014
+ INSERT INTO ${schema}.queue (
4015
+ name,
4016
+ policy,
4017
+ retry_limit,
4018
+ retry_delay,
4019
+ retry_backoff,
4020
+ retry_delay_max,
4021
+ expire_seconds,
4022
+ retention_seconds,
4023
+ deletion_seconds,
4024
+ warning_queued,
4025
+ dead_letter,
4026
+ partition,
4027
+ table_name,
4028
+ heartbeat_seconds
4029
+ )
4030
+ VALUES (
4031
+ queue_name,
4032
+ options->>'policy',
4033
+ COALESCE((options->>'retryLimit')::int, 2),
4034
+ COALESCE((options->>'retryDelay')::int, 0),
4035
+ COALESCE((options->>'retryBackoff')::bool, false),
4036
+ (options->>'retryDelayMax')::int,
4037
+ COALESCE((options->>'expireInSeconds')::int, 900),
4038
+ COALESCE((options->>'retentionSeconds')::int, 1209600),
4039
+ COALESCE((options->>'deleteAfterSeconds')::int, 604800),
4040
+ COALESCE((options->>'warningQueueSize')::int, 0),
4041
+ options->>'deadLetter',
4042
+ COALESCE((options->>'partition')::bool, false),
4043
+ tablename,
4044
+ (options->>'heartbeatSeconds')::int
4045
+ )
4046
+ ON CONFLICT DO NOTHING
4047
+ RETURNING created_on
4048
+ )
4049
+ SELECT created_on into queue_created_on from q;
4050
+
4051
+ IF queue_created_on IS NULL OR options->>'partition' IS DISTINCT FROM 'true' THEN
4052
+ RETURN;
4053
+ END IF;
4054
+
4055
+ EXECUTE format('CREATE TABLE ${schema}.%I (LIKE ${schema}.job INCLUDING DEFAULTS)', tablename);
4056
+
4057
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD PRIMARY KEY (name, id)$cmd$, tablename);
4058
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT q_fkey FOREIGN KEY (name) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
4059
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT dlq_fkey FOREIGN KEY (dead_letter) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
4060
+
4061
+ EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i5 ON ${schema}.job (name, start_after) INCLUDE (priority, created_on, id) WHERE state < 'active' AND NOT blocked$cmd$, tablename);
4062
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i4 ON ${schema}.job (name, singleton_on, COALESCE(singleton_key, '')) WHERE state <> 'cancelled' AND singleton_on IS NOT NULL$cmd$, tablename);
4063
+ EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i7 ON ${schema}.job (name, group_id) WHERE state = 'active' AND group_id IS NOT NULL$cmd$, tablename);
4064
+
4065
+ IF options->>'policy' = 'short' THEN
4066
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i1 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = 'created' AND policy = 'short'$cmd$, tablename);
4067
+ ELSIF options->>'policy' = 'singleton' THEN
4068
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i2 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = 'active' AND policy = 'singleton'$cmd$, tablename);
4069
+ ELSIF options->>'policy' = 'stately' THEN
4070
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i3 ON ${schema}.job (name, state, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'stately'$cmd$, tablename);
4071
+ ELSIF options->>'policy' = 'exclusive' THEN
4072
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i6 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'exclusive'$cmd$, tablename);
4073
+ ELSIF options->>'policy' = 'key_strict_fifo' THEN
4074
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i8 ON ${schema}.job (name, singleton_key) WHERE state IN ('active', 'retry', 'failed') AND policy = 'key_strict_fifo'$cmd$, tablename);
4075
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT job_key_strict_fifo_singleton_key_check CHECK (NOT (policy = 'key_strict_fifo' AND singleton_key IS NULL))$cmd$, tablename);
4076
+ END IF;
4077
+
4078
+ EXECUTE format('ALTER TABLE ${schema}.%I ADD CONSTRAINT cjc CHECK (name=%L)', tablename, queue_name);
4079
+ EXECUTE format('ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.%I FOR VALUES IN (%L)', tablename, queue_name);
4080
+ END;
4081
+ $$
4082
+ LANGUAGE plpgsql;
4083
+ `,
4084
+ 32: (schema) => `
4085
+ CREATE OR REPLACE FUNCTION ${schema}.create_queue(queue_name text, options jsonb)
4086
+ RETURNS VOID AS
4087
+ $$
4088
+ DECLARE
4089
+ tablename varchar := CASE WHEN options->>'partition' = 'true'
4090
+ THEN 'j' || encode(sha224(queue_name::bytea), 'hex')
4091
+ ELSE 'job_common'
4092
+ END;
4093
+ queue_created_on timestamptz;
4094
+ BEGIN
4095
+
4096
+ WITH q as (
4097
+ INSERT INTO ${schema}.queue (
4098
+ name,
4099
+ policy,
4100
+ retry_limit,
4101
+ retry_delay,
4102
+ retry_backoff,
4103
+ retry_delay_max,
4104
+ expire_seconds,
4105
+ retention_seconds,
4106
+ deletion_seconds,
4107
+ warning_queued,
4108
+ dead_letter,
4109
+ partition,
4110
+ table_name,
4111
+ heartbeat_seconds,
4112
+ notify
4113
+ )
4114
+ VALUES (
4115
+ queue_name,
4116
+ options->>'policy',
4117
+ COALESCE((options->>'retryLimit')::int, 2),
4118
+ COALESCE((options->>'retryDelay')::int, 0),
4119
+ COALESCE((options->>'retryBackoff')::bool, false),
4120
+ (options->>'retryDelayMax')::int,
4121
+ COALESCE((options->>'expireInSeconds')::int, 900),
4122
+ COALESCE((options->>'retentionSeconds')::int, 1209600),
4123
+ COALESCE((options->>'deleteAfterSeconds')::int, 604800),
4124
+ COALESCE((options->>'warningQueueSize')::int, 0),
4125
+ options->>'deadLetter',
4126
+ COALESCE((options->>'partition')::bool, false),
4127
+ tablename,
4128
+ (options->>'heartbeatSeconds')::int,
4129
+ COALESCE((options->>'notify')::bool, false)
4130
+ )
4131
+ ON CONFLICT DO NOTHING
4132
+ RETURNING created_on
4133
+ )
4134
+ SELECT created_on into queue_created_on from q;
4135
+
4136
+ IF queue_created_on IS NULL OR options->>'partition' IS DISTINCT FROM 'true' THEN
4137
+ RETURN;
4138
+ END IF;
4139
+
4140
+ EXECUTE format('CREATE TABLE ${schema}.%I (LIKE ${schema}.job INCLUDING DEFAULTS)', tablename);
4141
+
4142
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD PRIMARY KEY (name, id)$cmd$, tablename);
4143
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT q_fkey FOREIGN KEY (name) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
4144
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT dlq_fkey FOREIGN KEY (dead_letter) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
4145
+
4146
+ EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i5 ON ${schema}.job (name, start_after) INCLUDE (priority, created_on, id) WHERE state < 'active' AND NOT blocked$cmd$, tablename);
4147
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i4 ON ${schema}.job (name, singleton_on, COALESCE(singleton_key, '')) WHERE state <> 'cancelled' AND singleton_on IS NOT NULL$cmd$, tablename);
4148
+ EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i7 ON ${schema}.job (name, group_id) WHERE state = 'active' AND group_id IS NOT NULL$cmd$, tablename);
4149
+
4150
+ IF options->>'policy' = 'short' THEN
4151
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i1 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = 'created' AND policy = 'short'$cmd$, tablename);
4152
+ ELSIF options->>'policy' = 'singleton' THEN
4153
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i2 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = 'active' AND policy = 'singleton'$cmd$, tablename);
4154
+ ELSIF options->>'policy' = 'stately' THEN
4155
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i3 ON ${schema}.job (name, state, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'stately'$cmd$, tablename);
4156
+ ELSIF options->>'policy' = 'exclusive' THEN
4157
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i6 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'exclusive'$cmd$, tablename);
4158
+ ELSIF options->>'policy' = 'key_strict_fifo' THEN
4159
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i8 ON ${schema}.job (name, singleton_key) WHERE state IN ('active', 'retry', 'failed') AND policy = 'key_strict_fifo'$cmd$, tablename);
4160
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT job_key_strict_fifo_singleton_key_check CHECK (NOT (policy = 'key_strict_fifo' AND singleton_key IS NULL))$cmd$, tablename);
4161
+ END IF;
4162
+
4163
+ EXECUTE format('ALTER TABLE ${schema}.%I ADD CONSTRAINT cjc CHECK (name=%L)', tablename, queue_name);
4164
+ EXECUTE format('ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.%I FOR VALUES IN (%L)', tablename, queue_name);
4165
+ END;
4166
+ $$
4167
+ LANGUAGE plpgsql;
4168
+ `,
4169
+ 33: (schema) => `
4170
+ CREATE OR REPLACE FUNCTION ${schema}.create_queue(queue_name text, options jsonb)
4171
+ RETURNS VOID AS
4172
+ $$
4173
+ DECLARE
4174
+ tablename varchar := CASE WHEN options->>'partition' = 'true'
4175
+ THEN 'j' || encode(sha224(queue_name::bytea), 'hex')
4176
+ ELSE 'job_common'
4177
+ END;
4178
+ queue_created_on timestamptz;
4179
+ BEGIN
4180
+
4181
+ WITH q as (
4182
+ INSERT INTO ${schema}.queue (
4183
+ name,
4184
+ policy,
4185
+ retry_limit,
4186
+ retry_delay,
4187
+ retry_backoff,
4188
+ retry_delay_max,
4189
+ expire_seconds,
4190
+ retention_seconds,
4191
+ deletion_seconds,
4192
+ warning_queued,
4193
+ dead_letter,
4194
+ partition,
4195
+ table_name,
4196
+ heartbeat_seconds,
4197
+ notify
4198
+ )
4199
+ VALUES (
4200
+ queue_name,
4201
+ options->>'policy',
4202
+ COALESCE((options->>'retryLimit')::int, 2),
4203
+ COALESCE((options->>'retryDelay')::int, 0),
4204
+ COALESCE((options->>'retryBackoff')::bool, false),
4205
+ (options->>'retryDelayMax')::int,
4206
+ COALESCE((options->>'expireInSeconds')::int, 900),
4207
+ COALESCE((options->>'retentionSeconds')::int, 1209600),
4208
+ COALESCE((options->>'deleteAfterSeconds')::int, 604800),
4209
+ COALESCE((options->>'warningQueueSize')::int, 0),
4210
+ options->>'deadLetter',
4211
+ COALESCE((options->>'partition')::bool, false),
4212
+ tablename,
4213
+ (options->>'heartbeatSeconds')::int,
4214
+ COALESCE((options->>'notify')::bool, false)
4215
+ )
4216
+ ON CONFLICT DO NOTHING
4217
+ RETURNING created_on
4218
+ )
4219
+ SELECT created_on into queue_created_on from q;
4220
+
4221
+ IF queue_created_on IS NULL OR options->>'partition' IS DISTINCT FROM 'true' THEN
4222
+ RETURN;
4223
+ END IF;
4224
+
4225
+ EXECUTE format('CREATE TABLE ${schema}.%I (LIKE ${schema}.job INCLUDING DEFAULTS)', tablename);
4226
+
4227
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD PRIMARY KEY (name, id)$cmd$, tablename);
4228
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT q_fkey FOREIGN KEY (name) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
4229
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT dlq_fkey FOREIGN KEY (dead_letter) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
4230
+
4231
+ EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i5 ON ${schema}.job (name, start_after) WHERE state < 'active' AND NOT blocked$cmd$, tablename);
4232
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i4 ON ${schema}.job (name, singleton_on, COALESCE(singleton_key, '')) WHERE state <> 'cancelled' AND singleton_on IS NOT NULL$cmd$, tablename);
4233
+ EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i7 ON ${schema}.job (name, group_id) WHERE state = 'active' AND group_id IS NOT NULL$cmd$, tablename);
4234
+ EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i9 ON ${schema}.job (name, id) WHERE blocking AND state = 'completed'$cmd$, tablename);
4235
+
4236
+ IF options->>'policy' = 'short' THEN
4237
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i1 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = 'created' AND policy = 'short'$cmd$, tablename);
4238
+ ELSIF options->>'policy' = 'singleton' THEN
4239
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i2 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = 'active' AND policy = 'singleton'$cmd$, tablename);
4240
+ ELSIF options->>'policy' = 'stately' THEN
4241
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i3 ON ${schema}.job (name, state, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'stately'$cmd$, tablename);
4242
+ ELSIF options->>'policy' = 'exclusive' THEN
4243
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i6 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'exclusive'$cmd$, tablename);
4244
+ ELSIF options->>'policy' = 'key_strict_fifo' THEN
4245
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i8 ON ${schema}.job (name, singleton_key) WHERE state IN ('active', 'retry', 'failed') AND policy = 'key_strict_fifo'$cmd$, tablename);
4246
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT job_key_strict_fifo_singleton_key_check CHECK (NOT (policy = 'key_strict_fifo' AND singleton_key IS NULL))$cmd$, tablename);
4247
+ END IF;
4248
+
4249
+ EXECUTE format('ALTER TABLE ${schema}.%I ADD CONSTRAINT cjc CHECK (name=%L)', tablename, queue_name);
4250
+ EXECUTE format('ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.%I FOR VALUES IN (%L)', tablename, queue_name);
4251
+ END;
4252
+ $$
4253
+ LANGUAGE plpgsql;
4254
+ `
4255
+ };
4256
+ function getAll(schema) {
4257
+ return [
4258
+ {
4259
+ release: "11.1.0",
4260
+ version: 26,
4261
+ previous: 25,
4262
+ install: [createQueueFn[26](schema), `CREATE UNIQUE INDEX job_i6 ON ${schema}.job_common (name, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'exclusive'`],
4263
+ uninstall: [`DROP INDEX ${schema}.job_i6`]
4264
+ },
4265
+ {
4266
+ release: "12.6.0",
4267
+ version: 27,
4268
+ previous: 26,
4269
+ install: [
4270
+ `ALTER TABLE ${schema}.version ADD COLUMN IF NOT EXISTS bam_on timestamp with time zone`,
4271
+ `
4272
+ CREATE TABLE IF NOT EXISTS ${schema}.bam (
4273
+ id uuid PRIMARY KEY default gen_random_uuid(),
4274
+ name text NOT NULL,
4275
+ version int NOT NULL,
4276
+ status text NOT NULL DEFAULT 'pending',
4277
+ queue text,
4278
+ table_name text NOT NULL,
4279
+ command text NOT NULL,
4280
+ error text,
4281
+ created_on timestamp with time zone NOT NULL DEFAULT now(),
4282
+ started_on timestamp with time zone,
4283
+ completed_on timestamp with time zone
4284
+ )
4285
+ `,
4286
+ `CREATE FUNCTION ${schema}.job_table_format(command text, table_name text)
4287
+ RETURNS text AS
4288
+ $$
4289
+ SELECT format(
4290
+ replace(
4291
+ replace(command, '.job', '.%1$I'),
4292
+ 'job_i', '%1$s_i'
4293
+ ),
4294
+ table_name
4295
+ );
4296
+ $$
4297
+ LANGUAGE sql IMMUTABLE;
4298
+ `,
4299
+ `
4300
+ CREATE OR REPLACE FUNCTION ${schema}.job_table_run_async(command_name text, version int, command text, tbl_name text DEFAULT NULL, queue_name text DEFAULT NULL)
4301
+ RETURNS VOID AS
4302
+ $$
4303
+ BEGIN
4304
+ IF queue_name IS NOT NULL THEN
4305
+ SELECT table_name INTO tbl_name FROM ${schema}.queue WHERE name = queue_name;
4306
+ END IF;
4307
+
4308
+ IF tbl_name IS NOT NULL THEN
4309
+ INSERT INTO ${schema}.bam (name, version, status, queue, table_name, command)
4310
+ VALUES (
4311
+ command_name,
4312
+ version,
4313
+ 'pending',
4314
+ queue_name,
4315
+ tbl_name,
4316
+ ${schema}.job_table_format(command, tbl_name)
4317
+ );
4318
+ RETURN;
4319
+ END IF;
4320
+
4321
+ INSERT INTO ${schema}.bam (name, version, status, queue, table_name, command)
4322
+ SELECT
3562
4323
  command_name,
3563
4324
  version,
3564
4325
  'pending',
@@ -3607,84 +4368,7 @@ function getAll(schema) {
3607
4368
  `,
3608
4369
  `ALTER TABLE ${schema}.job ADD COLUMN IF NOT EXISTS group_id text`,
3609
4370
  `ALTER TABLE ${schema}.job ADD COLUMN IF NOT EXISTS group_tier text`,
3610
- `
3611
- CREATE OR REPLACE FUNCTION ${schema}.create_queue(queue_name text, options jsonb)
3612
- RETURNS VOID AS
3613
- $$
3614
- DECLARE
3615
- tablename varchar := CASE WHEN options->>'partition' = 'true'
3616
- THEN 'j' || encode(sha224(queue_name::bytea), 'hex')
3617
- ELSE 'job_common'
3618
- END;
3619
- queue_created_on timestamptz;
3620
- BEGIN
3621
-
3622
- WITH q as (
3623
- INSERT INTO ${schema}.queue (
3624
- name,
3625
- policy,
3626
- retry_limit,
3627
- retry_delay,
3628
- retry_backoff,
3629
- retry_delay_max,
3630
- expire_seconds,
3631
- retention_seconds,
3632
- deletion_seconds,
3633
- warning_queued,
3634
- dead_letter,
3635
- partition,
3636
- table_name
3637
- )
3638
- VALUES (
3639
- queue_name,
3640
- options->>'policy',
3641
- COALESCE((options->>'retryLimit')::int, 2),
3642
- COALESCE((options->>'retryDelay')::int, 0),
3643
- COALESCE((options->>'retryBackoff')::bool, false),
3644
- (options->>'retryDelayMax')::int,
3645
- COALESCE((options->>'expireInSeconds')::int, 900),
3646
- COALESCE((options->>'retentionSeconds')::int, 1209600),
3647
- COALESCE((options->>'deleteAfterSeconds')::int, 604800),
3648
- COALESCE((options->>'warningQueueSize')::int, 0),
3649
- options->>'deadLetter',
3650
- COALESCE((options->>'partition')::bool, false),
3651
- tablename
3652
- )
3653
- ON CONFLICT DO NOTHING
3654
- RETURNING created_on
3655
- )
3656
- SELECT created_on into queue_created_on from q;
3657
-
3658
- IF queue_created_on IS NULL OR options->>'partition' IS DISTINCT FROM 'true' THEN
3659
- RETURN;
3660
- END IF;
3661
-
3662
- EXECUTE format('CREATE TABLE ${schema}.%I (LIKE ${schema}.job INCLUDING DEFAULTS)', tablename);
3663
-
3664
- EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD PRIMARY KEY (name, id)$cmd$, tablename);
3665
- EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT q_fkey FOREIGN KEY (name) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
3666
- EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT dlq_fkey FOREIGN KEY (dead_letter) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
3667
-
3668
- EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i5 ON ${schema}.job (name, start_after) INCLUDE (priority, created_on, id) WHERE state < 'active'$cmd$, tablename);
3669
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i4 ON ${schema}.job (name, singleton_on, COALESCE(singleton_key, '')) WHERE state <> 'cancelled' AND singleton_on IS NOT NULL$cmd$, tablename);
3670
- EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i7 ON ${schema}.job (name, group_id) WHERE state = 'active' AND group_id IS NOT NULL$cmd$, tablename);
3671
-
3672
- IF options->>'policy' = 'short' THEN
3673
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i1 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = 'created' AND policy = 'short'$cmd$, tablename);
3674
- ELSIF options->>'policy' = 'singleton' THEN
3675
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i2 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = 'active' AND policy = 'singleton'$cmd$, tablename);
3676
- ELSIF options->>'policy' = 'stately' THEN
3677
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i3 ON ${schema}.job (name, state, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'stately'$cmd$, tablename);
3678
- ELSIF options->>'policy' = 'exclusive' THEN
3679
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i6 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'exclusive'$cmd$, tablename);
3680
- END IF;
3681
-
3682
- EXECUTE format('ALTER TABLE ${schema}.%I ADD CONSTRAINT cjc CHECK (name=%L)', tablename, queue_name);
3683
- EXECUTE format('ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.%I FOR VALUES IN (%L)', tablename, queue_name);
3684
- END;
3685
- $$
3686
- LANGUAGE plpgsql;
3687
- `,
4371
+ createQueueFn[27](schema),
3688
4372
  `ALTER INDEX IF EXISTS ${schema}.job_i1 RENAME TO job_common_i1`,
3689
4373
  `ALTER INDEX IF EXISTS ${schema}.job_i2 RENAME TO job_common_i2`,
3690
4374
  `ALTER INDEX IF EXISTS ${schema}.job_i3 RENAME TO job_common_i3`,
@@ -3708,83 +4392,7 @@ function getAll(schema) {
3708
4392
  `ALTER INDEX ${schema}.job_common_i2 RENAME TO job_i2`,
3709
4393
  `ALTER INDEX ${schema}.job_common_i1 RENAME TO job_i1`,
3710
4394
  `SELECT ${schema}.job_table_run('DROP INDEX ${schema}.job_i7')`,
3711
- `
3712
- CREATE OR REPLACE FUNCTION ${schema}.create_queue(queue_name text, options jsonb)
3713
- RETURNS VOID AS
3714
- $$
3715
- DECLARE
3716
- tablename varchar := CASE WHEN options->>'partition' = 'true'
3717
- THEN 'j' || encode(sha224(queue_name::bytea), 'hex')
3718
- ELSE 'job_common'
3719
- END;
3720
- queue_created_on timestamptz;
3721
- BEGIN
3722
-
3723
- WITH q as (
3724
- INSERT INTO ${schema}.queue (
3725
- name,
3726
- policy,
3727
- retry_limit,
3728
- retry_delay,
3729
- retry_backoff,
3730
- retry_delay_max,
3731
- expire_seconds,
3732
- retention_seconds,
3733
- deletion_seconds,
3734
- warning_queued,
3735
- dead_letter,
3736
- partition,
3737
- table_name
3738
- )
3739
- VALUES (
3740
- queue_name,
3741
- options->>'policy',
3742
- COALESCE((options->>'retryLimit')::int, 2),
3743
- COALESCE((options->>'retryDelay')::int, 0),
3744
- COALESCE((options->>'retryBackoff')::bool, false),
3745
- (options->>'retryDelayMax')::int,
3746
- COALESCE((options->>'expireInSeconds')::int, 900),
3747
- COALESCE((options->>'retentionSeconds')::int, 1209600),
3748
- COALESCE((options->>'deleteAfterSeconds')::int, 604800),
3749
- COALESCE((options->>'warningQueueSize')::int, 0),
3750
- options->>'deadLetter',
3751
- COALESCE((options->>'partition')::bool, false),
3752
- tablename
3753
- )
3754
- ON CONFLICT DO NOTHING
3755
- RETURNING created_on
3756
- )
3757
- SELECT created_on into queue_created_on from q;
3758
-
3759
- IF queue_created_on IS NULL OR options->>'partition' IS DISTINCT FROM 'true' THEN
3760
- RETURN;
3761
- END IF;
3762
-
3763
- EXECUTE format('CREATE TABLE ${schema}.%I (LIKE ${schema}.job INCLUDING DEFAULTS)', tablename);
3764
-
3765
- EXECUTE format('ALTER TABLE ${schema}.%1$I ADD PRIMARY KEY (name, id)', tablename);
3766
- EXECUTE format('ALTER TABLE ${schema}.%1$I ADD CONSTRAINT q_fkey FOREIGN KEY (name) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED', tablename);
3767
- EXECUTE format('ALTER TABLE ${schema}.%1$I ADD CONSTRAINT dlq_fkey FOREIGN KEY (dead_letter) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED', tablename);
3768
-
3769
- EXECUTE format('CREATE INDEX %1$s_i5 ON ${schema}.%1$I (name, start_after) INCLUDE (priority, created_on, id) WHERE state < ''active''', tablename);
3770
- EXECUTE format('CREATE UNIQUE INDEX %1$s_i4 ON ${schema}.%1$I (name, singleton_on, COALESCE(singleton_key, '''')) WHERE state <> ''cancelled'' AND singleton_on IS NOT NULL', tablename);
3771
-
3772
- IF options->>'policy' = 'short' THEN
3773
- EXECUTE format('CREATE UNIQUE INDEX %1$s_i1 ON ${schema}.%1$I (name, COALESCE(singleton_key, '''')) WHERE state = ''created'' AND policy = ''short''', tablename);
3774
- ELSIF options->>'policy' = 'singleton' THEN
3775
- EXECUTE format('CREATE UNIQUE INDEX %1$s_i2 ON ${schema}.%1$I (name, COALESCE(singleton_key, '''')) WHERE state = ''active'' AND policy = ''singleton''', tablename);
3776
- ELSIF options->>'policy' = 'stately' THEN
3777
- EXECUTE format('CREATE UNIQUE INDEX %1$s_i3 ON ${schema}.%1$I (name, state, COALESCE(singleton_key, '''')) WHERE state <= ''active'' AND policy = ''stately''', tablename);
3778
- ELSIF options->>'policy' = 'exclusive' THEN
3779
- EXECUTE format('CREATE UNIQUE INDEX %1$s_i6 ON ${schema}.%1$I (name, COALESCE(singleton_key, '''')) WHERE state <= ''active'' AND policy = ''exclusive''', tablename);
3780
- END IF;
3781
-
3782
- EXECUTE format('ALTER TABLE ${schema}.%I ADD CONSTRAINT cjc CHECK (name=%L)', tablename, queue_name);
3783
- EXECUTE format('ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.%I FOR VALUES IN (%L)', tablename, queue_name);
3784
- END;
3785
- $$
3786
- LANGUAGE plpgsql;
3787
- `,
4395
+ createQueueFn[26](schema),
3788
4396
  `DROP FUNCTION ${schema}.job_table_run(text, text, text)`,
3789
4397
  `DROP FUNCTION ${schema}.job_table_run_async(text, int, text, text, text)`,
3790
4398
  `DROP FUNCTION ${schema}.job_table_format(text, text)`,
@@ -3798,87 +4406,7 @@ function getAll(schema) {
3798
4406
  release: "12.10.0",
3799
4407
  version: 28,
3800
4408
  previous: 27,
3801
- install: [`SELECT ${schema}.job_table_run($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT job_key_strict_fifo_singleton_key_check CHECK (NOT (policy = 'key_strict_fifo' AND singleton_key IS NULL))$cmd$, 'job_common')`, `
3802
- CREATE OR REPLACE FUNCTION ${schema}.create_queue(queue_name text, options jsonb)
3803
- RETURNS VOID AS
3804
- $$
3805
- DECLARE
3806
- tablename varchar := CASE WHEN options->>'partition' = 'true'
3807
- THEN 'j' || encode(sha224(queue_name::bytea), 'hex')
3808
- ELSE 'job_common'
3809
- END;
3810
- queue_created_on timestamptz;
3811
- BEGIN
3812
-
3813
- WITH q as (
3814
- INSERT INTO ${schema}.queue (
3815
- name,
3816
- policy,
3817
- retry_limit,
3818
- retry_delay,
3819
- retry_backoff,
3820
- retry_delay_max,
3821
- expire_seconds,
3822
- retention_seconds,
3823
- deletion_seconds,
3824
- warning_queued,
3825
- dead_letter,
3826
- partition,
3827
- table_name
3828
- )
3829
- VALUES (
3830
- queue_name,
3831
- options->>'policy',
3832
- COALESCE((options->>'retryLimit')::int, 2),
3833
- COALESCE((options->>'retryDelay')::int, 0),
3834
- COALESCE((options->>'retryBackoff')::bool, false),
3835
- (options->>'retryDelayMax')::int,
3836
- COALESCE((options->>'expireInSeconds')::int, 900),
3837
- COALESCE((options->>'retentionSeconds')::int, 1209600),
3838
- COALESCE((options->>'deleteAfterSeconds')::int, 604800),
3839
- COALESCE((options->>'warningQueueSize')::int, 0),
3840
- options->>'deadLetter',
3841
- COALESCE((options->>'partition')::bool, false),
3842
- tablename
3843
- )
3844
- ON CONFLICT DO NOTHING
3845
- RETURNING created_on
3846
- )
3847
- SELECT created_on into queue_created_on from q;
3848
-
3849
- IF queue_created_on IS NULL OR options->>'partition' IS DISTINCT FROM 'true' THEN
3850
- RETURN;
3851
- END IF;
3852
-
3853
- EXECUTE format('CREATE TABLE ${schema}.%I (LIKE ${schema}.job INCLUDING DEFAULTS)', tablename);
3854
-
3855
- EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD PRIMARY KEY (name, id)$cmd$, tablename);
3856
- EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT q_fkey FOREIGN KEY (name) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
3857
- EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT dlq_fkey FOREIGN KEY (dead_letter) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
3858
-
3859
- EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i5 ON ${schema}.job (name, start_after) INCLUDE (priority, created_on, id) WHERE state < 'active'$cmd$, tablename);
3860
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i4 ON ${schema}.job (name, singleton_on, COALESCE(singleton_key, '')) WHERE state <> 'cancelled' AND singleton_on IS NOT NULL$cmd$, tablename);
3861
- EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i7 ON ${schema}.job (name, group_id) WHERE state = 'active' AND group_id IS NOT NULL$cmd$, tablename);
3862
-
3863
- IF options->>'policy' = 'short' THEN
3864
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i1 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = 'created' AND policy = 'short'$cmd$, tablename);
3865
- ELSIF options->>'policy' = 'singleton' THEN
3866
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i2 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = 'active' AND policy = 'singleton'$cmd$, tablename);
3867
- ELSIF options->>'policy' = 'stately' THEN
3868
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i3 ON ${schema}.job (name, state, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'stately'$cmd$, tablename);
3869
- ELSIF options->>'policy' = 'exclusive' THEN
3870
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i6 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'exclusive'$cmd$, tablename);
3871
- ELSIF options->>'policy' = 'key_strict_fifo' THEN
3872
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i8 ON ${schema}.job (name, singleton_key) WHERE state IN ('active', 'retry', 'failed') AND policy = 'key_strict_fifo'$cmd$, tablename);
3873
- EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT job_key_strict_fifo_singleton_key_check CHECK (NOT (policy = 'key_strict_fifo' AND singleton_key IS NULL))$cmd$, tablename);
3874
- END IF;
3875
-
3876
- EXECUTE format('ALTER TABLE ${schema}.%I ADD CONSTRAINT cjc CHECK (name=%L)', tablename, queue_name);
3877
- EXECUTE format('ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.%I FOR VALUES IN (%L)', tablename, queue_name);
3878
- END;
3879
- $$
3880
- LANGUAGE plpgsql;
3881
- `],
4409
+ install: [`SELECT ${schema}.job_table_run($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT job_key_strict_fifo_singleton_key_check CHECK (NOT (policy = 'key_strict_fifo' AND singleton_key IS NULL))$cmd$, 'job_common')`, createQueueFn[28](schema)],
3882
4410
  async: [`SELECT ${schema}.job_table_run_async(
3883
4411
  'key_strict_fifo_index',
3884
4412
  $VERSION$,
@@ -3889,84 +4417,7 @@ function getAll(schema) {
3889
4417
  uninstall: [
3890
4418
  `SELECT ${schema}.job_table_run('DROP INDEX IF EXISTS ${schema}.job_i8')`,
3891
4419
  `SELECT ${schema}.job_table_run('ALTER TABLE ${schema}.job DROP CONSTRAINT IF EXISTS job_key_strict_fifo_singleton_key_check')`,
3892
- `
3893
- CREATE OR REPLACE FUNCTION ${schema}.create_queue(queue_name text, options jsonb)
3894
- RETURNS VOID AS
3895
- $$
3896
- DECLARE
3897
- tablename varchar := CASE WHEN options->>'partition' = 'true'
3898
- THEN 'j' || encode(sha224(queue_name::bytea), 'hex')
3899
- ELSE 'job_common'
3900
- END;
3901
- queue_created_on timestamptz;
3902
- BEGIN
3903
-
3904
- WITH q as (
3905
- INSERT INTO ${schema}.queue (
3906
- name,
3907
- policy,
3908
- retry_limit,
3909
- retry_delay,
3910
- retry_backoff,
3911
- retry_delay_max,
3912
- expire_seconds,
3913
- retention_seconds,
3914
- deletion_seconds,
3915
- warning_queued,
3916
- dead_letter,
3917
- partition,
3918
- table_name
3919
- )
3920
- VALUES (
3921
- queue_name,
3922
- options->>'policy',
3923
- COALESCE((options->>'retryLimit')::int, 2),
3924
- COALESCE((options->>'retryDelay')::int, 0),
3925
- COALESCE((options->>'retryBackoff')::bool, false),
3926
- (options->>'retryDelayMax')::int,
3927
- COALESCE((options->>'expireInSeconds')::int, 900),
3928
- COALESCE((options->>'retentionSeconds')::int, 1209600),
3929
- COALESCE((options->>'deleteAfterSeconds')::int, 604800),
3930
- COALESCE((options->>'warningQueueSize')::int, 0),
3931
- options->>'deadLetter',
3932
- COALESCE((options->>'partition')::bool, false),
3933
- tablename
3934
- )
3935
- ON CONFLICT DO NOTHING
3936
- RETURNING created_on
3937
- )
3938
- SELECT created_on into queue_created_on from q;
3939
-
3940
- IF queue_created_on IS NULL OR options->>'partition' IS DISTINCT FROM 'true' THEN
3941
- RETURN;
3942
- END IF;
3943
-
3944
- EXECUTE format('CREATE TABLE ${schema}.%I (LIKE ${schema}.job INCLUDING DEFAULTS)', tablename);
3945
-
3946
- EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD PRIMARY KEY (name, id)$cmd$, tablename);
3947
- EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT q_fkey FOREIGN KEY (name) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
3948
- EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT dlq_fkey FOREIGN KEY (dead_letter) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
3949
-
3950
- EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i5 ON ${schema}.job (name, start_after) INCLUDE (priority, created_on, id) WHERE state < 'active'$cmd$, tablename);
3951
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i4 ON ${schema}.job (name, singleton_on, COALESCE(singleton_key, '')) WHERE state <> 'cancelled' AND singleton_on IS NOT NULL$cmd$, tablename);
3952
- EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i7 ON ${schema}.job (name, group_id) WHERE state = 'active' AND group_id IS NOT NULL$cmd$, tablename);
3953
-
3954
- IF options->>'policy' = 'short' THEN
3955
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i1 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = 'created' AND policy = 'short'$cmd$, tablename);
3956
- ELSIF options->>'policy' = 'singleton' THEN
3957
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i2 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = 'active' AND policy = 'singleton'$cmd$, tablename);
3958
- ELSIF options->>'policy' = 'stately' THEN
3959
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i3 ON ${schema}.job (name, state, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'stately'$cmd$, tablename);
3960
- ELSIF options->>'policy' = 'exclusive' THEN
3961
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i6 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'exclusive'$cmd$, tablename);
3962
- END IF;
3963
-
3964
- EXECUTE format('ALTER TABLE ${schema}.%I ADD CONSTRAINT cjc CHECK (name=%L)', tablename, queue_name);
3965
- EXECUTE format('ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.%I FOR VALUES IN (%L)', tablename, queue_name);
3966
- END;
3967
- $$
3968
- LANGUAGE plpgsql;
3969
- `
4420
+ createQueueFn[27](schema)
3970
4421
  ]
3971
4422
  },
3972
4423
  {
@@ -3986,176 +4437,14 @@ function getAll(schema) {
3986
4437
  release: "12.12.0",
3987
4438
  version: 30,
3988
4439
  previous: 29,
3989
- install: [
3990
- `ALTER TABLE ${schema}.job ADD COLUMN heartbeat_on timestamp with time zone`,
3991
- `ALTER TABLE ${schema}.job ADD COLUMN heartbeat_seconds int`,
3992
- `ALTER TABLE ${schema}.queue ADD COLUMN heartbeat_seconds int`,
3993
- `
3994
- CREATE OR REPLACE FUNCTION ${schema}.create_queue(queue_name text, options jsonb)
3995
- RETURNS VOID AS
3996
- $$
3997
- DECLARE
3998
- tablename varchar := CASE WHEN options->>'partition' = 'true'
3999
- THEN 'j' || encode(sha224(queue_name::bytea), 'hex')
4000
- ELSE 'job_common'
4001
- END;
4002
- queue_created_on timestamptz;
4003
- BEGIN
4004
-
4005
- WITH q as (
4006
- INSERT INTO ${schema}.queue (
4007
- name,
4008
- policy,
4009
- retry_limit,
4010
- retry_delay,
4011
- retry_backoff,
4012
- retry_delay_max,
4013
- expire_seconds,
4014
- retention_seconds,
4015
- deletion_seconds,
4016
- warning_queued,
4017
- dead_letter,
4018
- partition,
4019
- table_name,
4020
- heartbeat_seconds
4021
- )
4022
- VALUES (
4023
- queue_name,
4024
- options->>'policy',
4025
- COALESCE((options->>'retryLimit')::int, 2),
4026
- COALESCE((options->>'retryDelay')::int, 0),
4027
- COALESCE((options->>'retryBackoff')::bool, false),
4028
- (options->>'retryDelayMax')::int,
4029
- COALESCE((options->>'expireInSeconds')::int, 900),
4030
- COALESCE((options->>'retentionSeconds')::int, 1209600),
4031
- COALESCE((options->>'deleteAfterSeconds')::int, 604800),
4032
- COALESCE((options->>'warningQueueSize')::int, 0),
4033
- options->>'deadLetter',
4034
- COALESCE((options->>'partition')::bool, false),
4035
- tablename,
4036
- (options->>'heartbeatSeconds')::int
4037
- )
4038
- ON CONFLICT DO NOTHING
4039
- RETURNING created_on
4040
- )
4041
- SELECT created_on into queue_created_on from q;
4042
-
4043
- IF queue_created_on IS NULL OR options->>'partition' IS DISTINCT FROM 'true' THEN
4044
- RETURN;
4045
- END IF;
4046
-
4047
- EXECUTE format('CREATE TABLE ${schema}.%I (LIKE ${schema}.job INCLUDING DEFAULTS)', tablename);
4048
-
4049
- EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD PRIMARY KEY (name, id)$cmd$, tablename);
4050
- EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT q_fkey FOREIGN KEY (name) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
4051
- EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT dlq_fkey FOREIGN KEY (dead_letter) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
4052
-
4053
- EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i5 ON ${schema}.job (name, start_after) INCLUDE (priority, created_on, id) WHERE state < 'active'$cmd$, tablename);
4054
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i4 ON ${schema}.job (name, singleton_on, COALESCE(singleton_key, '')) WHERE state <> 'cancelled' AND singleton_on IS NOT NULL$cmd$, tablename);
4055
- EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i7 ON ${schema}.job (name, group_id) WHERE state = 'active' AND group_id IS NOT NULL$cmd$, tablename);
4056
-
4057
- IF options->>'policy' = 'short' THEN
4058
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i1 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = 'created' AND policy = 'short'$cmd$, tablename);
4059
- ELSIF options->>'policy' = 'singleton' THEN
4060
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i2 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = 'active' AND policy = 'singleton'$cmd$, tablename);
4061
- ELSIF options->>'policy' = 'stately' THEN
4062
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i3 ON ${schema}.job (name, state, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'stately'$cmd$, tablename);
4063
- ELSIF options->>'policy' = 'exclusive' THEN
4064
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i6 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'exclusive'$cmd$, tablename);
4065
- ELSIF options->>'policy' = 'key_strict_fifo' THEN
4066
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i8 ON ${schema}.job (name, singleton_key) WHERE state IN ('active', 'retry', 'failed') AND policy = 'key_strict_fifo'$cmd$, tablename);
4067
- EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT job_key_strict_fifo_singleton_key_check CHECK (NOT (policy = 'key_strict_fifo' AND singleton_key IS NULL))$cmd$, tablename);
4068
- END IF;
4069
-
4070
- EXECUTE format('ALTER TABLE ${schema}.%I ADD CONSTRAINT cjc CHECK (name=%L)', tablename, queue_name);
4071
- EXECUTE format('ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.%I FOR VALUES IN (%L)', tablename, queue_name);
4072
- END;
4073
- $$
4074
- LANGUAGE plpgsql;
4075
- `
4076
- ],
4077
- uninstall: [
4078
- `
4079
- CREATE OR REPLACE FUNCTION ${schema}.create_queue(queue_name text, options jsonb)
4080
- RETURNS VOID AS
4081
- $$
4082
- DECLARE
4083
- tablename varchar := CASE WHEN options->>'partition' = 'true'
4084
- THEN 'j' || encode(sha224(queue_name::bytea), 'hex')
4085
- ELSE 'job_common'
4086
- END;
4087
- queue_created_on timestamptz;
4088
- BEGIN
4089
-
4090
- WITH q as (
4091
- INSERT INTO ${schema}.queue (
4092
- name,
4093
- policy,
4094
- retry_limit,
4095
- retry_delay,
4096
- retry_backoff,
4097
- retry_delay_max,
4098
- expire_seconds,
4099
- retention_seconds,
4100
- deletion_seconds,
4101
- warning_queued,
4102
- dead_letter,
4103
- partition,
4104
- table_name
4105
- )
4106
- VALUES (
4107
- queue_name,
4108
- options->>'policy',
4109
- COALESCE((options->>'retryLimit')::int, 2),
4110
- COALESCE((options->>'retryDelay')::int, 0),
4111
- COALESCE((options->>'retryBackoff')::bool, false),
4112
- (options->>'retryDelayMax')::int,
4113
- COALESCE((options->>'expireInSeconds')::int, 900),
4114
- COALESCE((options->>'retentionSeconds')::int, 1209600),
4115
- COALESCE((options->>'deleteAfterSeconds')::int, 604800),
4116
- COALESCE((options->>'warningQueueSize')::int, 0),
4117
- options->>'deadLetter',
4118
- COALESCE((options->>'partition')::bool, false),
4119
- tablename
4120
- )
4121
- ON CONFLICT DO NOTHING
4122
- RETURNING created_on
4123
- )
4124
- SELECT created_on into queue_created_on from q;
4125
-
4126
- IF queue_created_on IS NULL OR options->>'partition' IS DISTINCT FROM 'true' THEN
4127
- RETURN;
4128
- END IF;
4129
-
4130
- EXECUTE format('CREATE TABLE ${schema}.%I (LIKE ${schema}.job INCLUDING DEFAULTS)', tablename);
4131
-
4132
- EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD PRIMARY KEY (name, id)$cmd$, tablename);
4133
- EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT q_fkey FOREIGN KEY (name) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
4134
- EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT dlq_fkey FOREIGN KEY (dead_letter) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
4135
-
4136
- EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i5 ON ${schema}.job (name, start_after) INCLUDE (priority, created_on, id) WHERE state < 'active'$cmd$, tablename);
4137
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i4 ON ${schema}.job (name, singleton_on, COALESCE(singleton_key, '')) WHERE state <> 'cancelled' AND singleton_on IS NOT NULL$cmd$, tablename);
4138
- EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i7 ON ${schema}.job (name, group_id) WHERE state = 'active' AND group_id IS NOT NULL$cmd$, tablename);
4139
-
4140
- IF options->>'policy' = 'short' THEN
4141
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i1 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = 'created' AND policy = 'short'$cmd$, tablename);
4142
- ELSIF options->>'policy' = 'singleton' THEN
4143
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i2 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = 'active' AND policy = 'singleton'$cmd$, tablename);
4144
- ELSIF options->>'policy' = 'stately' THEN
4145
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i3 ON ${schema}.job (name, state, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'stately'$cmd$, tablename);
4146
- ELSIF options->>'policy' = 'exclusive' THEN
4147
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i6 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'exclusive'$cmd$, tablename);
4148
- ELSIF options->>'policy' = 'key_strict_fifo' THEN
4149
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i8 ON ${schema}.job (name, singleton_key) WHERE state IN ('active', 'retry', 'failed') AND policy = 'key_strict_fifo'$cmd$, tablename);
4150
- EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT job_key_strict_fifo_singleton_key_check CHECK (NOT (policy = 'key_strict_fifo' AND singleton_key IS NULL))$cmd$, tablename);
4151
- END IF;
4152
-
4153
- EXECUTE format('ALTER TABLE ${schema}.%I ADD CONSTRAINT cjc CHECK (name=%L)', tablename, queue_name);
4154
- EXECUTE format('ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.%I FOR VALUES IN (%L)', tablename, queue_name);
4155
- END;
4156
- $$
4157
- LANGUAGE plpgsql;
4158
- `,
4440
+ install: [
4441
+ `ALTER TABLE ${schema}.job ADD COLUMN heartbeat_on timestamp with time zone`,
4442
+ `ALTER TABLE ${schema}.job ADD COLUMN heartbeat_seconds int`,
4443
+ `ALTER TABLE ${schema}.queue ADD COLUMN heartbeat_seconds int`,
4444
+ createQueueFn[30](schema)
4445
+ ],
4446
+ uninstall: [
4447
+ createQueueFn[28](schema),
4159
4448
  `ALTER TABLE ${schema}.queue DROP COLUMN heartbeat_seconds`,
4160
4449
  `ALTER TABLE ${schema}.job DROP COLUMN heartbeat_seconds`,
4161
4450
  `ALTER TABLE ${schema}.job DROP COLUMN heartbeat_on`
@@ -4181,182 +4470,54 @@ function getAll(schema) {
4181
4470
  `CREATE INDEX IF NOT EXISTS job_dep_parent_idx ON ${schema}.job_dependency (parent_name, parent_id)`,
4182
4471
  `SELECT ${schema}.job_table_run($cmd$DROP INDEX IF EXISTS ${schema}.job_i5$cmd$)`,
4183
4472
  `SELECT ${schema}.job_table_run($cmd$CREATE INDEX job_i5 ON ${schema}.job (name, start_after) INCLUDE (priority, created_on, id) WHERE state < 'active' AND NOT blocked$cmd$)`,
4184
- `
4185
- CREATE OR REPLACE FUNCTION ${schema}.create_queue(queue_name text, options jsonb)
4186
- RETURNS VOID AS
4187
- $$
4188
- DECLARE
4189
- tablename varchar := CASE WHEN options->>'partition' = 'true'
4190
- THEN 'j' || encode(sha224(queue_name::bytea), 'hex')
4191
- ELSE 'job_common'
4192
- END;
4193
- queue_created_on timestamptz;
4194
- BEGIN
4195
-
4196
- WITH q as (
4197
- INSERT INTO ${schema}.queue (
4198
- name,
4199
- policy,
4200
- retry_limit,
4201
- retry_delay,
4202
- retry_backoff,
4203
- retry_delay_max,
4204
- expire_seconds,
4205
- retention_seconds,
4206
- deletion_seconds,
4207
- warning_queued,
4208
- dead_letter,
4209
- partition,
4210
- table_name,
4211
- heartbeat_seconds
4212
- )
4213
- VALUES (
4214
- queue_name,
4215
- options->>'policy',
4216
- COALESCE((options->>'retryLimit')::int, 2),
4217
- COALESCE((options->>'retryDelay')::int, 0),
4218
- COALESCE((options->>'retryBackoff')::bool, false),
4219
- (options->>'retryDelayMax')::int,
4220
- COALESCE((options->>'expireInSeconds')::int, 900),
4221
- COALESCE((options->>'retentionSeconds')::int, 1209600),
4222
- COALESCE((options->>'deleteAfterSeconds')::int, 604800),
4223
- COALESCE((options->>'warningQueueSize')::int, 0),
4224
- options->>'deadLetter',
4225
- COALESCE((options->>'partition')::bool, false),
4226
- tablename,
4227
- (options->>'heartbeatSeconds')::int
4228
- )
4229
- ON CONFLICT DO NOTHING
4230
- RETURNING created_on
4231
- )
4232
- SELECT created_on into queue_created_on from q;
4233
-
4234
- IF queue_created_on IS NULL OR options->>'partition' IS DISTINCT FROM 'true' THEN
4235
- RETURN;
4236
- END IF;
4237
-
4238
- EXECUTE format('CREATE TABLE ${schema}.%I (LIKE ${schema}.job INCLUDING DEFAULTS)', tablename);
4239
-
4240
- EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD PRIMARY KEY (name, id)$cmd$, tablename);
4241
- EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT q_fkey FOREIGN KEY (name) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
4242
- EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT dlq_fkey FOREIGN KEY (dead_letter) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
4243
-
4244
- EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i5 ON ${schema}.job (name, start_after) INCLUDE (priority, created_on, id) WHERE state < 'active' AND NOT blocked$cmd$, tablename);
4245
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i4 ON ${schema}.job (name, singleton_on, COALESCE(singleton_key, '')) WHERE state <> 'cancelled' AND singleton_on IS NOT NULL$cmd$, tablename);
4246
- EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i7 ON ${schema}.job (name, group_id) WHERE state = 'active' AND group_id IS NOT NULL$cmd$, tablename);
4247
-
4248
- IF options->>'policy' = 'short' THEN
4249
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i1 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = 'created' AND policy = 'short'$cmd$, tablename);
4250
- ELSIF options->>'policy' = 'singleton' THEN
4251
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i2 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = 'active' AND policy = 'singleton'$cmd$, tablename);
4252
- ELSIF options->>'policy' = 'stately' THEN
4253
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i3 ON ${schema}.job (name, state, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'stately'$cmd$, tablename);
4254
- ELSIF options->>'policy' = 'exclusive' THEN
4255
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i6 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'exclusive'$cmd$, tablename);
4256
- ELSIF options->>'policy' = 'key_strict_fifo' THEN
4257
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i8 ON ${schema}.job (name, singleton_key) WHERE state IN ('active', 'retry', 'failed') AND policy = 'key_strict_fifo'$cmd$, tablename);
4258
- EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT job_key_strict_fifo_singleton_key_check CHECK (NOT (policy = 'key_strict_fifo' AND singleton_key IS NULL))$cmd$, tablename);
4259
- END IF;
4260
-
4261
- EXECUTE format('ALTER TABLE ${schema}.%I ADD CONSTRAINT cjc CHECK (name=%L)', tablename, queue_name);
4262
- EXECUTE format('ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.%I FOR VALUES IN (%L)', tablename, queue_name);
4263
- END;
4264
- $$
4265
- LANGUAGE plpgsql;
4266
- `
4473
+ createQueueFn[31](schema)
4267
4474
  ],
4268
4475
  uninstall: [
4269
4476
  `DROP INDEX IF EXISTS ${schema}.job_dep_parent_idx`,
4270
4477
  `DROP TABLE IF EXISTS ${schema}.job_dependency`,
4271
- `
4272
- CREATE OR REPLACE FUNCTION ${schema}.create_queue(queue_name text, options jsonb)
4273
- RETURNS VOID AS
4274
- $$
4275
- DECLARE
4276
- tablename varchar := CASE WHEN options->>'partition' = 'true'
4277
- THEN 'j' || encode(sha224(queue_name::bytea), 'hex')
4278
- ELSE 'job_common'
4279
- END;
4280
- queue_created_on timestamptz;
4281
- BEGIN
4282
-
4283
- WITH q as (
4284
- INSERT INTO ${schema}.queue (
4285
- name,
4286
- policy,
4287
- retry_limit,
4288
- retry_delay,
4289
- retry_backoff,
4290
- retry_delay_max,
4291
- expire_seconds,
4292
- retention_seconds,
4293
- deletion_seconds,
4294
- warning_queued,
4295
- dead_letter,
4296
- partition,
4297
- table_name,
4298
- heartbeat_seconds
4299
- )
4300
- VALUES (
4301
- queue_name,
4302
- options->>'policy',
4303
- COALESCE((options->>'retryLimit')::int, 2),
4304
- COALESCE((options->>'retryDelay')::int, 0),
4305
- COALESCE((options->>'retryBackoff')::bool, false),
4306
- (options->>'retryDelayMax')::int,
4307
- COALESCE((options->>'expireInSeconds')::int, 900),
4308
- COALESCE((options->>'retentionSeconds')::int, 1209600),
4309
- COALESCE((options->>'deleteAfterSeconds')::int, 604800),
4310
- COALESCE((options->>'warningQueueSize')::int, 0),
4311
- options->>'deadLetter',
4312
- COALESCE((options->>'partition')::bool, false),
4313
- tablename,
4314
- (options->>'heartbeatSeconds')::int
4315
- )
4316
- ON CONFLICT DO NOTHING
4317
- RETURNING created_on
4318
- )
4319
- SELECT created_on into queue_created_on from q;
4320
-
4321
- IF queue_created_on IS NULL OR options->>'partition' IS DISTINCT FROM 'true' THEN
4322
- RETURN;
4323
- END IF;
4324
-
4325
- EXECUTE format('CREATE TABLE ${schema}.%I (LIKE ${schema}.job INCLUDING DEFAULTS)', tablename);
4326
-
4327
- EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD PRIMARY KEY (name, id)$cmd$, tablename);
4328
- EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT q_fkey FOREIGN KEY (name) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
4329
- EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT dlq_fkey FOREIGN KEY (dead_letter) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
4330
-
4331
- EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i5 ON ${schema}.job (name, start_after) INCLUDE (priority, created_on, id) WHERE state < 'active'$cmd$, tablename);
4332
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i4 ON ${schema}.job (name, singleton_on, COALESCE(singleton_key, '')) WHERE state <> 'cancelled' AND singleton_on IS NOT NULL$cmd$, tablename);
4333
- EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i7 ON ${schema}.job (name, group_id) WHERE state = 'active' AND group_id IS NOT NULL$cmd$, tablename);
4334
-
4335
- IF options->>'policy' = 'short' THEN
4336
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i1 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = 'created' AND policy = 'short'$cmd$, tablename);
4337
- ELSIF options->>'policy' = 'singleton' THEN
4338
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i2 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = 'active' AND policy = 'singleton'$cmd$, tablename);
4339
- ELSIF options->>'policy' = 'stately' THEN
4340
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i3 ON ${schema}.job (name, state, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'stately'$cmd$, tablename);
4341
- ELSIF options->>'policy' = 'exclusive' THEN
4342
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i6 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'exclusive'$cmd$, tablename);
4343
- ELSIF options->>'policy' = 'key_strict_fifo' THEN
4344
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i8 ON ${schema}.job (name, singleton_key) WHERE state IN ('active', 'retry', 'failed') AND policy = 'key_strict_fifo'$cmd$, tablename);
4345
- EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT job_key_strict_fifo_singleton_key_check CHECK (NOT (policy = 'key_strict_fifo' AND singleton_key IS NULL))$cmd$, tablename);
4346
- END IF;
4347
-
4348
- EXECUTE format('ALTER TABLE ${schema}.%I ADD CONSTRAINT cjc CHECK (name=%L)', tablename, queue_name);
4349
- EXECUTE format('ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.%I FOR VALUES IN (%L)', tablename, queue_name);
4350
- END;
4351
- $$
4352
- LANGUAGE plpgsql;
4353
- `,
4478
+ createQueueFn[30](schema),
4354
4479
  `SELECT ${schema}.job_table_run($cmd$DROP INDEX IF EXISTS ${schema}.job_i5$cmd$)`,
4355
4480
  `SELECT ${schema}.job_table_run($cmd$CREATE INDEX job_i5 ON ${schema}.job (name, start_after) INCLUDE (priority, created_on, id) WHERE state < 'active'$cmd$)`,
4356
4481
  `ALTER TABLE ${schema}.job DROP COLUMN pending_dependencies`,
4357
4482
  `ALTER TABLE ${schema}.job DROP COLUMN blocking`,
4358
4483
  `ALTER TABLE ${schema}.job DROP COLUMN blocked`
4359
4484
  ]
4485
+ },
4486
+ {
4487
+ release: "12.21.0",
4488
+ version: 32,
4489
+ previous: 31,
4490
+ install: [
4491
+ `ALTER TABLE ${schema}.queue ADD COLUMN notify boolean NOT NULL DEFAULT false`,
4492
+ createQueueFn[32](schema),
4493
+ `ALTER TABLE ${schema}.queue ADD COLUMN failed_count int NOT NULL DEFAULT 0`,
4494
+ `ALTER TABLE ${schema}.queue ADD COLUMN ready_count int NOT NULL DEFAULT 0`
4495
+ ],
4496
+ uninstall: [
4497
+ `ALTER TABLE ${schema}.queue DROP COLUMN ready_count`,
4498
+ `ALTER TABLE ${schema}.queue DROP COLUMN failed_count`,
4499
+ createQueueFn[31](schema),
4500
+ `ALTER TABLE ${schema}.queue DROP COLUMN notify`
4501
+ ]
4502
+ },
4503
+ {
4504
+ release: "12.22.0",
4505
+ version: 33,
4506
+ previous: 32,
4507
+ install: [
4508
+ `ALTER TABLE ${schema}.version ADD COLUMN IF NOT EXISTS flow_on timestamp with time zone`,
4509
+ `SELECT ${schema}.job_table_run($cmd$CREATE INDEX job_i9 ON ${schema}.job (name, id) WHERE blocking AND state = 'completed'$cmd$)`,
4510
+ `SELECT ${schema}.job_table_run($cmd$DROP INDEX IF EXISTS ${schema}.job_i5$cmd$)`,
4511
+ `SELECT ${schema}.job_table_run($cmd$CREATE INDEX job_i5 ON ${schema}.job (name, start_after) WHERE state < 'active' AND NOT blocked$cmd$)`,
4512
+ createQueueFn[33](schema)
4513
+ ],
4514
+ uninstall: [
4515
+ createQueueFn[32](schema),
4516
+ `SELECT ${schema}.job_table_run($cmd$DROP INDEX IF EXISTS ${schema}.job_i5$cmd$)`,
4517
+ `SELECT ${schema}.job_table_run($cmd$CREATE INDEX job_i5 ON ${schema}.job (name, start_after) INCLUDE (priority, created_on, id) WHERE state < 'active' AND NOT blocked$cmd$)`,
4518
+ `SELECT ${schema}.job_table_run($cmd$DROP INDEX IF EXISTS ${schema}.job_i9$cmd$)`,
4519
+ `ALTER TABLE ${schema}.version DROP COLUMN flow_on`
4520
+ ]
4360
4521
  }
4361
4522
  ];
4362
4523
  }
@@ -4364,7 +4525,7 @@ function getAll(schema) {
4364
4525
  //#region ../../src/contractor.ts
4365
4526
  var schemaVersion = {
4366
4527
  name: "pg-boss",
4367
- version: "12.20.0",
4528
+ version: "12.22.0",
4368
4529
  description: "Queueing jobs in Postgres from Node.js like a boss",
4369
4530
  type: "module",
4370
4531
  main: "./dist/index.js",
@@ -4372,18 +4533,18 @@ var schemaVersion = {
4372
4533
  bin: { "pg-boss": "./dist/cli.js" },
4373
4534
  engines: { "node": ">=22.12.0" },
4374
4535
  dependencies: {
4375
- "cron-parser": "^5.5.0",
4376
- "pg": "^8.21.0",
4536
+ "cron-parser": "^5.6.1",
4537
+ "pg": "^8.22.0",
4377
4538
  "serialize-error": "^13.0.1"
4378
4539
  },
4379
4540
  devDependencies: {
4380
- "@electric-sql/pglite": "^0.4.1",
4541
+ "@electric-sql/pglite": "^0.5.3",
4381
4542
  "@prisma/adapter-pg": "^7.8.0",
4382
4543
  "@prisma/client": "^7.8.0",
4383
4544
  "@tsconfig/node-ts": "^23.6.4",
4384
4545
  "@tsconfig/node22": "^22.0.5",
4385
- "@types/luxon": "^3.7.1",
4386
- "@types/node": "^22.19.20",
4546
+ "@types/luxon": "^3.7.2",
4547
+ "@types/node": "^22.20.0",
4387
4548
  "@types/pg": "^8.20.0",
4388
4549
  "@vitest/coverage-v8": "^4.1.2",
4389
4550
  "cli-testlab": "^6.0.1",
@@ -4403,23 +4564,24 @@ var schemaVersion = {
4403
4564
  "build": "npm run clean && tsc --project tsconfig.build.json",
4404
4565
  "clean": "node -e \"fs.rmSync('dist',{recursive:true,force:true})\"",
4405
4566
  "prepublishOnly": "npm install && npm test && npm run build",
4406
- "pretest": "prisma generate --schema=test/prisma/schema.prisma",
4567
+ "pretest": "prisma generate --schema=test/prisma/schema.prisma && npm run tsc",
4407
4568
  "test": "eslint . && vitest run",
4408
4569
  "test:distributed": "cross-env DISTRIBUTED=true npm test",
4570
+ "test:ci": "npm run cover && cross-env DISTRIBUTED=true npm run cover && npm run test:pglite",
4409
4571
  "test:cockroachdb": "cross-env DB_TYPE=cockroachdb COCKROACH_HOST=localhost npm test -- test/distributedDatabaseTest.ts",
4410
4572
  "test:cockroachdb:full": "cross-env DB_TYPE=cockroachdb COCKROACH_HOST=localhost npm test -- --no-file-parallelism",
4411
4573
  "test:yugabytedb:full": "cross-env DB_TYPE=yugabytedb YUGABYTE_HOST=localhost npm test -- --no-file-parallelism",
4412
4574
  "test:citus:full": "cross-env DB_TYPE=citus CITUS_HOST=localhost npm test",
4413
4575
  "test:pglite": "cross-env DB_TYPE=pglite npm test -- --no-file-parallelism",
4414
4576
  "lint:fix": "eslint . --fix",
4415
- "precover": "prisma generate --schema=test/prisma/schema.prisma",
4416
- "cover": "vitest run --coverage",
4577
+ "cover": "npm test -- --coverage",
4417
4578
  "tsc": "tsc --noEmit",
4579
+ "cli": "node ./dist/cli.js",
4418
4580
  "readme": "node ./examples/readme.js",
4419
- "db:migrate": "node --import=tsx -e 'console.log(require(\"./src\").getMigrationPlans())'",
4420
- "db:construct": "node --import=tsx -e 'console.log(require(\"./src\").getConstructionPlans())'"
4581
+ "docs": "npm run docs:dev --prefix docs",
4582
+ "docs:readme": "node ./scripts/sync-readme.js"
4421
4583
  },
4422
- pgboss: { "schema": 31 },
4584
+ pgboss: { "schema": 33 },
4423
4585
  repository: {
4424
4586
  "type": "git",
4425
4587
  "url": "git+https://github.com/timgit/pg-boss.git"
@@ -4445,8 +4607,11 @@ var Contractor = class {
4445
4607
  static constructionPlans(schema = DEFAULT_SCHEMA, options = { createSchema: true }) {
4446
4608
  return create(schema, schemaVersion, options);
4447
4609
  }
4448
- static migrationPlans(schema = DEFAULT_SCHEMA, version = schemaVersion - 1) {
4449
- return migrate(schema, version);
4610
+ static migrationPlans(schema = DEFAULT_SCHEMA, version = schemaVersion - 1, options = {}) {
4611
+ return migrate(schema, version, void 0, void 0, {
4612
+ inlineAsync: true,
4613
+ partitionTables: options.partitionTables
4614
+ });
4450
4615
  }
4451
4616
  static rollbackPlans(schema = DEFAULT_SCHEMA, version = schemaVersion) {
4452
4617
  return rollback(schema, version);
@@ -4741,7 +4906,7 @@ var Worker = class {
4741
4906
  fetch;
4742
4907
  onFetch;
4743
4908
  onError;
4744
- interval;
4909
+ resolveInterval;
4745
4910
  jobs = [];
4746
4911
  createdOn = Date.now();
4747
4912
  state = WORKER_STATES.created;
@@ -4757,7 +4922,7 @@ var Worker = class {
4757
4922
  loopDelayPromise = null;
4758
4923
  beenNotified = false;
4759
4924
  runPromise = null;
4760
- constructor({ id, workId, name, options, interval, fetch, onFetch, onError }) {
4925
+ constructor({ id, workId, name, options, resolveInterval, fetch, onFetch, onError }) {
4761
4926
  this.id = id;
4762
4927
  this.workId = workId;
4763
4928
  this.name = name;
@@ -4765,7 +4930,7 @@ var Worker = class {
4765
4930
  this.fetch = fetch;
4766
4931
  this.onFetch = onFetch;
4767
4932
  this.onError = onError;
4768
- this.interval = interval;
4933
+ this.resolveInterval = resolveInterval;
4769
4934
  }
4770
4935
  start() {
4771
4936
  this.runPromise = this.run();
@@ -4774,11 +4939,13 @@ var Worker = class {
4774
4939
  this.state = WORKER_STATES.active;
4775
4940
  while (!this.stopping) {
4776
4941
  const started = Date.now();
4942
+ let fetchedCount = 0;
4777
4943
  try {
4778
4944
  this.beenNotified = false;
4779
4945
  const jobs = await this.fetch();
4780
4946
  this.lastFetchedOn = Date.now();
4781
4947
  if (jobs) {
4948
+ fetchedCount = jobs.length;
4782
4949
  this.jobs = jobs;
4783
4950
  this.lastJobStartedOn = this.lastFetchedOn;
4784
4951
  await this.onFetch(jobs);
@@ -4793,8 +4960,9 @@ var Worker = class {
4793
4960
  }
4794
4961
  const duration = Date.now() - started;
4795
4962
  this.lastJobDuration = duration;
4796
- if (!this.stopping && !this.beenNotified && this.interval - duration > 100) {
4797
- this.loopDelayPromise = delay(this.interval - duration);
4963
+ const interval = this.resolveInterval(fetchedCount);
4964
+ if (!this.stopping && !this.beenNotified && interval - duration > 100) {
4965
+ this.loopDelayPromise = delay(interval - duration);
4798
4966
  await this.loopDelayPromise;
4799
4967
  this.loopDelayPromise = null;
4800
4968
  }
@@ -4927,7 +5095,7 @@ var NUMERIC_QUEUE_FIELDS = [
4927
5095
  "activeCount",
4928
5096
  "totalCount"
4929
5097
  ];
4930
- var events$3 = {
5098
+ var events$5 = {
4931
5099
  error: "error",
4932
5100
  wip: "wip"
4933
5101
  };
@@ -4936,7 +5104,7 @@ function rethrowWriteError(err) {
4936
5104
  throw err;
4937
5105
  }
4938
5106
  var Manager = class extends EventEmitter {
4939
- events = events$3;
5107
+ events = events$5;
4940
5108
  db;
4941
5109
  config;
4942
5110
  wipTs;
@@ -4945,6 +5113,7 @@ var Manager = class extends EventEmitter {
4945
5113
  queueCacheInterval;
4946
5114
  wipInterval;
4947
5115
  timekeeper;
5116
+ notifier;
4948
5117
  queues;
4949
5118
  pendingOffWorkCleanups;
4950
5119
  #spies;
@@ -5013,19 +5182,134 @@ var Manager = class extends EventEmitter {
5013
5182
  const spy = this.config.__test__enableSpies ? this.#spies.get(name) : void 0;
5014
5183
  if (spy) for (const job of jobs) spy.addJob(job.id, name, job.data, "active");
5015
5184
  }
5016
- #trackJobsCompleted(name, jobs, result) {
5185
+ async #trackJobsCompleted(name, jobs, result, affected) {
5017
5186
  const spy = this.config.__test__enableSpies ? this.#spies.get(name) : void 0;
5018
- if (spy) {
5187
+ if (!spy) return;
5188
+ if (affected === jobs.length) {
5019
5189
  const output = jobs.length === 1 ? result : void 0;
5020
5190
  for (const job of jobs) spy.addJob(job.id, name, job.data, "completed", output);
5191
+ return;
5192
+ }
5193
+ for (const job of jobs) {
5194
+ const persisted = await this.getJobById(name, job.id);
5195
+ const state = persisted?.state;
5196
+ if (state === "completed" || state === "failed" || state === "active" || state === "created") spy.addJob(job.id, name, job.data, state, persisted?.output);
5197
+ else if (!persisted) spy.addJob(job.id, name, job.data, "completed", void 0);
5021
5198
  }
5022
5199
  }
5023
- #trackJobsFailed(name, jobs, err) {
5200
+ async #trackJobsFailed(name, jobs, err) {
5024
5201
  const spy = this.config.__test__enableSpies ? this.#spies.get(name) : void 0;
5025
- if (spy) for (const job of jobs) spy.addJob(job.id, name, job.data, "failed", {
5026
- message: err?.message,
5027
- stack: err?.stack
5028
- });
5202
+ if (!spy) return;
5203
+ for (const job of jobs) {
5204
+ const persisted = await this.getJobById(name, job.id);
5205
+ if (persisted?.state === "failed") spy.addJob(job.id, name, job.data, "failed", persisted.output ?? {
5206
+ message: err?.message,
5207
+ stack: err?.stack
5208
+ });
5209
+ }
5210
+ }
5211
+ #trackJobsSettled(name, completed, failed) {
5212
+ const spy = this.config.__test__enableSpies ? this.#spies.get(name) : void 0;
5213
+ if (!spy) return;
5214
+ for (const { job, output } of completed) spy.addJob(job.id, name, job.data, "completed", output);
5215
+ for (const { job, output } of failed) spy.addJob(job.id, name, job.data, "failed", serializeError(output));
5216
+ }
5217
+ async #settlePerJob(name, jobs, result) {
5218
+ if (!Array.isArray(result)) {
5219
+ const err = /* @__PURE__ */ new Error("perJobResults handler must resolve with an array of job results");
5220
+ await this.fail(name, jobs.map((job) => job.id), err);
5221
+ this.#trackJobsFailed(name, jobs, err);
5222
+ return;
5223
+ }
5224
+ const batch = new Map(jobs.map((job) => [job.id, job]));
5225
+ const disposition = /* @__PURE__ */ new Map();
5226
+ for (const item of result) if (item && batch.has(item.id) && (item.status === "completed" || item.status === "failed" || item.status === "deadletter")) disposition.set(item.id, item);
5227
+ const completed = [];
5228
+ const failed = [];
5229
+ const deadLettered = [];
5230
+ for (const job of jobs) {
5231
+ const item = disposition.get(job.id);
5232
+ if (item?.status === "completed") completed.push({
5233
+ job,
5234
+ output: item.output
5235
+ });
5236
+ else if (item?.status === "failed") failed.push({
5237
+ job,
5238
+ output: item.output
5239
+ });
5240
+ else if (item?.status === "deadletter") deadLettered.push({
5241
+ job,
5242
+ output: item.output
5243
+ });
5244
+ else failed.push({
5245
+ job,
5246
+ output: /* @__PURE__ */ new Error("no disposition returned by handler")
5247
+ });
5248
+ }
5249
+ if (completed.length > 0) await this.#completeWithOutputs(name, completed.map((c) => ({
5250
+ id: c.job.id,
5251
+ output: c.output
5252
+ })));
5253
+ if (failed.length > 0) await this.#failWithOutputs(name, failed.map((f) => ({
5254
+ id: f.job.id,
5255
+ output: f.output
5256
+ })));
5257
+ if (deadLettered.length > 0) await this.#failWithOutputs(name, deadLettered.map((d) => ({
5258
+ id: d.job.id,
5259
+ output: d.output
5260
+ })), true);
5261
+ this.#trackJobsSettled(name, completed, [...failed, ...deadLettered]);
5262
+ }
5263
+ async #completeWithOutputs(name, items) {
5264
+ const { table } = await this.getQueueCache(name);
5265
+ const payload = items.map((item) => ({
5266
+ id: item.id,
5267
+ output: this.mapCompletionDataArg(item.output)
5268
+ }));
5269
+ const ids = items.map((item) => item.id);
5270
+ if (this.config.noMultiMutationCte) {
5271
+ const sql = completeJobsWithOutputsDistributed(this.config.schema, table);
5272
+ const { rows } = await this.db.executeSql(sql, [name, JSON.stringify(payload)]);
5273
+ return {
5274
+ jobs: ids,
5275
+ requested: ids.length,
5276
+ affected: rows.length
5277
+ };
5278
+ }
5279
+ const sql = completeJobsWithOutputs(this.config.schema, table);
5280
+ const result = await this.db.executeSql(sql, [name, JSON.stringify(payload)]);
5281
+ return this.mapCommandResponse(ids, result);
5282
+ }
5283
+ async #failWithOutputs(name, items, forceTerminal = false) {
5284
+ const { table } = await this.getQueueCache(name);
5285
+ const ids = items.map((item) => item.id);
5286
+ if (this.config.noMultiMutationCte) {
5287
+ const outputById = new Map(items.map((item) => [item.id, this.mapCompletionDataArg(item.output)]));
5288
+ return this.withDistributedTransaction(this.db, async (tx) => {
5289
+ const selectQuery = selectJobsToFailById(this.config.schema, table);
5290
+ const { rows: jobs } = await tx.executeSql(selectQuery.text, [name, ids]);
5291
+ if (jobs.length === 0) return {
5292
+ jobs: ids,
5293
+ requested: ids.length,
5294
+ affected: 0
5295
+ };
5296
+ const deleteQuery = deleteJobsToFail(this.config.schema, table);
5297
+ await tx.executeSql(deleteQuery.text, [name, ids]);
5298
+ const count = await this.reinsertFailedJobs(tx, table, jobs, null, outputById, forceTerminal);
5299
+ return {
5300
+ jobs: ids,
5301
+ requested: ids.length,
5302
+ affected: count
5303
+ };
5304
+ });
5305
+ }
5306
+ const payload = items.map((item) => ({
5307
+ id: item.id,
5308
+ output: this.mapCompletionDataArg(item.output)
5309
+ }));
5310
+ const sql = forceTerminal ? deadLetterJobsByIdWithOutputs(this.config.schema, table) : failJobsByIdWithOutputs(this.config.schema, table);
5311
+ const result = await this.db.executeSql(sql, [name, JSON.stringify(payload)]);
5312
+ return this.mapCommandResponse(ids, result);
5029
5313
  }
5030
5314
  #storeLocalGroupConfig(name, localGroupConcurrency) {
5031
5315
  const config = typeof localGroupConcurrency === "number" ? { default: localGroupConcurrency } : localGroupConcurrency;
@@ -5063,7 +5347,7 @@ var Manager = class extends EventEmitter {
5063
5347
  #trackLocalGroupEnd(name, groupedJobs) {
5064
5348
  for (const job of groupedJobs) if (job.groupId) this.#decrementLocalGroupCount(name, job.groupId);
5065
5349
  }
5066
- async #processJobs(name, jobs, callback, worker, heartbeatRefreshSeconds) {
5350
+ async #processJobs(name, jobs, callback, worker, heartbeatRefreshSeconds, perJobResults = false) {
5067
5351
  const jobIds = jobs.map((job) => job.id);
5068
5352
  const maxExpiration = jobs.reduce((acc, i) => Math.max(acc, i.expireInSeconds), 0);
5069
5353
  const heartbeatSeconds = jobs.reduce((acc, j) => Math.max(acc, j.heartbeatSeconds || 0), 0);
@@ -5079,21 +5363,34 @@ var Manager = class extends EventEmitter {
5079
5363
  try {
5080
5364
  await this.touch(name, jobIds);
5081
5365
  } catch (err) {
5082
- this.emit(events$3.error, err);
5366
+ this.emit(events$5.error, err);
5083
5367
  }
5084
5368
  }, intervalMs);
5085
5369
  }
5370
+ let completedResult;
5371
+ let completedAffected = 0;
5372
+ let failedError;
5373
+ let didFail = false;
5086
5374
  try {
5087
5375
  const result = await resolveWithinSeconds(callback(jobs), maxExpiration, `handler execution exceeded ${maxExpiration}s`, ac);
5088
- await this.complete(name, jobIds, jobIds.length === 1 ? result : void 0);
5089
- this.#trackJobsCompleted(name, jobs, result);
5376
+ if (perJobResults) await this.#settlePerJob(name, jobs, result);
5377
+ else {
5378
+ const completion = await this.complete(name, jobIds, jobIds.length === 1 ? result : void 0);
5379
+ completedResult = result;
5380
+ completedAffected = completion.affected;
5381
+ }
5090
5382
  } catch (err) {
5091
5383
  await this.fail(name, jobIds, err);
5092
- this.#trackJobsFailed(name, jobs, err);
5384
+ failedError = err;
5385
+ didFail = true;
5093
5386
  } finally {
5094
5387
  if (heartbeatTimer) clearInterval(heartbeatTimer);
5095
5388
  if (worker) worker.abortController = null;
5096
5389
  }
5390
+ if (this.config.__test__enableSpies && this.#spies.has(name)) {
5391
+ if (didFail) await this.#trackJobsFailed(name, jobs, failedError);
5392
+ else if (!perJobResults) await this.#trackJobsCompleted(name, jobs, completedResult, completedAffected);
5393
+ }
5097
5394
  }
5098
5395
  async start() {
5099
5396
  this.stopped = false;
@@ -5103,7 +5400,7 @@ var Manager = class extends EventEmitter {
5103
5400
  if (now - this.wipTs < 2e3) return;
5104
5401
  const wip = this.getWipData();
5105
5402
  if (wip.some((w) => w.count > 0)) {
5106
- this.emit(events$3.wip, wip);
5403
+ this.emit(events$5.wip, wip);
5107
5404
  this.wipTs = now;
5108
5405
  }
5109
5406
  }, 2e3);
@@ -5118,7 +5415,7 @@ var Manager = class extends EventEmitter {
5118
5415
  return acc;
5119
5416
  }, {});
5120
5417
  } catch (error) {
5121
- emit && this.emit(events$3.error, {
5418
+ emit && this.emit(events$5.error, {
5122
5419
  ...error,
5123
5420
  message: error.message,
5124
5421
  stack: error.stack
@@ -5153,9 +5450,15 @@ var Manager = class extends EventEmitter {
5153
5450
  async work(name, ...args) {
5154
5451
  const { options, callback } = checkWorkArgs(name, args);
5155
5452
  if (this.stopped) throw new Error("Workers are disabled. pg-boss is stopped");
5156
- const { pollingInterval: interval, batchSize = 1, includeMetadata = false, priority = true, localConcurrency = 1, localGroupConcurrency, groupConcurrency, orderByCreatedOn = true, heartbeatRefreshSeconds, minPriority, maxPriority } = options;
5453
+ const { pollingInterval: interval, notifyPollingInterval: notifyInterval, burstWhenReadyExceeds, burstWhenBatchFull = false, batchSize = 1, includeMetadata = false, priority = true, localConcurrency = 1, localGroupConcurrency, groupConcurrency, orderByCreatedOn = true, heartbeatRefreshSeconds, minPriority, maxPriority, perJobResults = false } = options;
5157
5454
  if (localGroupConcurrency != null) this.#storeLocalGroupConfig(name, localGroupConcurrency);
5158
5455
  const firstWorkerId = randomUUID({ disableEntropyCache: true });
5456
+ const isNotifyActive = () => !!(this.notifier?.available && this.queues?.[name]?.notify);
5457
+ const getReadyCount = () => this.queues?.[name]?.readyCount ?? 0;
5458
+ const resolveInterval = (lastFetchCount) => {
5459
+ if (lastFetchCount >= batchSize && (burstWhenReadyExceeds !== void 0 && getReadyCount() > burstWhenReadyExceeds || burstWhenBatchFull && batchSize > 1)) return 0;
5460
+ return isNotifyActive() ? notifyInterval : interval;
5461
+ };
5159
5462
  const createWorker = (workerId, workId) => {
5160
5463
  const fetch = () => {
5161
5464
  const ignoreGroups = localGroupConcurrency != null ? this.#getGroupsAtLocalCapacity(name) : void 0;
@@ -5176,7 +5479,7 @@ var Manager = class extends EventEmitter {
5176
5479
  this.emitWip(name);
5177
5480
  this.#trackJobsActive(name, jobs);
5178
5481
  const worker = this.workers.get(workerId);
5179
- if (localGroupConcurrency == null) await this.#processJobs(name, jobs, callback, worker, heartbeatRefreshSeconds);
5482
+ if (localGroupConcurrency == null) await this.#processJobs(name, jobs, callback, worker, heartbeatRefreshSeconds, perJobResults);
5180
5483
  else {
5181
5484
  const { allowed, excess, groupedJobs } = this.#trackLocalGroupStart(name, jobs);
5182
5485
  if (excess.length > 0) {
@@ -5184,7 +5487,7 @@ var Manager = class extends EventEmitter {
5184
5487
  await this.restore(name, excessIds);
5185
5488
  }
5186
5489
  if (allowed.length > 0) try {
5187
- await this.#processJobs(name, allowed, callback, worker, heartbeatRefreshSeconds);
5490
+ await this.#processJobs(name, allowed, callback, worker, heartbeatRefreshSeconds, perJobResults);
5188
5491
  } finally {
5189
5492
  this.#trackLocalGroupEnd(name, groupedJobs);
5190
5493
  }
@@ -5192,7 +5495,7 @@ var Manager = class extends EventEmitter {
5192
5495
  this.emitWip(name);
5193
5496
  };
5194
5497
  const onError = (error) => {
5195
- this.emit(events$3.error, {
5498
+ this.emit(events$5.error, {
5196
5499
  ...error,
5197
5500
  message: error.message,
5198
5501
  stack: error.stack,
@@ -5205,7 +5508,7 @@ var Manager = class extends EventEmitter {
5205
5508
  workId,
5206
5509
  name,
5207
5510
  options,
5208
- interval,
5511
+ resolveInterval,
5209
5512
  fetch,
5210
5513
  onFetch,
5211
5514
  onError
@@ -5231,7 +5534,7 @@ var Manager = class extends EventEmitter {
5231
5534
  if (!INTERNAL_QUEUES[name]) {
5232
5535
  const now = Date.now();
5233
5536
  if (now - this.wipTs > 2e3) {
5234
- this.emit(events$3.wip, this.getWipData());
5537
+ this.emit(events$5.wip, this.getWipData());
5235
5538
  this.wipTs = now;
5236
5539
  }
5237
5540
  }
@@ -5267,6 +5570,15 @@ var Manager = class extends EventEmitter {
5267
5570
  notifyWorker(workerId) {
5268
5571
  this.workers.get(workerId)?.notify();
5269
5572
  }
5573
+ #notifyEnabled(queueNotify) {
5574
+ return !!queueNotify && !this.config.noListenNotify;
5575
+ }
5576
+ notifyQueue(name) {
5577
+ for (const worker of this.workers.values()) if (worker.name === name) worker.notify();
5578
+ }
5579
+ forceFetchLnWorkers() {
5580
+ for (const worker of this.workers.values()) if (this.queues?.[worker.name]?.notify) worker.notify();
5581
+ }
5270
5582
  async subscribe(event, name) {
5271
5583
  assert(event, "Missing required argument");
5272
5584
  assert(name, "Missing required argument");
@@ -5349,12 +5661,13 @@ var Manager = class extends EventEmitter {
5349
5661
  deadLetter
5350
5662
  };
5351
5663
  const db = wrapper || this.db;
5352
- const { table, policy } = await this.getQueueCache(name);
5664
+ const { table, policy, notify } = await this.getQueueCache(name);
5353
5665
  if (policy === QUEUE_POLICIES.key_strict_fifo && !singletonKey) throw new Error(`${QUEUE_POLICIES.key_strict_fifo} queues require a singletonKey`);
5354
5666
  const sql = insertJobs(this.config.schema, {
5355
5667
  table,
5356
5668
  name,
5357
- returnId: true
5669
+ returnId: true,
5670
+ notify: this.#notifyEnabled(notify)
5358
5671
  });
5359
5672
  const { rows: try1 } = await db.executeSql(sql, [JSON.stringify([job])]);
5360
5673
  if (try1.length === 1) {
@@ -5382,7 +5695,7 @@ var Manager = class extends EventEmitter {
5382
5695
  }
5383
5696
  async insert(name, jobs, options = {}) {
5384
5697
  assert(Array.isArray(jobs), "jobs argument should be an array");
5385
- const { table, policy } = await this.getQueueCache(name);
5698
+ const { table, policy, notify } = await this.getQueueCache(name);
5386
5699
  if (policy === QUEUE_POLICIES.key_strict_fifo) {
5387
5700
  for (const job of jobs) if (!job.singletonKey) throw new Error(`${QUEUE_POLICIES.key_strict_fifo} queues require a singletonKey`);
5388
5701
  }
@@ -5396,7 +5709,8 @@ var Manager = class extends EventEmitter {
5396
5709
  const sql = insertJobs(this.config.schema, {
5397
5710
  table,
5398
5711
  name,
5399
- returnId
5712
+ returnId,
5713
+ notify: this.#notifyEnabled(notify)
5400
5714
  });
5401
5715
  const { rows } = await db.executeSql(sql, [JSON.stringify(insertPayload)]);
5402
5716
  if (rows.length) {
@@ -5443,7 +5757,7 @@ var Manager = class extends EventEmitter {
5443
5757
  }
5444
5758
  const statements = [];
5445
5759
  for (const [queueName, queueJobs] of byQueue) {
5446
- const { table } = await this.getQueueCache(queueName);
5760
+ const { table, notify } = await this.getQueueCache(queueName);
5447
5761
  const insertPayload = queueJobs.map((j) => {
5448
5762
  const dependencyCount = dependencyCountByRef.get(j.ref) ?? 0;
5449
5763
  return {
@@ -5470,19 +5784,13 @@ var Manager = class extends EventEmitter {
5470
5784
  pendingDependencies: dependencyCount || void 0
5471
5785
  };
5472
5786
  });
5473
- const insertSql = insertJobs(this.config.schema, {
5787
+ statements.push(insertFlowJobs(this.config.schema, {
5474
5788
  table,
5475
- name: queueName,
5476
- returnId: true
5477
- }).replace("$1", () => serializeJsonParam(insertPayload));
5478
- statements.push(`
5479
- WITH ins AS (
5480
- ${insertSql}
5481
- )
5482
- SELECT 1 / (CASE WHEN (SELECT count(*) FROM ins) = ${insertPayload.length} THEN 1 ELSE 0 END)
5483
- `);
5789
+ name: queueName
5790
+ }, insertPayload));
5791
+ if (this.#notifyEnabled(notify)) statements.push(notifyQueue(this.config.schema, queueName));
5484
5792
  }
5485
- if (depRows.length > 0) statements.push(insertDependencies(this.config.schema).replace("$1", () => serializeJsonParam(depRows)));
5793
+ if (depRows.length > 0) statements.push(insertDependencies(this.config.schema, depRows));
5486
5794
  const db = options.db ?? this.db;
5487
5795
  const sql = options.db ? statements.join(";\n") : transaction(statements);
5488
5796
  try {
@@ -5561,24 +5869,17 @@ var Manager = class extends EventEmitter {
5561
5869
  return fn(db);
5562
5870
  }
5563
5871
  async completeDistributed(name, ids, outputData, table, db, includeQueued) {
5564
- return this.withDistributedTransaction(db, async (tx) => {
5565
- const completeSql = completeJobsDistributed(this.config.schema, table, includeQueued);
5566
- const { rows } = await tx.executeSql(completeSql, [
5567
- name,
5568
- ids,
5569
- outputData
5570
- ]);
5571
- const blockingIds = rows.filter((row) => row.blocking).map((row) => row.id);
5572
- if (blockingIds.length > 0) {
5573
- const decrementSql = decrementDependents(this.config.schema);
5574
- await tx.executeSql(decrementSql, [name, blockingIds]);
5575
- }
5576
- return {
5577
- jobs: ids,
5578
- requested: ids.length,
5579
- affected: rows.length
5580
- };
5581
- });
5872
+ const sql = completeJobsDistributed(this.config.schema, table, includeQueued);
5873
+ const { rows } = await db.executeSql(sql, [
5874
+ name,
5875
+ ids,
5876
+ outputData
5877
+ ]);
5878
+ return {
5879
+ jobs: ids,
5880
+ requested: ids.length,
5881
+ affected: rows.length
5882
+ };
5582
5883
  }
5583
5884
  async fail(name, id, data, options = {}) {
5584
5885
  assertQueueName(name);
@@ -5622,6 +5923,26 @@ var Manager = class extends EventEmitter {
5622
5923
  const select = selectJobsToFailByHeartbeat(this.config.schema, table, queues);
5623
5924
  return this.expireJobsDistributed(table, select, { value: { message: "job heartbeat timeout" } });
5624
5925
  }
5926
+ async resolveFlowJobsDistributed(table, names) {
5927
+ const select = selectBlockingParents(this.config.schema, table, names, this.config.noSkipLocked);
5928
+ return this.withDistributedTransaction(this.db, async (tx) => {
5929
+ const { rows } = await tx.executeSql(select.text, select.values);
5930
+ if (rows.length === 0) return 0;
5931
+ const idsByName = /* @__PURE__ */ new Map();
5932
+ for (const row of rows) {
5933
+ const list = idsByName.get(row.name) || [];
5934
+ list.push(row.id);
5935
+ idsByName.set(row.name, list);
5936
+ }
5937
+ const decrementSql = decrementDependents(this.config.schema);
5938
+ const clearSql = clearBlocking(this.config.schema);
5939
+ for (const [name, ids] of idsByName) {
5940
+ await tx.executeSql(decrementSql, [name, ids]);
5941
+ await tx.executeSql(clearSql, [name, ids]);
5942
+ }
5943
+ return rows.length;
5944
+ });
5945
+ }
5625
5946
  async expireJobsDistributed(table, select, outputData) {
5626
5947
  return this.withDistributedTransaction(this.db, async (tx) => {
5627
5948
  const { rows: jobs } = await tx.executeSql(select.text, []);
@@ -5632,16 +5953,17 @@ var Manager = class extends EventEmitter {
5632
5953
  return this.reinsertFailedJobs(tx, table, jobs, outputData);
5633
5954
  });
5634
5955
  }
5635
- async reinsertFailedJobs(tx, table, jobs, outputData) {
5956
+ async reinsertFailedJobs(tx, table, jobs, outputData, outputById, forceTerminal = false) {
5636
5957
  const insertSql = insertRetryJob(this.config.schema, table);
5637
5958
  const dlqSql = insertDeadLetterJob(this.config.schema);
5638
5959
  let count = 0;
5639
5960
  for (const job of jobs) {
5961
+ const jobOutput = outputById ? outputById.get(job.id) ?? null : outputData;
5640
5962
  const retryCount = Number(job.retry_count);
5641
5963
  const retryLimit = Number(job.retry_limit);
5642
5964
  const retryDelay = Number(job.retry_delay);
5643
5965
  const retryDelayMax = job.retry_delay_max != null ? Number(job.retry_delay_max) : null;
5644
- const canRetry = retryCount < retryLimit;
5966
+ const canRetry = !forceTerminal && retryCount < retryLimit;
5645
5967
  let retried = false;
5646
5968
  if (canRetry) {
5647
5969
  let startAfter = job.start_after;
@@ -5675,7 +5997,7 @@ var Manager = class extends EventEmitter {
5675
5997
  null,
5676
5998
  job.keep_until,
5677
5999
  job.policy,
5678
- outputData,
6000
+ jobOutput,
5679
6001
  job.dead_letter,
5680
6002
  null,
5681
6003
  job.heartbeat_seconds,
@@ -5709,7 +6031,7 @@ var Manager = class extends EventEmitter {
5709
6031
  /* @__PURE__ */ new Date(),
5710
6032
  job.keep_until,
5711
6033
  job.policy,
5712
- outputData,
6034
+ jobOutput,
5713
6035
  job.dead_letter,
5714
6036
  null,
5715
6037
  job.heartbeat_seconds,
@@ -5720,7 +6042,7 @@ var Manager = class extends EventEmitter {
5720
6042
  if (job.dead_letter) await tx.executeSql(dlqSql, [
5721
6043
  job.dead_letter,
5722
6044
  job.data,
5723
- outputData
6045
+ jobOutput
5724
6046
  ]);
5725
6047
  }
5726
6048
  count++;
@@ -5880,7 +6202,9 @@ var Manager = class extends EventEmitter {
5880
6202
  return Object.assign(queue, stats || {
5881
6203
  deferredCount: 0,
5882
6204
  queuedCount: 0,
6205
+ readyCount: 0,
5883
6206
  activeCount: 0,
6207
+ failedCount: 0,
5884
6208
  totalCount: 0
5885
6209
  });
5886
6210
  }
@@ -5943,7 +6267,7 @@ var Manager = class extends EventEmitter {
5943
6267
  };
5944
6268
  //#endregion
5945
6269
  //#region ../../src/boss.ts
5946
- var events$2 = {
6270
+ var events$4 = {
5947
6271
  error: "error",
5948
6272
  warning: "warning"
5949
6273
  };
@@ -5969,7 +6293,7 @@ var Boss = class extends EventEmitter {
5969
6293
  #db;
5970
6294
  #config;
5971
6295
  #manager;
5972
- events = events$2;
6296
+ events = events$4;
5973
6297
  constructor(db, manager, config) {
5974
6298
  super();
5975
6299
  this.#db = db;
@@ -6004,8 +6328,8 @@ var Boss = class extends EventEmitter {
6004
6328
  db: this.#db,
6005
6329
  schema: this.#config.schema,
6006
6330
  persistWarnings: this.#config.persistWarnings,
6007
- warningEvent: events$2.warning,
6008
- errorEvent: events$2.error
6331
+ warningEvent: events$4.warning,
6332
+ errorEvent: events$4.error
6009
6333
  };
6010
6334
  }
6011
6335
  async #executeQuery(query) {
@@ -6034,7 +6358,7 @@ var Boss = class extends EventEmitter {
6034
6358
  !this.#stopped && await this.supervise(queues);
6035
6359
  !this.#stopped && await this.#maintainWarnings();
6036
6360
  } catch (err) {
6037
- this.emit(events$2.error, err);
6361
+ this.emit(events$4.error, err);
6038
6362
  } finally {
6039
6363
  this.#maintaining = false;
6040
6364
  }
@@ -6112,7 +6436,7 @@ var Boss = class extends EventEmitter {
6112
6436
  };
6113
6437
  //#endregion
6114
6438
  //#region ../../src/bam.ts
6115
- var events$1 = {
6439
+ var events$3 = {
6116
6440
  error: "error",
6117
6441
  bam: "bam"
6118
6442
  };
@@ -6122,7 +6446,7 @@ var Bam = class extends EventEmitter {
6122
6446
  #pollInterval;
6123
6447
  #db;
6124
6448
  #config;
6125
- events = events$1;
6449
+ events = events$3;
6126
6450
  constructor(db, config) {
6127
6451
  super();
6128
6452
  this.#db = db;
@@ -6158,7 +6482,7 @@ var Bam = class extends EventEmitter {
6158
6482
  const { rows } = await this.#db.executeSql(sql);
6159
6483
  if (rows.length === 1) await this.#processCommands();
6160
6484
  } catch (err) {
6161
- this.emit(events$1.error, err);
6485
+ this.emit(events$3.error, err);
6162
6486
  } finally {
6163
6487
  this.#working = false;
6164
6488
  }
@@ -6167,7 +6491,7 @@ var Bam = class extends EventEmitter {
6167
6491
  if (this.#stopped) return;
6168
6492
  const entry = await this.#getNextCommand();
6169
6493
  if (!entry || this.#stopped) return;
6170
- this.emit(events$1.bam, {
6494
+ this.emit(events$3.bam, {
6171
6495
  id: entry.id,
6172
6496
  name: entry.name,
6173
6497
  status: "in_progress",
@@ -6178,7 +6502,7 @@ var Bam = class extends EventEmitter {
6178
6502
  await this.#db.executeSql(entry.command);
6179
6503
  if (this.#stopped) return;
6180
6504
  await this.#markCompleted(entry.id);
6181
- this.emit(events$1.bam, {
6505
+ this.emit(events$3.bam, {
6182
6506
  id: entry.id,
6183
6507
  name: entry.name,
6184
6508
  status: "completed",
@@ -6188,8 +6512,8 @@ var Bam = class extends EventEmitter {
6188
6512
  } catch (err) {
6189
6513
  if (this.#stopped) return;
6190
6514
  await this.#markFailed(entry.id, err);
6191
- this.emit(events$1.error, err);
6192
- this.emit(events$1.bam, {
6515
+ this.emit(events$3.error, err);
6516
+ this.emit(events$3.bam, {
6193
6517
  id: entry.id,
6194
6518
  name: entry.name,
6195
6519
  status: "failed",
@@ -6214,6 +6538,181 @@ var Bam = class extends EventEmitter {
6214
6538
  }
6215
6539
  };
6216
6540
  //#endregion
6541
+ //#region ../../src/navigator.ts
6542
+ var events$2 = {
6543
+ error: "error",
6544
+ flow: "flow"
6545
+ };
6546
+ var MAX_BATCHES_PER_PASS = 100;
6547
+ var Navigator = class extends EventEmitter {
6548
+ #stopped;
6549
+ #stopping;
6550
+ #working;
6551
+ #pollInterval;
6552
+ #db;
6553
+ #manager;
6554
+ #config;
6555
+ events = events$2;
6556
+ constructor(db, manager, config) {
6557
+ super();
6558
+ this.#db = db;
6559
+ this.#manager = manager;
6560
+ this.#config = config;
6561
+ this.#stopped = true;
6562
+ this.#stopping = false;
6563
+ this.#working = false;
6564
+ }
6565
+ get working() {
6566
+ return this.#working;
6567
+ }
6568
+ async start() {
6569
+ if (!this.#stopped) return;
6570
+ this.#stopped = false;
6571
+ this.#stopping = false;
6572
+ setImmediate(() => this.#onPoll());
6573
+ this.#pollInterval = setInterval(() => this.#onPoll(), this.#config.flowIntervalSeconds * 1e3);
6574
+ }
6575
+ async stop() {
6576
+ if (this.#stopped) return;
6577
+ this.#stopping = true;
6578
+ this.#stopped = true;
6579
+ if (this.#pollInterval) {
6580
+ clearInterval(this.#pollInterval);
6581
+ this.#pollInterval = void 0;
6582
+ }
6583
+ while (this.#working) await delay(10);
6584
+ }
6585
+ async #onPoll() {
6586
+ if (this.#stopped || this.#working) return;
6587
+ this.#working = true;
6588
+ try {
6589
+ if (this.#config.__test__throw_flow) throw new Error(this.#config.__test__throw_flow);
6590
+ if (this.#config.__test__delay_flow_ms) await delay(this.#config.__test__delay_flow_ms);
6591
+ const gate = trySetFlowTime(this.#config.schema, this.#config.flowIntervalSeconds);
6592
+ const { rows } = await this.#db.executeSql(gate);
6593
+ if (rows.length === 1) await this.#resolve();
6594
+ } catch (err) {
6595
+ this.emit(events$2.error, err);
6596
+ } finally {
6597
+ this.#working = false;
6598
+ }
6599
+ }
6600
+ async resolveNow() {
6601
+ while (this.#working) await delay(10);
6602
+ if (this.#stopping) return;
6603
+ this.#working = true;
6604
+ try {
6605
+ await this.#resolve();
6606
+ } finally {
6607
+ this.#working = false;
6608
+ }
6609
+ }
6610
+ async #resolve() {
6611
+ const queueGroups = (await this.#manager.getQueues()).reduce((acc, q) => {
6612
+ acc[q.table] = acc[q.table] || {
6613
+ table: q.table,
6614
+ names: []
6615
+ };
6616
+ acc[q.table].names.push(q.name);
6617
+ return acc;
6618
+ }, {});
6619
+ for (const group of Object.values(queueGroups)) {
6620
+ if (this.#stopping) return;
6621
+ const { table } = group;
6622
+ const names = [...group.names];
6623
+ while (names.length) {
6624
+ if (this.#stopping) return;
6625
+ const chunk = names.splice(0, 100);
6626
+ let batches = 0;
6627
+ let resolved = 0;
6628
+ do {
6629
+ if (this.#stopping) return;
6630
+ resolved = this.#config.noMultiMutationCte ? await this.#manager.resolveFlowJobsDistributed(table, chunk) : await this.#resolveStandard(table, chunk);
6631
+ if (resolved > 0) this.emit(events$2.flow, {
6632
+ table,
6633
+ resolved
6634
+ });
6635
+ } while (resolved >= 1e3 && ++batches < MAX_BATCHES_PER_PASS && !this.#stopping);
6636
+ }
6637
+ }
6638
+ }
6639
+ async #resolveStandard(table, names) {
6640
+ const query = resolveFlowJobs(this.#config.schema, table, names);
6641
+ const { rows } = await this.#db.executeSql(query.text, query.values);
6642
+ return Number(rows[0]?.resolved ?? 0);
6643
+ }
6644
+ };
6645
+ //#endregion
6646
+ //#region ../../src/notifier.ts
6647
+ var events$1 = {
6648
+ error: "error",
6649
+ warning: "warning"
6650
+ };
6651
+ var WARNING_TYPE = "listen_notify_unavailable";
6652
+ var Notifier = class extends EventEmitter {
6653
+ events = events$1;
6654
+ #db;
6655
+ #manager;
6656
+ #config;
6657
+ #handle = null;
6658
+ #stopped = true;
6659
+ constructor(db, manager, config) {
6660
+ super();
6661
+ this.#db = db;
6662
+ this.#manager = manager;
6663
+ this.#config = config;
6664
+ }
6665
+ get available() {
6666
+ return this.#handle !== null;
6667
+ }
6668
+ async start() {
6669
+ if (!this.#stopped) return;
6670
+ this.#stopped = false;
6671
+ if (this.#config.noListenNotify) {
6672
+ this.emit(events$1.warning, {
6673
+ message: `useListenNotify is not supported on the ${this.#config.backend} backend. Continuing with polling only.`,
6674
+ data: {
6675
+ type: WARNING_TYPE,
6676
+ backend: this.#config.backend
6677
+ }
6678
+ });
6679
+ return;
6680
+ }
6681
+ if (typeof this.#db.listen !== "function") {
6682
+ this.emit(events$1.warning, {
6683
+ message: "useListenNotify is enabled but the database connection does not support LISTEN/NOTIFY. Continuing with polling only.",
6684
+ data: { type: WARNING_TYPE }
6685
+ });
6686
+ return;
6687
+ }
6688
+ try {
6689
+ const { rows } = await this.#db.executeSql(`SELECT ${notifyChannelSql(this.#config.schema)} AS channel`);
6690
+ const channel = rows[0].channel;
6691
+ this.#handle = await this.#db.listen(channel, (payload) => this.#manager.notifyQueue(payload), () => this.#manager.forceFetchLnWorkers());
6692
+ } catch (err) {
6693
+ this.emit(events$1.warning, {
6694
+ message: "Failed to start LISTEN/NOTIFY listener. Continuing with polling only.",
6695
+ data: {
6696
+ type: WARNING_TYPE,
6697
+ error: err?.message
6698
+ }
6699
+ });
6700
+ }
6701
+ }
6702
+ async stop() {
6703
+ if (this.#stopped) return;
6704
+ this.#stopped = true;
6705
+ if (this.#handle) {
6706
+ try {
6707
+ await this.#handle.close();
6708
+ } catch (err) {
6709
+ this.emit(events$1.error, err);
6710
+ }
6711
+ this.#handle = null;
6712
+ }
6713
+ }
6714
+ };
6715
+ //#endregion
6217
6716
  //#region ../../src/db.ts
6218
6717
  var Db = class extends EventEmitter {
6219
6718
  pool;
@@ -6224,7 +6723,7 @@ var Db = class extends EventEmitter {
6224
6723
  constructor(config) {
6225
6724
  super();
6226
6725
  config.application_name = config.application_name || "pgboss";
6227
- config.connectionTimeoutMillis = config.connectionTimeoutMillis || 1e4;
6726
+ config.connectionTimeoutMillis ??= 1e4;
6228
6727
  this.config = config;
6229
6728
  this._pgbdb = true;
6230
6729
  this.opened = false;
@@ -6245,6 +6744,63 @@ var Db = class extends EventEmitter {
6245
6744
  assert(this.opened, "Database not opened. Call open() before executing SQL.");
6246
6745
  return await this.pool.query(text, values);
6247
6746
  }
6747
+ async listen(channel, onNotification, onReconnect) {
6748
+ assert(this.opened, "Database not opened. Call open() before listening.");
6749
+ let closed = false;
6750
+ let client = null;
6751
+ let reconnectTimer = null;
6752
+ let attempt = 0;
6753
+ const scheduleReconnect = () => {
6754
+ if (closed || reconnectTimer) return;
6755
+ const backoff = Math.min(3e4, 1e3 * 2 ** Math.min(attempt, 5));
6756
+ attempt++;
6757
+ reconnectTimer = setTimeout(() => {
6758
+ reconnectTimer = null;
6759
+ connect().catch(() => scheduleReconnect());
6760
+ }, backoff);
6761
+ };
6762
+ const connect = async () => {
6763
+ if (closed) return;
6764
+ const next = new pg.Client(this.config);
6765
+ next.on("error", (error) => {
6766
+ this.emit("error", error);
6767
+ if (!closed) {
6768
+ next.removeAllListeners();
6769
+ next.end().catch(() => {});
6770
+ if (client === next) client = null;
6771
+ scheduleReconnect();
6772
+ }
6773
+ });
6774
+ next.on("notification", (msg) => {
6775
+ if (msg.payload !== void 0) onNotification(msg.payload);
6776
+ });
6777
+ client = next;
6778
+ try {
6779
+ await next.connect();
6780
+ await next.query(`LISTEN "${channel}"`);
6781
+ } catch (err) {
6782
+ next.removeAllListeners();
6783
+ await next.end().catch(() => {});
6784
+ if (client === next) client = null;
6785
+ throw err;
6786
+ }
6787
+ attempt = 0;
6788
+ onReconnect();
6789
+ };
6790
+ await connect();
6791
+ return { close: async () => {
6792
+ closed = true;
6793
+ if (reconnectTimer) {
6794
+ clearTimeout(reconnectTimer);
6795
+ reconnectTimer = null;
6796
+ }
6797
+ if (client) {
6798
+ client.removeAllListeners();
6799
+ await client.end().catch(() => {});
6800
+ client = null;
6801
+ }
6802
+ } };
6803
+ }
6248
6804
  async withTransaction(fn) {
6249
6805
  assert(this.opened, "Database not opened. Call open() before executing SQL.");
6250
6806
  const client = await this.pool.connect();
@@ -6268,7 +6824,8 @@ var events = Object.freeze({
6268
6824
  warning: "warning",
6269
6825
  wip: "wip",
6270
6826
  stopped: "stopped",
6271
- bam: "bam"
6827
+ bam: "bam",
6828
+ flow: "flow"
6272
6829
  });
6273
6830
  var PgBoss = class extends EventEmitter {
6274
6831
  #stoppingOn;
@@ -6282,6 +6839,8 @@ var PgBoss = class extends EventEmitter {
6282
6839
  #manager;
6283
6840
  #timekeeper;
6284
6841
  #bam;
6842
+ #navigator;
6843
+ #notifier;
6285
6844
  constructor(value) {
6286
6845
  super();
6287
6846
  this.#stoppingOn = null;
@@ -6297,15 +6856,22 @@ var PgBoss = class extends EventEmitter {
6297
6856
  const timekeeper = new Timekeeper(db, manager, config);
6298
6857
  manager.timekeeper = timekeeper;
6299
6858
  const bam = new Bam(db, config);
6859
+ const navigator = new Navigator(db, manager, config);
6860
+ const notifier = new Notifier(db, manager, config);
6861
+ manager.notifier = notifier;
6300
6862
  this.#promoteEvents(manager);
6301
6863
  this.#promoteEvents(boss);
6302
6864
  this.#promoteEvents(timekeeper);
6303
6865
  this.#promoteEvents(bam);
6866
+ this.#promoteEvents(navigator);
6867
+ this.#promoteEvents(notifier);
6304
6868
  this.#boss = boss;
6305
6869
  this.#contractor = contractor;
6306
6870
  this.#manager = manager;
6307
6871
  this.#timekeeper = timekeeper;
6308
6872
  this.#bam = bam;
6873
+ this.#navigator = navigator;
6874
+ this.#notifier = notifier;
6309
6875
  }
6310
6876
  #promoteEvents(emitter) {
6311
6877
  for (const event of Object.values(emitter?.events)) emitter.on(event, (arg) => this.emit(event, arg));
@@ -6319,7 +6885,11 @@ var PgBoss = class extends EventEmitter {
6319
6885
  if (this.#config.migrate) await this.#contractor.start();
6320
6886
  else await this.#contractor.check();
6321
6887
  await this.#manager.start();
6322
- if (this.#config.supervise) await this.#boss.start();
6888
+ if (this.#config.useListenNotify) await this.#notifier.start();
6889
+ if (this.#config.supervise) {
6890
+ await this.#boss.start();
6891
+ await this.#navigator.start();
6892
+ }
6323
6893
  if (this.#config.schedule) await this.#timekeeper.start();
6324
6894
  if (this.#config.migrate) await this.#bam.start();
6325
6895
  } catch (err) {
@@ -6348,9 +6918,11 @@ var PgBoss = class extends EventEmitter {
6348
6918
  let { close = true, graceful = true, timeout = 3e4 } = options;
6349
6919
  timeout = Math.max(timeout, 1e3);
6350
6920
  this.#stoppingOn = Date.now();
6921
+ await this.#notifier.stop();
6351
6922
  await this.#manager.stop();
6352
6923
  await this.#timekeeper.stop();
6353
6924
  await this.#boss.stop();
6925
+ await this.#navigator.stop();
6354
6926
  await this.#bam.stop();
6355
6927
  const shutdown = async () => {
6356
6928
  await this.#manager.failWip();
@@ -6464,7 +7036,7 @@ var PgBoss = class extends EventEmitter {
6464
7036
  return this.#manager.deleteQueue(name);
6465
7037
  }
6466
7038
  getQueues(names) {
6467
- return this.#manager.getQueues();
7039
+ return this.#manager.getQueues(names);
6468
7040
  }
6469
7041
  getQueue(name) {
6470
7042
  return this.#manager.getQueue(name);
@@ -6478,12 +7050,18 @@ var PgBoss = class extends EventEmitter {
6478
7050
  isBamWorking() {
6479
7051
  return this.#bam.working;
6480
7052
  }
7053
+ isResolvingFlow() {
7054
+ return this.#navigator.working;
7055
+ }
6481
7056
  isCheckingSkew() {
6482
7057
  return this.#timekeeper.checkingSkew;
6483
7058
  }
6484
7059
  supervise(name) {
6485
7060
  return this.#boss.supervise(name);
6486
7061
  }
7062
+ resolveFlow() {
7063
+ return this.#navigator.resolveNow();
7064
+ }
6487
7065
  getWipData(options) {
6488
7066
  return this.#manager.getWipData(options);
6489
7067
  }
@@ -6514,7 +7092,7 @@ var PgBoss = class extends EventEmitter {
6514
7092
  return rows;
6515
7093
  }
6516
7094
  async getBamEntries() {
6517
- const sql = getBamEntries(this.#config.schema);
7095
+ const sql = getBamEntries$1(this.#config.schema);
6518
7096
  const { rows } = await this.#db.executeSql(sql);
6519
7097
  return rows;
6520
7098
  }
@@ -6609,7 +7187,9 @@ var QUEUE_COLUMNS = `
6609
7187
  deletion_seconds as "deleteAfterSeconds",
6610
7188
  deferred_count as "deferredCount",
6611
7189
  queued_count as "queuedCount",
7190
+ GREATEST(queued_count - deferred_count, 0) as "readyCount",
6612
7191
  active_count as "activeCount",
7192
+ failed_count as "failedCount",
6613
7193
  total_count as "totalCount",
6614
7194
  warning_queued as "warningQueueSize",
6615
7195
  singletons_active as "singletonsActive",
@@ -6862,19 +7442,81 @@ async function getWarningCount(dbUrl, schema, type) {
6862
7442
  throw err;
6863
7443
  }
6864
7444
  }
7445
+ async function getBamEntries(dbUrl, schema, options = {}) {
7446
+ const s = validateIdentifier(schema);
7447
+ const { status = null, limit = 200, offset = 0 } = options;
7448
+ const sql = `
7449
+ SELECT
7450
+ id,
7451
+ name,
7452
+ version,
7453
+ status,
7454
+ queue,
7455
+ table_name as "table",
7456
+ command,
7457
+ error,
7458
+ created_on as "createdOn",
7459
+ started_on as "startedOn",
7460
+ completed_on as "completedOn"
7461
+ FROM ${s}.bam
7462
+ WHERE ($1::text IS NULL OR status = $1)
7463
+ ORDER BY version DESC, created_on DESC
7464
+ LIMIT $2 OFFSET $3
7465
+ `;
7466
+ try {
7467
+ return await query(dbUrl, sql, [
7468
+ status,
7469
+ limit,
7470
+ offset
7471
+ ]);
7472
+ } catch (err) {
7473
+ if (err && typeof err === "object" && "code" in err && err.code === "42P01") return [];
7474
+ throw err;
7475
+ }
7476
+ }
7477
+ async function getBamCount(dbUrl, schema, status) {
7478
+ const sql = `
7479
+ SELECT COUNT(*)::int as count
7480
+ FROM ${validateIdentifier(schema)}.bam
7481
+ WHERE ($1::text IS NULL OR status = $1)
7482
+ `;
7483
+ try {
7484
+ return (await queryOne(dbUrl, sql, [status ?? null]))?.count ?? 0;
7485
+ } catch (err) {
7486
+ if (err && typeof err === "object" && "code" in err && err.code === "42P01") return 0;
7487
+ throw err;
7488
+ }
7489
+ }
7490
+ async function getBamStatusSummary(dbUrl, schema) {
7491
+ const sql = `
7492
+ SELECT status, count(*)::int as count, max(created_on) as "lastCreatedOn"
7493
+ FROM ${validateIdentifier(schema)}.bam
7494
+ GROUP BY status
7495
+ `;
7496
+ try {
7497
+ return await query(dbUrl, sql);
7498
+ } catch (err) {
7499
+ if (err && typeof err === "object" && "code" in err && err.code === "42P01") return [];
7500
+ throw err;
7501
+ }
7502
+ }
6865
7503
  async function getQueueStats(dbUrl, schema) {
6866
7504
  return await queryOne(dbUrl, `
6867
7505
  SELECT
6868
7506
  COALESCE(SUM(deferred_count), 0)::int as "totalDeferred",
6869
7507
  COALESCE(SUM(queued_count), 0)::int as "totalQueued",
7508
+ COALESCE(SUM(GREATEST(queued_count - deferred_count, 0)), 0)::int as "totalReady",
6870
7509
  COALESCE(SUM(active_count), 0)::int as "totalActive",
7510
+ COALESCE(SUM(failed_count), 0)::int as "totalFailed",
6871
7511
  COALESCE(SUM(total_count), 0)::int as "totalJobs",
6872
7512
  COUNT(*)::int as "queueCount"
6873
7513
  FROM ${validateIdentifier(schema)}.queue
6874
7514
  `) ?? {
6875
7515
  totalDeferred: 0,
6876
7516
  totalQueued: 0,
7517
+ totalReady: 0,
6877
7518
  totalActive: 0,
7519
+ totalFailed: 0,
6878
7520
  totalJobs: 0,
6879
7521
  queueCount: 0
6880
7522
  };
@@ -6975,9 +7617,21 @@ var statCards = [
6975
7617
  {
6976
7618
  name: "Queued Jobs",
6977
7619
  key: "totalQueued",
6978
- hint: "waiting to process",
7620
+ hint: "incl. deferred",
7621
+ accent: "neutral"
7622
+ },
7623
+ {
7624
+ name: "Deferred",
7625
+ key: "totalDeferred",
7626
+ hint: "scheduled for later",
6979
7627
  accent: "neutral"
6980
7628
  },
7629
+ {
7630
+ name: "Ready",
7631
+ key: "totalReady",
7632
+ hint: "ready to process",
7633
+ accent: "primary"
7634
+ },
6981
7635
  {
6982
7636
  name: "Active",
6983
7637
  key: "totalActive",
@@ -6985,9 +7639,9 @@ var statCards = [
6985
7639
  accent: "primary"
6986
7640
  },
6987
7641
  {
6988
- name: "Deferred",
6989
- key: "totalDeferred",
6990
- hint: "scheduled for later",
7642
+ name: "Failed",
7643
+ key: "totalFailed",
7644
+ hint: "recent failures",
6991
7645
  accent: "neutral"
6992
7646
  },
6993
7647
  {
@@ -7199,34 +7853,45 @@ function ErrorCard({ title, message = "Please check your database connection and
7199
7853
  //#endregion
7200
7854
  //#region app/routes/_index.tsx
7201
7855
  var _index_exports = /* @__PURE__ */ __exportAll({
7202
- ErrorBoundary: () => ErrorBoundary$10,
7856
+ ErrorBoundary: () => ErrorBoundary$11,
7203
7857
  default: () => _index_default,
7204
- loader: () => loader$10
7858
+ loader: () => loader$11
7205
7859
  });
7206
- async function loader$10({ context }) {
7860
+ async function loader$11({ context }) {
7207
7861
  const { DB_URL, SCHEMA } = context.get(dbContext);
7208
- const [warnings, stats, topQueues, totalQueues, problemQueuesCount] = await Promise.all([
7862
+ const [warnings, stats, topQueues, totalQueues, problemQueuesCount, bamSummary] = await Promise.all([
7209
7863
  getWarnings(DB_URL, SCHEMA, { limit: 5 }),
7210
7864
  getQueueStats(DB_URL, SCHEMA),
7211
7865
  getTopQueues(DB_URL, SCHEMA, 5),
7212
7866
  getQueueCount(DB_URL, SCHEMA),
7213
- getProblemQueuesCount(DB_URL, SCHEMA)
7867
+ getProblemQueuesCount(DB_URL, SCHEMA),
7868
+ getBamStatusSummary(DB_URL, SCHEMA)
7214
7869
  ]);
7215
7870
  return {
7216
7871
  stats,
7217
7872
  warnings,
7218
7873
  topQueues,
7874
+ migrations: bamSummary.reduce((acc, row) => {
7875
+ if (row.status === "pending") acc.pending += row.count;
7876
+ else if (row.status === "in_progress") acc.inProgress += row.count;
7877
+ else if (row.status === "failed") acc.failed += row.count;
7878
+ return acc;
7879
+ }, {
7880
+ pending: 0,
7881
+ inProgress: 0,
7882
+ failed: 0
7883
+ }),
7219
7884
  queueStats: {
7220
7885
  totalQueues,
7221
7886
  problemQueues: problemQueuesCount
7222
7887
  }
7223
7888
  };
7224
7889
  }
7225
- var ErrorBoundary$10 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
7890
+ var ErrorBoundary$11 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
7226
7891
  return /* @__PURE__ */ jsx(ErrorCard, { title: "Failed to load dashboard" });
7227
7892
  });
7228
7893
  var _index_default = UNSAFE_withComponentProps(function Overview({ loaderData }) {
7229
- const { stats, warnings, topQueues } = loaderData;
7894
+ const { stats, warnings, topQueues, migrations } = loaderData;
7230
7895
  return /* @__PURE__ */ jsxs("div", { children: [
7231
7896
  /* @__PURE__ */ jsx(PageHeader, {
7232
7897
  title: "Overview",
@@ -7241,9 +7906,10 @@ var _index_default = UNSAFE_withComponentProps(function Overview({ loaderData })
7241
7906
  })
7242
7907
  }),
7243
7908
  /* @__PURE__ */ jsx("div", {
7244
- className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4 mb-4",
7909
+ className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 mb-4",
7245
7910
  children: /* @__PURE__ */ jsx(StatsCards, { stats })
7246
7911
  }),
7912
+ /* @__PURE__ */ jsx(MigrationsBanner, { migrations }),
7247
7913
  /* @__PURE__ */ jsxs("div", {
7248
7914
  className: "grid grid-cols-1 lg:grid-cols-[1.5fr_1fr] gap-4",
7249
7915
  children: [/* @__PURE__ */ jsxs(Card, { children: [/* @__PURE__ */ jsxs(CardHeader, { children: [/* @__PURE__ */ jsx(CardTitle, { children: "Top Queues" }), /* @__PURE__ */ jsx(DbLink, {
@@ -7312,6 +7978,37 @@ var _index_default = UNSAFE_withComponentProps(function Overview({ loaderData })
7312
7978
  })
7313
7979
  ] });
7314
7980
  });
7981
+ function MigrationsBanner({ migrations }) {
7982
+ const { pending, inProgress, failed } = migrations;
7983
+ if (pending === 0 && inProgress === 0 && failed === 0) return null;
7984
+ const parts = [];
7985
+ if (pending > 0) parts.push(`${pending.toLocaleString()} pending`);
7986
+ if (inProgress > 0) parts.push(`${inProgress.toLocaleString()} in progress`);
7987
+ if (failed > 0) parts.push(`${failed.toLocaleString()} failed`);
7988
+ return /* @__PURE__ */ jsx(DbLink, {
7989
+ to: "/migrations",
7990
+ className: "block mb-4",
7991
+ children: /* @__PURE__ */ jsxs(Card, {
7992
+ className: "flex items-center gap-3 px-4 py-3 hover:bg-[var(--surface-hover)]",
7993
+ children: [
7994
+ /* @__PURE__ */ jsx(Badge, {
7995
+ variant: failed > 0 ? "error" : "warning",
7996
+ size: "sm",
7997
+ dot: true,
7998
+ children: "Async migrations"
7999
+ }),
8000
+ /* @__PURE__ */ jsx("span", {
8001
+ className: "text-sm text-[var(--text-secondary)]",
8002
+ children: parts.join(" · ")
8003
+ }),
8004
+ /* @__PURE__ */ jsx("span", {
8005
+ className: "ml-auto text-sm font-medium text-primary-600 dark:text-primary-400",
8006
+ children: "View"
8007
+ })
8008
+ ]
8009
+ })
8010
+ });
8011
+ }
7315
8012
  function QueueStatusBadge({ queue }) {
7316
8013
  if ((queue.warningQueueSize ?? 0) > 0 && queue.queuedCount > (queue.warningQueueSize ?? 0)) return /* @__PURE__ */ jsx(Badge, {
7317
8014
  variant: "error",
@@ -7826,10 +8523,10 @@ function SearchIcon$1({ className }) {
7826
8523
  //#endregion
7827
8524
  //#region app/routes/jobs.tsx
7828
8525
  var jobs_exports = /* @__PURE__ */ __exportAll({
7829
- ErrorBoundary: () => ErrorBoundary$9,
8526
+ ErrorBoundary: () => ErrorBoundary$10,
7830
8527
  buildSearchParams: () => buildSearchParams,
7831
8528
  default: () => jobs_default,
7832
- loader: () => loader$9,
8529
+ loader: () => loader$10,
7833
8530
  parseFiltersFromUrl: () => parseFiltersFromUrl
7834
8531
  });
7835
8532
  function parseFiltersFromUrl(searchParams) {
@@ -7866,7 +8563,7 @@ function parseFiltersFromUrl(searchParams) {
7866
8563
  shouldRunCount
7867
8564
  };
7868
8565
  }
7869
- async function loader$9({ request, context }) {
8566
+ async function loader$10({ request, context }) {
7870
8567
  const { DB_URL, SCHEMA } = context.get(dbContext);
7871
8568
  const url = new URL(request.url);
7872
8569
  const parsed = parseFiltersFromUrl(url.searchParams);
@@ -7914,7 +8611,7 @@ async function loader$9({ request, context }) {
7914
8611
  hasPrevPage
7915
8612
  };
7916
8613
  }
7917
- var ErrorBoundary$9 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
8614
+ var ErrorBoundary$10 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
7918
8615
  return /* @__PURE__ */ jsx(ErrorCard, { title: "Failed to load jobs" });
7919
8616
  });
7920
8617
  function buildSearchParams(filters) {
@@ -8094,11 +8791,11 @@ function Chip({ label, onRemove }) {
8094
8791
  //#endregion
8095
8792
  //#region app/routes/queues._index.tsx
8096
8793
  var queues__index_exports = /* @__PURE__ */ __exportAll({
8097
- ErrorBoundary: () => ErrorBoundary$8,
8794
+ ErrorBoundary: () => ErrorBoundary$9,
8098
8795
  default: () => queues__index_default,
8099
- loader: () => loader$8
8796
+ loader: () => loader$9
8100
8797
  });
8101
- async function loader$8({ request, context }) {
8798
+ async function loader$9({ request, context }) {
8102
8799
  const { DB_URL, SCHEMA } = context.get(dbContext);
8103
8800
  const url = new URL(request.url);
8104
8801
  const page = parsePageNumber(url.searchParams.get("page"));
@@ -8131,7 +8828,7 @@ async function loader$8({ request, context }) {
8131
8828
  search
8132
8829
  };
8133
8830
  }
8134
- var ErrorBoundary$8 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
8831
+ var ErrorBoundary$9 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
8135
8832
  return /* @__PURE__ */ jsx("div", {
8136
8833
  className: "p-6",
8137
8834
  children: /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs(CardContent, {
@@ -8297,13 +8994,21 @@ var queues__index_default = UNSAFE_withComponentProps(function QueuesIndex({ loa
8297
8994
  className: "text-right",
8298
8995
  children: "Queued"
8299
8996
  }),
8997
+ /* @__PURE__ */ jsx(TableHead, {
8998
+ className: "text-right",
8999
+ children: "Deferred"
9000
+ }),
9001
+ /* @__PURE__ */ jsx(TableHead, {
9002
+ className: "text-right",
9003
+ children: "Ready"
9004
+ }),
8300
9005
  /* @__PURE__ */ jsx(TableHead, {
8301
9006
  className: "text-right",
8302
9007
  children: "Active"
8303
9008
  }),
8304
9009
  /* @__PURE__ */ jsx(TableHead, {
8305
9010
  className: "text-right",
8306
- children: "Deferred"
9011
+ children: "Failed"
8307
9012
  }),
8308
9013
  /* @__PURE__ */ jsx(TableHead, {
8309
9014
  className: "text-right",
@@ -8313,7 +9018,7 @@ var queues__index_default = UNSAFE_withComponentProps(function QueuesIndex({ loa
8313
9018
  /* @__PURE__ */ jsx(TableHead, { children: "Status" })
8314
9019
  ] }) }), /* @__PURE__ */ jsx(TableBody, { children: queues.length === 0 ? /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(TableCell, {
8315
9020
  className: "text-center text-[var(--text-tertiary)] py-8",
8316
- colSpan: 9,
9021
+ colSpan: 11,
8317
9022
  children: "No queues found"
8318
9023
  }) }) : queues.map((queue) => {
8319
9024
  const hasBacklog = (queue.warningQueueSize ?? 0) > 0 && queue.queuedCount > (queue.warningQueueSize ?? 0);
@@ -8336,13 +9041,21 @@ var queues__index_default = UNSAFE_withComponentProps(function QueuesIndex({ loa
8336
9041
  className: "text-right pgb-num text-[var(--text-primary)]",
8337
9042
  children: queue.queuedCount.toLocaleString()
8338
9043
  }),
9044
+ /* @__PURE__ */ jsx(TableCell, {
9045
+ className: "text-right pgb-num text-[var(--text-primary)]",
9046
+ children: queue.deferredCount.toLocaleString()
9047
+ }),
9048
+ /* @__PURE__ */ jsx(TableCell, {
9049
+ className: "text-right pgb-num text-[var(--text-primary)]",
9050
+ children: queue.readyCount.toLocaleString()
9051
+ }),
8339
9052
  /* @__PURE__ */ jsx(TableCell, {
8340
9053
  className: "text-right pgb-num text-[var(--text-primary)]",
8341
9054
  children: queue.activeCount.toLocaleString()
8342
9055
  }),
8343
9056
  /* @__PURE__ */ jsx(TableCell, {
8344
9057
  className: "text-right pgb-num text-[var(--text-primary)]",
8345
- children: queue.deferredCount.toLocaleString()
9058
+ children: queue.failedCount.toLocaleString()
8346
9059
  }),
8347
9060
  /* @__PURE__ */ jsx(TableCell, {
8348
9061
  className: "text-right pgb-num text-[var(--text-primary)]",
@@ -8520,12 +9233,12 @@ var SelectContent = ({ children }) => /* @__PURE__ */ jsx(Fragment, { children }
8520
9233
  //#endregion
8521
9234
  //#region app/routes/queues.create.tsx
8522
9235
  var queues_create_exports = /* @__PURE__ */ __exportAll({
8523
- ErrorBoundary: () => ErrorBoundary$7,
9236
+ ErrorBoundary: () => ErrorBoundary$8,
8524
9237
  action: () => action$5,
8525
9238
  default: () => queues_create_default,
8526
- loader: () => loader$7
9239
+ loader: () => loader$8
8527
9240
  });
8528
- async function loader$7({ context }) {
9241
+ async function loader$8({ context }) {
8529
9242
  const { DB_URL, SCHEMA } = context.get(dbContext);
8530
9243
  return { queues: await getQueues(DB_URL, SCHEMA) };
8531
9244
  }
@@ -8593,7 +9306,7 @@ async function action$5({ request, context }) {
8593
9306
  const dbParam = new URL(request.url).searchParams.get("db");
8594
9307
  return redirect(dbParam ? `/queues/${encodeURIComponent(queueName.trim())}?db=${encodeURIComponent(dbParam)}` : `/queues/${encodeURIComponent(queueName.trim())}`);
8595
9308
  }
8596
- var ErrorBoundary$7 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
9309
+ var ErrorBoundary$8 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
8597
9310
  return /* @__PURE__ */ jsx(ErrorCard, {
8598
9311
  title: "Failed to load queue creation page",
8599
9312
  backTo: {
@@ -9041,12 +9754,12 @@ DialogDescription.displayName = "DialogDescription";
9041
9754
  //#endregion
9042
9755
  //#region app/routes/queues.$name.tsx
9043
9756
  var queues_$name_exports = /* @__PURE__ */ __exportAll({
9044
- ErrorBoundary: () => ErrorBoundary$6,
9757
+ ErrorBoundary: () => ErrorBoundary$7,
9045
9758
  action: () => action$4,
9046
9759
  default: () => queues_$name_default,
9047
- loader: () => loader$6
9760
+ loader: () => loader$7
9048
9761
  });
9049
- async function loader$6({ params, request, context }) {
9762
+ async function loader$7({ params, request, context }) {
9050
9763
  const { DB_URL, SCHEMA } = context.get(dbContext);
9051
9764
  const url = new URL(request.url);
9052
9765
  const stateParam = url.searchParams.get("state");
@@ -9119,7 +9832,7 @@ async function action$4({ params, request, context }) {
9119
9832
  message
9120
9833
  };
9121
9834
  }
9122
- var ErrorBoundary$6 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
9835
+ var ErrorBoundary$7 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
9123
9836
  return /* @__PURE__ */ jsx(ErrorCard, {
9124
9837
  title: "Failed to load queue",
9125
9838
  backTo: {
@@ -9178,13 +9891,23 @@ var queues_$name_default = UNSAFE_withComponentProps(function QueueDetail({ load
9178
9891
  ]
9179
9892
  }),
9180
9893
  /* @__PURE__ */ jsxs("div", {
9181
- className: "grid grid-cols-2 sm:grid-cols-4 gap-4",
9894
+ className: "grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-4",
9182
9895
  children: [
9183
9896
  /* @__PURE__ */ jsx(StatCard, {
9184
9897
  label: "Queued",
9185
9898
  value: queue.queuedCount.toLocaleString(),
9186
9899
  accent: overThreshold ? "error" : "neutral",
9187
- hint: overThreshold ? "over threshold" : "within threshold"
9900
+ hint: overThreshold ? "over threshold" : "incl. deferred"
9901
+ }),
9902
+ /* @__PURE__ */ jsx(StatCard, {
9903
+ label: "Deferred",
9904
+ value: queue.deferredCount.toLocaleString()
9905
+ }),
9906
+ /* @__PURE__ */ jsx(StatCard, {
9907
+ label: "Ready",
9908
+ value: queue.readyCount.toLocaleString(),
9909
+ accent: "primary",
9910
+ hint: "ready to process"
9188
9911
  }),
9189
9912
  /* @__PURE__ */ jsx(StatCard, {
9190
9913
  label: "Active",
@@ -9192,8 +9915,9 @@ var queues_$name_default = UNSAFE_withComponentProps(function QueueDetail({ load
9192
9915
  accent: "primary"
9193
9916
  }),
9194
9917
  /* @__PURE__ */ jsx(StatCard, {
9195
- label: "Deferred",
9196
- value: queue.deferredCount.toLocaleString()
9918
+ label: "Failed",
9919
+ value: queue.failedCount.toLocaleString(),
9920
+ hint: "recent failures"
9197
9921
  }),
9198
9922
  /* @__PURE__ */ jsx(StatCard, {
9199
9923
  label: "Total",
@@ -9539,12 +10263,12 @@ function ConfirmDialog({ title, description, confirmLabel, confirmVariant = "pri
9539
10263
  //#endregion
9540
10264
  //#region app/routes/queues.$name.jobs.$jobId.tsx
9541
10265
  var queues_$name_jobs_$jobId_exports = /* @__PURE__ */ __exportAll({
9542
- ErrorBoundary: () => ErrorBoundary$5,
10266
+ ErrorBoundary: () => ErrorBoundary$6,
9543
10267
  action: () => action$3,
9544
10268
  default: () => queues_$name_jobs_$jobId_default,
9545
- loader: () => loader$5
10269
+ loader: () => loader$6
9546
10270
  });
9547
- async function loader$5({ params, context }) {
10271
+ async function loader$6({ params, context }) {
9548
10272
  const { DB_URL, SCHEMA } = context.get(dbContext);
9549
10273
  const job = await getJobById(DB_URL, SCHEMA, params.name, params.jobId);
9550
10274
  if (!job) throw new Response("Job not found", { status: 404 });
@@ -9598,7 +10322,7 @@ async function action$3({ params, request, context }) {
9598
10322
  message
9599
10323
  };
9600
10324
  }
9601
- var ErrorBoundary$5 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
10325
+ var ErrorBoundary$6 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
9602
10326
  return /* @__PURE__ */ jsx(ErrorCard, {
9603
10327
  title: "Failed to load job",
9604
10328
  backTo: {
@@ -9851,11 +10575,11 @@ function ConfigItem({ label, value, mono = false }) {
9851
10575
  //#endregion
9852
10576
  //#region app/routes/schedules.tsx
9853
10577
  var schedules_exports = /* @__PURE__ */ __exportAll({
9854
- ErrorBoundary: () => ErrorBoundary$4,
10578
+ ErrorBoundary: () => ErrorBoundary$5,
9855
10579
  default: () => schedules_default,
9856
- loader: () => loader$4
10580
+ loader: () => loader$5
9857
10581
  });
9858
- async function loader$4({ request, context }) {
10582
+ async function loader$5({ request, context }) {
9859
10583
  const { DB_URL, SCHEMA } = context.get(dbContext);
9860
10584
  const page = parsePageNumber(new URL(request.url).searchParams.get("page"));
9861
10585
  const limit = 20;
@@ -9873,7 +10597,7 @@ async function loader$4({ request, context }) {
9873
10597
  hasPrevPage: page > 1
9874
10598
  };
9875
10599
  }
9876
- var ErrorBoundary$4 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
10600
+ var ErrorBoundary$5 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
9877
10601
  return /* @__PURE__ */ jsx(ErrorCard, { title: "Failed to load schedules" });
9878
10602
  });
9879
10603
  function cronHuman(cron) {
@@ -9983,12 +10707,12 @@ var schedules_default = UNSAFE_withComponentProps(function Schedules({ loaderDat
9983
10707
  //#endregion
9984
10708
  //#region app/routes/schedules.$name.$key.tsx
9985
10709
  var schedules_$name_$key_exports = /* @__PURE__ */ __exportAll({
9986
- ErrorBoundary: () => ErrorBoundary$3,
10710
+ ErrorBoundary: () => ErrorBoundary$4,
9987
10711
  action: () => action$2,
9988
10712
  default: () => schedules_$name_$key_default,
9989
- loader: () => loader$3
10713
+ loader: () => loader$4
9990
10714
  });
9991
- async function loader$3({ params, context }) {
10715
+ async function loader$4({ params, context }) {
9992
10716
  const { DB_URL, SCHEMA } = context.get(dbContext);
9993
10717
  const key = params.key === "__default__" ? "" : params.key;
9994
10718
  const schedule = await getSchedule(DB_URL, SCHEMA, params.name, key);
@@ -10006,7 +10730,7 @@ async function action$2({ params, request, context }) {
10006
10730
  }
10007
10731
  return { error: "Invalid action" };
10008
10732
  }
10009
- var ErrorBoundary$3 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
10733
+ var ErrorBoundary$4 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
10010
10734
  return /* @__PURE__ */ jsx(ErrorCard, {
10011
10735
  title: "Failed to load schedule",
10012
10736
  backTo: {
@@ -10166,12 +10890,12 @@ var schedules_$name_$key_default = UNSAFE_withComponentProps(function ScheduleDe
10166
10890
  //#endregion
10167
10891
  //#region app/routes/schedules.new.tsx
10168
10892
  var schedules_new_exports = /* @__PURE__ */ __exportAll({
10169
- ErrorBoundary: () => ErrorBoundary$2,
10893
+ ErrorBoundary: () => ErrorBoundary$3,
10170
10894
  action: () => action$1,
10171
10895
  default: () => schedules_new_default,
10172
- loader: () => loader$2
10896
+ loader: () => loader$3
10173
10897
  });
10174
- async function loader$2({ context }) {
10898
+ async function loader$3({ context }) {
10175
10899
  const { DB_URL, SCHEMA } = context.get(dbContext);
10176
10900
  return { queues: await getQueues(DB_URL, SCHEMA) };
10177
10901
  }
@@ -10224,7 +10948,7 @@ async function action$1({ request, context }) {
10224
10948
  const dbParam = new URL(request.url).searchParams.get("db");
10225
10949
  return redirect(dbParam ? `/schedules?db=${encodeURIComponent(dbParam)}` : `/schedules`);
10226
10950
  }
10227
- var ErrorBoundary$2 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
10951
+ var ErrorBoundary$3 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
10228
10952
  return /* @__PURE__ */ jsx(ErrorCard, {
10229
10953
  title: "Failed to load schedule creation",
10230
10954
  backTo: {
@@ -10490,12 +11214,12 @@ var schedules_new_default = UNSAFE_withComponentProps(function CreateSchedule({
10490
11214
  //#endregion
10491
11215
  //#region app/routes/send.tsx
10492
11216
  var send_exports = /* @__PURE__ */ __exportAll({
10493
- ErrorBoundary: () => ErrorBoundary$1,
11217
+ ErrorBoundary: () => ErrorBoundary$2,
10494
11218
  action: () => action,
10495
11219
  default: () => send_default,
10496
- loader: () => loader$1
11220
+ loader: () => loader$2
10497
11221
  });
10498
- async function loader$1({ context }) {
11222
+ async function loader$2({ context }) {
10499
11223
  const { DB_URL, SCHEMA } = context.get(dbContext);
10500
11224
  return { queues: await getQueues(DB_URL, SCHEMA) };
10501
11225
  }
@@ -10545,7 +11269,7 @@ async function action({ request, context }) {
10545
11269
  const dbParam = new URL(request.url).searchParams.get("db");
10546
11270
  return redirect(dbParam ? `/queues/${encodeURIComponent(queueName.trim())}?db=${encodeURIComponent(dbParam)}` : `/queues/${encodeURIComponent(queueName.trim())}`);
10547
11271
  }
10548
- var ErrorBoundary$1 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
11272
+ var ErrorBoundary$2 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
10549
11273
  return /* @__PURE__ */ jsx(ErrorCard, {
10550
11274
  title: "Failed to load job sending page",
10551
11275
  backTo: {
@@ -10777,6 +11501,182 @@ var send_default = UNSAFE_withComponentProps(function SendJob({ loaderData }) {
10777
11501
  });
10778
11502
  });
10779
11503
  //#endregion
11504
+ //#region app/routes/migrations.tsx
11505
+ var migrations_exports = /* @__PURE__ */ __exportAll({
11506
+ ErrorBoundary: () => ErrorBoundary$1,
11507
+ default: () => migrations_default,
11508
+ loader: () => loader$1
11509
+ });
11510
+ var PAGE_SIZE = 50;
11511
+ var STATUS_ACCENT = {
11512
+ pending: "warning",
11513
+ in_progress: "primary",
11514
+ completed: "success",
11515
+ failed: "error"
11516
+ };
11517
+ async function loader$1({ request, context }) {
11518
+ const { DB_URL, SCHEMA } = context.get(dbContext);
11519
+ const url = new URL(request.url);
11520
+ const statusParam = url.searchParams.get("status");
11521
+ const statusFilter = isValidBamStatus(statusParam) ? statusParam : null;
11522
+ const page = parsePageNumber(url.searchParams.get("page"));
11523
+ const offset = (page - 1) * PAGE_SIZE;
11524
+ const [entries, totalCount, summary] = await Promise.all([
11525
+ getBamEntries(DB_URL, SCHEMA, {
11526
+ status: statusFilter,
11527
+ limit: PAGE_SIZE,
11528
+ offset
11529
+ }),
11530
+ getBamCount(DB_URL, SCHEMA, statusFilter),
11531
+ getBamStatusSummary(DB_URL, SCHEMA)
11532
+ ]);
11533
+ return {
11534
+ entries,
11535
+ summary,
11536
+ statusFilter,
11537
+ page,
11538
+ totalPages: Math.ceil(totalCount / PAGE_SIZE)
11539
+ };
11540
+ }
11541
+ var ErrorBoundary$1 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
11542
+ return /* @__PURE__ */ jsx(ErrorCard, {
11543
+ title: "Failed to load migrations",
11544
+ backTo: {
11545
+ href: "/",
11546
+ label: "Back to Dashboard"
11547
+ }
11548
+ });
11549
+ });
11550
+ var migrations_default = UNSAFE_withComponentProps(function Migrations({ loaderData }) {
11551
+ const { entries, summary, statusFilter, page, totalPages } = loaderData;
11552
+ const [searchParams, setSearchParams] = useSearchParams();
11553
+ const counts = countByStatus(summary);
11554
+ const handleFilterChange = (key, value) => {
11555
+ const params = new URLSearchParams(searchParams);
11556
+ if (value) params.set(key, value);
11557
+ else params.delete(key);
11558
+ params.delete("page");
11559
+ setSearchParams(params);
11560
+ };
11561
+ const handlePageChange = (newPage) => {
11562
+ const params = new URLSearchParams(searchParams);
11563
+ params.set("page", newPage.toString());
11564
+ setSearchParams(params);
11565
+ };
11566
+ return /* @__PURE__ */ jsxs("div", {
11567
+ className: "space-y-4",
11568
+ children: [
11569
+ /* @__PURE__ */ jsx(PageHeader, {
11570
+ title: "Migrations",
11571
+ subtitle: "Background async migrations (BAM) — schema changes such as concurrent index builds that run outside the install transaction"
11572
+ }),
11573
+ /* @__PURE__ */ jsx("div", {
11574
+ className: "grid grid-cols-2 gap-4 lg:grid-cols-4",
11575
+ children: BAM_STATUSES.map((status) => /* @__PURE__ */ jsx(StatCard, {
11576
+ label: BAM_STATUS_LABELS[status],
11577
+ value: counts[status].toLocaleString(),
11578
+ accent: counts[status] > 0 ? STATUS_ACCENT[status] : "neutral"
11579
+ }, status))
11580
+ }),
11581
+ /* @__PURE__ */ jsxs(Card, { children: [
11582
+ /* @__PURE__ */ jsxs(CardHeader, { children: [/* @__PURE__ */ jsx(CardTitle, { children: "Migration commands" }), /* @__PURE__ */ jsx(FilterSelect, {
11583
+ value: statusFilter,
11584
+ options: BAM_STATUS_OPTIONS,
11585
+ onChange: (value) => handleFilterChange("status", value)
11586
+ })] }),
11587
+ /* @__PURE__ */ jsx(CardContent, {
11588
+ className: "p-0",
11589
+ children: /* @__PURE__ */ jsxs(Table, { children: [/* @__PURE__ */ jsx(TableHeader, { children: /* @__PURE__ */ jsxs(TableRow, { children: [
11590
+ /* @__PURE__ */ jsx(TableHead, { children: "Name" }),
11591
+ /* @__PURE__ */ jsx(TableHead, {
11592
+ className: "text-right",
11593
+ children: "Ver"
11594
+ }),
11595
+ /* @__PURE__ */ jsx(TableHead, { children: "Status" }),
11596
+ /* @__PURE__ */ jsx(TableHead, { children: "Table" }),
11597
+ /* @__PURE__ */ jsx(TableHead, { children: "Created" }),
11598
+ /* @__PURE__ */ jsx(TableHead, { children: "Started" }),
11599
+ /* @__PURE__ */ jsx(TableHead, { children: "Completed" }),
11600
+ /* @__PURE__ */ jsx(TableHead, { children: "Command / Error" })
11601
+ ] }) }), /* @__PURE__ */ jsx(TableBody, { children: entries.length === 0 ? /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(TableCell, {
11602
+ className: "text-center text-[var(--text-tertiary)] py-8",
11603
+ colSpan: 8,
11604
+ children: statusFilter ? `No ${BAM_STATUS_LABELS[statusFilter].toLowerCase()} migrations found` : "No async migrations recorded."
11605
+ }) }) : entries.map((entry) => /* @__PURE__ */ jsxs(TableRow, { children: [
11606
+ /* @__PURE__ */ jsx(TableCell, {
11607
+ className: "text-[var(--text-primary)] font-medium",
11608
+ children: entry.name
11609
+ }),
11610
+ /* @__PURE__ */ jsx(TableCell, {
11611
+ className: "text-right pgb-num text-[var(--text-tertiary)]",
11612
+ children: entry.version
11613
+ }),
11614
+ /* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsx(Badge, {
11615
+ variant: BAM_STATUS_VARIANTS[entry.status],
11616
+ size: "sm",
11617
+ dot: true,
11618
+ children: BAM_STATUS_LABELS[entry.status]
11619
+ }) }),
11620
+ /* @__PURE__ */ jsxs(TableCell, {
11621
+ className: "font-mono text-xs text-[var(--text-secondary)] whitespace-nowrap",
11622
+ children: [entry.table, entry.queue ? /* @__PURE__ */ jsxs("span", {
11623
+ className: "text-[var(--text-tertiary)]",
11624
+ children: [" · ", entry.queue]
11625
+ }) : null]
11626
+ }),
11627
+ /* @__PURE__ */ jsx(TableCell, {
11628
+ className: "pgb-num text-[var(--text-tertiary)] whitespace-nowrap",
11629
+ children: formatTimestamp(entry.createdOn)
11630
+ }),
11631
+ /* @__PURE__ */ jsx(TableCell, {
11632
+ className: "pgb-num text-[var(--text-tertiary)] whitespace-nowrap",
11633
+ children: formatTimestamp(entry.startedOn)
11634
+ }),
11635
+ /* @__PURE__ */ jsx(TableCell, {
11636
+ className: "pgb-num text-[var(--text-tertiary)] whitespace-nowrap",
11637
+ children: formatTimestamp(entry.completedOn)
11638
+ }),
11639
+ /* @__PURE__ */ jsxs(TableCell, {
11640
+ className: "max-w-md",
11641
+ children: [entry.error ? /* @__PURE__ */ jsx("p", {
11642
+ className: "mb-1 font-mono text-xs text-[var(--error-600)] break-words whitespace-pre-wrap",
11643
+ children: entry.error
11644
+ }) : null, /* @__PURE__ */ jsxs("details", { children: [/* @__PURE__ */ jsx("summary", {
11645
+ className: "cursor-pointer text-xs text-[var(--text-tertiary)] select-none",
11646
+ children: "View command"
11647
+ }), /* @__PURE__ */ jsx("pre", {
11648
+ className: "mt-1 overflow-x-auto rounded bg-[var(--surface-sunken)] p-2 font-mono text-xs text-[var(--text-secondary)] whitespace-pre-wrap",
11649
+ children: entry.command
11650
+ })] })]
11651
+ })
11652
+ ] }, entry.id)) })] })
11653
+ }),
11654
+ /* @__PURE__ */ jsx(Pagination, {
11655
+ page,
11656
+ totalPages,
11657
+ hasNextPage: page < totalPages,
11658
+ hasPrevPage: page > 1,
11659
+ onPageChange: handlePageChange
11660
+ })
11661
+ ] })
11662
+ ]
11663
+ });
11664
+ });
11665
+ function formatTimestamp(value) {
11666
+ if (!value) return "—";
11667
+ return formatDateWithSeconds(new Date(value));
11668
+ }
11669
+ function countByStatus(summary) {
11670
+ const counts = {
11671
+ pending: 0,
11672
+ in_progress: 0,
11673
+ completed: 0,
11674
+ failed: 0
11675
+ };
11676
+ for (const row of summary) if (row.status in counts) counts[row.status] += row.count;
11677
+ return counts;
11678
+ }
11679
+ //#endregion
10780
11680
  //#region app/routes/warnings.tsx
10781
11681
  var warnings_exports = /* @__PURE__ */ __exportAll({
10782
11682
  ErrorBoundary: () => ErrorBoundary,
@@ -10888,8 +11788,8 @@ function WarningTypeBadge({ type }) {
10888
11788
  //#region \0virtual:react-router/server-manifest
10889
11789
  var server_manifest_default = {
10890
11790
  "entry": {
10891
- "module": "/assets/entry.client-DL_oPh96.js",
10892
- "imports": ["/assets/jsx-runtime-BgbGXvsu.js", "/assets/react-dom-QnGHOQwT.js"],
11791
+ "module": "/assets/entry.client-CqyjuPDB.js",
11792
+ "imports": ["/assets/jsx-runtime-RQyiN6Nr.js", "/assets/react-dom-D_m_Zgd3.js"],
10893
11793
  "css": []
10894
11794
  },
10895
11795
  "routes": {
@@ -10906,16 +11806,16 @@ var server_manifest_default = {
10906
11806
  "hasClientMiddleware": false,
10907
11807
  "hasDefaultExport": true,
10908
11808
  "hasErrorBoundary": true,
10909
- "module": "/assets/root-Df70GAY3.js",
11809
+ "module": "/assets/root-qxoeL6W3.js",
10910
11810
  "imports": [
10911
- "/assets/jsx-runtime-BgbGXvsu.js",
10912
- "/assets/react-dom-QnGHOQwT.js",
10913
- "/assets/db-link-BWWnHM0k.js",
10914
- "/assets/MenuTrigger-BhalG0aG.js",
10915
- "/assets/createLucideIcon-DVP_i62f.js",
10916
- "/assets/useOpenInteractionType-D3JsvupP.js"
11811
+ "/assets/jsx-runtime-RQyiN6Nr.js",
11812
+ "/assets/react-dom-D_m_Zgd3.js",
11813
+ "/assets/db-link-BajQ1v8I.js",
11814
+ "/assets/createLucideIcon-C-LI4enx.js",
11815
+ "/assets/MenuTrigger-BNvpjhsQ.js",
11816
+ "/assets/useOpenInteractionType-BQ1arb0B.js"
10917
11817
  ],
10918
- "css": ["/assets/root-C0MdPLOa.css"],
11818
+ "css": ["/assets/root-B0MB8jZH.css"],
10919
11819
  "clientActionModule": void 0,
10920
11820
  "clientLoaderModule": void 0,
10921
11821
  "clientMiddlewareModule": void 0,
@@ -10934,15 +11834,15 @@ var server_manifest_default = {
10934
11834
  "hasClientMiddleware": false,
10935
11835
  "hasDefaultExport": true,
10936
11836
  "hasErrorBoundary": true,
10937
- "module": "/assets/_index-D1-nZ7Th.js",
11837
+ "module": "/assets/_index-DqpFaaQw.js",
10938
11838
  "imports": [
10939
- "/assets/jsx-runtime-BgbGXvsu.js",
10940
- "/assets/db-link-BWWnHM0k.js",
10941
- "/assets/error-card-B0ANyjh3.js",
10942
- "/assets/badge-DCQvSdiR.js",
10943
- "/assets/button-BxLcuaPM.js",
10944
- "/assets/stat-card-DLtQnscf.js",
10945
- "/assets/table-DqqzSNik.js"
11839
+ "/assets/jsx-runtime-RQyiN6Nr.js",
11840
+ "/assets/db-link-BajQ1v8I.js",
11841
+ "/assets/stat-card-dyg1wY5p.js",
11842
+ "/assets/button-9NpSS9Ow.js",
11843
+ "/assets/badge-CMnQO7Lq.js",
11844
+ "/assets/table-Cz7ujmH_.js",
11845
+ "/assets/error-card-BH7i86fH.js"
10946
11846
  ],
10947
11847
  "css": [],
10948
11848
  "clientActionModule": void 0,
@@ -10963,24 +11863,24 @@ var server_manifest_default = {
10963
11863
  "hasClientMiddleware": false,
10964
11864
  "hasDefaultExport": true,
10965
11865
  "hasErrorBoundary": true,
10966
- "module": "/assets/jobs-D0a6Lwq0.js",
11866
+ "module": "/assets/jobs-CAd_qqLH.js",
10967
11867
  "imports": [
10968
- "/assets/jsx-runtime-BgbGXvsu.js",
10969
- "/assets/db-link-BWWnHM0k.js",
10970
- "/assets/error-card-B0ANyjh3.js",
10971
- "/assets/badge-DCQvSdiR.js",
10972
- "/assets/button-BxLcuaPM.js",
10973
- "/assets/filter-select--qLjbs9m.js",
10974
- "/assets/pagination-Bzx8wbXG.js",
10975
- "/assets/table-DqqzSNik.js",
10976
- "/assets/MenuTrigger-BhalG0aG.js",
10977
- "/assets/useOpenInteractionType-D3JsvupP.js",
10978
- "/assets/createLucideIcon-DVP_i62f.js",
10979
- "/assets/check-Ch42cXMT.js",
10980
- "/assets/chevron-down-Byq-CYG9.js",
10981
- "/assets/chevron-right-CKAGD7DJ.js",
10982
- "/assets/x-BPKZwOn9.js",
10983
- "/assets/react-dom-QnGHOQwT.js"
11868
+ "/assets/jsx-runtime-RQyiN6Nr.js",
11869
+ "/assets/db-link-BajQ1v8I.js",
11870
+ "/assets/createLucideIcon-C-LI4enx.js",
11871
+ "/assets/check-7jwc5sb1.js",
11872
+ "/assets/chevron-down-BFFjfYD4.js",
11873
+ "/assets/chevron-right-DGk5QFJF.js",
11874
+ "/assets/x-AhXI_F1j.js",
11875
+ "/assets/MenuTrigger-BNvpjhsQ.js",
11876
+ "/assets/useOpenInteractionType-BQ1arb0B.js",
11877
+ "/assets/button-9NpSS9Ow.js",
11878
+ "/assets/badge-CMnQO7Lq.js",
11879
+ "/assets/table-Cz7ujmH_.js",
11880
+ "/assets/error-card-BH7i86fH.js",
11881
+ "/assets/pagination-C-ohiBmY.js",
11882
+ "/assets/filter-select-Bn_oSiip.js",
11883
+ "/assets/react-dom-D_m_Zgd3.js"
10984
11884
  ],
10985
11885
  "css": [],
10986
11886
  "clientActionModule": void 0,
@@ -11001,14 +11901,14 @@ var server_manifest_default = {
11001
11901
  "hasClientMiddleware": false,
11002
11902
  "hasDefaultExport": true,
11003
11903
  "hasErrorBoundary": true,
11004
- "module": "/assets/queues._index-D8903DTa.js",
11904
+ "module": "/assets/queues._index-8YriSqbQ.js",
11005
11905
  "imports": [
11006
- "/assets/jsx-runtime-BgbGXvsu.js",
11007
- "/assets/db-link-BWWnHM0k.js",
11008
- "/assets/badge-DCQvSdiR.js",
11009
- "/assets/button-BxLcuaPM.js",
11010
- "/assets/filter-select--qLjbs9m.js",
11011
- "/assets/table-DqqzSNik.js"
11906
+ "/assets/jsx-runtime-RQyiN6Nr.js",
11907
+ "/assets/db-link-BajQ1v8I.js",
11908
+ "/assets/button-9NpSS9Ow.js",
11909
+ "/assets/badge-CMnQO7Lq.js",
11910
+ "/assets/table-Cz7ujmH_.js",
11911
+ "/assets/filter-select-Bn_oSiip.js"
11012
11912
  ],
11013
11913
  "css": [],
11014
11914
  "clientActionModule": void 0,
@@ -11029,14 +11929,14 @@ var server_manifest_default = {
11029
11929
  "hasClientMiddleware": false,
11030
11930
  "hasDefaultExport": true,
11031
11931
  "hasErrorBoundary": true,
11032
- "module": "/assets/queues.create-CMqQVLup.js",
11932
+ "module": "/assets/queues.create-DsY0Sc19.js",
11033
11933
  "imports": [
11034
- "/assets/jsx-runtime-BgbGXvsu.js",
11035
- "/assets/db-link-BWWnHM0k.js",
11036
- "/assets/error-card-B0ANyjh3.js",
11037
- "/assets/button-BxLcuaPM.js",
11038
- "/assets/chevron-down-Byq-CYG9.js",
11039
- "/assets/createLucideIcon-DVP_i62f.js"
11934
+ "/assets/jsx-runtime-RQyiN6Nr.js",
11935
+ "/assets/db-link-BajQ1v8I.js",
11936
+ "/assets/chevron-down-BFFjfYD4.js",
11937
+ "/assets/button-9NpSS9Ow.js",
11938
+ "/assets/error-card-BH7i86fH.js",
11939
+ "/assets/createLucideIcon-C-LI4enx.js"
11040
11940
  ],
11041
11941
  "css": [],
11042
11942
  "clientActionModule": void 0,
@@ -11057,25 +11957,25 @@ var server_manifest_default = {
11057
11957
  "hasClientMiddleware": false,
11058
11958
  "hasDefaultExport": true,
11059
11959
  "hasErrorBoundary": true,
11060
- "module": "/assets/queues._name-BVt_4pav.js",
11960
+ "module": "/assets/queues._name-Cb17IB2u.js",
11061
11961
  "imports": [
11062
- "/assets/jsx-runtime-BgbGXvsu.js",
11063
- "/assets/db-link-BWWnHM0k.js",
11064
- "/assets/error-card-B0ANyjh3.js",
11065
- "/assets/badge-DCQvSdiR.js",
11066
- "/assets/button-BxLcuaPM.js",
11067
- "/assets/dialog-Bik519zD.js",
11068
- "/assets/filter-select--qLjbs9m.js",
11069
- "/assets/pagination-Bzx8wbXG.js",
11070
- "/assets/stat-card-DLtQnscf.js",
11071
- "/assets/table-DqqzSNik.js",
11072
- "/assets/MenuTrigger-BhalG0aG.js",
11073
- "/assets/createLucideIcon-DVP_i62f.js",
11074
- "/assets/chevron-down-Byq-CYG9.js",
11075
- "/assets/chevron-right-CKAGD7DJ.js",
11076
- "/assets/useOpenInteractionType-D3JsvupP.js",
11077
- "/assets/x-BPKZwOn9.js",
11078
- "/assets/react-dom-QnGHOQwT.js"
11962
+ "/assets/jsx-runtime-RQyiN6Nr.js",
11963
+ "/assets/db-link-BajQ1v8I.js",
11964
+ "/assets/createLucideIcon-C-LI4enx.js",
11965
+ "/assets/chevron-down-BFFjfYD4.js",
11966
+ "/assets/chevron-right-DGk5QFJF.js",
11967
+ "/assets/MenuTrigger-BNvpjhsQ.js",
11968
+ "/assets/dialog-D-oczDM2.js",
11969
+ "/assets/stat-card-dyg1wY5p.js",
11970
+ "/assets/button-9NpSS9Ow.js",
11971
+ "/assets/badge-CMnQO7Lq.js",
11972
+ "/assets/table-Cz7ujmH_.js",
11973
+ "/assets/error-card-BH7i86fH.js",
11974
+ "/assets/pagination-C-ohiBmY.js",
11975
+ "/assets/filter-select-Bn_oSiip.js",
11976
+ "/assets/react-dom-D_m_Zgd3.js",
11977
+ "/assets/useOpenInteractionType-BQ1arb0B.js",
11978
+ "/assets/x-AhXI_F1j.js"
11079
11979
  ],
11080
11980
  "css": [],
11081
11981
  "clientActionModule": void 0,
@@ -11096,19 +11996,19 @@ var server_manifest_default = {
11096
11996
  "hasClientMiddleware": false,
11097
11997
  "hasDefaultExport": true,
11098
11998
  "hasErrorBoundary": true,
11099
- "module": "/assets/queues._name.jobs._jobId-BkG9y75k.js",
11999
+ "module": "/assets/queues._name.jobs._jobId-Bkv8POBj.js",
11100
12000
  "imports": [
11101
- "/assets/jsx-runtime-BgbGXvsu.js",
11102
- "/assets/db-link-BWWnHM0k.js",
11103
- "/assets/error-card-B0ANyjh3.js",
11104
- "/assets/badge-DCQvSdiR.js",
11105
- "/assets/button-BxLcuaPM.js",
11106
- "/assets/dialog-Bik519zD.js",
11107
- "/assets/createLucideIcon-DVP_i62f.js",
11108
- "/assets/check-Ch42cXMT.js",
11109
- "/assets/useOpenInteractionType-D3JsvupP.js",
11110
- "/assets/x-BPKZwOn9.js",
11111
- "/assets/react-dom-QnGHOQwT.js"
12001
+ "/assets/jsx-runtime-RQyiN6Nr.js",
12002
+ "/assets/db-link-BajQ1v8I.js",
12003
+ "/assets/createLucideIcon-C-LI4enx.js",
12004
+ "/assets/check-7jwc5sb1.js",
12005
+ "/assets/dialog-D-oczDM2.js",
12006
+ "/assets/button-9NpSS9Ow.js",
12007
+ "/assets/badge-CMnQO7Lq.js",
12008
+ "/assets/error-card-BH7i86fH.js",
12009
+ "/assets/x-AhXI_F1j.js",
12010
+ "/assets/useOpenInteractionType-BQ1arb0B.js",
12011
+ "/assets/react-dom-D_m_Zgd3.js"
11112
12012
  ],
11113
12013
  "css": [],
11114
12014
  "clientActionModule": void 0,
@@ -11129,15 +12029,15 @@ var server_manifest_default = {
11129
12029
  "hasClientMiddleware": false,
11130
12030
  "hasDefaultExport": true,
11131
12031
  "hasErrorBoundary": true,
11132
- "module": "/assets/schedules-DPXQoaEE.js",
12032
+ "module": "/assets/schedules-iYfIJxOD.js",
11133
12033
  "imports": [
11134
- "/assets/jsx-runtime-BgbGXvsu.js",
11135
- "/assets/db-link-BWWnHM0k.js",
11136
- "/assets/error-card-B0ANyjh3.js",
11137
- "/assets/badge-DCQvSdiR.js",
11138
- "/assets/button-BxLcuaPM.js",
11139
- "/assets/pagination-Bzx8wbXG.js",
11140
- "/assets/table-DqqzSNik.js"
12034
+ "/assets/jsx-runtime-RQyiN6Nr.js",
12035
+ "/assets/db-link-BajQ1v8I.js",
12036
+ "/assets/button-9NpSS9Ow.js",
12037
+ "/assets/badge-CMnQO7Lq.js",
12038
+ "/assets/table-Cz7ujmH_.js",
12039
+ "/assets/error-card-BH7i86fH.js",
12040
+ "/assets/pagination-C-ohiBmY.js"
11141
12041
  ],
11142
12042
  "css": [],
11143
12043
  "clientActionModule": void 0,
@@ -11158,17 +12058,17 @@ var server_manifest_default = {
11158
12058
  "hasClientMiddleware": false,
11159
12059
  "hasDefaultExport": true,
11160
12060
  "hasErrorBoundary": true,
11161
- "module": "/assets/schedules._name._key-B_luxy1w.js",
12061
+ "module": "/assets/schedules._name._key-CJVu73XY.js",
11162
12062
  "imports": [
11163
- "/assets/jsx-runtime-BgbGXvsu.js",
11164
- "/assets/db-link-BWWnHM0k.js",
11165
- "/assets/error-card-B0ANyjh3.js",
11166
- "/assets/button-BxLcuaPM.js",
11167
- "/assets/dialog-Bik519zD.js",
11168
- "/assets/useOpenInteractionType-D3JsvupP.js",
11169
- "/assets/x-BPKZwOn9.js",
11170
- "/assets/react-dom-QnGHOQwT.js",
11171
- "/assets/createLucideIcon-DVP_i62f.js"
12063
+ "/assets/jsx-runtime-RQyiN6Nr.js",
12064
+ "/assets/db-link-BajQ1v8I.js",
12065
+ "/assets/dialog-D-oczDM2.js",
12066
+ "/assets/button-9NpSS9Ow.js",
12067
+ "/assets/error-card-BH7i86fH.js",
12068
+ "/assets/x-AhXI_F1j.js",
12069
+ "/assets/useOpenInteractionType-BQ1arb0B.js",
12070
+ "/assets/createLucideIcon-C-LI4enx.js",
12071
+ "/assets/react-dom-D_m_Zgd3.js"
11172
12072
  ],
11173
12073
  "css": [],
11174
12074
  "clientActionModule": void 0,
@@ -11189,12 +12089,12 @@ var server_manifest_default = {
11189
12089
  "hasClientMiddleware": false,
11190
12090
  "hasDefaultExport": true,
11191
12091
  "hasErrorBoundary": true,
11192
- "module": "/assets/schedules.new-BQV7GWzs.js",
12092
+ "module": "/assets/schedules.new-Cq0Mxa7G.js",
11193
12093
  "imports": [
11194
- "/assets/jsx-runtime-BgbGXvsu.js",
11195
- "/assets/db-link-BWWnHM0k.js",
11196
- "/assets/error-card-B0ANyjh3.js",
11197
- "/assets/button-BxLcuaPM.js"
12094
+ "/assets/jsx-runtime-RQyiN6Nr.js",
12095
+ "/assets/db-link-BajQ1v8I.js",
12096
+ "/assets/button-9NpSS9Ow.js",
12097
+ "/assets/error-card-BH7i86fH.js"
11198
12098
  ],
11199
12099
  "css": [],
11200
12100
  "clientActionModule": void 0,
@@ -11215,12 +12115,43 @@ var server_manifest_default = {
11215
12115
  "hasClientMiddleware": false,
11216
12116
  "hasDefaultExport": true,
11217
12117
  "hasErrorBoundary": true,
11218
- "module": "/assets/send-DJBsfnx_.js",
12118
+ "module": "/assets/send-8X9ZisG-.js",
12119
+ "imports": [
12120
+ "/assets/jsx-runtime-RQyiN6Nr.js",
12121
+ "/assets/db-link-BajQ1v8I.js",
12122
+ "/assets/button-9NpSS9Ow.js",
12123
+ "/assets/error-card-BH7i86fH.js"
12124
+ ],
12125
+ "css": [],
12126
+ "clientActionModule": void 0,
12127
+ "clientLoaderModule": void 0,
12128
+ "clientMiddlewareModule": void 0,
12129
+ "hydrateFallbackModule": void 0
12130
+ },
12131
+ "routes/migrations": {
12132
+ "id": "routes/migrations",
12133
+ "parentId": "root",
12134
+ "path": "migrations",
12135
+ "index": void 0,
12136
+ "caseSensitive": void 0,
12137
+ "hasAction": false,
12138
+ "hasLoader": true,
12139
+ "hasClientAction": false,
12140
+ "hasClientLoader": false,
12141
+ "hasClientMiddleware": false,
12142
+ "hasDefaultExport": true,
12143
+ "hasErrorBoundary": true,
12144
+ "module": "/assets/migrations-D5l0n4Jn.js",
11219
12145
  "imports": [
11220
- "/assets/jsx-runtime-BgbGXvsu.js",
11221
- "/assets/db-link-BWWnHM0k.js",
11222
- "/assets/error-card-B0ANyjh3.js",
11223
- "/assets/button-BxLcuaPM.js"
12146
+ "/assets/jsx-runtime-RQyiN6Nr.js",
12147
+ "/assets/db-link-BajQ1v8I.js",
12148
+ "/assets/stat-card-dyg1wY5p.js",
12149
+ "/assets/button-9NpSS9Ow.js",
12150
+ "/assets/badge-CMnQO7Lq.js",
12151
+ "/assets/table-Cz7ujmH_.js",
12152
+ "/assets/error-card-BH7i86fH.js",
12153
+ "/assets/pagination-C-ohiBmY.js",
12154
+ "/assets/filter-select-Bn_oSiip.js"
11224
12155
  ],
11225
12156
  "css": [],
11226
12157
  "clientActionModule": void 0,
@@ -11241,16 +12172,16 @@ var server_manifest_default = {
11241
12172
  "hasClientMiddleware": false,
11242
12173
  "hasDefaultExport": true,
11243
12174
  "hasErrorBoundary": true,
11244
- "module": "/assets/warnings-CHKaRfIW.js",
12175
+ "module": "/assets/warnings-C1R_RzIe.js",
11245
12176
  "imports": [
11246
- "/assets/jsx-runtime-BgbGXvsu.js",
11247
- "/assets/db-link-BWWnHM0k.js",
11248
- "/assets/error-card-B0ANyjh3.js",
11249
- "/assets/badge-DCQvSdiR.js",
11250
- "/assets/button-BxLcuaPM.js",
11251
- "/assets/filter-select--qLjbs9m.js",
11252
- "/assets/pagination-Bzx8wbXG.js",
11253
- "/assets/table-DqqzSNik.js"
12177
+ "/assets/jsx-runtime-RQyiN6Nr.js",
12178
+ "/assets/db-link-BajQ1v8I.js",
12179
+ "/assets/button-9NpSS9Ow.js",
12180
+ "/assets/badge-CMnQO7Lq.js",
12181
+ "/assets/table-Cz7ujmH_.js",
12182
+ "/assets/error-card-BH7i86fH.js",
12183
+ "/assets/pagination-C-ohiBmY.js",
12184
+ "/assets/filter-select-Bn_oSiip.js"
11254
12185
  ],
11255
12186
  "css": [],
11256
12187
  "clientActionModule": void 0,
@@ -11259,8 +12190,8 @@ var server_manifest_default = {
11259
12190
  "hydrateFallbackModule": void 0
11260
12191
  }
11261
12192
  },
11262
- "url": "/assets/manifest-ef81a0f9.js",
11263
- "version": "ef81a0f9",
12193
+ "url": "/assets/manifest-27e8e133.js",
12194
+ "version": "27e8e133",
11264
12195
  "sri": void 0
11265
12196
  };
11266
12197
  //#endregion
@@ -11366,6 +12297,14 @@ var routes = {
11366
12297
  caseSensitive: void 0,
11367
12298
  module: send_exports
11368
12299
  },
12300
+ "routes/migrations": {
12301
+ id: "routes/migrations",
12302
+ parentId: "root",
12303
+ path: "migrations",
12304
+ index: void 0,
12305
+ caseSensitive: void 0,
12306
+ module: migrations_exports
12307
+ },
11369
12308
  "routes/warnings": {
11370
12309
  id: "routes/warnings",
11371
12310
  parentId: "root",