@pg-boss/dashboard 1.2.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +1 -1
  2. package/build/client/assets/MenuTrigger-BhalG0aG.js +1 -0
  3. package/build/client/assets/_index-D1-nZ7Th.js +1 -0
  4. package/build/client/assets/{badge-DFReduIj.js → badge-DCQvSdiR.js} +1 -1
  5. package/build/client/assets/button-BxLcuaPM.js +1 -0
  6. package/build/client/assets/check-Ch42cXMT.js +1 -0
  7. package/build/client/assets/{chevron-down-C8oENez_.js → chevron-down-Byq-CYG9.js} +1 -1
  8. package/build/client/assets/chevron-right-CKAGD7DJ.js +1 -0
  9. package/build/client/assets/createLucideIcon-DVP_i62f.js +1 -0
  10. package/build/client/assets/db-link-BWWnHM0k.js +1 -0
  11. package/build/client/assets/dialog-Bik519zD.js +1 -0
  12. package/build/client/assets/entry.client-DL_oPh96.js +9 -0
  13. package/build/client/assets/{error-card-2aexlkmv.js → error-card-B0ANyjh3.js} +1 -1
  14. package/build/client/assets/{filter-select-Dln9CAM3.js → filter-select--qLjbs9m.js} +1 -1
  15. package/build/client/assets/jobs-D0a6Lwq0.js +1 -0
  16. package/build/client/assets/jsx-runtime-BgbGXvsu.js +16 -0
  17. package/build/client/assets/manifest-ef81a0f9.js +1 -0
  18. package/build/client/assets/{pagination-5gEldBFM.js → pagination-Bzx8wbXG.js} +1 -1
  19. package/build/client/assets/queues._index-D8903DTa.js +1 -0
  20. package/build/client/assets/queues._name-BVt_4pav.js +1 -0
  21. package/build/client/assets/queues._name.jobs._jobId-BkG9y75k.js +1 -0
  22. package/build/client/assets/queues.create-CMqQVLup.js +1 -0
  23. package/build/client/assets/react-dom-QnGHOQwT.js +1 -0
  24. package/build/client/assets/root-C0MdPLOa.css +2 -0
  25. package/build/client/assets/root-Df70GAY3.js +40 -0
  26. package/build/client/assets/{schedules-CE-hWC9e.js → schedules-DPXQoaEE.js} +1 -1
  27. package/build/client/assets/schedules._name._key-B_luxy1w.js +1 -0
  28. package/build/client/assets/schedules.new-BQV7GWzs.js +1 -0
  29. package/build/client/assets/send-DJBsfnx_.js +1 -0
  30. package/build/client/assets/{stat-card-y2feeHt0.js → stat-card-DLtQnscf.js} +1 -1
  31. package/build/client/assets/{table-B5BEGV9S.js → table-DqqzSNik.js} +1 -1
  32. package/build/client/assets/useOpenInteractionType-D3JsvupP.js +1 -0
  33. package/build/client/assets/{warnings-lzi34PdC.js → warnings-CHKaRfIW.js} +1 -1
  34. package/build/client/assets/x-BPKZwOn9.js +1 -0
  35. package/build/server/index.js +1206 -231
  36. package/package.json +13 -13
  37. package/build/client/assets/MenuTrigger-CkqlwCsV.js +0 -1
  38. package/build/client/assets/_index-CNX0dPQi.js +0 -1
  39. package/build/client/assets/button-CafwM-5D.js +0 -1
  40. package/build/client/assets/check-BePyOaOM.js +0 -1
  41. package/build/client/assets/chevron-right-B0sTJmdc.js +0 -1
  42. package/build/client/assets/createLucideIcon-oZ0wXCaF.js +0 -1
  43. package/build/client/assets/db-link-DISwKBYZ.js +0 -1
  44. package/build/client/assets/dialog-IBPuwpas.js +0 -1
  45. package/build/client/assets/entry.client-6KjasuHH.js +0 -9
  46. package/build/client/assets/jobs-BjoGjnb0.js +0 -1
  47. package/build/client/assets/jsx-runtime-DcdjQ3vN.js +0 -26
  48. package/build/client/assets/manifest-e6a94de0.js +0 -1
  49. package/build/client/assets/queues._index-BM3eKnSr.js +0 -1
  50. package/build/client/assets/queues._name-DSOGXBen.js +0 -1
  51. package/build/client/assets/queues._name.jobs._jobId-DKGu8NkE.js +0 -1
  52. package/build/client/assets/queues.create-BVR72i85.js +0 -1
  53. package/build/client/assets/react-dom-DQHA9Ik4.js +0 -1
  54. package/build/client/assets/root-Cvarn0sH.js +0 -40
  55. package/build/client/assets/root-D24FtLBP.css +0 -2
  56. package/build/client/assets/schedules._name._key-DT8Kg4jU.js +0 -1
  57. package/build/client/assets/schedules.new-bKRusXXO.js +0 -1
  58. package/build/client/assets/send-BaWw8x7J.js +0 -1
  59. package/build/client/assets/useOpenInteractionType-BGObzouY.js +0 -1
  60. package/build/client/assets/x-EA6eZ1AP.js +0 -1
@@ -1434,6 +1434,7 @@ if (typeof process !== "undefined") {
1434
1434
  }
1435
1435
  //#endregion
1436
1436
  //#region ../../src/plans.ts
1437
+ var PG_ERROR = { divisionByZero: "22012" };
1437
1438
  var DEFAULT_SCHEMA = "pgboss";
1438
1439
  var MIGRATE_RACE_MESSAGE = "division by zero";
1439
1440
  var CREATE_RACE_MESSAGE = "already exists";
@@ -1469,6 +1470,10 @@ var QUEUE_DEFAULTS = {
1469
1470
  };
1470
1471
  var COMMON_JOB_TABLE = "job_common";
1471
1472
  function create(schema, version, options) {
1473
+ const noPartitioning = options?.noTablePartitioning ?? false;
1474
+ const noDeferrable = options?.noDeferrableConstraints ?? false;
1475
+ const noLocks = options?.noAdvisoryLocks ?? false;
1476
+ const noCovering = options?.noCoveringIndexes ?? false;
1472
1477
  return locked(schema, [
1473
1478
  options?.createSchema ? createSchema(schema) : "",
1474
1479
  createEnumJobState(schema),
@@ -1477,18 +1482,20 @@ function create(schema, version, options) {
1477
1482
  createTableSchedule(schema),
1478
1483
  createTableSubscription(schema),
1479
1484
  createTableBam(schema),
1480
- jobTableFormatFunction(schema),
1481
- jobTableRunFunction(schema),
1482
- jobTableRunAsyncFunction(schema),
1483
- createTableJob(schema),
1485
+ noPartitioning ? "" : jobTableFormatFunction(schema),
1486
+ noPartitioning ? "" : jobTableRunFunction(schema),
1487
+ noPartitioning ? "" : jobTableRunAsyncFunction(schema),
1488
+ createTableJob(schema, noPartitioning),
1484
1489
  createPrimaryKeyJob(schema),
1485
- createTableJobCommon(schema),
1490
+ noPartitioning ? createTableJobIndexes(schema, noDeferrable, noCovering) : createTableJobCommon(schema),
1486
1491
  createTableWarning(schema),
1487
1492
  createIndexWarning(schema),
1488
- createQueueFunction(schema),
1489
- deleteQueueFunction(schema),
1493
+ createTableJobDependency(schema),
1494
+ createIndexJobDependencyParent(schema),
1495
+ createQueueFunction(schema, noPartitioning),
1496
+ deleteQueueFunction(schema, noPartitioning),
1490
1497
  insertVersion(schema, version)
1491
- ]);
1498
+ ], void 0, noLocks);
1492
1499
  }
