@pg-boss/dashboard 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +1 -1
  2. package/build/client/assets/MenuTrigger-BNvpjhsQ.js +1 -0
  3. package/build/client/assets/_index-DqpFaaQw.js +1 -0
  4. package/build/client/assets/{badge-DFReduIj.js → badge-CMnQO7Lq.js} +1 -1
  5. package/build/client/assets/button-9NpSS9Ow.js +1 -0
  6. package/build/client/assets/check-7jwc5sb1.js +1 -0
  7. package/build/client/assets/{chevron-down-C8oENez_.js → chevron-down-BFFjfYD4.js} +1 -1
  8. package/build/client/assets/chevron-right-DGk5QFJF.js +1 -0
  9. package/build/client/assets/createLucideIcon-C-LI4enx.js +1 -0
  10. package/build/client/assets/db-link-BajQ1v8I.js +1 -0
  11. package/build/client/assets/dialog-D-oczDM2.js +1 -0
  12. package/build/client/assets/entry.client-CqyjuPDB.js +9 -0
  13. package/build/client/assets/{error-card-2aexlkmv.js → error-card-BH7i86fH.js} +1 -1
  14. package/build/client/assets/{filter-select-Dln9CAM3.js → filter-select-Bn_oSiip.js} +1 -1
  15. package/build/client/assets/jobs-CAd_qqLH.js +1 -0
  16. package/build/client/assets/jsx-runtime-RQyiN6Nr.js +16 -0
  17. package/build/client/assets/manifest-27e8e133.js +1 -0
  18. package/build/client/assets/migrations-D5l0n4Jn.js +1 -0
  19. package/build/client/assets/{pagination-5gEldBFM.js → pagination-C-ohiBmY.js} +1 -1
  20. package/build/client/assets/queues._index-8YriSqbQ.js +1 -0
  21. package/build/client/assets/queues._name-Cb17IB2u.js +1 -0
  22. package/build/client/assets/queues._name.jobs._jobId-Bkv8POBj.js +1 -0
  23. package/build/client/assets/queues.create-DsY0Sc19.js +1 -0
  24. package/build/client/assets/react-dom-D_m_Zgd3.js +1 -0
  25. package/build/client/assets/root-B0MB8jZH.css +2 -0
  26. package/build/client/assets/root-qxoeL6W3.js +40 -0
  27. package/build/client/assets/{schedules-CE-hWC9e.js → schedules-iYfIJxOD.js} +1 -1
  28. package/build/client/assets/schedules._name._key-CJVu73XY.js +1 -0
  29. package/build/client/assets/schedules.new-Cq0Mxa7G.js +1 -0
  30. package/build/client/assets/send-8X9ZisG-.js +1 -0
  31. package/build/client/assets/{stat-card-y2feeHt0.js → stat-card-dyg1wY5p.js} +1 -1
  32. package/build/client/assets/{table-B5BEGV9S.js → table-Cz7ujmH_.js} +1 -1
  33. package/build/client/assets/useOpenInteractionType-BQ1arb0B.js +1 -0
  34. package/build/client/assets/warnings-C1R_RzIe.js +1 -0
  35. package/build/client/assets/x-AhXI_F1j.js +1 -0
  36. package/build/server/index.js +2569 -943
  37. package/package.json +19 -16
  38. package/build/client/assets/MenuTrigger-CkqlwCsV.js +0 -1
  39. package/build/client/assets/_index-CNX0dPQi.js +0 -1
  40. package/build/client/assets/button-CafwM-5D.js +0 -1
  41. package/build/client/assets/check-BePyOaOM.js +0 -1
  42. package/build/client/assets/chevron-right-B0sTJmdc.js +0 -1
  43. package/build/client/assets/createLucideIcon-oZ0wXCaF.js +0 -1
  44. package/build/client/assets/db-link-DISwKBYZ.js +0 -1
  45. package/build/client/assets/dialog-IBPuwpas.js +0 -1
  46. package/build/client/assets/entry.client-6KjasuHH.js +0 -9
  47. package/build/client/assets/jobs-BjoGjnb0.js +0 -1
  48. package/build/client/assets/jsx-runtime-DcdjQ3vN.js +0 -26
  49. package/build/client/assets/manifest-e6a94de0.js +0 -1
  50. package/build/client/assets/queues._index-BM3eKnSr.js +0 -1
  51. package/build/client/assets/queues._name-DSOGXBen.js +0 -1
  52. package/build/client/assets/queues._name.jobs._jobId-DKGu8NkE.js +0 -1
  53. package/build/client/assets/queues.create-BVR72i85.js +0 -1
  54. package/build/client/assets/react-dom-DQHA9Ik4.js +0 -1
  55. package/build/client/assets/root-Cvarn0sH.js +0 -40
  56. package/build/client/assets/root-D24FtLBP.css +0 -2
  57. package/build/client/assets/schedules._name._key-DT8Kg4jU.js +0 -1
  58. package/build/client/assets/schedules.new-bKRusXXO.js +0 -1
  59. package/build/client/assets/send-BaWw8x7J.js +0 -1
  60. package/build/client/assets/useOpenInteractionType-BGObzouY.js +0 -1
  61. package/build/client/assets/warnings-lzi34PdC.js +0 -1
  62. package/build/client/assets/x-EA6eZ1AP.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;
@@ -1434,6 +1485,7 @@ if (typeof process !== "undefined") {
1434
1485
  }
1435
1486
  //#endregion
1436
1487
  //#region ../../src/plans.ts
1488
+ var PG_ERROR = { divisionByZero: "22012" };
1437
1489
  var DEFAULT_SCHEMA = "pgboss";
1438
1490
  var MIGRATE_RACE_MESSAGE = "division by zero";
1439
1491
  var CREATE_RACE_MESSAGE = "already exists";
@@ -1469,6 +1521,10 @@ var QUEUE_DEFAULTS = {
1469
1521
  };
1470
1522
  var COMMON_JOB_TABLE = "job_common";
1471
1523
  function create(schema, version, options) {
1524
+ const noPartitioning = options?.noTablePartitioning ?? false;
1525
+ const noDeferrable = options?.noDeferrableConstraints ?? false;
1526
+ const noLocks = options?.noAdvisoryLocks ?? false;
1527
+ const noCovering = options?.noCoveringIndexes ?? false;
1472
1528
  return locked(schema, [
1473
1529
  options?.createSchema ? createSchema(schema) : "",
1474
1530
  createEnumJobState(schema),
@@ -1477,18 +1533,20 @@ function create(schema, version, options) {
1477
1533
  createTableSchedule(schema),
1478
1534
  createTableSubscription(schema),
1479
1535
  createTableBam(schema),
1480
- jobTableFormatFunction(schema),
1481
- jobTableRunFunction(schema),
1482
- jobTableRunAsyncFunction(schema),
1483
- createTableJob(schema),
1536
+ noPartitioning ? "" : jobTableFormatFunction(schema),
1537
+ noPartitioning ? "" : jobTableRunFunction(schema),
1538
+ noPartitioning ? "" : jobTableRunAsyncFunction(schema),
1539
+ createTableJob(schema, noPartitioning),
1484
1540
  createPrimaryKeyJob(schema),
1485
- createTableJobCommon(schema),
1541
+ noPartitioning ? createTableJobIndexes(schema, noDeferrable, noCovering) : createTableJobCommon(schema),
1486
1542
  createTableWarning(schema),
1487
1543
  createIndexWarning(schema),
1488
- createQueueFunction(schema),
1489
- deleteQueueFunction(schema),
1544
+ createTableJobDependency(schema),
1545
+ createIndexJobDependencyParent(schema),
1546
+ createQueueFunction(schema, noPartitioning),
1547
+ deleteQueueFunction(schema, noPartitioning),
1490
1548
  insertVersion(schema, version)
1491
- ]);
1549
+ ], void 0, noLocks);
1492
1550
  }
1493
1551
  function createSchema(schema) {
1494
1552
  return `CREATE SCHEMA IF NOT EXISTS ${schema}`;
@@ -1531,10 +1589,13 @@ function createTableQueue(schema) {
1531
1589
  table_name text NOT NULL,
1532
1590
  deferred_count int NOT NULL default 0,
1533
1591
  queued_count int NOT NULL default 0,
1592
+ ready_count int NOT NULL default 0,
1534
1593
  warning_queued int NOT NULL default 0,
1535
1594
  active_count int NOT NULL default 0,
1595
+ failed_count int NOT NULL default 0,
1536
1596
  total_count int NOT NULL default 0,
1537
1597
  heartbeat_seconds int,
1598
+ notify bool NOT NULL DEFAULT false,
1538
1599
  singletons_active text[],
1539
1600
  monitor_on timestamp with time zone,
1540
1601
  maintain_on timestamp with time zone,
@@ -1601,6 +1662,20 @@ function createTableWarning(schema) {
1601
1662
  function createIndexWarning(schema) {
1602
1663
  return `CREATE INDEX warning_i1 ON ${schema}.warning (created_on DESC)`;
1603
1664
  }
1665
+ function createTableJobDependency(schema) {
1666
+ return `
1667
+ CREATE TABLE ${schema}.job_dependency (
1668
+ child_name text NOT NULL,
1669
+ child_id uuid NOT NULL,
1670
+ parent_name text NOT NULL,
1671
+ parent_id uuid NOT NULL,
1672
+ PRIMARY KEY (child_name, child_id, parent_name, parent_id)
1673
+ )
1674
+ `;
1675
+ }
1676
+ function createIndexJobDependencyParent(schema) {
1677
+ return `CREATE INDEX IF NOT EXISTS job_dep_parent_idx ON ${schema}.job_dependency (parent_name, parent_id)`;
1678
+ }
1604
1679
  function jobTableFormatFunction(schema) {
1605
1680
  return `
1606
1681
  CREATE FUNCTION ${schema}.job_table_format(command text, table_name text)
@@ -1691,7 +1766,8 @@ function jobTableRunAsyncFunction(schema) {
1691
1766
  LANGUAGE plpgsql;
1692
1767
  `;
1693
1768
  }
1694
- function createTableJob(schema) {
1769
+ function createTableJob(schema, noPartitioning = false) {
1770
+ const partitionClause = noPartitioning ? "" : "PARTITION BY LIST (name)";
1695
1771
  return `
1696
1772
  CREATE TABLE ${schema}.job (
1697
1773
  id uuid not null default gen_random_uuid(),
@@ -1719,8 +1795,11 @@ function createTableJob(schema) {
1719
1795
  dead_letter text,
1720
1796
  policy text,
1721
1797
  heartbeat_on timestamp with time zone,
1722
- heartbeat_seconds int
1723
- ) PARTITION BY LIST (name)
1798
+ heartbeat_seconds int,
1799
+ blocked boolean not null default false,
1800
+ blocking boolean not null default false,
1801
+ pending_dependencies int not null default 0
1802
+ ) ${partitionClause}
1724
1803
  `;
1725
1804
  }
1726
1805
  var JOB_COLUMNS_MIN = "id, name, data, expire_seconds as \"expireInSeconds\", heartbeat_seconds as \"heartbeatSeconds\", group_id as \"groupId\", group_tier as \"groupTier\"";
@@ -1743,6 +1822,9 @@ var JOB_COLUMNS_ALL = `${JOB_COLUMNS_MIN},
1743
1822
  completed_on as "completedOn",
1744
1823
  keep_until as "keepUntil",
1745
1824
  dead_letter as "deadLetter",
1825
+ blocked,
1826
+ blocking,
1827
+ pending_dependencies as "pendingDependencies",
1746
1828
  output
1747
1829
  `;
1748
1830
  function createTableJobCommon(schema) {
@@ -1765,7 +1847,64 @@ function createTableJobCommon(schema) {
1765
1847
  ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.${COMMON_JOB_TABLE} DEFAULT;
1766
1848
  `;
1767
1849
  }
1768
- function createQueueFunction(schema) {
1850
+ function createTableJobIndexes(schema, noDeferrableConstraints = false, noCoveringIndex = false) {
1851
+ return `
1852
+ ${createQueueForeignKeyJob(schema, noDeferrableConstraints)};
1853
+ ${createQueueForeignKeyJobDeadLetter(schema, noDeferrableConstraints)};
1854
+ ${createIndexJobPolicyShort(schema)};
1855
+ ${createIndexJobPolicySingleton(schema)};
1856
+ ${createIndexJobPolicyStately(schema)};
1857
+ ${createIndexJobPolicyExclusive(schema)};
1858
+ ${createIndexJobPolicyKeyStrictFifo(schema)};
1859
+ ${createCheckConstraintKeyStrictFifo(schema)};
1860
+ ${createIndexJobThrottle(schema)};
1861
+ ${createIndexJobFetch(schema, noCoveringIndex)};
1862
+ ${createIndexJobGroupConcurrency(schema)};
1863
+ `;
1864
+ }
1865
+ function createQueueFunction(schema, noPartitioning = false) {
1866
+ if (noPartitioning) return `
1867
+ CREATE FUNCTION ${schema}.create_queue(queue_name text, options jsonb)
1868
+ RETURNS VOID AS
1869
+ $$
1870
+ BEGIN
1871
+ INSERT INTO ${schema}.queue (
1872
+ name,
1873
+ policy,
1874
+ retry_limit,
1875
+ retry_delay,
1876
+ retry_backoff,
1877
+ retry_delay_max,
1878
+ expire_seconds,
1879
+ retention_seconds,
1880
+ deletion_seconds,
1881
+ warning_queued,
1882
+ dead_letter,
1883
+ partition,
1884
+ table_name,
1885
+ heartbeat_seconds
1886
+ )
1887
+ VALUES (
1888
+ queue_name,
1889
+ options->>'policy',
1890
+ COALESCE((options->>'retryLimit')::int, ${QUEUE_DEFAULTS.retry_limit}),
1891
+ COALESCE((options->>'retryDelay')::int, ${QUEUE_DEFAULTS.retry_delay}),
1892
+ COALESCE((options->>'retryBackoff')::bool, ${QUEUE_DEFAULTS.retry_backoff}),
1893
+ (options->>'retryDelayMax')::int,
1894
+ COALESCE((options->>'expireInSeconds')::int, ${QUEUE_DEFAULTS.expire_seconds}),
1895
+ COALESCE((options->>'retentionSeconds')::int, ${QUEUE_DEFAULTS.retention_seconds}),
1896
+ COALESCE((options->>'deleteAfterSeconds')::int, ${QUEUE_DEFAULTS.deletion_seconds}),
1897
+ COALESCE((options->>'warningQueueSize')::int, ${QUEUE_DEFAULTS.warning_queued}),
1898
+ options->>'deadLetter',
1899
+ false,
1900
+ 'job',
1901
+ (options->>'heartbeatSeconds')::int
1902
+ )
1903
+ ON CONFLICT DO NOTHING;
1904
+ END;
1905
+ $$
1906
+ LANGUAGE plpgsql;
1907
+ `;
1769
1908
  return `
1770
1909
  CREATE FUNCTION ${schema}.create_queue(queue_name text, options jsonb)
1771
1910
  RETURNS VOID AS
@@ -1793,7 +1932,8 @@ function createQueueFunction(schema) {
1793
1932
  dead_letter,
1794
1933
  partition,
1795
1934
  table_name,
1796
- heartbeat_seconds
1935
+ heartbeat_seconds,
1936
+ notify
1797
1937
  )
1798
1938
  VALUES (
1799
1939
  queue_name,
@@ -1809,7 +1949,8 @@ function createQueueFunction(schema) {
1809
1949
  options->>'deadLetter',
1810
1950
  COALESCE((options->>'partition')::bool, ${QUEUE_DEFAULTS.partition}),
1811
1951
  tablename,
1812
- (options->>'heartbeatSeconds')::int
1952
+ (options->>'heartbeatSeconds')::int,
1953
+ COALESCE((options->>'notify')::bool, false)
1813
1954
  )
1814
1955
  ON CONFLICT DO NOTHING
1815
1956
  RETURNING created_on
@@ -1850,15 +1991,8 @@ function createQueueFunction(schema) {
1850
1991
  LANGUAGE plpgsql;
1851
1992
  `;
1852
1993
  }
1853
- function deleteQueueFunction(schema) {
1854
- return `
1855
- CREATE FUNCTION ${schema}.delete_queue(queue_name text)
1856
- RETURNS VOID AS
1857
- $$
1858
- DECLARE
1859
- v_table varchar;
1860
- v_partition bool;
1861
- BEGIN
1994
+ function deleteQueueFunction(schema, noPartitioning = false) {
1995
+ const deleteJobsSql = noPartitioning ? `DELETE FROM ${schema}.job WHERE name = queue_name;` : `
1862
1996
  SELECT table_name, partition
1863
1997
  FROM ${schema}.queue
1864
1998
  WHERE name = queue_name
@@ -1869,27 +2003,42 @@ function deleteQueueFunction(schema) {
1869
2003
  ELSE
1870
2004
  EXECUTE format('DELETE FROM ${schema}.%I WHERE name = %L', v_table, queue_name);
1871
2005
  END IF;
1872
-
2006
+ `;
2007
+ return `
2008
+ CREATE FUNCTION ${schema}.delete_queue(queue_name text)
2009
+ RETURNS VOID AS
2010
+ $$${noPartitioning ? "" : `
2011
+ DECLARE
2012
+ v_table varchar;
2013
+ v_partition bool;`}
2014
+ BEGIN
2015
+ ${deleteJobsSql}
1873
2016
  DELETE FROM ${schema}.queue WHERE name = queue_name;
1874
2017
  END;
1875
2018
  $$
1876
2019
  LANGUAGE plpgsql;
1877
2020
  `;
1878
2021
  }
1879
- function createQueue$1(schema, name, options) {
1880
- return locked(schema, `SELECT ${schema}.create_queue('${name}', '${JSON.stringify(options)}'::jsonb)`, "create-queue");
2022
+ function createQueue$1(schema, name, options, noAdvisoryLocks) {
2023
+ return locked(schema, `SELECT ${schema}.create_queue('${name}', '${JSON.stringify(options)}'::jsonb)`, "create-queue", noAdvisoryLocks);
2024
+ }
2025
+ function notifyChannelSql(schema) {
2026
+ return `('pgboss_' || left(encode(sha224('${schema}'::bytea), 'hex'), 24))`;
1881
2027
  }
1882
- function deleteQueue(schema, name) {
1883
- return locked(schema, `SELECT ${schema}.delete_queue('${name}')`, "delete-queue");
2028
+ function notifyQueue(schema, name) {
2029
+ return `SELECT pg_notify(${notifyChannelSql(schema)}, '${name}')`;
2030
+ }
2031
+ function deleteQueue(schema, name, noAdvisoryLocks) {
2032
+ return locked(schema, `SELECT ${schema}.delete_queue('${name}')`, "delete-queue", noAdvisoryLocks);
1884
2033
  }
1885
2034
  function createPrimaryKeyJob(schema) {
1886
2035
  return `ALTER TABLE ${schema}.job ADD PRIMARY KEY (name, id)`;
1887
2036
  }
1888
- function createQueueForeignKeyJob(schema) {
1889
- return `ALTER TABLE ${schema}.job ADD CONSTRAINT q_fkey FOREIGN KEY (name) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED`;
2037
+ function createQueueForeignKeyJob(schema, noPartitioning = false) {
2038
+ return `ALTER TABLE ${schema}.job ADD CONSTRAINT q_fkey FOREIGN KEY (name) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT${noPartitioning ? "" : " DEFERRABLE INITIALLY DEFERRED"}`;
1890
2039
  }
1891
- function createQueueForeignKeyJobDeadLetter(schema) {
1892
- return `ALTER TABLE ${schema}.job ADD CONSTRAINT dlq_fkey FOREIGN KEY (dead_letter) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED`;
2040
+ function createQueueForeignKeyJobDeadLetter(schema, noPartitioning = false) {
2041
+ return `ALTER TABLE ${schema}.job ADD CONSTRAINT dlq_fkey FOREIGN KEY (dead_letter) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT${noPartitioning ? "" : " DEFERRABLE INITIALLY DEFERRED"}`;
1893
2042
  }
1894
2043
  function createIndexJobPolicyShort(schema) {
1895
2044
  return `CREATE UNIQUE INDEX job_i1 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = '${JOB_STATES.created}' AND policy = '${QUEUE_POLICIES.short}'`;
@@ -1903,8 +2052,8 @@ function createIndexJobPolicyStately(schema) {
1903
2052
  function createIndexJobThrottle(schema) {
1904
2053
  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`;
1905
2054
  }
1906
- function createIndexJobFetch(schema) {
1907
- return `CREATE INDEX job_i5 ON ${schema}.job (name, start_after) INCLUDE (priority, created_on, id) WHERE state < '${JOB_STATES.active}'`;
2055
+ function createIndexJobFetch(schema, noCoveringIndex = false) {
2056
+ 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`;
1908
2057
  }
1909
2058
  function createIndexJobPolicyExclusive(schema) {
1910
2059
  return `CREATE UNIQUE INDEX job_i6 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state <= '${JOB_STATES.active}' AND policy = '${QUEUE_POLICIES.exclusive}'`;
@@ -1967,6 +2116,7 @@ function updateQueue(schema, { deadLetter } = {}) {
1967
2116
  heartbeat_seconds = CASE WHEN o.data ? 'heartbeatSeconds'
1968
2117
  THEN (o.data->>'heartbeatSeconds')::int
1969
2118
  ELSE heartbeat_seconds END,
2119
+ notify = COALESCE((o.data->>'notify')::bool, notify),
1970
2120
  ${deadLetter === void 0 ? "" : `dead_letter = CASE WHEN '${deadLetter}' IS DISTINCT FROM dead_letter THEN '${deadLetter}' ELSE dead_letter END,`}
1971
2121
  updated_on = now()
1972
2122
  FROM options o
@@ -1989,11 +2139,14 @@ function getQueues$1(schema, names) {
1989
2139
  q.deletion_seconds as "deleteAfterSeconds",
1990
2140
  q.partition,
1991
2141
  q.heartbeat_seconds as "heartbeatSeconds",
2142
+ q.notify,
1992
2143
  q.dead_letter as "deadLetter",
1993
2144
  q.deferred_count as "deferredCount",
1994
2145
  q.warning_queued as "warningQueueSize",
1995
2146
  q.queued_count as "queuedCount",
2147
+ q.ready_count as "readyCount",
1996
2148
  q.active_count as "activeCount",
2149
+ q.failed_count as "failedCount",
1997
2150
  q.total_count as "totalCount",
1998
2151
  q.singletons_active as "singletonsActive",
1999
2152
  q.table_name as "table",
@@ -2159,7 +2312,23 @@ function buildFetchParams(options) {
2159
2312
  maxPriorityParam
2160
2313
  };
2161
2314
  }
2162
- function fetchNextJob(options) {
2315
+ /**
2316
+ * Builds the fetch query for claiming jobs from the queue.
2317
+ *
2318
+ * With SKIP LOCKED (noSkipLocked=false, the default), uses SELECT FOR UPDATE SKIP
2319
+ * LOCKED, which lets multiple workers efficiently fetch different jobs simultaneously.
2320
+ *
2321
+ * With noSkipLocked=true, omits FOR UPDATE SKIP LOCKED and adds an additional state
2322
+ * check in the WHERE clause. This pattern works better with distributed databases like
2323
+ * CockroachDB where SKIP LOCKED has performance issues and can unexpectedly skip
2324
+ * unlocked rows.
2325
+ *
2326
+ * Trade-off when noSkipLocked is set: under high contention, workers may receive fewer
2327
+ * jobs per fetch as concurrent updates to the same rows will result in some workers
2328
+ * getting empty results. This is acceptable for job queues where processing time
2329
+ * exceeds fetch time.
2330
+ */
2331
+ function fetchNextJob(options, noSkipLocked = false) {
2163
2332
  const { schema, table, name, policy, limit, includeMetadata, priority = true, orderByCreatedOn = true, ignoreStartAfter = false, groupConcurrency, minPriority, maxPriority } = options;
2164
2333
  const singletonFetch = limit > 1 && (policy === QUEUE_POLICIES.singleton || policy === QUEUE_POLICIES.stately);
2165
2334
  const hasIgnoreSingletons = options.ignoreSingletons != null && options.ignoreSingletons.length > 0;
@@ -2180,9 +2349,11 @@ function fetchNextJob(options) {
2180
2349
  WHERE name = '${name}' AND state = '${JOB_STATES.active}' AND group_id IS NOT NULL
2181
2350
  GROUP BY group_id
2182
2351
  ), ` : "";
2352
+ const lockClause = noSkipLocked ? "" : "FOR UPDATE OF j SKIP LOCKED";
2183
2353
  const whereConditions = [
2184
2354
  `j.name = '${name}'`,
2185
2355
  `j.state < '${JOB_STATES.active}'`,
2356
+ "NOT j.blocked",
2186
2357
  !ignoreStartAfter ? "j.start_after < now()" : "",
2187
2358
  hasIgnoreSingletons ? `j.singleton_key <> ALL(${params.ignoreSingletonsParam})` : "",
2188
2359
  hasIgnoreGroups ? `(j.group_id IS NULL OR j.group_id <> ALL(${params.ignoreGroupsParam}))` : "",
@@ -2200,7 +2371,7 @@ function fetchNextJob(options) {
2200
2371
  WHERE ${whereConditions}
2201
2372
  ORDER BY ${priority ? "j.priority desc, " : ""}${orderByCreatedOn ? "j.created_on, " : ""}j.id
2202
2373
  LIMIT ${limit}
2203
- FOR UPDATE OF j SKIP LOCKED
2374
+ ${lockClause}
2204
2375
  )`;
2205
2376
  const singletonCte = singletonFetch ? `, singleton_ranking AS (
2206
2377
  SELECT id, ${hasGroupConcurrency ? "group_id, group_tier, " : ""}
@@ -2225,6 +2396,7 @@ function fetchNextJob(options) {
2225
2396
  OR (active_cnt + group_rn) <= ${hasTiers ? `COALESCE((${params.tiersParam} ->> group_tier)::int, ${params.defaultGroupLimitParam})` : params.defaultGroupLimitParam}
2226
2397
  )` : "";
2227
2398
  const finalCte = hasGroupConcurrency ? "group_filtered" : singletonFetch ? "singleton_ranking" : "next";
2399
+ const distributedStateCheck = noSkipLocked ? `AND j.state < '${JOB_STATES.active}'` : "";
2228
2400
  return {
2229
2401
  text: `
2230
2402
  WITH
@@ -2240,25 +2412,110 @@ function fetchNextJob(options) {
2240
2412
  FROM ${finalCte}
2241
2413
  WHERE name = '${name}' AND j.id = ${finalCte}.id
2242
2414
  ${singletonFetch && !hasGroupConcurrency ? "AND singleton_rn = 1" : ""}
2415
+ ${distributedStateCheck}
2243
2416
  RETURNING j.${includeMetadata ? JOB_COLUMNS_ALL : JOB_COLUMNS_MIN}
2244
2417
  `,
2245
2418
  values: params.values
2246
2419
  };
2247
2420
  }
2248
- function completeJobs(schema, table, includeQueued) {
2249
- const stateFilter = includeQueued ? `state < '${JOB_STATES.completed}'` : `state = '${JOB_STATES.active}'`;
2250
- return `
2251
- WITH results AS (
2252
- UPDATE ${schema}.${table}
2421
+ function completeJobsUpdate(schema, table, includeQueued) {
2422
+ return `UPDATE ${schema}.${table}
2253
2423
  SET completed_on = now(),
2254
2424
  state = '${JOB_STATES.completed}',
2255
- output = $3::jsonb
2425
+ output = $3::jsonb,
2426
+ blocked = ${includeQueued ? "false" : "blocked"},
2427
+ pending_dependencies = ${includeQueued ? "0" : "pending_dependencies"}
2256
2428
  WHERE name = $1
2257
2429
  AND id IN (SELECT UNNEST($2::uuid[]))
2258
- AND ${stateFilter}
2259
- RETURNING *
2430
+ AND ${includeQueued ? `state < '${JOB_STATES.completed}'` : `state = '${JOB_STATES.active}'`}`;
2431
+ }
2432
+ function lockedChildrenCte(schema) {
2433
+ return `locked_children AS (
2434
+ SELECT j.name, j.id, d.n
2435
+ FROM ${schema}.job j
2436
+ JOIN decremented d ON d.child_name = j.name
2437
+ AND d.child_id = j.id
2438
+ WHERE j.blocked
2439
+ ORDER BY j.name, j.id
2440
+ FOR UPDATE OF j
2441
+ )`;
2442
+ }
2443
+ function unblockChildrenUpdate(schema) {
2444
+ return `UPDATE ${schema}.job j
2445
+ SET pending_dependencies = GREATEST(j.pending_dependencies - lc.n, 0),
2446
+ blocked = GREATEST(j.pending_dependencies - lc.n, 0) > 0
2447
+ FROM locked_children lc
2448
+ WHERE j.name = lc.name
2449
+ AND j.id = lc.id`;
2450
+ }
2451
+ function completeJobs(schema, table, includeQueued) {
2452
+ return `
2453
+ WITH completed AS (
2454
+ ${completeJobsUpdate(schema, table, includeQueued)}
2455
+ RETURNING name, id, blocking
2456
+ ),
2457
+ decremented AS (
2458
+ SELECT d.child_name, d.child_id, COUNT(*)::int AS n
2459
+ FROM ${schema}.job_dependency d
2460
+ JOIN completed c ON c.blocking
2461
+ AND d.parent_name = c.name
2462
+ AND d.parent_id = c.id
2463
+ GROUP BY d.child_name, d.child_id
2464
+ ),
2465
+ ${lockedChildrenCte(schema)},
2466
+ unblocked AS (
2467
+ ${unblockChildrenUpdate(schema)}
2468
+ RETURNING 1
2260
2469
  )
2261
- SELECT COUNT(*) FROM results
2470
+ SELECT COUNT(*) FROM completed
2471
+ `;
2472
+ }
2473
+ function completeJobsWithOutputs(schema, table) {
2474
+ return `
2475
+ WITH input AS (
2476
+ SELECT * FROM json_to_recordset($2::json) AS x (id uuid, output jsonb)
2477
+ ),
2478
+ completed AS (
2479
+ UPDATE ${schema}.${table} j
2480
+ SET completed_on = now(),
2481
+ state = '${JOB_STATES.completed}',
2482
+ output = i.output
2483
+ FROM input i
2484
+ WHERE j.name = $1
2485
+ AND j.id = i.id
2486
+ AND j.state = '${JOB_STATES.active}'
2487
+ RETURNING j.name, j.id, j.blocking
2488
+ ),
2489
+ decremented AS (
2490
+ SELECT d.child_name, d.child_id, COUNT(*)::int AS n
2491
+ FROM ${schema}.job_dependency d
2492
+ JOIN completed c ON c.blocking
2493
+ AND d.parent_name = c.name
2494
+ AND d.parent_id = c.id
2495
+ GROUP BY d.child_name, d.child_id
2496
+ ),
2497
+ ${lockedChildrenCte(schema)},
2498
+ unblocked AS (
2499
+ ${unblockChildrenUpdate(schema)}
2500
+ RETURNING 1
2501
+ )
2502
+ SELECT COUNT(*) FROM completed
2503
+ `;
2504
+ }
2505
+ function completeJobsWithOutputsDistributed(schema, table) {
2506
+ return `
2507
+ WITH input AS (
2508
+ SELECT * FROM json_to_recordset($2::json) AS x (id uuid, output jsonb)
2509
+ )
2510
+ UPDATE ${schema}.${table} j
2511
+ SET completed_on = now(),
2512
+ state = '${JOB_STATES.completed}',
2513
+ output = i.output
2514
+ FROM input i
2515
+ WHERE j.name = $1
2516
+ AND j.id = i.id
2517
+ AND j.state = '${JOB_STATES.active}'
2518
+ RETURNING j.id, j.blocking
2262
2519
  `;
2263
2520
  }
2264
2521
  function cancelJobs(schema, table) {
@@ -2299,8 +2556,8 @@ function restoreJobs(schema, table) {
2299
2556
  AND id IN (SELECT UNNEST($2::uuid[]))
2300
2557
  `;
2301
2558
  }
2302
- function insertJobs(schema, { table, name, returnId = true }) {
2303
- return `
2559
+ function insertJobs(schema, { table, name, returnId = true, notify = false }) {
2560
+ const insert = `
2304
2561
  INSERT INTO ${schema}.${table} (
2305
2562
  id,
2306
2563
  name,
@@ -2320,7 +2577,10 @@ function insertJobs(schema, { table, name, returnId = true }) {
2320
2577
  retry_delay_max,
2321
2578
  policy,
2322
2579
  dead_letter,
2323
- heartbeat_seconds
2580
+ heartbeat_seconds,
2581
+ blocked,
2582
+ blocking,
2583
+ pending_dependencies
2324
2584
  )
2325
2585
  SELECT
2326
2586
  COALESCE(id, gen_random_uuid()) as id,
@@ -2330,7 +2590,7 @@ function insertJobs(schema, { table, name, returnId = true }) {
2330
2590
  j.start_after,
2331
2591
  "singletonKey",
2332
2592
  CASE
2333
- WHEN "singletonSeconds" IS NOT NULL THEN 'epoch'::timestamp + '1s'::interval * ("singletonSeconds" * floor(( date_part('epoch', now()) + COALESCE("singletonOffset",0)) / "singletonSeconds" ))
2593
+ WHEN "singletonSeconds" IS NOT NULL THEN 'epoch'::timestamp + '1s'::interval * ("singletonSeconds"::float8 * floor(( date_part('epoch', now()) + COALESCE("singletonOffset",0)::float8) / "singletonSeconds"::float8 ))
2334
2594
  ELSE NULL
2335
2595
  END as singleton_on,
2336
2596
  "groupId" as group_id,
@@ -2344,7 +2604,10 @@ function insertJobs(schema, { table, name, returnId = true }) {
2344
2604
  COALESCE("retryDelayMax", q.retry_delay_max) as retry_delay_max,
2345
2605
  q.policy,
2346
2606
  COALESCE("deadLetter", q.dead_letter) as dead_letter,
2347
- COALESCE("heartbeatSeconds", q.heartbeat_seconds) as heartbeat_seconds
2607
+ COALESCE("heartbeatSeconds", q.heartbeat_seconds) as heartbeat_seconds,
2608
+ COALESCE(blocked, false) as blocked,
2609
+ COALESCE(blocking, false) as blocking,
2610
+ COALESCE("pendingDependencies", 0) as pending_dependencies
2348
2611
  FROM (
2349
2612
  SELECT *,
2350
2613
  CASE
@@ -2369,27 +2632,54 @@ function insertJobs(schema, { table, name, returnId = true }) {
2369
2632
  "deleteAfterSeconds" integer,
2370
2633
  "retentionSeconds" integer,
2371
2634
  "deadLetter" text,
2372
- "heartbeatSeconds" integer
2635
+ "heartbeatSeconds" integer,
2636
+ blocked boolean,
2637
+ blocking boolean,
2638
+ "pendingDependencies" integer
2373
2639
  )
2374
2640
  ) j
2375
2641
  JOIN ${schema}.queue q ON q.name = '${name}'
2376
2642
  ON CONFLICT DO NOTHING
2377
- ${returnId ? "RETURNING id" : ""}
2643
+ ${notify ? "RETURNING id, start_after" : returnId ? "RETURNING id" : ""}
2644
+ `;
2645
+ if (!notify) return insert;
2646
+ const comparator = returnId ? ">= 0" : "< 0";
2647
+ return `
2648
+ WITH ins AS (
2649
+ ${insert}
2650
+ ),
2651
+ notified AS (
2652
+ SELECT pg_notify(${notifyChannelSql(schema)}, '${name}')
2653
+ FROM ins WHERE start_after <= now() LIMIT 1
2654
+ )
2655
+ SELECT id FROM ins WHERE (SELECT count(*) FROM notified) ${comparator}
2656
+ `;
2657
+ }
2658
+ function insertFlowJobs(schema, { table, name }, jobs) {
2659
+ return `
2660
+ WITH ins AS (
2661
+ ${insertJobs(schema, {
2662
+ table,
2663
+ name,
2664
+ returnId: true
2665
+ }).replace("$1", () => serializeJsonParam(jobs))}
2666
+ )
2667
+ SELECT 1 / (CASE WHEN (SELECT count(*) FROM ins) = ${jobs.length} THEN 1 ELSE 0 END)
2378
2668
  `;
2379
2669
  }
2380
2670
  function failJobsById(schema, table) {
2381
2671
  return failJobs(schema, table, `name = $1 AND id IN (SELECT UNNEST($2::uuid[])) AND state < '${JOB_STATES.completed}'`, "$3::jsonb");
2382
2672
  }
2383
- function failJobsByTimeout(schema, table, queues) {
2673
+ function failJobsByTimeout(schema, table, queues, noAdvisoryLocks) {
2384
2674
  return locked(schema, failJobs(schema, table, `state = '${JOB_STATES.active}'
2385
2675
  AND (started_on + expire_seconds * interval '1s') < now()
2386
- AND name = ANY(${serializeArrayParam(queues)})`, "'{ \"value\": { \"message\": \"job timed out\" } }'::jsonb"), table + "failJobsByTimeout");
2676
+ AND name = ANY(${serializeArrayParam(queues)})`, "'{ \"value\": { \"message\": \"job timed out\" } }'::jsonb"), table + "failJobsByTimeout", noAdvisoryLocks);
2387
2677
  }
2388
- function failJobsByHeartbeat(schema, table, queues) {
2678
+ function failJobsByHeartbeat(schema, table, queues, noAdvisoryLocks) {
2389
2679
  return locked(schema, failJobs(schema, table, `state = '${JOB_STATES.active}'
2390
2680
  AND heartbeat_seconds IS NOT NULL
2391
2681
  AND (heartbeat_on + heartbeat_seconds * interval '1s') < now()
2392
- AND name = ANY(${serializeArrayParam(queues)})`, "'{ \"value\": { \"message\": \"job heartbeat timeout\" } }'::jsonb"), table + "failJobsByHeartbeat");
2682
+ AND name = ANY(${serializeArrayParam(queues)})`, "'{ \"value\": { \"message\": \"job heartbeat timeout\" } }'::jsonb"), table + "failJobsByHeartbeat", noAdvisoryLocks);
2393
2683
  }
2394
2684
  function touchJobs(schema, table) {
2395
2685
  return `
@@ -2406,7 +2696,12 @@ function touchJobs(schema, table) {
2406
2696
  }
2407
2697
  function failJobs(schema, table, where, output) {
2408
2698
  return `
2409
- WITH deleted_jobs AS (
2699
+ WITH ${failJobsBody(schema, table, where, output)}
2700
+ SELECT COUNT(*) FROM results
2701
+ `;
2702
+ }
2703
+ function failJobsBody(schema, table, where, output, forceTerminal = false) {
2704
+ return `deleted_jobs AS (
2410
2705
  DELETE FROM ${schema}.${table}
2411
2706
  WHERE ${where}
2412
2707
  RETURNING *
@@ -2438,17 +2733,20 @@ function failJobs(schema, table, where, output) {
2438
2733
  output,
2439
2734
  dead_letter,
2440
2735
  heartbeat_on,
2441
- heartbeat_seconds
2736
+ heartbeat_seconds,
2737
+ blocked,
2738
+ blocking,
2739
+ pending_dependencies
2442
2740
  )
2443
2741
  SELECT
2444
2742
  id,
2445
2743
  name,
2446
2744
  priority,
2447
2745
  data,
2448
- CASE
2746
+ ${forceTerminal ? `'${JOB_STATES.failed}'::${schema}.job_state` : `CASE
2449
2747
  WHEN retry_count < retry_limit THEN '${JOB_STATES.retry}'::${schema}.job_state
2450
2748
  ELSE '${JOB_STATES.failed}'::${schema}.job_state
2451
- END as state,
2749
+ END`} as state,
2452
2750
  retry_limit,
2453
2751
  retry_count,
2454
2752
  retry_delay,
@@ -2472,13 +2770,16 @@ function failJobs(schema, table, where, output) {
2472
2770
  expire_seconds,
2473
2771
  deletion_seconds,
2474
2772
  created_on,
2475
- CASE WHEN retry_count < retry_limit THEN NULL ELSE now() END as completed_on,
2773
+ ${forceTerminal ? "now()" : "CASE WHEN retry_count < retry_limit THEN NULL ELSE now() END"} as completed_on,
2476
2774
  keep_until,
2477
2775
  policy,
2478
2776
  ${output},
2479
2777
  dead_letter,
2480
2778
  NULL as heartbeat_on,
2481
- heartbeat_seconds
2779
+ heartbeat_seconds,
2780
+ blocked,
2781
+ blocking,
2782
+ pending_dependencies
2482
2783
  FROM deleted_jobs
2483
2784
  ON CONFLICT DO NOTHING
2484
2785
  RETURNING *
@@ -2510,7 +2811,10 @@ function failJobs(schema, table, where, output) {
2510
2811
  output,
2511
2812
  dead_letter,
2512
2813
  heartbeat_on,
2513
- heartbeat_seconds
2814
+ heartbeat_seconds,
2815
+ blocked,
2816
+ blocking,
2817
+ pending_dependencies
2514
2818
  )
2515
2819
  SELECT
2516
2820
  id,
@@ -2538,7 +2842,10 @@ function failJobs(schema, table, where, output) {
2538
2842
  ${output},
2539
2843
  dead_letter,
2540
2844
  NULL as heartbeat_on,
2541
- heartbeat_seconds
2845
+ heartbeat_seconds,
2846
+ blocked,
2847
+ blocking,
2848
+ pending_dependencies
2542
2849
  FROM deleted_jobs
2543
2850
  WHERE id NOT IN (SELECT id from retried_jobs)
2544
2851
  RETURNING *
@@ -2562,11 +2869,105 @@ function failJobs(schema, table, where, output) {
2562
2869
  FROM results r
2563
2870
  JOIN ${schema}.queue q ON q.name = r.dead_letter
2564
2871
  WHERE state = '${JOB_STATES.failed}'
2565
- )
2872
+ )`;
2873
+ }
2874
+ function failJobsByIdWithOutputs(schema, table) {
2875
+ return `
2876
+ WITH output_map AS (
2877
+ SELECT * FROM json_to_recordset($2::json) AS x (id uuid, output jsonb)
2878
+ ),
2879
+ ${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)")}
2880
+ SELECT COUNT(*) FROM results
2881
+ `;
2882
+ }
2883
+ function deadLetterJobsByIdWithOutputs(schema, table) {
2884
+ return `
2885
+ WITH output_map AS (
2886
+ SELECT * FROM json_to_recordset($2::json) AS x (id uuid, output jsonb)
2887
+ ),
2888
+ ${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)}
2566
2889
  SELECT COUNT(*) FROM results
2567
2890
  `;
2568
2891
  }
2569
- function deletion(schema, table, queues) {
2892
+ function selectJobsToFailById(schema, table) {
2893
+ return {
2894
+ text: `SELECT * FROM ${schema}.${table} WHERE name = $1 AND id IN (SELECT UNNEST($2::uuid[])) AND state < '${JOB_STATES.completed}'`,
2895
+ values: []
2896
+ };
2897
+ }
2898
+ function deleteJobsToFail(schema, table) {
2899
+ return {
2900
+ text: `DELETE FROM ${schema}.${table} WHERE name = $1 AND id IN (SELECT UNNEST($2::uuid[]))`,
2901
+ values: []
2902
+ };
2903
+ }
2904
+ function selectJobsToFailByTimeout(schema, table, queues) {
2905
+ return {
2906
+ text: `SELECT * FROM ${schema}.${table}
2907
+ WHERE state = '${JOB_STATES.active}'
2908
+ AND (started_on + expire_seconds * interval '1s') < now()
2909
+ AND name = ANY(${serializeArrayParam(queues)})`,
2910
+ values: []
2911
+ };
2912
+ }
2913
+ function selectJobsToFailByHeartbeat(schema, table, queues) {
2914
+ return {
2915
+ text: `SELECT * FROM ${schema}.${table}
2916
+ WHERE state = '${JOB_STATES.active}'
2917
+ AND heartbeat_seconds IS NOT NULL
2918
+ AND (heartbeat_on + heartbeat_seconds * interval '1s') < now()
2919
+ AND name = ANY(${serializeArrayParam(queues)})`,
2920
+ values: []
2921
+ };
2922
+ }
2923
+ function deleteJobsByIds(schema, table) {
2924
+ return {
2925
+ text: `DELETE FROM ${schema}.${table} WHERE id IN (SELECT UNNEST($1::uuid[]))`,
2926
+ values: []
2927
+ };
2928
+ }
2929
+ function completeJobsDistributed(schema, table, includeQueued) {
2930
+ return `
2931
+ ${completeJobsUpdate(schema, table, includeQueued)}
2932
+ RETURNING id, blocking
2933
+ `;
2934
+ }
2935
+ function decrementDependents(schema) {
2936
+ return `
2937
+ WITH decremented AS (
2938
+ SELECT d.child_name, d.child_id, COUNT(*)::int AS n
2939
+ FROM ${schema}.job_dependency d
2940
+ WHERE d.parent_name = $1
2941
+ AND d.parent_id IN (SELECT UNNEST($2::uuid[]))
2942
+ GROUP BY d.child_name, d.child_id
2943
+ ),
2944
+ ${lockedChildrenCte(schema)}
2945
+ ${unblockChildrenUpdate(schema)}
2946
+ `;
2947
+ }
2948
+ function insertRetryJob(schema, table) {
2949
+ return `
2950
+ INSERT INTO ${schema}.${table} (
2951
+ id, name, priority, data, state, retry_limit, retry_count, retry_delay,
2952
+ retry_backoff, retry_delay_max, start_after, started_on, singleton_key, singleton_on,
2953
+ group_id, group_tier, expire_seconds, deletion_seconds, created_on, completed_on,
2954
+ keep_until, policy, output, dead_letter,
2955
+ heartbeat_on, heartbeat_seconds, blocked, blocking, pending_dependencies
2956
+ ) VALUES (
2957
+ $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24,
2958
+ $25, $26, $27, $28, $29
2959
+ ) ON CONFLICT DO NOTHING
2960
+ RETURNING id
2961
+ `;
2962
+ }
2963
+ function insertDeadLetterJob(schema) {
2964
+ return `
2965
+ INSERT INTO ${schema}.job (name, data, output, retry_limit, retry_backoff, retry_delay, keep_until, deletion_seconds)
2966
+ SELECT $1, $2, $3, q.retry_limit, q.retry_backoff, q.retry_delay, now() + q.retention_seconds * interval '1s', q.deletion_seconds
2967
+ FROM ${schema}.queue q WHERE q.name = $1
2968
+ `;
2969
+ }
2970
+ function deletion(schema, table, queues, noAdvisoryLocks) {
2570
2971
  return locked(schema, `
2571
2972
  DELETE FROM ${schema}.${table}
2572
2973
  WHERE name = ANY(${serializeArrayParam(queues)})
@@ -2576,7 +2977,7 @@ function deletion(schema, table, queues) {
2576
2977
  OR
2577
2978
  (state < '${JOB_STATES.active}' AND keep_until < now())
2578
2979
  )
2579
- `, table + "deletion");
2980
+ `, table + "deletion", noAdvisoryLocks);
2580
2981
  }
2581
2982
  function retryJobs(schema, table) {
2582
2983
  return `
@@ -2597,25 +2998,39 @@ function getQueueStats$1(schema, table, queues) {
2597
2998
  text: `
2598
2999
  SELECT
2599
3000
  name,
2600
- (count(*) FILTER (WHERE start_after > now()))::int as "deferredCount",
2601
- (count(*) FILTER (WHERE state < '${JOB_STATES.active}'))::int as "queuedCount",
2602
- (count(*) FILTER (WHERE state = '${JOB_STATES.active}'))::int as "activeCount",
2603
- count(*)::int as "totalCount",
2604
- array_agg(singleton_key) FILTER (WHERE policy IN ('${QUEUE_POLICIES.singleton}','${QUEUE_POLICIES.stately}') AND state = '${JOB_STATES.active}') as "singletonsActive"
2605
- FROM ${schema}.${table}
2606
- WHERE name = ANY($1::text[])
2607
- GROUP BY 1
3001
+ "deferredCount",
3002
+ "queuedCount",
3003
+ GREATEST("queuedCount" - "deferredCount", 0) as "readyCount",
3004
+ "activeCount",
3005
+ "failedCount",
3006
+ "totalCount",
3007
+ "singletonsActive"
3008
+ FROM (
3009
+ SELECT
3010
+ name,
3011
+ (count(*) FILTER (WHERE start_after > now()))::int as "deferredCount",
3012
+ (count(*) FILTER (WHERE state < '${JOB_STATES.active}'))::int as "queuedCount",
3013
+ (count(*) FILTER (WHERE state = '${JOB_STATES.active}'))::int as "activeCount",
3014
+ (count(*) FILTER (WHERE state = '${JOB_STATES.failed}'))::int as "failedCount",
3015
+ count(*)::int as "totalCount",
3016
+ array_agg(singleton_key) FILTER (WHERE policy IN ('${QUEUE_POLICIES.singleton}','${QUEUE_POLICIES.stately}') AND state = '${JOB_STATES.active}') as "singletonsActive"
3017
+ FROM ${schema}.${table}
3018
+ WHERE name = ANY($1::text[])
3019
+ GROUP BY 1
3020
+ ) stats
2608
3021
  `,
2609
3022
  values: [queues]
2610
3023
  };
2611
3024
  }
2612
- function cacheQueueStats(schema, table, queues) {
3025
+ function cacheQueueStats(schema, table, queues, noAdvisoryLocks) {
2613
3026
  return locked(schema, `
2614
3027
  WITH stats AS (${getQueueStats$1(schema, table, queues).text.replace("$1::text[]", serializeArrayParam(queues))})
2615
3028
  UPDATE ${schema}.queue SET
2616
3029
  deferred_count = COALESCE(stats."deferredCount", 0),
2617
3030
  queued_count = COALESCE(stats."queuedCount", 0),
3031
+ ready_count = COALESCE(stats."readyCount", 0),
2618
3032
  active_count = COALESCE(stats."activeCount", 0),
3033
+ failed_count = COALESCE(stats."failedCount", 0),
2619
3034
  total_count = COALESCE(stats."totalCount", 0),
2620
3035
  singletons_active = stats."singletonsActive"
2621
3036
  FROM (
@@ -2628,22 +3043,27 @@ function cacheQueueStats(schema, table, queues) {
2628
3043
  queue.name,
2629
3044
  queue.queued_count as "queuedCount",
2630
3045
  queue.warning_queued as "warningQueueSize"
2631
- `, "queue-stats");
3046
+ `, "queue-stats", noAdvisoryLocks);
2632
3047
  }
2633
3048
  function serializeArrayParam(values) {
2634
3049
  return `ARRAY[${values.map((v) => `'${v.replace(SINGLE_QUOTE_REGEX, "''")}'`).join(",")}]::text[]`;
2635
3050
  }
2636
- function locked(schema, query, key) {
2637
- const sql = Array.isArray(query) ? query.join(";\n") : query;
3051
+ function serializeJsonParam(value) {
3052
+ return `'${JSON.stringify(value).replace(SINGLE_QUOTE_REGEX, "''")}'`;
3053
+ }
3054
+ function transaction(query) {
2638
3055
  return `
2639
3056
  BEGIN;
2640
3057
  SET LOCAL lock_timeout = 30000;
2641
3058
  SET LOCAL idle_in_transaction_session_timeout = 30000;
2642
- ${advisoryLock(schema, key)};
2643
- ${sql};
3059
+ ${Array.isArray(query) ? query.join(";\n") : query};
2644
3060
  COMMIT;
2645
3061
  `;
2646
3062
  }
3063
+ function locked(schema, query, key, noAdvisoryLocks) {
3064
+ const statements = Array.isArray(query) ? query : [query];
3065
+ return transaction(noAdvisoryLocks ? statements : [advisoryLock(schema, key), ...statements]);
3066
+ }
2647
3067
  function advisoryLock(schema, key) {
2648
3068
  return `SELECT pg_advisory_xact_lock(
2649
3069
  ('x' || encode(sha224((current_database() || '.pgboss.${schema}${key || ""}')::bytea), 'hex'))::bit(64)::bigint
@@ -2684,6 +3104,49 @@ function getJobById$1(schema, table) {
2684
3104
  AND id = $2
2685
3105
  `;
2686
3106
  }
3107
+ function insertDependencies(schema, deps) {
3108
+ const sql = `
3109
+ INSERT INTO ${schema}.job_dependency (child_name, child_id, parent_name, parent_id)
3110
+ SELECT child_name, child_id, parent_name, parent_id
3111
+ FROM json_to_recordset($1::json) AS x (
3112
+ child_name text,
3113
+ child_id uuid,
3114
+ parent_name text,
3115
+ parent_id uuid
3116
+ )
3117
+ ON CONFLICT DO NOTHING
3118
+ `;
3119
+ return deps ? sql.replace("$1", () => serializeJsonParam(deps)) : sql;
3120
+ }
3121
+ function getDependencies(schema) {
3122
+ return `
3123
+ SELECT parent_name as "parentName", parent_id as "parentId"
3124
+ FROM ${schema}.job_dependency
3125
+ WHERE child_name = $1 AND child_id = $2
3126
+ `;
3127
+ }
3128
+ function getDependents(schema) {
3129
+ return `
3130
+ SELECT child_name as "childName", child_id as "childId"
3131
+ FROM ${schema}.job_dependency
3132
+ WHERE parent_name = $1 AND parent_id = $2
3133
+ `;
3134
+ }
3135
+ function cleanupDependencies(schema, table, queues, noAdvisoryLocks) {
3136
+ return locked(schema, `
3137
+ DELETE FROM ${schema}.job_dependency
3138
+ WHERE (child_name = ANY(${serializeArrayParam(queues)})
3139
+ AND NOT EXISTS (
3140
+ SELECT 1 FROM ${schema}.${table} j
3141
+ WHERE j.name = child_name AND j.id = child_id
3142
+ ))
3143
+ OR (parent_name = ANY(${serializeArrayParam(queues)})
3144
+ AND NOT EXISTS (
3145
+ SELECT 1 FROM ${schema}.${table} j
3146
+ WHERE j.name = parent_name AND j.id = parent_id
3147
+ ))
3148
+ `, table + "cleanupDependencies", noAdvisoryLocks);
3149
+ }
2687
3150
  function getBlockedKeys(schema, table) {
2688
3151
  return `
2689
3152
  SELECT DISTINCT singleton_key as "singletonKey"
@@ -2729,7 +3192,7 @@ function getBamStatus(schema) {
2729
3192
  GROUP BY status
2730
3193
  `;
2731
3194
  }
2732
- function getBamEntries(schema) {
3195
+ function getBamEntries$1(schema) {
2733
3196
  return `
2734
3197
  SELECT id, name, version, status, queue, table_name as "table", command, error,
2735
3198
  created_on as "createdOn", started_on as "startedOn", completed_on as "completedOn"
@@ -2744,11 +3207,54 @@ var POLICY = {
2744
3207
  MIN_POLLING_INTERVAL_MS: 500,
2745
3208
  MAX_RETENTION_DAYS: 365
2746
3209
  };
3210
+ var COMPATIBILITY_FLAGS = [
3211
+ "noSkipLocked",
3212
+ "noMultiMutationCte",
3213
+ "noTablePartitioning",
3214
+ "noDeferrableConstraints",
3215
+ "noAdvisoryLocks",
3216
+ "noCoveringIndexes",
3217
+ "noListenNotify"
3218
+ ];
3219
+ var BACKEND_PROFILES = {
3220
+ postgres: {
3221
+ kind: "standard",
3222
+ flags: {}
3223
+ },
3224
+ cockroachdb: {
3225
+ kind: "distributed",
3226
+ flags: {
3227
+ noSkipLocked: true,
3228
+ noMultiMutationCte: true,
3229
+ noTablePartitioning: true,
3230
+ noDeferrableConstraints: true,
3231
+ noAdvisoryLocks: true,
3232
+ noCoveringIndexes: true,
3233
+ noListenNotify: true
3234
+ }
3235
+ },
3236
+ yugabytedb: {
3237
+ kind: "distributed",
3238
+ flags: {
3239
+ noAdvisoryLocks: true,
3240
+ noTablePartitioning: true
3241
+ }
3242
+ },
3243
+ citus: {
3244
+ kind: "distributed",
3245
+ flags: {}
3246
+ },
3247
+ pglite: {
3248
+ kind: "embedded",
3249
+ flags: {}
3250
+ }
3251
+ };
2747
3252
  function assertObjectName(value, name = "Name") {
2748
3253
  assert(/^[\w.\-/]+$/.test(value), `${name} can only contain alphanumeric characters, underscores, hyphens, periods, or forward slashes`);
2749
3254
  }
2750
3255
  function validateQueueArgs(config = {}) {
2751
3256
  assert(!("deadLetter" in config) || config.deadLetter === null || typeof config.deadLetter === "string", "deadLetter must be a string");
3257
+ assert(!("notify" in config) || typeof config.notify === "boolean", "notify must be a boolean");
2752
3258
  if (config.deadLetter) assertObjectName(config.deadLetter, "deadLetter");
2753
3259
  validateRetryConfig(config);
2754
3260
  validateExpirationConfig(config);
@@ -2796,6 +3302,82 @@ function validateGroupConfig(config) {
2796
3302
  assert(typeof config.group.id === "string" && config.group.id.length > 0, "group.id must be a non-empty string");
2797
3303
  assert(!("tier" in config.group) || typeof config.group.tier === "string" && config.group.tier.length > 0, "group.tier must be a non-empty string if provided");
2798
3304
  }
3305
+ function validateFlowJobs(jobs) {
3306
+ assert(Array.isArray(jobs), "flow requires an array of jobs");
3307
+ assert(jobs.length >= 2, "flow requires at least 2 jobs");
3308
+ const refs = /* @__PURE__ */ new Set();
3309
+ for (const job of jobs) {
3310
+ assert(typeof job.ref === "string" && job.ref.length > 0, "each flow job must have a non-empty ref");
3311
+ assert(!refs.has(job.ref), `duplicate ref: "${job.ref}"`);
3312
+ refs.add(job.ref);
3313
+ assert(typeof job.name === "string" && job.name.length > 0, "each flow job must have a non-empty name");
3314
+ assertObjectName(job.name);
3315
+ }
3316
+ assert(jobs.some((j) => j.dependsOn && j.dependsOn.length > 0), "flow requires at least one job with dependsOn");
3317
+ for (const job of jobs) {
3318
+ if (!job.dependsOn) continue;
3319
+ assert(Array.isArray(job.dependsOn), `dependsOn for ref "${job.ref}" must be an array`);
3320
+ for (const dep of job.dependsOn) {
3321
+ assert(typeof dep === "string" && dep.length > 0, "dependsOn entries must be non-empty strings");
3322
+ assert(dep !== job.ref, `job "${job.ref}" cannot depend on itself`);
3323
+ assert(refs.has(dep), `dependsOn ref "${dep}" not found in flow`);
3324
+ }
3325
+ }
3326
+ const inDegree = /* @__PURE__ */ new Map();
3327
+ const edges = /* @__PURE__ */ new Map();
3328
+ for (const job of jobs) {
3329
+ inDegree.set(job.ref, 0);
3330
+ edges.set(job.ref, []);
3331
+ }
3332
+ for (const job of jobs) {
3333
+ if (!job.dependsOn) continue;
3334
+ for (const dep of job.dependsOn) {
3335
+ edges.get(dep).push(job.ref);
3336
+ inDegree.set(job.ref, inDegree.get(job.ref) + 1);
3337
+ }
3338
+ }
3339
+ const queue = [];
3340
+ for (const [ref, deg] of inDegree) if (deg === 0) queue.push(ref);
3341
+ let visited = 0;
3342
+ while (queue.length > 0) {
3343
+ const current = queue.shift();
3344
+ visited++;
3345
+ for (const child of edges.get(current)) {
3346
+ const newDeg = inDegree.get(child) - 1;
3347
+ inDegree.set(child, newDeg);
3348
+ if (newDeg === 0) queue.push(child);
3349
+ }
3350
+ }
3351
+ if (visited !== jobs.length) assert(false, `flow contains a dependency cycle: ${findDependencyCycle(edges).join(" -> ")}`);
3352
+ }
3353
+ function findDependencyCycle(edges) {
3354
+ const visiting = /* @__PURE__ */ new Set();
3355
+ const visited = /* @__PURE__ */ new Set();
3356
+ const path = [];
3357
+ function visit(ref) {
3358
+ if (visiting.has(ref)) {
3359
+ const start = path.indexOf(ref);
3360
+ return [...path.slice(start), ref];
3361
+ }
3362
+ if (visited.has(ref)) return null;
3363
+ visiting.add(ref);
3364
+ path.push(ref);
3365
+ for (const child of edges.get(ref) || []) {
3366
+ const cycle = visit(child);
3367
+ if (cycle) return cycle;
3368
+ }
3369
+ path.pop();
3370
+ visiting.delete(ref);
3371
+ visited.add(ref);
3372
+ return null;
3373
+ }
3374
+ let cycle = null;
3375
+ for (const ref of edges.keys()) {
3376
+ cycle = visit(ref);
3377
+ if (cycle) break;
3378
+ }
3379
+ return cycle;
3380
+ }
2799
3381
  function validateGroupConcurrencyValue(value, optionName) {
2800
3382
  if (typeof value === "number") {
2801
3383
  assert(Number.isInteger(value) && value >= 1, `${optionName} must be an integer >= 1`);
@@ -2852,6 +3434,7 @@ function checkWorkArgs(name, args) {
2852
3434
  assert(!("includeMetadata" in options) || typeof options.includeMetadata === "boolean", "includeMetadata must be a boolean");
2853
3435
  assert(!("priority" in options) || typeof options.priority === "boolean", "priority must be a boolean");
2854
3436
  assert(!("localConcurrency" in options) || Number.isInteger(options.localConcurrency) && options.localConcurrency >= 1, "localConcurrency must be an integer >= 1");
3437
+ assert(!("perJobResults" in options) || typeof options.perJobResults === "boolean", "perJobResults must be a boolean");
2855
3438
  validatePriorityRangeConfig(options);
2856
3439
  validateGroupConcurrencyConfig(options);
2857
3440
  validateHeartbeatRefreshConfig(options);
@@ -2875,6 +3458,8 @@ function getConfig(value) {
2875
3458
  config.supervise = "supervise" in config ? config.supervise : true;
2876
3459
  config.migrate = "migrate" in config ? config.migrate : true;
2877
3460
  config.createSchema = "createSchema" in config ? config.createSchema : true;
3461
+ config.useListenNotify = "useListenNotify" in config ? config.useListenNotify : false;
3462
+ resolveBackend(config);
2878
3463
  applySchemaConfig(config);
2879
3464
  applyOpsConfig(config);
2880
3465
  applyScheduleConfig(config);
@@ -2892,6 +3477,17 @@ function validateWarningConfig(config) {
2892
3477
  assert(!("warningRetentionDays" in config) || Number.isInteger(config.warningRetentionDays) && config.warningRetentionDays >= 1, "configuration assert: warningRetentionDays must be an integer >= 1");
2893
3478
  assert(!("warningRetentionDays" in config) || config.warningRetentionDays <= POLICY.MAX_RETENTION_DAYS, `configuration assert: warningRetentionDays cannot exceed ${POLICY.MAX_RETENTION_DAYS} days`);
2894
3479
  }
3480
+ function resolveBackend(config) {
3481
+ const backend = "backend" in config ? config.backend : "postgres";
3482
+ assert(backend in BACKEND_PROFILES, `configuration assert: backend must be one of ${Object.keys(BACKEND_PROFILES).join(", ")}`);
3483
+ config.backend = backend;
3484
+ const { flags } = BACKEND_PROFILES[backend];
3485
+ for (const flag of COMPATIBILITY_FLAGS) config[flag] = flags[flag] ?? false;
3486
+ if (config.__test__distributed) {
3487
+ config.noSkipLocked = true;
3488
+ config.noMultiMutationCte = true;
3489
+ }
3490
+ }
2895
3491
  function assertPostgresObjectName(name) {
2896
3492
  assert(typeof name === "string", "Name must be a string");
2897
3493
  assert(name.length <= 50, "Name cannot exceed 50 characters");
@@ -2932,6 +3528,13 @@ function validateHeartbeatRefreshConfig(config) {
2932
3528
  function applyPollingInterval(config) {
2933
3529
  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`);
2934
3530
  config.pollingInterval = "pollingIntervalSeconds" in config ? config.pollingIntervalSeconds * 1e3 : 2e3;
3531
+ 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`);
3532
+ if ("notifyPollingIntervalSeconds" in config) {
3533
+ config.notifyPollingInterval = config.notifyPollingIntervalSeconds * 1e3;
3534
+ assert(config.notifyPollingInterval >= config.pollingInterval, "configuration assert: notifyPollingIntervalSeconds must be at least pollingIntervalSeconds");
3535
+ } else config.notifyPollingInterval = Math.max(3e4, config.pollingInterval);
3536
+ assert(!("burstWhenReadyExceeds" in config) || Number.isInteger(config.burstWhenReadyExceeds) && config.burstWhenReadyExceeds >= 1, "configuration assert: burstWhenReadyExceeds must be an integer >= 1");
3537
+ assert(!("burstWhenBatchFull" in config) || typeof config.burstWhenBatchFull === "boolean", "configuration assert: burstWhenBatchFull must be a boolean");
2935
3538
  }
2936
3539
  function applyOpsConfig(config) {
2937
3540
  assert(!("superviseIntervalSeconds" in config) || config.superviseIntervalSeconds >= 1, "configuration assert: superviseIntervalSeconds must be at least every second");
@@ -2965,28 +3568,45 @@ function applyBamConfig(config) {
2965
3568
  }
2966
3569
  //#endregion
2967
3570
  //#region ../../src/migrationStore.ts
2968
- function flatten(schema, commands, version) {
3571
+ function formatJobTable(command, table) {
3572
+ return command.replaceAll(".job", `.${table}`).replaceAll("job_i", `${table}_i`);
3573
+ }
3574
+ function inlineAsyncCommand(schema, asyncCommand, version, partitionTables) {
3575
+ const nameMatch = asyncCommand.match(/job_table_run_async\(\s*'([^']+)'/);
3576
+ const bodyMatch = asyncCommand.match(/\$\$([\s\S]*?)\$\$/);
3577
+ const tableMatch = asyncCommand.match(/\$\$\s*,\s*'([^']+)'/);
3578
+ assert(nameMatch && bodyMatch, `Unable to inline async migration command: ${asyncCommand}`);
3579
+ const commandName = nameMatch[1];
3580
+ const body = bodyMatch[1].trim();
3581
+ return (tableMatch ? [tableMatch[1]] : ["job_common", ...partitionTables]).map((table) => {
3582
+ const ddl = formatJobTable(body, table).replace(/(CREATE (?:UNIQUE )?INDEX CONCURRENTLY) /, "$1 IF NOT EXISTS ");
3583
+ return `${`-- inlined from ${schema}.job_table_run_async (migration v${version}, command: ${commandName})`}\n${ddl}`;
3584
+ });
3585
+ }
3586
+ function flatten(schema, commands, version, noAdvisoryLocks) {
2969
3587
  commands.unshift(assertMigration(schema, version));
2970
3588
  commands.push(setVersion(schema, version));
2971
- return locked(schema, commands);
3589
+ return locked(schema, commands, void 0, noAdvisoryLocks);
2972
3590
  }
2973
- function rollback(schema, version, migrations) {
3591
+ function rollback(schema, version, migrations, noAdvisoryLocks) {
2974
3592
  migrations = migrations || getAll(schema);
2975
3593
  const result = migrations.find((i) => i.version === version);
2976
3594
  assert(result, `Version ${version} not found.`);
2977
- return flatten(schema, result.uninstall || [], result.previous);
3595
+ return flatten(schema, result.uninstall || [], result.previous, noAdvisoryLocks);
2978
3596
  }
2979
- function next(schema, version, migrations) {
3597
+ function next(schema, version, migrations, noAdvisoryLocks) {
2980
3598
  migrations = migrations || getAll(schema);
2981
3599
  const result = migrations.find((i) => i.previous === version);
2982
3600
  assert(result, `Version ${version} not found.`);
2983
- return flatten(schema, result.install, result.version);
3601
+ return flatten(schema, result.install, result.version, noAdvisoryLocks);
2984
3602
  }
2985
- function migrate(schema, version, migrations) {
3603
+ function migrateCommands(schema, version, migrations, noAdvisoryLocks, options = {}) {
2986
3604
  migrations = migrations || getAll(schema);
3605
+ const concurrent = [];
2987
3606
  const result = migrations.filter((i) => i.previous >= version).sort((a, b) => a.version - b.version).reduce((acc, migration) => {
2988
3607
  acc.install = acc.install.concat(migration.install);
2989
- if (migration.async) {
3608
+ if (migration.async) if (options.inlineAsync) for (const cmd of migration.async) concurrent.push(...inlineAsyncCommand(schema, cmd, migration.version, options.partitionTables || []));
3609
+ else {
2990
3610
  const bamCommands = migration.async.map((cmd) => cmd.replace(/\$VERSION\$/g, String(migration.version)));
2991
3611
  acc.install = acc.install.concat(bamCommands);
2992
3612
  }
@@ -2997,100 +3617,520 @@ function migrate(schema, version, migrations) {
2997
3617
  version
2998
3618
  });
2999
3619
  assert(result.install.length > 0, `Version ${version} not found.`);
3000
- return flatten(schema, result.install, result.version);
3620
+ return {
3621
+ sql: flatten(schema, result.install, result.version, noAdvisoryLocks),
3622
+ concurrent
3623
+ };
3001
3624
  }
3002
- function getAll(schema) {
3003
- return [
3004
- {
3005
- release: "11.1.0",
3006
- version: 26,
3007
- previous: 25,
3008
- install: [`
3009
- CREATE OR REPLACE FUNCTION ${schema}.create_queue(queue_name text, options jsonb)
3010
- RETURNS VOID AS
3011
- $$
3012
- DECLARE
3013
- tablename varchar := CASE WHEN options->>'partition' = 'true'
3014
- THEN 'j' || encode(sha224(queue_name::bytea), 'hex')
3015
- ELSE 'job_common'
3016
- END;
3017
- queue_created_on timestamptz;
3018
- BEGIN
3019
-
3020
- WITH q as (
3021
- INSERT INTO ${schema}.queue (
3022
- name,
3023
- policy,
3024
- retry_limit,
3025
- retry_delay,
3026
- retry_backoff,
3027
- retry_delay_max,
3028
- expire_seconds,
3029
- retention_seconds,
3030
- deletion_seconds,
3031
- warning_queued,
3032
- dead_letter,
3033
- partition,
3034
- table_name
3035
- )
3036
- VALUES (
3037
- queue_name,
3038
- options->>'policy',
3039
- COALESCE((options->>'retryLimit')::int, 2),
3040
- COALESCE((options->>'retryDelay')::int, 0),
3041
- COALESCE((options->>'retryBackoff')::bool, false),
3042
- (options->>'retryDelayMax')::int,
3043
- COALESCE((options->>'expireInSeconds')::int, 900),
3044
- COALESCE((options->>'retentionSeconds')::int, 1209600),
3045
- COALESCE((options->>'deleteAfterSeconds')::int, 604800),
3046
- COALESCE((options->>'warningQueueSize')::int, 0),
3047
- options->>'deadLetter',
3048
- COALESCE((options->>'partition')::bool, false),
3049
- tablename
3050
- )
3051
- ON CONFLICT DO NOTHING
3052
- RETURNING created_on
3053
- )
3054
- SELECT created_on into queue_created_on from q;
3055
-
3056
- IF queue_created_on IS NULL OR options->>'partition' IS DISTINCT FROM 'true' THEN
3057
- RETURN;
3058
- END IF;
3059
-
3060
- EXECUTE format('CREATE TABLE ${schema}.%I (LIKE ${schema}.job INCLUDING DEFAULTS)', tablename);
3061
-
3062
- EXECUTE format('ALTER TABLE ${schema}.%1$I ADD PRIMARY KEY (name, id)', tablename);
3063
- 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);
3064
- 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);
3065
-
3066
- EXECUTE format('CREATE INDEX %1$s_i5 ON ${schema}.%1$I (name, start_after) INCLUDE (priority, created_on, id) WHERE state < ''active''', tablename);
3067
- 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);
3068
-
3069
- IF options->>'policy' = 'short' THEN
3070
- EXECUTE format('CREATE UNIQUE INDEX %1$s_i1 ON ${schema}.%1$I (name, COALESCE(singleton_key, '''')) WHERE state = ''created'' AND policy = ''short''', tablename);
3071
- ELSIF options->>'policy' = 'singleton' THEN
3072
- EXECUTE format('CREATE UNIQUE INDEX %1$s_i2 ON ${schema}.%1$I (name, COALESCE(singleton_key, '''')) WHERE state = ''active'' AND policy = ''singleton''', tablename);
3073
- ELSIF options->>'policy' = 'stately' THEN
3074
- EXECUTE format('CREATE UNIQUE INDEX %1$s_i3 ON ${schema}.%1$I (name, state, COALESCE(singleton_key, '''')) WHERE state <= ''active'' AND policy = ''stately''', tablename);
3075
- ELSIF options->>'policy' = 'exclusive' THEN
3076
- EXECUTE format('CREATE UNIQUE INDEX %1$s_i6 ON ${schema}.%1$I (name, COALESCE(singleton_key, '''')) WHERE state <= ''active'' AND policy = ''exclusive''', tablename);
3077
- END IF;
3625
+ function migrate(schema, version, migrations, noAdvisoryLocks, options = {}) {
3626
+ const { sql, concurrent } = migrateCommands(schema, version, migrations, noAdvisoryLocks, options);
3627
+ return concurrent.length ? `${sql}\n${concurrent.join(";\n")};` : sql;
3628
+ }
3629
+ var createQueueFn = {
3630
+ 26: (schema) => `
3631
+ CREATE OR REPLACE FUNCTION ${schema}.create_queue(queue_name text, options jsonb)
3632
+ RETURNS VOID AS
3633
+ $$
3634
+ DECLARE
3635
+ tablename varchar := CASE WHEN options->>'partition' = 'true'
3636
+ THEN 'j' || encode(sha224(queue_name::bytea), 'hex')
3637
+ ELSE 'job_common'
3638
+ END;
3639
+ queue_created_on timestamptz;
3640
+ BEGIN
3078
3641
 
3079
- EXECUTE format('ALTER TABLE ${schema}.%I ADD CONSTRAINT cjc CHECK (name=%L)', tablename, queue_name);
3080
- EXECUTE format('ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.%I FOR VALUES IN (%L)', tablename, queue_name);
3081
- END;
3082
- $$
3083
- LANGUAGE plpgsql;
3084
- `, `CREATE UNIQUE INDEX job_i6 ON ${schema}.job_common (name, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'exclusive'`],
3085
- uninstall: [`DROP INDEX ${schema}.job_i6`]
3086
- },
3087
- {
3088
- release: "12.6.0",
3089
- version: 27,
3090
- previous: 26,
3091
- install: [
3092
- `ALTER TABLE ${schema}.version ADD COLUMN IF NOT EXISTS bam_on timestamp with time zone`,
3093
- `
3642
+ WITH q as (
3643
+ INSERT INTO ${schema}.queue (
3644
+ name,
3645
+ policy,
3646
+ retry_limit,
3647
+ retry_delay,
3648
+ retry_backoff,
3649
+ retry_delay_max,
3650
+ expire_seconds,
3651
+ retention_seconds,
3652
+ deletion_seconds,
3653
+ warning_queued,
3654
+ dead_letter,
3655
+ partition,
3656
+ table_name
3657
+ )
3658
+ VALUES (
3659
+ queue_name,
3660
+ options->>'policy',
3661
+ COALESCE((options->>'retryLimit')::int, 2),
3662
+ COALESCE((options->>'retryDelay')::int, 0),
3663
+ COALESCE((options->>'retryBackoff')::bool, false),
3664
+ (options->>'retryDelayMax')::int,
3665
+ COALESCE((options->>'expireInSeconds')::int, 900),
3666
+ COALESCE((options->>'retentionSeconds')::int, 1209600),
3667
+ COALESCE((options->>'deleteAfterSeconds')::int, 604800),
3668
+ COALESCE((options->>'warningQueueSize')::int, 0),
3669
+ options->>'deadLetter',
3670
+ COALESCE((options->>'partition')::bool, false),
3671
+ tablename
3672
+ )
3673
+ ON CONFLICT DO NOTHING
3674
+ RETURNING created_on
3675
+ )
3676
+ SELECT created_on into queue_created_on from q;
3677
+
3678
+ IF queue_created_on IS NULL OR options->>'partition' IS DISTINCT FROM 'true' THEN
3679
+ RETURN;
3680
+ END IF;
3681
+
3682
+ EXECUTE format('CREATE TABLE ${schema}.%I (LIKE ${schema}.job INCLUDING DEFAULTS)', tablename);
3683
+
3684
+ EXECUTE format('ALTER TABLE ${schema}.%1$I ADD PRIMARY KEY (name, id)', tablename);
3685
+ 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);
3686
+ 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);
3687
+
3688
+ EXECUTE format('CREATE INDEX %1$s_i5 ON ${schema}.%1$I (name, start_after) INCLUDE (priority, created_on, id) WHERE state < ''active''', tablename);
3689
+ 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);
3690
+
3691
+ IF options->>'policy' = 'short' THEN
3692
+ EXECUTE format('CREATE UNIQUE INDEX %1$s_i1 ON ${schema}.%1$I (name, COALESCE(singleton_key, '''')) WHERE state = ''created'' AND policy = ''short''', tablename);
3693
+ ELSIF options->>'policy' = 'singleton' THEN
3694
+ EXECUTE format('CREATE UNIQUE INDEX %1$s_i2 ON ${schema}.%1$I (name, COALESCE(singleton_key, '''')) WHERE state = ''active'' AND policy = ''singleton''', tablename);
3695
+ ELSIF options->>'policy' = 'stately' THEN
3696
+ EXECUTE format('CREATE UNIQUE INDEX %1$s_i3 ON ${schema}.%1$I (name, state, COALESCE(singleton_key, '''')) WHERE state <= ''active'' AND policy = ''stately''', tablename);
3697
+ ELSIF options->>'policy' = 'exclusive' THEN
3698
+ EXECUTE format('CREATE UNIQUE INDEX %1$s_i6 ON ${schema}.%1$I (name, COALESCE(singleton_key, '''')) WHERE state <= ''active'' AND policy = ''exclusive''', tablename);
3699
+ END IF;
3700
+
3701
+ EXECUTE format('ALTER TABLE ${schema}.%I ADD CONSTRAINT cjc CHECK (name=%L)', tablename, queue_name);
3702
+ EXECUTE format('ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.%I FOR VALUES IN (%L)', tablename, queue_name);
3703
+ END;
3704
+ $$
3705
+ LANGUAGE plpgsql;
3706
+ `,
3707
+ 27: (schema) => `
3708
+ CREATE OR REPLACE FUNCTION ${schema}.create_queue(queue_name text, options jsonb)
3709
+ RETURNS VOID AS
3710
+ $$
3711
+ DECLARE
3712
+ tablename varchar := CASE WHEN options->>'partition' = 'true'
3713
+ THEN 'j' || encode(sha224(queue_name::bytea), 'hex')
3714
+ ELSE 'job_common'
3715
+ END;
3716
+ queue_created_on timestamptz;
3717
+ BEGIN
3718
+
3719
+ WITH q as (
3720
+ INSERT INTO ${schema}.queue (
3721
+ name,
3722
+ policy,
3723
+ retry_limit,
3724
+ retry_delay,
3725
+ retry_backoff,
3726
+ retry_delay_max,
3727
+ expire_seconds,
3728
+ retention_seconds,
3729
+ deletion_seconds,
3730
+ warning_queued,
3731
+ dead_letter,
3732
+ partition,
3733
+ table_name
3734
+ )
3735
+ VALUES (
3736
+ queue_name,
3737
+ options->>'policy',
3738
+ COALESCE((options->>'retryLimit')::int, 2),
3739
+ COALESCE((options->>'retryDelay')::int, 0),
3740
+ COALESCE((options->>'retryBackoff')::bool, false),
3741
+ (options->>'retryDelayMax')::int,
3742
+ COALESCE((options->>'expireInSeconds')::int, 900),
3743
+ COALESCE((options->>'retentionSeconds')::int, 1209600),
3744
+ COALESCE((options->>'deleteAfterSeconds')::int, 604800),
3745
+ COALESCE((options->>'warningQueueSize')::int, 0),
3746
+ options->>'deadLetter',
3747
+ COALESCE((options->>'partition')::bool, false),
3748
+ tablename
3749
+ )
3750
+ ON CONFLICT DO NOTHING
3751
+ RETURNING created_on
3752
+ )
3753
+ SELECT created_on into queue_created_on from q;
3754
+
3755
+ IF queue_created_on IS NULL OR options->>'partition' IS DISTINCT FROM 'true' THEN
3756
+ RETURN;
3757
+ END IF;
3758
+
3759
+ EXECUTE format('CREATE TABLE ${schema}.%I (LIKE ${schema}.job INCLUDING DEFAULTS)', tablename);
3760
+
3761
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD PRIMARY KEY (name, id)$cmd$, tablename);
3762
+ 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);
3763
+ 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);
3764
+
3765
+ 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);
3766
+ 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);
3767
+ 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);
3768
+
3769
+ IF options->>'policy' = 'short' THEN
3770
+ 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);
3771
+ ELSIF options->>'policy' = 'singleton' THEN
3772
+ 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);
3773
+ ELSIF options->>'policy' = 'stately' THEN
3774
+ 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);
3775
+ ELSIF options->>'policy' = 'exclusive' THEN
3776
+ 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);
3777
+ END IF;
3778
+
3779
+ EXECUTE format('ALTER TABLE ${schema}.%I ADD CONSTRAINT cjc CHECK (name=%L)', tablename, queue_name);
3780
+ EXECUTE format('ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.%I FOR VALUES IN (%L)', tablename, queue_name);
3781
+ END;
3782
+ $$
3783
+ LANGUAGE plpgsql;
3784
+ `,
3785
+ 28: (schema) => `
3786
+ CREATE OR REPLACE FUNCTION ${schema}.create_queue(queue_name text, options jsonb)
3787
+ RETURNS VOID AS
3788
+ $$
3789
+ DECLARE
3790
+ tablename varchar := CASE WHEN options->>'partition' = 'true'
3791
+ THEN 'j' || encode(sha224(queue_name::bytea), 'hex')
3792
+ ELSE 'job_common'
3793
+ END;
3794
+ queue_created_on timestamptz;
3795
+ BEGIN
3796
+
3797
+ WITH q as (
3798
+ INSERT INTO ${schema}.queue (
3799
+ name,
3800
+ policy,
3801
+ retry_limit,
3802
+ retry_delay,
3803
+ retry_backoff,
3804
+ retry_delay_max,
3805
+ expire_seconds,
3806
+ retention_seconds,
3807
+ deletion_seconds,
3808
+ warning_queued,
3809
+ dead_letter,
3810
+ partition,
3811
+ table_name
3812
+ )
3813
+ VALUES (
3814
+ queue_name,
3815
+ options->>'policy',
3816
+ COALESCE((options->>'retryLimit')::int, 2),
3817
+ COALESCE((options->>'retryDelay')::int, 0),
3818
+ COALESCE((options->>'retryBackoff')::bool, false),
3819
+ (options->>'retryDelayMax')::int,
3820
+ COALESCE((options->>'expireInSeconds')::int, 900),
3821
+ COALESCE((options->>'retentionSeconds')::int, 1209600),
3822
+ COALESCE((options->>'deleteAfterSeconds')::int, 604800),
3823
+ COALESCE((options->>'warningQueueSize')::int, 0),
3824
+ options->>'deadLetter',
3825
+ COALESCE((options->>'partition')::bool, false),
3826
+ tablename
3827
+ )
3828
+ ON CONFLICT DO NOTHING
3829
+ RETURNING created_on
3830
+ )
3831
+ SELECT created_on into queue_created_on from q;
3832
+
3833
+ IF queue_created_on IS NULL OR options->>'partition' IS DISTINCT FROM 'true' THEN
3834
+ RETURN;
3835
+ END IF;
3836
+
3837
+ EXECUTE format('CREATE TABLE ${schema}.%I (LIKE ${schema}.job INCLUDING DEFAULTS)', tablename);
3838
+
3839
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD PRIMARY KEY (name, id)$cmd$, tablename);
3840
+ 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);
3841
+ 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);
3842
+
3843
+ 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);
3844
+ 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);
3845
+ 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);
3846
+
3847
+ IF options->>'policy' = 'short' THEN
3848
+ 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);
3849
+ ELSIF options->>'policy' = 'singleton' THEN
3850
+ 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);
3851
+ ELSIF options->>'policy' = 'stately' THEN
3852
+ 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);
3853
+ ELSIF options->>'policy' = 'exclusive' THEN
3854
+ 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);
3855
+ ELSIF options->>'policy' = 'key_strict_fifo' THEN
3856
+ 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);
3857
+ 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);
3858
+ END IF;
3859
+
3860
+ EXECUTE format('ALTER TABLE ${schema}.%I ADD CONSTRAINT cjc CHECK (name=%L)', tablename, queue_name);
3861
+ EXECUTE format('ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.%I FOR VALUES IN (%L)', tablename, queue_name);
3862
+ END;
3863
+ $$
3864
+ LANGUAGE plpgsql;
3865
+ `,
3866
+ 30: (schema) => `
3867
+ CREATE OR REPLACE FUNCTION ${schema}.create_queue(queue_name text, options jsonb)
3868
+ RETURNS VOID AS
3869
+ $$
3870
+ DECLARE
3871
+ tablename varchar := CASE WHEN options->>'partition' = 'true'
3872
+ THEN 'j' || encode(sha224(queue_name::bytea), 'hex')
3873
+ ELSE 'job_common'
3874
+ END;
3875
+ queue_created_on timestamptz;
3876
+ BEGIN
3877
+
3878
+ WITH q as (
3879
+ INSERT INTO ${schema}.queue (
3880
+ name,
3881
+ policy,
3882
+ retry_limit,
3883
+ retry_delay,
3884
+ retry_backoff,
3885
+ retry_delay_max,
3886
+ expire_seconds,
3887
+ retention_seconds,
3888
+ deletion_seconds,
3889
+ warning_queued,
3890
+ dead_letter,
3891
+ partition,
3892
+ table_name,
3893
+ heartbeat_seconds
3894
+ )
3895
+ VALUES (
3896
+ queue_name,
3897
+ options->>'policy',
3898
+ COALESCE((options->>'retryLimit')::int, 2),
3899
+ COALESCE((options->>'retryDelay')::int, 0),
3900
+ COALESCE((options->>'retryBackoff')::bool, false),
3901
+ (options->>'retryDelayMax')::int,
3902
+ COALESCE((options->>'expireInSeconds')::int, 900),
3903
+ COALESCE((options->>'retentionSeconds')::int, 1209600),
3904
+ COALESCE((options->>'deleteAfterSeconds')::int, 604800),
3905
+ COALESCE((options->>'warningQueueSize')::int, 0),
3906
+ options->>'deadLetter',
3907
+ COALESCE((options->>'partition')::bool, false),
3908
+ tablename,
3909
+ (options->>'heartbeatSeconds')::int
3910
+ )
3911
+ ON CONFLICT DO NOTHING
3912
+ RETURNING created_on
3913
+ )
3914
+ SELECT created_on into queue_created_on from q;
3915
+
3916
+ IF queue_created_on IS NULL OR options->>'partition' IS DISTINCT FROM 'true' THEN
3917
+ RETURN;
3918
+ END IF;
3919
+
3920
+ EXECUTE format('CREATE TABLE ${schema}.%I (LIKE ${schema}.job INCLUDING DEFAULTS)', tablename);
3921
+
3922
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD PRIMARY KEY (name, id)$cmd$, tablename);
3923
+ 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);
3924
+ 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);
3925
+
3926
+ 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);
3927
+ 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);
3928
+ 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);
3929
+
3930
+ IF options->>'policy' = 'short' THEN
3931
+ 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);
3932
+ ELSIF options->>'policy' = 'singleton' THEN
3933
+ 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);
3934
+ ELSIF options->>'policy' = 'stately' THEN
3935
+ 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);
3936
+ ELSIF options->>'policy' = 'exclusive' THEN
3937
+ 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);
3938
+ ELSIF options->>'policy' = 'key_strict_fifo' THEN
3939
+ 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);
3940
+ 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);
3941
+ END IF;
3942
+
3943
+ EXECUTE format('ALTER TABLE ${schema}.%I ADD CONSTRAINT cjc CHECK (name=%L)', tablename, queue_name);
3944
+ EXECUTE format('ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.%I FOR VALUES IN (%L)', tablename, queue_name);
3945
+ END;
3946
+ $$
3947
+ LANGUAGE plpgsql;
3948
+ `,
3949
+ 31: (schema) => `
3950
+ CREATE OR REPLACE FUNCTION ${schema}.create_queue(queue_name text, options jsonb)
3951
+ RETURNS VOID AS
3952
+ $$
3953
+ DECLARE
3954
+ tablename varchar := CASE WHEN options->>'partition' = 'true'
3955
+ THEN 'j' || encode(sha224(queue_name::bytea), 'hex')
3956
+ ELSE 'job_common'
3957
+ END;
3958
+ queue_created_on timestamptz;
3959
+ BEGIN
3960
+
3961
+ WITH q as (
3962
+ INSERT INTO ${schema}.queue (
3963
+ name,
3964
+ policy,
3965
+ retry_limit,
3966
+ retry_delay,
3967
+ retry_backoff,
3968
+ retry_delay_max,
3969
+ expire_seconds,
3970
+ retention_seconds,
3971
+ deletion_seconds,
3972
+ warning_queued,
3973
+ dead_letter,
3974
+ partition,
3975
+ table_name,
3976
+ heartbeat_seconds
3977
+ )
3978
+ VALUES (
3979
+ queue_name,
3980
+ options->>'policy',
3981
+ COALESCE((options->>'retryLimit')::int, 2),
3982
+ COALESCE((options->>'retryDelay')::int, 0),
3983
+ COALESCE((options->>'retryBackoff')::bool, false),
3984
+ (options->>'retryDelayMax')::int,
3985
+ COALESCE((options->>'expireInSeconds')::int, 900),
3986
+ COALESCE((options->>'retentionSeconds')::int, 1209600),
3987
+ COALESCE((options->>'deleteAfterSeconds')::int, 604800),
3988
+ COALESCE((options->>'warningQueueSize')::int, 0),
3989
+ options->>'deadLetter',
3990
+ COALESCE((options->>'partition')::bool, false),
3991
+ tablename,
3992
+ (options->>'heartbeatSeconds')::int
3993
+ )
3994
+ ON CONFLICT DO NOTHING
3995
+ RETURNING created_on
3996
+ )
3997
+ SELECT created_on into queue_created_on from q;
3998
+
3999
+ IF queue_created_on IS NULL OR options->>'partition' IS DISTINCT FROM 'true' THEN
4000
+ RETURN;
4001
+ END IF;
4002
+
4003
+ EXECUTE format('CREATE TABLE ${schema}.%I (LIKE ${schema}.job INCLUDING DEFAULTS)', tablename);
4004
+
4005
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD PRIMARY KEY (name, id)$cmd$, tablename);
4006
+ 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);
4007
+ 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);
4008
+
4009
+ 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);
4010
+ 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);
4011
+ 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);
4012
+
4013
+ IF options->>'policy' = 'short' THEN
4014
+ 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);
4015
+ ELSIF options->>'policy' = 'singleton' THEN
4016
+ 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);
4017
+ ELSIF options->>'policy' = 'stately' THEN
4018
+ 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);
4019
+ ELSIF options->>'policy' = 'exclusive' THEN
4020
+ 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);
4021
+ ELSIF options->>'policy' = 'key_strict_fifo' THEN
4022
+ 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);
4023
+ 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);
4024
+ END IF;
4025
+
4026
+ EXECUTE format('ALTER TABLE ${schema}.%I ADD CONSTRAINT cjc CHECK (name=%L)', tablename, queue_name);
4027
+ EXECUTE format('ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.%I FOR VALUES IN (%L)', tablename, queue_name);
4028
+ END;
4029
+ $$
4030
+ LANGUAGE plpgsql;
4031
+ `,
4032
+ 32: (schema) => `
4033
+ CREATE OR REPLACE FUNCTION ${schema}.create_queue(queue_name text, options jsonb)
4034
+ RETURNS VOID AS
4035
+ $$
4036
+ DECLARE
4037
+ tablename varchar := CASE WHEN options->>'partition' = 'true'
4038
+ THEN 'j' || encode(sha224(queue_name::bytea), 'hex')
4039
+ ELSE 'job_common'
4040
+ END;
4041
+ queue_created_on timestamptz;
4042
+ BEGIN
4043
+
4044
+ WITH q as (
4045
+ INSERT INTO ${schema}.queue (
4046
+ name,
4047
+ policy,
4048
+ retry_limit,
4049
+ retry_delay,
4050
+ retry_backoff,
4051
+ retry_delay_max,
4052
+ expire_seconds,
4053
+ retention_seconds,
4054
+ deletion_seconds,
4055
+ warning_queued,
4056
+ dead_letter,
4057
+ partition,
4058
+ table_name,
4059
+ heartbeat_seconds,
4060
+ notify
4061
+ )
4062
+ VALUES (
4063
+ queue_name,
4064
+ options->>'policy',
4065
+ COALESCE((options->>'retryLimit')::int, 2),
4066
+ COALESCE((options->>'retryDelay')::int, 0),
4067
+ COALESCE((options->>'retryBackoff')::bool, false),
4068
+ (options->>'retryDelayMax')::int,
4069
+ COALESCE((options->>'expireInSeconds')::int, 900),
4070
+ COALESCE((options->>'retentionSeconds')::int, 1209600),
4071
+ COALESCE((options->>'deleteAfterSeconds')::int, 604800),
4072
+ COALESCE((options->>'warningQueueSize')::int, 0),
4073
+ options->>'deadLetter',
4074
+ COALESCE((options->>'partition')::bool, false),
4075
+ tablename,
4076
+ (options->>'heartbeatSeconds')::int,
4077
+ COALESCE((options->>'notify')::bool, false)
4078
+ )
4079
+ ON CONFLICT DO NOTHING
4080
+ RETURNING created_on
4081
+ )
4082
+ SELECT created_on into queue_created_on from q;
4083
+
4084
+ IF queue_created_on IS NULL OR options->>'partition' IS DISTINCT FROM 'true' THEN
4085
+ RETURN;
4086
+ END IF;
4087
+
4088
+ EXECUTE format('CREATE TABLE ${schema}.%I (LIKE ${schema}.job INCLUDING DEFAULTS)', tablename);
4089
+
4090
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD PRIMARY KEY (name, id)$cmd$, tablename);
4091
+ 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);
4092
+ 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);
4093
+
4094
+ 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);
4095
+ 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);
4096
+ 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);
4097
+
4098
+ IF options->>'policy' = 'short' THEN
4099
+ 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);
4100
+ ELSIF options->>'policy' = 'singleton' THEN
4101
+ 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);
4102
+ ELSIF options->>'policy' = 'stately' THEN
4103
+ 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);
4104
+ ELSIF options->>'policy' = 'exclusive' THEN
4105
+ 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);
4106
+ ELSIF options->>'policy' = 'key_strict_fifo' THEN
4107
+ 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);
4108
+ 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);
4109
+ END IF;
4110
+
4111
+ EXECUTE format('ALTER TABLE ${schema}.%I ADD CONSTRAINT cjc CHECK (name=%L)', tablename, queue_name);
4112
+ EXECUTE format('ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.%I FOR VALUES IN (%L)', tablename, queue_name);
4113
+ END;
4114
+ $$
4115
+ LANGUAGE plpgsql;
4116
+ `
4117
+ };
4118
+ function getAll(schema) {
4119
+ return [
4120
+ {
4121
+ release: "11.1.0",
4122
+ version: 26,
4123
+ previous: 25,
4124
+ install: [createQueueFn[26](schema), `CREATE UNIQUE INDEX job_i6 ON ${schema}.job_common (name, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'exclusive'`],
4125
+ uninstall: [`DROP INDEX ${schema}.job_i6`]
4126
+ },
4127
+ {
4128
+ release: "12.6.0",
4129
+ version: 27,
4130
+ previous: 26,
4131
+ install: [
4132
+ `ALTER TABLE ${schema}.version ADD COLUMN IF NOT EXISTS bam_on timestamp with time zone`,
4133
+ `
3094
4134
  CREATE TABLE IF NOT EXISTS ${schema}.bam (
3095
4135
  id uuid PRIMARY KEY default gen_random_uuid(),
3096
4136
  name text NOT NULL,
@@ -3190,84 +4230,7 @@ function getAll(schema) {
3190
4230
  `,
3191
4231
  `ALTER TABLE ${schema}.job ADD COLUMN IF NOT EXISTS group_id text`,
3192
4232
  `ALTER TABLE ${schema}.job ADD COLUMN IF NOT EXISTS group_tier text`,
3193
- `
3194
- CREATE OR REPLACE FUNCTION ${schema}.create_queue(queue_name text, options jsonb)
3195
- RETURNS VOID AS
3196
- $$
3197
- DECLARE
3198
- tablename varchar := CASE WHEN options->>'partition' = 'true'
3199
- THEN 'j' || encode(sha224(queue_name::bytea), 'hex')
3200
- ELSE 'job_common'
3201
- END;
3202
- queue_created_on timestamptz;
3203
- BEGIN
3204
-
3205
- WITH q as (
3206
- INSERT INTO ${schema}.queue (
3207
- name,
3208
- policy,
3209
- retry_limit,
3210
- retry_delay,
3211
- retry_backoff,
3212
- retry_delay_max,
3213
- expire_seconds,
3214
- retention_seconds,
3215
- deletion_seconds,
3216
- warning_queued,
3217
- dead_letter,
3218
- partition,
3219
- table_name
3220
- )
3221
- VALUES (
3222
- queue_name,
3223
- options->>'policy',
3224
- COALESCE((options->>'retryLimit')::int, 2),
3225
- COALESCE((options->>'retryDelay')::int, 0),
3226
- COALESCE((options->>'retryBackoff')::bool, false),
3227
- (options->>'retryDelayMax')::int,
3228
- COALESCE((options->>'expireInSeconds')::int, 900),
3229
- COALESCE((options->>'retentionSeconds')::int, 1209600),
3230
- COALESCE((options->>'deleteAfterSeconds')::int, 604800),
3231
- COALESCE((options->>'warningQueueSize')::int, 0),
3232
- options->>'deadLetter',
3233
- COALESCE((options->>'partition')::bool, false),
3234
- tablename
3235
- )
3236
- ON CONFLICT DO NOTHING
3237
- RETURNING created_on
3238
- )
3239
- SELECT created_on into queue_created_on from q;
3240
-
3241
- IF queue_created_on IS NULL OR options->>'partition' IS DISTINCT FROM 'true' THEN
3242
- RETURN;
3243
- END IF;
3244
-
3245
- EXECUTE format('CREATE TABLE ${schema}.%I (LIKE ${schema}.job INCLUDING DEFAULTS)', tablename);
3246
-
3247
- EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD PRIMARY KEY (name, id)$cmd$, tablename);
3248
- 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);
3249
- 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);
3250
-
3251
- 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);
3252
- 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);
3253
- 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);
3254
-
3255
- IF options->>'policy' = 'short' THEN
3256
- 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);
3257
- ELSIF options->>'policy' = 'singleton' THEN
3258
- 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);
3259
- ELSIF options->>'policy' = 'stately' THEN
3260
- 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);
3261
- ELSIF options->>'policy' = 'exclusive' THEN
3262
- 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);
3263
- END IF;
3264
-
3265
- EXECUTE format('ALTER TABLE ${schema}.%I ADD CONSTRAINT cjc CHECK (name=%L)', tablename, queue_name);
3266
- EXECUTE format('ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.%I FOR VALUES IN (%L)', tablename, queue_name);
3267
- END;
3268
- $$
3269
- LANGUAGE plpgsql;
3270
- `,
4233
+ createQueueFn[27](schema),
3271
4234
  `ALTER INDEX IF EXISTS ${schema}.job_i1 RENAME TO job_common_i1`,
3272
4235
  `ALTER INDEX IF EXISTS ${schema}.job_i2 RENAME TO job_common_i2`,
3273
4236
  `ALTER INDEX IF EXISTS ${schema}.job_i3 RENAME TO job_common_i3`,
@@ -3291,177 +4254,21 @@ function getAll(schema) {
3291
4254
  `ALTER INDEX ${schema}.job_common_i2 RENAME TO job_i2`,
3292
4255
  `ALTER INDEX ${schema}.job_common_i1 RENAME TO job_i1`,
3293
4256
  `SELECT ${schema}.job_table_run('DROP INDEX ${schema}.job_i7')`,
3294
- `
3295
- CREATE OR REPLACE FUNCTION ${schema}.create_queue(queue_name text, options jsonb)
3296
- RETURNS VOID AS
3297
- $$
3298
- DECLARE
3299
- tablename varchar := CASE WHEN options->>'partition' = 'true'
3300
- THEN 'j' || encode(sha224(queue_name::bytea), 'hex')
3301
- ELSE 'job_common'
3302
- END;
3303
- queue_created_on timestamptz;
3304
- BEGIN
3305
-
3306
- WITH q as (
3307
- INSERT INTO ${schema}.queue (
3308
- name,
3309
- policy,
3310
- retry_limit,
3311
- retry_delay,
3312
- retry_backoff,
3313
- retry_delay_max,
3314
- expire_seconds,
3315
- retention_seconds,
3316
- deletion_seconds,
3317
- warning_queued,
3318
- dead_letter,
3319
- partition,
3320
- table_name
3321
- )
3322
- VALUES (
3323
- queue_name,
3324
- options->>'policy',
3325
- COALESCE((options->>'retryLimit')::int, 2),
3326
- COALESCE((options->>'retryDelay')::int, 0),
3327
- COALESCE((options->>'retryBackoff')::bool, false),
3328
- (options->>'retryDelayMax')::int,
3329
- COALESCE((options->>'expireInSeconds')::int, 900),
3330
- COALESCE((options->>'retentionSeconds')::int, 1209600),
3331
- COALESCE((options->>'deleteAfterSeconds')::int, 604800),
3332
- COALESCE((options->>'warningQueueSize')::int, 0),
3333
- options->>'deadLetter',
3334
- COALESCE((options->>'partition')::bool, false),
3335
- tablename
3336
- )
3337
- ON CONFLICT DO NOTHING
3338
- RETURNING created_on
3339
- )
3340
- SELECT created_on into queue_created_on from q;
3341
-
3342
- IF queue_created_on IS NULL OR options->>'partition' IS DISTINCT FROM 'true' THEN
3343
- RETURN;
3344
- END IF;
3345
-
3346
- EXECUTE format('CREATE TABLE ${schema}.%I (LIKE ${schema}.job INCLUDING DEFAULTS)', tablename);
3347
-
3348
- EXECUTE format('ALTER TABLE ${schema}.%1$I ADD PRIMARY KEY (name, id)', tablename);
3349
- 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);
3350
- 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);
3351
-
3352
- EXECUTE format('CREATE INDEX %1$s_i5 ON ${schema}.%1$I (name, start_after) INCLUDE (priority, created_on, id) WHERE state < ''active''', tablename);
3353
- 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);
3354
-
3355
- IF options->>'policy' = 'short' THEN
3356
- EXECUTE format('CREATE UNIQUE INDEX %1$s_i1 ON ${schema}.%1$I (name, COALESCE(singleton_key, '''')) WHERE state = ''created'' AND policy = ''short''', tablename);
3357
- ELSIF options->>'policy' = 'singleton' THEN
3358
- EXECUTE format('CREATE UNIQUE INDEX %1$s_i2 ON ${schema}.%1$I (name, COALESCE(singleton_key, '''')) WHERE state = ''active'' AND policy = ''singleton''', tablename);
3359
- ELSIF options->>'policy' = 'stately' THEN
3360
- EXECUTE format('CREATE UNIQUE INDEX %1$s_i3 ON ${schema}.%1$I (name, state, COALESCE(singleton_key, '''')) WHERE state <= ''active'' AND policy = ''stately''', tablename);
3361
- ELSIF options->>'policy' = 'exclusive' THEN
3362
- EXECUTE format('CREATE UNIQUE INDEX %1$s_i6 ON ${schema}.%1$I (name, COALESCE(singleton_key, '''')) WHERE state <= ''active'' AND policy = ''exclusive''', tablename);
3363
- END IF;
3364
-
3365
- EXECUTE format('ALTER TABLE ${schema}.%I ADD CONSTRAINT cjc CHECK (name=%L)', tablename, queue_name);
3366
- EXECUTE format('ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.%I FOR VALUES IN (%L)', tablename, queue_name);
3367
- END;
3368
- $$
3369
- LANGUAGE plpgsql;
3370
- `,
4257
+ createQueueFn[26](schema),
3371
4258
  `DROP FUNCTION ${schema}.job_table_run(text, text, text)`,
3372
4259
  `DROP FUNCTION ${schema}.job_table_run_async(text, int, text, text, text)`,
3373
4260
  `DROP FUNCTION ${schema}.job_table_format(text, text)`,
3374
- `DROP TABLE ${schema}.bam`,
3375
- `ALTER TABLE ${schema}.version DROP COLUMN bam_on`,
3376
- `ALTER TABLE ${schema}.job DROP COLUMN group_tier`,
3377
- `ALTER TABLE ${schema}.job DROP COLUMN group_id`
3378
- ]
3379
- },
3380
- {
3381
- release: "12.10.0",
3382
- version: 28,
3383
- previous: 27,
3384
- 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')`, `
3385
- CREATE OR REPLACE FUNCTION ${schema}.create_queue(queue_name text, options jsonb)
3386
- RETURNS VOID AS
3387
- $$
3388
- DECLARE
3389
- tablename varchar := CASE WHEN options->>'partition' = 'true'
3390
- THEN 'j' || encode(sha224(queue_name::bytea), 'hex')
3391
- ELSE 'job_common'
3392
- END;
3393
- queue_created_on timestamptz;
3394
- BEGIN
3395
-
3396
- WITH q as (
3397
- INSERT INTO ${schema}.queue (
3398
- name,
3399
- policy,
3400
- retry_limit,
3401
- retry_delay,
3402
- retry_backoff,
3403
- retry_delay_max,
3404
- expire_seconds,
3405
- retention_seconds,
3406
- deletion_seconds,
3407
- warning_queued,
3408
- dead_letter,
3409
- partition,
3410
- table_name
3411
- )
3412
- VALUES (
3413
- queue_name,
3414
- options->>'policy',
3415
- COALESCE((options->>'retryLimit')::int, 2),
3416
- COALESCE((options->>'retryDelay')::int, 0),
3417
- COALESCE((options->>'retryBackoff')::bool, false),
3418
- (options->>'retryDelayMax')::int,
3419
- COALESCE((options->>'expireInSeconds')::int, 900),
3420
- COALESCE((options->>'retentionSeconds')::int, 1209600),
3421
- COALESCE((options->>'deleteAfterSeconds')::int, 604800),
3422
- COALESCE((options->>'warningQueueSize')::int, 0),
3423
- options->>'deadLetter',
3424
- COALESCE((options->>'partition')::bool, false),
3425
- tablename
3426
- )
3427
- ON CONFLICT DO NOTHING
3428
- RETURNING created_on
3429
- )
3430
- SELECT created_on into queue_created_on from q;
3431
-
3432
- IF queue_created_on IS NULL OR options->>'partition' IS DISTINCT FROM 'true' THEN
3433
- RETURN;
3434
- END IF;
3435
-
3436
- EXECUTE format('CREATE TABLE ${schema}.%I (LIKE ${schema}.job INCLUDING DEFAULTS)', tablename);
3437
-
3438
- EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD PRIMARY KEY (name, id)$cmd$, tablename);
3439
- 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);
3440
- 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);
3441
-
3442
- 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);
3443
- 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);
3444
- 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);
3445
-
3446
- IF options->>'policy' = 'short' THEN
3447
- 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);
3448
- ELSIF options->>'policy' = 'singleton' THEN
3449
- 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);
3450
- ELSIF options->>'policy' = 'stately' THEN
3451
- 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);
3452
- ELSIF options->>'policy' = 'exclusive' THEN
3453
- 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);
3454
- ELSIF options->>'policy' = 'key_strict_fifo' THEN
3455
- 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);
3456
- 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);
3457
- END IF;
3458
-
3459
- EXECUTE format('ALTER TABLE ${schema}.%I ADD CONSTRAINT cjc CHECK (name=%L)', tablename, queue_name);
3460
- EXECUTE format('ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.%I FOR VALUES IN (%L)', tablename, queue_name);
3461
- END;
3462
- $$
3463
- LANGUAGE plpgsql;
3464
- `],
4261
+ `DROP TABLE ${schema}.bam`,
4262
+ `ALTER TABLE ${schema}.version DROP COLUMN bam_on`,
4263
+ `ALTER TABLE ${schema}.job DROP COLUMN group_tier`,
4264
+ `ALTER TABLE ${schema}.job DROP COLUMN group_id`
4265
+ ]
4266
+ },
4267
+ {
4268
+ release: "12.10.0",
4269
+ version: 28,
4270
+ previous: 27,
4271
+ 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)],
3465
4272
  async: [`SELECT ${schema}.job_table_run_async(
3466
4273
  'key_strict_fifo_index',
3467
4274
  $VERSION$,
@@ -3472,84 +4279,7 @@ function getAll(schema) {
3472
4279
  uninstall: [
3473
4280
  `SELECT ${schema}.job_table_run('DROP INDEX IF EXISTS ${schema}.job_i8')`,
3474
4281
  `SELECT ${schema}.job_table_run('ALTER TABLE ${schema}.job DROP CONSTRAINT IF EXISTS job_key_strict_fifo_singleton_key_check')`,
3475
- `
3476
- CREATE OR REPLACE FUNCTION ${schema}.create_queue(queue_name text, options jsonb)
3477
- RETURNS VOID AS
3478
- $$
3479
- DECLARE
3480
- tablename varchar := CASE WHEN options->>'partition' = 'true'
3481
- THEN 'j' || encode(sha224(queue_name::bytea), 'hex')
3482
- ELSE 'job_common'
3483
- END;
3484
- queue_created_on timestamptz;
3485
- BEGIN
3486
-
3487
- WITH q as (
3488
- INSERT INTO ${schema}.queue (
3489
- name,
3490
- policy,
3491
- retry_limit,
3492
- retry_delay,
3493
- retry_backoff,
3494
- retry_delay_max,
3495
- expire_seconds,
3496
- retention_seconds,
3497
- deletion_seconds,
3498
- warning_queued,
3499
- dead_letter,
3500
- partition,
3501
- table_name
3502
- )
3503
- VALUES (
3504
- queue_name,
3505
- options->>'policy',
3506
- COALESCE((options->>'retryLimit')::int, 2),
3507
- COALESCE((options->>'retryDelay')::int, 0),
3508
- COALESCE((options->>'retryBackoff')::bool, false),
3509
- (options->>'retryDelayMax')::int,
3510
- COALESCE((options->>'expireInSeconds')::int, 900),
3511
- COALESCE((options->>'retentionSeconds')::int, 1209600),
3512
- COALESCE((options->>'deleteAfterSeconds')::int, 604800),
3513
- COALESCE((options->>'warningQueueSize')::int, 0),
3514
- options->>'deadLetter',
3515
- COALESCE((options->>'partition')::bool, false),
3516
- tablename
3517
- )
3518
- ON CONFLICT DO NOTHING
3519
- RETURNING created_on
3520
- )
3521
- SELECT created_on into queue_created_on from q;
3522
-
3523
- IF queue_created_on IS NULL OR options->>'partition' IS DISTINCT FROM 'true' THEN
3524
- RETURN;
3525
- END IF;
3526
-
3527
- EXECUTE format('CREATE TABLE ${schema}.%I (LIKE ${schema}.job INCLUDING DEFAULTS)', tablename);
3528
-
3529
- EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD PRIMARY KEY (name, id)$cmd$, tablename);
3530
- 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);
3531
- 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);
3532
-
3533
- 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);
3534
- 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);
3535
- 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);
3536
-
3537
- IF options->>'policy' = 'short' THEN
3538
- 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);
3539
- ELSIF options->>'policy' = 'singleton' THEN
3540
- 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);
3541
- ELSIF options->>'policy' = 'stately' THEN
3542
- 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);
3543
- ELSIF options->>'policy' = 'exclusive' THEN
3544
- 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);
3545
- END IF;
3546
-
3547
- EXECUTE format('ALTER TABLE ${schema}.%I ADD CONSTRAINT cjc CHECK (name=%L)', tablename, queue_name);
3548
- EXECUTE format('ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.%I FOR VALUES IN (%L)', tablename, queue_name);
3549
- END;
3550
- $$
3551
- LANGUAGE plpgsql;
3552
- `
4282
+ createQueueFn[27](schema)
3553
4283
  ]
3554
4284
  },
3555
4285
  {
@@ -3573,176 +4303,64 @@ function getAll(schema) {
3573
4303
  `ALTER TABLE ${schema}.job ADD COLUMN heartbeat_on timestamp with time zone`,
3574
4304
  `ALTER TABLE ${schema}.job ADD COLUMN heartbeat_seconds int`,
3575
4305
  `ALTER TABLE ${schema}.queue ADD COLUMN heartbeat_seconds int`,
3576
- `
3577
- CREATE OR REPLACE FUNCTION ${schema}.create_queue(queue_name text, options jsonb)
3578
- RETURNS VOID AS
3579
- $$
3580
- DECLARE
3581
- tablename varchar := CASE WHEN options->>'partition' = 'true'
3582
- THEN 'j' || encode(sha224(queue_name::bytea), 'hex')
3583
- ELSE 'job_common'
3584
- END;
3585
- queue_created_on timestamptz;
3586
- BEGIN
3587
-
3588
- WITH q as (
3589
- INSERT INTO ${schema}.queue (
3590
- name,
3591
- policy,
3592
- retry_limit,
3593
- retry_delay,
3594
- retry_backoff,
3595
- retry_delay_max,
3596
- expire_seconds,
3597
- retention_seconds,
3598
- deletion_seconds,
3599
- warning_queued,
3600
- dead_letter,
3601
- partition,
3602
- table_name,
3603
- heartbeat_seconds
3604
- )
3605
- VALUES (
3606
- queue_name,
3607
- options->>'policy',
3608
- COALESCE((options->>'retryLimit')::int, 2),
3609
- COALESCE((options->>'retryDelay')::int, 0),
3610
- COALESCE((options->>'retryBackoff')::bool, false),
3611
- (options->>'retryDelayMax')::int,
3612
- COALESCE((options->>'expireInSeconds')::int, 900),
3613
- COALESCE((options->>'retentionSeconds')::int, 1209600),
3614
- COALESCE((options->>'deleteAfterSeconds')::int, 604800),
3615
- COALESCE((options->>'warningQueueSize')::int, 0),
3616
- options->>'deadLetter',
3617
- COALESCE((options->>'partition')::bool, false),
3618
- tablename,
3619
- (options->>'heartbeatSeconds')::int
3620
- )
3621
- ON CONFLICT DO NOTHING
3622
- RETURNING created_on
3623
- )
3624
- SELECT created_on into queue_created_on from q;
3625
-
3626
- IF queue_created_on IS NULL OR options->>'partition' IS DISTINCT FROM 'true' THEN
3627
- RETURN;
3628
- END IF;
3629
-
3630
- EXECUTE format('CREATE TABLE ${schema}.%I (LIKE ${schema}.job INCLUDING DEFAULTS)', tablename);
3631
-
3632
- EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD PRIMARY KEY (name, id)$cmd$, tablename);
3633
- 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);
3634
- 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);
3635
-
3636
- 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);
3637
- 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);
3638
- 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);
3639
-
3640
- IF options->>'policy' = 'short' THEN
3641
- 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);
3642
- ELSIF options->>'policy' = 'singleton' THEN
3643
- 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);
3644
- ELSIF options->>'policy' = 'stately' THEN
3645
- 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);
3646
- ELSIF options->>'policy' = 'exclusive' THEN
3647
- 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);
3648
- ELSIF options->>'policy' = 'key_strict_fifo' THEN
3649
- 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);
3650
- 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);
3651
- END IF;
3652
-
3653
- EXECUTE format('ALTER TABLE ${schema}.%I ADD CONSTRAINT cjc CHECK (name=%L)', tablename, queue_name);
3654
- EXECUTE format('ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.%I FOR VALUES IN (%L)', tablename, queue_name);
3655
- END;
3656
- $$
3657
- LANGUAGE plpgsql;
3658
- `
4306
+ createQueueFn[30](schema)
3659
4307
  ],
3660
4308
  uninstall: [
3661
- `
3662
- CREATE OR REPLACE FUNCTION ${schema}.create_queue(queue_name text, options jsonb)
3663
- RETURNS VOID AS
3664
- $$
3665
- DECLARE
3666
- tablename varchar := CASE WHEN options->>'partition' = 'true'
3667
- THEN 'j' || encode(sha224(queue_name::bytea), 'hex')
3668
- ELSE 'job_common'
3669
- END;
3670
- queue_created_on timestamptz;
3671
- BEGIN
3672
-
3673
- WITH q as (
3674
- INSERT INTO ${schema}.queue (
3675
- name,
3676
- policy,
3677
- retry_limit,
3678
- retry_delay,
3679
- retry_backoff,
3680
- retry_delay_max,
3681
- expire_seconds,
3682
- retention_seconds,
3683
- deletion_seconds,
3684
- warning_queued,
3685
- dead_letter,
3686
- partition,
3687
- table_name
3688
- )
3689
- VALUES (
3690
- queue_name,
3691
- options->>'policy',
3692
- COALESCE((options->>'retryLimit')::int, 2),
3693
- COALESCE((options->>'retryDelay')::int, 0),
3694
- COALESCE((options->>'retryBackoff')::bool, false),
3695
- (options->>'retryDelayMax')::int,
3696
- COALESCE((options->>'expireInSeconds')::int, 900),
3697
- COALESCE((options->>'retentionSeconds')::int, 1209600),
3698
- COALESCE((options->>'deleteAfterSeconds')::int, 604800),
3699
- COALESCE((options->>'warningQueueSize')::int, 0),
3700
- options->>'deadLetter',
3701
- COALESCE((options->>'partition')::bool, false),
3702
- tablename
3703
- )
3704
- ON CONFLICT DO NOTHING
3705
- RETURNING created_on
3706
- )
3707
- SELECT created_on into queue_created_on from q;
3708
-
3709
- IF queue_created_on IS NULL OR options->>'partition' IS DISTINCT FROM 'true' THEN
3710
- RETURN;
3711
- END IF;
3712
-
3713
- EXECUTE format('CREATE TABLE ${schema}.%I (LIKE ${schema}.job INCLUDING DEFAULTS)', tablename);
3714
-
3715
- EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD PRIMARY KEY (name, id)$cmd$, tablename);
3716
- 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);
3717
- 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);
3718
-
3719
- 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);
3720
- 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);
3721
- 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);
3722
-
3723
- IF options->>'policy' = 'short' THEN
3724
- 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);
3725
- ELSIF options->>'policy' = 'singleton' THEN
3726
- 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);
3727
- ELSIF options->>'policy' = 'stately' THEN
3728
- 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);
3729
- ELSIF options->>'policy' = 'exclusive' THEN
3730
- 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);
3731
- ELSIF options->>'policy' = 'key_strict_fifo' THEN
3732
- 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);
3733
- 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);
3734
- END IF;
3735
-
3736
- EXECUTE format('ALTER TABLE ${schema}.%I ADD CONSTRAINT cjc CHECK (name=%L)', tablename, queue_name);
3737
- EXECUTE format('ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.%I FOR VALUES IN (%L)', tablename, queue_name);
3738
- END;
3739
- $$
3740
- LANGUAGE plpgsql;
3741
- `,
4309
+ createQueueFn[28](schema),
3742
4310
  `ALTER TABLE ${schema}.queue DROP COLUMN heartbeat_seconds`,
3743
4311
  `ALTER TABLE ${schema}.job DROP COLUMN heartbeat_seconds`,
3744
4312
  `ALTER TABLE ${schema}.job DROP COLUMN heartbeat_on`
3745
4313
  ]
4314
+ },
4315
+ {
4316
+ release: "12.19.0",
4317
+ version: 31,
4318
+ previous: 30,
4319
+ install: [
4320
+ `ALTER TABLE ${schema}.job ADD COLUMN blocked boolean NOT NULL DEFAULT false`,
4321
+ `ALTER TABLE ${schema}.job ADD COLUMN blocking boolean NOT NULL DEFAULT false`,
4322
+ `ALTER TABLE ${schema}.job ADD COLUMN pending_dependencies int NOT NULL DEFAULT 0`,
4323
+ `
4324
+ CREATE TABLE IF NOT EXISTS ${schema}.job_dependency (
4325
+ child_name text NOT NULL,
4326
+ child_id uuid NOT NULL,
4327
+ parent_name text NOT NULL,
4328
+ parent_id uuid NOT NULL,
4329
+ PRIMARY KEY (child_name, child_id, parent_name, parent_id)
4330
+ )
4331
+ `,
4332
+ `CREATE INDEX IF NOT EXISTS job_dep_parent_idx ON ${schema}.job_dependency (parent_name, parent_id)`,
4333
+ `SELECT ${schema}.job_table_run($cmd$DROP INDEX IF EXISTS ${schema}.job_i5$cmd$)`,
4334
+ `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$)`,
4335
+ createQueueFn[31](schema)
4336
+ ],
4337
+ uninstall: [
4338
+ `DROP INDEX IF EXISTS ${schema}.job_dep_parent_idx`,
4339
+ `DROP TABLE IF EXISTS ${schema}.job_dependency`,
4340
+ createQueueFn[30](schema),
4341
+ `SELECT ${schema}.job_table_run($cmd$DROP INDEX IF EXISTS ${schema}.job_i5$cmd$)`,
4342
+ `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$)`,
4343
+ `ALTER TABLE ${schema}.job DROP COLUMN pending_dependencies`,
4344
+ `ALTER TABLE ${schema}.job DROP COLUMN blocking`,
4345
+ `ALTER TABLE ${schema}.job DROP COLUMN blocked`
4346
+ ]
4347
+ },
4348
+ {
4349
+ release: "12.21.0",
4350
+ version: 32,
4351
+ previous: 31,
4352
+ install: [
4353
+ `ALTER TABLE ${schema}.queue ADD COLUMN notify boolean NOT NULL DEFAULT false`,
4354
+ createQueueFn[32](schema),
4355
+ `ALTER TABLE ${schema}.queue ADD COLUMN failed_count int NOT NULL DEFAULT 0`,
4356
+ `ALTER TABLE ${schema}.queue ADD COLUMN ready_count int NOT NULL DEFAULT 0`
4357
+ ],
4358
+ uninstall: [
4359
+ `ALTER TABLE ${schema}.queue DROP COLUMN ready_count`,
4360
+ `ALTER TABLE ${schema}.queue DROP COLUMN failed_count`,
4361
+ createQueueFn[31](schema),
4362
+ `ALTER TABLE ${schema}.queue DROP COLUMN notify`
4363
+ ]
3746
4364
  }
3747
4365
  ];
3748
4366
  }
@@ -3750,7 +4368,7 @@ function getAll(schema) {
3750
4368
  //#region ../../src/contractor.ts
3751
4369
  var schemaVersion = {
3752
4370
  name: "pg-boss",
3753
- version: "12.18.3",
4371
+ version: "12.21.0",
3754
4372
  description: "Queueing jobs in Postgres from Node.js like a boss",
3755
4373
  type: "module",
3756
4374
  main: "./dist/index.js",
@@ -3758,20 +4376,22 @@ var schemaVersion = {
3758
4376
  bin: { "pg-boss": "./dist/cli.js" },
3759
4377
  engines: { "node": ">=22.12.0" },
3760
4378
  dependencies: {
3761
- "cron-parser": "^5.5.0",
3762
- "pg": "^8.21.0",
4379
+ "cron-parser": "^5.6.0",
4380
+ "pg": "^8.22.0",
3763
4381
  "serialize-error": "^13.0.1"
3764
4382
  },
3765
4383
  devDependencies: {
4384
+ "@electric-sql/pglite": "^0.5.3",
3766
4385
  "@prisma/adapter-pg": "^7.8.0",
3767
4386
  "@prisma/client": "^7.8.0",
3768
4387
  "@tsconfig/node-ts": "^23.6.4",
3769
4388
  "@tsconfig/node22": "^22.0.5",
3770
- "@types/luxon": "^3.7.1",
3771
- "@types/node": "^22.19.20",
4389
+ "@types/luxon": "^3.7.2",
4390
+ "@types/node": "^22.20.0",
3772
4391
  "@types/pg": "^8.20.0",
3773
4392
  "@vitest/coverage-v8": "^4.1.2",
3774
4393
  "cli-testlab": "^6.0.1",
4394
+ "cross-env": "^10.1.0",
3775
4395
  "drizzle-orm": "^1.0.0-beta.22",
3776
4396
  "eslint": "^9.39.4",
3777
4397
  "knex": "^3.2.10",
@@ -3787,17 +4407,24 @@ var schemaVersion = {
3787
4407
  "build": "npm run clean && tsc --project tsconfig.build.json",
3788
4408
  "clean": "node -e \"fs.rmSync('dist',{recursive:true,force:true})\"",
3789
4409
  "prepublishOnly": "npm install && npm test && npm run build",
3790
- "pretest": "prisma generate --schema=test/prisma/schema.prisma",
4410
+ "pretest": "prisma generate --schema=test/prisma/schema.prisma && npm run tsc",
3791
4411
  "test": "eslint . && vitest run",
4412
+ "test:distributed": "cross-env DISTRIBUTED=true npm test",
4413
+ "test:ci": "npm run cover && cross-env DISTRIBUTED=true npm run cover && npm run test:pglite",
4414
+ "test:cockroachdb": "cross-env DB_TYPE=cockroachdb COCKROACH_HOST=localhost npm test -- test/distributedDatabaseTest.ts",
4415
+ "test:cockroachdb:full": "cross-env DB_TYPE=cockroachdb COCKROACH_HOST=localhost npm test -- --no-file-parallelism",
4416
+ "test:yugabytedb:full": "cross-env DB_TYPE=yugabytedb YUGABYTE_HOST=localhost npm test -- --no-file-parallelism",
4417
+ "test:citus:full": "cross-env DB_TYPE=citus CITUS_HOST=localhost npm test",
4418
+ "test:pglite": "cross-env DB_TYPE=pglite npm test -- --no-file-parallelism",
3792
4419
  "lint:fix": "eslint . --fix",
3793
- "precover": "prisma generate --schema=test/prisma/schema.prisma",
3794
- "cover": "vitest run --coverage",
4420
+ "cover": "npm test -- --coverage",
3795
4421
  "tsc": "tsc --noEmit",
4422
+ "cli": "node ./dist/cli.js",
3796
4423
  "readme": "node ./examples/readme.js",
3797
- "db:migrate": "node --import=tsx -e 'console.log(require(\"./src\").getMigrationPlans())'",
3798
- "db:construct": "node --import=tsx -e 'console.log(require(\"./src\").getConstructionPlans())'"
4424
+ "docs": "npm run docs:dev --prefix docs",
4425
+ "docs:readme": "node ./scripts/sync-readme.js"
3799
4426
  },
3800
- pgboss: { "schema": 30 },
4427
+ pgboss: { "schema": 32 },
3801
4428
  repository: {
3802
4429
  "type": "git",
3803
4430
  "url": "git+https://github.com/timgit/pg-boss.git"
@@ -3823,8 +4450,11 @@ var Contractor = class {
3823
4450
  static constructionPlans(schema = DEFAULT_SCHEMA, options = { createSchema: true }) {
3824
4451
  return create(schema, schemaVersion, options);
3825
4452
  }
3826
- static migrationPlans(schema = DEFAULT_SCHEMA, version = schemaVersion - 1) {
3827
- return migrate(schema, version);
4453
+ static migrationPlans(schema = DEFAULT_SCHEMA, version = schemaVersion - 1, options = {}) {
4454
+ return migrate(schema, version, void 0, void 0, {
4455
+ inlineAsync: true,
4456
+ partitionTables: options.partitionTables
4457
+ });
3828
4458
  }
3829
4459
  static rollbackPlans(schema = DEFAULT_SCHEMA, version = schemaVersion) {
3830
4460
  return rollback(schema, version);
@@ -3864,18 +4494,18 @@ var Contractor = class {
3864
4494
  }
3865
4495
  async migrate(version) {
3866
4496
  try {
3867
- const commands = migrate(this.config.schema, version, this.migrations);
4497
+ const commands = migrate(this.config.schema, version, this.migrations, this.config.noAdvisoryLocks);
3868
4498
  await this.db.executeSql(commands);
3869
4499
  } catch (err) {
3870
4500
  assert(err.message.includes(MIGRATE_RACE_MESSAGE), err);
3871
4501
  }
3872
4502
  }
3873
4503
  async next(version) {
3874
- const commands = next(this.config.schema, version, this.migrations);
4504
+ const commands = next(this.config.schema, version, this.migrations, this.config.noAdvisoryLocks);
3875
4505
  await this.db.executeSql(commands);
3876
4506
  }
3877
4507
  async rollback(version) {
3878
- const commands = rollback(this.config.schema, version, this.migrations);
4508
+ const commands = rollback(this.config.schema, version, this.migrations, this.config.noAdvisoryLocks);
3879
4509
  await this.db.executeSql(commands);
3880
4510
  }
3881
4511
  };
@@ -4119,7 +4749,7 @@ var Worker = class {
4119
4749
  fetch;
4120
4750
  onFetch;
4121
4751
  onError;
4122
- interval;
4752
+ resolveInterval;
4123
4753
  jobs = [];
4124
4754
  createdOn = Date.now();
4125
4755
  state = WORKER_STATES.created;
@@ -4135,7 +4765,7 @@ var Worker = class {
4135
4765
  loopDelayPromise = null;
4136
4766
  beenNotified = false;
4137
4767
  runPromise = null;
4138
- constructor({ id, workId, name, options, interval, fetch, onFetch, onError }) {
4768
+ constructor({ id, workId, name, options, resolveInterval, fetch, onFetch, onError }) {
4139
4769
  this.id = id;
4140
4770
  this.workId = workId;
4141
4771
  this.name = name;
@@ -4143,7 +4773,7 @@ var Worker = class {
4143
4773
  this.fetch = fetch;
4144
4774
  this.onFetch = onFetch;
4145
4775
  this.onError = onError;
4146
- this.interval = interval;
4776
+ this.resolveInterval = resolveInterval;
4147
4777
  }
4148
4778
  start() {
4149
4779
  this.runPromise = this.run();
@@ -4152,11 +4782,13 @@ var Worker = class {
4152
4782
  this.state = WORKER_STATES.active;
4153
4783
  while (!this.stopping) {
4154
4784
  const started = Date.now();
4785
+ let fetchedCount = 0;
4155
4786
  try {
4156
4787
  this.beenNotified = false;
4157
4788
  const jobs = await this.fetch();
4158
4789
  this.lastFetchedOn = Date.now();
4159
4790
  if (jobs) {
4791
+ fetchedCount = jobs.length;
4160
4792
  this.jobs = jobs;
4161
4793
  this.lastJobStartedOn = this.lastFetchedOn;
4162
4794
  await this.onFetch(jobs);
@@ -4171,8 +4803,9 @@ var Worker = class {
4171
4803
  }
4172
4804
  const duration = Date.now() - started;
4173
4805
  this.lastJobDuration = duration;
4174
- if (!this.stopping && !this.beenNotified && this.interval - duration > 100) {
4175
- this.loopDelayPromise = delay(this.interval - duration);
4806
+ const interval = this.resolveInterval(fetchedCount);
4807
+ if (!this.stopping && !this.beenNotified && interval - duration > 100) {
4808
+ this.loopDelayPromise = delay(interval - duration);
4176
4809
  await this.loopDelayPromise;
4177
4810
  this.loopDelayPromise = null;
4178
4811
  }
@@ -4280,12 +4913,41 @@ var INTERNAL_QUEUES = Object.values(QUEUES).reduce((acc, i) => ({
4280
4913
  ...acc,
4281
4914
  [i]: i
4282
4915
  }), {});
4283
- var events$3 = {
4916
+ var NUMERIC_METADATA_FIELDS = [
4917
+ "priority",
4918
+ "retryLimit",
4919
+ "retryCount",
4920
+ "retryDelay",
4921
+ "retryDelayMax",
4922
+ "expireInSeconds",
4923
+ "heartbeatSeconds",
4924
+ "deleteAfterSeconds",
4925
+ "pendingDependencies"
4926
+ ];
4927
+ var NUMERIC_QUEUE_FIELDS = [
4928
+ "retryLimit",
4929
+ "retryDelay",
4930
+ "retryDelayMax",
4931
+ "expireInSeconds",
4932
+ "retentionSeconds",
4933
+ "deleteAfterSeconds",
4934
+ "heartbeatSeconds",
4935
+ "deferredCount",
4936
+ "warningQueueSize",
4937
+ "queuedCount",
4938
+ "activeCount",
4939
+ "totalCount"
4940
+ ];
4941
+ var events$4 = {
4284
4942
  error: "error",
4285
4943
  wip: "wip"
4286
4944
  };
4945
+ function rethrowWriteError(err) {
4946
+ if (err?.code === PG_ERROR.divisionByZero) throw new Error("one or more jobs could not be created. This usually means a job id was duplicated, collided with an existing job, or was rejected by a queue policy (short, singleton, stately, or exclusive).", { cause: err });
4947
+ throw err;
4948
+ }
4287
4949
  var Manager = class extends EventEmitter {
4288
- events = events$3;
4950
+ events = events$4;
4289
4951
  db;
4290
4952
  config;
4291
4953
  wipTs;
@@ -4294,6 +4956,7 @@ var Manager = class extends EventEmitter {
4294
4956
  queueCacheInterval;
4295
4957
  wipInterval;
4296
4958
  timekeeper;
4959
+ notifier;
4297
4960
  queues;
4298
4961
  pendingOffWorkCleanups;
4299
4962
  #spies;
@@ -4362,19 +5025,136 @@ var Manager = class extends EventEmitter {
4362
5025
  const spy = this.config.__test__enableSpies ? this.#spies.get(name) : void 0;
4363
5026
  if (spy) for (const job of jobs) spy.addJob(job.id, name, job.data, "active");
4364
5027
  }
4365
- #trackJobsCompleted(name, jobs, result) {
5028
+ async #trackJobsCompleted(name, jobs, result, affected) {
4366
5029
  const spy = this.config.__test__enableSpies ? this.#spies.get(name) : void 0;
4367
- if (spy) {
5030
+ if (!spy) return;
5031
+ if (affected === jobs.length) {
4368
5032
  const output = jobs.length === 1 ? result : void 0;
4369
5033
  for (const job of jobs) spy.addJob(job.id, name, job.data, "completed", output);
5034
+ return;
5035
+ }
5036
+ for (const job of jobs) {
5037
+ const persisted = await this.getJobById(name, job.id);
5038
+ const state = persisted?.state;
5039
+ if (state === "completed" || state === "failed" || state === "active" || state === "created") spy.addJob(job.id, name, job.data, state, persisted?.output);
5040
+ else if (!persisted) spy.addJob(job.id, name, job.data, "completed", void 0);
5041
+ }
5042
+ }
5043
+ async #trackJobsFailed(name, jobs, err) {
5044
+ const spy = this.config.__test__enableSpies ? this.#spies.get(name) : void 0;
5045
+ if (!spy) return;
5046
+ for (const job of jobs) {
5047
+ const persisted = await this.getJobById(name, job.id);
5048
+ if (persisted?.state === "failed") spy.addJob(job.id, name, job.data, "failed", persisted.output ?? {
5049
+ message: err?.message,
5050
+ stack: err?.stack
5051
+ });
4370
5052
  }
4371
5053
  }
4372
- #trackJobsFailed(name, jobs, err) {
5054
+ #trackJobsSettled(name, completed, failed) {
4373
5055
  const spy = this.config.__test__enableSpies ? this.#spies.get(name) : void 0;
4374
- if (spy) for (const job of jobs) spy.addJob(job.id, name, job.data, "failed", {
4375
- message: err?.message,
4376
- stack: err?.stack
5056
+ if (!spy) return;
5057
+ for (const { job, output } of completed) spy.addJob(job.id, name, job.data, "completed", output);
5058
+ for (const { job, output } of failed) spy.addJob(job.id, name, job.data, "failed", serializeError(output));
5059
+ }
5060
+ async #settlePerJob(name, jobs, result) {
5061
+ if (!Array.isArray(result)) {
5062
+ const err = /* @__PURE__ */ new Error("perJobResults handler must resolve with an array of job results");
5063
+ await this.fail(name, jobs.map((job) => job.id), err);
5064
+ this.#trackJobsFailed(name, jobs, err);
5065
+ return;
5066
+ }
5067
+ const batch = new Map(jobs.map((job) => [job.id, job]));
5068
+ const disposition = /* @__PURE__ */ new Map();
5069
+ 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);
5070
+ const completed = [];
5071
+ const failed = [];
5072
+ const deadLettered = [];
5073
+ for (const job of jobs) {
5074
+ const item = disposition.get(job.id);
5075
+ if (item?.status === "completed") completed.push({
5076
+ job,
5077
+ output: item.output
5078
+ });
5079
+ else if (item?.status === "failed") failed.push({
5080
+ job,
5081
+ output: item.output
5082
+ });
5083
+ else if (item?.status === "deadletter") deadLettered.push({
5084
+ job,
5085
+ output: item.output
5086
+ });
5087
+ else failed.push({
5088
+ job,
5089
+ output: /* @__PURE__ */ new Error("no disposition returned by handler")
5090
+ });
5091
+ }
5092
+ if (completed.length > 0) await this.#completeWithOutputs(name, completed.map((c) => ({
5093
+ id: c.job.id,
5094
+ output: c.output
5095
+ })));
5096
+ if (failed.length > 0) await this.#failWithOutputs(name, failed.map((f) => ({
5097
+ id: f.job.id,
5098
+ output: f.output
5099
+ })));
5100
+ if (deadLettered.length > 0) await this.#failWithOutputs(name, deadLettered.map((d) => ({
5101
+ id: d.job.id,
5102
+ output: d.output
5103
+ })), true);
5104
+ this.#trackJobsSettled(name, completed, [...failed, ...deadLettered]);
5105
+ }
5106
+ async #completeWithOutputs(name, items) {
5107
+ const { table } = await this.getQueueCache(name);
5108
+ const payload = items.map((item) => ({
5109
+ id: item.id,
5110
+ output: this.mapCompletionDataArg(item.output)
5111
+ }));
5112
+ const ids = items.map((item) => item.id);
5113
+ if (this.config.noMultiMutationCte) return this.withDistributedTransaction(this.db, async (tx) => {
5114
+ const sql = completeJobsWithOutputsDistributed(this.config.schema, table);
5115
+ const { rows } = await tx.executeSql(sql, [name, JSON.stringify(payload)]);
5116
+ const blockingIds = rows.filter((row) => row.blocking).map((row) => row.id);
5117
+ if (blockingIds.length > 0) await tx.executeSql(decrementDependents(this.config.schema), [name, blockingIds]);
5118
+ return {
5119
+ jobs: ids,
5120
+ requested: ids.length,
5121
+ affected: rows.length
5122
+ };
4377
5123
  });
5124
+ const sql = completeJobsWithOutputs(this.config.schema, table);
5125
+ const result = await this.db.executeSql(sql, [name, JSON.stringify(payload)]);
5126
+ return this.mapCommandResponse(ids, result);
5127
+ }
5128
+ async #failWithOutputs(name, items, forceTerminal = false) {
5129
+ const { table } = await this.getQueueCache(name);
5130
+ const ids = items.map((item) => item.id);
5131
+ if (this.config.noMultiMutationCte) {
5132
+ const outputById = new Map(items.map((item) => [item.id, this.mapCompletionDataArg(item.output)]));
5133
+ return this.withDistributedTransaction(this.db, async (tx) => {
5134
+ const selectQuery = selectJobsToFailById(this.config.schema, table);
5135
+ const { rows: jobs } = await tx.executeSql(selectQuery.text, [name, ids]);
5136
+ if (jobs.length === 0) return {
5137
+ jobs: ids,
5138
+ requested: ids.length,
5139
+ affected: 0
5140
+ };
5141
+ const deleteQuery = deleteJobsToFail(this.config.schema, table);
5142
+ await tx.executeSql(deleteQuery.text, [name, ids]);
5143
+ const count = await this.reinsertFailedJobs(tx, table, jobs, null, outputById, forceTerminal);
5144
+ return {
5145
+ jobs: ids,
5146
+ requested: ids.length,
5147
+ affected: count
5148
+ };
5149
+ });
5150
+ }
5151
+ const payload = items.map((item) => ({
5152
+ id: item.id,
5153
+ output: this.mapCompletionDataArg(item.output)
5154
+ }));
5155
+ const sql = forceTerminal ? deadLetterJobsByIdWithOutputs(this.config.schema, table) : failJobsByIdWithOutputs(this.config.schema, table);
5156
+ const result = await this.db.executeSql(sql, [name, JSON.stringify(payload)]);
5157
+ return this.mapCommandResponse(ids, result);
4378
5158
  }
4379
5159
  #storeLocalGroupConfig(name, localGroupConcurrency) {
4380
5160
  const config = typeof localGroupConcurrency === "number" ? { default: localGroupConcurrency } : localGroupConcurrency;
@@ -4412,7 +5192,7 @@ var Manager = class extends EventEmitter {
4412
5192
  #trackLocalGroupEnd(name, groupedJobs) {
4413
5193
  for (const job of groupedJobs) if (job.groupId) this.#decrementLocalGroupCount(name, job.groupId);
4414
5194
  }
4415
- async #processJobs(name, jobs, callback, worker, heartbeatRefreshSeconds) {
5195
+ async #processJobs(name, jobs, callback, worker, heartbeatRefreshSeconds, perJobResults = false) {
4416
5196
  const jobIds = jobs.map((job) => job.id);
4417
5197
  const maxExpiration = jobs.reduce((acc, i) => Math.max(acc, i.expireInSeconds), 0);
4418
5198
  const heartbeatSeconds = jobs.reduce((acc, j) => Math.max(acc, j.heartbeatSeconds || 0), 0);
@@ -4428,21 +5208,34 @@ var Manager = class extends EventEmitter {
4428
5208
  try {
4429
5209
  await this.touch(name, jobIds);
4430
5210
  } catch (err) {
4431
- this.emit(events$3.error, err);
5211
+ this.emit(events$4.error, err);
4432
5212
  }
4433
5213
  }, intervalMs);
4434
5214
  }
5215
+ let completedResult;
5216
+ let completedAffected = 0;
5217
+ let failedError;
5218
+ let didFail = false;
4435
5219
  try {
4436
5220
  const result = await resolveWithinSeconds(callback(jobs), maxExpiration, `handler execution exceeded ${maxExpiration}s`, ac);
4437
- await this.complete(name, jobIds, jobIds.length === 1 ? result : void 0);
4438
- this.#trackJobsCompleted(name, jobs, result);
5221
+ if (perJobResults) await this.#settlePerJob(name, jobs, result);
5222
+ else {
5223
+ const completion = await this.complete(name, jobIds, jobIds.length === 1 ? result : void 0);
5224
+ completedResult = result;
5225
+ completedAffected = completion.affected;
5226
+ }
4439
5227
  } catch (err) {
4440
5228
  await this.fail(name, jobIds, err);
4441
- this.#trackJobsFailed(name, jobs, err);
5229
+ failedError = err;
5230
+ didFail = true;
4442
5231
  } finally {
4443
5232
  if (heartbeatTimer) clearInterval(heartbeatTimer);
4444
5233
  if (worker) worker.abortController = null;
4445
5234
  }
5235
+ if (this.config.__test__enableSpies && this.#spies.has(name)) {
5236
+ if (didFail) await this.#trackJobsFailed(name, jobs, failedError);
5237
+ else if (!perJobResults) await this.#trackJobsCompleted(name, jobs, completedResult, completedAffected);
5238
+ }
4446
5239
  }
4447
5240
  async start() {
4448
5241
  this.stopped = false;
@@ -4452,7 +5245,7 @@ var Manager = class extends EventEmitter {
4452
5245
  if (now - this.wipTs < 2e3) return;
4453
5246
  const wip = this.getWipData();
4454
5247
  if (wip.some((w) => w.count > 0)) {
4455
- this.emit(events$3.wip, wip);
5248
+ this.emit(events$4.wip, wip);
4456
5249
  this.wipTs = now;
4457
5250
  }
4458
5251
  }, 2e3);
@@ -4467,7 +5260,7 @@ var Manager = class extends EventEmitter {
4467
5260
  return acc;
4468
5261
  }, {});
4469
5262
  } catch (error) {
4470
- emit && this.emit(events$3.error, {
5263
+ emit && this.emit(events$4.error, {
4471
5264
  ...error,
4472
5265
  message: error.message,
4473
5266
  stack: error.stack
@@ -4502,9 +5295,15 @@ var Manager = class extends EventEmitter {
4502
5295
  async work(name, ...args) {
4503
5296
  const { options, callback } = checkWorkArgs(name, args);
4504
5297
  if (this.stopped) throw new Error("Workers are disabled. pg-boss is stopped");
4505
- const { pollingInterval: interval, batchSize = 1, includeMetadata = false, priority = true, localConcurrency = 1, localGroupConcurrency, groupConcurrency, orderByCreatedOn = true, heartbeatRefreshSeconds, minPriority, maxPriority } = options;
5298
+ 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;
4506
5299
  if (localGroupConcurrency != null) this.#storeLocalGroupConfig(name, localGroupConcurrency);
4507
5300
  const firstWorkerId = randomUUID({ disableEntropyCache: true });
5301
+ const isNotifyActive = () => !!(this.notifier?.available && this.queues?.[name]?.notify);
5302
+ const getReadyCount = () => this.queues?.[name]?.readyCount ?? 0;
5303
+ const resolveInterval = (lastFetchCount) => {
5304
+ if (lastFetchCount >= batchSize && (burstWhenReadyExceeds !== void 0 && getReadyCount() > burstWhenReadyExceeds || burstWhenBatchFull && batchSize > 1)) return 0;
5305
+ return isNotifyActive() ? notifyInterval : interval;
5306
+ };
4508
5307
  const createWorker = (workerId, workId) => {
4509
5308
  const fetch = () => {
4510
5309
  const ignoreGroups = localGroupConcurrency != null ? this.#getGroupsAtLocalCapacity(name) : void 0;
@@ -4525,7 +5324,7 @@ var Manager = class extends EventEmitter {
4525
5324
  this.emitWip(name);
4526
5325
  this.#trackJobsActive(name, jobs);
4527
5326
  const worker = this.workers.get(workerId);
4528
- if (localGroupConcurrency == null) await this.#processJobs(name, jobs, callback, worker, heartbeatRefreshSeconds);
5327
+ if (localGroupConcurrency == null) await this.#processJobs(name, jobs, callback, worker, heartbeatRefreshSeconds, perJobResults);
4529
5328
  else {
4530
5329
  const { allowed, excess, groupedJobs } = this.#trackLocalGroupStart(name, jobs);
4531
5330
  if (excess.length > 0) {
@@ -4533,7 +5332,7 @@ var Manager = class extends EventEmitter {
4533
5332
  await this.restore(name, excessIds);
4534
5333
  }
4535
5334
  if (allowed.length > 0) try {
4536
- await this.#processJobs(name, allowed, callback, worker, heartbeatRefreshSeconds);
5335
+ await this.#processJobs(name, allowed, callback, worker, heartbeatRefreshSeconds, perJobResults);
4537
5336
  } finally {
4538
5337
  this.#trackLocalGroupEnd(name, groupedJobs);
4539
5338
  }
@@ -4541,7 +5340,7 @@ var Manager = class extends EventEmitter {
4541
5340
  this.emitWip(name);
4542
5341
  };
4543
5342
  const onError = (error) => {
4544
- this.emit(events$3.error, {
5343
+ this.emit(events$4.error, {
4545
5344
  ...error,
4546
5345
  message: error.message,
4547
5346
  stack: error.stack,
@@ -4554,7 +5353,7 @@ var Manager = class extends EventEmitter {
4554
5353
  workId,
4555
5354
  name,
4556
5355
  options,
4557
- interval,
5356
+ resolveInterval,
4558
5357
  fetch,
4559
5358
  onFetch,
4560
5359
  onError
@@ -4580,7 +5379,7 @@ var Manager = class extends EventEmitter {
4580
5379
  if (!INTERNAL_QUEUES[name]) {
4581
5380
  const now = Date.now();
4582
5381
  if (now - this.wipTs > 2e3) {
4583
- this.emit(events$3.wip, this.getWipData());
5382
+ this.emit(events$4.wip, this.getWipData());
4584
5383
  this.wipTs = now;
4585
5384
  }
4586
5385
  }
@@ -4616,6 +5415,15 @@ var Manager = class extends EventEmitter {
4616
5415
  notifyWorker(workerId) {
4617
5416
  this.workers.get(workerId)?.notify();
4618
5417
  }
5418
+ #notifyEnabled(queueNotify) {
5419
+ return !!queueNotify && !this.config.noListenNotify;
5420
+ }
5421
+ notifyQueue(name) {
5422
+ for (const worker of this.workers.values()) if (worker.name === name) worker.notify();
5423
+ }
5424
+ forceFetchLnWorkers() {
5425
+ for (const worker of this.workers.values()) if (this.queues?.[worker.name]?.notify) worker.notify();
5426
+ }
4619
5427
  async subscribe(event, name) {
4620
5428
  assert(event, "Missing required argument");
4621
5429
  assert(name, "Missing required argument");
@@ -4698,12 +5506,13 @@ var Manager = class extends EventEmitter {
4698
5506
  deadLetter
4699
5507
  };
4700
5508
  const db = wrapper || this.db;
4701
- const { table, policy } = await this.getQueueCache(name);
5509
+ const { table, policy, notify } = await this.getQueueCache(name);
4702
5510
  if (policy === QUEUE_POLICIES.key_strict_fifo && !singletonKey) throw new Error(`${QUEUE_POLICIES.key_strict_fifo} queues require a singletonKey`);
4703
5511
  const sql = insertJobs(this.config.schema, {
4704
5512
  table,
4705
5513
  name,
4706
- returnId: true
5514
+ returnId: true,
5515
+ notify: this.#notifyEnabled(notify)
4707
5516
  });
4708
5517
  const { rows: try1 } = await db.executeSql(sql, [JSON.stringify([job])]);
4709
5518
  if (try1.length === 1) {
@@ -4731,25 +5540,111 @@ var Manager = class extends EventEmitter {
4731
5540
  }
4732
5541
  async insert(name, jobs, options = {}) {
4733
5542
  assert(Array.isArray(jobs), "jobs argument should be an array");
4734
- const { table, policy } = await this.getQueueCache(name);
5543
+ const { table, policy, notify } = await this.getQueueCache(name);
4735
5544
  if (policy === QUEUE_POLICIES.key_strict_fifo) {
4736
5545
  for (const job of jobs) if (!job.singletonKey) throw new Error(`${QUEUE_POLICIES.key_strict_fifo} queues require a singletonKey`);
4737
5546
  }
5547
+ const insertPayload = jobs.map((j) => {
5548
+ const { blocked, blocking, pendingDependencies, ...rest } = j;
5549
+ return rest;
5550
+ });
4738
5551
  const db = this.assertDb(options);
4739
5552
  const spy = this.config.__test__enableSpies ? this.#spies.get(name) : void 0;
4740
5553
  const returnId = !!spy || !!options.returnId;
4741
5554
  const sql = insertJobs(this.config.schema, {
4742
5555
  table,
4743
5556
  name,
4744
- returnId
5557
+ returnId,
5558
+ notify: this.#notifyEnabled(notify)
4745
5559
  });
4746
- const { rows } = await db.executeSql(sql, [JSON.stringify(jobs)]);
5560
+ const { rows } = await db.executeSql(sql, [JSON.stringify(insertPayload)]);
4747
5561
  if (rows.length) {
4748
5562
  if (spy) for (let i = 0; i < rows.length; i++) spy.addJob(rows[i].id, name, jobs[i].data || {}, "created");
4749
5563
  return rows.map((i) => i.id);
4750
5564
  }
4751
5565
  return null;
4752
5566
  }
5567
+ async flow(jobs, options = {}) {
5568
+ validateFlowJobs(jobs);
5569
+ const flowJobs = jobs.map((job) => ({
5570
+ ...job,
5571
+ options: checkSendArgs([{
5572
+ name: job.name,
5573
+ data: job.data,
5574
+ options: job.options
5575
+ }]).options
5576
+ }));
5577
+ const refToId = {};
5578
+ for (const job of flowJobs) refToId[job.ref] = job.options?.id ?? randomUUID();
5579
+ const refToJob = new Map(flowJobs.map((job) => [job.ref, job]));
5580
+ const dependencyCountByRef = /* @__PURE__ */ new Map();
5581
+ const parentRefs = /* @__PURE__ */ new Set();
5582
+ const depRows = [];
5583
+ for (const job of flowJobs) {
5584
+ const dependsOn = [...new Set(job.dependsOn ?? [])];
5585
+ dependencyCountByRef.set(job.ref, dependsOn.length);
5586
+ for (const depRef of dependsOn) {
5587
+ const parentJob = refToJob.get(depRef);
5588
+ parentRefs.add(depRef);
5589
+ depRows.push({
5590
+ child_name: job.name,
5591
+ child_id: refToId[job.ref],
5592
+ parent_name: parentJob.name,
5593
+ parent_id: refToId[depRef]
5594
+ });
5595
+ }
5596
+ }
5597
+ const byQueue = /* @__PURE__ */ new Map();
5598
+ for (const job of flowJobs) {
5599
+ const group = byQueue.get(job.name) || [];
5600
+ group.push(job);
5601
+ byQueue.set(job.name, group);
5602
+ }
5603
+ const statements = [];
5604
+ for (const [queueName, queueJobs] of byQueue) {
5605
+ const { table, notify } = await this.getQueueCache(queueName);
5606
+ const insertPayload = queueJobs.map((j) => {
5607
+ const dependencyCount = dependencyCountByRef.get(j.ref) ?? 0;
5608
+ return {
5609
+ id: refToId[j.ref],
5610
+ name: queueName,
5611
+ data: j.data ?? null,
5612
+ priority: j.options?.priority,
5613
+ startAfter: j.options?.startAfter,
5614
+ singletonKey: j.options?.singletonKey ?? void 0,
5615
+ singletonSeconds: j.options?.singletonSeconds,
5616
+ groupId: j.options?.group?.id ?? void 0,
5617
+ groupTier: j.options?.group?.tier ?? void 0,
5618
+ expireInSeconds: j.options?.expireInSeconds,
5619
+ deleteAfterSeconds: j.options?.deleteAfterSeconds,
5620
+ retentionSeconds: j.options?.retentionSeconds,
5621
+ retryLimit: j.options?.retryLimit,
5622
+ retryDelay: j.options?.retryDelay,
5623
+ retryBackoff: j.options?.retryBackoff,
5624
+ retryDelayMax: j.options?.retryDelayMax,
5625
+ heartbeatSeconds: j.options?.heartbeatSeconds,
5626
+ deadLetter: j.options?.deadLetter ?? void 0,
5627
+ blocked: dependencyCount > 0 || void 0,
5628
+ blocking: parentRefs.has(j.ref) || void 0,
5629
+ pendingDependencies: dependencyCount || void 0
5630
+ };
5631
+ });
5632
+ statements.push(insertFlowJobs(this.config.schema, {
5633
+ table,
5634
+ name: queueName
5635
+ }, insertPayload));
5636
+ if (this.#notifyEnabled(notify)) statements.push(notifyQueue(this.config.schema, queueName));
5637
+ }
5638
+ if (depRows.length > 0) statements.push(insertDependencies(this.config.schema, depRows));
5639
+ const db = options.db ?? this.db;
5640
+ const sql = options.db ? statements.join(";\n") : transaction(statements);
5641
+ try {
5642
+ await db.executeSql(sql);
5643
+ } catch (err) {
5644
+ rethrowWriteError(err);
5645
+ }
5646
+ return refToId;
5647
+ }
4753
5648
  getDebounceStartAfter(singletonSeconds, clockOffset) {
4754
5649
  const debounceInterval = singletonSeconds * 1e3;
4755
5650
  const now = Date.now() + clockOffset;
@@ -4770,12 +5665,16 @@ var Manager = class extends EventEmitter {
4770
5665
  policy,
4771
5666
  limit: options.batchSize || 1,
4772
5667
  ignoreSingletons: singletonsActive
4773
- });
5668
+ }, this.config.noSkipLocked);
4774
5669
  let result;
4775
5670
  try {
4776
5671
  result = await db.executeSql(query.text, query.values);
4777
5672
  } catch (err) {}
4778
- return result?.rows || [];
5673
+ const rows = result?.rows || [];
5674
+ if (this.config.backend === "cockroachdb") {
5675
+ for (const row of rows) for (const field of NUMERIC_METADATA_FIELDS) if (row[field] !== void 0 && row[field] !== null) row[field] = Number(row[field]);
5676
+ }
5677
+ return rows;
4779
5678
  }
4780
5679
  mapCompletionIdArg(id, funcName) {
4781
5680
  const errorMessage = `${funcName}() requires an id`;
@@ -4800,27 +5699,188 @@ var Manager = class extends EventEmitter {
4800
5699
  const db = this.assertDb(options);
4801
5700
  const ids = this.mapCompletionIdArg(id, "complete");
4802
5701
  const { table } = await this.getQueueCache(name);
5702
+ const outputData = this.mapCompletionDataArg(data);
5703
+ if (this.config.noMultiMutationCte) return this.completeDistributed(name, ids, outputData, table, db, options.includeQueued);
4803
5704
  const sql = completeJobs(this.config.schema, table, options.includeQueued);
4804
5705
  const result = await db.executeSql(sql, [
4805
5706
  name,
4806
5707
  ids,
4807
- this.mapCompletionDataArg(data)
5708
+ outputData
4808
5709
  ]);
4809
5710
  return this.mapCommandResponse(ids, result);
4810
5711
  }
5712
+ async withDistributedTransaction(db, fn) {
5713
+ if (db === this.db && this.db._pgbdb) return this.db.withTransaction(fn);
5714
+ return fn(db);
5715
+ }
5716
+ async completeDistributed(name, ids, outputData, table, db, includeQueued) {
5717
+ return this.withDistributedTransaction(db, async (tx) => {
5718
+ const completeSql = completeJobsDistributed(this.config.schema, table, includeQueued);
5719
+ const { rows } = await tx.executeSql(completeSql, [
5720
+ name,
5721
+ ids,
5722
+ outputData
5723
+ ]);
5724
+ const blockingIds = rows.filter((row) => row.blocking).map((row) => row.id);
5725
+ if (blockingIds.length > 0) {
5726
+ const decrementSql = decrementDependents(this.config.schema);
5727
+ await tx.executeSql(decrementSql, [name, blockingIds]);
5728
+ }
5729
+ return {
5730
+ jobs: ids,
5731
+ requested: ids.length,
5732
+ affected: rows.length
5733
+ };
5734
+ });
5735
+ }
4811
5736
  async fail(name, id, data, options = {}) {
4812
5737
  assertQueueName(name);
4813
5738
  const db = this.assertDb(options);
4814
5739
  const ids = this.mapCompletionIdArg(id, "fail");
4815
5740
  const { table } = await this.getQueueCache(name);
5741
+ const outputData = this.mapCompletionDataArg(data);
5742
+ if (this.config.noMultiMutationCte) return this.failDistributed(name, ids, outputData, table, db);
4816
5743
  const sql = failJobsById(this.config.schema, table);
4817
5744
  const result = await db.executeSql(sql, [
4818
5745
  name,
4819
5746
  ids,
4820
- this.mapCompletionDataArg(data)
5747
+ outputData
4821
5748
  ]);
4822
5749
  return this.mapCommandResponse(ids, result);
4823
5750
  }
5751
+ async failDistributed(name, ids, outputData, table, db) {
5752
+ return this.withDistributedTransaction(db, async (tx) => {
5753
+ const selectQuery = selectJobsToFailById(this.config.schema, table);
5754
+ const { rows: jobs } = await tx.executeSql(selectQuery.text, [name, ids]);
5755
+ if (jobs.length === 0) return {
5756
+ jobs: ids,
5757
+ requested: ids.length,
5758
+ affected: 0
5759
+ };
5760
+ const deleteQuery = deleteJobsToFail(this.config.schema, table);
5761
+ await tx.executeSql(deleteQuery.text, [name, ids]);
5762
+ const count = await this.reinsertFailedJobs(tx, table, jobs, outputData);
5763
+ return {
5764
+ jobs: ids,
5765
+ requested: ids.length,
5766
+ affected: count
5767
+ };
5768
+ });
5769
+ }
5770
+ async failJobsByTimeoutDistributed(table, queues) {
5771
+ const select = selectJobsToFailByTimeout(this.config.schema, table, queues);
5772
+ return this.expireJobsDistributed(table, select, { value: { message: "job timed out" } });
5773
+ }
5774
+ async failJobsByHeartbeatDistributed(table, queues) {
5775
+ const select = selectJobsToFailByHeartbeat(this.config.schema, table, queues);
5776
+ return this.expireJobsDistributed(table, select, { value: { message: "job heartbeat timeout" } });
5777
+ }
5778
+ async expireJobsDistributed(table, select, outputData) {
5779
+ return this.withDistributedTransaction(this.db, async (tx) => {
5780
+ const { rows: jobs } = await tx.executeSql(select.text, []);
5781
+ if (jobs.length === 0) return 0;
5782
+ const ids = jobs.map((job) => job.id);
5783
+ const deleteSql = deleteJobsByIds(this.config.schema, table);
5784
+ await tx.executeSql(deleteSql.text, [ids]);
5785
+ return this.reinsertFailedJobs(tx, table, jobs, outputData);
5786
+ });
5787
+ }
5788
+ async reinsertFailedJobs(tx, table, jobs, outputData, outputById, forceTerminal = false) {
5789
+ const insertSql = insertRetryJob(this.config.schema, table);
5790
+ const dlqSql = insertDeadLetterJob(this.config.schema);
5791
+ let count = 0;
5792
+ for (const job of jobs) {
5793
+ const jobOutput = outputById ? outputById.get(job.id) ?? null : outputData;
5794
+ const retryCount = Number(job.retry_count);
5795
+ const retryLimit = Number(job.retry_limit);
5796
+ const retryDelay = Number(job.retry_delay);
5797
+ const retryDelayMax = job.retry_delay_max != null ? Number(job.retry_delay_max) : null;
5798
+ const canRetry = !forceTerminal && retryCount < retryLimit;
5799
+ let retried = false;
5800
+ if (canRetry) {
5801
+ let startAfter = job.start_after;
5802
+ if (!job.retry_backoff) startAfter = new Date(Date.now() + retryDelay * 1e3);
5803
+ else {
5804
+ const exp = Math.min(16, retryCount + 1);
5805
+ const delay = retryDelay * (Math.pow(2, exp) / 2 + Math.pow(2, exp) / 2 * Math.random());
5806
+ const cappedDelay = retryDelayMax != null ? Math.min(retryDelayMax, delay) : delay;
5807
+ startAfter = new Date(Date.now() + cappedDelay * 1e3);
5808
+ }
5809
+ const { rows } = await tx.executeSql(insertSql, [
5810
+ job.id,
5811
+ job.name,
5812
+ job.priority,
5813
+ job.data,
5814
+ "retry",
5815
+ job.retry_limit,
5816
+ job.retry_count,
5817
+ job.retry_delay,
5818
+ job.retry_backoff,
5819
+ job.retry_delay_max,
5820
+ startAfter,
5821
+ job.started_on,
5822
+ job.singleton_key,
5823
+ job.singleton_on,
5824
+ job.group_id,
5825
+ job.group_tier,
5826
+ job.expire_seconds,
5827
+ job.deletion_seconds,
5828
+ job.created_on,
5829
+ null,
5830
+ job.keep_until,
5831
+ job.policy,
5832
+ jobOutput,
5833
+ job.dead_letter,
5834
+ null,
5835
+ job.heartbeat_seconds,
5836
+ job.blocked,
5837
+ job.blocking,
5838
+ job.pending_dependencies
5839
+ ]);
5840
+ retried = rows.length > 0;
5841
+ }
5842
+ if (!retried) {
5843
+ await tx.executeSql(insertSql, [
5844
+ job.id,
5845
+ job.name,
5846
+ job.priority,
5847
+ job.data,
5848
+ "failed",
5849
+ job.retry_limit,
5850
+ job.retry_count,
5851
+ job.retry_delay,
5852
+ job.retry_backoff,
5853
+ job.retry_delay_max,
5854
+ job.start_after,
5855
+ job.started_on,
5856
+ job.singleton_key,
5857
+ job.singleton_on,
5858
+ job.group_id,
5859
+ job.group_tier,
5860
+ job.expire_seconds,
5861
+ job.deletion_seconds,
5862
+ job.created_on,
5863
+ /* @__PURE__ */ new Date(),
5864
+ job.keep_until,
5865
+ job.policy,
5866
+ jobOutput,
5867
+ job.dead_letter,
5868
+ null,
5869
+ job.heartbeat_seconds,
5870
+ job.blocked,
5871
+ job.blocking,
5872
+ job.pending_dependencies
5873
+ ]);
5874
+ if (job.dead_letter) await tx.executeSql(dlqSql, [
5875
+ job.dead_letter,
5876
+ job.data,
5877
+ jobOutput
5878
+ ]);
5879
+ }
5880
+ count++;
5881
+ }
5882
+ return count;
5883
+ }
4824
5884
  async deleteJob(name, id, options = {}) {
4825
5885
  assertQueueName(name);
4826
5886
  const db = this.assertDb(options);
@@ -4888,7 +5948,7 @@ var Manager = class extends EventEmitter {
4888
5948
  const sql = createQueue$1(this.config.schema, name, {
4889
5949
  ...options,
4890
5950
  policy
4891
- });
5951
+ }, this.config.noAdvisoryLocks);
4892
5952
  await this.db.executeSql(sql);
4893
5953
  }
4894
5954
  async getBlockedKeys(name) {
@@ -4904,6 +5964,9 @@ var Manager = class extends EventEmitter {
4904
5964
  if (names) for (const name of names) assertQueueName(name);
4905
5965
  const query = getQueues$1(this.config.schema, names);
4906
5966
  const { rows } = await this.db.executeSql(query.text, query.values);
5967
+ if (this.config.backend === "cockroachdb") {
5968
+ for (const row of rows) for (const field of NUMERIC_QUEUE_FIELDS) if (row[field] !== void 0 && row[field] !== null) row[field] = Number(row[field]);
5969
+ }
4907
5970
  return rows;
4908
5971
  }
4909
5972
  async updateQueue(name, options = {}) {
@@ -4921,16 +5984,13 @@ var Manager = class extends EventEmitter {
4921
5984
  await this.db.executeSql(sql, [name, options]);
4922
5985
  }
4923
5986
  async getQueue(name) {
4924
- assertQueueName(name);
4925
- const query = getQueues$1(this.config.schema, [name]);
4926
- const { rows } = await this.db.executeSql(query.text, query.values);
4927
- return rows[0] || null;
5987
+ return (await this.getQueues([name]))[0] || null;
4928
5988
  }
4929
5989
  async deleteQueue(name) {
4930
5990
  assertQueueName(name);
4931
5991
  try {
4932
5992
  await this.getQueueCache(name);
4933
- const sql = deleteQueue(this.config.schema, name);
5993
+ const sql = deleteQueue(this.config.schema, name, this.config.noAdvisoryLocks);
4934
5994
  await this.db.executeSql(sql);
4935
5995
  } catch {}
4936
5996
  }
@@ -4967,10 +6027,16 @@ var Manager = class extends EventEmitter {
4967
6027
  const queue = await this.getQueueCache(name);
4968
6028
  const query = getQueueStats$1(this.config.schema, queue.table, [name]);
4969
6029
  const { rows } = await this.db.executeSql(query.text, query.values);
4970
- return Object.assign(queue, rows.at(0) || {
6030
+ const stats = rows.at(0);
6031
+ if (stats && this.config.backend === "cockroachdb") {
6032
+ for (const field of NUMERIC_QUEUE_FIELDS) if (stats[field] !== void 0 && stats[field] !== null) stats[field] = Number(stats[field]);
6033
+ }
6034
+ return Object.assign(queue, stats || {
4971
6035
  deferredCount: 0,
4972
6036
  queuedCount: 0,
6037
+ readyCount: 0,
4973
6038
  activeCount: 0,
6039
+ failedCount: 0,
4974
6040
  totalCount: 0
4975
6041
  });
4976
6042
  }
@@ -4980,8 +6046,13 @@ var Manager = class extends EventEmitter {
4980
6046
  const { table } = await this.getQueueCache(name);
4981
6047
  const sql = getJobById$1(this.config.schema, table);
4982
6048
  const result1 = await db.executeSql(sql, [name, id]);
4983
- if (result1?.rows?.length === 1) return result1.rows[0];
4984
- else return null;
6049
+ if (result1?.rows?.length === 1) {
6050
+ const row = result1.rows[0];
6051
+ if (this.config.backend === "cockroachdb") {
6052
+ for (const field of NUMERIC_METADATA_FIELDS) if (row[field] !== void 0 && row[field] !== null) row[field] = Number(row[field]);
6053
+ }
6054
+ return row;
6055
+ } else return null;
4985
6056
  }
4986
6057
  async findJobs(name, options = {}) {
4987
6058
  assertQueueName(name);
@@ -5000,6 +6071,26 @@ var Manager = class extends EventEmitter {
5000
6071
  if (data !== void 0) values.push(JSON.stringify(data));
5001
6072
  return (await db.executeSql(sql, values))?.rows || [];
5002
6073
  }
6074
+ async getDependencies(name, id, options = {}) {
6075
+ assertQueueName(name);
6076
+ const db = this.assertDb(options);
6077
+ const sql = getDependencies(this.config.schema);
6078
+ const { rows } = await db.executeSql(sql, [name, id]);
6079
+ return rows.map((r) => ({
6080
+ name: r.parentName,
6081
+ id: r.parentId
6082
+ }));
6083
+ }
6084
+ async getDependents(name, id, options = {}) {
6085
+ assertQueueName(name);
6086
+ const db = this.assertDb(options);
6087
+ const sql = getDependents(this.config.schema);
6088
+ const { rows } = await db.executeSql(sql, [name, id]);
6089
+ return rows.map((r) => ({
6090
+ name: r.childName,
6091
+ id: r.childId
6092
+ }));
6093
+ }
5003
6094
  assertDb(options) {
5004
6095
  if (options.db) return options.db;
5005
6096
  if (this.db._pgbdb) assert(this.db.opened, "Database connection is not opened");
@@ -5008,7 +6099,7 @@ var Manager = class extends EventEmitter {
5008
6099
  };
5009
6100
  //#endregion
5010
6101
  //#region ../../src/boss.ts
5011
- var events$2 = {
6102
+ var events$3 = {
5012
6103
  error: "error",
5013
6104
  warning: "warning"
5014
6105
  };
@@ -5034,7 +6125,7 @@ var Boss = class extends EventEmitter {
5034
6125
  #db;
5035
6126
  #config;
5036
6127
  #manager;
5037
- events = events$2;
6128
+ events = events$3;
5038
6129
  constructor(db, manager, config) {
5039
6130
  super();
5040
6131
  this.#db = db;
@@ -5069,8 +6160,8 @@ var Boss = class extends EventEmitter {
5069
6160
  db: this.#db,
5070
6161
  schema: this.#config.schema,
5071
6162
  persistWarnings: this.#config.persistWarnings,
5072
- warningEvent: events$2.warning,
5073
- errorEvent: events$2.error
6163
+ warningEvent: events$3.warning,
6164
+ errorEvent: events$3.error
5074
6165
  };
5075
6166
  }
5076
6167
  async #executeQuery(query) {
@@ -5099,7 +6190,7 @@ var Boss = class extends EventEmitter {
5099
6190
  !this.#stopped && await this.supervise(queues);
5100
6191
  !this.#stopped && await this.#maintainWarnings();
5101
6192
  } catch (err) {
5102
- this.emit(events$2.error, err);
6193
+ this.emit(events$3.error, err);
5103
6194
  } finally {
5104
6195
  this.#maintaining = false;
5105
6196
  }
@@ -5142,17 +6233,21 @@ var Boss = class extends EventEmitter {
5142
6233
  if (this.#stopping) return;
5143
6234
  if (rows.length) {
5144
6235
  const queues = rows.map((q) => q.name);
5145
- const cacheStatsSql = cacheQueueStats(this.#config.schema, table, queues);
6236
+ const cacheStatsSql = cacheQueueStats(this.#config.schema, table, queues, this.#config.noAdvisoryLocks);
5146
6237
  const { rows: rowsCacheStats } = await this.#executeQuery(cacheStatsSql);
5147
6238
  if (this.#stopping) return;
5148
- const warnings = rowsCacheStats.filter((i) => i.queuedCount > (i.warningQueueSize || WARNINGS.LARGE_QUEUE.size));
6239
+ const warnings = rowsCacheStats.filter((i) => Number(i.queuedCount) > (Number(i.warningQueueSize) || WARNINGS.LARGE_QUEUE.size));
5149
6240
  for (const warning of warnings) await emitAndPersistWarning(this.#warningContext, WARNING_TYPES.QUEUE_BACKLOG, WARNINGS.LARGE_QUEUE.message, warning);
5150
- const sql = failJobsByTimeout(this.#config.schema, table, queues);
5151
- await this.#executeQuery(sql);
6241
+ if (this.#config.noMultiMutationCte) await this.#manager.failJobsByTimeoutDistributed(table, queues);
6242
+ else {
6243
+ const sql = failJobsByTimeout(this.#config.schema, table, queues, this.#config.noAdvisoryLocks);
6244
+ await this.#executeQuery(sql);
6245
+ }
5152
6246
  if (this.#stopping) return;
5153
6247
  const heartbeatQueues = queues.filter((q) => heartbeatQueueNames.has(q));
5154
- if (heartbeatQueues.length) {
5155
- const heartbeatSql = failJobsByHeartbeat(this.#config.schema, table, heartbeatQueues);
6248
+ if (heartbeatQueues.length) if (this.#config.noMultiMutationCte) await this.#manager.failJobsByHeartbeatDistributed(table, heartbeatQueues);
6249
+ else {
6250
+ const heartbeatSql = failJobsByHeartbeat(this.#config.schema, table, heartbeatQueues, this.#config.noAdvisoryLocks);
5156
6251
  await this.#executeQuery(heartbeatSql);
5157
6252
  }
5158
6253
  }
@@ -5164,14 +6259,16 @@ var Boss = class extends EventEmitter {
5164
6259
  if (this.#stopping) return;
5165
6260
  if (rows.length) {
5166
6261
  const queues = rows.map((q) => q.name);
5167
- const sql = deletion(this.#config.schema, table, queues);
6262
+ const sql = deletion(this.#config.schema, table, queues, this.#config.noAdvisoryLocks);
5168
6263
  await this.#executeQuery(sql);
6264
+ const depSql = cleanupDependencies(this.#config.schema, table, queues, this.#config.noAdvisoryLocks);
6265
+ await this.#executeQuery(depSql);
5169
6266
  }
5170
6267
  }
5171
6268
  };
5172
6269
  //#endregion
5173
6270
  //#region ../../src/bam.ts
5174
- var events$1 = {
6271
+ var events$2 = {
5175
6272
  error: "error",
5176
6273
  bam: "bam"
5177
6274
  };
@@ -5181,7 +6278,7 @@ var Bam = class extends EventEmitter {
5181
6278
  #pollInterval;
5182
6279
  #db;
5183
6280
  #config;
5184
- events = events$1;
6281
+ events = events$2;
5185
6282
  constructor(db, config) {
5186
6283
  super();
5187
6284
  this.#db = db;
@@ -5217,7 +6314,7 @@ var Bam = class extends EventEmitter {
5217
6314
  const { rows } = await this.#db.executeSql(sql);
5218
6315
  if (rows.length === 1) await this.#processCommands();
5219
6316
  } catch (err) {
5220
- this.emit(events$1.error, err);
6317
+ this.emit(events$2.error, err);
5221
6318
  } finally {
5222
6319
  this.#working = false;
5223
6320
  }
@@ -5226,7 +6323,7 @@ var Bam = class extends EventEmitter {
5226
6323
  if (this.#stopped) return;
5227
6324
  const entry = await this.#getNextCommand();
5228
6325
  if (!entry || this.#stopped) return;
5229
- this.emit(events$1.bam, {
6326
+ this.emit(events$2.bam, {
5230
6327
  id: entry.id,
5231
6328
  name: entry.name,
5232
6329
  status: "in_progress",
@@ -5237,7 +6334,7 @@ var Bam = class extends EventEmitter {
5237
6334
  await this.#db.executeSql(entry.command);
5238
6335
  if (this.#stopped) return;
5239
6336
  await this.#markCompleted(entry.id);
5240
- this.emit(events$1.bam, {
6337
+ this.emit(events$2.bam, {
5241
6338
  id: entry.id,
5242
6339
  name: entry.name,
5243
6340
  status: "completed",
@@ -5247,8 +6344,8 @@ var Bam = class extends EventEmitter {
5247
6344
  } catch (err) {
5248
6345
  if (this.#stopped) return;
5249
6346
  await this.#markFailed(entry.id, err);
5250
- this.emit(events$1.error, err);
5251
- this.emit(events$1.bam, {
6347
+ this.emit(events$2.error, err);
6348
+ this.emit(events$2.bam, {
5252
6349
  id: entry.id,
5253
6350
  name: entry.name,
5254
6351
  status: "failed",
@@ -5273,6 +6370,76 @@ var Bam = class extends EventEmitter {
5273
6370
  }
5274
6371
  };
5275
6372
  //#endregion
6373
+ //#region ../../src/notifier.ts
6374
+ var events$1 = {
6375
+ error: "error",
6376
+ warning: "warning"
6377
+ };
6378
+ var WARNING_TYPE = "listen_notify_unavailable";
6379
+ var Notifier = class extends EventEmitter {
6380
+ events = events$1;
6381
+ #db;
6382
+ #manager;
6383
+ #config;
6384
+ #handle = null;
6385
+ #stopped = true;
6386
+ constructor(db, manager, config) {
6387
+ super();
6388
+ this.#db = db;
6389
+ this.#manager = manager;
6390
+ this.#config = config;
6391
+ }
6392
+ get available() {
6393
+ return this.#handle !== null;
6394
+ }
6395
+ async start() {
6396
+ if (!this.#stopped) return;
6397
+ this.#stopped = false;
6398
+ if (this.#config.noListenNotify) {
6399
+ this.emit(events$1.warning, {
6400
+ message: `useListenNotify is not supported on the ${this.#config.backend} backend. Continuing with polling only.`,
6401
+ data: {
6402
+ type: WARNING_TYPE,
6403
+ backend: this.#config.backend
6404
+ }
6405
+ });
6406
+ return;
6407
+ }
6408
+ if (typeof this.#db.listen !== "function") {
6409
+ this.emit(events$1.warning, {
6410
+ message: "useListenNotify is enabled but the database connection does not support LISTEN/NOTIFY. Continuing with polling only.",
6411
+ data: { type: WARNING_TYPE }
6412
+ });
6413
+ return;
6414
+ }
6415
+ try {
6416
+ const { rows } = await this.#db.executeSql(`SELECT ${notifyChannelSql(this.#config.schema)} AS channel`);
6417
+ const channel = rows[0].channel;
6418
+ this.#handle = await this.#db.listen(channel, (payload) => this.#manager.notifyQueue(payload), () => this.#manager.forceFetchLnWorkers());
6419
+ } catch (err) {
6420
+ this.emit(events$1.warning, {
6421
+ message: "Failed to start LISTEN/NOTIFY listener. Continuing with polling only.",
6422
+ data: {
6423
+ type: WARNING_TYPE,
6424
+ error: err?.message
6425
+ }
6426
+ });
6427
+ }
6428
+ }
6429
+ async stop() {
6430
+ if (this.#stopped) return;
6431
+ this.#stopped = true;
6432
+ if (this.#handle) {
6433
+ try {
6434
+ await this.#handle.close();
6435
+ } catch (err) {
6436
+ this.emit(events$1.error, err);
6437
+ }
6438
+ this.#handle = null;
6439
+ }
6440
+ }
6441
+ };
6442
+ //#endregion
5276
6443
  //#region ../../src/db.ts