1493
1500
  function createSchema(schema) {
1494
1501
  return `CREATE SCHEMA IF NOT EXISTS ${schema}`;
@@ -1601,6 +1608,20 @@ function createTableWarning(schema) {
1601
1608
  function createIndexWarning(schema) {
1602
1609
  return `CREATE INDEX warning_i1 ON ${schema}.warning (created_on DESC)`;
1603
1610
  }
1611
+ function createTableJobDependency(schema) {
1612
+ return `
1613
+ CREATE TABLE ${schema}.job_dependency (
1614
+ child_name text NOT NULL,
1615
+ child_id uuid NOT NULL,
1616
+ parent_name text NOT NULL,
1617
+ parent_id uuid NOT NULL,
1618
+ PRIMARY KEY (child_name, child_id, parent_name, parent_id)
1619
+ )
1620
+ `;
1621
+ }
1622
+ function createIndexJobDependencyParent(schema) {
1623
+ return `CREATE INDEX IF NOT EXISTS job_dep_parent_idx ON ${schema}.job_dependency (parent_name, parent_id)`;
1624
+ }
1604
1625
  function jobTableFormatFunction(schema) {
1605
1626
  return `
1606
1627
  CREATE FUNCTION ${schema}.job_table_format(command text, table_name text)
@@ -1691,7 +1712,8 @@ function jobTableRunAsyncFunction(schema) {
1691
1712
  LANGUAGE plpgsql;
1692
1713
  `;
1693
1714
  }
1694
- function createTableJob(schema) {
1715
+ function createTableJob(schema, noPartitioning = false) {
1716
+ const partitionClause = noPartitioning ? "" : "PARTITION BY LIST (name)";
1695
1717
  return `
1696
1718
  CREATE TABLE ${schema}.job (
1697
1719
  id uuid not null default gen_random_uuid(),
@@ -1719,8 +1741,11 @@ function createTableJob(schema) {
1719
1741
  dead_letter text,
1720
1742
  policy text,
1721
1743
  heartbeat_on timestamp with time zone,
1722
- heartbeat_seconds int
1723
- ) PARTITION BY LIST (name)
1744
+ heartbeat_seconds int,
1745
+ blocked boolean not null default false,
1746
+ blocking boolean not null default false,
1747
+ pending_dependencies int not null default 0
1748
+ ) ${partitionClause}
1724
1749
  `;
1725
1750
  }
1726
1751
  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 +1768,9 @@ var JOB_COLUMNS_ALL = `${JOB_COLUMNS_MIN},
1743
1768
  completed_on as "completedOn",
1744
1769
  keep_until as "keepUntil",
1745
1770
  dead_letter as "deadLetter",
1771
+ blocked,
1772
+ blocking,
1773
+ pending_dependencies as "pendingDependencies",
1746
1774
  output
1747
1775
  `;
1748
1776
  function createTableJobCommon(schema) {
@@ -1765,7 +1793,64 @@ function createTableJobCommon(schema) {
1765
1793
  ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.${COMMON_JOB_TABLE} DEFAULT;
1766
1794
  `;
1767
1795
  }
1768
- function createQueueFunction(schema) {
1796
+ function createTableJobIndexes(schema, noDeferrableConstraints = false, noCoveringIndex = false) {
1797
+ return `
1798
+ ${createQueueForeignKeyJob(schema, noDeferrableConstraints)};
1799
+ ${createQueueForeignKeyJobDeadLetter(schema, noDeferrableConstraints)};
1800
+ ${createIndexJobPolicyShort(schema)};
1801
+ ${createIndexJobPolicySingleton(schema)};
1802
+ ${createIndexJobPolicyStately(schema)};
1803
+ ${createIndexJobPolicyExclusive(schema)};
1804
+ ${createIndexJobPolicyKeyStrictFifo(schema)};
1805
+ ${createCheckConstraintKeyStrictFifo(schema)};
1806
+ ${createIndexJobThrottle(schema)};
1807
+ ${createIndexJobFetch(schema, noCoveringIndex)};
1808
+ ${createIndexJobGroupConcurrency(schema)};
1809
+ `;
1810
+ }
1811
+ function createQueueFunction(schema, noPartitioning = false) {
1812
+ if (noPartitioning) return `
1813
+ CREATE FUNCTION ${schema}.create_queue(queue_name text, options jsonb)
1814
+ RETURNS VOID AS
1815
+ $$
1816
+ BEGIN
1817
+ INSERT INTO ${schema}.queue (
1818
+ name,
1819
+ policy,
1820
+ retry_limit,
1821
+ retry_delay,
1822
+ retry_backoff,
1823
+ retry_delay_max,
1824
+ expire_seconds,
1825
+ retention_seconds,
1826
+ deletion_seconds,
1827
+ warning_queued,
1828
+ dead_letter,
1829
+ partition,
1830
+ table_name,
1831
+ heartbeat_seconds
1832
+ )
1833
+ VALUES (
1834
+ queue_name,
1835
+ options->>'policy',
1836
+ COALESCE((options->>'retryLimit')::int, ${QUEUE_DEFAULTS.retry_limit}),
1837
+ COALESCE((options->>'retryDelay')::int, ${QUEUE_DEFAULTS.retry_delay}),
1838
+ COALESCE((options->>'retryBackoff')::bool, ${QUEUE_DEFAULTS.retry_backoff}),
1839
+ (options->>'retryDelayMax')::int,
1840
+ COALESCE((options->>'expireInSeconds')::int, ${QUEUE_DEFAULTS.expire_seconds}),
1841
+ COALESCE((options->>'retentionSeconds')::int, ${QUEUE_DEFAULTS.retention_seconds}),
1842
+ COALESCE((options->>'deleteAfterSeconds')::int, ${QUEUE_DEFAULTS.deletion_seconds}),
1843
+ COALESCE((options->>'warningQueueSize')::int, ${QUEUE_DEFAULTS.warning_queued}),
1844
+ options->>'deadLetter',
1845
+ false,
1846
+ 'job',
1847
+ (options->>'heartbeatSeconds')::int
1848
+ )
1849
+ ON CONFLICT DO NOTHING;
1850
+ END;
1851
+ $$
1852
+ LANGUAGE plpgsql;
1853
+ `;
1769
1854
  return `
1770
1855
  CREATE FUNCTION ${schema}.create_queue(queue_name text, options jsonb)
1771
1856
  RETURNS VOID AS
@@ -1850,15 +1935,8 @@ function createQueueFunction(schema) {
1850
1935
  LANGUAGE plpgsql;
1851
1936
  `;
1852
1937
  }
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
1938
+ function deleteQueueFunction(schema, noPartitioning = false) {
1939
+ const deleteJobsSql = noPartitioning ? `DELETE FROM ${schema}.job WHERE name = queue_name;` : `
1862
1940
  SELECT table_name, partition
1863
1941
  FROM ${schema}.queue
1864
1942
  WHERE name = queue_name
@@ -1869,27 +1947,36 @@ function deleteQueueFunction(schema) {
1869
1947
  ELSE
1870
1948
  EXECUTE format('DELETE FROM ${schema}.%I WHERE name = %L', v_table, queue_name);
1871
1949
  END IF;
1872
-
1950
+ `;
1951
+ return `
1952
+ CREATE FUNCTION ${schema}.delete_queue(queue_name text)
1953
+ RETURNS VOID AS
1954
+ $$${noPartitioning ? "" : `
1955
+ DECLARE
1956
+ v_table varchar;
1957
+ v_partition bool;`}
1958
+ BEGIN
1959
+ ${deleteJobsSql}
1873
1960
  DELETE FROM ${schema}.queue WHERE name = queue_name;
1874
1961
  END;
1875
1962
  $$
1876
1963
  LANGUAGE plpgsql;
1877
1964
  `;
1878
1965
  }
1879
- function createQueue$1(schema, name, options) {
1880
- return locked(schema, `SELECT ${schema}.create_queue('${name}', '${JSON.stringify(options)}'::jsonb)`, "create-queue");
1966
+ function createQueue$1(schema, name, options, noAdvisoryLocks) {
1967
+ return locked(schema, `SELECT ${schema}.create_queue('${name}', '${JSON.stringify(options)}'::jsonb)`, "create-queue", noAdvisoryLocks);
1881
1968
  }
1882
- function deleteQueue(schema, name) {
1883
- return locked(schema, `SELECT ${schema}.delete_queue('${name}')`, "delete-queue");
1969
+ function deleteQueue(schema, name, noAdvisoryLocks) {
1970
+ return locked(schema, `SELECT ${schema}.delete_queue('${name}')`, "delete-queue", noAdvisoryLocks);
1884
1971
  }
1885
1972
  function createPrimaryKeyJob(schema) {
1886
1973
  return `ALTER TABLE ${schema}.job ADD PRIMARY KEY (name, id)`;
1887
1974
  }
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`;
1975
+ function createQueueForeignKeyJob(schema, noPartitioning = false) {
1976
+ return `ALTER TABLE ${schema}.job ADD CONSTRAINT q_fkey FOREIGN KEY (name) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT${noPartitioning ? "" : " DEFERRABLE INITIALLY DEFERRED"}`;
1890
1977
  }
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`;
1978
+ function createQueueForeignKeyJobDeadLetter(schema, noPartitioning = false) {
1979
+ 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
1980
  }
1894
1981
  function createIndexJobPolicyShort(schema) {
1895
1982
  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 +1990,8 @@ function createIndexJobPolicyStately(schema) {
1903
1990
  function createIndexJobThrottle(schema) {
1904
1991
  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
1992
  }
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}'`;
1993
+ function createIndexJobFetch(schema, noCoveringIndex = false) {
1994
+ return `CREATE INDEX job_i5 ON ${schema}.job (name, start_after) ${noCoveringIndex ? "" : "INCLUDE (priority, created_on, id) "}WHERE state < '${JOB_STATES.active}' AND NOT blocked`;
1908
1995
  }
1909
1996
  function createIndexJobPolicyExclusive(schema) {
1910
1997
  return `CREATE UNIQUE INDEX job_i6 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state <= '${JOB_STATES.active}' AND policy = '${QUEUE_POLICIES.exclusive}'`;
@@ -2159,7 +2246,23 @@ function buildFetchParams(options) {
2159
2246
  maxPriorityParam
2160
2247
  };
2161
2248
  }
2162
- function fetchNextJob(options) {
2249
+ /**
2250
+ * Builds the fetch query for claiming jobs from the queue.
2251
+ *
2252
+ * With SKIP LOCKED (noSkipLocked=false, the default), uses SELECT FOR UPDATE SKIP
2253
+ * LOCKED, which lets multiple workers efficiently fetch different jobs simultaneously.
2254
+ *
2255
+ * With noSkipLocked=true, omits FOR UPDATE SKIP LOCKED and adds an additional state
2256
+ * check in the WHERE clause. This pattern works better with distributed databases like
2257
+ * CockroachDB where SKIP LOCKED has performance issues and can unexpectedly skip
2258
+ * unlocked rows.
2259
+ *
2260
+ * Trade-off when noSkipLocked is set: under high contention, workers may receive fewer
2261
+ * jobs per fetch as concurrent updates to the same rows will result in some workers
2262
+ * getting empty results. This is acceptable for job queues where processing time
2263
+ * exceeds fetch time.
2264
+ */
2265
+ function fetchNextJob(options, noSkipLocked = false) {
2163
2266
  const { schema, table, name, policy, limit, includeMetadata, priority = true, orderByCreatedOn = true, ignoreStartAfter = false, groupConcurrency, minPriority, maxPriority } = options;
2164
2267
  const singletonFetch = limit > 1 && (policy === QUEUE_POLICIES.singleton || policy === QUEUE_POLICIES.stately);
2165
2268
  const hasIgnoreSingletons = options.ignoreSingletons != null && options.ignoreSingletons.length > 0;
@@ -2180,9 +2283,11 @@ function fetchNextJob(options) {
2180
2283
  WHERE name = '${name}' AND state = '${JOB_STATES.active}' AND group_id IS NOT NULL
2181
2284
  GROUP BY group_id
2182
2285
  ), ` : "";
2286
+ const lockClause = noSkipLocked ? "" : "FOR UPDATE OF j SKIP LOCKED";
2183
2287
  const whereConditions = [
2184
2288
  `j.name = '${name}'`,
2185
2289
  `j.state < '${JOB_STATES.active}'`,
2290
+ "NOT j.blocked",
2186
2291
  !ignoreStartAfter ? "j.start_after < now()" : "",
2187
2292
  hasIgnoreSingletons ? `j.singleton_key <> ALL(${params.ignoreSingletonsParam})` : "",
2188
2293
  hasIgnoreGroups ? `(j.group_id IS NULL OR j.group_id <> ALL(${params.ignoreGroupsParam}))` : "",
@@ -2200,7 +2305,7 @@ function fetchNextJob(options) {
2200
2305
  WHERE ${whereConditions}
2201
2306
  ORDER BY ${priority ? "j.priority desc, " : ""}${orderByCreatedOn ? "j.created_on, " : ""}j.id
2202
2307
  LIMIT ${limit}
2203
- FOR UPDATE OF j SKIP LOCKED
2308
+ ${lockClause}
2204
2309
  )`;
2205
2310
  const singletonCte = singletonFetch ? `, singleton_ranking AS (
2206
2311
  SELECT id, ${hasGroupConcurrency ? "group_id, group_tier, " : ""}
@@ -2225,6 +2330,7 @@ function fetchNextJob(options) {
2225
2330
  OR (active_cnt + group_rn) <= ${hasTiers ? `COALESCE((${params.tiersParam} ->> group_tier)::int, ${params.defaultGroupLimitParam})` : params.defaultGroupLimitParam}
2226
2331
  )` : "";
2227
2332
  const finalCte = hasGroupConcurrency ? "group_filtered" : singletonFetch ? "singleton_ranking" : "next";
2333
+ const distributedStateCheck = noSkipLocked ? `AND j.state < '${JOB_STATES.active}'` : "";
2228
2334
  return {
2229
2335
  text: `
2230
2336
  WITH
@@ -2240,25 +2346,62 @@ function fetchNextJob(options) {
2240
2346
  FROM ${finalCte}
2241
2347
  WHERE name = '${name}' AND j.id = ${finalCte}.id
2242
2348
  ${singletonFetch && !hasGroupConcurrency ? "AND singleton_rn = 1" : ""}
2349
+ ${distributedStateCheck}
2243
2350
  RETURNING j.${includeMetadata ? JOB_COLUMNS_ALL : JOB_COLUMNS_MIN}
2244
2351
  `,
2245
2352
  values: params.values
2246
2353
  };
2247
2354
  }
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}
2355
+ function completeJobsUpdate(schema, table, includeQueued) {
2356
+ return `UPDATE ${schema}.${table}
2253
2357
  SET completed_on = now(),
2254
2358
  state = '${JOB_STATES.completed}',
2255
- output = $3::jsonb
2359
+ output = $3::jsonb,
2360
+ blocked = ${includeQueued ? "false" : "blocked"},
2361
+ pending_dependencies = ${includeQueued ? "0" : "pending_dependencies"}
2256
2362
  WHERE name = $1
2257
2363
  AND id IN (SELECT UNNEST($2::uuid[]))
2258
- AND ${stateFilter}
2259
- RETURNING *
2364
+ AND ${includeQueued ? `state < '${JOB_STATES.completed}'` : `state = '${JOB_STATES.active}'`}`;
2365
+ }
2366
+ function lockedChildrenCte(schema) {
2367
+ return `locked_children AS (
2368
+ SELECT j.name, j.id, d.n
2369
+ FROM ${schema}.job j
2370
+ JOIN decremented d ON d.child_name = j.name
2371
+ AND d.child_id = j.id
2372
+ WHERE j.blocked
2373
+ ORDER BY j.name, j.id
2374
+ FOR UPDATE OF j
2375
+ )`;
2376
+ }
2377
+ function unblockChildrenUpdate(schema) {
2378
+ return `UPDATE ${schema}.job j
2379
+ SET pending_dependencies = GREATEST(j.pending_dependencies - lc.n, 0),
2380
+ blocked = GREATEST(j.pending_dependencies - lc.n, 0) > 0
2381
+ FROM locked_children lc
2382
+ WHERE j.name = lc.name
2383
+ AND j.id = lc.id`;
2384
+ }
2385
+ function completeJobs(schema, table, includeQueued) {
2386
+ return `
2387
+ WITH completed AS (
2388
+ ${completeJobsUpdate(schema, table, includeQueued)}
2389
+ RETURNING name, id, blocking
2390
+ ),
2391
+ decremented AS (
2392
+ SELECT d.child_name, d.child_id, COUNT(*)::int AS n
2393
+ FROM ${schema}.job_dependency d
2394
+ JOIN completed c ON c.blocking
2395
+ AND d.parent_name = c.name
2396
+ AND d.parent_id = c.id
2397
+ GROUP BY d.child_name, d.child_id
2398
+ ),
2399
+ ${lockedChildrenCte(schema)},
2400
+ unblocked AS (
2401
+ ${unblockChildrenUpdate(schema)}
2402
+ RETURNING 1
2260
2403
  )
2261
- SELECT COUNT(*) FROM results
2404
+ SELECT COUNT(*) FROM completed
2262
2405
  `;
2263
2406
  }
2264
2407
  function cancelJobs(schema, table) {
@@ -2320,7 +2463,10 @@ function insertJobs(schema, { table, name, returnId = true }) {
2320
2463
  retry_delay_max,
2321
2464
  policy,
2322
2465
  dead_letter,
2323
- heartbeat_seconds
2466
+ heartbeat_seconds,
2467
+ blocked,
2468
+ blocking,
2469
+ pending_dependencies
2324
2470
  )
2325
2471
  SELECT
2326
2472
  COALESCE(id, gen_random_uuid()) as id,
@@ -2330,7 +2476,7 @@ function insertJobs(schema, { table, name, returnId = true }) {
2330
2476
  j.start_after,
2331
2477
  "singletonKey",
2332
2478
  CASE
2333
- WHEN "singletonSeconds" IS NOT NULL THEN 'epoch'::timestamp + '1s'::interval * ("singletonSeconds" * floor(( date_part('epoch', now()) + COALESCE("singletonOffset",0)) / "singletonSeconds" ))
2479
+ WHEN "singletonSeconds" IS NOT NULL THEN 'epoch'::timestamp + '1s'::interval * ("singletonSeconds"::float8 * floor(( date_part('epoch', now()) + COALESCE("singletonOffset",0)::float8) / "singletonSeconds"::float8 ))
2334
2480
  ELSE NULL
2335
2481
  END as singleton_on,
2336
2482
  "groupId" as group_id,
@@ -2344,7 +2490,10 @@ function insertJobs(schema, { table, name, returnId = true }) {
2344
2490
  COALESCE("retryDelayMax", q.retry_delay_max) as retry_delay_max,
2345
2491
  q.policy,
2346
2492
  COALESCE("deadLetter", q.dead_letter) as dead_letter,
2347
- COALESCE("heartbeatSeconds", q.heartbeat_seconds) as heartbeat_seconds
2493
+ COALESCE("heartbeatSeconds", q.heartbeat_seconds) as heartbeat_seconds,
2494
+ COALESCE(blocked, false) as blocked,
2495
+ COALESCE(blocking, false) as blocking,
2496
+ COALESCE("pendingDependencies", 0) as pending_dependencies
2348
2497
  FROM (
2349
2498
  SELECT *,
2350
2499
  CASE
@@ -2369,7 +2518,10 @@ function insertJobs(schema, { table, name, returnId = true }) {
2369
2518
  "deleteAfterSeconds" integer,
2370
2519
  "retentionSeconds" integer,
2371
2520
  "deadLetter" text,
2372
- "heartbeatSeconds" integer
2521
+ "heartbeatSeconds" integer,
2522
+ blocked boolean,
2523
+ blocking boolean,
2524
+ "pendingDependencies" integer
2373
2525
  )
2374
2526
  ) j
2375
2527
  JOIN ${schema}.queue q ON q.name = '${name}'
@@ -2380,16 +2532,16 @@ function insertJobs(schema, { table, name, returnId = true }) {
2380
2532
  function failJobsById(schema, table) {
2381
2533
  return failJobs(schema, table, `name = $1 AND id IN (SELECT UNNEST($2::uuid[])) AND state < '${JOB_STATES.completed}'`, "$3::jsonb");
2382
2534
  }
2383
- function failJobsByTimeout(schema, table, queues) {
2535
+ function failJobsByTimeout(schema, table, queues, noAdvisoryLocks) {
2384
2536
  return locked(schema, failJobs(schema, table, `state = '${JOB_STATES.active}'
2385
2537
  AND (started_on + expire_seconds * interval '1s') < now()
2386
- AND name = ANY(${serializeArrayParam(queues)})`, "'{ \"value\": { \"message\": \"job timed out\" } }'::jsonb"), table + "failJobsByTimeout");
2538
+ AND name = ANY(${serializeArrayParam(queues)})`, "'{ \"value\": { \"message\": \"job timed out\" } }'::jsonb"), table + "failJobsByTimeout", noAdvisoryLocks);
2387
2539
  }
2388
- function failJobsByHeartbeat(schema, table, queues) {
2540
+ function failJobsByHeartbeat(schema, table, queues, noAdvisoryLocks) {
2389
2541
  return locked(schema, failJobs(schema, table, `state = '${JOB_STATES.active}'
2390
2542
  AND heartbeat_seconds IS NOT NULL
2391
2543
  AND (heartbeat_on + heartbeat_seconds * interval '1s') < now()
2392
- AND name = ANY(${serializeArrayParam(queues)})`, "'{ \"value\": { \"message\": \"job heartbeat timeout\" } }'::jsonb"), table + "failJobsByHeartbeat");
2544
+ AND name = ANY(${serializeArrayParam(queues)})`, "'{ \"value\": { \"message\": \"job heartbeat timeout\" } }'::jsonb"), table + "failJobsByHeartbeat", noAdvisoryLocks);
2393
2545
  }
2394
2546
  function touchJobs(schema, table) {
2395
2547
  return `
@@ -2438,7 +2590,10 @@ function failJobs(schema, table, where, output) {
2438
2590
  output,
2439
2591
  dead_letter,
2440
2592
  heartbeat_on,
2441
- heartbeat_seconds
2593
+ heartbeat_seconds,
2594
+ blocked,
2595
+ blocking,
2596
+ pending_dependencies
2442
2597
  )
2443
2598
  SELECT
2444
2599
  id,
@@ -2478,7 +2633,10 @@ function failJobs(schema, table, where, output) {
2478
2633
  ${output},
2479
2634
  dead_letter,
2480
2635
  NULL as heartbeat_on,
2481
- heartbeat_seconds
2636
+ heartbeat_seconds,
2637
+ blocked,
2638
+ blocking,
2639
+ pending_dependencies
2482
2640
  FROM deleted_jobs
2483
2641
  ON CONFLICT DO NOTHING
2484
2642
  RETURNING *
@@ -2510,7 +2668,10 @@ function failJobs(schema, table, where, output) {
2510
2668
  output,
2511
2669
  dead_letter,
2512
2670
  heartbeat_on,
2513
- heartbeat_seconds
2671
+ heartbeat_seconds,
2672
+ blocked,
2673
+ blocking,
2674
+ pending_dependencies
2514
2675
  )
2515
2676
  SELECT
2516
2677
  id,
@@ -2538,7 +2699,10 @@ function failJobs(schema, table, where, output) {
2538
2699
  ${output},
2539
2700
  dead_letter,
2540
2701
  NULL as heartbeat_on,
2541
- heartbeat_seconds
2702
+ heartbeat_seconds,
2703
+ blocked,
2704
+ blocking,
2705
+ pending_dependencies
2542
2706
  FROM deleted_jobs
2543
2707
  WHERE id NOT IN (SELECT id from retried_jobs)
2544
2708
  RETURNING *
@@ -2566,7 +2730,85 @@ function failJobs(schema, table, where, output) {
2566
2730
  SELECT COUNT(*) FROM results
2567
2731
  `;
2568
2732
  }
2569
- function deletion(schema, table, queues) {
2733
+ function selectJobsToFailById(schema, table) {
2734
+ return {
2735
+ text: `SELECT * FROM ${schema}.${table} WHERE name = $1 AND id IN (SELECT UNNEST($2::uuid[])) AND state < '${JOB_STATES.completed}'`,
2736
+ values: []
2737
+ };
2738
+ }
2739
+ function deleteJobsToFail(schema, table) {
2740
+ return {
2741
+ text: `DELETE FROM ${schema}.${table} WHERE name = $1 AND id IN (SELECT UNNEST($2::uuid[]))`,
2742
+ values: []
2743
+ };
2744
+ }
2745
+ function selectJobsToFailByTimeout(schema, table, queues) {
2746
+ return {
2747
+ text: `SELECT * FROM ${schema}.${table}
2748
+ WHERE state = '${JOB_STATES.active}'
2749
+ AND (started_on + expire_seconds * interval '1s') < now()
2750
+ AND name = ANY(${serializeArrayParam(queues)})`,
2751
+ values: []
2752
+ };
2753
+ }
2754
+ function selectJobsToFailByHeartbeat(schema, table, queues) {
2755
+ return {
2756
+ text: `SELECT * FROM ${schema}.${table}
2757
+ WHERE state = '${JOB_STATES.active}'
2758
+ AND heartbeat_seconds IS NOT NULL
2759
+ AND (heartbeat_on + heartbeat_seconds * interval '1s') < now()
2760
+ AND name = ANY(${serializeArrayParam(queues)})`,
2761
+ values: []
2762
+ };
2763
+ }
2764
+ function deleteJobsByIds(schema, table) {
2765
+ return {
2766
+ text: `DELETE FROM ${schema}.${table} WHERE id IN (SELECT UNNEST($1::uuid[]))`,
2767
+ values: []
2768
+ };
2769
+ }
2770
+ function completeJobsDistributed(schema, table, includeQueued) {
2771
+ return `
2772
+ ${completeJobsUpdate(schema, table, includeQueued)}
2773
+ RETURNING id, blocking
2774
+ `;
2775
+ }
2776
+ function decrementDependents(schema) {
2777
+ return `
2778
+ WITH decremented AS (
2779
+ SELECT d.child_name, d.child_id, COUNT(*)::int AS n
2780
+ FROM ${schema}.job_dependency d
2781
+ WHERE d.parent_name = $1
2782
+ AND d.parent_id IN (SELECT UNNEST($2::uuid[]))
2783
+ GROUP BY d.child_name, d.child_id
2784
+ ),
2785
+ ${lockedChildrenCte(schema)}
2786
+ ${unblockChildrenUpdate(schema)}
2787
+ `;
2788
+ }
2789
+ function insertRetryJob(schema, table) {
2790
+ return `
2791
+ INSERT INTO ${schema}.${table} (
2792
+ id, name, priority, data, state, retry_limit, retry_count, retry_delay,
2793
+ retry_backoff, retry_delay_max, start_after, started_on, singleton_key, singleton_on,
2794
+ group_id, group_tier, expire_seconds, deletion_seconds, created_on, completed_on,
2795
+ keep_until, policy, output, dead_letter,
2796
+ heartbeat_on, heartbeat_seconds, blocked, blocking, pending_dependencies
2797
+ ) VALUES (
2798
+ $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24,
2799
+ $25, $26, $27, $28, $29
2800
+ ) ON CONFLICT DO NOTHING
2801
+ RETURNING id
2802
+ `;
2803
+ }
2804
+ function insertDeadLetterJob(schema) {
2805
+ return `
2806
+ INSERT INTO ${schema}.job (name, data, output, retry_limit, retry_backoff, retry_delay, keep_until, deletion_seconds)
2807
+ SELECT $1, $2, $3, q.retry_limit, q.retry_backoff, q.retry_delay, now() + q.retention_seconds * interval '1s', q.deletion_seconds
2808
+ FROM ${schema}.queue q WHERE q.name = $1
2809
+ `;
2810
+ }
2811
+ function deletion(schema, table, queues, noAdvisoryLocks) {
2570
2812
  return locked(schema, `
2571
2813
  DELETE FROM ${schema}.${table}
2572
2814
  WHERE name = ANY(${serializeArrayParam(queues)})
@@ -2576,7 +2818,7 @@ function deletion(schema, table, queues) {
2576
2818
  OR
2577
2819
  (state < '${JOB_STATES.active}' AND keep_until < now())
2578
2820
  )
2579
- `, table + "deletion");
2821
+ `, table + "deletion", noAdvisoryLocks);
2580
2822
  }
2581
2823
  function retryJobs(schema, table) {
2582
2824
  return `
@@ -2609,7 +2851,7 @@ function getQueueStats$1(schema, table, queues) {
2609
2851
  values: [queues]
2610
2852
  };
2611
2853
  }
2612
- function cacheQueueStats(schema, table, queues) {
2854
+ function cacheQueueStats(schema, table, queues, noAdvisoryLocks) {
2613
2855
  return locked(schema, `
2614
2856
  WITH stats AS (${getQueueStats$1(schema, table, queues).text.replace("$1::text[]", serializeArrayParam(queues))})
2615
2857
  UPDATE ${schema}.queue SET
@@ -2628,22 +2870,27 @@ function cacheQueueStats(schema, table, queues) {
2628
2870
  queue.name,
2629
2871
  queue.queued_count as "queuedCount",
2630
2872
  queue.warning_queued as "warningQueueSize"
2631
- `, "queue-stats");
2873
+ `, "queue-stats", noAdvisoryLocks);
2632
2874
  }
2633
2875
  function serializeArrayParam(values) {
2634
2876
  return `ARRAY[${values.map((v) => `'${v.replace(SINGLE_QUOTE_REGEX, "''")}'`).join(",")}]::text[]`;
2635
2877
  }
2636
- function locked(schema, query, key) {
2637
- const sql = Array.isArray(query) ? query.join(";\n") : query;
2878
+ function serializeJsonParam(value) {
2879
+ return `'${JSON.stringify(value).replace(SINGLE_QUOTE_REGEX, "''")}'`;
2880
+ }
2881
+ function transaction(query) {
2638
2882
  return `
2639
2883
  BEGIN;
2640
2884
  SET LOCAL lock_timeout = 30000;
2641
2885
  SET LOCAL idle_in_transaction_session_timeout = 30000;
2642
- ${advisoryLock(schema, key)};
2643
- ${sql};
2886
+ ${Array.isArray(query) ? query.join(";\n") : query};
2644
2887
  COMMIT;
2645
2888
  `;
2646
2889
  }
2890
+ function locked(schema, query, key, noAdvisoryLocks) {
2891
+ const statements = Array.isArray(query) ? query : [query];
2892
+ return transaction(noAdvisoryLocks ? statements : [advisoryLock(schema, key), ...statements]);
2893
+ }
2647
2894
  function advisoryLock(schema, key) {
2648
2895
  return `SELECT pg_advisory_xact_lock(
2649
2896
  ('x' || encode(sha224((current_database() || '.pgboss.${schema}${key || ""}')::bytea), 'hex'))::bit(64)::bigint
@@ -2684,6 +2931,48 @@ function getJobById$1(schema, table) {
2684
2931
  AND id = $2
2685
2932
  `;
2686
2933
  }
2934
+ function insertDependencies(schema) {
2935
+ return `
2936
+ INSERT INTO ${schema}.job_dependency (child_name, child_id, parent_name, parent_id)
2937
+ SELECT child_name, child_id, parent_name, parent_id
2938
+ FROM json_to_recordset($1::json) AS x (
2939
+ child_name text,
2940
+ child_id uuid,
2941
+ parent_name text,
2942
+ parent_id uuid
2943
+ )
2944
+ ON CONFLICT DO NOTHING
2945
+ `;
2946
+ }
2947
+ function getDependencies(schema) {
2948
+ return `
2949
+ SELECT parent_name as "parentName", parent_id as "parentId"
2950
+ FROM ${schema}.job_dependency
2951
+ WHERE child_name = $1 AND child_id = $2
2952
+ `;
2953
+ }
2954
+ function getDependents(schema) {
2955
+ return `
2956
+ SELECT child_name as "childName", child_id as "childId"
2957
+ FROM ${schema}.job_dependency
2958
+ WHERE parent_name = $1 AND parent_id = $2
2959
+ `;
2960
+ }
2961
+ function cleanupDependencies(schema, table, queues, noAdvisoryLocks) {
2962
+ return locked(schema, `
2963
+ DELETE FROM ${schema}.job_dependency
2964
+ WHERE (child_name = ANY(${serializeArrayParam(queues)})
2965
+ AND NOT EXISTS (
2966
+ SELECT 1 FROM ${schema}.${table} j
2967
+ WHERE j.name = child_name AND j.id = child_id
2968
+ ))
2969
+ OR (parent_name = ANY(${serializeArrayParam(queues)})
2970
+ AND NOT EXISTS (
2971
+ SELECT 1 FROM ${schema}.${table} j
2972
+ WHERE j.name = parent_name AND j.id = parent_id
2973
+ ))
2974
+ `, table + "cleanupDependencies", noAdvisoryLocks);
2975
+ }
2687
2976
  function getBlockedKeys(schema, table) {
2688
2977
  return `
2689
2978
  SELECT DISTINCT singleton_key as "singletonKey"
@@ -2744,6 +3033,46 @@ var POLICY = {
2744
3033
  MIN_POLLING_INTERVAL_MS: 500,
2745
3034
  MAX_RETENTION_DAYS: 365
2746
3035
  };
3036
+ var COMPATIBILITY_FLAGS = [
3037
+ "noSkipLocked",
3038
+ "noMultiMutationCte",
3039
+ "noTablePartitioning",
3040
+ "noDeferrableConstraints",
3041
+ "noAdvisoryLocks",
3042
+ "noCoveringIndexes"
3043
+ ];
3044
+ var BACKEND_PROFILES = {
3045
+ postgres: {
3046
+ kind: "standard",
3047
+ flags: {}
3048
+ },
3049
+ cockroachdb: {
3050
+ kind: "distributed",
3051
+ flags: {
3052
+ noSkipLocked: true,
3053
+ noMultiMutationCte: true,
3054
+ noTablePartitioning: true,
3055
+ noDeferrableConstraints: true,
3056
+ noAdvisoryLocks: true,
3057
+ noCoveringIndexes: true
3058
+ }
3059
+ },
3060
+ yugabytedb: {
3061
+ kind: "distributed",
3062
+ flags: {
3063
+ noAdvisoryLocks: true,
3064
+ noTablePartitioning: true
3065
+ }
3066
+ },
3067
+ citus: {
3068
+ kind: "distributed",
3069
+ flags: {}
3070
+ },
3071
+ pglite: {
3072
+ kind: "embedded",
3073
+ flags: {}
3074
+ }
3075
+ };
2747
3076
  function assertObjectName(value, name = "Name") {
2748
3077
  assert(/^[\w.\-/]+$/.test(value), `${name} can only contain alphanumeric characters, underscores, hyphens, periods, or forward slashes`);
2749
3078
  }
@@ -2796,6 +3125,82 @@ function validateGroupConfig(config) {
2796
3125
  assert(typeof config.group.id === "string" && config.group.id.length > 0, "group.id must be a non-empty string");
2797
3126
  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
3127
  }
3128
+ function validateFlowJobs(jobs) {
3129
+ assert(Array.isArray(jobs), "flow requires an array of jobs");
3130
+ assert(jobs.length >= 2, "flow requires at least 2 jobs");
3131
+ const refs = /* @__PURE__ */ new Set();
3132
+ for (const job of jobs) {
3133
+ assert(typeof job.ref === "string" && job.ref.length > 0, "each flow job must have a non-empty ref");
3134
+ assert(!refs.has(job.ref), `duplicate ref: "${job.ref}"`);
3135
+ refs.add(job.ref);
3136
+ assert(typeof job.name === "string" && job.name.length > 0, "each flow job must have a non-empty name");
3137
+ assertObjectName(job.name);
3138
+ }
3139
+ assert(jobs.some((j) => j.dependsOn && j.dependsOn.length > 0), "flow requires at least one job with dependsOn");
3140
+ for (const job of jobs) {
3141
+ if (!job.dependsOn) continue;
3142
+ assert(Array.isArray(job.dependsOn), `dependsOn for ref "${job.ref}" must be an array`);
3143
+ for (const dep of job.dependsOn) {
3144
+ assert(typeof dep === "string" && dep.length > 0, "dependsOn entries must be non-empty strings");
3145
+ assert(dep !== job.ref, `job "${job.ref}" cannot depend on itself`);
3146
+ assert(refs.has(dep), `dependsOn ref "${dep}" not found in flow`);
3147
+ }
3148
+ }
3149
+ const inDegree = /* @__PURE__ */ new Map();
3150
+ const edges = /* @__PURE__ */ new Map();
3151
+ for (const job of jobs) {
3152
+ inDegree.set(job.ref, 0);
3153
+ edges.set(job.ref, []);
3154
+ }
3155
+ for (const job of jobs) {
3156
+ if (!job.dependsOn) continue;
3157
+ for (const dep of job.dependsOn) {
3158
+ edges.get(dep).push(job.ref);
3159
+ inDegree.set(job.ref, inDegree.get(job.ref) + 1);
3160
+ }
3161
+ }
3162
+ const queue = [];
3163
+ for (const [ref, deg] of inDegree) if (deg === 0) queue.push(ref);
3164
+ let visited = 0;
3165
+ while (queue.length > 0) {
3166
+ const current = queue.shift();
3167
+ visited++;
3168
+ for (const child of edges.get(current)) {
3169
+ const newDeg = inDegree.get(child) - 1;
3170
+ inDegree.set(child, newDeg);
3171
+ if (newDeg === 0) queue.push(child);
3172
+ }
3173
+ }
3174
+ if (visited !== jobs.length) assert(false, `flow contains a dependency cycle: ${findDependencyCycle(edges).join(" -> ")}`);
3175
+ }
3176
+ function findDependencyCycle(edges) {
3177
+ const visiting = /* @__PURE__ */ new Set();
3178
+ const visited = /* @__PURE__ */ new Set();
3179
+ const path = [];
3180
+ function visit(ref) {
3181
+ if (visiting.has(ref)) {
3182
+ const start = path.indexOf(ref);
3183
+ return [...path.slice(start), ref];
3184
+ }
3185
+ if (visited.has(ref)) return null;
3186
+ visiting.add(ref);
3187
+ path.push(ref);
3188
+ for (const child of edges.get(ref) || []) {
3189
+ const cycle = visit(child);
3190
+ if (cycle) return cycle;
3191
+ }
3192
+ path.pop();
3193
+ visiting.delete(ref);
3194
+ visited.add(ref);
3195
+ return null;
3196
+ }
3197
+ let cycle = null;
3198
+ for (const ref of edges.keys()) {
3199
+ cycle = visit(ref);
3200
+ if (cycle) break;
3201
+ }
3202
+ return cycle;
3203
+ }
2799
3204
  function validateGroupConcurrencyValue(value, optionName) {
2800
3205
  if (typeof value === "number") {
2801
3206
  assert(Number.isInteger(value) && value >= 1, `${optionName} must be an integer >= 1`);
@@ -2875,6 +3280,7 @@ function getConfig(value) {
2875
3280
  config.supervise = "supervise" in config ? config.supervise : true;
2876
3281
  config.migrate = "migrate" in config ? config.migrate : true;
2877
3282
  config.createSchema = "createSchema" in config ? config.createSchema : true;
3283
+ resolveBackend(config);
2878
3284
  applySchemaConfig(config);
2879
3285
  applyOpsConfig(config);
2880
3286
  applyScheduleConfig(config);
@@ -2892,6 +3298,17 @@ function validateWarningConfig(config) {
2892
3298
  assert(!("warningRetentionDays" in config) || Number.isInteger(config.warningRetentionDays) && config.warningRetentionDays >= 1, "configuration assert: warningRetentionDays must be an integer >= 1");
2893
3299
  assert(!("warningRetentionDays" in config) || config.warningRetentionDays <= POLICY.MAX_RETENTION_DAYS, `configuration assert: warningRetentionDays cannot exceed ${POLICY.MAX_RETENTION_DAYS} days`);
2894
3300
  }
3301
+ function resolveBackend(config) {
3302
+ const backend = "backend" in config ? config.backend : "postgres";
3303
+ assert(backend in BACKEND_PROFILES, `configuration assert: backend must be one of ${Object.keys(BACKEND_PROFILES).join(", ")}`);
3304
+ config.backend = backend;
3305
+ const { flags } = BACKEND_PROFILES[backend];
3306
+ for (const flag of COMPATIBILITY_FLAGS) config[flag] = flags[flag] ?? false;
3307
+ if (config.__test__distributed) {
3308
+ config.noSkipLocked = true;
3309
+ config.noMultiMutationCte = true;
3310
+ }
3311
+ }
2895
3312
  function assertPostgresObjectName(name) {
2896
3313
  assert(typeof name === "string", "Name must be a string");
2897
3314
  assert(name.length <= 50, "Name cannot exceed 50 characters");
@@ -2965,24 +3382,24 @@ function applyBamConfig(config) {
2965
3382
  }
2966
3383
  //#endregion
2967
3384
  //#region ../../src/migrationStore.ts
2968
- function flatten(schema, commands, version) {
3385
+ function flatten(schema, commands, version, noAdvisoryLocks) {
2969
3386
  commands.unshift(assertMigration(schema, version));
2970
3387
  commands.push(setVersion(schema, version));
2971
- return locked(schema, commands);
3388
+ return locked(schema, commands, void 0, noAdvisoryLocks);
2972
3389
  }
2973
- function rollback(schema, version, migrations) {
3390
+ function rollback(schema, version, migrations, noAdvisoryLocks) {
2974
3391
  migrations = migrations || getAll(schema);
2975
3392
  const result = migrations.find((i) => i.version === version);
2976
3393
  assert(result, `Version ${version} not found.`);
2977
- return flatten(schema, result.uninstall || [], result.previous);
3394
+ return flatten(schema, result.uninstall || [], result.previous, noAdvisoryLocks);
2978
3395
  }
2979
- function next(schema, version, migrations) {
3396
+ function next(schema, version, migrations, noAdvisoryLocks) {
2980
3397
  migrations = migrations || getAll(schema);
2981
3398
  const result = migrations.find((i) => i.previous === version);
2982
3399
  assert(result, `Version ${version} not found.`);
2983
- return flatten(schema, result.install, result.version);
3400
+ return flatten(schema, result.install, result.version, noAdvisoryLocks);
2984
3401
  }
2985
- function migrate(schema, version, migrations) {
3402
+ function migrate(schema, version, migrations, noAdvisoryLocks) {
2986
3403
  migrations = migrations || getAll(schema);
2987
3404
  const result = migrations.filter((i) => i.previous >= version).sort((a, b) => a.version - b.version).reduce((acc, migration) => {
2988
3405
  acc.install = acc.install.concat(migration.install);
@@ -2997,7 +3414,7 @@ function migrate(schema, version, migrations) {
2997
3414
  version
2998
3415
  });
2999
3416
  assert(result.install.length > 0, `Version ${version} not found.`);
3000
- return flatten(schema, result.install, result.version);
3417
+ return flatten(schema, result.install, result.version, noAdvisoryLocks);
3001
3418
  }
3002
3419
  function getAll(schema) {
3003
3420
  return [
@@ -3743,6 +4160,203 @@ function getAll(schema) {
3743
4160
  `ALTER TABLE ${schema}.job DROP COLUMN heartbeat_seconds`,
3744
4161
  `ALTER TABLE ${schema}.job DROP COLUMN heartbeat_on`
3745
4162
  ]
4163
+ },
4164
+ {
4165
+ release: "12.19.0",
4166
+ version: 31,
4167
+ previous: 30,
4168
+ install: [
4169
+ `ALTER TABLE ${schema}.job ADD COLUMN blocked boolean NOT NULL DEFAULT false`,
4170
+ `ALTER TABLE ${schema}.job ADD COLUMN blocking boolean NOT NULL DEFAULT false`,
4171
+ `ALTER TABLE ${schema}.job ADD COLUMN pending_dependencies int NOT NULL DEFAULT 0`,
4172
+ `
4173
+ CREATE TABLE IF NOT EXISTS ${schema}.job_dependency (
4174
+ child_name text NOT NULL,
4175
+ child_id uuid NOT NULL,
4176
+ parent_name text NOT NULL,
4177
+ parent_id uuid NOT NULL,
4178
+ PRIMARY KEY (child_name, child_id, parent_name, parent_id)
4179
+ )
4180
+ `,
4181
+ `CREATE INDEX IF NOT EXISTS job_dep_parent_idx ON ${schema}.job_dependency (parent_name, parent_id)`,
4182
+ `SELECT ${schema}.job_table_run($cmd$DROP INDEX IF EXISTS ${schema}.job_i5$cmd$)`,
4183
+ `SELECT ${schema}.job_table_run($cmd$CREATE INDEX job_i5 ON ${schema}.job (name, start_after) INCLUDE (priority, created_on, id) WHERE state < 'active' AND NOT blocked$cmd$)`,
4184
+ `
4185
+ CREATE OR REPLACE FUNCTION ${schema}.create_queue(queue_name text, options jsonb)
4186
+ RETURNS VOID AS
4187
+ $$
4188
+ DECLARE
4189
+ tablename varchar := CASE WHEN options->>'partition' = 'true'
4190
+ THEN 'j' || encode(sha224(queue_name::bytea), 'hex')
4191
+ ELSE 'job_common'
4192
+ END;
4193
+ queue_created_on timestamptz;
4194
+ BEGIN
4195
+
4196
+ WITH q as (
4197
+ INSERT INTO ${schema}.queue (
4198
+ name,
4199
+ policy,
4200
+ retry_limit,
4201
+ retry_delay,
4202
+ retry_backoff,
4203
+ retry_delay_max,
4204
+ expire_seconds,
4205
+ retention_seconds,
4206
+ deletion_seconds,
4207
+ warning_queued,
4208
+ dead_letter,
4209
+ partition,
4210
+ table_name,
4211
+ heartbeat_seconds
4212
+ )
4213
+ VALUES (
4214
+ queue_name,
4215
+ options->>'policy',
4216
+ COALESCE((options->>'retryLimit')::int, 2),
4217
+ COALESCE((options->>'retryDelay')::int, 0),
4218
+ COALESCE((options->>'retryBackoff')::bool, false),
4219
+ (options->>'retryDelayMax')::int,
4220
+ COALESCE((options->>'expireInSeconds')::int, 900),
4221
+ COALESCE((options->>'retentionSeconds')::int, 1209600),
4222
+ COALESCE((options->>'deleteAfterSeconds')::int, 604800),
4223
+ COALESCE((options->>'warningQueueSize')::int, 0),
4224
+ options->>'deadLetter',
4225
+ COALESCE((options->>'partition')::bool, false),
4226
+ tablename,
4227
+ (options->>'heartbeatSeconds')::int
4228
+ )
4229
+ ON CONFLICT DO NOTHING
4230
+ RETURNING created_on
4231
+ )
4232
+ SELECT created_on into queue_created_on from q;
4233
+
4234
+ IF queue_created_on IS NULL OR options->>'partition' IS DISTINCT FROM 'true' THEN
4235
+ RETURN;
4236
+ END IF;
4237
+
4238
+ EXECUTE format('CREATE TABLE ${schema}.%I (LIKE ${schema}.job INCLUDING DEFAULTS)', tablename);
4239
+
4240
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD PRIMARY KEY (name, id)$cmd$, tablename);
4241
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT q_fkey FOREIGN KEY (name) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
4242
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT dlq_fkey FOREIGN KEY (dead_letter) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
4243
+
4244
+ EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i5 ON ${schema}.job (name, start_after) INCLUDE (priority, created_on, id) WHERE state < 'active' AND NOT blocked$cmd$, tablename);
4245
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i4 ON ${schema}.job (name, singleton_on, COALESCE(singleton_key, '')) WHERE state <> 'cancelled' AND singleton_on IS NOT NULL$cmd$, tablename);
4246
+ EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i7 ON ${schema}.job (name, group_id) WHERE state = 'active' AND group_id IS NOT NULL$cmd$, tablename);
4247
+
4248
+ IF options->>'policy' = 'short' THEN
4249
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i1 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = 'created' AND policy = 'short'$cmd$, tablename);
4250
+ ELSIF options->>'policy' = 'singleton' THEN
4251
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i2 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = 'active' AND policy = 'singleton'$cmd$, tablename);
4252
+ ELSIF options->>'policy' = 'stately' THEN
4253
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i3 ON ${schema}.job (name, state, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'stately'$cmd$, tablename);
4254
+ ELSIF options->>'policy' = 'exclusive' THEN
4255
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i6 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'exclusive'$cmd$, tablename);
4256
+ ELSIF options->>'policy' = 'key_strict_fifo' THEN
4257
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i8 ON ${schema}.job (name, singleton_key) WHERE state IN ('active', 'retry', 'failed') AND policy = 'key_strict_fifo'$cmd$, tablename);
4258
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT job_key_strict_fifo_singleton_key_check CHECK (NOT (policy = 'key_strict_fifo' AND singleton_key IS NULL))$cmd$, tablename);
4259
+ END IF;
4260
+
4261
+ EXECUTE format('ALTER TABLE ${schema}.%I ADD CONSTRAINT cjc CHECK (name=%L)', tablename, queue_name);
4262
+ EXECUTE format('ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.%I FOR VALUES IN (%L)', tablename, queue_name);
4263
+ END;
4264
+ $$
4265
+ LANGUAGE plpgsql;
4266
+ `
4267
+ ],
4268
+ uninstall: [
4269
+ `DROP INDEX IF EXISTS ${schema}.job_dep_parent_idx`,
4270
+ `DROP TABLE IF EXISTS ${schema}.job_dependency`,
4271
+ `
4272
+ CREATE OR REPLACE FUNCTION ${schema}.create_queue(queue_name text, options jsonb)
4273
+ RETURNS VOID AS
4274
+ $$
4275
+ DECLARE
4276
+ tablename varchar := CASE WHEN options->>'partition' = 'true'
4277
+ THEN 'j' || encode(sha224(queue_name::bytea), 'hex')
4278
+ ELSE 'job_common'
4279
+ END;
4280
+ queue_created_on timestamptz;
4281
+ BEGIN
4282
+
4283
+ WITH q as (
4284
+ INSERT INTO ${schema}.queue (
4285
+ name,
4286
+ policy,
4287
+ retry_limit,
4288
+ retry_delay,
4289
+ retry_backoff,
4290
+ retry_delay_max,
4291
+ expire_seconds,
4292
+ retention_seconds,
4293
+ deletion_seconds,
4294
+ warning_queued,
4295
+ dead_letter,
4296
+ partition,
4297
+ table_name,
4298
+ heartbeat_seconds
4299
+ )
4300
+ VALUES (
4301
+ queue_name,
4302
+ options->>'policy',
4303
+ COALESCE((options->>'retryLimit')::int, 2),
4304
+ COALESCE((options->>'retryDelay')::int, 0),
4305
+ COALESCE((options->>'retryBackoff')::bool, false),
4306
+ (options->>'retryDelayMax')::int,
4307
+ COALESCE((options->>'expireInSeconds')::int, 900),
4308
+ COALESCE((options->>'retentionSeconds')::int, 1209600),
4309
+ COALESCE((options->>'deleteAfterSeconds')::int, 604800),
4310
+ COALESCE((options->>'warningQueueSize')::int, 0),
4311
+ options->>'deadLetter',
4312
+ COALESCE((options->>'partition')::bool, false),
4313
+ tablename,
4314
+ (options->>'heartbeatSeconds')::int
4315
+ )
4316
+ ON CONFLICT DO NOTHING
4317
+ RETURNING created_on
4318
+ )
4319
+ SELECT created_on into queue_created_on from q;
4320
+
4321
+ IF queue_created_on IS NULL OR options->>'partition' IS DISTINCT FROM 'true' THEN
4322
+ RETURN;
4323
+ END IF;
4324
+
4325
+ EXECUTE format('CREATE TABLE ${schema}.%I (LIKE ${schema}.job INCLUDING DEFAULTS)', tablename);
4326
+
4327
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD PRIMARY KEY (name, id)$cmd$, tablename);
4328
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT q_fkey FOREIGN KEY (name) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
4329
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT dlq_fkey FOREIGN KEY (dead_letter) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
4330
+
4331
+ EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i5 ON ${schema}.job (name, start_after) INCLUDE (priority, created_on, id) WHERE state < 'active'$cmd$, tablename);
4332
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i4 ON ${schema}.job (name, singleton_on, COALESCE(singleton_key, '')) WHERE state <> 'cancelled' AND singleton_on IS NOT NULL$cmd$, tablename);
4333
+ EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i7 ON ${schema}.job (name, group_id) WHERE state = 'active' AND group_id IS NOT NULL$cmd$, tablename);
4334
+
4335
+ IF options->>'policy' = 'short' THEN
4336
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i1 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = 'created' AND policy = 'short'$cmd$, tablename);
4337
+ ELSIF options->>'policy' = 'singleton' THEN
4338
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i2 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = 'active' AND policy = 'singleton'$cmd$, tablename);
4339
+ ELSIF options->>'policy' = 'stately' THEN
4340
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i3 ON ${schema}.job (name, state, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'stately'$cmd$, tablename);
4341
+ ELSIF options->>'policy' = 'exclusive' THEN
4342
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i6 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'exclusive'$cmd$, tablename);
4343
+ ELSIF options->>'policy' = 'key_strict_fifo' THEN
4344
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i8 ON ${schema}.job (name, singleton_key) WHERE state IN ('active', 'retry', 'failed') AND policy = 'key_strict_fifo'$cmd$, tablename);
4345
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT job_key_strict_fifo_singleton_key_check CHECK (NOT (policy = 'key_strict_fifo' AND singleton_key IS NULL))$cmd$, tablename);
4346
+ END IF;
4347
+
4348
+ EXECUTE format('ALTER TABLE ${schema}.%I ADD CONSTRAINT cjc CHECK (name=%L)', tablename, queue_name);
4349
+ EXECUTE format('ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.%I FOR VALUES IN (%L)', tablename, queue_name);
4350
+ END;
4351
+ $$
4352
+ LANGUAGE plpgsql;
4353
+ `,
4354
+ `SELECT ${schema}.job_table_run($cmd$DROP INDEX IF EXISTS ${schema}.job_i5$cmd$)`,
4355
+ `SELECT ${schema}.job_table_run($cmd$CREATE INDEX job_i5 ON ${schema}.job (name, start_after) INCLUDE (priority, created_on, id) WHERE state < 'active'$cmd$)`,
4356
+ `ALTER TABLE ${schema}.job DROP COLUMN pending_dependencies`,
4357
+ `ALTER TABLE ${schema}.job DROP COLUMN blocking`,
4358
+ `ALTER TABLE ${schema}.job DROP COLUMN blocked`
4359
+ ]
3746
4360
  }
3747
4361
  ];
3748
4362
  }
@@ -3750,7 +4364,7 @@ function getAll(schema) {
3750
4364
  //#region ../../src/contractor.ts
3751
4365
  var schemaVersion = {
3752
4366
  name: "pg-boss",
3753
- version: "12.18.3",
4367
+ version: "12.20.0",
3754
4368
  description: "Queueing jobs in Postgres from Node.js like a boss",
3755
4369
  type: "module",
3756
4370
  main: "./dist/index.js",
@@ -3763,6 +4377,7 @@ var schemaVersion = {
3763
4377
  "serialize-error": "^13.0.1"
3764
4378
  },
3765
4379
  devDependencies: {
4380
+ "@electric-sql/pglite": "^0.4.1",
3766
4381
  "@prisma/adapter-pg": "^7.8.0",
3767
4382
  "@prisma/client": "^7.8.0",
3768
4383
  "@tsconfig/node-ts": "^23.6.4",
@@ -3772,6 +4387,7 @@ var schemaVersion = {
3772
4387
  "@types/pg": "^8.20.0",
3773
4388
  "@vitest/coverage-v8": "^4.1.2",
3774
4389
  "cli-testlab": "^6.0.1",
4390
+ "cross-env": "^10.1.0",
3775
4391
  "drizzle-orm": "^1.0.0-beta.22",
3776
4392
  "eslint": "^9.39.4",
3777
4393
  "knex": "^3.2.10",
@@ -3789,6 +4405,12 @@ var schemaVersion = {
3789
4405
  "prepublishOnly": "npm install && npm test && npm run build",
3790
4406
  "pretest": "prisma generate --schema=test/prisma/schema.prisma",
3791
4407
  "test": "eslint . && vitest run",
4408
+ "test:distributed": "cross-env DISTRIBUTED=true npm test",
4409
+ "test:cockroachdb": "cross-env DB_TYPE=cockroachdb COCKROACH_HOST=localhost npm test -- test/distributedDatabaseTest.ts",
4410
+ "test:cockroachdb:full": "cross-env DB_TYPE=cockroachdb COCKROACH_HOST=localhost npm test -- --no-file-parallelism",
4411
+ "test:yugabytedb:full": "cross-env DB_TYPE=yugabytedb YUGABYTE_HOST=localhost npm test -- --no-file-parallelism",
4412
+ "test:citus:full": "cross-env DB_TYPE=citus CITUS_HOST=localhost npm test",
4413
+ "test:pglite": "cross-env DB_TYPE=pglite npm test -- --no-file-parallelism",
3792
4414
  "lint:fix": "eslint . --fix",
3793
4415
  "precover": "prisma generate --schema=test/prisma/schema.prisma",
3794
4416
  "cover": "vitest run --coverage",
@@ -3797,7 +4419,7 @@ var schemaVersion = {
3797
4419
  "db:migrate": "node --import=tsx -e 'console.log(require(\"./src\").getMigrationPlans())'",
3798
4420
  "db:construct": "node --import=tsx -e 'console.log(require(\"./src\").getConstructionPlans())'"
3799
4421
  },
3800
- pgboss: { "schema": 30 },
4422
+ pgboss: { "schema": 31 },
3801
4423
  repository: {
3802
4424
  "type": "git",
3803
4425
  "url": "git+https://github.com/timgit/pg-boss.git"
@@ -3864,18 +4486,18 @@ var Contractor = class {
3864
4486
  }
3865
4487
  async migrate(version) {
3866
4488
  try {
3867
- const commands = migrate(this.config.schema, version, this.migrations);
4489
+ const commands = migrate(this.config.schema, version, this.migrations, this.config.noAdvisoryLocks);
3868
4490
  await this.db.executeSql(commands);
3869
4491
  } catch (err) {
3870
4492
  assert(err.message.includes(MIGRATE_RACE_MESSAGE), err);
3871
4493
  }
3872
4494
  }
3873
4495
  async next(version) {
3874
- const commands = next(this.config.schema, version, this.migrations);
4496
+ const commands = next(this.config.schema, version, this.migrations, this.config.noAdvisoryLocks);
3875
4497
  await this.db.executeSql(commands);
3876
4498
  }
3877
4499
  async rollback(version) {
3878
- const commands = rollback(this.config.schema, version, this.migrations);
4500
+ const commands = rollback(this.config.schema, version, this.migrations, this.config.noAdvisoryLocks);
3879
4501
  await this.db.executeSql(commands);
3880
4502
  }
3881
4503
  };
@@ -4280,10 +4902,39 @@ var INTERNAL_QUEUES = Object.values(QUEUES).reduce((acc, i) => ({
4280
4902
  ...acc,
4281
4903
  [i]: i
4282
4904
  }), {});
4905
+ var NUMERIC_METADATA_FIELDS = [
4906
+ "priority",
4907
+ "retryLimit",
4908
+ "retryCount",
4909
+ "retryDelay",
4910
+ "retryDelayMax",
4911
+ "expireInSeconds",
4912
+ "heartbeatSeconds",
4913
+ "deleteAfterSeconds",
4914
+ "pendingDependencies"
4915
+ ];
4916
+ var NUMERIC_QUEUE_FIELDS = [
4917
+ "retryLimit",
4918
+ "retryDelay",
4919
+ "retryDelayMax",
4920
+ "expireInSeconds",
4921
+ "retentionSeconds",
4922
+ "deleteAfterSeconds",
4923
+ "heartbeatSeconds",
4924
+ "deferredCount",
4925
+ "warningQueueSize",
4926
+ "queuedCount",
4927
+ "activeCount",
4928
+ "totalCount"
4929
+ ];
4283
4930
  var events$3 = {
4284
4931
  error: "error",
4285
4932
  wip: "wip"
4286
4933
  };
4934
+ function rethrowWriteError(err) {
4935
+ 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 });
4936
+ throw err;
4937
+ }
4287
4938
  var Manager = class extends EventEmitter {
4288
4939
  events = events$3;
4289
4940
  db;
@@ -4735,6 +5386,10 @@ var Manager = class extends EventEmitter {
4735
5386
  if (policy === QUEUE_POLICIES.key_strict_fifo) {
4736
5387
  for (const job of jobs) if (!job.singletonKey) throw new Error(`${QUEUE_POLICIES.key_strict_fifo} queues require a singletonKey`);
4737
5388
  }
5389
+ const insertPayload = jobs.map((j) => {
5390
+ const { blocked, blocking, pendingDependencies, ...rest } = j;
5391
+ return rest;
5392
+ });
4738
5393
  const db = this.assertDb(options);
4739
5394
  const spy = this.config.__test__enableSpies ? this.#spies.get(name) : void 0;
4740
5395
  const returnId = !!spy || !!options.returnId;
@@ -4743,13 +5398,100 @@ var Manager = class extends EventEmitter {
4743
5398
  name,
4744
5399
  returnId
4745
5400
  });
4746
- const { rows } = await db.executeSql(sql, [JSON.stringify(jobs)]);
5401
+ const { rows } = await db.executeSql(sql, [JSON.stringify(insertPayload)]);
4747
5402
  if (rows.length) {
4748
5403
  if (spy) for (let i = 0; i < rows.length; i++) spy.addJob(rows[i].id, name, jobs[i].data || {}, "created");
4749
5404
  return rows.map((i) => i.id);
4750
5405
  }
4751
5406
  return null;
4752
5407
  }
5408
+ async flow(jobs, options = {}) {
5409
+ validateFlowJobs(jobs);
5410
+ const flowJobs = jobs.map((job) => ({
5411
+ ...job,
5412
+ options: checkSendArgs([{
5413
+ name: job.name,
5414
+ data: job.data,
5415
+ options: job.options
5416
+ }]).options
5417
+ }));
5418
+ const refToId = {};
5419
+ for (const job of flowJobs) refToId[job.ref] = job.options?.id ?? randomUUID();
5420
+ const refToJob = new Map(flowJobs.map((job) => [job.ref, job]));
5421
+ const dependencyCountByRef = /* @__PURE__ */ new Map();
5422
+ const parentRefs = /* @__PURE__ */ new Set();
5423
+ const depRows = [];
5424
+ for (const job of flowJobs) {
5425
+ const dependsOn = [...new Set(job.dependsOn ?? [])];
5426
+ dependencyCountByRef.set(job.ref, dependsOn.length);
5427
+ for (const depRef of dependsOn) {
5428
+ const parentJob = refToJob.get(depRef);
5429
+ parentRefs.add(depRef);
5430
+ depRows.push({
5431
+ child_name: job.name,
5432
+ child_id: refToId[job.ref],
5433
+ parent_name: parentJob.name,
5434
+ parent_id: refToId[depRef]
5435
+ });
5436
+ }
5437
+ }
5438
+ const byQueue = /* @__PURE__ */ new Map();
5439
+ for (const job of flowJobs) {
5440
+ const group = byQueue.get(job.name) || [];
5441
+ group.push(job);
5442
+ byQueue.set(job.name, group);
5443
+ }
5444
+ const statements = [];
5445
+ for (const [queueName, queueJobs] of byQueue) {
5446
+ const { table } = await this.getQueueCache(queueName);
5447
+ const insertPayload = queueJobs.map((j) => {
5448
+ const dependencyCount = dependencyCountByRef.get(j.ref) ?? 0;
5449
+ return {
5450
+ id: refToId[j.ref],
5451
+ name: queueName,
5452
+ data: j.data ?? null,
5453
+ priority: j.options?.priority,
5454
+ startAfter: j.options?.startAfter,
5455
+ singletonKey: j.options?.singletonKey ?? void 0,
5456
+ singletonSeconds: j.options?.singletonSeconds,
5457
+ groupId: j.options?.group?.id ?? void 0,
5458
+ groupTier: j.options?.group?.tier ?? void 0,
5459
+ expireInSeconds: j.options?.expireInSeconds,
5460
+ deleteAfterSeconds: j.options?.deleteAfterSeconds,
5461
+ retentionSeconds: j.options?.retentionSeconds,
5462
+ retryLimit: j.options?.retryLimit,
5463
+ retryDelay: j.options?.retryDelay,
5464
+ retryBackoff: j.options?.retryBackoff,
5465
+ retryDelayMax: j.options?.retryDelayMax,
5466
+ heartbeatSeconds: j.options?.heartbeatSeconds,
5467
+ deadLetter: j.options?.deadLetter ?? void 0,
5468
+ blocked: dependencyCount > 0 || void 0,
5469
+ blocking: parentRefs.has(j.ref) || void 0,
5470
+ pendingDependencies: dependencyCount || void 0
5471
+ };
5472
+ });
5473
+ const insertSql = insertJobs(this.config.schema, {
5474
+ table,
5475
+ name: queueName,
5476
+ returnId: true
5477
+ }).replace("$1", () => serializeJsonParam(insertPayload));
5478
+ statements.push(`
5479
+ WITH ins AS (
5480
+ ${insertSql}
5481
+ )
5482
+ SELECT 1 / (CASE WHEN (SELECT count(*) FROM ins) = ${insertPayload.length} THEN 1 ELSE 0 END)
5483
+ `);
5484
+ }
5485
+ if (depRows.length > 0) statements.push(insertDependencies(this.config.schema).replace("$1", () => serializeJsonParam(depRows)));
5486
+ const db = options.db ?? this.db;
5487
+ const sql = options.db ? statements.join(";\n") : transaction(statements);
5488
+ try {
5489
+ await db.executeSql(sql);
5490
+ } catch (err) {
5491
+ rethrowWriteError(err);
5492
+ }
5493
+ return refToId;
5494
+ }
4753
5495
  getDebounceStartAfter(singletonSeconds, clockOffset) {
4754
5496
  const debounceInterval = singletonSeconds * 1e3;
4755
5497
  const now = Date.now() + clockOffset;
@@ -4770,12 +5512,16 @@ var Manager = class extends EventEmitter {
4770
5512
  policy,
4771
5513
  limit: options.batchSize || 1,
4772
5514
  ignoreSingletons: singletonsActive
4773
- });
5515
+ }, this.config.noSkipLocked);
4774
5516
  let result;
4775
5517
  try {
4776
5518
  result = await db.executeSql(query.text, query.values);
4777
5519
  } catch (err) {}
4778
- return result?.rows || [];
5520
+ const rows = result?.rows || [];
5521
+ if (this.config.backend === "cockroachdb") {
5522
+ 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]);
5523
+ }
5524
+ return rows;
4779
5525
  }
4780
5526
  mapCompletionIdArg(id, funcName) {
4781
5527
  const errorMessage = `${funcName}() requires an id`;
@@ -4800,27 +5546,187 @@ var Manager = class extends EventEmitter {
4800
5546
  const db = this.assertDb(options);
4801
5547
  const ids = this.mapCompletionIdArg(id, "complete");
4802
5548
  const { table } = await this.getQueueCache(name);
5549
+ const outputData = this.mapCompletionDataArg(data);
5550
+ if (this.config.noMultiMutationCte) return this.completeDistributed(name, ids, outputData, table, db, options.includeQueued);
4803
5551
  const sql = completeJobs(this.config.schema, table, options.includeQueued);
4804
5552
  const result = await db.executeSql(sql, [
4805
5553
  name,
4806
5554
  ids,
4807
- this.mapCompletionDataArg(data)
5555
+ outputData
4808
5556
  ]);
4809
5557
  return this.mapCommandResponse(ids, result);
4810
5558
  }
5559
+ async withDistributedTransaction(db, fn) {
5560
+ if (db === this.db && this.db._pgbdb) return this.db.withTransaction(fn);
5561
+ return fn(db);
5562
+ }
5563
+ async completeDistributed(name, ids, outputData, table, db, includeQueued) {
5564
+ return this.withDistributedTransaction(db, async (tx) => {
5565
+ const completeSql = completeJobsDistributed(this.config.schema, table, includeQueued);
5566
+ const { rows } = await tx.executeSql(completeSql, [
5567
+ name,
5568
+ ids,
5569
+ outputData
5570
+ ]);
5571
+ const blockingIds = rows.filter((row) => row.blocking).map((row) => row.id);
5572
+ if (blockingIds.length > 0) {
5573
+ const decrementSql = decrementDependents(this.config.schema);
5574
+ await tx.executeSql(decrementSql, [name, blockingIds]);
5575
+ }
5576
+ return {
5577
+ jobs: ids,
5578
+ requested: ids.length,
5579
+ affected: rows.length
5580
+ };
5581
+ });
5582
+ }
4811
5583
  async fail(name, id, data, options = {}) {
4812
5584
  assertQueueName(name);
4813
5585
  const db = this.assertDb(options);
4814
5586
  const ids = this.mapCompletionIdArg(id, "fail");
4815
5587
  const { table } = await this.getQueueCache(name);
5588
+ const outputData = this.mapCompletionDataArg(data);
5589
+ if (this.config.noMultiMutationCte) return this.failDistributed(name, ids, outputData, table, db);
4816
5590
  const sql = failJobsById(this.config.schema, table);
4817
5591
  const result = await db.executeSql(sql, [
4818
5592
  name,
4819
5593
  ids,
4820
- this.mapCompletionDataArg(data)
5594
+ outputData
4821
5595
  ]);
4822
5596
  return this.mapCommandResponse(ids, result);
4823
5597
  }
5598
+ async failDistributed(name, ids, outputData, table, db) {
5599
+ return this.withDistributedTransaction(db, async (tx) => {
5600
+ const selectQuery = selectJobsToFailById(this.config.schema, table);
5601
+ const { rows: jobs } = await tx.executeSql(selectQuery.text, [name, ids]);
5602
+ if (jobs.length === 0) return {
5603
+ jobs: ids,
5604
+ requested: ids.length,
5605
+ affected: 0
5606
+ };
5607
+ const deleteQuery = deleteJobsToFail(this.config.schema, table);
5608
+ await tx.executeSql(deleteQuery.text, [name, ids]);
5609
+ const count = await this.reinsertFailedJobs(tx, table, jobs, outputData);
5610
+ return {
5611
+ jobs: ids,
5612
+ requested: ids.length,
5613
+ affected: count
5614
+ };
5615
+ });
5616
+ }
5617
+ async failJobsByTimeoutDistributed(table, queues) {
5618
+ const select = selectJobsToFailByTimeout(this.config.schema, table, queues);
5619
+ return this.expireJobsDistributed(table, select, { value: { message: "job timed out" } });
5620
+ }
5621
+ async failJobsByHeartbeatDistributed(table, queues) {
5622
+ const select = selectJobsToFailByHeartbeat(this.config.schema, table, queues);
5623
+ return this.expireJobsDistributed(table, select, { value: { message: "job heartbeat timeout" } });
5624
+ }
5625
+ async expireJobsDistributed(table, select, outputData) {
5626
+ return this.withDistributedTransaction(this.db, async (tx) => {
5627
+ const { rows: jobs } = await tx.executeSql(select.text, []);
5628
+ if (jobs.length === 0) return 0;
5629
+ const ids = jobs.map((job) => job.id);
5630
+ const deleteSql = deleteJobsByIds(this.config.schema, table);
5631
+ await tx.executeSql(deleteSql.text, [ids]);
5632
+ return this.reinsertFailedJobs(tx, table, jobs, outputData);
5633
+ });
5634
+ }
5635
+ async reinsertFailedJobs(tx, table, jobs, outputData) {
5636
+ const insertSql = insertRetryJob(this.config.schema, table);
5637
+ const dlqSql = insertDeadLetterJob(this.config.schema);
5638
+ let count = 0;
5639
+ for (const job of jobs) {
5640
+ const retryCount = Number(job.retry_count);
5641
+ const retryLimit = Number(job.retry_limit);
5642
+ const retryDelay = Number(job.retry_delay);
5643
+ const retryDelayMax = job.retry_delay_max != null ? Number(job.retry_delay_max) : null;
5644
+ const canRetry = retryCount < retryLimit;
5645
+ let retried = false;
5646
+ if (canRetry) {
5647
+ let startAfter = job.start_after;
5648
+ if (!job.retry_backoff) startAfter = new Date(Date.now() + retryDelay * 1e3);
5649
+ else {
5650
+ const exp = Math.min(16, retryCount + 1);
5651
+ const delay = retryDelay * (Math.pow(2, exp) / 2 + Math.pow(2, exp) / 2 * Math.random());
5652
+ const cappedDelay = retryDelayMax != null ? Math.min(retryDelayMax, delay) : delay;
5653
+ startAfter = new Date(Date.now() + cappedDelay * 1e3);
5654
+ }
5655
+ const { rows } = await tx.executeSql(insertSql, [
5656
+ job.id,
5657
+ job.name,
5658
+ job.priority,
5659
+ job.data,
5660
+ "retry",
5661
+ job.retry_limit,
5662
+ job.retry_count,
5663
+ job.retry_delay,
5664
+ job.retry_backoff,
5665
+ job.retry_delay_max,
5666
+ startAfter,
5667
+ job.started_on,
5668
+ job.singleton_key,
5669
+ job.singleton_on,
5670
+ job.group_id,
5671
+ job.group_tier,
5672
+ job.expire_seconds,
5673
+ job.deletion_seconds,
5674
+ job.created_on,
5675
+ null,
5676
+ job.keep_until,
5677
+ job.policy,
5678
+ outputData,
5679
+ job.dead_letter,
5680
+ null,
5681
+ job.heartbeat_seconds,
5682
+ job.blocked,
5683
+ job.blocking,
5684
+ job.pending_dependencies
5685
+ ]);
5686
+ retried = rows.length > 0;
5687
+ }
5688
+ if (!retried) {
5689
+ await tx.executeSql(insertSql, [
5690
+ job.id,
5691
+ job.name,
5692
+ job.priority,
5693
+ job.data,
5694
+ "failed",
5695
+ job.retry_limit,
5696
+ job.retry_count,
5697
+ job.retry_delay,
5698
+ job.retry_backoff,
5699
+ job.retry_delay_max,
5700
+ job.start_after,
5701
+ job.started_on,
5702
+ job.singleton_key,
5703
+ job.singleton_on,
5704
+ job.group_id,
5705
+ job.group_tier,
5706
+ job.expire_seconds,
5707
+ job.deletion_seconds,
5708
+ job.created_on,
5709
+ /* @__PURE__ */ new Date(),
5710
+ job.keep_until,
5711
+ job.policy,
5712
+ outputData,
5713
+ job.dead_letter,
5714
+ null,
5715
+ job.heartbeat_seconds,
5716
+ job.blocked,
5717
+ job.blocking,
5718
+ job.pending_dependencies
5719
+ ]);
5720
+ if (job.dead_letter) await tx.executeSql(dlqSql, [
5721
+ job.dead_letter,
5722
+ job.data,
5723
+ outputData
5724
+ ]);
5725
+ }
5726
+ count++;
5727
+ }
5728
+ return count;
5729
+ }
4824
5730
  async deleteJob(name, id, options = {}) {
4825
5731
  assertQueueName(name);
4826
5732
  const db = this.assertDb(options);
@@ -4888,7 +5794,7 @@ var Manager = class extends EventEmitter {
4888
5794
  const sql = createQueue$1(this.config.schema, name, {
4889
5795
  ...options,
4890
5796
  policy
4891
- });
5797
+ }, this.config.noAdvisoryLocks);
4892
5798
  await this.db.executeSql(sql);
4893
5799
  }
4894
5800
  async getBlockedKeys(name) {
@@ -4904,6 +5810,9 @@ var Manager = class extends EventEmitter {
4904
5810
  if (names) for (const name of names) assertQueueName(name);
4905
5811
  const query = getQueues$1(this.config.schema, names);
4906
5812
  const { rows } = await this.db.executeSql(query.text, query.values);
5813
+ if (this.config.backend === "cockroachdb") {
5814
+ 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]);
5815
+ }
4907
5816
  return rows;
4908
5817
  }
4909
5818
  async updateQueue(name, options = {}) {
@@ -4921,16 +5830,13 @@ var Manager = class extends EventEmitter {
4921
5830
  await this.db.executeSql(sql, [name, options]);
4922
5831
  }
4923
5832
  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;
5833
+ return (await this.getQueues([name]))[0] || null;
4928
5834
  }
4929
5835
  async deleteQueue(name) {
4930
5836
  assertQueueName(name);
4931
5837
  try {
4932
5838
  await this.getQueueCache(name);
4933
- const sql = deleteQueue(this.config.schema, name);
5839
+ const sql = deleteQueue(this.config.schema, name, this.config.noAdvisoryLocks);
4934
5840
  await this.db.executeSql(sql);
4935
5841
  } catch {}
4936
5842
  }
@@ -4967,7 +5873,11 @@ var Manager = class extends EventEmitter {
4967
5873
  const queue = await this.getQueueCache(name);
4968
5874
  const query = getQueueStats$1(this.config.schema, queue.table, [name]);
4969
5875
  const { rows } = await this.db.executeSql(query.text, query.values);
4970
- return Object.assign(queue, rows.at(0) || {
5876
+ const stats = rows.at(0);
5877
+ if (stats && this.config.backend === "cockroachdb") {
5878
+ for (const field of NUMERIC_QUEUE_FIELDS) if (stats[field] !== void 0 && stats[field] !== null) stats[field] = Number(stats[field]);
5879
+ }
5880
+ return Object.assign(queue, stats || {
4971
5881
  deferredCount: 0,
4972
5882
  queuedCount: 0,
4973
5883
  activeCount: 0,
@@ -4980,8 +5890,13 @@ var Manager = class extends EventEmitter {
4980
5890
  const { table } = await this.getQueueCache(name);
4981
5891
  const sql = getJobById$1(this.config.schema, table);
4982
5892
  const result1 = await db.executeSql(sql, [name, id]);
4983
- if (result1?.rows?.length === 1) return result1.rows[0];
4984
- else return null;
5893
+ if (result1?.rows?.length === 1) {
5894
+ const row = result1.rows[0];
5895
+ if (this.config.backend === "cockroachdb") {
5896
+ for (const field of NUMERIC_METADATA_FIELDS) if (row[field] !== void 0 && row[field] !== null) row[field] = Number(row[field]);
5897
+ }
5898
+ return row;
5899
+ } else return null;
4985
5900
  }
4986
5901
  async findJobs(name, options = {}) {
4987
5902
  assertQueueName(name);
@@ -5000,6 +5915,26 @@ var Manager = class extends EventEmitter {
5000
5915
  if (data !== void 0) values.push(JSON.stringify(data));
5001
5916
  return (await db.executeSql(sql, values))?.rows || [];
5002
5917
  }
5918
+ async getDependencies(name, id, options = {}) {
5919
+ assertQueueName(name);
5920
+ const db = this.assertDb(options);
5921
+ const sql = getDependencies(this.config.schema);
5922
+ const { rows } = await db.executeSql(sql, [name, id]);
5923
+ return rows.map((r) => ({
5924
+ name: r.parentName,
5925
+ id: r.parentId
5926
+ }));
5927
+ }
5928
+ async getDependents(name, id, options = {}) {
5929
+ assertQueueName(name);
5930
+ const db = this.assertDb(options);
5931
+ const sql = getDependents(this.config.schema);
5932
+ const { rows } = await db.executeSql(sql, [name, id]);
5933
+ return rows.map((r) => ({
5934
+ name: r.childName,
5935
+ id: r.childId
5936
+ }));
5937
+ }
5003
5938
  assertDb(options) {
5004
5939
  if (options.db) return options.db;
5005
5940
  if (this.db._pgbdb) assert(this.db.opened, "Database connection is not opened");
@@ -5142,17 +6077,21 @@ var Boss = class extends EventEmitter {
5142
6077
  if (this.#stopping) return;
5143
6078
  if (rows.length) {
5144
6079
  const queues = rows.map((q) => q.name);
5145
- const cacheStatsSql = cacheQueueStats(this.#config.schema, table, queues);
6080
+ const cacheStatsSql = cacheQueueStats(this.#config.schema, table, queues, this.#config.noAdvisoryLocks);
5146
6081
  const { rows: rowsCacheStats } = await this.#executeQuery(cacheStatsSql);
5147
6082
  if (this.#stopping) return;
5148
- const warnings = rowsCacheStats.filter((i) => i.queuedCount > (i.warningQueueSize || WARNINGS.LARGE_QUEUE.size));
6083
+ const warnings = rowsCacheStats.filter((i) => Number(i.queuedCount) > (Number(i.warningQueueSize) || WARNINGS.LARGE_QUEUE.size));
5149
6084
  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);
6085
+ if (this.#config.noMultiMutationCte) await this.#manager.failJobsByTimeoutDistributed(table, queues);
6086
+ else {
6087
+ const sql = failJobsByTimeout(this.#config.schema, table, queues, this.#config.noAdvisoryLocks);
6088
+ await this.#executeQuery(sql);
6089
+ }
5152
6090
  if (this.#stopping) return;
5153
6091
  const heartbeatQueues = queues.filter((q) => heartbeatQueueNames.has(q));
5154
- if (heartbeatQueues.length) {
5155
- const heartbeatSql = failJobsByHeartbeat(this.#config.schema, table, heartbeatQueues);
6092
+ if (heartbeatQueues.length) if (this.#config.noMultiMutationCte) await this.#manager.failJobsByHeartbeatDistributed(table, heartbeatQueues);
6093
+ else {
6094
+ const heartbeatSql = failJobsByHeartbeat(this.#config.schema, table, heartbeatQueues, this.#config.noAdvisoryLocks);
5156
6095
  await this.#executeQuery(heartbeatSql);
5157
6096
  }
5158
6097
  }
@@ -5164,8 +6103,10 @@ var Boss = class extends EventEmitter {
5164
6103
  if (this.#stopping) return;
5165
6104
  if (rows.length) {
5166
6105
  const queues = rows.map((q) => q.name);
5167
- const sql = deletion(this.#config.schema, table, queues);
6106
+ const sql = deletion(this.#config.schema, table, queues, this.#config.noAdvisoryLocks);
5168
6107
  await this.#executeQuery(sql);
6108
+ const depSql = cleanupDependencies(this.#config.schema, table, queues, this.#config.noAdvisoryLocks);
6109
+ await this.#executeQuery(depSql);
5169
6110
  }
5170
6111
  }
5171
6112
  };
@@ -5304,6 +6245,21 @@ var Db = class extends EventEmitter {
5304
6245
  assert(this.opened, "Database not opened. Call open() before executing SQL.");
5305
6246
  return await this.pool.query(text, values);
5306
6247
  }
6248
+ async withTransaction(fn) {
6249
+ assert(this.opened, "Database not opened. Call open() before executing SQL.");
6250
+ const client = await this.pool.connect();
6251
+ try {
6252
+ await client.query("BEGIN");
6253
+ const result = await fn({ executeSql: (text, values) => client.query(text, values) });
6254
+ await client.query("COMMIT");
6255
+ return result;
6256
+ } catch (err) {
6257
+ await client.query("ROLLBACK");
6258
+ throw err;
6259
+ } finally {
6260
+ client.release();
6261
+ }
6262
+ }
5307
6263
  };
5308
6264
  //#endregion
5309
6265
  //#region ../../src/index.ts
@@ -5357,18 +6313,36 @@ var PgBoss = class extends EventEmitter {
5357
6313
  async start() {
5358
6314
  if (this.#starting || this.#started) return this;
5359
6315
  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();
6316
+ try {
6317
+ if (this.#db._pgbdb && !this.#db.opened) await this.#db.open();
6318
+ await this.#warnIfDistributedMisconfigured();
6319
+ if (this.#config.migrate) await this.#contractor.start();
6320
+ else await this.#contractor.check();
6321
+ await this.#manager.start();
6322
+ if (this.#config.supervise) await this.#boss.start();
6323
+ if (this.#config.schedule) await this.#timekeeper.start();
6324
+ if (this.#config.migrate) await this.#bam.start();
6325
+ } catch (err) {
6326
+ this.#starting = false;
6327
+ throw err;
6328
+ }
5367
6329
  this.#starting = false;
5368
6330
  this.#started = true;
5369
6331
  this.#stopped = false;
5370
6332
  return this;
5371
6333
  }
6334
+ async #warnIfDistributedMisconfigured() {
6335
+ try {
6336
+ const { rows } = await this.#db.executeSql("SELECT version()");
6337
+ const version = rows?.[0]?.version || "";
6338
+ if (/yugabyte|-yb-/i.test(version)) {
6339
+ if (!this.#config.noTablePartitioning || !this.#config.noAdvisoryLocks) this.emit(events.warning, {
6340
+ message: "YugabyteDB detected: set backend: 'yugabytedb' for compatibility. Partitioned queues (partition: true) are not supported on YugabyteDB.",
6341
+ data: { backend: "yugabytedb" }
6342
+ });
6343
+ }
6344
+ } catch {}
6345
+ }
5372
6346
  async stop(options = {}) {
5373
6347
  if (this.#stoppingOn || this.#stopped) return;
5374
6348
  let { close = true, graceful = true, timeout = 3e4 } = options;
@@ -5408,6 +6382,9 @@ var PgBoss = class extends EventEmitter {
5408
6382
  insert(name, jobs, options) {
5409
6383
  return this.#manager.insert(name, jobs, options);
5410
6384
  }
6385
+ flow(jobs, options) {
6386
+ return this.#manager.flow(jobs, options);
6387
+ }
5411
6388
  fetch(name, options = {}) {
5412
6389
  return this.#manager.fetch(name, options);
5413
6390
  }
@@ -5474,6 +6451,12 @@ var PgBoss = class extends EventEmitter {
5474
6451
  getBlockedKeys(name) {
5475
6452
  return this.#manager.getBlockedKeys(name);
5476
6453
  }
6454
+ getDependencies(name, id, options) {
6455
+ return this.#manager.getDependencies(name, id, options);
6456
+ }
6457
+ getDependents(name, id, options) {
6458
+ return this.#manager.getDependents(name, id, options);
6459
+ }
5477
6460
  updateQueue(name, options) {
5478
6461
  return this.#manager.updateQueue(name, options);
5479
6462
  }
@@ -9905,8 +10888,8 @@ function WarningTypeBadge({ type }) {
9905
10888
  //#region \0virtual:react-router/server-manifest
9906
10889
  var server_manifest_default = {
9907
10890
  "entry": {
9908
- "module": "/assets/entry.client-6KjasuHH.js",
9909
- "imports": ["/assets/jsx-runtime-DcdjQ3vN.js", "/assets/react-dom-DQHA9Ik4.js"],
10891
+ "module": "/assets/entry.client-DL_oPh96.js",
10892
+ "imports": ["/assets/jsx-runtime-BgbGXvsu.js", "/assets/react-dom-QnGHOQwT.js"],
9910
10893
  "css": []
9911
10894
  },
9912
10895
  "routes": {
@@ -9923,16 +10906,16 @@ var server_manifest_default = {
9923
10906
  "hasClientMiddleware": false,
9924
10907
  "hasDefaultExport": true,
9925
10908
  "hasErrorBoundary": true,
9926
- "module": "/assets/root-Cvarn0sH.js",
10909
+ "module": "/assets/root-Df70GAY3.js",
9927
10910
  "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"
10911
+ "/assets/jsx-runtime-BgbGXvsu.js",
10912
+ "/assets/react-dom-QnGHOQwT.js",
10913
+ "/assets/db-link-BWWnHM0k.js",
10914
+ "/assets/MenuTrigger-BhalG0aG.js",
10915
+ "/assets/createLucideIcon-DVP_i62f.js",
10916
+ "/assets/useOpenInteractionType-D3JsvupP.js"
9934
10917
  ],
9935
- "css": ["/assets/root-D24FtLBP.css"],
10918
+ "css": ["/assets/root-C0MdPLOa.css"],
9936
10919
  "clientActionModule": void 0,
9937
10920
  "clientLoaderModule": void 0,
9938
10921
  "clientMiddlewareModule": void 0,
@@ -9951,15 +10934,15 @@ var server_manifest_default = {
9951
10934
  "hasClientMiddleware": false,
9952
10935
  "hasDefaultExport": true,
9953
10936
  "hasErrorBoundary": true,
9954
- "module": "/assets/_index-CNX0dPQi.js",
10937
+ "module": "/assets/_index-D1-nZ7Th.js",
9955
10938
  "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"
10939
+ "/assets/jsx-runtime-BgbGXvsu.js",
10940
+ "/assets/db-link-BWWnHM0k.js",
10941
+ "/assets/error-card-B0ANyjh3.js",
10942
+ "/assets/badge-DCQvSdiR.js",
10943
+ "/assets/button-BxLcuaPM.js",
10944
+ "/assets/stat-card-DLtQnscf.js",
10945
+ "/assets/table-DqqzSNik.js"
9963
10946
  ],
9964
10947
  "css": [],
9965
10948
  "clientActionModule": void 0,
@@ -9980,24 +10963,24 @@ var server_manifest_default = {
9980
10963
  "hasClientMiddleware": false,
9981
10964
  "hasDefaultExport": true,
9982
10965
  "hasErrorBoundary": true,
9983
- "module": "/assets/jobs-BjoGjnb0.js",
10966
+ "module": "/assets/jobs-D0a6Lwq0.js",
9984
10967
  "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"
10968
+ "/assets/jsx-runtime-BgbGXvsu.js",
10969
+ "/assets/db-link-BWWnHM0k.js",
10970
+ "/assets/error-card-B0ANyjh3.js",
10971
+ "/assets/badge-DCQvSdiR.js",
10972
+ "/assets/button-BxLcuaPM.js",
10973
+ "/assets/filter-select--qLjbs9m.js",
10974
+ "/assets/pagination-Bzx8wbXG.js",
10975
+ "/assets/table-DqqzSNik.js",
10976
+ "/assets/MenuTrigger-BhalG0aG.js",
10977
+ "/assets/useOpenInteractionType-D3JsvupP.js",
10978
+ "/assets/createLucideIcon-DVP_i62f.js",
10979
+ "/assets/check-Ch42cXMT.js",
10980
+ "/assets/chevron-down-Byq-CYG9.js",
10981
+ "/assets/chevron-right-CKAGD7DJ.js",
10982
+ "/assets/x-BPKZwOn9.js",
10983
+ "/assets/react-dom-QnGHOQwT.js"
10001
10984
  ],
10002
10985
  "css": [],
10003
10986
  "clientActionModule": void 0,
@@ -10018,14 +11001,14 @@ var server_manifest_default = {
10018
11001
  "hasClientMiddleware": false,
10019
11002
  "hasDefaultExport": true,
10020
11003
  "hasErrorBoundary": true,
10021
- "module": "/assets/queues._index-BM3eKnSr.js",
11004
+ "module": "/assets/queues._index-D8903DTa.js",
10022
11005
  "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"
11006
+ "/assets/jsx-runtime-BgbGXvsu.js",
11007
+ "/assets/db-link-BWWnHM0k.js",
11008
+ "/assets/badge-DCQvSdiR.js",
11009
+ "/assets/button-BxLcuaPM.js",
11010
+ "/assets/filter-select--qLjbs9m.js",
11011
+ "/assets/table-DqqzSNik.js"
10029
11012
  ],
10030
11013
  "css": [],
10031
11014
  "clientActionModule": void 0,
@@ -10046,14 +11029,14 @@ var server_manifest_default = {
10046
11029
  "hasClientMiddleware": false,
10047
11030
  "hasDefaultExport": true,
10048
11031
  "hasErrorBoundary": true,
10049
- "module": "/assets/queues.create-BVR72i85.js",
11032
+ "module": "/assets/queues.create-CMqQVLup.js",
10050
11033
  "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"
11034
+ "/assets/jsx-runtime-BgbGXvsu.js",
11035
+ "/assets/db-link-BWWnHM0k.js",
11036
+ "/assets/error-card-B0ANyjh3.js",
11037
+ "/assets/button-BxLcuaPM.js",
11038
+ "/assets/chevron-down-Byq-CYG9.js",
11039
+ "/assets/createLucideIcon-DVP_i62f.js"
10057
11040
  ],
10058
11041
  "css": [],
10059
11042
  "clientActionModule": void 0,
@@ -10074,25 +11057,25 @@ var server_manifest_default = {
10074
11057
  "hasClientMiddleware": false,
10075
11058
  "hasDefaultExport": true,
10076
11059
  "hasErrorBoundary": true,
10077
- "module": "/assets/queues._name-DSOGXBen.js",
11060
+ "module": "/assets/queues._name-BVt_4pav.js",
10078
11061
  "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"
11062
+ "/assets/jsx-runtime-BgbGXvsu.js",
11063
+ "/assets/db-link-BWWnHM0k.js",
11064
+ "/assets/error-card-B0ANyjh3.js",
11065
+ "/assets/badge-DCQvSdiR.js",
11066
+ "/assets/button-BxLcuaPM.js",
11067
+ "/assets/dialog-Bik519zD.js",
11068
+ "/assets/filter-select--qLjbs9m.js",
11069
+ "/assets/pagination-Bzx8wbXG.js",
11070
+ "/assets/stat-card-DLtQnscf.js",
11071
+ "/assets/table-DqqzSNik.js",
11072
+ "/assets/MenuTrigger-BhalG0aG.js",
11073
+ "/assets/createLucideIcon-DVP_i62f.js",
11074
+ "/assets/chevron-down-Byq-CYG9.js",
11075
+ "/assets/chevron-right-CKAGD7DJ.js",
11076
+ "/assets/useOpenInteractionType-D3JsvupP.js",
11077
+ "/assets/x-BPKZwOn9.js",
11078
+ "/assets/react-dom-QnGHOQwT.js"
10096
11079
  ],
10097
11080
  "css": [],
10098
11081
  "clientActionModule": void 0,
@@ -10113,19 +11096,19 @@ var server_manifest_default = {
10113
11096
  "hasClientMiddleware": false,
10114
11097
  "hasDefaultExport": true,
10115
11098
  "hasErrorBoundary": true,
10116
- "module": "/assets/queues._name.jobs._jobId-DKGu8NkE.js",
11099
+ "module": "/assets/queues._name.jobs._jobId-BkG9y75k.js",
10117
11100
  "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"
11101
+ "/assets/jsx-runtime-BgbGXvsu.js",
11102
+ "/assets/db-link-BWWnHM0k.js",
11103
+ "/assets/error-card-B0ANyjh3.js",
11104
+ "/assets/badge-DCQvSdiR.js",
11105
+ "/assets/button-BxLcuaPM.js",
11106
+ "/assets/dialog-Bik519zD.js",
11107
+ "/assets/createLucideIcon-DVP_i62f.js",
11108
+ "/assets/check-Ch42cXMT.js",
11109
+ "/assets/useOpenInteractionType-D3JsvupP.js",
11110
+ "/assets/x-BPKZwOn9.js",
11111
+ "/assets/react-dom-QnGHOQwT.js"
10129
11112
  ],
10130
11113
  "css": [],
10131
11114
  "clientActionModule": void 0,
@@ -10146,15 +11129,15 @@ var server_manifest_default = {
10146
11129
  "hasClientMiddleware": false,
10147
11130
  "hasDefaultExport": true,
10148
11131
  "hasErrorBoundary": true,
10149
- "module": "/assets/schedules-CE-hWC9e.js",
11132
+ "module": "/assets/schedules-DPXQoaEE.js",
10150
11133
  "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"
11134
+ "/assets/jsx-runtime-BgbGXvsu.js",
11135
+ "/assets/db-link-BWWnHM0k.js",
11136
+ "/assets/error-card-B0ANyjh3.js",
11137
+ "/assets/badge-DCQvSdiR.js",
11138
+ "/assets/button-BxLcuaPM.js",
11139
+ "/assets/pagination-Bzx8wbXG.js",
11140
+ "/assets/table-DqqzSNik.js"
10158
11141
  ],
10159
11142
  "css": [],
10160
11143
  "clientActionModule": void 0,
@@ -10175,17 +11158,17 @@ var server_manifest_default = {
10175
11158
  "hasClientMiddleware": false,
10176
11159
  "hasDefaultExport": true,
10177
11160
  "hasErrorBoundary": true,
10178
- "module": "/assets/schedules._name._key-DT8Kg4jU.js",
11161
+ "module": "/assets/schedules._name._key-B_luxy1w.js",
10179
11162
  "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"
11163
+ "/assets/jsx-runtime-BgbGXvsu.js",
11164
+ "/assets/db-link-BWWnHM0k.js",
11165
+ "/assets/error-card-B0ANyjh3.js",
11166
+ "/assets/button-BxLcuaPM.js",
11167
+ "/assets/dialog-Bik519zD.js",
11168
+ "/assets/useOpenInteractionType-D3JsvupP.js",
11169
+ "/assets/x-BPKZwOn9.js",
11170
+ "/assets/react-dom-QnGHOQwT.js",
11171
+ "/assets/createLucideIcon-DVP_i62f.js"
10189
11172
  ],
10190
11173
  "css": [],
10191
11174
  "clientActionModule": void 0,
@@ -10206,12 +11189,12 @@ var server_manifest_default = {
10206
11189
  "hasClientMiddleware": false,
10207
11190
  "hasDefaultExport": true,
10208
11191
  "hasErrorBoundary": true,
10209
- "module": "/assets/schedules.new-bKRusXXO.js",
11192
+ "module": "/assets/schedules.new-BQV7GWzs.js",
10210
11193
  "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"
11194
+ "/assets/jsx-runtime-BgbGXvsu.js",
11195
+ "/assets/db-link-BWWnHM0k.js",
11196
+ "/assets/error-card-B0ANyjh3.js",
11197
+ "/assets/button-BxLcuaPM.js"
10215
11198
  ],
10216
11199
  "css": [],
10217
11200
  "clientActionModule": void 0,
@@ -10232,12 +11215,12 @@ var server_manifest_default = {
10232
11215
  "hasClientMiddleware": false,
10233
11216
  "hasDefaultExport": true,
10234
11217
  "hasErrorBoundary": true,
10235
- "module": "/assets/send-BaWw8x7J.js",
11218
+ "module": "/assets/send-DJBsfnx_.js",
10236
11219
  "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"
11220
+ "/assets/jsx-runtime-BgbGXvsu.js",
11221
+ "/assets/db-link-BWWnHM0k.js",
11222
+ "/assets/error-card-B0ANyjh3.js",
11223
+ "/assets/button-BxLcuaPM.js"
10241
11224
  ],
10242
11225
  "css": [],
10243
11226
  "clientActionModule": void 0,
@@ -10258,16 +11241,16 @@ var server_manifest_default = {
10258
11241
  "hasClientMiddleware": false,
10259
11242
  "hasDefaultExport": true,
10260
11243
  "hasErrorBoundary": true,
10261
- "module": "/assets/warnings-lzi34PdC.js",
11244
+ "module": "/assets/warnings-CHKaRfIW.js",
10262
11245
  "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"
11246
+ "/assets/jsx-runtime-BgbGXvsu.js",
11247
+ "/assets/db-link-BWWnHM0k.js",
11248
+ "/assets/error-card-B0ANyjh3.js",
11249
+ "/assets/badge-DCQvSdiR.js",
11250
+ "/assets/button-BxLcuaPM.js",
11251
+ "/assets/filter-select--qLjbs9m.js",
11252
+ "/assets/pagination-Bzx8wbXG.js",
11253
+ "/assets/table-DqqzSNik.js"
10271
11254
  ],
10272
11255
  "css": [],
10273
11256
  "clientActionModule": void 0,
@@ -10276,23 +11259,15 @@ var server_manifest_default = {
10276
11259
  "hydrateFallbackModule": void 0
10277
11260
  }
10278
11261
  },
10279
- "url": "/assets/manifest-e6a94de0.js",
10280
- "version": "e6a94de0",
11262
+ "url": "/assets/manifest-ef81a0f9.js",
11263
+ "version": "ef81a0f9",
10281
11264
  "sri": void 0
10282
11265
  };
10283
11266
  //#endregion
10284
11267
  //#region \0virtual:react-router/server-build
10285
11268
  var assetsBuildDirectory = "build/client";
10286
11269
  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
- };
11270
+ var future = { "unstable_optimizeDeps": false };
10296
11271
  var ssr = true;
10297
11272
  var isSpaMode = false;
10298
11273
  var prerender = [];