5277
6444
  var Db = class extends EventEmitter {
5278
6445
  pool;
@@ -5304,6 +6471,78 @@ var Db = class extends EventEmitter {
5304
6471
  assert(this.opened, "Database not opened. Call open() before executing SQL.");
5305
6472
  return await this.pool.query(text, values);
5306
6473
  }
6474
+ async listen(channel, onNotification, onReconnect) {
6475
+ assert(this.opened, "Database not opened. Call open() before listening.");
6476
+ let closed = false;
6477
+ let client = null;
6478
+ let reconnectTimer = null;
6479
+ let attempt = 0;
6480
+ const scheduleReconnect = () => {
6481
+ if (closed || reconnectTimer) return;
6482
+ const backoff = Math.min(3e4, 1e3 * 2 ** Math.min(attempt, 5));
6483
+ attempt++;
6484
+ reconnectTimer = setTimeout(() => {
6485
+ reconnectTimer = null;
6486
+ connect().catch(() => scheduleReconnect());
6487
+ }, backoff);
6488
+ };
6489
+ const connect = async () => {
6490
+ if (closed) return;
6491
+ const next = new pg.Client(this.config);
6492
+ next.on("error", (error) => {
6493
+ this.emit("error", error);
6494
+ if (!closed) {
6495
+ next.removeAllListeners();
6496
+ next.end().catch(() => {});
6497
+ if (client === next) client = null;
6498
+ scheduleReconnect();
6499
+ }
6500
+ });
6501
+ next.on("notification", (msg) => {
6502
+ if (msg.payload !== void 0) onNotification(msg.payload);
6503
+ });
6504
+ client = next;
6505
+ try {
6506
+ await next.connect();
6507
+ await next.query(`LISTEN "${channel}"`);
6508
+ } catch (err) {
6509
+ next.removeAllListeners();
6510
+ await next.end().catch(() => {});
6511
+ if (client === next) client = null;
6512
+ throw err;
6513
+ }
6514
+ attempt = 0;
6515
+ onReconnect();
6516
+ };
6517
+ await connect();
6518
+ return { close: async () => {
6519
+ closed = true;
6520
+ if (reconnectTimer) {
6521
+ clearTimeout(reconnectTimer);
6522
+ reconnectTimer = null;
6523
+ }
6524
+ if (client) {
6525
+ client.removeAllListeners();
6526
+ await client.end().catch(() => {});
6527
+ client = null;
6528
+ }
6529
+ } };
6530
+ }
6531
+ async withTransaction(fn) {
6532
+ assert(this.opened, "Database not opened. Call open() before executing SQL.");
6533
+ const client = await this.pool.connect();
6534
+ try {
6535
+ await client.query("BEGIN");
6536
+ const result = await fn({ executeSql: (text, values) => client.query(text, values) });
6537
+ await client.query("COMMIT");
6538
+ return result;
6539
+ } catch (err) {
6540
+ await client.query("ROLLBACK");
6541
+ throw err;
6542
+ } finally {
6543
+ client.release();
6544
+ }
6545
+ }
5307
6546
  };
5308
6547
  //#endregion
5309
6548
  //#region ../../src/index.ts
@@ -5326,6 +6565,7 @@ var PgBoss = class extends EventEmitter {
5326
6565
  #manager;
5327
6566
  #timekeeper;
5328
6567
  #bam;
6568
+ #notifier;
5329
6569
  constructor(value) {
5330
6570
  super();
5331
6571
  this.#stoppingOn = null;
@@ -5341,15 +6581,19 @@ var PgBoss = class extends EventEmitter {
5341
6581
  const timekeeper = new Timekeeper(db, manager, config);
5342
6582
  manager.timekeeper = timekeeper;
5343
6583
  const bam = new Bam(db, config);
6584
+ const notifier = new Notifier(db, manager, config);
6585
+ manager.notifier = notifier;
5344
6586
  this.#promoteEvents(manager);
5345
6587
  this.#promoteEvents(boss);
5346
6588
  this.#promoteEvents(timekeeper);
5347
6589
  this.#promoteEvents(bam);
6590
+ this.#promoteEvents(notifier);
5348
6591
  this.#boss = boss;
5349
6592
  this.#contractor = contractor;
5350
6593
  this.#manager = manager;
5351
6594
  this.#timekeeper = timekeeper;
5352
6595
  this.#bam = bam;
6596
+ this.#notifier = notifier;
5353
6597
  }
5354
6598
  #promoteEvents(emitter) {
5355
6599
  for (const event of Object.values(emitter?.events)) emitter.on(event, (arg) => this.emit(event, arg));
@@ -5357,23 +6601,43 @@ var PgBoss = class extends EventEmitter {
5357
6601
  async start() {
5358
6602
  if (this.#starting || this.#started) return this;
5359
6603
  this.#starting = true;
5360
- if (this.#db._pgbdb && !this.#db.opened) await this.#db.open();
5361
- if (this.#config.migrate) await this.#contractor.start();
5362
- else await this.#contractor.check();
5363
- await this.#manager.start();
5364
- if (this.#config.supervise) await this.#boss.start();
5365
- if (this.#config.schedule) await this.#timekeeper.start();
5366
- if (this.#config.migrate) await this.#bam.start();
6604
+ try {
6605
+ if (this.#db._pgbdb && !this.#db.opened) await this.#db.open();
6606
+ await this.#warnIfDistributedMisconfigured();
6607
+ if (this.#config.migrate) await this.#contractor.start();
6608
+ else await this.#contractor.check();
6609
+ await this.#manager.start();
6610
+ if (this.#config.useListenNotify) await this.#notifier.start();
6611
+ if (this.#config.supervise) await this.#boss.start();
6612
+ if (this.#config.schedule) await this.#timekeeper.start();
6613
+ if (this.#config.migrate) await this.#bam.start();
6614
+ } catch (err) {
6615
+ this.#starting = false;
6616
+ throw err;
6617
+ }
5367
6618
  this.#starting = false;
5368
6619
  this.#started = true;
5369
6620
  this.#stopped = false;
5370
6621
  return this;
5371
6622
  }
6623
+ async #warnIfDistributedMisconfigured() {
6624
+ try {
6625
+ const { rows } = await this.#db.executeSql("SELECT version()");
6626
+ const version = rows?.[0]?.version || "";
6627
+ if (/yugabyte|-yb-/i.test(version)) {
6628
+ if (!this.#config.noTablePartitioning || !this.#config.noAdvisoryLocks) this.emit(events.warning, {
6629
+ message: "YugabyteDB detected: set backend: 'yugabytedb' for compatibility. Partitioned queues (partition: true) are not supported on YugabyteDB.",
6630
+ data: { backend: "yugabytedb" }
6631
+ });
6632
+ }
6633
+ } catch {}
6634
+ }
5372
6635
  async stop(options = {}) {
5373
6636
  if (this.#stoppingOn || this.#stopped) return;
5374
6637
  let { close = true, graceful = true, timeout = 3e4 } = options;
5375
6638
  timeout = Math.max(timeout, 1e3);
5376
6639
  this.#stoppingOn = Date.now();
6640
+ await this.#notifier.stop();
5377
6641
  await this.#manager.stop();
5378
6642
  await this.#timekeeper.stop();
5379
6643
  await this.#boss.stop();
@@ -5408,6 +6672,9 @@ var PgBoss = class extends EventEmitter {
5408
6672
  insert(name, jobs, options) {
5409
6673
  return this.#manager.insert(name, jobs, options);
5410
6674
  }
6675
+ flow(jobs, options) {
6676
+ return this.#manager.flow(jobs, options);
6677
+ }
5411
6678
  fetch(name, options = {}) {
5412
6679
  return this.#manager.fetch(name, options);
5413
6680
  }
@@ -5474,6 +6741,12 @@ var PgBoss = class extends EventEmitter {
5474
6741
  getBlockedKeys(name) {
5475
6742
  return this.#manager.getBlockedKeys(name);
5476
6743
  }
6744
+ getDependencies(name, id, options) {
6745
+ return this.#manager.getDependencies(name, id, options);
6746
+ }
6747
+ getDependents(name, id, options) {
6748
+ return this.#manager.getDependents(name, id, options);
6749
+ }
5477
6750
  updateQueue(name, options) {
5478
6751
  return this.#manager.updateQueue(name, options);
5479
6752
  }
@@ -5531,7 +6804,7 @@ var PgBoss = class extends EventEmitter {
5531
6804
  return rows;
5532
6805
  }
5533
6806
  async getBamEntries() {
5534
- const sql = getBamEntries(this.#config.schema);
6807
+ const sql = getBamEntries$1(this.#config.schema);
5535
6808
  const { rows } = await this.#db.executeSql(sql);
5536
6809
  return rows;
5537
6810
  }
@@ -5626,7 +6899,9 @@ var QUEUE_COLUMNS = `
5626
6899
  deletion_seconds as "deleteAfterSeconds",
5627
6900
  deferred_count as "deferredCount",
5628
6901
  queued_count as "queuedCount",
6902
+ GREATEST(queued_count - deferred_count, 0) as "readyCount",
5629
6903
  active_count as "activeCount",
6904
+ failed_count as "failedCount",
5630
6905
  total_count as "totalCount",
5631
6906
  warning_queued as "warningQueueSize",
5632
6907
  singletons_active as "singletonsActive",
@@ -5879,19 +7154,81 @@ async function getWarningCount(dbUrl, schema, type) {
5879
7154
  throw err;
5880
7155
  }
5881
7156
  }
7157
+ async function getBamEntries(dbUrl, schema, options = {}) {
7158
+ const s = validateIdentifier(schema);
7159
+ const { status = null, limit = 200, offset = 0 } = options;
7160
+ const sql = `
7161
+ SELECT
7162
+ id,
7163
+ name,
7164
+ version,
7165
+ status,
7166
+ queue,
7167
+ table_name as "table",
7168
+ command,
7169
+ error,
7170
+ created_on as "createdOn",
7171
+ started_on as "startedOn",
7172
+ completed_on as "completedOn"
7173
+ FROM ${s}.bam
7174
+ WHERE ($1::text IS NULL OR status = $1)
7175
+ ORDER BY version DESC, created_on DESC
7176
+ LIMIT $2 OFFSET $3
7177
+ `;
7178
+ try {
7179
+ return await query(dbUrl, sql, [
7180
+ status,
7181
+ limit,
7182
+ offset
7183
+ ]);
7184
+ } catch (err) {
7185
+ if (err && typeof err === "object" && "code" in err && err.code === "42P01") return [];
7186
+ throw err;
7187
+ }
7188
+ }
7189
+ async function getBamCount(dbUrl, schema, status) {
7190
+ const sql = `
7191
+ SELECT COUNT(*)::int as count
7192
+ FROM ${validateIdentifier(schema)}.bam
7193
+ WHERE ($1::text IS NULL OR status = $1)
7194
+ `;
7195
+ try {
7196
+ return (await queryOne(dbUrl, sql, [status ?? null]))?.count ?? 0;
7197
+ } catch (err) {
7198
+ if (err && typeof err === "object" && "code" in err && err.code === "42P01") return 0;
7199
+ throw err;
7200
+ }
7201
+ }
7202
+ async function getBamStatusSummary(dbUrl, schema) {
7203
+ const sql = `
7204
+ SELECT status, count(*)::int as count, max(created_on) as "lastCreatedOn"
7205
+ FROM ${validateIdentifier(schema)}.bam
7206
+ GROUP BY status
7207
+ `;
7208
+ try {
7209
+ return await query(dbUrl, sql);
7210
+ } catch (err) {
7211
+ if (err && typeof err === "object" && "code" in err && err.code === "42P01") return [];
7212
+ throw err;
7213
+ }
7214
+ }
5882
7215
  async function getQueueStats(dbUrl, schema) {
5883
7216
  return await queryOne(dbUrl, `
5884
7217
  SELECT
5885
7218
  COALESCE(SUM(deferred_count), 0)::int as "totalDeferred",
5886
7219
  COALESCE(SUM(queued_count), 0)::int as "totalQueued",
7220
+ COALESCE(SUM(GREATEST(queued_count - deferred_count, 0)), 0)::int as "totalReady",
5887
7221
  COALESCE(SUM(active_count), 0)::int as "totalActive",
7222
+ COALESCE(SUM(failed_count), 0)::int as "totalFailed",
5888
7223
  COALESCE(SUM(total_count), 0)::int as "totalJobs",
5889
7224
  COUNT(*)::int as "queueCount"
5890
7225
  FROM ${validateIdentifier(schema)}.queue
5891
7226
  `) ?? {
5892
7227
  totalDeferred: 0,
5893
7228
  totalQueued: 0,
7229
+ totalReady: 0,
5894
7230
  totalActive: 0,
7231
+ totalFailed: 0,
5895
7232
  totalJobs: 0,
5896
7233
  queueCount: 0
5897
7234
  };
@@ -5992,9 +7329,21 @@ var statCards = [
5992
7329
  {
5993
7330
  name: "Queued Jobs",
5994
7331
  key: "totalQueued",
5995
- hint: "waiting to process",
7332
+ hint: "incl. deferred",
7333
+ accent: "neutral"
7334
+ },
7335
+ {
7336
+ name: "Deferred",
7337
+ key: "totalDeferred",
7338
+ hint: "scheduled for later",
5996
7339
  accent: "neutral"
5997
7340
  },
7341
+ {
7342
+ name: "Ready",
7343
+ key: "totalReady",
7344
+ hint: "ready to process",
7345
+ accent: "primary"
7346
+ },
5998
7347
  {
5999
7348
  name: "Active",
6000
7349
  key: "totalActive",
@@ -6002,9 +7351,9 @@ var statCards = [
6002
7351
  accent: "primary"
6003
7352
  },
6004
7353
  {
6005
- name: "Deferred",
6006
- key: "totalDeferred",
6007
- hint: "scheduled for later",
7354
+ name: "Failed",
7355
+ key: "totalFailed",
7356
+ hint: "recent failures",
6008
7357
  accent: "neutral"
6009
7358
  },
6010
7359
  {
@@ -6216,34 +7565,45 @@ function ErrorCard({ title, message = "Please check your database connection and
6216
7565
  //#endregion
6217
7566
  //#region app/routes/_index.tsx
6218
7567
  var _index_exports = /* @__PURE__ */ __exportAll({
6219
- ErrorBoundary: () => ErrorBoundary$10,
7568
+ ErrorBoundary: () => ErrorBoundary$11,
6220
7569
  default: () => _index_default,
6221
- loader: () => loader$10
7570
+ loader: () => loader$11
6222
7571
  });
6223
- async function loader$10({ context }) {
7572
+ async function loader$11({ context }) {
6224
7573
  const { DB_URL, SCHEMA } = context.get(dbContext);
6225
- const [warnings, stats, topQueues, totalQueues, problemQueuesCount] = await Promise.all([
7574
+ const [warnings, stats, topQueues, totalQueues, problemQueuesCount, bamSummary] = await Promise.all([
6226
7575
  getWarnings(DB_URL, SCHEMA, { limit: 5 }),
6227
7576
  getQueueStats(DB_URL, SCHEMA),
6228
7577
  getTopQueues(DB_URL, SCHEMA, 5),
6229
7578
  getQueueCount(DB_URL, SCHEMA),
6230
- getProblemQueuesCount(DB_URL, SCHEMA)
7579
+ getProblemQueuesCount(DB_URL, SCHEMA),
7580
+ getBamStatusSummary(DB_URL, SCHEMA)
6231
7581
  ]);
6232
7582
  return {
6233
7583
  stats,
6234
7584
  warnings,
6235
7585
  topQueues,
7586
+ migrations: bamSummary.reduce((acc, row) => {
7587
+ if (row.status === "pending") acc.pending += row.count;
7588
+ else if (row.status === "in_progress") acc.inProgress += row.count;
7589
+ else if (row.status === "failed") acc.failed += row.count;
7590
+ return acc;
7591
+ }, {
7592
+ pending: 0,
7593
+ inProgress: 0,
7594
+ failed: 0
7595
+ }),
6236
7596
  queueStats: {
6237
7597
  totalQueues,
6238
7598
  problemQueues: problemQueuesCount
6239
7599
  }
6240
7600
  };
6241
7601
  }
6242
- var ErrorBoundary$10 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
7602
+ var ErrorBoundary$11 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
6243
7603
  return /* @__PURE__ */ jsx(ErrorCard, { title: "Failed to load dashboard" });
6244
7604
  });
6245
7605
  var _index_default = UNSAFE_withComponentProps(function Overview({ loaderData }) {
6246
- const { stats, warnings, topQueues } = loaderData;
7606
+ const { stats, warnings, topQueues, migrations } = loaderData;
6247
7607
  return /* @__PURE__ */ jsxs("div", { children: [
6248
7608
  /* @__PURE__ */ jsx(PageHeader, {
6249
7609
  title: "Overview",
@@ -6258,9 +7618,10 @@ var _index_default = UNSAFE_withComponentProps(function Overview({ loaderData })
6258
7618
  })
6259
7619
  }),
6260
7620
  /* @__PURE__ */ jsx("div", {
6261
- className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4 mb-4",
7621
+ className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 mb-4",
6262
7622
  children: /* @__PURE__ */ jsx(StatsCards, { stats })
6263
7623
  }),
7624
+ /* @__PURE__ */ jsx(MigrationsBanner, { migrations }),
6264
7625
  /* @__PURE__ */ jsxs("div", {
6265
7626
  className: "grid grid-cols-1 lg:grid-cols-[1.5fr_1fr] gap-4",
6266
7627
  children: [/* @__PURE__ */ jsxs(Card, { children: [/* @__PURE__ */ jsxs(CardHeader, { children: [/* @__PURE__ */ jsx(CardTitle, { children: "Top Queues" }), /* @__PURE__ */ jsx(DbLink, {
@@ -6329,6 +7690,37 @@ var _index_default = UNSAFE_withComponentProps(function Overview({ loaderData })
6329
7690
  })
6330
7691
  ] });
6331
7692
  });
7693
+ function MigrationsBanner({ migrations }) {
7694
+ const { pending, inProgress, failed } = migrations;
7695
+ if (pending === 0 && inProgress === 0 && failed === 0) return null;
7696
+ const parts = [];
7697
+ if (pending > 0) parts.push(`${pending.toLocaleString()} pending`);
7698
+ if (inProgress > 0) parts.push(`${inProgress.toLocaleString()} in progress`);
7699
+ if (failed > 0) parts.push(`${failed.toLocaleString()} failed`);
7700
+ return /* @__PURE__ */ jsx(DbLink, {
7701
+ to: "/migrations",
7702
+ className: "block mb-4",
7703
+ children: /* @__PURE__ */ jsxs(Card, {
7704
+ className: "flex items-center gap-3 px-4 py-3 hover:bg-[var(--surface-hover)]",
7705
+ children: [
7706
+ /* @__PURE__ */ jsx(Badge, {
7707
+ variant: failed > 0 ? "error" : "warning",
7708
+ size: "sm",
7709
+ dot: true,
7710
+ children: "Async migrations"
7711
+ }),
7712
+ /* @__PURE__ */ jsx("span", {
7713
+ className: "text-sm text-[var(--text-secondary)]",
7714
+ children: parts.join(" · ")
7715
+ }),
7716
+ /* @__PURE__ */ jsx("span", {
7717
+ className: "ml-auto text-sm font-medium text-primary-600 dark:text-primary-400",
7718
+ children: "View"
7719
+ })
7720
+ ]
7721
+ })
7722
+ });
7723
+ }
6332
7724
  function QueueStatusBadge({ queue }) {
6333
7725
  if ((queue.warningQueueSize ?? 0) > 0 && queue.queuedCount > (queue.warningQueueSize ?? 0)) return /* @__PURE__ */ jsx(Badge, {
6334
7726
  variant: "error",
@@ -6843,10 +8235,10 @@ function SearchIcon$1({ className }) {
6843
8235
  //#endregion
6844
8236
  //#region app/routes/jobs.tsx
6845
8237
  var jobs_exports = /* @__PURE__ */ __exportAll({
6846
- ErrorBoundary: () => ErrorBoundary$9,
8238
+ ErrorBoundary: () => ErrorBoundary$10,
6847
8239
  buildSearchParams: () => buildSearchParams,
6848
8240
  default: () => jobs_default,
6849
- loader: () => loader$9,
8241
+ loader: () => loader$10,
6850
8242
  parseFiltersFromUrl: () => parseFiltersFromUrl
6851
8243
  });
6852
8244
  function parseFiltersFromUrl(searchParams) {
@@ -6883,7 +8275,7 @@ function parseFiltersFromUrl(searchParams) {
6883
8275
  shouldRunCount
6884
8276
  };
6885
8277
  }
6886
- async function loader$9({ request, context }) {
8278
+ async function loader$10({ request, context }) {
6887
8279
  const { DB_URL, SCHEMA } = context.get(dbContext);
6888
8280
  const url = new URL(request.url);
6889
8281
  const parsed = parseFiltersFromUrl(url.searchParams);
@@ -6931,7 +8323,7 @@ async function loader$9({ request, context }) {
6931
8323
  hasPrevPage
6932
8324
  };
6933
8325
  }
6934
- var ErrorBoundary$9 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
8326
+ var ErrorBoundary$10 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
6935
8327
  return /* @__PURE__ */ jsx(ErrorCard, { title: "Failed to load jobs" });
6936
8328
  });
6937
8329
  function buildSearchParams(filters) {
@@ -7111,11 +8503,11 @@ function Chip({ label, onRemove }) {
7111
8503
  //#endregion
7112
8504
  //#region app/routes/queues._index.tsx
7113
8505
  var queues__index_exports = /* @__PURE__ */ __exportAll({
7114
- ErrorBoundary: () => ErrorBoundary$8,
8506
+ ErrorBoundary: () => ErrorBoundary$9,
7115
8507
  default: () => queues__index_default,
7116
- loader: () => loader$8
8508
+ loader: () => loader$9
7117
8509
  });
7118
- async function loader$8({ request, context }) {
8510
+ async function loader$9({ request, context }) {
7119
8511
  const { DB_URL, SCHEMA } = context.get(dbContext);
7120
8512
  const url = new URL(request.url);
7121
8513
  const page = parsePageNumber(url.searchParams.get("page"));
@@ -7148,7 +8540,7 @@ async function loader$8({ request, context }) {
7148
8540
  search
7149
8541
  };
7150
8542
  }
7151
- var ErrorBoundary$8 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
8543
+ var ErrorBoundary$9 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
7152
8544
  return /* @__PURE__ */ jsx("div", {
7153
8545
  className: "p-6",
7154
8546
  children: /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs(CardContent, {
@@ -7314,13 +8706,21 @@ var queues__index_default = UNSAFE_withComponentProps(function QueuesIndex({ loa
7314
8706
  className: "text-right",
7315
8707
  children: "Queued"
7316
8708
  }),
8709
+ /* @__PURE__ */ jsx(TableHead, {
8710
+ className: "text-right",
8711
+ children: "Deferred"
8712
+ }),
8713
+ /* @__PURE__ */ jsx(TableHead, {
8714
+ className: "text-right",
8715
+ children: "Ready"
8716
+ }),
7317
8717
  /* @__PURE__ */ jsx(TableHead, {
7318
8718
  className: "text-right",
7319
8719
  children: "Active"
7320
8720
  }),
7321
8721
  /* @__PURE__ */ jsx(TableHead, {
7322
8722
  className: "text-right",
7323
- children: "Deferred"
8723
+ children: "Failed"
7324
8724
  }),
7325
8725
  /* @__PURE__ */ jsx(TableHead, {
7326
8726
  className: "text-right",
@@ -7330,7 +8730,7 @@ var queues__index_default = UNSAFE_withComponentProps(function QueuesIndex({ loa
7330
8730
  /* @__PURE__ */ jsx(TableHead, { children: "Status" })
7331
8731
  ] }) }), /* @__PURE__ */ jsx(TableBody, { children: queues.length === 0 ? /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(TableCell, {
7332
8732
  className: "text-center text-[var(--text-tertiary)] py-8",
7333
- colSpan: 9,
8733
+ colSpan: 11,
7334
8734
  children: "No queues found"
7335
8735
  }) }) : queues.map((queue) => {
7336
8736
  const hasBacklog = (queue.warningQueueSize ?? 0) > 0 && queue.queuedCount > (queue.warningQueueSize ?? 0);
@@ -7353,13 +8753,21 @@ var queues__index_default = UNSAFE_withComponentProps(function QueuesIndex({ loa
7353
8753
  className: "text-right pgb-num text-[var(--text-primary)]",
7354
8754
  children: queue.queuedCount.toLocaleString()
7355
8755
  }),
8756
+ /* @__PURE__ */ jsx(TableCell, {
8757
+ className: "text-right pgb-num text-[var(--text-primary)]",
8758
+ children: queue.deferredCount.toLocaleString()
8759
+ }),
8760
+ /* @__PURE__ */ jsx(TableCell, {
8761
+ className: "text-right pgb-num text-[var(--text-primary)]",
8762
+ children: queue.readyCount.toLocaleString()
8763
+ }),
7356
8764
  /* @__PURE__ */ jsx(TableCell, {
7357
8765
  className: "text-right pgb-num text-[var(--text-primary)]",
7358
8766
  children: queue.activeCount.toLocaleString()
7359
8767
  }),
7360
8768
  /* @__PURE__ */ jsx(TableCell, {
7361
8769
  className: "text-right pgb-num text-[var(--text-primary)]",
7362
- children: queue.deferredCount.toLocaleString()
8770
+ children: queue.failedCount.toLocaleString()
7363
8771
  }),
7364
8772
  /* @__PURE__ */ jsx(TableCell, {
7365
8773
  className: "text-right pgb-num text-[var(--text-primary)]",
@@ -7537,12 +8945,12 @@ var SelectContent = ({ children }) => /* @__PURE__ */ jsx(Fragment, { children }
7537
8945
  //#endregion
7538
8946
  //#region app/routes/queues.create.tsx
7539
8947
  var queues_create_exports = /* @__PURE__ */ __exportAll({
7540
- ErrorBoundary: () => ErrorBoundary$7,
8948
+ ErrorBoundary: () => ErrorBoundary$8,
7541
8949
  action: () => action$5,
7542
8950
  default: () => queues_create_default,
7543
- loader: () => loader$7
8951
+ loader: () => loader$8
7544
8952
  });
7545
- async function loader$7({ context }) {
8953
+ async function loader$8({ context }) {
7546
8954
  const { DB_URL, SCHEMA } = context.get(dbContext);
7547
8955
  return { queues: await getQueues(DB_URL, SCHEMA) };
7548
8956
  }
@@ -7610,7 +9018,7 @@ async function action$5({ request, context }) {
7610
9018
  const dbParam = new URL(request.url).searchParams.get("db");
7611
9019
  return redirect(dbParam ? `/queues/${encodeURIComponent(queueName.trim())}?db=${encodeURIComponent(dbParam)}` : `/queues/${encodeURIComponent(queueName.trim())}`);
7612
9020
  }
7613
- var ErrorBoundary$7 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
9021
+ var ErrorBoundary$8 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
7614
9022
  return /* @__PURE__ */ jsx(ErrorCard, {
7615
9023
  title: "Failed to load queue creation page",
7616
9024
  backTo: {
@@ -8058,12 +9466,12 @@ DialogDescription.displayName = "DialogDescription";
8058
9466
  //#endregion
8059
9467
  //#region app/routes/queues.$name.tsx
8060
9468
  var queues_$name_exports = /* @__PURE__ */ __exportAll({
8061
- ErrorBoundary: () => ErrorBoundary$6,
9469
+ ErrorBoundary: () => ErrorBoundary$7,
8062
9470
  action: () => action$4,
8063
9471
  default: () => queues_$name_default,
8064
- loader: () => loader$6
9472
+ loader: () => loader$7
8065
9473
  });
8066
- async function loader$6({ params, request, context }) {
9474
+ async function loader$7({ params, request, context }) {
8067
9475
  const { DB_URL, SCHEMA } = context.get(dbContext);
8068
9476
  const url = new URL(request.url);
8069
9477
  const stateParam = url.searchParams.get("state");
@@ -8136,7 +9544,7 @@ async function action$4({ params, request, context }) {
8136
9544
  message
8137
9545
  };
8138
9546
  }
8139
- var ErrorBoundary$6 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
9547
+ var ErrorBoundary$7 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
8140
9548
  return /* @__PURE__ */ jsx(ErrorCard, {
8141
9549
  title: "Failed to load queue",
8142
9550
  backTo: {
@@ -8195,13 +9603,23 @@ var queues_$name_default = UNSAFE_withComponentProps(function QueueDetail({ load
8195
9603
  ]
8196
9604
  }),
8197
9605
  /* @__PURE__ */ jsxs("div", {
8198
- className: "grid grid-cols-2 sm:grid-cols-4 gap-4",
9606
+ className: "grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-4",
8199
9607
  children: [
8200
9608
  /* @__PURE__ */ jsx(StatCard, {
8201
9609
  label: "Queued",
8202
9610
  value: queue.queuedCount.toLocaleString(),
8203
9611
  accent: overThreshold ? "error" : "neutral",
8204
- hint: overThreshold ? "over threshold" : "within threshold"
9612
+ hint: overThreshold ? "over threshold" : "incl. deferred"
9613
+ }),
9614
+ /* @__PURE__ */ jsx(StatCard, {
9615
+ label: "Deferred",
9616
+ value: queue.deferredCount.toLocaleString()
9617
+ }),
9618
+ /* @__PURE__ */ jsx(StatCard, {
9619
+ label: "Ready",
9620
+ value: queue.readyCount.toLocaleString(),
9621
+ accent: "primary",
9622
+ hint: "ready to process"
8205
9623
  }),
8206
9624
  /* @__PURE__ */ jsx(StatCard, {
8207
9625
  label: "Active",
@@ -8209,8 +9627,9 @@ var queues_$name_default = UNSAFE_withComponentProps(function QueueDetail({ load
8209
9627
  accent: "primary"
8210
9628
  }),
8211
9629
  /* @__PURE__ */ jsx(StatCard, {
8212
- label: "Deferred",
8213
- value: queue.deferredCount.toLocaleString()
9630
+ label: "Failed",
9631
+ value: queue.failedCount.toLocaleString(),
9632
+ hint: "recent failures"
8214
9633
  }),
8215
9634
  /* @__PURE__ */ jsx(StatCard, {
8216
9635
  label: "Total",
@@ -8556,12 +9975,12 @@ function ConfirmDialog({ title, description, confirmLabel, confirmVariant = "pri
8556
9975
  //#endregion
8557
9976
  //#region app/routes/queues.$name.jobs.$jobId.tsx
8558
9977
  var queues_$name_jobs_$jobId_exports = /* @__PURE__ */ __exportAll({
8559
- ErrorBoundary: () => ErrorBoundary$5,
9978
+ ErrorBoundary: () => ErrorBoundary$6,
8560
9979
  action: () => action$3,
8561
9980
  default: () => queues_$name_jobs_$jobId_default,
8562
- loader: () => loader$5
9981
+ loader: () => loader$6
8563
9982
  });
8564
- async function loader$5({ params, context }) {
9983
+ async function loader$6({ params, context }) {
8565
9984
  const { DB_URL, SCHEMA } = context.get(dbContext);
8566
9985
  const job = await getJobById(DB_URL, SCHEMA, params.name, params.jobId);
8567
9986
  if (!job) throw new Response("Job not found", { status: 404 });
@@ -8615,7 +10034,7 @@ async function action$3({ params, request, context }) {
8615
10034
  message
8616
10035
  };
8617
10036
  }
8618
- var ErrorBoundary$5 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
10037
+ var ErrorBoundary$6 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
8619
10038
  return /* @__PURE__ */ jsx(ErrorCard, {
8620
10039
  title: "Failed to load job",
8621
10040
  backTo: {
@@ -8868,11 +10287,11 @@ function ConfigItem({ label, value, mono = false }) {
8868
10287
  //#endregion
8869
10288
  //#region app/routes/schedules.tsx
8870
10289
  var schedules_exports = /* @__PURE__ */ __exportAll({
8871
- ErrorBoundary: () => ErrorBoundary$4,
10290
+ ErrorBoundary: () => ErrorBoundary$5,
8872
10291
  default: () => schedules_default,
8873
- loader: () => loader$4
10292
+ loader: () => loader$5
8874
10293
  });
8875
- async function loader$4({ request, context }) {
10294
+ async function loader$5({ request, context }) {
8876
10295
  const { DB_URL, SCHEMA } = context.get(dbContext);
8877
10296
  const page = parsePageNumber(new URL(request.url).searchParams.get("page"));
8878
10297
  const limit = 20;
@@ -8890,7 +10309,7 @@ async function loader$4({ request, context }) {
8890
10309
  hasPrevPage: page > 1
8891
10310
  };
8892
10311
  }
8893
- var ErrorBoundary$4 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
10312
+ var ErrorBoundary$5 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
8894
10313
  return /* @__PURE__ */ jsx(ErrorCard, { title: "Failed to load schedules" });
8895
10314
  });
8896
10315
  function cronHuman(cron) {
@@ -9000,12 +10419,12 @@ var schedules_default = UNSAFE_withComponentProps(function Schedules({ loaderDat
9000
10419
  //#endregion
9001
10420
  //#region app/routes/schedules.$name.$key.tsx
9002
10421
  var schedules_$name_$key_exports = /* @__PURE__ */ __exportAll({
9003
- ErrorBoundary: () => ErrorBoundary$3,
10422
+ ErrorBoundary: () => ErrorBoundary$4,
9004
10423
  action: () => action$2,
9005
10424
  default: () => schedules_$name_$key_default,
9006
- loader: () => loader$3
10425
+ loader: () => loader$4
9007
10426
  });
9008
- async function loader$3({ params, context }) {
10427
+ async function loader$4({ params, context }) {
9009
10428
  const { DB_URL, SCHEMA } = context.get(dbContext);
9010
10429
  const key = params.key === "__default__" ? "" : params.key;
9011
10430
  const schedule = await getSchedule(DB_URL, SCHEMA, params.name, key);
@@ -9023,7 +10442,7 @@ async function action$2({ params, request, context }) {
9023
10442
  }
9024
10443
  return { error: "Invalid action" };
9025
10444
  }
9026
- var ErrorBoundary$3 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
10445
+ var ErrorBoundary$4 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
9027
10446
  return /* @__PURE__ */ jsx(ErrorCard, {
9028
10447
  title: "Failed to load schedule",
9029
10448
  backTo: {
@@ -9183,12 +10602,12 @@ var schedules_$name_$key_default = UNSAFE_withComponentProps(function ScheduleDe
9183
10602
  //#endregion
9184
10603
  //#region app/routes/schedules.new.tsx
9185
10604
  var schedules_new_exports = /* @__PURE__ */ __exportAll({
9186
- ErrorBoundary: () => ErrorBoundary$2,
10605
+ ErrorBoundary: () => ErrorBoundary$3,
9187
10606
  action: () => action$1,
9188
10607
  default: () => schedules_new_default,
9189
- loader: () => loader$2
10608
+ loader: () => loader$3
9190
10609
  });
9191
- async function loader$2({ context }) {
10610
+ async function loader$3({ context }) {
9192
10611
  const { DB_URL, SCHEMA } = context.get(dbContext);
9193
10612
  return { queues: await getQueues(DB_URL, SCHEMA) };
9194
10613
  }
@@ -9241,7 +10660,7 @@ async function action$1({ request, context }) {
9241
10660
  const dbParam = new URL(request.url).searchParams.get("db");
9242
10661
  return redirect(dbParam ? `/schedules?db=${encodeURIComponent(dbParam)}` : `/schedules`);
9243
10662
  }
9244
- var ErrorBoundary$2 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
10663
+ var ErrorBoundary$3 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
9245
10664
  return /* @__PURE__ */ jsx(ErrorCard, {
9246
10665
  title: "Failed to load schedule creation",
9247
10666
  backTo: {
@@ -9507,12 +10926,12 @@ var schedules_new_default = UNSAFE_withComponentProps(function CreateSchedule({
9507
10926
  //#endregion
9508
10927
  //#region app/routes/send.tsx
9509
10928
  var send_exports = /* @__PURE__ */ __exportAll({
9510
- ErrorBoundary: () => ErrorBoundary$1,
10929
+ ErrorBoundary: () => ErrorBoundary$2,
9511
10930
  action: () => action,
9512
10931
  default: () => send_default,
9513
- loader: () => loader$1
10932
+ loader: () => loader$2
9514
10933
  });
9515
- async function loader$1({ context }) {
10934
+ async function loader$2({ context }) {
9516
10935
  const { DB_URL, SCHEMA } = context.get(dbContext);
9517
10936
  return { queues: await getQueues(DB_URL, SCHEMA) };
9518
10937
  }
@@ -9562,7 +10981,7 @@ async function action({ request, context }) {
9562
10981
  const dbParam = new URL(request.url).searchParams.get("db");
9563
10982
  return redirect(dbParam ? `/queues/${encodeURIComponent(queueName.trim())}?db=${encodeURIComponent(dbParam)}` : `/queues/${encodeURIComponent(queueName.trim())}`);
9564
10983
  }
9565
- var ErrorBoundary$1 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
10984
+ var ErrorBoundary$2 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
9566
10985
  return /* @__PURE__ */ jsx(ErrorCard, {
9567
10986
  title: "Failed to load job sending page",
9568
10987
  backTo: {
@@ -9794,6 +11213,182 @@ var send_default = UNSAFE_withComponentProps(function SendJob({ loaderData }) {
9794
11213
  });
9795
11214
  });
9796
11215
  //#endregion
11216
+ //#region app/routes/migrations.tsx
11217
+ var migrations_exports = /* @__PURE__ */ __exportAll({
11218
+ ErrorBoundary: () => ErrorBoundary$1,
11219
+ default: () => migrations_default,
11220
+ loader: () => loader$1
11221
+ });
11222
+ var PAGE_SIZE = 50;
11223
+ var STATUS_ACCENT = {
11224
+ pending: "warning",
11225
+ in_progress: "primary",
11226
+ completed: "success",
11227
+ failed: "error"
11228
+ };
11229
+ async function loader$1({ request, context }) {
11230
+ const { DB_URL, SCHEMA } = context.get(dbContext);
11231
+ const url = new URL(request.url);
11232
+ const statusParam = url.searchParams.get("status");
11233
+ const statusFilter = isValidBamStatus(statusParam) ? statusParam : null;
11234
+ const page = parsePageNumber(url.searchParams.get("page"));
11235
+ const offset = (page - 1) * PAGE_SIZE;
11236
+ const [entries, totalCount, summary] = await Promise.all([
11237
+ getBamEntries(DB_URL, SCHEMA, {
11238
+ status: statusFilter,
11239
+ limit: PAGE_SIZE,
11240
+ offset
11241
+ }),
11242
+ getBamCount(DB_URL, SCHEMA, statusFilter),
11243
+ getBamStatusSummary(DB_URL, SCHEMA)
11244
+ ]);
11245
+ return {
11246
+ entries,
11247
+ summary,
11248
+ statusFilter,
11249
+ page,
11250
+ totalPages: Math.ceil(totalCount / PAGE_SIZE)
11251
+ };
11252
+ }
11253
+ var ErrorBoundary$1 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
11254
+ return /* @__PURE__ */ jsx(ErrorCard, {
11255
+ title: "Failed to load migrations",
11256
+ backTo: {
11257
+ href: "/",
11258
+ label: "Back to Dashboard"
11259
+ }
11260
+ });
11261
+ });
11262
+ var migrations_default = UNSAFE_withComponentProps(function Migrations({ loaderData }) {
11263
+ const { entries, summary, statusFilter, page, totalPages } = loaderData;
11264
+ const [searchParams, setSearchParams] = useSearchParams();
11265
+ const counts = countByStatus(summary);
11266
+ const handleFilterChange = (key, value) => {
11267
+ const params = new URLSearchParams(searchParams);
11268
+ if (value) params.set(key, value);
11269
+ else params.delete(key);
11270
+ params.delete("page");
11271
+ setSearchParams(params);
11272
+ };
11273
+ const handlePageChange = (newPage) => {
11274
+ const params = new URLSearchParams(searchParams);
11275
+ params.set("page", newPage.toString());
11276
+ setSearchParams(params);
11277
+ };
11278
+ return /* @__PURE__ */ jsxs("div", {
11279
+ className: "space-y-4",
11280
+ children: [
11281
+ /* @__PURE__ */ jsx(PageHeader, {
11282
+ title: "Migrations",
11283
+ subtitle: "Background async migrations (BAM) — schema changes such as concurrent index builds that run outside the install transaction"
11284
+ }),
11285
+ /* @__PURE__ */ jsx("div", {
11286
+ className: "grid grid-cols-2 gap-4 lg:grid-cols-4",
11287
+ children: BAM_STATUSES.map((status) => /* @__PURE__ */ jsx(StatCard, {
11288
+ label: BAM_STATUS_LABELS[status],
11289
+ value: counts[status].toLocaleString(),
11290
+ accent: counts[status] > 0 ? STATUS_ACCENT[status] : "neutral"
11291
+ }, status))
11292
+ }),
11293
+ /* @__PURE__ */ jsxs(Card, { children: [
11294
+ /* @__PURE__ */ jsxs(CardHeader, { children: [/* @__PURE__ */ jsx(CardTitle, { children: "Migration commands" }), /* @__PURE__ */ jsx(FilterSelect, {
11295
+ value: statusFilter,
11296
+ options: BAM_STATUS_OPTIONS,
11297
+ onChange: (value) => handleFilterChange("status", value)
11298
+ })] }),
11299
+ /* @__PURE__ */ jsx(CardContent, {
11300
+ className: "p-0",
11301
+ children: /* @__PURE__ */ jsxs(Table, { children: [/* @__PURE__ */ jsx(TableHeader, { children: /* @__PURE__ */ jsxs(TableRow, { children: [
11302
+ /* @__PURE__ */ jsx(TableHead, { children: "Name" }),
11303
+ /* @__PURE__ */ jsx(TableHead, {
11304
+ className: "text-right",
11305
+ children: "Ver"
11306
+ }),
11307
+ /* @__PURE__ */ jsx(TableHead, { children: "Status" }),
11308
+ /* @__PURE__ */ jsx(TableHead, { children: "Table" }),
11309
+ /* @__PURE__ */ jsx(TableHead, { children: "Created" }),
11310
+ /* @__PURE__ */ jsx(TableHead, { children: "Started" }),
11311
+ /* @__PURE__ */ jsx(TableHead, { children: "Completed" }),
11312
+ /* @__PURE__ */ jsx(TableHead, { children: "Command / Error" })
11313
+ ] }) }), /* @__PURE__ */ jsx(TableBody, { children: entries.length === 0 ? /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(TableCell, {
11314
+ className: "text-center text-[var(--text-tertiary)] py-8",
11315
+ colSpan: 8,
11316
+ children: statusFilter ? `No ${BAM_STATUS_LABELS[statusFilter].toLowerCase()} migrations found` : "No async migrations recorded."
11317
+ }) }) : entries.map((entry) => /* @__PURE__ */ jsxs(TableRow, { children: [
11318
+ /* @__PURE__ */ jsx(TableCell, {
11319
+ className: "text-[var(--text-primary)] font-medium",
11320
+ children: entry.name
11321
+ }),
11322
+ /* @__PURE__ */ jsx(TableCell, {
11323
+ className: "text-right pgb-num text-[var(--text-tertiary)]",
11324
+ children: entry.version
11325
+ }),
11326
+ /* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsx(Badge, {
11327
+ variant: BAM_STATUS_VARIANTS[entry.status],
11328
+ size: "sm",
11329
+ dot: true,
11330
+ children: BAM_STATUS_LABELS[entry.status]
11331
+ }) }),
11332
+ /* @__PURE__ */ jsxs(TableCell, {
11333
+ className: "font-mono text-xs text-[var(--text-secondary)] whitespace-nowrap",
11334
+ children: [entry.table, entry.queue ? /* @__PURE__ */ jsxs("span", {
11335
+ className: "text-[var(--text-tertiary)]",
11336
+ children: [" · ", entry.queue]
11337
+ }) : null]
11338
+ }),
11339
+ /* @__PURE__ */ jsx(TableCell, {
11340
+ className: "pgb-num text-[var(--text-tertiary)] whitespace-nowrap",
11341
+ children: formatTimestamp(entry.createdOn)
11342
+ }),
11343
+ /* @__PURE__ */ jsx(TableCell, {
11344
+ className: "pgb-num text-[var(--text-tertiary)] whitespace-nowrap",
11345
+ children: formatTimestamp(entry.startedOn)
11346
+ }),
11347
+ /* @__PURE__ */ jsx(TableCell, {
11348
+ className: "pgb-num text-[var(--text-tertiary)] whitespace-nowrap",
11349
+ children: formatTimestamp(entry.completedOn)
11350
+ }),
11351
+ /* @__PURE__ */ jsxs(TableCell, {
11352
+ className: "max-w-md",
11353
+ children: [entry.error ? /* @__PURE__ */ jsx("p", {
11354
+ className: "mb-1 font-mono text-xs text-[var(--error-600)] break-words whitespace-pre-wrap",
11355
+ children: entry.error
11356
+ }) : null, /* @__PURE__ */ jsxs("details", { children: [/* @__PURE__ */ jsx("summary", {
11357
+ className: "cursor-pointer text-xs text-[var(--text-tertiary)] select-none",
11358
+ children: "View command"
11359
+ }), /* @__PURE__ */ jsx("pre", {
11360
+ className: "mt-1 overflow-x-auto rounded bg-[var(--surface-sunken)] p-2 font-mono text-xs text-[var(--text-secondary)] whitespace-pre-wrap",
11361
+ children: entry.command
11362
+ })] })]
11363
+ })
11364
+ ] }, entry.id)) })] })
11365
+ }),
11366
+ /* @__PURE__ */ jsx(Pagination, {
11367
+ page,
11368
+ totalPages,
11369
+ hasNextPage: page < totalPages,
11370
+ hasPrevPage: page > 1,
11371
+ onPageChange: handlePageChange
11372
+ })
11373
+ ] })
11374
+ ]
11375
+ });
11376
+ });
11377
+ function formatTimestamp(value) {
11378
+ if (!value) return "—";
11379
+ return formatDateWithSeconds(new Date(value));
11380
+ }
11381
+ function countByStatus(summary) {
11382
+ const counts = {
11383
+ pending: 0,
11384
+ in_progress: 0,
11385
+ completed: 0,
11386
+ failed: 0
11387
+ };
11388
+ for (const row of summary) if (row.status in counts) counts[row.status] += row.count;
11389
+ return counts;
11390
+ }
11391
+ //#endregion
9797
11392
  //#region app/routes/warnings.tsx
9798
11393
  var warnings_exports = /* @__PURE__ */ __exportAll({
9799
11394
  ErrorBoundary: () => ErrorBoundary,
@@ -9905,8 +11500,8 @@ function WarningTypeBadge({ type }) {
9905
11500
  //#region \0virtual:react-router/server-manifest
9906
11501
  var server_manifest_default = {
9907
11502
  "entry": {
9908
- "module": "/assets/entry.client-6KjasuHH.js",
9909
- "imports": ["/assets/jsx-runtime-DcdjQ3vN.js", "/assets/react-dom-DQHA9Ik4.js"],
11503
+ "module": "/assets/entry.client-CqyjuPDB.js",
11504
+ "imports": ["/assets/jsx-runtime-RQyiN6Nr.js", "/assets/react-dom-D_m_Zgd3.js"],
9910
11505
  "css": []
9911
11506
  },
9912
11507
  "routes": {
@@ -9923,16 +11518,16 @@ var server_manifest_default = {
9923
11518
  "hasClientMiddleware": false,
9924
11519
  "hasDefaultExport": true,
9925
11520
  "hasErrorBoundary": true,
9926
- "module": "/assets/root-Cvarn0sH.js",
11521
+ "module": "/assets/root-qxoeL6W3.js",
9927
11522
  "imports": [
9928
- "/assets/jsx-runtime-DcdjQ3vN.js",
9929
- "/assets/react-dom-DQHA9Ik4.js",
9930
- "/assets/db-link-DISwKBYZ.js",
9931
- "/assets/MenuTrigger-CkqlwCsV.js",
9932
- "/assets/createLucideIcon-oZ0wXCaF.js",
9933
- "/assets/useOpenInteractionType-BGObzouY.js"
11523
+ "/assets/jsx-runtime-RQyiN6Nr.js",
11524
+ "/assets/react-dom-D_m_Zgd3.js",
11525
+ "/assets/db-link-BajQ1v8I.js",
11526
+ "/assets/createLucideIcon-C-LI4enx.js",
11527
+ "/assets/MenuTrigger-BNvpjhsQ.js",
11528
+ "/assets/useOpenInteractionType-BQ1arb0B.js"
9934
11529
  ],
9935
- "css": ["/assets/root-D24FtLBP.css"],
11530
+ "css": ["/assets/root-B0MB8jZH.css"],
9936
11531
  "clientActionModule": void 0,
9937
11532
  "clientLoaderModule": void 0,
9938
11533
  "clientMiddlewareModule": void 0,
@@ -9951,15 +11546,15 @@ var server_manifest_default = {
9951
11546
  "hasClientMiddleware": false,
9952
11547
  "hasDefaultExport": true,
9953
11548
  "hasErrorBoundary": true,
9954
- "module": "/assets/_index-CNX0dPQi.js",
11549
+ "module": "/assets/_index-DqpFaaQw.js",
9955
11550
  "imports": [
9956
- "/assets/jsx-runtime-DcdjQ3vN.js",
9957
- "/assets/db-link-DISwKBYZ.js",
9958
- "/assets/error-card-2aexlkmv.js",
9959
- "/assets/badge-DFReduIj.js",
9960
- "/assets/button-CafwM-5D.js",
9961
- "/assets/stat-card-y2feeHt0.js",
9962
- "/assets/table-B5BEGV9S.js"
11551
+ "/assets/jsx-runtime-RQyiN6Nr.js",
11552
+ "/assets/db-link-BajQ1v8I.js",
11553
+ "/assets/stat-card-dyg1wY5p.js",
11554
+ "/assets/button-9NpSS9Ow.js",
11555
+ "/assets/badge-CMnQO7Lq.js",
11556
+ "/assets/table-Cz7ujmH_.js",
11557
+ "/assets/error-card-BH7i86fH.js"
9963
11558
  ],
9964
11559
  "css": [],
9965
11560
  "clientActionModule": void 0,
@@ -9980,24 +11575,24 @@ var server_manifest_default = {
9980
11575
  "hasClientMiddleware": false,
9981
11576
  "hasDefaultExport": true,
9982
11577
  "hasErrorBoundary": true,
9983
- "module": "/assets/jobs-BjoGjnb0.js",
11578
+ "module": "/assets/jobs-CAd_qqLH.js",
9984
11579
  "imports": [
9985
- "/assets/jsx-runtime-DcdjQ3vN.js",
9986
- "/assets/db-link-DISwKBYZ.js",
9987
- "/assets/error-card-2aexlkmv.js",
9988
- "/assets/badge-DFReduIj.js",
9989
- "/assets/button-CafwM-5D.js",
9990
- "/assets/filter-select-Dln9CAM3.js",
9991
- "/assets/pagination-5gEldBFM.js",
9992
- "/assets/table-B5BEGV9S.js",
9993
- "/assets/MenuTrigger-CkqlwCsV.js",
9994
- "/assets/useOpenInteractionType-BGObzouY.js",
9995
- "/assets/createLucideIcon-oZ0wXCaF.js",
9996
- "/assets/check-BePyOaOM.js",
9997
- "/assets/chevron-down-C8oENez_.js",
9998
- "/assets/chevron-right-B0sTJmdc.js",
9999
- "/assets/x-EA6eZ1AP.js",
10000
- "/assets/react-dom-DQHA9Ik4.js"
11580
+ "/assets/jsx-runtime-RQyiN6Nr.js",
11581
+ "/assets/db-link-BajQ1v8I.js",
11582
+ "/assets/createLucideIcon-C-LI4enx.js",
11583
+ "/assets/check-7jwc5sb1.js",
11584
+ "/assets/chevron-down-BFFjfYD4.js",
11585
+ "/assets/chevron-right-DGk5QFJF.js",
11586
+ "/assets/x-AhXI_F1j.js",
11587
+ "/assets/MenuTrigger-BNvpjhsQ.js",
11588
+ "/assets/useOpenInteractionType-BQ1arb0B.js",
11589
+ "/assets/button-9NpSS9Ow.js",
11590
+ "/assets/badge-CMnQO7Lq.js",
11591
+ "/assets/table-Cz7ujmH_.js",
11592
+ "/assets/error-card-BH7i86fH.js",
11593
+ "/assets/pagination-C-ohiBmY.js",
11594
+ "/assets/filter-select-Bn_oSiip.js",
11595
+ "/assets/react-dom-D_m_Zgd3.js"
10001
11596
  ],
10002
11597
  "css": [],
10003
11598
  "clientActionModule": void 0,
@@ -10018,14 +11613,14 @@ var server_manifest_default = {
10018
11613
  "hasClientMiddleware": false,
10019
11614
  "hasDefaultExport": true,
10020
11615
  "hasErrorBoundary": true,
10021
- "module": "/assets/queues._index-BM3eKnSr.js",
11616
+ "module": "/assets/queues._index-8YriSqbQ.js",
10022
11617
  "imports": [
10023
- "/assets/jsx-runtime-DcdjQ3vN.js",
10024
- "/assets/db-link-DISwKBYZ.js",
10025
- "/assets/badge-DFReduIj.js",
10026
- "/assets/button-CafwM-5D.js",
10027
- "/assets/filter-select-Dln9CAM3.js",
10028
- "/assets/table-B5BEGV9S.js"
11618
+ "/assets/jsx-runtime-RQyiN6Nr.js",
11619
+ "/assets/db-link-BajQ1v8I.js",
11620
+ "/assets/button-9NpSS9Ow.js",
11621
+ "/assets/badge-CMnQO7Lq.js",
11622
+ "/assets/table-Cz7ujmH_.js",
11623
+ "/assets/filter-select-Bn_oSiip.js"
10029
11624
  ],
10030
11625
  "css": [],
10031
11626
  "clientActionModule": void 0,
@@ -10046,14 +11641,14 @@ var server_manifest_default = {
10046
11641
  "hasClientMiddleware": false,
10047
11642
  "hasDefaultExport": true,
10048
11643
  "hasErrorBoundary": true,
10049
- "module": "/assets/queues.create-BVR72i85.js",
11644
+ "module": "/assets/queues.create-DsY0Sc19.js",
10050
11645
  "imports": [
10051
- "/assets/jsx-runtime-DcdjQ3vN.js",
10052
- "/assets/db-link-DISwKBYZ.js",
10053
- "/assets/error-card-2aexlkmv.js",
10054
- "/assets/button-CafwM-5D.js",
10055
- "/assets/chevron-down-C8oENez_.js",
10056
- "/assets/createLucideIcon-oZ0wXCaF.js"
11646
+ "/assets/jsx-runtime-RQyiN6Nr.js",
11647
+ "/assets/db-link-BajQ1v8I.js",
11648
+ "/assets/chevron-down-BFFjfYD4.js",
11649
+ "/assets/button-9NpSS9Ow.js",
11650
+ "/assets/error-card-BH7i86fH.js",
11651
+ "/assets/createLucideIcon-C-LI4enx.js"
10057
11652
  ],
10058
11653
  "css": [],
10059
11654
  "clientActionModule": void 0,
@@ -10074,25 +11669,25 @@ var server_manifest_default = {
10074
11669
  "hasClientMiddleware": false,
10075
11670
  "hasDefaultExport": true,
10076
11671
  "hasErrorBoundary": true,
10077
- "module": "/assets/queues._name-DSOGXBen.js",
11672
+ "module": "/assets/queues._name-Cb17IB2u.js",
10078
11673
  "imports": [
10079
- "/assets/jsx-runtime-DcdjQ3vN.js",
10080
- "/assets/db-link-DISwKBYZ.js",
10081
- "/assets/error-card-2aexlkmv.js",
10082
- "/assets/badge-DFReduIj.js",
10083
- "/assets/button-CafwM-5D.js",
10084
- "/assets/dialog-IBPuwpas.js",
10085
- "/assets/filter-select-Dln9CAM3.js",
10086
- "/assets/pagination-5gEldBFM.js",
10087
- "/assets/stat-card-y2feeHt0.js",
10088
- "/assets/table-B5BEGV9S.js",
10089
- "/assets/MenuTrigger-CkqlwCsV.js",
10090
- "/assets/createLucideIcon-oZ0wXCaF.js",
10091
- "/assets/chevron-down-C8oENez_.js",
10092
- "/assets/chevron-right-B0sTJmdc.js",
10093
- "/assets/useOpenInteractionType-BGObzouY.js",
10094
- "/assets/x-EA6eZ1AP.js",
10095
- "/assets/react-dom-DQHA9Ik4.js"
11674
+ "/assets/jsx-runtime-RQyiN6Nr.js",
11675
+ "/assets/db-link-BajQ1v8I.js",
11676
+ "/assets/createLucideIcon-C-LI4enx.js",
11677
+ "/assets/chevron-down-BFFjfYD4.js",
11678
+ "/assets/chevron-right-DGk5QFJF.js",
11679
+ "/assets/MenuTrigger-BNvpjhsQ.js",
11680
+ "/assets/dialog-D-oczDM2.js",
11681
+ "/assets/stat-card-dyg1wY5p.js",
11682
+ "/assets/button-9NpSS9Ow.js",
11683
+ "/assets/badge-CMnQO7Lq.js",
11684
+ "/assets/table-Cz7ujmH_.js",
11685
+ "/assets/error-card-BH7i86fH.js",
11686
+ "/assets/pagination-C-ohiBmY.js",
11687
+ "/assets/filter-select-Bn_oSiip.js",
11688
+ "/assets/react-dom-D_m_Zgd3.js",
11689
+ "/assets/useOpenInteractionType-BQ1arb0B.js",
11690
+ "/assets/x-AhXI_F1j.js"
10096
11691
  ],
10097
11692
  "css": [],
10098
11693
  "clientActionModule": void 0,
@@ -10113,19 +11708,19 @@ var server_manifest_default = {
10113
11708
  "hasClientMiddleware": false,
10114
11709
  "hasDefaultExport": true,
10115
11710
  "hasErrorBoundary": true,
10116
- "module": "/assets/queues._name.jobs._jobId-DKGu8NkE.js",
11711
+ "module": "/assets/queues._name.jobs._jobId-Bkv8POBj.js",
10117
11712
  "imports": [
10118
- "/assets/jsx-runtime-DcdjQ3vN.js",
10119
- "/assets/db-link-DISwKBYZ.js",
10120
- "/assets/error-card-2aexlkmv.js",
10121
- "/assets/badge-DFReduIj.js",
10122
- "/assets/button-CafwM-5D.js",
10123
- "/assets/dialog-IBPuwpas.js",
10124
- "/assets/createLucideIcon-oZ0wXCaF.js",
10125
- "/assets/check-BePyOaOM.js",
10126
- "/assets/useOpenInteractionType-BGObzouY.js",
10127
- "/assets/x-EA6eZ1AP.js",
10128
- "/assets/react-dom-DQHA9Ik4.js"
11713
+ "/assets/jsx-runtime-RQyiN6Nr.js",
11714
+ "/assets/db-link-BajQ1v8I.js",
11715
+ "/assets/createLucideIcon-C-LI4enx.js",
11716
+ "/assets/check-7jwc5sb1.js",
11717
+ "/assets/dialog-D-oczDM2.js",
11718
+ "/assets/button-9NpSS9Ow.js",
11719
+ "/assets/badge-CMnQO7Lq.js",
11720
+ "/assets/error-card-BH7i86fH.js",
11721
+ "/assets/x-AhXI_F1j.js",
11722
+ "/assets/useOpenInteractionType-BQ1arb0B.js",
11723
+ "/assets/react-dom-D_m_Zgd3.js"
10129
11724
  ],
10130
11725
  "css": [],
10131
11726
  "clientActionModule": void 0,
@@ -10146,15 +11741,15 @@ var server_manifest_default = {
10146
11741
  "hasClientMiddleware": false,
10147
11742
  "hasDefaultExport": true,
10148
11743
  "hasErrorBoundary": true,
10149
- "module": "/assets/schedules-CE-hWC9e.js",
11744
+ "module": "/assets/schedules-iYfIJxOD.js",
10150
11745
  "imports": [
10151
- "/assets/jsx-runtime-DcdjQ3vN.js",
10152
- "/assets/db-link-DISwKBYZ.js",
10153
- "/assets/error-card-2aexlkmv.js",
10154
- "/assets/badge-DFReduIj.js",
10155
- "/assets/button-CafwM-5D.js",
10156
- "/assets/pagination-5gEldBFM.js",
10157
- "/assets/table-B5BEGV9S.js"
11746
+ "/assets/jsx-runtime-RQyiN6Nr.js",
11747
+ "/assets/db-link-BajQ1v8I.js",
11748
+ "/assets/button-9NpSS9Ow.js",
11749
+ "/assets/badge-CMnQO7Lq.js",
11750
+ "/assets/table-Cz7ujmH_.js",
11751
+ "/assets/error-card-BH7i86fH.js",
11752
+ "/assets/pagination-C-ohiBmY.js"
10158
11753
  ],
10159
11754
  "css": [],
10160
11755
  "clientActionModule": void 0,
@@ -10175,17 +11770,17 @@ var server_manifest_default = {
10175
11770
  "hasClientMiddleware": false,
10176
11771
  "hasDefaultExport": true,
10177
11772
  "hasErrorBoundary": true,
10178
- "module": "/assets/schedules._name._key-DT8Kg4jU.js",
11773
+ "module": "/assets/schedules._name._key-CJVu73XY.js",
10179
11774
  "imports": [
10180
- "/assets/jsx-runtime-DcdjQ3vN.js",
10181
- "/assets/db-link-DISwKBYZ.js",
10182
- "/assets/error-card-2aexlkmv.js",
10183
- "/assets/button-CafwM-5D.js",
10184
- "/assets/dialog-IBPuwpas.js",
10185
- "/assets/useOpenInteractionType-BGObzouY.js",
10186
- "/assets/x-EA6eZ1AP.js",
10187
- "/assets/react-dom-DQHA9Ik4.js",
10188
- "/assets/createLucideIcon-oZ0wXCaF.js"
11775
+ "/assets/jsx-runtime-RQyiN6Nr.js",
11776
+ "/assets/db-link-BajQ1v8I.js",
11777
+ "/assets/dialog-D-oczDM2.js",
11778
+ "/assets/button-9NpSS9Ow.js",
11779
+ "/assets/error-card-BH7i86fH.js",
11780
+ "/assets/x-AhXI_F1j.js",
11781
+ "/assets/useOpenInteractionType-BQ1arb0B.js",
11782
+ "/assets/createLucideIcon-C-LI4enx.js",
11783
+ "/assets/react-dom-D_m_Zgd3.js"
10189
11784
  ],
10190
11785
  "css": [],
10191
11786
  "clientActionModule": void 0,
@@ -10206,12 +11801,12 @@ var server_manifest_default = {
10206
11801
  "hasClientMiddleware": false,
10207
11802
  "hasDefaultExport": true,
10208
11803
  "hasErrorBoundary": true,
10209
- "module": "/assets/schedules.new-bKRusXXO.js",
11804
+ "module": "/assets/schedules.new-Cq0Mxa7G.js",
10210
11805
  "imports": [
10211
- "/assets/jsx-runtime-DcdjQ3vN.js",
10212
- "/assets/db-link-DISwKBYZ.js",
10213
- "/assets/error-card-2aexlkmv.js",
10214
- "/assets/button-CafwM-5D.js"
11806
+ "/assets/jsx-runtime-RQyiN6Nr.js",
11807
+ "/assets/db-link-BajQ1v8I.js",
11808
+ "/assets/button-9NpSS9Ow.js",
11809
+ "/assets/error-card-BH7i86fH.js"
10215
11810
  ],
10216
11811
  "css": [],
10217
11812
  "clientActionModule": void 0,
@@ -10232,12 +11827,43 @@ var server_manifest_default = {
10232
11827
  "hasClientMiddleware": false,
10233
11828
  "hasDefaultExport": true,
10234
11829
  "hasErrorBoundary": true,
10235
- "module": "/assets/send-BaWw8x7J.js",
11830
+ "module": "/assets/send-8X9ZisG-.js",
11831
+ "imports": [
11832
+ "/assets/jsx-runtime-RQyiN6Nr.js",
11833
+ "/assets/db-link-BajQ1v8I.js",
11834
+ "/assets/button-9NpSS9Ow.js",
11835
+ "/assets/error-card-BH7i86fH.js"
11836
+ ],
11837
+ "css": [],
11838
+ "clientActionModule": void 0,
11839
+ "clientLoaderModule": void 0,
11840
+ "clientMiddlewareModule": void 0,
11841
+ "hydrateFallbackModule": void 0
11842
+ },
11843
+ "routes/migrations": {
11844
+ "id": "routes/migrations",
11845
+ "parentId": "root",
11846
+ "path": "migrations",
11847
+ "index": void 0,
11848
+ "caseSensitive": void 0,
11849
+ "hasAction": false,
11850
+ "hasLoader": true,
11851
+ "hasClientAction": false,
11852
+ "hasClientLoader": false,
11853
+ "hasClientMiddleware": false,
11854
+ "hasDefaultExport": true,
11855
+ "hasErrorBoundary": true,
11856
+ "module": "/assets/migrations-D5l0n4Jn.js",
10236
11857
  "imports": [
10237
- "/assets/jsx-runtime-DcdjQ3vN.js",
10238
- "/assets/db-link-DISwKBYZ.js",
10239
- "/assets/error-card-2aexlkmv.js",
10240
- "/assets/button-CafwM-5D.js"
11858
+ "/assets/jsx-runtime-RQyiN6Nr.js",
11859
+ "/assets/db-link-BajQ1v8I.js",
11860
+ "/assets/stat-card-dyg1wY5p.js",
11861
+ "/assets/button-9NpSS9Ow.js",
11862
+ "/assets/badge-CMnQO7Lq.js",
11863
+ "/assets/table-Cz7ujmH_.js",
11864
+ "/assets/error-card-BH7i86fH.js",
11865
+ "/assets/pagination-C-ohiBmY.js",
11866
+ "/assets/filter-select-Bn_oSiip.js"
10241
11867
  ],
10242
11868
  "css": [],
10243
11869
  "clientActionModule": void 0,
@@ -10258,16 +11884,16 @@ var server_manifest_default = {
10258
11884
  "hasClientMiddleware": false,
10259
11885
  "hasDefaultExport": true,
10260
11886
  "hasErrorBoundary": true,
10261
- "module": "/assets/warnings-lzi34PdC.js",
11887
+ "module": "/assets/warnings-C1R_RzIe.js",
10262
11888
  "imports": [
10263
- "/assets/jsx-runtime-DcdjQ3vN.js",
10264
- "/assets/db-link-DISwKBYZ.js",
10265
- "/assets/error-card-2aexlkmv.js",
10266
- "/assets/badge-DFReduIj.js",
10267
- "/assets/button-CafwM-5D.js",
10268
- "/assets/filter-select-Dln9CAM3.js",
10269
- "/assets/pagination-5gEldBFM.js",
10270
- "/assets/table-B5BEGV9S.js"
11889
+ "/assets/jsx-runtime-RQyiN6Nr.js",
11890
+ "/assets/db-link-BajQ1v8I.js",
11891
+ "/assets/button-9NpSS9Ow.js",
11892
+ "/assets/badge-CMnQO7Lq.js",
11893
+ "/assets/table-Cz7ujmH_.js",
11894
+ "/assets/error-card-BH7i86fH.js",
11895
+ "/assets/pagination-C-ohiBmY.js",
11896
+ "/assets/filter-select-Bn_oSiip.js"
10271
11897
  ],
10272
11898
  "css": [],
10273
11899
  "clientActionModule": void 0,
@@ -10276,23 +11902,15 @@ var server_manifest_default = {
10276
11902
  "hydrateFallbackModule": void 0
10277
11903
  }
10278
11904
  },
10279
- "url": "/assets/manifest-e6a94de0.js",
10280
- "version": "e6a94de0",
11905
+ "url": "/assets/manifest-27e8e133.js",
11906
+ "version": "27e8e133",
10281
11907
  "sri": void 0
10282
11908
  };
10283
11909
  //#endregion
10284
11910
  //#region \0virtual:react-router/server-build
10285
11911
  var assetsBuildDirectory = "build/client";
10286
11912
  var basename = "/";
10287
- var future = {
10288
- "unstable_optimizeDeps": false,
10289
- "v8_passThroughRequests": true,
10290
- "v8_trailingSlashAwareDataRequests": true,
10291
- "unstable_previewServerPrerendering": false,
10292
- "v8_middleware": true,
10293
- "v8_splitRouteModules": true,
10294
- "v8_viteEnvironmentApi": true
10295
- };
11913
+ var future = { "unstable_optimizeDeps": false };
10296
11914
  var ssr = true;
10297
11915
  var isSpaMode = false;
10298
11916
  var prerender = [];
@@ -10391,6 +12009,14 @@ var routes = {
10391
12009
  caseSensitive: void 0,
10392
12010
  module: send_exports
10393
12011
  },
12012
+ "routes/migrations": {
12013
+ id: "routes/migrations",
12014
+ parentId: "root",
12015
+ path: "migrations",
12016
+ index: void 0,
12017
+ caseSensitive: void 0,
12018
+ module: migrations_exports
12019
+ },
10394
12020
  "routes/warnings": {
10395
12021
  id: "routes/warnings",
10396
12022
  parentId: "root",