@pg-boss/dashboard 1.2.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/build/client/assets/MenuTrigger-BNvpjhsQ.js +1 -0
  2. package/build/client/assets/_index-DqpFaaQw.js +1 -0
  3. package/build/client/assets/{badge-DCQvSdiR.js → badge-CMnQO7Lq.js} +1 -1
  4. package/build/client/assets/{button-BxLcuaPM.js → button-9NpSS9Ow.js} +1 -1
  5. package/build/client/assets/check-7jwc5sb1.js +1 -0
  6. package/build/client/assets/{chevron-down-Byq-CYG9.js → chevron-down-BFFjfYD4.js} +1 -1
  7. package/build/client/assets/chevron-right-DGk5QFJF.js +1 -0
  8. package/build/client/assets/{createLucideIcon-DVP_i62f.js → createLucideIcon-C-LI4enx.js} +1 -1
  9. package/build/client/assets/db-link-BajQ1v8I.js +1 -0
  10. package/build/client/assets/dialog-D-oczDM2.js +1 -0
  11. package/build/client/assets/{entry.client-DL_oPh96.js → entry.client-CqyjuPDB.js} +3 -3
  12. package/build/client/assets/{error-card-B0ANyjh3.js → error-card-BH7i86fH.js} +1 -1
  13. package/build/client/assets/{filter-select--qLjbs9m.js → filter-select-Bn_oSiip.js} +1 -1
  14. package/build/client/assets/jobs-CAd_qqLH.js +1 -0
  15. package/build/client/assets/jsx-runtime-RQyiN6Nr.js +16 -0
  16. package/build/client/assets/manifest-27e8e133.js +1 -0
  17. package/build/client/assets/migrations-D5l0n4Jn.js +1 -0
  18. package/build/client/assets/{pagination-Bzx8wbXG.js → pagination-C-ohiBmY.js} +1 -1
  19. package/build/client/assets/queues._index-8YriSqbQ.js +1 -0
  20. package/build/client/assets/queues._name-Cb17IB2u.js +1 -0
  21. package/build/client/assets/{queues._name.jobs._jobId-BkG9y75k.js → queues._name.jobs._jobId-Bkv8POBj.js} +1 -1
  22. package/build/client/assets/{queues.create-CMqQVLup.js → queues.create-DsY0Sc19.js} +1 -1
  23. package/build/client/assets/{react-dom-QnGHOQwT.js → react-dom-D_m_Zgd3.js} +1 -1
  24. package/build/client/assets/root-B0MB8jZH.css +2 -0
  25. package/build/client/assets/root-qxoeL6W3.js +40 -0
  26. package/build/client/assets/{schedules-DPXQoaEE.js → schedules-iYfIJxOD.js} +1 -1
  27. package/build/client/assets/{schedules._name._key-B_luxy1w.js → schedules._name._key-CJVu73XY.js} +1 -1
  28. package/build/client/assets/{schedules.new-BQV7GWzs.js → schedules.new-Cq0Mxa7G.js} +1 -1
  29. package/build/client/assets/send-8X9ZisG-.js +1 -0
  30. package/build/client/assets/{stat-card-DLtQnscf.js → stat-card-dyg1wY5p.js} +1 -1
  31. package/build/client/assets/{table-DqqzSNik.js → table-Cz7ujmH_.js} +1 -1
  32. package/build/client/assets/useOpenInteractionType-BQ1arb0B.js +1 -0
  33. package/build/client/assets/{warnings-CHKaRfIW.js → warnings-C1R_RzIe.js} +1 -1
  34. package/build/client/assets/x-AhXI_F1j.js +1 -0
  35. package/build/server/index.js +1712 -1061
  36. package/package.json +11 -8
  37. package/build/client/assets/MenuTrigger-BhalG0aG.js +0 -1
  38. package/build/client/assets/_index-D1-nZ7Th.js +0 -1
  39. package/build/client/assets/check-Ch42cXMT.js +0 -1
  40. package/build/client/assets/chevron-right-CKAGD7DJ.js +0 -1
  41. package/build/client/assets/db-link-BWWnHM0k.js +0 -1
  42. package/build/client/assets/dialog-Bik519zD.js +0 -1
  43. package/build/client/assets/jobs-D0a6Lwq0.js +0 -1
  44. package/build/client/assets/jsx-runtime-BgbGXvsu.js +0 -16
  45. package/build/client/assets/manifest-ef81a0f9.js +0 -1
  46. package/build/client/assets/queues._index-D8903DTa.js +0 -1
  47. package/build/client/assets/queues._name-BVt_4pav.js +0 -1
  48. package/build/client/assets/root-C0MdPLOa.css +0 -2
  49. package/build/client/assets/root-Df70GAY3.js +0 -40
  50. package/build/client/assets/send-DJBsfnx_.js +0 -1
  51. package/build/client/assets/useOpenInteractionType-D3JsvupP.js +0 -1
  52. package/build/client/assets/x-BPKZwOn9.js +0 -1
@@ -347,6 +347,38 @@ var WARNING_TYPE_OPTIONS = [{
347
347
  value: type,
348
348
  label: WARNING_TYPE_LABELS[type]
349
349
  }))];
350
+ var BAM_STATUSES = [
351
+ "pending",
352
+ "in_progress",
353
+ "completed",
354
+ "failed"
355
+ ];
356
+ /**
357
+ * Validate a BAM status filter value
358
+ */
359
+ function isValidBamStatus(value) {
360
+ if (value === null) return true;
361
+ return BAM_STATUSES.includes(value);
362
+ }
363
+ var BAM_STATUS_VARIANTS = {
364
+ pending: "gray",
365
+ in_progress: "primary",
366
+ completed: "success",
367
+ failed: "error"
368
+ };
369
+ var BAM_STATUS_LABELS = {
370
+ pending: "Pending",
371
+ in_progress: "In Progress",
372
+ completed: "Completed",
373
+ failed: "Failed"
374
+ };
375
+ var BAM_STATUS_OPTIONS = [{
376
+ value: null,
377
+ label: "All Statuses"
378
+ }, ...BAM_STATUSES.map((status) => ({
379
+ value: status,
380
+ label: BAM_STATUS_LABELS[status]
381
+ }))];
350
382
  /**
351
383
  * Format warning data for display
352
384
  */
@@ -840,6 +872,11 @@ var navigation = [
840
872
  href: "/schedules",
841
873
  icon: SchedulesIcon
842
874
  },
875
+ {
876
+ name: "Migrations",
877
+ href: "/migrations",
878
+ icon: MigrationsIcon
879
+ },
843
880
  {
844
881
  name: "Warnings",
845
882
  href: "/warnings",
@@ -916,6 +953,20 @@ function WarningIcon$1({ className }) {
916
953
  })
917
954
  });
918
955
  }
956
+ function MigrationsIcon({ className }) {
957
+ return /* @__PURE__ */ jsx("svg", {
958
+ className,
959
+ fill: "none",
960
+ viewBox: "0 0 24 24",
961
+ strokeWidth: 1.5,
962
+ stroke: "currentColor",
963
+ children: /* @__PURE__ */ jsx("path", {
964
+ strokeLinecap: "round",
965
+ strokeLinejoin: "round",
966
+ d: "M7.5 21 3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5"
967
+ })
968
+ });
969
+ }
919
970
  function DatabaseIcon({ className }) {
920
971
  return /* @__PURE__ */ jsx("svg", {
921
972
  className,
@@ -1208,11 +1259,11 @@ var dbContext = globalStore[TOKEN_KEY] ??= createContext();
1208
1259
  //#endregion
1209
1260
  //#region app/root.tsx
1210
1261
  var root_exports = /* @__PURE__ */ __exportAll({
1211
- ErrorBoundary: () => ErrorBoundary$11,
1262
+ ErrorBoundary: () => ErrorBoundary$12,
1212
1263
  Layout: () => Layout,
1213
1264
  default: () => root_default,
1214
1265
  links: () => links,
1215
- loader: () => loader$11,
1266
+ loader: () => loader$12,
1216
1267
  meta: () => meta
1217
1268
  });
1218
1269
  function MainContent({ children }) {
@@ -1288,7 +1339,7 @@ var themeScript = `
1288
1339
  link.href = 'data:image/svg+xml,' + encodeURIComponent(svg);
1289
1340
  })();
1290
1341
  `;
1291
- async function loader$11({ context }) {
1342
+ async function loader$12({ context }) {
1292
1343
  const { databases, currentDb } = context.get(dbContext);
1293
1344
  return {
1294
1345
  databases,
@@ -1321,7 +1372,7 @@ function Layout({ children }) {
1321
1372
  var root_default = UNSAFE_withComponentProps(function App() {
1322
1373
  return /* @__PURE__ */ jsx(Outlet, {});
1323
1374
  });
1324
- var ErrorBoundary$11 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary({ error }) {
1375
+ var ErrorBoundary$12 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary({ error }) {
1325
1376
  let message = "Oops!";
1326
1377
  let details = "An unexpected error occurred.";
1327
1378
  let stack;
@@ -1538,10 +1589,13 @@ function createTableQueue(schema) {
1538
1589
  table_name text NOT NULL,
1539
1590
  deferred_count int NOT NULL default 0,
1540
1591
  queued_count int NOT NULL default 0,
1592
+ ready_count int NOT NULL default 0,
1541
1593
  warning_queued int NOT NULL default 0,
1542
1594
  active_count int NOT NULL default 0,
1595
+ failed_count int NOT NULL default 0,
1543
1596
  total_count int NOT NULL default 0,
1544
1597
  heartbeat_seconds int,
1598
+ notify bool NOT NULL DEFAULT false,
1545
1599
  singletons_active text[],
1546
1600
  monitor_on timestamp with time zone,
1547
1601
  maintain_on timestamp with time zone,
@@ -1878,7 +1932,8 @@ function createQueueFunction(schema, noPartitioning = false) {
1878
1932
  dead_letter,
1879
1933
  partition,
1880
1934
  table_name,
1881
- heartbeat_seconds
1935
+ heartbeat_seconds,
1936
+ notify
1882
1937
  )
1883
1938
  VALUES (
1884
1939
  queue_name,
@@ -1894,7 +1949,8 @@ function createQueueFunction(schema, noPartitioning = false) {
1894
1949
  options->>'deadLetter',
1895
1950
  COALESCE((options->>'partition')::bool, ${QUEUE_DEFAULTS.partition}),
1896
1951
  tablename,
1897
- (options->>'heartbeatSeconds')::int
1952
+ (options->>'heartbeatSeconds')::int,
1953
+ COALESCE((options->>'notify')::bool, false)
1898
1954
  )
1899
1955
  ON CONFLICT DO NOTHING
1900
1956
  RETURNING created_on
@@ -1966,6 +2022,12 @@ function deleteQueueFunction(schema, noPartitioning = false) {
1966
2022
  function createQueue$1(schema, name, options, noAdvisoryLocks) {
1967
2023
  return locked(schema, `SELECT ${schema}.create_queue('${name}', '${JSON.stringify(options)}'::jsonb)`, "create-queue", noAdvisoryLocks);
1968
2024
  }
2025
+ function notifyChannelSql(schema) {
2026
+ return `('pgboss_' || left(encode(sha224('${schema}'::bytea), 'hex'), 24))`;
2027
+ }
2028
+ function notifyQueue(schema, name) {
2029
+ return `SELECT pg_notify(${notifyChannelSql(schema)}, '${name}')`;
2030
+ }
1969
2031
  function deleteQueue(schema, name, noAdvisoryLocks) {
1970
2032
  return locked(schema, `SELECT ${schema}.delete_queue('${name}')`, "delete-queue", noAdvisoryLocks);
1971
2033
  }
@@ -2054,6 +2116,7 @@ function updateQueue(schema, { deadLetter } = {}) {
2054
2116
  heartbeat_seconds = CASE WHEN o.data ? 'heartbeatSeconds'
2055
2117
  THEN (o.data->>'heartbeatSeconds')::int
2056
2118
  ELSE heartbeat_seconds END,
2119
+ notify = COALESCE((o.data->>'notify')::bool, notify),
2057
2120
  ${deadLetter === void 0 ? "" : `dead_letter = CASE WHEN '${deadLetter}' IS DISTINCT FROM dead_letter THEN '${deadLetter}' ELSE dead_letter END,`}
2058
2121
  updated_on = now()
2059
2122
  FROM options o
@@ -2076,11 +2139,14 @@ function getQueues$1(schema, names) {
2076
2139
  q.deletion_seconds as "deleteAfterSeconds",
2077
2140
  q.partition,
2078
2141
  q.heartbeat_seconds as "heartbeatSeconds",
2142
+ q.notify,
2079
2143
  q.dead_letter as "deadLetter",
2080
2144
  q.deferred_count as "deferredCount",
2081
2145
  q.warning_queued as "warningQueueSize",
2082
2146
  q.queued_count as "queuedCount",
2147
+ q.ready_count as "readyCount",
2083
2148
  q.active_count as "activeCount",
2149
+ q.failed_count as "failedCount",
2084
2150
  q.total_count as "totalCount",
2085
2151
  q.singletons_active as "singletonsActive",
2086
2152
  q.table_name as "table",
@@ -2404,6 +2470,54 @@ function completeJobs(schema, table, includeQueued) {
2404
2470
  SELECT COUNT(*) FROM completed
2405
2471
  `;
2406
2472
  }
2473
+ function completeJobsWithOutputs(schema, table) {
2474
+ return `
2475
+ WITH input AS (
2476
+ SELECT * FROM json_to_recordset($2::json) AS x (id uuid, output jsonb)
2477
+ ),
2478
+ completed AS (
2479
+ UPDATE ${schema}.${table} j
2480
+ SET completed_on = now(),
2481
+ state = '${JOB_STATES.completed}',
2482
+ output = i.output
2483
+ FROM input i
2484
+ WHERE j.name = $1
2485
+ AND j.id = i.id
2486
+ AND j.state = '${JOB_STATES.active}'
2487
+ RETURNING j.name, j.id, j.blocking
2488
+ ),
2489
+ decremented AS (
2490
+ SELECT d.child_name, d.child_id, COUNT(*)::int AS n
2491
+ FROM ${schema}.job_dependency d
2492
+ JOIN completed c ON c.blocking
2493
+ AND d.parent_name = c.name
2494
+ AND d.parent_id = c.id
2495
+ GROUP BY d.child_name, d.child_id
2496
+ ),
2497
+ ${lockedChildrenCte(schema)},
2498
+ unblocked AS (
2499
+ ${unblockChildrenUpdate(schema)}
2500
+ RETURNING 1
2501
+ )
2502
+ SELECT COUNT(*) FROM completed
2503
+ `;
2504
+ }
2505
+ function completeJobsWithOutputsDistributed(schema, table) {
2506
+ return `
2507
+ WITH input AS (
2508
+ SELECT * FROM json_to_recordset($2::json) AS x (id uuid, output jsonb)
2509
+ )
2510
+ UPDATE ${schema}.${table} j
2511
+ SET completed_on = now(),
2512
+ state = '${JOB_STATES.completed}',
2513
+ output = i.output
2514
+ FROM input i
2515
+ WHERE j.name = $1
2516
+ AND j.id = i.id
2517
+ AND j.state = '${JOB_STATES.active}'
2518
+ RETURNING j.id, j.blocking
2519
+ `;
2520
+ }
2407
2521
  function cancelJobs(schema, table) {
2408
2522
  return `
2409
2523
  WITH results as (
@@ -2442,8 +2556,8 @@ function restoreJobs(schema, table) {
2442
2556
  AND id IN (SELECT UNNEST($2::uuid[]))
2443
2557
  `;
2444
2558
  }
2445
- function insertJobs(schema, { table, name, returnId = true }) {
2446
- return `
2559
+ function insertJobs(schema, { table, name, returnId = true, notify = false }) {
2560
+ const insert = `
2447
2561
  INSERT INTO ${schema}.${table} (
2448
2562
  id,
2449
2563
  name,
@@ -2526,7 +2640,31 @@ function insertJobs(schema, { table, name, returnId = true }) {
2526
2640
  ) j
2527
2641
  JOIN ${schema}.queue q ON q.name = '${name}'
2528
2642
  ON CONFLICT DO NOTHING
2529
- ${returnId ? "RETURNING id" : ""}
2643
+ ${notify ? "RETURNING id, start_after" : returnId ? "RETURNING id" : ""}
2644
+ `;
2645
+ if (!notify) return insert;
2646
+ const comparator = returnId ? ">= 0" : "< 0";
2647
+ return `
2648
+ WITH ins AS (
2649
+ ${insert}
2650
+ ),
2651
+ notified AS (
2652
+ SELECT pg_notify(${notifyChannelSql(schema)}, '${name}')
2653
+ FROM ins WHERE start_after <= now() LIMIT 1
2654
+ )
2655
+ SELECT id FROM ins WHERE (SELECT count(*) FROM notified) ${comparator}
2656
+ `;
2657
+ }
2658
+ function insertFlowJobs(schema, { table, name }, jobs) {
2659
+ return `
2660
+ WITH ins AS (
2661
+ ${insertJobs(schema, {
2662
+ table,
2663
+ name,
2664
+ returnId: true
2665
+ }).replace("$1", () => serializeJsonParam(jobs))}
2666
+ )
2667
+ SELECT 1 / (CASE WHEN (SELECT count(*) FROM ins) = ${jobs.length} THEN 1 ELSE 0 END)
2530
2668
  `;
2531
2669
  }
2532
2670
  function failJobsById(schema, table) {
@@ -2558,7 +2696,12 @@ function touchJobs(schema, table) {
2558
2696
  }
2559
2697
  function failJobs(schema, table, where, output) {
2560
2698
  return `
2561
- WITH deleted_jobs AS (
2699
+ WITH ${failJobsBody(schema, table, where, output)}
2700
+ SELECT COUNT(*) FROM results
2701
+ `;
2702
+ }
2703
+ function failJobsBody(schema, table, where, output, forceTerminal = false) {
2704
+ return `deleted_jobs AS (
2562
2705
  DELETE FROM ${schema}.${table}
2563
2706
  WHERE ${where}
2564
2707
  RETURNING *
@@ -2600,10 +2743,10 @@ function failJobs(schema, table, where, output) {
2600
2743
  name,
2601
2744
  priority,
2602
2745
  data,
2603
- CASE
2746
+ ${forceTerminal ? `'${JOB_STATES.failed}'::${schema}.job_state` : `CASE
2604
2747
  WHEN retry_count < retry_limit THEN '${JOB_STATES.retry}'::${schema}.job_state
2605
2748
  ELSE '${JOB_STATES.failed}'::${schema}.job_state
2606
- END as state,
2749
+ END`} as state,
2607
2750
  retry_limit,
2608
2751
  retry_count,
2609
2752
  retry_delay,
@@ -2627,7 +2770,7 @@ function failJobs(schema, table, where, output) {
2627
2770
  expire_seconds,
2628
2771
  deletion_seconds,
2629
2772
  created_on,
2630
- CASE WHEN retry_count < retry_limit THEN NULL ELSE now() END as completed_on,
2773
+ ${forceTerminal ? "now()" : "CASE WHEN retry_count < retry_limit THEN NULL ELSE now() END"} as completed_on,
2631
2774
  keep_until,
2632
2775
  policy,
2633
2776
  ${output},
@@ -2726,7 +2869,23 @@ function failJobs(schema, table, where, output) {
2726
2869
  FROM results r
2727
2870
  JOIN ${schema}.queue q ON q.name = r.dead_letter
2728
2871
  WHERE state = '${JOB_STATES.failed}'
2729
- )
2872
+ )`;
2873
+ }
2874
+ function failJobsByIdWithOutputs(schema, table) {
2875
+ return `
2876
+ WITH output_map AS (
2877
+ SELECT * FROM json_to_recordset($2::json) AS x (id uuid, output jsonb)
2878
+ ),
2879
+ ${failJobsBody(schema, table, `name = $1 AND id IN (SELECT id FROM output_map) AND state < '${JOB_STATES.completed}'`, "(SELECT om.output FROM output_map om WHERE om.id = deleted_jobs.id)")}
2880
+ SELECT COUNT(*) FROM results
2881
+ `;
2882
+ }
2883
+ function deadLetterJobsByIdWithOutputs(schema, table) {
2884
+ return `
2885
+ WITH output_map AS (
2886
+ SELECT * FROM json_to_recordset($2::json) AS x (id uuid, output jsonb)
2887
+ ),
2888
+ ${failJobsBody(schema, table, `name = $1 AND id IN (SELECT id FROM output_map) AND state < '${JOB_STATES.completed}'`, "(SELECT om.output FROM output_map om WHERE om.id = deleted_jobs.id)", true)}
2730
2889
  SELECT COUNT(*) FROM results
2731
2890
  `;
2732
2891
  }
@@ -2839,14 +2998,26 @@ function getQueueStats$1(schema, table, queues) {
2839
2998
  text: `
2840
2999
  SELECT
2841
3000
  name,
2842
- (count(*) FILTER (WHERE start_after > now()))::int as "deferredCount",
2843
- (count(*) FILTER (WHERE state < '${JOB_STATES.active}'))::int as "queuedCount",
2844
- (count(*) FILTER (WHERE state = '${JOB_STATES.active}'))::int as "activeCount",
2845
- count(*)::int as "totalCount",
2846
- array_agg(singleton_key) FILTER (WHERE policy IN ('${QUEUE_POLICIES.singleton}','${QUEUE_POLICIES.stately}') AND state = '${JOB_STATES.active}') as "singletonsActive"
2847
- FROM ${schema}.${table}
2848
- WHERE name = ANY($1::text[])
2849
- GROUP BY 1
3001
+ "deferredCount",
3002
+ "queuedCount",
3003
+ GREATEST("queuedCount" - "deferredCount", 0) as "readyCount",
3004
+ "activeCount",
3005
+ "failedCount",
3006
+ "totalCount",
3007
+ "singletonsActive"
3008
+ FROM (
3009
+ SELECT
3010
+ name,
3011
+ (count(*) FILTER (WHERE start_after > now()))::int as "deferredCount",
3012
+ (count(*) FILTER (WHERE state < '${JOB_STATES.active}'))::int as "queuedCount",
3013
+ (count(*) FILTER (WHERE state = '${JOB_STATES.active}'))::int as "activeCount",
3014
+ (count(*) FILTER (WHERE state = '${JOB_STATES.failed}'))::int as "failedCount",
3015
+ count(*)::int as "totalCount",
3016
+ array_agg(singleton_key) FILTER (WHERE policy IN ('${QUEUE_POLICIES.singleton}','${QUEUE_POLICIES.stately}') AND state = '${JOB_STATES.active}') as "singletonsActive"
3017
+ FROM ${schema}.${table}
3018
+ WHERE name = ANY($1::text[])
3019
+ GROUP BY 1
3020
+ ) stats
2850
3021
  `,
2851
3022
  values: [queues]
2852
3023
  };
@@ -2857,7 +3028,9 @@ function cacheQueueStats(schema, table, queues, noAdvisoryLocks) {
2857
3028
  UPDATE ${schema}.queue SET
2858
3029
  deferred_count = COALESCE(stats."deferredCount", 0),
2859
3030
  queued_count = COALESCE(stats."queuedCount", 0),
3031
+ ready_count = COALESCE(stats."readyCount", 0),
2860
3032
  active_count = COALESCE(stats."activeCount", 0),
3033
+ failed_count = COALESCE(stats."failedCount", 0),
2861
3034
  total_count = COALESCE(stats."totalCount", 0),
2862
3035
  singletons_active = stats."singletonsActive"
2863
3036
  FROM (
@@ -2931,8 +3104,8 @@ function getJobById$1(schema, table) {
2931
3104
  AND id = $2
2932
3105
  `;
2933
3106
  }
2934
- function insertDependencies(schema) {
2935
- return `
3107
+ function insertDependencies(schema, deps) {
3108
+ const sql = `
2936
3109
  INSERT INTO ${schema}.job_dependency (child_name, child_id, parent_name, parent_id)
2937
3110
  SELECT child_name, child_id, parent_name, parent_id
2938
3111
  FROM json_to_recordset($1::json) AS x (
@@ -2943,6 +3116,7 @@ function insertDependencies(schema) {
2943
3116
  )
2944
3117
  ON CONFLICT DO NOTHING
2945
3118
  `;
3119
+ return deps ? sql.replace("$1", () => serializeJsonParam(deps)) : sql;
2946
3120
  }
2947
3121
  function getDependencies(schema) {
2948
3122
  return `
@@ -3018,7 +3192,7 @@ function getBamStatus(schema) {
3018
3192
  GROUP BY status
3019
3193
  `;
3020
3194
  }
3021
- function getBamEntries(schema) {
3195
+ function getBamEntries$1(schema) {
3022
3196
  return `
3023
3197
  SELECT id, name, version, status, queue, table_name as "table", command, error,
3024
3198
  created_on as "createdOn", started_on as "startedOn", completed_on as "completedOn"
@@ -3039,7 +3213,8 @@ var COMPATIBILITY_FLAGS = [
3039
3213
  "noTablePartitioning",
3040
3214
  "noDeferrableConstraints",
3041
3215
  "noAdvisoryLocks",
3042
- "noCoveringIndexes"
3216
+ "noCoveringIndexes",
3217
+ "noListenNotify"
3043
3218
  ];
3044
3219
  var BACKEND_PROFILES = {
3045
3220
  postgres: {
@@ -3054,7 +3229,8 @@ var BACKEND_PROFILES = {
3054
3229
  noTablePartitioning: true,
3055
3230
  noDeferrableConstraints: true,
3056
3231
  noAdvisoryLocks: true,
3057
- noCoveringIndexes: true
3232
+ noCoveringIndexes: true,
3233
+ noListenNotify: true
3058
3234
  }
3059
3235
  },
3060
3236
  yugabytedb: {
@@ -3078,6 +3254,7 @@ function assertObjectName(value, name = "Name") {
3078
3254
  }
3079
3255
  function validateQueueArgs(config = {}) {
3080
3256
  assert(!("deadLetter" in config) || config.deadLetter === null || typeof config.deadLetter === "string", "deadLetter must be a string");
3257
+ assert(!("notify" in config) || typeof config.notify === "boolean", "notify must be a boolean");
3081
3258
  if (config.deadLetter) assertObjectName(config.deadLetter, "deadLetter");
3082
3259
  validateRetryConfig(config);
3083
3260
  validateExpirationConfig(config);
@@ -3257,6 +3434,7 @@ function checkWorkArgs(name, args) {
3257
3434
  assert(!("includeMetadata" in options) || typeof options.includeMetadata === "boolean", "includeMetadata must be a boolean");
3258
3435
  assert(!("priority" in options) || typeof options.priority === "boolean", "priority must be a boolean");
3259
3436
  assert(!("localConcurrency" in options) || Number.isInteger(options.localConcurrency) && options.localConcurrency >= 1, "localConcurrency must be an integer >= 1");
3437
+ assert(!("perJobResults" in options) || typeof options.perJobResults === "boolean", "perJobResults must be a boolean");
3260
3438
  validatePriorityRangeConfig(options);
3261
3439
  validateGroupConcurrencyConfig(options);
3262
3440
  validateHeartbeatRefreshConfig(options);
@@ -3280,6 +3458,7 @@ function getConfig(value) {
3280
3458
  config.supervise = "supervise" in config ? config.supervise : true;
3281
3459
  config.migrate = "migrate" in config ? config.migrate : true;
3282
3460
  config.createSchema = "createSchema" in config ? config.createSchema : true;
3461
+ config.useListenNotify = "useListenNotify" in config ? config.useListenNotify : false;
3283
3462
  resolveBackend(config);
3284
3463
  applySchemaConfig(config);
3285
3464
  applyOpsConfig(config);
@@ -3349,6 +3528,13 @@ function validateHeartbeatRefreshConfig(config) {
3349
3528
  function applyPollingInterval(config) {
3350
3529
  assert(!("pollingIntervalSeconds" in config) || config.pollingIntervalSeconds >= POLICY.MIN_POLLING_INTERVAL_MS / 1e3, `configuration assert: pollingIntervalSeconds must be at least every ${POLICY.MIN_POLLING_INTERVAL_MS}ms`);
3351
3530
  config.pollingInterval = "pollingIntervalSeconds" in config ? config.pollingIntervalSeconds * 1e3 : 2e3;
3531
+ assert(!("notifyPollingIntervalSeconds" in config) || config.notifyPollingIntervalSeconds >= POLICY.MIN_POLLING_INTERVAL_MS / 1e3, `configuration assert: notifyPollingIntervalSeconds must be at least every ${POLICY.MIN_POLLING_INTERVAL_MS}ms`);
3532
+ if ("notifyPollingIntervalSeconds" in config) {
3533
+ config.notifyPollingInterval = config.notifyPollingIntervalSeconds * 1e3;
3534
+ assert(config.notifyPollingInterval >= config.pollingInterval, "configuration assert: notifyPollingIntervalSeconds must be at least pollingIntervalSeconds");
3535
+ } else config.notifyPollingInterval = Math.max(3e4, config.pollingInterval);
3536
+ assert(!("burstWhenReadyExceeds" in config) || Number.isInteger(config.burstWhenReadyExceeds) && config.burstWhenReadyExceeds >= 1, "configuration assert: burstWhenReadyExceeds must be an integer >= 1");
3537
+ assert(!("burstWhenBatchFull" in config) || typeof config.burstWhenBatchFull === "boolean", "configuration assert: burstWhenBatchFull must be a boolean");
3352
3538
  }
3353
3539
  function applyOpsConfig(config) {
3354
3540
  assert(!("superviseIntervalSeconds" in config) || config.superviseIntervalSeconds >= 1, "configuration assert: superviseIntervalSeconds must be at least every second");
@@ -3382,6 +3568,21 @@ function applyBamConfig(config) {
3382
3568
  }
3383
3569
  //#endregion
3384
3570
  //#region ../../src/migrationStore.ts
3571
+ function formatJobTable(command, table) {
3572
+ return command.replaceAll(".job", `.${table}`).replaceAll("job_i", `${table}_i`);
3573
+ }
3574
+ function inlineAsyncCommand(schema, asyncCommand, version, partitionTables) {
3575
+ const nameMatch = asyncCommand.match(/job_table_run_async\(\s*'([^']+)'/);
3576
+ const bodyMatch = asyncCommand.match(/\$\$([\s\S]*?)\$\$/);
3577
+ const tableMatch = asyncCommand.match(/\$\$\s*,\s*'([^']+)'/);
3578
+ assert(nameMatch && bodyMatch, `Unable to inline async migration command: ${asyncCommand}`);
3579
+ const commandName = nameMatch[1];
3580
+ const body = bodyMatch[1].trim();
3581
+ return (tableMatch ? [tableMatch[1]] : ["job_common", ...partitionTables]).map((table) => {
3582
+ const ddl = formatJobTable(body, table).replace(/(CREATE (?:UNIQUE )?INDEX CONCURRENTLY) /, "$1 IF NOT EXISTS ");
3583
+ return `${`-- inlined from ${schema}.job_table_run_async (migration v${version}, command: ${commandName})`}\n${ddl}`;
3584
+ });
3585
+ }
3385
3586
  function flatten(schema, commands, version, noAdvisoryLocks) {
3386
3587
  commands.unshift(assertMigration(schema, version));
3387
3588
  commands.push(setVersion(schema, version));
@@ -3399,11 +3600,13 @@ function next(schema, version, migrations, noAdvisoryLocks) {
3399
3600
  assert(result, `Version ${version} not found.`);
3400
3601
  return flatten(schema, result.install, result.version, noAdvisoryLocks);
3401
3602
  }
3402
- function migrate(schema, version, migrations, noAdvisoryLocks) {
3603
+ function migrateCommands(schema, version, migrations, noAdvisoryLocks, options = {}) {
3403
3604
  migrations = migrations || getAll(schema);
3605
+ const concurrent = [];
3404
3606
  const result = migrations.filter((i) => i.previous >= version).sort((a, b) => a.version - b.version).reduce((acc, migration) => {
3405
3607
  acc.install = acc.install.concat(migration.install);
3406
- if (migration.async) {
3608
+ if (migration.async) if (options.inlineAsync) for (const cmd of migration.async) concurrent.push(...inlineAsyncCommand(schema, cmd, migration.version, options.partitionTables || []));
3609
+ else {
3407
3610
  const bamCommands = migration.async.map((cmd) => cmd.replace(/\$VERSION\$/g, String(migration.version)));
3408
3611
  acc.install = acc.install.concat(bamCommands);
3409
3612
  }
@@ -3414,156 +3617,576 @@ function migrate(schema, version, migrations, noAdvisoryLocks) {
3414
3617
  version
3415
3618
  });
3416
3619
  assert(result.install.length > 0, `Version ${version} not found.`);
3417
- return flatten(schema, result.install, result.version, noAdvisoryLocks);
3620
+ return {
3621
+ sql: flatten(schema, result.install, result.version, noAdvisoryLocks),
3622
+ concurrent
3623
+ };
3418
3624
  }
3419
- function getAll(schema) {
3420
- return [
3421
- {
3422
- release: "11.1.0",
3423
- version: 26,
3424
- previous: 25,
3425
- install: [`
3426
- CREATE OR REPLACE FUNCTION ${schema}.create_queue(queue_name text, options jsonb)
3427
- RETURNS VOID AS
3428
- $$
3429
- DECLARE
3430
- tablename varchar := CASE WHEN options->>'partition' = 'true'
3431
- THEN 'j' || encode(sha224(queue_name::bytea), 'hex')
3432
- ELSE 'job_common'
3433
- END;
3434
- queue_created_on timestamptz;
3435
- BEGIN
3625
+ function migrate(schema, version, migrations, noAdvisoryLocks, options = {}) {
3626
+ const { sql, concurrent } = migrateCommands(schema, version, migrations, noAdvisoryLocks, options);
3627
+ return concurrent.length ? `${sql}\n${concurrent.join(";\n")};` : sql;
3628
+ }
3629
+ var createQueueFn = {
3630
+ 26: (schema) => `
3631
+ CREATE OR REPLACE FUNCTION ${schema}.create_queue(queue_name text, options jsonb)
3632
+ RETURNS VOID AS
3633
+ $$
3634
+ DECLARE
3635
+ tablename varchar := CASE WHEN options->>'partition' = 'true'
3636
+ THEN 'j' || encode(sha224(queue_name::bytea), 'hex')
3637
+ ELSE 'job_common'
3638
+ END;
3639
+ queue_created_on timestamptz;
3640
+ BEGIN
3436
3641
 
3437
- WITH q as (
3438
- INSERT INTO ${schema}.queue (
3439
- name,
3440
- policy,
3441
- retry_limit,
3442
- retry_delay,
3443
- retry_backoff,
3444
- retry_delay_max,
3445
- expire_seconds,
3446
- retention_seconds,
3447
- deletion_seconds,
3448
- warning_queued,
3449
- dead_letter,
3450
- partition,
3451
- table_name
3452
- )
3453
- VALUES (
3454
- queue_name,
3455
- options->>'policy',
3456
- COALESCE((options->>'retryLimit')::int, 2),
3457
- COALESCE((options->>'retryDelay')::int, 0),
3458
- COALESCE((options->>'retryBackoff')::bool, false),
3459
- (options->>'retryDelayMax')::int,
3460
- COALESCE((options->>'expireInSeconds')::int, 900),
3461
- COALESCE((options->>'retentionSeconds')::int, 1209600),
3462
- COALESCE((options->>'deleteAfterSeconds')::int, 604800),
3463
- COALESCE((options->>'warningQueueSize')::int, 0),
3464
- options->>'deadLetter',
3465
- COALESCE((options->>'partition')::bool, false),
3466
- tablename
3467
- )
3468
- ON CONFLICT DO NOTHING
3469
- RETURNING created_on
3470
- )
3471
- SELECT created_on into queue_created_on from q;
3642
+ WITH q as (
3643
+ INSERT INTO ${schema}.queue (
3644
+ name,
3645
+ policy,
3646
+ retry_limit,
3647
+ retry_delay,
3648
+ retry_backoff,
3649
+ retry_delay_max,
3650
+ expire_seconds,
3651
+ retention_seconds,
3652
+ deletion_seconds,
3653
+ warning_queued,
3654
+ dead_letter,
3655
+ partition,
3656
+ table_name
3657
+ )
3658
+ VALUES (
3659
+ queue_name,
3660
+ options->>'policy',
3661
+ COALESCE((options->>'retryLimit')::int, 2),
3662
+ COALESCE((options->>'retryDelay')::int, 0),
3663
+ COALESCE((options->>'retryBackoff')::bool, false),
3664
+ (options->>'retryDelayMax')::int,
3665
+ COALESCE((options->>'expireInSeconds')::int, 900),
3666
+ COALESCE((options->>'retentionSeconds')::int, 1209600),
3667
+ COALESCE((options->>'deleteAfterSeconds')::int, 604800),
3668
+ COALESCE((options->>'warningQueueSize')::int, 0),
3669
+ options->>'deadLetter',
3670
+ COALESCE((options->>'partition')::bool, false),
3671
+ tablename
3672
+ )
3673
+ ON CONFLICT DO NOTHING
3674
+ RETURNING created_on
3675
+ )
3676
+ SELECT created_on into queue_created_on from q;
3472
3677
 
3473
- IF queue_created_on IS NULL OR options->>'partition' IS DISTINCT FROM 'true' THEN
3474
- RETURN;
3475
- END IF;
3678
+ IF queue_created_on IS NULL OR options->>'partition' IS DISTINCT FROM 'true' THEN
3679
+ RETURN;
3680
+ END IF;
3476
3681
 
3477
- EXECUTE format('CREATE TABLE ${schema}.%I (LIKE ${schema}.job INCLUDING DEFAULTS)', tablename);
3682
+ EXECUTE format('CREATE TABLE ${schema}.%I (LIKE ${schema}.job INCLUDING DEFAULTS)', tablename);
3478
3683
 
3479
- EXECUTE format('ALTER TABLE ${schema}.%1$I ADD PRIMARY KEY (name, id)', tablename);
3480
- EXECUTE format('ALTER TABLE ${schema}.%1$I ADD CONSTRAINT q_fkey FOREIGN KEY (name) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED', tablename);
3481
- EXECUTE format('ALTER TABLE ${schema}.%1$I ADD CONSTRAINT dlq_fkey FOREIGN KEY (dead_letter) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED', tablename);
3684
+ EXECUTE format('ALTER TABLE ${schema}.%1$I ADD PRIMARY KEY (name, id)', tablename);
3685
+ EXECUTE format('ALTER TABLE ${schema}.%1$I ADD CONSTRAINT q_fkey FOREIGN KEY (name) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED', tablename);
3686
+ EXECUTE format('ALTER TABLE ${schema}.%1$I ADD CONSTRAINT dlq_fkey FOREIGN KEY (dead_letter) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED', tablename);
3482
3687
 
3483
- EXECUTE format('CREATE INDEX %1$s_i5 ON ${schema}.%1$I (name, start_after) INCLUDE (priority, created_on, id) WHERE state < ''active''', tablename);
3484
- EXECUTE format('CREATE UNIQUE INDEX %1$s_i4 ON ${schema}.%1$I (name, singleton_on, COALESCE(singleton_key, '''')) WHERE state <> ''cancelled'' AND singleton_on IS NOT NULL', tablename);
3688
+ EXECUTE format('CREATE INDEX %1$s_i5 ON ${schema}.%1$I (name, start_after) INCLUDE (priority, created_on, id) WHERE state < ''active''', tablename);
3689
+ EXECUTE format('CREATE UNIQUE INDEX %1$s_i4 ON ${schema}.%1$I (name, singleton_on, COALESCE(singleton_key, '''')) WHERE state <> ''cancelled'' AND singleton_on IS NOT NULL', tablename);
3485
3690
 
3486
- IF options->>'policy' = 'short' THEN
3487
- EXECUTE format('CREATE UNIQUE INDEX %1$s_i1 ON ${schema}.%1$I (name, COALESCE(singleton_key, '''')) WHERE state = ''created'' AND policy = ''short''', tablename);
3488
- ELSIF options->>'policy' = 'singleton' THEN
3489
- EXECUTE format('CREATE UNIQUE INDEX %1$s_i2 ON ${schema}.%1$I (name, COALESCE(singleton_key, '''')) WHERE state = ''active'' AND policy = ''singleton''', tablename);
3490
- ELSIF options->>'policy' = 'stately' THEN
3491
- EXECUTE format('CREATE UNIQUE INDEX %1$s_i3 ON ${schema}.%1$I (name, state, COALESCE(singleton_key, '''')) WHERE state <= ''active'' AND policy = ''stately''', tablename);
3492
- ELSIF options->>'policy' = 'exclusive' THEN
3493
- EXECUTE format('CREATE UNIQUE INDEX %1$s_i6 ON ${schema}.%1$I (name, COALESCE(singleton_key, '''')) WHERE state <= ''active'' AND policy = ''exclusive''', tablename);
3494
- END IF;
3691
+ IF options->>'policy' = 'short' THEN
3692
+ EXECUTE format('CREATE UNIQUE INDEX %1$s_i1 ON ${schema}.%1$I (name, COALESCE(singleton_key, '''')) WHERE state = ''created'' AND policy = ''short''', tablename);
3693
+ ELSIF options->>'policy' = 'singleton' THEN
3694
+ EXECUTE format('CREATE UNIQUE INDEX %1$s_i2 ON ${schema}.%1$I (name, COALESCE(singleton_key, '''')) WHERE state = ''active'' AND policy = ''singleton''', tablename);
3695
+ ELSIF options->>'policy' = 'stately' THEN
3696
+ EXECUTE format('CREATE UNIQUE INDEX %1$s_i3 ON ${schema}.%1$I (name, state, COALESCE(singleton_key, '''')) WHERE state <= ''active'' AND policy = ''stately''', tablename);
3697
+ ELSIF options->>'policy' = 'exclusive' THEN
3698
+ EXECUTE format('CREATE UNIQUE INDEX %1$s_i6 ON ${schema}.%1$I (name, COALESCE(singleton_key, '''')) WHERE state <= ''active'' AND policy = ''exclusive''', tablename);
3699
+ END IF;
3495
3700
 
3496
- EXECUTE format('ALTER TABLE ${schema}.%I ADD CONSTRAINT cjc CHECK (name=%L)', tablename, queue_name);
3497
- EXECUTE format('ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.%I FOR VALUES IN (%L)', tablename, queue_name);
3498
- END;
3499
- $$
3500
- LANGUAGE plpgsql;
3501
- `, `CREATE UNIQUE INDEX job_i6 ON ${schema}.job_common (name, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'exclusive'`],
3502
- uninstall: [`DROP INDEX ${schema}.job_i6`]
3503
- },
3504
- {
3505
- release: "12.6.0",
3506
- version: 27,
3507
- previous: 26,
3508
- install: [
3509
- `ALTER TABLE ${schema}.version ADD COLUMN IF NOT EXISTS bam_on timestamp with time zone`,
3510
- `
3511
- CREATE TABLE IF NOT EXISTS ${schema}.bam (
3512
- id uuid PRIMARY KEY default gen_random_uuid(),
3513
- name text NOT NULL,
3514
- version int NOT NULL,
3515
- status text NOT NULL DEFAULT 'pending',
3516
- queue text,
3517
- table_name text NOT NULL,
3518
- command text NOT NULL,
3519
- error text,
3520
- created_on timestamp with time zone NOT NULL DEFAULT now(),
3521
- started_on timestamp with time zone,
3522
- completed_on timestamp with time zone
3701
+ EXECUTE format('ALTER TABLE ${schema}.%I ADD CONSTRAINT cjc CHECK (name=%L)', tablename, queue_name);
3702
+ EXECUTE format('ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.%I FOR VALUES IN (%L)', tablename, queue_name);
3703
+ END;
3704
+ $$
3705
+ LANGUAGE plpgsql;
3706
+ `,
3707
+ 27: (schema) => `
3708
+ CREATE OR REPLACE FUNCTION ${schema}.create_queue(queue_name text, options jsonb)
3709
+ RETURNS VOID AS
3710
+ $$
3711
+ DECLARE
3712
+ tablename varchar := CASE WHEN options->>'partition' = 'true'
3713
+ THEN 'j' || encode(sha224(queue_name::bytea), 'hex')
3714
+ ELSE 'job_common'
3715
+ END;
3716
+ queue_created_on timestamptz;
3717
+ BEGIN
3718
+
3719
+ WITH q as (
3720
+ INSERT INTO ${schema}.queue (
3721
+ name,
3722
+ policy,
3723
+ retry_limit,
3724
+ retry_delay,
3725
+ retry_backoff,
3726
+ retry_delay_max,
3727
+ expire_seconds,
3728
+ retention_seconds,
3729
+ deletion_seconds,
3730
+ warning_queued,
3731
+ dead_letter,
3732
+ partition,
3733
+ table_name
3523
3734
  )
3524
- `,
3525
- `CREATE FUNCTION ${schema}.job_table_format(command text, table_name text)
3526
- RETURNS text AS
3527
- $$
3528
- SELECT format(
3529
- replace(
3530
- replace(command, '.job', '.%1$I'),
3531
- 'job_i', '%1$s_i'
3532
- ),
3533
- table_name
3534
- );
3535
- $$
3536
- LANGUAGE sql IMMUTABLE;
3537
- `,
3538
- `
3539
- CREATE OR REPLACE FUNCTION ${schema}.job_table_run_async(command_name text, version int, command text, tbl_name text DEFAULT NULL, queue_name text DEFAULT NULL)
3540
- RETURNS VOID AS
3541
- $$
3542
- BEGIN
3543
- IF queue_name IS NOT NULL THEN
3544
- SELECT table_name INTO tbl_name FROM ${schema}.queue WHERE name = queue_name;
3545
- END IF;
3735
+ VALUES (
3736
+ queue_name,
3737
+ options->>'policy',
3738
+ COALESCE((options->>'retryLimit')::int, 2),
3739
+ COALESCE((options->>'retryDelay')::int, 0),
3740
+ COALESCE((options->>'retryBackoff')::bool, false),
3741
+ (options->>'retryDelayMax')::int,
3742
+ COALESCE((options->>'expireInSeconds')::int, 900),
3743
+ COALESCE((options->>'retentionSeconds')::int, 1209600),
3744
+ COALESCE((options->>'deleteAfterSeconds')::int, 604800),
3745
+ COALESCE((options->>'warningQueueSize')::int, 0),
3746
+ options->>'deadLetter',
3747
+ COALESCE((options->>'partition')::bool, false),
3748
+ tablename
3749
+ )
3750
+ ON CONFLICT DO NOTHING
3751
+ RETURNING created_on
3752
+ )
3753
+ SELECT created_on into queue_created_on from q;
3546
3754
 
3547
- IF tbl_name IS NOT NULL THEN
3548
- INSERT INTO ${schema}.bam (name, version, status, queue, table_name, command)
3549
- VALUES (
3550
- command_name,
3551
- version,
3552
- 'pending',
3553
- queue_name,
3554
- tbl_name,
3555
- ${schema}.job_table_format(command, tbl_name)
3556
- );
3557
- RETURN;
3558
- END IF;
3755
+ IF queue_created_on IS NULL OR options->>'partition' IS DISTINCT FROM 'true' THEN
3756
+ RETURN;
3757
+ END IF;
3559
3758
 
3560
- INSERT INTO ${schema}.bam (name, version, status, queue, table_name, command)
3561
- SELECT
3562
- command_name,
3563
- version,
3564
- 'pending',
3565
- NULL,
3566
- 'job_common',
3759
+ EXECUTE format('CREATE TABLE ${schema}.%I (LIKE ${schema}.job INCLUDING DEFAULTS)', tablename);
3760
+
3761
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD PRIMARY KEY (name, id)$cmd$, tablename);
3762
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT q_fkey FOREIGN KEY (name) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
3763
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT dlq_fkey FOREIGN KEY (dead_letter) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
3764
+
3765
+ EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i5 ON ${schema}.job (name, start_after) INCLUDE (priority, created_on, id) WHERE state < 'active'$cmd$, tablename);
3766
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i4 ON ${schema}.job (name, singleton_on, COALESCE(singleton_key, '')) WHERE state <> 'cancelled' AND singleton_on IS NOT NULL$cmd$, tablename);
3767
+ EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i7 ON ${schema}.job (name, group_id) WHERE state = 'active' AND group_id IS NOT NULL$cmd$, tablename);
3768
+
3769
+ IF options->>'policy' = 'short' THEN
3770
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i1 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = 'created' AND policy = 'short'$cmd$, tablename);
3771
+ ELSIF options->>'policy' = 'singleton' THEN
3772
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i2 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = 'active' AND policy = 'singleton'$cmd$, tablename);
3773
+ ELSIF options->>'policy' = 'stately' THEN
3774
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i3 ON ${schema}.job (name, state, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'stately'$cmd$, tablename);
3775
+ ELSIF options->>'policy' = 'exclusive' THEN
3776
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i6 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'exclusive'$cmd$, tablename);
3777
+ END IF;
3778
+
3779
+ EXECUTE format('ALTER TABLE ${schema}.%I ADD CONSTRAINT cjc CHECK (name=%L)', tablename, queue_name);
3780
+ EXECUTE format('ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.%I FOR VALUES IN (%L)', tablename, queue_name);
3781
+ END;
3782
+ $$
3783
+ LANGUAGE plpgsql;
3784
+ `,
3785
+ 28: (schema) => `
3786
+ CREATE OR REPLACE FUNCTION ${schema}.create_queue(queue_name text, options jsonb)
3787
+ RETURNS VOID AS
3788
+ $$
3789
+ DECLARE
3790
+ tablename varchar := CASE WHEN options->>'partition' = 'true'
3791
+ THEN 'j' || encode(sha224(queue_name::bytea), 'hex')
3792
+ ELSE 'job_common'
3793
+ END;
3794
+ queue_created_on timestamptz;
3795
+ BEGIN
3796
+
3797
+ WITH q as (
3798
+ INSERT INTO ${schema}.queue (
3799
+ name,
3800
+ policy,
3801
+ retry_limit,
3802
+ retry_delay,
3803
+ retry_backoff,
3804
+ retry_delay_max,
3805
+ expire_seconds,
3806
+ retention_seconds,
3807
+ deletion_seconds,
3808
+ warning_queued,
3809
+ dead_letter,
3810
+ partition,
3811
+ table_name
3812
+ )
3813
+ VALUES (
3814
+ queue_name,
3815
+ options->>'policy',
3816
+ COALESCE((options->>'retryLimit')::int, 2),
3817
+ COALESCE((options->>'retryDelay')::int, 0),
3818
+ COALESCE((options->>'retryBackoff')::bool, false),
3819
+ (options->>'retryDelayMax')::int,
3820
+ COALESCE((options->>'expireInSeconds')::int, 900),
3821
+ COALESCE((options->>'retentionSeconds')::int, 1209600),
3822
+ COALESCE((options->>'deleteAfterSeconds')::int, 604800),
3823
+ COALESCE((options->>'warningQueueSize')::int, 0),
3824
+ options->>'deadLetter',
3825
+ COALESCE((options->>'partition')::bool, false),
3826
+ tablename
3827
+ )
3828
+ ON CONFLICT DO NOTHING
3829
+ RETURNING created_on
3830
+ )
3831
+ SELECT created_on into queue_created_on from q;
3832
+
3833
+ IF queue_created_on IS NULL OR options->>'partition' IS DISTINCT FROM 'true' THEN
3834
+ RETURN;
3835
+ END IF;
3836
+
3837
+ EXECUTE format('CREATE TABLE ${schema}.%I (LIKE ${schema}.job INCLUDING DEFAULTS)', tablename);
3838
+
3839
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD PRIMARY KEY (name, id)$cmd$, tablename);
3840
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT q_fkey FOREIGN KEY (name) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
3841
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT dlq_fkey FOREIGN KEY (dead_letter) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
3842
+
3843
+ EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i5 ON ${schema}.job (name, start_after) INCLUDE (priority, created_on, id) WHERE state < 'active'$cmd$, tablename);
3844
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i4 ON ${schema}.job (name, singleton_on, COALESCE(singleton_key, '')) WHERE state <> 'cancelled' AND singleton_on IS NOT NULL$cmd$, tablename);
3845
+ EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i7 ON ${schema}.job (name, group_id) WHERE state = 'active' AND group_id IS NOT NULL$cmd$, tablename);
3846
+
3847
+ IF options->>'policy' = 'short' THEN
3848
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i1 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = 'created' AND policy = 'short'$cmd$, tablename);
3849
+ ELSIF options->>'policy' = 'singleton' THEN
3850
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i2 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = 'active' AND policy = 'singleton'$cmd$, tablename);
3851
+ ELSIF options->>'policy' = 'stately' THEN
3852
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i3 ON ${schema}.job (name, state, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'stately'$cmd$, tablename);
3853
+ ELSIF options->>'policy' = 'exclusive' THEN
3854
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i6 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'exclusive'$cmd$, tablename);
3855
+ ELSIF options->>'policy' = 'key_strict_fifo' THEN
3856
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i8 ON ${schema}.job (name, singleton_key) WHERE state IN ('active', 'retry', 'failed') AND policy = 'key_strict_fifo'$cmd$, tablename);
3857
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT job_key_strict_fifo_singleton_key_check CHECK (NOT (policy = 'key_strict_fifo' AND singleton_key IS NULL))$cmd$, tablename);
3858
+ END IF;
3859
+
3860
+ EXECUTE format('ALTER TABLE ${schema}.%I ADD CONSTRAINT cjc CHECK (name=%L)', tablename, queue_name);
3861
+ EXECUTE format('ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.%I FOR VALUES IN (%L)', tablename, queue_name);
3862
+ END;
3863
+ $$
3864
+ LANGUAGE plpgsql;
3865
+ `,
3866
+ 30: (schema) => `
3867
+ CREATE OR REPLACE FUNCTION ${schema}.create_queue(queue_name text, options jsonb)
3868
+ RETURNS VOID AS
3869
+ $$
3870
+ DECLARE
3871
+ tablename varchar := CASE WHEN options->>'partition' = 'true'
3872
+ THEN 'j' || encode(sha224(queue_name::bytea), 'hex')
3873
+ ELSE 'job_common'
3874
+ END;
3875
+ queue_created_on timestamptz;
3876
+ BEGIN
3877
+
3878
+ WITH q as (
3879
+ INSERT INTO ${schema}.queue (
3880
+ name,
3881
+ policy,
3882
+ retry_limit,
3883
+ retry_delay,
3884
+ retry_backoff,
3885
+ retry_delay_max,
3886
+ expire_seconds,
3887
+ retention_seconds,
3888
+ deletion_seconds,
3889
+ warning_queued,
3890
+ dead_letter,
3891
+ partition,
3892
+ table_name,
3893
+ heartbeat_seconds
3894
+ )
3895
+ VALUES (
3896
+ queue_name,
3897
+ options->>'policy',
3898
+ COALESCE((options->>'retryLimit')::int, 2),
3899
+ COALESCE((options->>'retryDelay')::int, 0),
3900
+ COALESCE((options->>'retryBackoff')::bool, false),
3901
+ (options->>'retryDelayMax')::int,
3902
+ COALESCE((options->>'expireInSeconds')::int, 900),
3903
+ COALESCE((options->>'retentionSeconds')::int, 1209600),
3904
+ COALESCE((options->>'deleteAfterSeconds')::int, 604800),
3905
+ COALESCE((options->>'warningQueueSize')::int, 0),
3906
+ options->>'deadLetter',
3907
+ COALESCE((options->>'partition')::bool, false),
3908
+ tablename,
3909
+ (options->>'heartbeatSeconds')::int
3910
+ )
3911
+ ON CONFLICT DO NOTHING
3912
+ RETURNING created_on
3913
+ )
3914
+ SELECT created_on into queue_created_on from q;
3915
+
3916
+ IF queue_created_on IS NULL OR options->>'partition' IS DISTINCT FROM 'true' THEN
3917
+ RETURN;
3918
+ END IF;
3919
+
3920
+ EXECUTE format('CREATE TABLE ${schema}.%I (LIKE ${schema}.job INCLUDING DEFAULTS)', tablename);
3921
+
3922
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD PRIMARY KEY (name, id)$cmd$, tablename);
3923
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT q_fkey FOREIGN KEY (name) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
3924
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT dlq_fkey FOREIGN KEY (dead_letter) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
3925
+
3926
+ EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i5 ON ${schema}.job (name, start_after) INCLUDE (priority, created_on, id) WHERE state < 'active'$cmd$, tablename);
3927
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i4 ON ${schema}.job (name, singleton_on, COALESCE(singleton_key, '')) WHERE state <> 'cancelled' AND singleton_on IS NOT NULL$cmd$, tablename);
3928
+ EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i7 ON ${schema}.job (name, group_id) WHERE state = 'active' AND group_id IS NOT NULL$cmd$, tablename);
3929
+
3930
+ IF options->>'policy' = 'short' THEN
3931
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i1 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = 'created' AND policy = 'short'$cmd$, tablename);
3932
+ ELSIF options->>'policy' = 'singleton' THEN
3933
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i2 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = 'active' AND policy = 'singleton'$cmd$, tablename);
3934
+ ELSIF options->>'policy' = 'stately' THEN
3935
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i3 ON ${schema}.job (name, state, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'stately'$cmd$, tablename);
3936
+ ELSIF options->>'policy' = 'exclusive' THEN
3937
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i6 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'exclusive'$cmd$, tablename);
3938
+ ELSIF options->>'policy' = 'key_strict_fifo' THEN
3939
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i8 ON ${schema}.job (name, singleton_key) WHERE state IN ('active', 'retry', 'failed') AND policy = 'key_strict_fifo'$cmd$, tablename);
3940
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT job_key_strict_fifo_singleton_key_check CHECK (NOT (policy = 'key_strict_fifo' AND singleton_key IS NULL))$cmd$, tablename);
3941
+ END IF;
3942
+
3943
+ EXECUTE format('ALTER TABLE ${schema}.%I ADD CONSTRAINT cjc CHECK (name=%L)', tablename, queue_name);
3944
+ EXECUTE format('ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.%I FOR VALUES IN (%L)', tablename, queue_name);
3945
+ END;
3946
+ $$
3947
+ LANGUAGE plpgsql;
3948
+ `,
3949
+ 31: (schema) => `
3950
+ CREATE OR REPLACE FUNCTION ${schema}.create_queue(queue_name text, options jsonb)
3951
+ RETURNS VOID AS
3952
+ $$
3953
+ DECLARE
3954
+ tablename varchar := CASE WHEN options->>'partition' = 'true'
3955
+ THEN 'j' || encode(sha224(queue_name::bytea), 'hex')
3956
+ ELSE 'job_common'
3957
+ END;
3958
+ queue_created_on timestamptz;
3959
+ BEGIN
3960
+
3961
+ WITH q as (
3962
+ INSERT INTO ${schema}.queue (
3963
+ name,
3964
+ policy,
3965
+ retry_limit,
3966
+ retry_delay,
3967
+ retry_backoff,
3968
+ retry_delay_max,
3969
+ expire_seconds,
3970
+ retention_seconds,
3971
+ deletion_seconds,
3972
+ warning_queued,
3973
+ dead_letter,
3974
+ partition,
3975
+ table_name,
3976
+ heartbeat_seconds
3977
+ )
3978
+ VALUES (
3979
+ queue_name,
3980
+ options->>'policy',
3981
+ COALESCE((options->>'retryLimit')::int, 2),
3982
+ COALESCE((options->>'retryDelay')::int, 0),
3983
+ COALESCE((options->>'retryBackoff')::bool, false),
3984
+ (options->>'retryDelayMax')::int,
3985
+ COALESCE((options->>'expireInSeconds')::int, 900),
3986
+ COALESCE((options->>'retentionSeconds')::int, 1209600),
3987
+ COALESCE((options->>'deleteAfterSeconds')::int, 604800),
3988
+ COALESCE((options->>'warningQueueSize')::int, 0),
3989
+ options->>'deadLetter',
3990
+ COALESCE((options->>'partition')::bool, false),
3991
+ tablename,
3992
+ (options->>'heartbeatSeconds')::int
3993
+ )
3994
+ ON CONFLICT DO NOTHING
3995
+ RETURNING created_on
3996
+ )
3997
+ SELECT created_on into queue_created_on from q;
3998
+
3999
+ IF queue_created_on IS NULL OR options->>'partition' IS DISTINCT FROM 'true' THEN
4000
+ RETURN;
4001
+ END IF;
4002
+
4003
+ EXECUTE format('CREATE TABLE ${schema}.%I (LIKE ${schema}.job INCLUDING DEFAULTS)', tablename);
4004
+
4005
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD PRIMARY KEY (name, id)$cmd$, tablename);
4006
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT q_fkey FOREIGN KEY (name) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
4007
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT dlq_fkey FOREIGN KEY (dead_letter) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
4008
+
4009
+ EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i5 ON ${schema}.job (name, start_after) INCLUDE (priority, created_on, id) WHERE state < 'active' AND NOT blocked$cmd$, tablename);
4010
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i4 ON ${schema}.job (name, singleton_on, COALESCE(singleton_key, '')) WHERE state <> 'cancelled' AND singleton_on IS NOT NULL$cmd$, tablename);
4011
+ EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i7 ON ${schema}.job (name, group_id) WHERE state = 'active' AND group_id IS NOT NULL$cmd$, tablename);
4012
+
4013
+ IF options->>'policy' = 'short' THEN
4014
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i1 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = 'created' AND policy = 'short'$cmd$, tablename);
4015
+ ELSIF options->>'policy' = 'singleton' THEN
4016
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i2 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = 'active' AND policy = 'singleton'$cmd$, tablename);
4017
+ ELSIF options->>'policy' = 'stately' THEN
4018
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i3 ON ${schema}.job (name, state, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'stately'$cmd$, tablename);
4019
+ ELSIF options->>'policy' = 'exclusive' THEN
4020
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i6 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'exclusive'$cmd$, tablename);
4021
+ ELSIF options->>'policy' = 'key_strict_fifo' THEN
4022
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i8 ON ${schema}.job (name, singleton_key) WHERE state IN ('active', 'retry', 'failed') AND policy = 'key_strict_fifo'$cmd$, tablename);
4023
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT job_key_strict_fifo_singleton_key_check CHECK (NOT (policy = 'key_strict_fifo' AND singleton_key IS NULL))$cmd$, tablename);
4024
+ END IF;
4025
+
4026
+ EXECUTE format('ALTER TABLE ${schema}.%I ADD CONSTRAINT cjc CHECK (name=%L)', tablename, queue_name);
4027
+ EXECUTE format('ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.%I FOR VALUES IN (%L)', tablename, queue_name);
4028
+ END;
4029
+ $$
4030
+ LANGUAGE plpgsql;
4031
+ `,
4032
+ 32: (schema) => `
4033
+ CREATE OR REPLACE FUNCTION ${schema}.create_queue(queue_name text, options jsonb)
4034
+ RETURNS VOID AS
4035
+ $$
4036
+ DECLARE
4037
+ tablename varchar := CASE WHEN options->>'partition' = 'true'
4038
+ THEN 'j' || encode(sha224(queue_name::bytea), 'hex')
4039
+ ELSE 'job_common'
4040
+ END;
4041
+ queue_created_on timestamptz;
4042
+ BEGIN
4043
+
4044
+ WITH q as (
4045
+ INSERT INTO ${schema}.queue (
4046
+ name,
4047
+ policy,
4048
+ retry_limit,
4049
+ retry_delay,
4050
+ retry_backoff,
4051
+ retry_delay_max,
4052
+ expire_seconds,
4053
+ retention_seconds,
4054
+ deletion_seconds,
4055
+ warning_queued,
4056
+ dead_letter,
4057
+ partition,
4058
+ table_name,
4059
+ heartbeat_seconds,
4060
+ notify
4061
+ )
4062
+ VALUES (
4063
+ queue_name,
4064
+ options->>'policy',
4065
+ COALESCE((options->>'retryLimit')::int, 2),
4066
+ COALESCE((options->>'retryDelay')::int, 0),
4067
+ COALESCE((options->>'retryBackoff')::bool, false),
4068
+ (options->>'retryDelayMax')::int,
4069
+ COALESCE((options->>'expireInSeconds')::int, 900),
4070
+ COALESCE((options->>'retentionSeconds')::int, 1209600),
4071
+ COALESCE((options->>'deleteAfterSeconds')::int, 604800),
4072
+ COALESCE((options->>'warningQueueSize')::int, 0),
4073
+ options->>'deadLetter',
4074
+ COALESCE((options->>'partition')::bool, false),
4075
+ tablename,
4076
+ (options->>'heartbeatSeconds')::int,
4077
+ COALESCE((options->>'notify')::bool, false)
4078
+ )
4079
+ ON CONFLICT DO NOTHING
4080
+ RETURNING created_on
4081
+ )
4082
+ SELECT created_on into queue_created_on from q;
4083
+
4084
+ IF queue_created_on IS NULL OR options->>'partition' IS DISTINCT FROM 'true' THEN
4085
+ RETURN;
4086
+ END IF;
4087
+
4088
+ EXECUTE format('CREATE TABLE ${schema}.%I (LIKE ${schema}.job INCLUDING DEFAULTS)', tablename);
4089
+
4090
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD PRIMARY KEY (name, id)$cmd$, tablename);
4091
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT q_fkey FOREIGN KEY (name) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
4092
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT dlq_fkey FOREIGN KEY (dead_letter) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
4093
+
4094
+ EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i5 ON ${schema}.job (name, start_after) INCLUDE (priority, created_on, id) WHERE state < 'active' AND NOT blocked$cmd$, tablename);
4095
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i4 ON ${schema}.job (name, singleton_on, COALESCE(singleton_key, '')) WHERE state <> 'cancelled' AND singleton_on IS NOT NULL$cmd$, tablename);
4096
+ EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i7 ON ${schema}.job (name, group_id) WHERE state = 'active' AND group_id IS NOT NULL$cmd$, tablename);
4097
+
4098
+ IF options->>'policy' = 'short' THEN
4099
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i1 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = 'created' AND policy = 'short'$cmd$, tablename);
4100
+ ELSIF options->>'policy' = 'singleton' THEN
4101
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i2 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = 'active' AND policy = 'singleton'$cmd$, tablename);
4102
+ ELSIF options->>'policy' = 'stately' THEN
4103
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i3 ON ${schema}.job (name, state, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'stately'$cmd$, tablename);
4104
+ ELSIF options->>'policy' = 'exclusive' THEN
4105
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i6 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'exclusive'$cmd$, tablename);
4106
+ ELSIF options->>'policy' = 'key_strict_fifo' THEN
4107
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i8 ON ${schema}.job (name, singleton_key) WHERE state IN ('active', 'retry', 'failed') AND policy = 'key_strict_fifo'$cmd$, tablename);
4108
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT job_key_strict_fifo_singleton_key_check CHECK (NOT (policy = 'key_strict_fifo' AND singleton_key IS NULL))$cmd$, tablename);
4109
+ END IF;
4110
+
4111
+ EXECUTE format('ALTER TABLE ${schema}.%I ADD CONSTRAINT cjc CHECK (name=%L)', tablename, queue_name);
4112
+ EXECUTE format('ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.%I FOR VALUES IN (%L)', tablename, queue_name);
4113
+ END;
4114
+ $$
4115
+ LANGUAGE plpgsql;
4116
+ `
4117
+ };
4118
+ function getAll(schema) {
4119
+ return [
4120
+ {
4121
+ release: "11.1.0",
4122
+ version: 26,
4123
+ previous: 25,
4124
+ install: [createQueueFn[26](schema), `CREATE UNIQUE INDEX job_i6 ON ${schema}.job_common (name, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'exclusive'`],
4125
+ uninstall: [`DROP INDEX ${schema}.job_i6`]
4126
+ },
4127
+ {
4128
+ release: "12.6.0",
4129
+ version: 27,
4130
+ previous: 26,
4131
+ install: [
4132
+ `ALTER TABLE ${schema}.version ADD COLUMN IF NOT EXISTS bam_on timestamp with time zone`,
4133
+ `
4134
+ CREATE TABLE IF NOT EXISTS ${schema}.bam (
4135
+ id uuid PRIMARY KEY default gen_random_uuid(),
4136
+ name text NOT NULL,
4137
+ version int NOT NULL,
4138
+ status text NOT NULL DEFAULT 'pending',
4139
+ queue text,
4140
+ table_name text NOT NULL,
4141
+ command text NOT NULL,
4142
+ error text,
4143
+ created_on timestamp with time zone NOT NULL DEFAULT now(),
4144
+ started_on timestamp with time zone,
4145
+ completed_on timestamp with time zone
4146
+ )
4147
+ `,
4148
+ `CREATE FUNCTION ${schema}.job_table_format(command text, table_name text)
4149
+ RETURNS text AS
4150
+ $$
4151
+ SELECT format(
4152
+ replace(
4153
+ replace(command, '.job', '.%1$I'),
4154
+ 'job_i', '%1$s_i'
4155
+ ),
4156
+ table_name
4157
+ );
4158
+ $$
4159
+ LANGUAGE sql IMMUTABLE;
4160
+ `,
4161
+ `
4162
+ CREATE OR REPLACE FUNCTION ${schema}.job_table_run_async(command_name text, version int, command text, tbl_name text DEFAULT NULL, queue_name text DEFAULT NULL)
4163
+ RETURNS VOID AS
4164
+ $$
4165
+ BEGIN
4166
+ IF queue_name IS NOT NULL THEN
4167
+ SELECT table_name INTO tbl_name FROM ${schema}.queue WHERE name = queue_name;
4168
+ END IF;
4169
+
4170
+ IF tbl_name IS NOT NULL THEN
4171
+ INSERT INTO ${schema}.bam (name, version, status, queue, table_name, command)
4172
+ VALUES (
4173
+ command_name,
4174
+ version,
4175
+ 'pending',
4176
+ queue_name,
4177
+ tbl_name,
4178
+ ${schema}.job_table_format(command, tbl_name)
4179
+ );
4180
+ RETURN;
4181
+ END IF;
4182
+
4183
+ INSERT INTO ${schema}.bam (name, version, status, queue, table_name, command)
4184
+ SELECT
4185
+ command_name,
4186
+ version,
4187
+ 'pending',
4188
+ NULL,
4189
+ 'job_common',
3567
4190
  ${schema}.job_table_format(command, 'job_common')
3568
4191
  UNION ALL
3569
4192
  SELECT
@@ -3605,86 +4228,9 @@ function getAll(schema) {
3605
4228
  $$
3606
4229
  LANGUAGE plpgsql;
3607
4230
  `,
3608
- `ALTER TABLE ${schema}.job ADD COLUMN IF NOT EXISTS group_id text`,
3609
- `ALTER TABLE ${schema}.job ADD COLUMN IF NOT EXISTS group_tier text`,
3610
- `
3611
- CREATE OR REPLACE FUNCTION ${schema}.create_queue(queue_name text, options jsonb)
3612
- RETURNS VOID AS
3613
- $$
3614
- DECLARE
3615
- tablename varchar := CASE WHEN options->>'partition' = 'true'
3616
- THEN 'j' || encode(sha224(queue_name::bytea), 'hex')
3617
- ELSE 'job_common'
3618
- END;
3619
- queue_created_on timestamptz;
3620
- BEGIN
3621
-
3622
- WITH q as (
3623
- INSERT INTO ${schema}.queue (
3624
- name,
3625
- policy,
3626
- retry_limit,
3627
- retry_delay,
3628
- retry_backoff,
3629
- retry_delay_max,
3630
- expire_seconds,
3631
- retention_seconds,
3632
- deletion_seconds,
3633
- warning_queued,
3634
- dead_letter,
3635
- partition,
3636
- table_name
3637
- )
3638
- VALUES (
3639
- queue_name,
3640
- options->>'policy',
3641
- COALESCE((options->>'retryLimit')::int, 2),
3642
- COALESCE((options->>'retryDelay')::int, 0),
3643
- COALESCE((options->>'retryBackoff')::bool, false),
3644
- (options->>'retryDelayMax')::int,
3645
- COALESCE((options->>'expireInSeconds')::int, 900),
3646
- COALESCE((options->>'retentionSeconds')::int, 1209600),
3647
- COALESCE((options->>'deleteAfterSeconds')::int, 604800),
3648
- COALESCE((options->>'warningQueueSize')::int, 0),
3649
- options->>'deadLetter',
3650
- COALESCE((options->>'partition')::bool, false),
3651
- tablename
3652
- )
3653
- ON CONFLICT DO NOTHING
3654
- RETURNING created_on
3655
- )
3656
- SELECT created_on into queue_created_on from q;
3657
-
3658
- IF queue_created_on IS NULL OR options->>'partition' IS DISTINCT FROM 'true' THEN
3659
- RETURN;
3660
- END IF;
3661
-
3662
- EXECUTE format('CREATE TABLE ${schema}.%I (LIKE ${schema}.job INCLUDING DEFAULTS)', tablename);
3663
-
3664
- EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD PRIMARY KEY (name, id)$cmd$, tablename);
3665
- EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT q_fkey FOREIGN KEY (name) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
3666
- EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT dlq_fkey FOREIGN KEY (dead_letter) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
3667
-
3668
- EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i5 ON ${schema}.job (name, start_after) INCLUDE (priority, created_on, id) WHERE state < 'active'$cmd$, tablename);
3669
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i4 ON ${schema}.job (name, singleton_on, COALESCE(singleton_key, '')) WHERE state <> 'cancelled' AND singleton_on IS NOT NULL$cmd$, tablename);
3670
- EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i7 ON ${schema}.job (name, group_id) WHERE state = 'active' AND group_id IS NOT NULL$cmd$, tablename);
3671
-
3672
- IF options->>'policy' = 'short' THEN
3673
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i1 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = 'created' AND policy = 'short'$cmd$, tablename);
3674
- ELSIF options->>'policy' = 'singleton' THEN
3675
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i2 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = 'active' AND policy = 'singleton'$cmd$, tablename);
3676
- ELSIF options->>'policy' = 'stately' THEN
3677
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i3 ON ${schema}.job (name, state, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'stately'$cmd$, tablename);
3678
- ELSIF options->>'policy' = 'exclusive' THEN
3679
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i6 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'exclusive'$cmd$, tablename);
3680
- END IF;
3681
-
3682
- EXECUTE format('ALTER TABLE ${schema}.%I ADD CONSTRAINT cjc CHECK (name=%L)', tablename, queue_name);
3683
- EXECUTE format('ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.%I FOR VALUES IN (%L)', tablename, queue_name);
3684
- END;
3685
- $$
3686
- LANGUAGE plpgsql;
3687
- `,
4231
+ `ALTER TABLE ${schema}.job ADD COLUMN IF NOT EXISTS group_id text`,
4232
+ `ALTER TABLE ${schema}.job ADD COLUMN IF NOT EXISTS group_tier text`,
4233
+ createQueueFn[27](schema),
3688
4234
  `ALTER INDEX IF EXISTS ${schema}.job_i1 RENAME TO job_common_i1`,
3689
4235
  `ALTER INDEX IF EXISTS ${schema}.job_i2 RENAME TO job_common_i2`,
3690
4236
  `ALTER INDEX IF EXISTS ${schema}.job_i3 RENAME TO job_common_i3`,
@@ -3708,83 +4254,7 @@ function getAll(schema) {
3708
4254
  `ALTER INDEX ${schema}.job_common_i2 RENAME TO job_i2`,
3709
4255
  `ALTER INDEX ${schema}.job_common_i1 RENAME TO job_i1`,
3710
4256
  `SELECT ${schema}.job_table_run('DROP INDEX ${schema}.job_i7')`,
3711
- `
3712
- CREATE OR REPLACE FUNCTION ${schema}.create_queue(queue_name text, options jsonb)
3713
- RETURNS VOID AS
3714
- $$
3715
- DECLARE
3716
- tablename varchar := CASE WHEN options->>'partition' = 'true'
3717
- THEN 'j' || encode(sha224(queue_name::bytea), 'hex')
3718
- ELSE 'job_common'
3719
- END;
3720
- queue_created_on timestamptz;
3721
- BEGIN
3722
-
3723
- WITH q as (
3724
- INSERT INTO ${schema}.queue (
3725
- name,
3726
- policy,
3727
- retry_limit,
3728
- retry_delay,
3729
- retry_backoff,
3730
- retry_delay_max,
3731
- expire_seconds,
3732
- retention_seconds,
3733
- deletion_seconds,
3734
- warning_queued,
3735
- dead_letter,
3736
- partition,
3737
- table_name
3738
- )
3739
- VALUES (
3740
- queue_name,
3741
- options->>'policy',
3742
- COALESCE((options->>'retryLimit')::int, 2),
3743
- COALESCE((options->>'retryDelay')::int, 0),
3744
- COALESCE((options->>'retryBackoff')::bool, false),
3745
- (options->>'retryDelayMax')::int,
3746
- COALESCE((options->>'expireInSeconds')::int, 900),
3747
- COALESCE((options->>'retentionSeconds')::int, 1209600),
3748
- COALESCE((options->>'deleteAfterSeconds')::int, 604800),
3749
- COALESCE((options->>'warningQueueSize')::int, 0),
3750
- options->>'deadLetter',
3751
- COALESCE((options->>'partition')::bool, false),
3752
- tablename
3753
- )
3754
- ON CONFLICT DO NOTHING
3755
- RETURNING created_on
3756
- )
3757
- SELECT created_on into queue_created_on from q;
3758
-
3759
- IF queue_created_on IS NULL OR options->>'partition' IS DISTINCT FROM 'true' THEN
3760
- RETURN;
3761
- END IF;
3762
-
3763
- EXECUTE format('CREATE TABLE ${schema}.%I (LIKE ${schema}.job INCLUDING DEFAULTS)', tablename);
3764
-
3765
- EXECUTE format('ALTER TABLE ${schema}.%1$I ADD PRIMARY KEY (name, id)', tablename);
3766
- EXECUTE format('ALTER TABLE ${schema}.%1$I ADD CONSTRAINT q_fkey FOREIGN KEY (name) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED', tablename);
3767
- EXECUTE format('ALTER TABLE ${schema}.%1$I ADD CONSTRAINT dlq_fkey FOREIGN KEY (dead_letter) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED', tablename);
3768
-
3769
- EXECUTE format('CREATE INDEX %1$s_i5 ON ${schema}.%1$I (name, start_after) INCLUDE (priority, created_on, id) WHERE state < ''active''', tablename);
3770
- EXECUTE format('CREATE UNIQUE INDEX %1$s_i4 ON ${schema}.%1$I (name, singleton_on, COALESCE(singleton_key, '''')) WHERE state <> ''cancelled'' AND singleton_on IS NOT NULL', tablename);
3771
-
3772
- IF options->>'policy' = 'short' THEN
3773
- EXECUTE format('CREATE UNIQUE INDEX %1$s_i1 ON ${schema}.%1$I (name, COALESCE(singleton_key, '''')) WHERE state = ''created'' AND policy = ''short''', tablename);
3774
- ELSIF options->>'policy' = 'singleton' THEN
3775
- EXECUTE format('CREATE UNIQUE INDEX %1$s_i2 ON ${schema}.%1$I (name, COALESCE(singleton_key, '''')) WHERE state = ''active'' AND policy = ''singleton''', tablename);
3776
- ELSIF options->>'policy' = 'stately' THEN
3777
- EXECUTE format('CREATE UNIQUE INDEX %1$s_i3 ON ${schema}.%1$I (name, state, COALESCE(singleton_key, '''')) WHERE state <= ''active'' AND policy = ''stately''', tablename);
3778
- ELSIF options->>'policy' = 'exclusive' THEN
3779
- EXECUTE format('CREATE UNIQUE INDEX %1$s_i6 ON ${schema}.%1$I (name, COALESCE(singleton_key, '''')) WHERE state <= ''active'' AND policy = ''exclusive''', tablename);
3780
- END IF;
3781
-
3782
- EXECUTE format('ALTER TABLE ${schema}.%I ADD CONSTRAINT cjc CHECK (name=%L)', tablename, queue_name);
3783
- EXECUTE format('ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.%I FOR VALUES IN (%L)', tablename, queue_name);
3784
- END;
3785
- $$
3786
- LANGUAGE plpgsql;
3787
- `,
4257
+ createQueueFn[26](schema),
3788
4258
  `DROP FUNCTION ${schema}.job_table_run(text, text, text)`,
3789
4259
  `DROP FUNCTION ${schema}.job_table_run_async(text, int, text, text, text)`,
3790
4260
  `DROP FUNCTION ${schema}.job_table_format(text, text)`,
@@ -3798,87 +4268,7 @@ function getAll(schema) {
3798
4268
  release: "12.10.0",
3799
4269
  version: 28,
3800
4270
  previous: 27,
3801
- install: [`SELECT ${schema}.job_table_run($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT job_key_strict_fifo_singleton_key_check CHECK (NOT (policy = 'key_strict_fifo' AND singleton_key IS NULL))$cmd$, 'job_common')`, `
3802
- CREATE OR REPLACE FUNCTION ${schema}.create_queue(queue_name text, options jsonb)
3803
- RETURNS VOID AS
3804
- $$
3805
- DECLARE
3806
- tablename varchar := CASE WHEN options->>'partition' = 'true'
3807
- THEN 'j' || encode(sha224(queue_name::bytea), 'hex')
3808
- ELSE 'job_common'
3809
- END;
3810
- queue_created_on timestamptz;
3811
- BEGIN
3812
-
3813
- WITH q as (
3814
- INSERT INTO ${schema}.queue (
3815
- name,
3816
- policy,
3817
- retry_limit,
3818
- retry_delay,
3819
- retry_backoff,
3820
- retry_delay_max,
3821
- expire_seconds,
3822
- retention_seconds,
3823
- deletion_seconds,
3824
- warning_queued,
3825
- dead_letter,
3826
- partition,
3827
- table_name
3828
- )
3829
- VALUES (
3830
- queue_name,
3831
- options->>'policy',
3832
- COALESCE((options->>'retryLimit')::int, 2),
3833
- COALESCE((options->>'retryDelay')::int, 0),
3834
- COALESCE((options->>'retryBackoff')::bool, false),
3835
- (options->>'retryDelayMax')::int,
3836
- COALESCE((options->>'expireInSeconds')::int, 900),
3837
- COALESCE((options->>'retentionSeconds')::int, 1209600),
3838
- COALESCE((options->>'deleteAfterSeconds')::int, 604800),
3839
- COALESCE((options->>'warningQueueSize')::int, 0),
3840
- options->>'deadLetter',
3841
- COALESCE((options->>'partition')::bool, false),
3842
- tablename
3843
- )
3844
- ON CONFLICT DO NOTHING
3845
- RETURNING created_on
3846
- )
3847
- SELECT created_on into queue_created_on from q;
3848
-
3849
- IF queue_created_on IS NULL OR options->>'partition' IS DISTINCT FROM 'true' THEN
3850
- RETURN;
3851
- END IF;
3852
-
3853
- EXECUTE format('CREATE TABLE ${schema}.%I (LIKE ${schema}.job INCLUDING DEFAULTS)', tablename);
3854
-
3855
- EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD PRIMARY KEY (name, id)$cmd$, tablename);
3856
- EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT q_fkey FOREIGN KEY (name) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
3857
- EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT dlq_fkey FOREIGN KEY (dead_letter) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
3858
-
3859
- EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i5 ON ${schema}.job (name, start_after) INCLUDE (priority, created_on, id) WHERE state < 'active'$cmd$, tablename);
3860
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i4 ON ${schema}.job (name, singleton_on, COALESCE(singleton_key, '')) WHERE state <> 'cancelled' AND singleton_on IS NOT NULL$cmd$, tablename);
3861
- EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i7 ON ${schema}.job (name, group_id) WHERE state = 'active' AND group_id IS NOT NULL$cmd$, tablename);
3862
-
3863
- IF options->>'policy' = 'short' THEN
3864
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i1 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = 'created' AND policy = 'short'$cmd$, tablename);
3865
- ELSIF options->>'policy' = 'singleton' THEN
3866
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i2 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = 'active' AND policy = 'singleton'$cmd$, tablename);
3867
- ELSIF options->>'policy' = 'stately' THEN
3868
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i3 ON ${schema}.job (name, state, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'stately'$cmd$, tablename);
3869
- ELSIF options->>'policy' = 'exclusive' THEN
3870
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i6 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'exclusive'$cmd$, tablename);
3871
- ELSIF options->>'policy' = 'key_strict_fifo' THEN
3872
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i8 ON ${schema}.job (name, singleton_key) WHERE state IN ('active', 'retry', 'failed') AND policy = 'key_strict_fifo'$cmd$, tablename);
3873
- EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT job_key_strict_fifo_singleton_key_check CHECK (NOT (policy = 'key_strict_fifo' AND singleton_key IS NULL))$cmd$, tablename);
3874
- END IF;
3875
-
3876
- EXECUTE format('ALTER TABLE ${schema}.%I ADD CONSTRAINT cjc CHECK (name=%L)', tablename, queue_name);
3877
- EXECUTE format('ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.%I FOR VALUES IN (%L)', tablename, queue_name);
3878
- END;
3879
- $$
3880
- LANGUAGE plpgsql;
3881
- `],
4271
+ install: [`SELECT ${schema}.job_table_run($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT job_key_strict_fifo_singleton_key_check CHECK (NOT (policy = 'key_strict_fifo' AND singleton_key IS NULL))$cmd$, 'job_common')`, createQueueFn[28](schema)],
3882
4272
  async: [`SELECT ${schema}.job_table_run_async(
3883
4273
  'key_strict_fifo_index',
3884
4274
  $VERSION$,
@@ -3889,84 +4279,7 @@ function getAll(schema) {
3889
4279
  uninstall: [
3890
4280
  `SELECT ${schema}.job_table_run('DROP INDEX IF EXISTS ${schema}.job_i8')`,
3891
4281
  `SELECT ${schema}.job_table_run('ALTER TABLE ${schema}.job DROP CONSTRAINT IF EXISTS job_key_strict_fifo_singleton_key_check')`,
3892
- `
3893
- CREATE OR REPLACE FUNCTION ${schema}.create_queue(queue_name text, options jsonb)
3894
- RETURNS VOID AS
3895
- $$
3896
- DECLARE
3897
- tablename varchar := CASE WHEN options->>'partition' = 'true'
3898
- THEN 'j' || encode(sha224(queue_name::bytea), 'hex')
3899
- ELSE 'job_common'
3900
- END;
3901
- queue_created_on timestamptz;
3902
- BEGIN
3903
-
3904
- WITH q as (
3905
- INSERT INTO ${schema}.queue (
3906
- name,
3907
- policy,
3908
- retry_limit,
3909
- retry_delay,
3910
- retry_backoff,
3911
- retry_delay_max,
3912
- expire_seconds,
3913
- retention_seconds,
3914
- deletion_seconds,
3915
- warning_queued,
3916
- dead_letter,
3917
- partition,
3918
- table_name
3919
- )
3920
- VALUES (
3921
- queue_name,
3922
- options->>'policy',
3923
- COALESCE((options->>'retryLimit')::int, 2),
3924
- COALESCE((options->>'retryDelay')::int, 0),
3925
- COALESCE((options->>'retryBackoff')::bool, false),
3926
- (options->>'retryDelayMax')::int,
3927
- COALESCE((options->>'expireInSeconds')::int, 900),
3928
- COALESCE((options->>'retentionSeconds')::int, 1209600),
3929
- COALESCE((options->>'deleteAfterSeconds')::int, 604800),
3930
- COALESCE((options->>'warningQueueSize')::int, 0),
3931
- options->>'deadLetter',
3932
- COALESCE((options->>'partition')::bool, false),
3933
- tablename
3934
- )
3935
- ON CONFLICT DO NOTHING
3936
- RETURNING created_on
3937
- )
3938
- SELECT created_on into queue_created_on from q;
3939
-
3940
- IF queue_created_on IS NULL OR options->>'partition' IS DISTINCT FROM 'true' THEN
3941
- RETURN;
3942
- END IF;
3943
-
3944
- EXECUTE format('CREATE TABLE ${schema}.%I (LIKE ${schema}.job INCLUDING DEFAULTS)', tablename);
3945
-
3946
- EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD PRIMARY KEY (name, id)$cmd$, tablename);
3947
- EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT q_fkey FOREIGN KEY (name) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
3948
- EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT dlq_fkey FOREIGN KEY (dead_letter) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
3949
-
3950
- EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i5 ON ${schema}.job (name, start_after) INCLUDE (priority, created_on, id) WHERE state < 'active'$cmd$, tablename);
3951
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i4 ON ${schema}.job (name, singleton_on, COALESCE(singleton_key, '')) WHERE state <> 'cancelled' AND singleton_on IS NOT NULL$cmd$, tablename);
3952
- EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i7 ON ${schema}.job (name, group_id) WHERE state = 'active' AND group_id IS NOT NULL$cmd$, tablename);
3953
-
3954
- IF options->>'policy' = 'short' THEN
3955
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i1 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = 'created' AND policy = 'short'$cmd$, tablename);
3956
- ELSIF options->>'policy' = 'singleton' THEN
3957
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i2 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = 'active' AND policy = 'singleton'$cmd$, tablename);
3958
- ELSIF options->>'policy' = 'stately' THEN
3959
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i3 ON ${schema}.job (name, state, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'stately'$cmd$, tablename);
3960
- ELSIF options->>'policy' = 'exclusive' THEN
3961
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i6 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'exclusive'$cmd$, tablename);
3962
- END IF;
3963
-
3964
- EXECUTE format('ALTER TABLE ${schema}.%I ADD CONSTRAINT cjc CHECK (name=%L)', tablename, queue_name);
3965
- EXECUTE format('ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.%I FOR VALUES IN (%L)', tablename, queue_name);
3966
- END;
3967
- $$
3968
- LANGUAGE plpgsql;
3969
- `
4282
+ createQueueFn[27](schema)
3970
4283
  ]
3971
4284
  },
3972
4285
  {
@@ -3990,172 +4303,10 @@ function getAll(schema) {
3990
4303
  `ALTER TABLE ${schema}.job ADD COLUMN heartbeat_on timestamp with time zone`,
3991
4304
  `ALTER TABLE ${schema}.job ADD COLUMN heartbeat_seconds int`,
3992
4305
  `ALTER TABLE ${schema}.queue ADD COLUMN heartbeat_seconds int`,
3993
- `
3994
- CREATE OR REPLACE FUNCTION ${schema}.create_queue(queue_name text, options jsonb)
3995
- RETURNS VOID AS
3996
- $$
3997
- DECLARE
3998
- tablename varchar := CASE WHEN options->>'partition' = 'true'
3999
- THEN 'j' || encode(sha224(queue_name::bytea), 'hex')
4000
- ELSE 'job_common'
4001
- END;
4002
- queue_created_on timestamptz;
4003
- BEGIN
4004
-
4005
- WITH q as (
4006
- INSERT INTO ${schema}.queue (
4007
- name,
4008
- policy,
4009
- retry_limit,
4010
- retry_delay,
4011
- retry_backoff,
4012
- retry_delay_max,
4013
- expire_seconds,
4014
- retention_seconds,
4015
- deletion_seconds,
4016
- warning_queued,
4017
- dead_letter,
4018
- partition,
4019
- table_name,
4020
- heartbeat_seconds
4021
- )
4022
- VALUES (
4023
- queue_name,
4024
- options->>'policy',
4025
- COALESCE((options->>'retryLimit')::int, 2),
4026
- COALESCE((options->>'retryDelay')::int, 0),
4027
- COALESCE((options->>'retryBackoff')::bool, false),
4028
- (options->>'retryDelayMax')::int,
4029
- COALESCE((options->>'expireInSeconds')::int, 900),
4030
- COALESCE((options->>'retentionSeconds')::int, 1209600),
4031
- COALESCE((options->>'deleteAfterSeconds')::int, 604800),
4032
- COALESCE((options->>'warningQueueSize')::int, 0),
4033
- options->>'deadLetter',
4034
- COALESCE((options->>'partition')::bool, false),
4035
- tablename,
4036
- (options->>'heartbeatSeconds')::int
4037
- )
4038
- ON CONFLICT DO NOTHING
4039
- RETURNING created_on
4040
- )
4041
- SELECT created_on into queue_created_on from q;
4042
-
4043
- IF queue_created_on IS NULL OR options->>'partition' IS DISTINCT FROM 'true' THEN
4044
- RETURN;
4045
- END IF;
4046
-
4047
- EXECUTE format('CREATE TABLE ${schema}.%I (LIKE ${schema}.job INCLUDING DEFAULTS)', tablename);
4048
-
4049
- EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD PRIMARY KEY (name, id)$cmd$, tablename);
4050
- EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT q_fkey FOREIGN KEY (name) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
4051
- EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT dlq_fkey FOREIGN KEY (dead_letter) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
4052
-
4053
- EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i5 ON ${schema}.job (name, start_after) INCLUDE (priority, created_on, id) WHERE state < 'active'$cmd$, tablename);
4054
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i4 ON ${schema}.job (name, singleton_on, COALESCE(singleton_key, '')) WHERE state <> 'cancelled' AND singleton_on IS NOT NULL$cmd$, tablename);
4055
- EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i7 ON ${schema}.job (name, group_id) WHERE state = 'active' AND group_id IS NOT NULL$cmd$, tablename);
4056
-
4057
- IF options->>'policy' = 'short' THEN
4058
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i1 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = 'created' AND policy = 'short'$cmd$, tablename);
4059
- ELSIF options->>'policy' = 'singleton' THEN
4060
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i2 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = 'active' AND policy = 'singleton'$cmd$, tablename);
4061
- ELSIF options->>'policy' = 'stately' THEN
4062
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i3 ON ${schema}.job (name, state, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'stately'$cmd$, tablename);
4063
- ELSIF options->>'policy' = 'exclusive' THEN
4064
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i6 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'exclusive'$cmd$, tablename);
4065
- ELSIF options->>'policy' = 'key_strict_fifo' THEN
4066
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i8 ON ${schema}.job (name, singleton_key) WHERE state IN ('active', 'retry', 'failed') AND policy = 'key_strict_fifo'$cmd$, tablename);
4067
- EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT job_key_strict_fifo_singleton_key_check CHECK (NOT (policy = 'key_strict_fifo' AND singleton_key IS NULL))$cmd$, tablename);
4068
- END IF;
4069
-
4070
- EXECUTE format('ALTER TABLE ${schema}.%I ADD CONSTRAINT cjc CHECK (name=%L)', tablename, queue_name);
4071
- EXECUTE format('ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.%I FOR VALUES IN (%L)', tablename, queue_name);
4072
- END;
4073
- $$
4074
- LANGUAGE plpgsql;
4075
- `
4306
+ createQueueFn[30](schema)
4076
4307
  ],
4077
4308
  uninstall: [
4078
- `
4079
- CREATE OR REPLACE FUNCTION ${schema}.create_queue(queue_name text, options jsonb)
4080
- RETURNS VOID AS
4081
- $$
4082
- DECLARE
4083
- tablename varchar := CASE WHEN options->>'partition' = 'true'
4084
- THEN 'j' || encode(sha224(queue_name::bytea), 'hex')
4085
- ELSE 'job_common'
4086
- END;
4087
- queue_created_on timestamptz;
4088
- BEGIN
4089
-
4090
- WITH q as (
4091
- INSERT INTO ${schema}.queue (
4092
- name,
4093
- policy,
4094
- retry_limit,
4095
- retry_delay,
4096
- retry_backoff,
4097
- retry_delay_max,
4098
- expire_seconds,
4099
- retention_seconds,
4100
- deletion_seconds,
4101
- warning_queued,
4102
- dead_letter,
4103
- partition,
4104
- table_name
4105
- )
4106
- VALUES (
4107
- queue_name,
4108
- options->>'policy',
4109
- COALESCE((options->>'retryLimit')::int, 2),
4110
- COALESCE((options->>'retryDelay')::int, 0),
4111
- COALESCE((options->>'retryBackoff')::bool, false),
4112
- (options->>'retryDelayMax')::int,
4113
- COALESCE((options->>'expireInSeconds')::int, 900),
4114
- COALESCE((options->>'retentionSeconds')::int, 1209600),
4115
- COALESCE((options->>'deleteAfterSeconds')::int, 604800),
4116
- COALESCE((options->>'warningQueueSize')::int, 0),
4117
- options->>'deadLetter',
4118
- COALESCE((options->>'partition')::bool, false),
4119
- tablename
4120
- )
4121
- ON CONFLICT DO NOTHING
4122
- RETURNING created_on
4123
- )
4124
- SELECT created_on into queue_created_on from q;
4125
-
4126
- IF queue_created_on IS NULL OR options->>'partition' IS DISTINCT FROM 'true' THEN
4127
- RETURN;
4128
- END IF;
4129
-
4130
- EXECUTE format('CREATE TABLE ${schema}.%I (LIKE ${schema}.job INCLUDING DEFAULTS)', tablename);
4131
-
4132
- EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD PRIMARY KEY (name, id)$cmd$, tablename);
4133
- EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT q_fkey FOREIGN KEY (name) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
4134
- EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT dlq_fkey FOREIGN KEY (dead_letter) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
4135
-
4136
- EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i5 ON ${schema}.job (name, start_after) INCLUDE (priority, created_on, id) WHERE state < 'active'$cmd$, tablename);
4137
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i4 ON ${schema}.job (name, singleton_on, COALESCE(singleton_key, '')) WHERE state <> 'cancelled' AND singleton_on IS NOT NULL$cmd$, tablename);
4138
- EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i7 ON ${schema}.job (name, group_id) WHERE state = 'active' AND group_id IS NOT NULL$cmd$, tablename);
4139
-
4140
- IF options->>'policy' = 'short' THEN
4141
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i1 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = 'created' AND policy = 'short'$cmd$, tablename);
4142
- ELSIF options->>'policy' = 'singleton' THEN
4143
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i2 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = 'active' AND policy = 'singleton'$cmd$, tablename);
4144
- ELSIF options->>'policy' = 'stately' THEN
4145
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i3 ON ${schema}.job (name, state, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'stately'$cmd$, tablename);
4146
- ELSIF options->>'policy' = 'exclusive' THEN
4147
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i6 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'exclusive'$cmd$, tablename);
4148
- ELSIF options->>'policy' = 'key_strict_fifo' THEN
4149
- EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i8 ON ${schema}.job (name, singleton_key) WHERE state IN ('active', 'retry', 'failed') AND policy = 'key_strict_fifo'$cmd$, tablename);
4150
- EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT job_key_strict_fifo_singleton_key_check CHECK (NOT (policy = 'key_strict_fifo' AND singleton_key IS NULL))$cmd$, tablename);
4151
- END IF;
4152
-
4153
- EXECUTE format('ALTER TABLE ${schema}.%I ADD CONSTRAINT cjc CHECK (name=%L)', tablename, queue_name);
4154
- EXECUTE format('ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.%I FOR VALUES IN (%L)', tablename, queue_name);
4155
- END;
4156
- $$
4157
- LANGUAGE plpgsql;
4158
- `,
4309
+ createQueueFn[28](schema),
4159
4310
  `ALTER TABLE ${schema}.queue DROP COLUMN heartbeat_seconds`,
4160
4311
  `ALTER TABLE ${schema}.job DROP COLUMN heartbeat_seconds`,
4161
4312
  `ALTER TABLE ${schema}.job DROP COLUMN heartbeat_on`
@@ -4181,182 +4332,35 @@ function getAll(schema) {
4181
4332
  `CREATE INDEX IF NOT EXISTS job_dep_parent_idx ON ${schema}.job_dependency (parent_name, parent_id)`,
4182
4333
  `SELECT ${schema}.job_table_run($cmd$DROP INDEX IF EXISTS ${schema}.job_i5$cmd$)`,
4183
4334
  `SELECT ${schema}.job_table_run($cmd$CREATE INDEX job_i5 ON ${schema}.job (name, start_after) INCLUDE (priority, created_on, id) WHERE state < 'active' AND NOT blocked$cmd$)`,
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
- `
4335
+ createQueueFn[31](schema)
4267
4336
  ],
4268
4337
  uninstall: [
4269
4338
  `DROP INDEX IF EXISTS ${schema}.job_dep_parent_idx`,
4270
4339
  `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
- `,
4340
+ createQueueFn[30](schema),
4354
4341
  `SELECT ${schema}.job_table_run($cmd$DROP INDEX IF EXISTS ${schema}.job_i5$cmd$)`,
4355
4342
  `SELECT ${schema}.job_table_run($cmd$CREATE INDEX job_i5 ON ${schema}.job (name, start_after) INCLUDE (priority, created_on, id) WHERE state < 'active'$cmd$)`,
4356
4343
  `ALTER TABLE ${schema}.job DROP COLUMN pending_dependencies`,
4357
4344
  `ALTER TABLE ${schema}.job DROP COLUMN blocking`,
4358
4345
  `ALTER TABLE ${schema}.job DROP COLUMN blocked`
4359
4346
  ]
4347
+ },
4348
+ {
4349
+ release: "12.21.0",
4350
+ version: 32,
4351
+ previous: 31,
4352
+ install: [
4353
+ `ALTER TABLE ${schema}.queue ADD COLUMN notify boolean NOT NULL DEFAULT false`,
4354
+ createQueueFn[32](schema),
4355
+ `ALTER TABLE ${schema}.queue ADD COLUMN failed_count int NOT NULL DEFAULT 0`,
4356
+ `ALTER TABLE ${schema}.queue ADD COLUMN ready_count int NOT NULL DEFAULT 0`
4357
+ ],
4358
+ uninstall: [
4359
+ `ALTER TABLE ${schema}.queue DROP COLUMN ready_count`,
4360
+ `ALTER TABLE ${schema}.queue DROP COLUMN failed_count`,
4361
+ createQueueFn[31](schema),
4362
+ `ALTER TABLE ${schema}.queue DROP COLUMN notify`
4363
+ ]
4360
4364
  }
4361
4365
  ];
4362
4366
  }
@@ -4364,7 +4368,7 @@ function getAll(schema) {
4364
4368
  //#region ../../src/contractor.ts
4365
4369
  var schemaVersion = {
4366
4370
  name: "pg-boss",
4367
- version: "12.20.0",
4371
+ version: "12.21.0",
4368
4372
  description: "Queueing jobs in Postgres from Node.js like a boss",
4369
4373
  type: "module",
4370
4374
  main: "./dist/index.js",
@@ -4372,18 +4376,18 @@ var schemaVersion = {
4372
4376
  bin: { "pg-boss": "./dist/cli.js" },
4373
4377
  engines: { "node": ">=22.12.0" },
4374
4378
  dependencies: {
4375
- "cron-parser": "^5.5.0",
4376
- "pg": "^8.21.0",
4379
+ "cron-parser": "^5.6.0",
4380
+ "pg": "^8.22.0",
4377
4381
  "serialize-error": "^13.0.1"
4378
4382
  },
4379
4383
  devDependencies: {
4380
- "@electric-sql/pglite": "^0.4.1",
4384
+ "@electric-sql/pglite": "^0.5.3",
4381
4385
  "@prisma/adapter-pg": "^7.8.0",
4382
4386
  "@prisma/client": "^7.8.0",
4383
4387
  "@tsconfig/node-ts": "^23.6.4",
4384
4388
  "@tsconfig/node22": "^22.0.5",
4385
- "@types/luxon": "^3.7.1",
4386
- "@types/node": "^22.19.20",
4389
+ "@types/luxon": "^3.7.2",
4390
+ "@types/node": "^22.20.0",
4387
4391
  "@types/pg": "^8.20.0",
4388
4392
  "@vitest/coverage-v8": "^4.1.2",
4389
4393
  "cli-testlab": "^6.0.1",
@@ -4403,23 +4407,24 @@ var schemaVersion = {
4403
4407
  "build": "npm run clean && tsc --project tsconfig.build.json",
4404
4408
  "clean": "node -e \"fs.rmSync('dist',{recursive:true,force:true})\"",
4405
4409
  "prepublishOnly": "npm install && npm test && npm run build",
4406
- "pretest": "prisma generate --schema=test/prisma/schema.prisma",
4410
+ "pretest": "prisma generate --schema=test/prisma/schema.prisma && npm run tsc",
4407
4411
  "test": "eslint . && vitest run",
4408
4412
  "test:distributed": "cross-env DISTRIBUTED=true npm test",
4413
+ "test:ci": "npm run cover && cross-env DISTRIBUTED=true npm run cover && npm run test:pglite",
4409
4414
  "test:cockroachdb": "cross-env DB_TYPE=cockroachdb COCKROACH_HOST=localhost npm test -- test/distributedDatabaseTest.ts",
4410
4415
  "test:cockroachdb:full": "cross-env DB_TYPE=cockroachdb COCKROACH_HOST=localhost npm test -- --no-file-parallelism",
4411
4416
  "test:yugabytedb:full": "cross-env DB_TYPE=yugabytedb YUGABYTE_HOST=localhost npm test -- --no-file-parallelism",
4412
4417
  "test:citus:full": "cross-env DB_TYPE=citus CITUS_HOST=localhost npm test",
4413
4418
  "test:pglite": "cross-env DB_TYPE=pglite npm test -- --no-file-parallelism",
4414
4419
  "lint:fix": "eslint . --fix",
4415
- "precover": "prisma generate --schema=test/prisma/schema.prisma",
4416
- "cover": "vitest run --coverage",
4420
+ "cover": "npm test -- --coverage",
4417
4421
  "tsc": "tsc --noEmit",
4422
+ "cli": "node ./dist/cli.js",
4418
4423
  "readme": "node ./examples/readme.js",
4419
- "db:migrate": "node --import=tsx -e 'console.log(require(\"./src\").getMigrationPlans())'",
4420
- "db:construct": "node --import=tsx -e 'console.log(require(\"./src\").getConstructionPlans())'"
4424
+ "docs": "npm run docs:dev --prefix docs",
4425
+ "docs:readme": "node ./scripts/sync-readme.js"
4421
4426
  },
4422
- pgboss: { "schema": 31 },
4427
+ pgboss: { "schema": 32 },
4423
4428
  repository: {
4424
4429
  "type": "git",
4425
4430
  "url": "git+https://github.com/timgit/pg-boss.git"
@@ -4445,8 +4450,11 @@ var Contractor = class {
4445
4450
  static constructionPlans(schema = DEFAULT_SCHEMA, options = { createSchema: true }) {
4446
4451
  return create(schema, schemaVersion, options);
4447
4452
  }
4448
- static migrationPlans(schema = DEFAULT_SCHEMA, version = schemaVersion - 1) {
4449
- return migrate(schema, version);
4453
+ static migrationPlans(schema = DEFAULT_SCHEMA, version = schemaVersion - 1, options = {}) {
4454
+ return migrate(schema, version, void 0, void 0, {
4455
+ inlineAsync: true,
4456
+ partitionTables: options.partitionTables
4457
+ });
4450
4458
  }
4451
4459
  static rollbackPlans(schema = DEFAULT_SCHEMA, version = schemaVersion) {
4452
4460
  return rollback(schema, version);
@@ -4741,7 +4749,7 @@ var Worker = class {
4741
4749
  fetch;
4742
4750
  onFetch;
4743
4751
  onError;
4744
- interval;
4752
+ resolveInterval;
4745
4753
  jobs = [];
4746
4754
  createdOn = Date.now();
4747
4755
  state = WORKER_STATES.created;
@@ -4757,7 +4765,7 @@ var Worker = class {
4757
4765
  loopDelayPromise = null;
4758
4766
  beenNotified = false;
4759
4767
  runPromise = null;
4760
- constructor({ id, workId, name, options, interval, fetch, onFetch, onError }) {
4768
+ constructor({ id, workId, name, options, resolveInterval, fetch, onFetch, onError }) {
4761
4769
  this.id = id;
4762
4770
  this.workId = workId;
4763
4771
  this.name = name;
@@ -4765,7 +4773,7 @@ var Worker = class {
4765
4773
  this.fetch = fetch;
4766
4774
  this.onFetch = onFetch;
4767
4775
  this.onError = onError;
4768
- this.interval = interval;
4776
+ this.resolveInterval = resolveInterval;
4769
4777
  }
4770
4778
  start() {
4771
4779
  this.runPromise = this.run();
@@ -4774,11 +4782,13 @@ var Worker = class {
4774
4782
  this.state = WORKER_STATES.active;
4775
4783
  while (!this.stopping) {
4776
4784
  const started = Date.now();
4785
+ let fetchedCount = 0;
4777
4786
  try {
4778
4787
  this.beenNotified = false;
4779
4788
  const jobs = await this.fetch();
4780
4789
  this.lastFetchedOn = Date.now();
4781
4790
  if (jobs) {
4791
+ fetchedCount = jobs.length;
4782
4792
  this.jobs = jobs;
4783
4793
  this.lastJobStartedOn = this.lastFetchedOn;
4784
4794
  await this.onFetch(jobs);
@@ -4793,8 +4803,9 @@ var Worker = class {
4793
4803
  }
4794
4804
  const duration = Date.now() - started;
4795
4805
  this.lastJobDuration = duration;
4796
- if (!this.stopping && !this.beenNotified && this.interval - duration > 100) {
4797
- this.loopDelayPromise = delay(this.interval - duration);
4806
+ const interval = this.resolveInterval(fetchedCount);
4807
+ if (!this.stopping && !this.beenNotified && interval - duration > 100) {
4808
+ this.loopDelayPromise = delay(interval - duration);
4798
4809
  await this.loopDelayPromise;
4799
4810
  this.loopDelayPromise = null;
4800
4811
  }
@@ -4927,7 +4938,7 @@ var NUMERIC_QUEUE_FIELDS = [
4927
4938
  "activeCount",
4928
4939
  "totalCount"
4929
4940
  ];
4930
- var events$3 = {
4941
+ var events$4 = {
4931
4942
  error: "error",
4932
4943
  wip: "wip"
4933
4944
  };
@@ -4936,7 +4947,7 @@ function rethrowWriteError(err) {
4936
4947
  throw err;
4937
4948
  }
4938
4949
  var Manager = class extends EventEmitter {
4939
- events = events$3;
4950
+ events = events$4;
4940
4951
  db;
4941
4952
  config;
4942
4953
  wipTs;
@@ -4945,6 +4956,7 @@ var Manager = class extends EventEmitter {
4945
4956
  queueCacheInterval;
4946
4957
  wipInterval;
4947
4958
  timekeeper;
4959
+ notifier;
4948
4960
  queues;
4949
4961
  pendingOffWorkCleanups;
4950
4962
  #spies;
@@ -5013,19 +5025,136 @@ var Manager = class extends EventEmitter {
5013
5025
  const spy = this.config.__test__enableSpies ? this.#spies.get(name) : void 0;
5014
5026
  if (spy) for (const job of jobs) spy.addJob(job.id, name, job.data, "active");
5015
5027
  }
5016
- #trackJobsCompleted(name, jobs, result) {
5028
+ async #trackJobsCompleted(name, jobs, result, affected) {
5017
5029
  const spy = this.config.__test__enableSpies ? this.#spies.get(name) : void 0;
5018
- if (spy) {
5030
+ if (!spy) return;
5031
+ if (affected === jobs.length) {
5019
5032
  const output = jobs.length === 1 ? result : void 0;
5020
5033
  for (const job of jobs) spy.addJob(job.id, name, job.data, "completed", output);
5034
+ return;
5035
+ }
5036
+ for (const job of jobs) {
5037
+ const persisted = await this.getJobById(name, job.id);
5038
+ const state = persisted?.state;
5039
+ if (state === "completed" || state === "failed" || state === "active" || state === "created") spy.addJob(job.id, name, job.data, state, persisted?.output);
5040
+ else if (!persisted) spy.addJob(job.id, name, job.data, "completed", void 0);
5021
5041
  }
5022
5042
  }
5023
- #trackJobsFailed(name, jobs, err) {
5043
+ async #trackJobsFailed(name, jobs, err) {
5024
5044
  const spy = this.config.__test__enableSpies ? this.#spies.get(name) : void 0;
5025
- if (spy) for (const job of jobs) spy.addJob(job.id, name, job.data, "failed", {
5026
- message: err?.message,
5027
- stack: err?.stack
5045
+ if (!spy) return;
5046
+ for (const job of jobs) {
5047
+ const persisted = await this.getJobById(name, job.id);
5048
+ if (persisted?.state === "failed") spy.addJob(job.id, name, job.data, "failed", persisted.output ?? {
5049
+ message: err?.message,
5050
+ stack: err?.stack
5051
+ });
5052
+ }
5053
+ }
5054
+ #trackJobsSettled(name, completed, failed) {
5055
+ const spy = this.config.__test__enableSpies ? this.#spies.get(name) : void 0;
5056
+ if (!spy) return;
5057
+ for (const { job, output } of completed) spy.addJob(job.id, name, job.data, "completed", output);
5058
+ for (const { job, output } of failed) spy.addJob(job.id, name, job.data, "failed", serializeError(output));
5059
+ }
5060
+ async #settlePerJob(name, jobs, result) {
5061
+ if (!Array.isArray(result)) {
5062
+ const err = /* @__PURE__ */ new Error("perJobResults handler must resolve with an array of job results");
5063
+ await this.fail(name, jobs.map((job) => job.id), err);
5064
+ this.#trackJobsFailed(name, jobs, err);
5065
+ return;
5066
+ }
5067
+ const batch = new Map(jobs.map((job) => [job.id, job]));
5068
+ const disposition = /* @__PURE__ */ new Map();
5069
+ for (const item of result) if (item && batch.has(item.id) && (item.status === "completed" || item.status === "failed" || item.status === "deadletter")) disposition.set(item.id, item);
5070
+ const completed = [];
5071
+ const failed = [];
5072
+ const deadLettered = [];
5073
+ for (const job of jobs) {
5074
+ const item = disposition.get(job.id);
5075
+ if (item?.status === "completed") completed.push({
5076
+ job,
5077
+ output: item.output
5078
+ });
5079
+ else if (item?.status === "failed") failed.push({
5080
+ job,
5081
+ output: item.output
5082
+ });
5083
+ else if (item?.status === "deadletter") deadLettered.push({
5084
+ job,
5085
+ output: item.output
5086
+ });
5087
+ else failed.push({
5088
+ job,
5089
+ output: /* @__PURE__ */ new Error("no disposition returned by handler")
5090
+ });
5091
+ }
5092
+ if (completed.length > 0) await this.#completeWithOutputs(name, completed.map((c) => ({
5093
+ id: c.job.id,
5094
+ output: c.output
5095
+ })));
5096
+ if (failed.length > 0) await this.#failWithOutputs(name, failed.map((f) => ({
5097
+ id: f.job.id,
5098
+ output: f.output
5099
+ })));
5100
+ if (deadLettered.length > 0) await this.#failWithOutputs(name, deadLettered.map((d) => ({
5101
+ id: d.job.id,
5102
+ output: d.output
5103
+ })), true);
5104
+ this.#trackJobsSettled(name, completed, [...failed, ...deadLettered]);
5105
+ }
5106
+ async #completeWithOutputs(name, items) {
5107
+ const { table } = await this.getQueueCache(name);
5108
+ const payload = items.map((item) => ({
5109
+ id: item.id,
5110
+ output: this.mapCompletionDataArg(item.output)
5111
+ }));
5112
+ const ids = items.map((item) => item.id);
5113
+ if (this.config.noMultiMutationCte) return this.withDistributedTransaction(this.db, async (tx) => {
5114
+ const sql = completeJobsWithOutputsDistributed(this.config.schema, table);
5115
+ const { rows } = await tx.executeSql(sql, [name, JSON.stringify(payload)]);
5116
+ const blockingIds = rows.filter((row) => row.blocking).map((row) => row.id);
5117
+ if (blockingIds.length > 0) await tx.executeSql(decrementDependents(this.config.schema), [name, blockingIds]);
5118
+ return {
5119
+ jobs: ids,
5120
+ requested: ids.length,
5121
+ affected: rows.length
5122
+ };
5028
5123
  });
5124
+ const sql = completeJobsWithOutputs(this.config.schema, table);
5125
+ const result = await this.db.executeSql(sql, [name, JSON.stringify(payload)]);
5126
+ return this.mapCommandResponse(ids, result);
5127
+ }
5128
+ async #failWithOutputs(name, items, forceTerminal = false) {
5129
+ const { table } = await this.getQueueCache(name);
5130
+ const ids = items.map((item) => item.id);
5131
+ if (this.config.noMultiMutationCte) {
5132
+ const outputById = new Map(items.map((item) => [item.id, this.mapCompletionDataArg(item.output)]));
5133
+ return this.withDistributedTransaction(this.db, async (tx) => {
5134
+ const selectQuery = selectJobsToFailById(this.config.schema, table);
5135
+ const { rows: jobs } = await tx.executeSql(selectQuery.text, [name, ids]);
5136
+ if (jobs.length === 0) return {
5137
+ jobs: ids,
5138
+ requested: ids.length,
5139
+ affected: 0
5140
+ };
5141
+ const deleteQuery = deleteJobsToFail(this.config.schema, table);
5142
+ await tx.executeSql(deleteQuery.text, [name, ids]);
5143
+ const count = await this.reinsertFailedJobs(tx, table, jobs, null, outputById, forceTerminal);
5144
+ return {
5145
+ jobs: ids,
5146
+ requested: ids.length,
5147
+ affected: count
5148
+ };
5149
+ });
5150
+ }
5151
+ const payload = items.map((item) => ({
5152
+ id: item.id,
5153
+ output: this.mapCompletionDataArg(item.output)
5154
+ }));
5155
+ const sql = forceTerminal ? deadLetterJobsByIdWithOutputs(this.config.schema, table) : failJobsByIdWithOutputs(this.config.schema, table);
5156
+ const result = await this.db.executeSql(sql, [name, JSON.stringify(payload)]);
5157
+ return this.mapCommandResponse(ids, result);
5029
5158
  }
5030
5159
  #storeLocalGroupConfig(name, localGroupConcurrency) {
5031
5160
  const config = typeof localGroupConcurrency === "number" ? { default: localGroupConcurrency } : localGroupConcurrency;
@@ -5063,7 +5192,7 @@ var Manager = class extends EventEmitter {
5063
5192
  #trackLocalGroupEnd(name, groupedJobs) {
5064
5193
  for (const job of groupedJobs) if (job.groupId) this.#decrementLocalGroupCount(name, job.groupId);
5065
5194
  }
5066
- async #processJobs(name, jobs, callback, worker, heartbeatRefreshSeconds) {
5195
+ async #processJobs(name, jobs, callback, worker, heartbeatRefreshSeconds, perJobResults = false) {
5067
5196
  const jobIds = jobs.map((job) => job.id);
5068
5197
  const maxExpiration = jobs.reduce((acc, i) => Math.max(acc, i.expireInSeconds), 0);
5069
5198
  const heartbeatSeconds = jobs.reduce((acc, j) => Math.max(acc, j.heartbeatSeconds || 0), 0);
@@ -5079,21 +5208,34 @@ var Manager = class extends EventEmitter {
5079
5208
  try {
5080
5209
  await this.touch(name, jobIds);
5081
5210
  } catch (err) {
5082
- this.emit(events$3.error, err);
5211
+ this.emit(events$4.error, err);
5083
5212
  }
5084
5213
  }, intervalMs);
5085
5214
  }
5215
+ let completedResult;
5216
+ let completedAffected = 0;
5217
+ let failedError;
5218
+ let didFail = false;
5086
5219
  try {
5087
5220
  const result = await resolveWithinSeconds(callback(jobs), maxExpiration, `handler execution exceeded ${maxExpiration}s`, ac);
5088
- await this.complete(name, jobIds, jobIds.length === 1 ? result : void 0);
5089
- this.#trackJobsCompleted(name, jobs, result);
5221
+ if (perJobResults) await this.#settlePerJob(name, jobs, result);
5222
+ else {
5223
+ const completion = await this.complete(name, jobIds, jobIds.length === 1 ? result : void 0);
5224
+ completedResult = result;
5225
+ completedAffected = completion.affected;
5226
+ }
5090
5227
  } catch (err) {
5091
5228
  await this.fail(name, jobIds, err);
5092
- this.#trackJobsFailed(name, jobs, err);
5229
+ failedError = err;
5230
+ didFail = true;
5093
5231
  } finally {
5094
5232
  if (heartbeatTimer) clearInterval(heartbeatTimer);
5095
5233
  if (worker) worker.abortController = null;
5096
5234
  }
5235
+ if (this.config.__test__enableSpies && this.#spies.has(name)) {
5236
+ if (didFail) await this.#trackJobsFailed(name, jobs, failedError);
5237
+ else if (!perJobResults) await this.#trackJobsCompleted(name, jobs, completedResult, completedAffected);
5238
+ }
5097
5239
  }
5098
5240
  async start() {
5099
5241
  this.stopped = false;
@@ -5103,7 +5245,7 @@ var Manager = class extends EventEmitter {
5103
5245
  if (now - this.wipTs < 2e3) return;
5104
5246
  const wip = this.getWipData();
5105
5247
  if (wip.some((w) => w.count > 0)) {
5106
- this.emit(events$3.wip, wip);
5248
+ this.emit(events$4.wip, wip);
5107
5249
  this.wipTs = now;
5108
5250
  }
5109
5251
  }, 2e3);
@@ -5118,7 +5260,7 @@ var Manager = class extends EventEmitter {
5118
5260
  return acc;
5119
5261
  }, {});
5120
5262
  } catch (error) {
5121
- emit && this.emit(events$3.error, {
5263
+ emit && this.emit(events$4.error, {
5122
5264
  ...error,
5123
5265
  message: error.message,
5124
5266
  stack: error.stack
@@ -5153,9 +5295,15 @@ var Manager = class extends EventEmitter {
5153
5295
  async work(name, ...args) {
5154
5296
  const { options, callback } = checkWorkArgs(name, args);
5155
5297
  if (this.stopped) throw new Error("Workers are disabled. pg-boss is stopped");
5156
- const { pollingInterval: interval, batchSize = 1, includeMetadata = false, priority = true, localConcurrency = 1, localGroupConcurrency, groupConcurrency, orderByCreatedOn = true, heartbeatRefreshSeconds, minPriority, maxPriority } = options;
5298
+ const { pollingInterval: interval, notifyPollingInterval: notifyInterval, burstWhenReadyExceeds, burstWhenBatchFull = false, batchSize = 1, includeMetadata = false, priority = true, localConcurrency = 1, localGroupConcurrency, groupConcurrency, orderByCreatedOn = true, heartbeatRefreshSeconds, minPriority, maxPriority, perJobResults = false } = options;
5157
5299
  if (localGroupConcurrency != null) this.#storeLocalGroupConfig(name, localGroupConcurrency);
5158
5300
  const firstWorkerId = randomUUID({ disableEntropyCache: true });
5301
+ const isNotifyActive = () => !!(this.notifier?.available && this.queues?.[name]?.notify);
5302
+ const getReadyCount = () => this.queues?.[name]?.readyCount ?? 0;
5303
+ const resolveInterval = (lastFetchCount) => {
5304
+ if (lastFetchCount >= batchSize && (burstWhenReadyExceeds !== void 0 && getReadyCount() > burstWhenReadyExceeds || burstWhenBatchFull && batchSize > 1)) return 0;
5305
+ return isNotifyActive() ? notifyInterval : interval;
5306
+ };
5159
5307
  const createWorker = (workerId, workId) => {
5160
5308
  const fetch = () => {
5161
5309
  const ignoreGroups = localGroupConcurrency != null ? this.#getGroupsAtLocalCapacity(name) : void 0;
@@ -5176,7 +5324,7 @@ var Manager = class extends EventEmitter {
5176
5324
  this.emitWip(name);
5177
5325
  this.#trackJobsActive(name, jobs);
5178
5326
  const worker = this.workers.get(workerId);
5179
- if (localGroupConcurrency == null) await this.#processJobs(name, jobs, callback, worker, heartbeatRefreshSeconds);
5327
+ if (localGroupConcurrency == null) await this.#processJobs(name, jobs, callback, worker, heartbeatRefreshSeconds, perJobResults);
5180
5328
  else {
5181
5329
  const { allowed, excess, groupedJobs } = this.#trackLocalGroupStart(name, jobs);
5182
5330
  if (excess.length > 0) {
@@ -5184,7 +5332,7 @@ var Manager = class extends EventEmitter {
5184
5332
  await this.restore(name, excessIds);
5185
5333
  }
5186
5334
  if (allowed.length > 0) try {
5187
- await this.#processJobs(name, allowed, callback, worker, heartbeatRefreshSeconds);
5335
+ await this.#processJobs(name, allowed, callback, worker, heartbeatRefreshSeconds, perJobResults);
5188
5336
  } finally {
5189
5337
  this.#trackLocalGroupEnd(name, groupedJobs);
5190
5338
  }
@@ -5192,7 +5340,7 @@ var Manager = class extends EventEmitter {
5192
5340
  this.emitWip(name);
5193
5341
  };
5194
5342
  const onError = (error) => {
5195
- this.emit(events$3.error, {
5343
+ this.emit(events$4.error, {
5196
5344
  ...error,
5197
5345
  message: error.message,
5198
5346
  stack: error.stack,
@@ -5205,7 +5353,7 @@ var Manager = class extends EventEmitter {
5205
5353
  workId,
5206
5354
  name,
5207
5355
  options,
5208
- interval,
5356
+ resolveInterval,
5209
5357
  fetch,
5210
5358
  onFetch,
5211
5359
  onError
@@ -5231,7 +5379,7 @@ var Manager = class extends EventEmitter {
5231
5379
  if (!INTERNAL_QUEUES[name]) {
5232
5380
  const now = Date.now();
5233
5381
  if (now - this.wipTs > 2e3) {
5234
- this.emit(events$3.wip, this.getWipData());
5382
+ this.emit(events$4.wip, this.getWipData());
5235
5383
  this.wipTs = now;
5236
5384
  }
5237
5385
  }
@@ -5267,6 +5415,15 @@ var Manager = class extends EventEmitter {
5267
5415
  notifyWorker(workerId) {
5268
5416
  this.workers.get(workerId)?.notify();
5269
5417
  }
5418
+ #notifyEnabled(queueNotify) {
5419
+ return !!queueNotify && !this.config.noListenNotify;
5420
+ }
5421
+ notifyQueue(name) {
5422
+ for (const worker of this.workers.values()) if (worker.name === name) worker.notify();
5423
+ }
5424
+ forceFetchLnWorkers() {
5425
+ for (const worker of this.workers.values()) if (this.queues?.[worker.name]?.notify) worker.notify();
5426
+ }
5270
5427
  async subscribe(event, name) {
5271
5428
  assert(event, "Missing required argument");
5272
5429
  assert(name, "Missing required argument");
@@ -5349,12 +5506,13 @@ var Manager = class extends EventEmitter {
5349
5506
  deadLetter
5350
5507
  };
5351
5508
  const db = wrapper || this.db;
5352
- const { table, policy } = await this.getQueueCache(name);
5509
+ const { table, policy, notify } = await this.getQueueCache(name);
5353
5510
  if (policy === QUEUE_POLICIES.key_strict_fifo && !singletonKey) throw new Error(`${QUEUE_POLICIES.key_strict_fifo} queues require a singletonKey`);
5354
5511
  const sql = insertJobs(this.config.schema, {
5355
5512
  table,
5356
5513
  name,
5357
- returnId: true
5514
+ returnId: true,
5515
+ notify: this.#notifyEnabled(notify)
5358
5516
  });
5359
5517
  const { rows: try1 } = await db.executeSql(sql, [JSON.stringify([job])]);
5360
5518
  if (try1.length === 1) {
@@ -5382,7 +5540,7 @@ var Manager = class extends EventEmitter {
5382
5540
  }
5383
5541
  async insert(name, jobs, options = {}) {
5384
5542
  assert(Array.isArray(jobs), "jobs argument should be an array");
5385
- const { table, policy } = await this.getQueueCache(name);
5543
+ const { table, policy, notify } = await this.getQueueCache(name);
5386
5544
  if (policy === QUEUE_POLICIES.key_strict_fifo) {
5387
5545
  for (const job of jobs) if (!job.singletonKey) throw new Error(`${QUEUE_POLICIES.key_strict_fifo} queues require a singletonKey`);
5388
5546
  }
@@ -5396,7 +5554,8 @@ var Manager = class extends EventEmitter {
5396
5554
  const sql = insertJobs(this.config.schema, {
5397
5555
  table,
5398
5556
  name,
5399
- returnId
5557
+ returnId,
5558
+ notify: this.#notifyEnabled(notify)
5400
5559
  });
5401
5560
  const { rows } = await db.executeSql(sql, [JSON.stringify(insertPayload)]);
5402
5561
  if (rows.length) {
@@ -5443,7 +5602,7 @@ var Manager = class extends EventEmitter {
5443
5602
  }
5444
5603
  const statements = [];
5445
5604
  for (const [queueName, queueJobs] of byQueue) {
5446
- const { table } = await this.getQueueCache(queueName);
5605
+ const { table, notify } = await this.getQueueCache(queueName);
5447
5606
  const insertPayload = queueJobs.map((j) => {
5448
5607
  const dependencyCount = dependencyCountByRef.get(j.ref) ?? 0;
5449
5608
  return {
@@ -5470,19 +5629,13 @@ var Manager = class extends EventEmitter {
5470
5629
  pendingDependencies: dependencyCount || void 0
5471
5630
  };
5472
5631
  });
5473
- const insertSql = insertJobs(this.config.schema, {
5632
+ statements.push(insertFlowJobs(this.config.schema, {
5474
5633
  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
- `);
5634
+ name: queueName
5635
+ }, insertPayload));
5636
+ if (this.#notifyEnabled(notify)) statements.push(notifyQueue(this.config.schema, queueName));
5484
5637
  }
5485
- if (depRows.length > 0) statements.push(insertDependencies(this.config.schema).replace("$1", () => serializeJsonParam(depRows)));
5638
+ if (depRows.length > 0) statements.push(insertDependencies(this.config.schema, depRows));
5486
5639
  const db = options.db ?? this.db;
5487
5640
  const sql = options.db ? statements.join(";\n") : transaction(statements);
5488
5641
  try {
@@ -5632,16 +5785,17 @@ var Manager = class extends EventEmitter {
5632
5785
  return this.reinsertFailedJobs(tx, table, jobs, outputData);
5633
5786
  });
5634
5787
  }
5635
- async reinsertFailedJobs(tx, table, jobs, outputData) {
5788
+ async reinsertFailedJobs(tx, table, jobs, outputData, outputById, forceTerminal = false) {
5636
5789
  const insertSql = insertRetryJob(this.config.schema, table);
5637
5790
  const dlqSql = insertDeadLetterJob(this.config.schema);
5638
5791
  let count = 0;
5639
5792
  for (const job of jobs) {
5793
+ const jobOutput = outputById ? outputById.get(job.id) ?? null : outputData;
5640
5794
  const retryCount = Number(job.retry_count);
5641
5795
  const retryLimit = Number(job.retry_limit);
5642
5796
  const retryDelay = Number(job.retry_delay);
5643
5797
  const retryDelayMax = job.retry_delay_max != null ? Number(job.retry_delay_max) : null;
5644
- const canRetry = retryCount < retryLimit;
5798
+ const canRetry = !forceTerminal && retryCount < retryLimit;
5645
5799
  let retried = false;
5646
5800
  if (canRetry) {
5647
5801
  let startAfter = job.start_after;
@@ -5675,7 +5829,7 @@ var Manager = class extends EventEmitter {
5675
5829
  null,
5676
5830
  job.keep_until,
5677
5831
  job.policy,
5678
- outputData,
5832
+ jobOutput,
5679
5833
  job.dead_letter,
5680
5834
  null,
5681
5835
  job.heartbeat_seconds,
@@ -5709,7 +5863,7 @@ var Manager = class extends EventEmitter {
5709
5863
  /* @__PURE__ */ new Date(),
5710
5864
  job.keep_until,
5711
5865
  job.policy,
5712
- outputData,
5866
+ jobOutput,
5713
5867
  job.dead_letter,
5714
5868
  null,
5715
5869
  job.heartbeat_seconds,
@@ -5720,7 +5874,7 @@ var Manager = class extends EventEmitter {
5720
5874
  if (job.dead_letter) await tx.executeSql(dlqSql, [
5721
5875
  job.dead_letter,
5722
5876
  job.data,
5723
- outputData
5877
+ jobOutput
5724
5878
  ]);
5725
5879
  }
5726
5880
  count++;
@@ -5880,7 +6034,9 @@ var Manager = class extends EventEmitter {
5880
6034
  return Object.assign(queue, stats || {
5881
6035
  deferredCount: 0,
5882
6036
  queuedCount: 0,
6037
+ readyCount: 0,
5883
6038
  activeCount: 0,
6039
+ failedCount: 0,
5884
6040
  totalCount: 0
5885
6041
  });
5886
6042
  }
@@ -5943,7 +6099,7 @@ var Manager = class extends EventEmitter {
5943
6099
  };
5944
6100
  //#endregion
5945
6101
  //#region ../../src/boss.ts
5946
- var events$2 = {
6102
+ var events$3 = {
5947
6103
  error: "error",
5948
6104
  warning: "warning"
5949
6105
  };
@@ -5969,7 +6125,7 @@ var Boss = class extends EventEmitter {
5969
6125
  #db;
5970
6126
  #config;
5971
6127
  #manager;
5972
- events = events$2;
6128
+ events = events$3;
5973
6129
  constructor(db, manager, config) {
5974
6130
  super();
5975
6131
  this.#db = db;
@@ -6004,8 +6160,8 @@ var Boss = class extends EventEmitter {
6004
6160
  db: this.#db,
6005
6161
  schema: this.#config.schema,
6006
6162
  persistWarnings: this.#config.persistWarnings,
6007
- warningEvent: events$2.warning,
6008
- errorEvent: events$2.error
6163
+ warningEvent: events$3.warning,
6164
+ errorEvent: events$3.error
6009
6165
  };
6010
6166
  }
6011
6167
  async #executeQuery(query) {
@@ -6034,7 +6190,7 @@ var Boss = class extends EventEmitter {
6034
6190
  !this.#stopped && await this.supervise(queues);
6035
6191
  !this.#stopped && await this.#maintainWarnings();
6036
6192
  } catch (err) {
6037
- this.emit(events$2.error, err);
6193
+ this.emit(events$3.error, err);
6038
6194
  } finally {
6039
6195
  this.#maintaining = false;
6040
6196
  }
@@ -6112,7 +6268,7 @@ var Boss = class extends EventEmitter {
6112
6268
  };
6113
6269
  //#endregion
6114
6270
  //#region ../../src/bam.ts
6115
- var events$1 = {
6271
+ var events$2 = {
6116
6272
  error: "error",
6117
6273
  bam: "bam"
6118
6274
  };
@@ -6122,7 +6278,7 @@ var Bam = class extends EventEmitter {
6122
6278
  #pollInterval;
6123
6279
  #db;
6124
6280
  #config;
6125
- events = events$1;
6281
+ events = events$2;
6126
6282
  constructor(db, config) {
6127
6283
  super();
6128
6284
  this.#db = db;
@@ -6158,7 +6314,7 @@ var Bam = class extends EventEmitter {
6158
6314
  const { rows } = await this.#db.executeSql(sql);
6159
6315
  if (rows.length === 1) await this.#processCommands();
6160
6316
  } catch (err) {
6161
- this.emit(events$1.error, err);
6317
+ this.emit(events$2.error, err);
6162
6318
  } finally {
6163
6319
  this.#working = false;
6164
6320
  }
@@ -6167,7 +6323,7 @@ var Bam = class extends EventEmitter {
6167
6323
  if (this.#stopped) return;
6168
6324
  const entry = await this.#getNextCommand();
6169
6325
  if (!entry || this.#stopped) return;
6170
- this.emit(events$1.bam, {
6326
+ this.emit(events$2.bam, {
6171
6327
  id: entry.id,
6172
6328
  name: entry.name,
6173
6329
  status: "in_progress",
@@ -6178,7 +6334,7 @@ var Bam = class extends EventEmitter {
6178
6334
  await this.#db.executeSql(entry.command);
6179
6335
  if (this.#stopped) return;
6180
6336
  await this.#markCompleted(entry.id);
6181
- this.emit(events$1.bam, {
6337
+ this.emit(events$2.bam, {
6182
6338
  id: entry.id,
6183
6339
  name: entry.name,
6184
6340
  status: "completed",
@@ -6188,8 +6344,8 @@ var Bam = class extends EventEmitter {
6188
6344
  } catch (err) {
6189
6345
  if (this.#stopped) return;
6190
6346
  await this.#markFailed(entry.id, err);
6191
- this.emit(events$1.error, err);
6192
- this.emit(events$1.bam, {
6347
+ this.emit(events$2.error, err);
6348
+ this.emit(events$2.bam, {
6193
6349
  id: entry.id,
6194
6350
  name: entry.name,
6195
6351
  status: "failed",
@@ -6214,6 +6370,76 @@ var Bam = class extends EventEmitter {
6214
6370
  }
6215
6371
  };
6216
6372
  //#endregion
6373
+ //#region ../../src/notifier.ts
6374
+ var events$1 = {
6375
+ error: "error",
6376
+ warning: "warning"
6377
+ };
6378
+ var WARNING_TYPE = "listen_notify_unavailable";
6379
+ var Notifier = class extends EventEmitter {
6380
+ events = events$1;
6381
+ #db;
6382
+ #manager;
6383
+ #config;
6384
+ #handle = null;
6385
+ #stopped = true;
6386
+ constructor(db, manager, config) {
6387
+ super();
6388
+ this.#db = db;
6389
+ this.#manager = manager;
6390
+ this.#config = config;
6391
+ }
6392
+ get available() {
6393
+ return this.#handle !== null;
6394
+ }
6395
+ async start() {
6396
+ if (!this.#stopped) return;
6397
+ this.#stopped = false;
6398
+ if (this.#config.noListenNotify) {
6399
+ this.emit(events$1.warning, {
6400
+ message: `useListenNotify is not supported on the ${this.#config.backend} backend. Continuing with polling only.`,
6401
+ data: {
6402
+ type: WARNING_TYPE,
6403
+ backend: this.#config.backend
6404
+ }
6405
+ });
6406
+ return;
6407
+ }
6408
+ if (typeof this.#db.listen !== "function") {
6409
+ this.emit(events$1.warning, {
6410
+ message: "useListenNotify is enabled but the database connection does not support LISTEN/NOTIFY. Continuing with polling only.",
6411
+ data: { type: WARNING_TYPE }
6412
+ });
6413
+ return;
6414
+ }
6415
+ try {
6416
+ const { rows } = await this.#db.executeSql(`SELECT ${notifyChannelSql(this.#config.schema)} AS channel`);
6417
+ const channel = rows[0].channel;
6418
+ this.#handle = await this.#db.listen(channel, (payload) => this.#manager.notifyQueue(payload), () => this.#manager.forceFetchLnWorkers());
6419
+ } catch (err) {
6420
+ this.emit(events$1.warning, {
6421
+ message: "Failed to start LISTEN/NOTIFY listener. Continuing with polling only.",
6422
+ data: {
6423
+ type: WARNING_TYPE,
6424
+ error: err?.message
6425
+ }
6426
+ });
6427
+ }
6428
+ }
6429
+ async stop() {
6430
+ if (this.#stopped) return;
6431
+ this.#stopped = true;
6432
+ if (this.#handle) {
6433
+ try {
6434
+ await this.#handle.close();
6435
+ } catch (err) {
6436
+ this.emit(events$1.error, err);
6437
+ }
6438
+ this.#handle = null;
6439
+ }
6440
+ }
6441
+ };
6442
+ //#endregion
6217
6443
  //#region ../../src/db.ts
6218
6444
  var Db = class extends EventEmitter {
6219
6445
  pool;
@@ -6245,6 +6471,63 @@ var Db = class extends EventEmitter {
6245
6471
  assert(this.opened, "Database not opened. Call open() before executing SQL.");
6246
6472
  return await this.pool.query(text, values);
6247
6473
  }
6474
+ async listen(channel, onNotification, onReconnect) {
6475
+ assert(this.opened, "Database not opened. Call open() before listening.");
6476
+ let closed = false;
6477
+ let client = null;
6478
+ let reconnectTimer = null;
6479
+ let attempt = 0;
6480
+ const scheduleReconnect = () => {
6481
+ if (closed || reconnectTimer) return;
6482
+ const backoff = Math.min(3e4, 1e3 * 2 ** Math.min(attempt, 5));
6483
+ attempt++;
6484
+ reconnectTimer = setTimeout(() => {
6485
+ reconnectTimer = null;
6486
+ connect().catch(() => scheduleReconnect());
6487
+ }, backoff);
6488
+ };
6489
+ const connect = async () => {
6490
+ if (closed) return;
6491
+ const next = new pg.Client(this.config);
6492
+ next.on("error", (error) => {
6493
+ this.emit("error", error);
6494
+ if (!closed) {
6495
+ next.removeAllListeners();
6496
+ next.end().catch(() => {});
6497
+ if (client === next) client = null;
6498
+ scheduleReconnect();
6499
+ }
6500
+ });
6501
+ next.on("notification", (msg) => {
6502
+ if (msg.payload !== void 0) onNotification(msg.payload);
6503
+ });
6504
+ client = next;
6505
+ try {
6506
+ await next.connect();
6507
+ await next.query(`LISTEN "${channel}"`);
6508
+ } catch (err) {
6509
+ next.removeAllListeners();
6510
+ await next.end().catch(() => {});
6511
+ if (client === next) client = null;
6512
+ throw err;
6513
+ }
6514
+ attempt = 0;
6515
+ onReconnect();
6516
+ };
6517
+ await connect();
6518
+ return { close: async () => {
6519
+ closed = true;
6520
+ if (reconnectTimer) {
6521
+ clearTimeout(reconnectTimer);
6522
+ reconnectTimer = null;
6523
+ }
6524
+ if (client) {
6525
+ client.removeAllListeners();
6526
+ await client.end().catch(() => {});
6527
+ client = null;
6528
+ }
6529
+ } };
6530
+ }
6248
6531
  async withTransaction(fn) {
6249
6532
  assert(this.opened, "Database not opened. Call open() before executing SQL.");
6250
6533
  const client = await this.pool.connect();
@@ -6282,6 +6565,7 @@ var PgBoss = class extends EventEmitter {
6282
6565
  #manager;
6283
6566
  #timekeeper;
6284
6567
  #bam;
6568
+ #notifier;
6285
6569
  constructor(value) {
6286
6570
  super();
6287
6571
  this.#stoppingOn = null;
@@ -6297,15 +6581,19 @@ var PgBoss = class extends EventEmitter {
6297
6581
  const timekeeper = new Timekeeper(db, manager, config);
6298
6582
  manager.timekeeper = timekeeper;
6299
6583
  const bam = new Bam(db, config);
6584
+ const notifier = new Notifier(db, manager, config);
6585
+ manager.notifier = notifier;
6300
6586
  this.#promoteEvents(manager);
6301
6587
  this.#promoteEvents(boss);
6302
6588
  this.#promoteEvents(timekeeper);
6303
6589
  this.#promoteEvents(bam);
6590
+ this.#promoteEvents(notifier);
6304
6591
  this.#boss = boss;
6305
6592
  this.#contractor = contractor;
6306
6593
  this.#manager = manager;
6307
6594
  this.#timekeeper = timekeeper;
6308
6595
  this.#bam = bam;
6596
+ this.#notifier = notifier;
6309
6597
  }
6310
6598
  #promoteEvents(emitter) {
6311
6599
  for (const event of Object.values(emitter?.events)) emitter.on(event, (arg) => this.emit(event, arg));
@@ -6319,6 +6607,7 @@ var PgBoss = class extends EventEmitter {
6319
6607
  if (this.#config.migrate) await this.#contractor.start();
6320
6608
  else await this.#contractor.check();
6321
6609
  await this.#manager.start();
6610
+ if (this.#config.useListenNotify) await this.#notifier.start();
6322
6611
  if (this.#config.supervise) await this.#boss.start();
6323
6612
  if (this.#config.schedule) await this.#timekeeper.start();
6324
6613
  if (this.#config.migrate) await this.#bam.start();
@@ -6348,6 +6637,7 @@ var PgBoss = class extends EventEmitter {
6348
6637
  let { close = true, graceful = true, timeout = 3e4 } = options;
6349
6638
  timeout = Math.max(timeout, 1e3);
6350
6639
  this.#stoppingOn = Date.now();
6640
+ await this.#notifier.stop();
6351
6641
  await this.#manager.stop();
6352
6642
  await this.#timekeeper.stop();
6353
6643
  await this.#boss.stop();
@@ -6514,7 +6804,7 @@ var PgBoss = class extends EventEmitter {
6514
6804
  return rows;
6515
6805
  }
6516
6806
  async getBamEntries() {
6517
- const sql = getBamEntries(this.#config.schema);
6807
+ const sql = getBamEntries$1(this.#config.schema);
6518
6808
  const { rows } = await this.#db.executeSql(sql);
6519
6809
  return rows;
6520
6810
  }
@@ -6609,7 +6899,9 @@ var QUEUE_COLUMNS = `
6609
6899
  deletion_seconds as "deleteAfterSeconds",
6610
6900
  deferred_count as "deferredCount",
6611
6901
  queued_count as "queuedCount",
6902
+ GREATEST(queued_count - deferred_count, 0) as "readyCount",
6612
6903
  active_count as "activeCount",
6904
+ failed_count as "failedCount",
6613
6905
  total_count as "totalCount",
6614
6906
  warning_queued as "warningQueueSize",
6615
6907
  singletons_active as "singletonsActive",
@@ -6862,19 +7154,81 @@ async function getWarningCount(dbUrl, schema, type) {
6862
7154
  throw err;
6863
7155
  }
6864
7156
  }
7157
+ async function getBamEntries(dbUrl, schema, options = {}) {
7158
+ const s = validateIdentifier(schema);
7159
+ const { status = null, limit = 200, offset = 0 } = options;
7160
+ const sql = `
7161
+ SELECT
7162
+ id,
7163
+ name,
7164
+ version,
7165
+ status,
7166
+ queue,
7167
+ table_name as "table",
7168
+ command,
7169
+ error,
7170
+ created_on as "createdOn",
7171
+ started_on as "startedOn",
7172
+ completed_on as "completedOn"
7173
+ FROM ${s}.bam
7174
+ WHERE ($1::text IS NULL OR status = $1)
7175
+ ORDER BY version DESC, created_on DESC
7176
+ LIMIT $2 OFFSET $3
7177
+ `;
7178
+ try {
7179
+ return await query(dbUrl, sql, [
7180
+ status,
7181
+ limit,
7182
+ offset
7183
+ ]);
7184
+ } catch (err) {
7185
+ if (err && typeof err === "object" && "code" in err && err.code === "42P01") return [];
7186
+ throw err;
7187
+ }
7188
+ }
7189
+ async function getBamCount(dbUrl, schema, status) {
7190
+ const sql = `
7191
+ SELECT COUNT(*)::int as count
7192
+ FROM ${validateIdentifier(schema)}.bam
7193
+ WHERE ($1::text IS NULL OR status = $1)
7194
+ `;
7195
+ try {
7196
+ return (await queryOne(dbUrl, sql, [status ?? null]))?.count ?? 0;
7197
+ } catch (err) {
7198
+ if (err && typeof err === "object" && "code" in err && err.code === "42P01") return 0;
7199
+ throw err;
7200
+ }
7201
+ }
7202
+ async function getBamStatusSummary(dbUrl, schema) {
7203
+ const sql = `
7204
+ SELECT status, count(*)::int as count, max(created_on) as "lastCreatedOn"
7205
+ FROM ${validateIdentifier(schema)}.bam
7206
+ GROUP BY status
7207
+ `;
7208
+ try {
7209
+ return await query(dbUrl, sql);
7210
+ } catch (err) {
7211
+ if (err && typeof err === "object" && "code" in err && err.code === "42P01") return [];
7212
+ throw err;
7213
+ }
7214
+ }
6865
7215
  async function getQueueStats(dbUrl, schema) {
6866
7216
  return await queryOne(dbUrl, `
6867
7217
  SELECT
6868
7218
  COALESCE(SUM(deferred_count), 0)::int as "totalDeferred",
6869
7219
  COALESCE(SUM(queued_count), 0)::int as "totalQueued",
7220
+ COALESCE(SUM(GREATEST(queued_count - deferred_count, 0)), 0)::int as "totalReady",
6870
7221
  COALESCE(SUM(active_count), 0)::int as "totalActive",
7222
+ COALESCE(SUM(failed_count), 0)::int as "totalFailed",
6871
7223
  COALESCE(SUM(total_count), 0)::int as "totalJobs",
6872
7224
  COUNT(*)::int as "queueCount"
6873
7225
  FROM ${validateIdentifier(schema)}.queue
6874
7226
  `) ?? {
6875
7227
  totalDeferred: 0,
6876
7228
  totalQueued: 0,
7229
+ totalReady: 0,
6877
7230
  totalActive: 0,
7231
+ totalFailed: 0,
6878
7232
  totalJobs: 0,
6879
7233
  queueCount: 0
6880
7234
  };
@@ -6975,9 +7329,21 @@ var statCards = [
6975
7329
  {
6976
7330
  name: "Queued Jobs",
6977
7331
  key: "totalQueued",
6978
- hint: "waiting to process",
7332
+ hint: "incl. deferred",
7333
+ accent: "neutral"
7334
+ },
7335
+ {
7336
+ name: "Deferred",
7337
+ key: "totalDeferred",
7338
+ hint: "scheduled for later",
6979
7339
  accent: "neutral"
6980
7340
  },
7341
+ {
7342
+ name: "Ready",
7343
+ key: "totalReady",
7344
+ hint: "ready to process",
7345
+ accent: "primary"
7346
+ },
6981
7347
  {
6982
7348
  name: "Active",
6983
7349
  key: "totalActive",
@@ -6985,9 +7351,9 @@ var statCards = [
6985
7351
  accent: "primary"
6986
7352
  },
6987
7353
  {
6988
- name: "Deferred",
6989
- key: "totalDeferred",
6990
- hint: "scheduled for later",
7354
+ name: "Failed",
7355
+ key: "totalFailed",
7356
+ hint: "recent failures",
6991
7357
  accent: "neutral"
6992
7358
  },
6993
7359
  {
@@ -7199,34 +7565,45 @@ function ErrorCard({ title, message = "Please check your database connection and
7199
7565
  //#endregion
7200
7566
  //#region app/routes/_index.tsx
7201
7567
  var _index_exports = /* @__PURE__ */ __exportAll({
7202
- ErrorBoundary: () => ErrorBoundary$10,
7568
+ ErrorBoundary: () => ErrorBoundary$11,
7203
7569
  default: () => _index_default,
7204
- loader: () => loader$10
7570
+ loader: () => loader$11
7205
7571
  });
7206
- async function loader$10({ context }) {
7572
+ async function loader$11({ context }) {
7207
7573
  const { DB_URL, SCHEMA } = context.get(dbContext);
7208
- const [warnings, stats, topQueues, totalQueues, problemQueuesCount] = await Promise.all([
7574
+ const [warnings, stats, topQueues, totalQueues, problemQueuesCount, bamSummary] = await Promise.all([
7209
7575
  getWarnings(DB_URL, SCHEMA, { limit: 5 }),
7210
7576
  getQueueStats(DB_URL, SCHEMA),
7211
7577
  getTopQueues(DB_URL, SCHEMA, 5),
7212
7578
  getQueueCount(DB_URL, SCHEMA),
7213
- getProblemQueuesCount(DB_URL, SCHEMA)
7579
+ getProblemQueuesCount(DB_URL, SCHEMA),
7580
+ getBamStatusSummary(DB_URL, SCHEMA)
7214
7581
  ]);
7215
7582
  return {
7216
7583
  stats,
7217
7584
  warnings,
7218
7585
  topQueues,
7586
+ migrations: bamSummary.reduce((acc, row) => {
7587
+ if (row.status === "pending") acc.pending += row.count;
7588
+ else if (row.status === "in_progress") acc.inProgress += row.count;
7589
+ else if (row.status === "failed") acc.failed += row.count;
7590
+ return acc;
7591
+ }, {
7592
+ pending: 0,
7593
+ inProgress: 0,
7594
+ failed: 0
7595
+ }),
7219
7596
  queueStats: {
7220
7597
  totalQueues,
7221
7598
  problemQueues: problemQueuesCount
7222
7599
  }
7223
7600
  };
7224
7601
  }
7225
- var ErrorBoundary$10 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
7602
+ var ErrorBoundary$11 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
7226
7603
  return /* @__PURE__ */ jsx(ErrorCard, { title: "Failed to load dashboard" });
7227
7604
  });
7228
7605
  var _index_default = UNSAFE_withComponentProps(function Overview({ loaderData }) {
7229
- const { stats, warnings, topQueues } = loaderData;
7606
+ const { stats, warnings, topQueues, migrations } = loaderData;
7230
7607
  return /* @__PURE__ */ jsxs("div", { children: [
7231
7608
  /* @__PURE__ */ jsx(PageHeader, {
7232
7609
  title: "Overview",
@@ -7241,9 +7618,10 @@ var _index_default = UNSAFE_withComponentProps(function Overview({ loaderData })
7241
7618
  })
7242
7619
  }),
7243
7620
  /* @__PURE__ */ jsx("div", {
7244
- className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4 mb-4",
7621
+ className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 mb-4",
7245
7622
  children: /* @__PURE__ */ jsx(StatsCards, { stats })
7246
7623
  }),
7624
+ /* @__PURE__ */ jsx(MigrationsBanner, { migrations }),
7247
7625
  /* @__PURE__ */ jsxs("div", {
7248
7626
  className: "grid grid-cols-1 lg:grid-cols-[1.5fr_1fr] gap-4",
7249
7627
  children: [/* @__PURE__ */ jsxs(Card, { children: [/* @__PURE__ */ jsxs(CardHeader, { children: [/* @__PURE__ */ jsx(CardTitle, { children: "Top Queues" }), /* @__PURE__ */ jsx(DbLink, {
@@ -7312,6 +7690,37 @@ var _index_default = UNSAFE_withComponentProps(function Overview({ loaderData })
7312
7690
  })
7313
7691
  ] });
7314
7692
  });
7693
+ function MigrationsBanner({ migrations }) {
7694
+ const { pending, inProgress, failed } = migrations;
7695
+ if (pending === 0 && inProgress === 0 && failed === 0) return null;
7696
+ const parts = [];
7697
+ if (pending > 0) parts.push(`${pending.toLocaleString()} pending`);
7698
+ if (inProgress > 0) parts.push(`${inProgress.toLocaleString()} in progress`);
7699
+ if (failed > 0) parts.push(`${failed.toLocaleString()} failed`);
7700
+ return /* @__PURE__ */ jsx(DbLink, {
7701
+ to: "/migrations",
7702
+ className: "block mb-4",
7703
+ children: /* @__PURE__ */ jsxs(Card, {
7704
+ className: "flex items-center gap-3 px-4 py-3 hover:bg-[var(--surface-hover)]",
7705
+ children: [
7706
+ /* @__PURE__ */ jsx(Badge, {
7707
+ variant: failed > 0 ? "error" : "warning",
7708
+ size: "sm",
7709
+ dot: true,
7710
+ children: "Async migrations"
7711
+ }),
7712
+ /* @__PURE__ */ jsx("span", {
7713
+ className: "text-sm text-[var(--text-secondary)]",
7714
+ children: parts.join(" · ")
7715
+ }),
7716
+ /* @__PURE__ */ jsx("span", {
7717
+ className: "ml-auto text-sm font-medium text-primary-600 dark:text-primary-400",
7718
+ children: "View"
7719
+ })
7720
+ ]
7721
+ })
7722
+ });
7723
+ }
7315
7724
  function QueueStatusBadge({ queue }) {
7316
7725
  if ((queue.warningQueueSize ?? 0) > 0 && queue.queuedCount > (queue.warningQueueSize ?? 0)) return /* @__PURE__ */ jsx(Badge, {
7317
7726
  variant: "error",
@@ -7826,10 +8235,10 @@ function SearchIcon$1({ className }) {
7826
8235
  //#endregion
7827
8236
  //#region app/routes/jobs.tsx
7828
8237
  var jobs_exports = /* @__PURE__ */ __exportAll({
7829
- ErrorBoundary: () => ErrorBoundary$9,
8238
+ ErrorBoundary: () => ErrorBoundary$10,
7830
8239
  buildSearchParams: () => buildSearchParams,
7831
8240
  default: () => jobs_default,
7832
- loader: () => loader$9,
8241
+ loader: () => loader$10,
7833
8242
  parseFiltersFromUrl: () => parseFiltersFromUrl
7834
8243
  });
7835
8244
  function parseFiltersFromUrl(searchParams) {
@@ -7866,7 +8275,7 @@ function parseFiltersFromUrl(searchParams) {
7866
8275
  shouldRunCount
7867
8276
  };
7868
8277
  }
7869
- async function loader$9({ request, context }) {
8278
+ async function loader$10({ request, context }) {
7870
8279
  const { DB_URL, SCHEMA } = context.get(dbContext);
7871
8280
  const url = new URL(request.url);
7872
8281
  const parsed = parseFiltersFromUrl(url.searchParams);
@@ -7914,7 +8323,7 @@ async function loader$9({ request, context }) {
7914
8323
  hasPrevPage
7915
8324
  };
7916
8325
  }
7917
- var ErrorBoundary$9 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
8326
+ var ErrorBoundary$10 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
7918
8327
  return /* @__PURE__ */ jsx(ErrorCard, { title: "Failed to load jobs" });
7919
8328
  });
7920
8329
  function buildSearchParams(filters) {
@@ -8094,11 +8503,11 @@ function Chip({ label, onRemove }) {
8094
8503
  //#endregion
8095
8504
  //#region app/routes/queues._index.tsx
8096
8505
  var queues__index_exports = /* @__PURE__ */ __exportAll({
8097
- ErrorBoundary: () => ErrorBoundary$8,
8506
+ ErrorBoundary: () => ErrorBoundary$9,
8098
8507
  default: () => queues__index_default,
8099
- loader: () => loader$8
8508
+ loader: () => loader$9
8100
8509
  });
8101
- async function loader$8({ request, context }) {
8510
+ async function loader$9({ request, context }) {
8102
8511
  const { DB_URL, SCHEMA } = context.get(dbContext);
8103
8512
  const url = new URL(request.url);
8104
8513
  const page = parsePageNumber(url.searchParams.get("page"));
@@ -8131,7 +8540,7 @@ async function loader$8({ request, context }) {
8131
8540
  search
8132
8541
  };
8133
8542
  }
8134
- var ErrorBoundary$8 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
8543
+ var ErrorBoundary$9 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
8135
8544
  return /* @__PURE__ */ jsx("div", {
8136
8545
  className: "p-6",
8137
8546
  children: /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs(CardContent, {
@@ -8297,13 +8706,21 @@ var queues__index_default = UNSAFE_withComponentProps(function QueuesIndex({ loa
8297
8706
  className: "text-right",
8298
8707
  children: "Queued"
8299
8708
  }),
8709
+ /* @__PURE__ */ jsx(TableHead, {
8710
+ className: "text-right",
8711
+ children: "Deferred"
8712
+ }),
8713
+ /* @__PURE__ */ jsx(TableHead, {
8714
+ className: "text-right",
8715
+ children: "Ready"
8716
+ }),
8300
8717
  /* @__PURE__ */ jsx(TableHead, {
8301
8718
  className: "text-right",
8302
8719
  children: "Active"
8303
8720
  }),
8304
8721
  /* @__PURE__ */ jsx(TableHead, {
8305
8722
  className: "text-right",
8306
- children: "Deferred"
8723
+ children: "Failed"
8307
8724
  }),
8308
8725
  /* @__PURE__ */ jsx(TableHead, {
8309
8726
  className: "text-right",
@@ -8313,7 +8730,7 @@ var queues__index_default = UNSAFE_withComponentProps(function QueuesIndex({ loa
8313
8730
  /* @__PURE__ */ jsx(TableHead, { children: "Status" })
8314
8731
  ] }) }), /* @__PURE__ */ jsx(TableBody, { children: queues.length === 0 ? /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(TableCell, {
8315
8732
  className: "text-center text-[var(--text-tertiary)] py-8",
8316
- colSpan: 9,
8733
+ colSpan: 11,
8317
8734
  children: "No queues found"
8318
8735
  }) }) : queues.map((queue) => {
8319
8736
  const hasBacklog = (queue.warningQueueSize ?? 0) > 0 && queue.queuedCount > (queue.warningQueueSize ?? 0);
@@ -8336,13 +8753,21 @@ var queues__index_default = UNSAFE_withComponentProps(function QueuesIndex({ loa
8336
8753
  className: "text-right pgb-num text-[var(--text-primary)]",
8337
8754
  children: queue.queuedCount.toLocaleString()
8338
8755
  }),
8756
+ /* @__PURE__ */ jsx(TableCell, {
8757
+ className: "text-right pgb-num text-[var(--text-primary)]",
8758
+ children: queue.deferredCount.toLocaleString()
8759
+ }),
8760
+ /* @__PURE__ */ jsx(TableCell, {
8761
+ className: "text-right pgb-num text-[var(--text-primary)]",
8762
+ children: queue.readyCount.toLocaleString()
8763
+ }),
8339
8764
  /* @__PURE__ */ jsx(TableCell, {
8340
8765
  className: "text-right pgb-num text-[var(--text-primary)]",
8341
8766
  children: queue.activeCount.toLocaleString()
8342
8767
  }),
8343
8768
  /* @__PURE__ */ jsx(TableCell, {
8344
8769
  className: "text-right pgb-num text-[var(--text-primary)]",
8345
- children: queue.deferredCount.toLocaleString()
8770
+ children: queue.failedCount.toLocaleString()
8346
8771
  }),
8347
8772
  /* @__PURE__ */ jsx(TableCell, {
8348
8773
  className: "text-right pgb-num text-[var(--text-primary)]",
@@ -8520,12 +8945,12 @@ var SelectContent = ({ children }) => /* @__PURE__ */ jsx(Fragment, { children }
8520
8945
  //#endregion
8521
8946
  //#region app/routes/queues.create.tsx
8522
8947
  var queues_create_exports = /* @__PURE__ */ __exportAll({
8523
- ErrorBoundary: () => ErrorBoundary$7,
8948
+ ErrorBoundary: () => ErrorBoundary$8,
8524
8949
  action: () => action$5,
8525
8950
  default: () => queues_create_default,
8526
- loader: () => loader$7
8951
+ loader: () => loader$8
8527
8952
  });
8528
- async function loader$7({ context }) {
8953
+ async function loader$8({ context }) {
8529
8954
  const { DB_URL, SCHEMA } = context.get(dbContext);
8530
8955
  return { queues: await getQueues(DB_URL, SCHEMA) };
8531
8956
  }
@@ -8593,7 +9018,7 @@ async function action$5({ request, context }) {
8593
9018
  const dbParam = new URL(request.url).searchParams.get("db");
8594
9019
  return redirect(dbParam ? `/queues/${encodeURIComponent(queueName.trim())}?db=${encodeURIComponent(dbParam)}` : `/queues/${encodeURIComponent(queueName.trim())}`);
8595
9020
  }
8596
- var ErrorBoundary$7 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
9021
+ var ErrorBoundary$8 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
8597
9022
  return /* @__PURE__ */ jsx(ErrorCard, {
8598
9023
  title: "Failed to load queue creation page",
8599
9024
  backTo: {
@@ -9041,12 +9466,12 @@ DialogDescription.displayName = "DialogDescription";
9041
9466
  //#endregion
9042
9467
  //#region app/routes/queues.$name.tsx
9043
9468
  var queues_$name_exports = /* @__PURE__ */ __exportAll({
9044
- ErrorBoundary: () => ErrorBoundary$6,
9469
+ ErrorBoundary: () => ErrorBoundary$7,
9045
9470
  action: () => action$4,
9046
9471
  default: () => queues_$name_default,
9047
- loader: () => loader$6
9472
+ loader: () => loader$7
9048
9473
  });
9049
- async function loader$6({ params, request, context }) {
9474
+ async function loader$7({ params, request, context }) {
9050
9475
  const { DB_URL, SCHEMA } = context.get(dbContext);
9051
9476
  const url = new URL(request.url);
9052
9477
  const stateParam = url.searchParams.get("state");
@@ -9119,7 +9544,7 @@ async function action$4({ params, request, context }) {
9119
9544
  message
9120
9545
  };
9121
9546
  }
9122
- var ErrorBoundary$6 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
9547
+ var ErrorBoundary$7 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
9123
9548
  return /* @__PURE__ */ jsx(ErrorCard, {
9124
9549
  title: "Failed to load queue",
9125
9550
  backTo: {
@@ -9178,13 +9603,23 @@ var queues_$name_default = UNSAFE_withComponentProps(function QueueDetail({ load
9178
9603
  ]
9179
9604
  }),
9180
9605
  /* @__PURE__ */ jsxs("div", {
9181
- className: "grid grid-cols-2 sm:grid-cols-4 gap-4",
9606
+ className: "grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-4",
9182
9607
  children: [
9183
9608
  /* @__PURE__ */ jsx(StatCard, {
9184
9609
  label: "Queued",
9185
9610
  value: queue.queuedCount.toLocaleString(),
9186
9611
  accent: overThreshold ? "error" : "neutral",
9187
- hint: overThreshold ? "over threshold" : "within threshold"
9612
+ hint: overThreshold ? "over threshold" : "incl. deferred"
9613
+ }),
9614
+ /* @__PURE__ */ jsx(StatCard, {
9615
+ label: "Deferred",
9616
+ value: queue.deferredCount.toLocaleString()
9617
+ }),
9618
+ /* @__PURE__ */ jsx(StatCard, {
9619
+ label: "Ready",
9620
+ value: queue.readyCount.toLocaleString(),
9621
+ accent: "primary",
9622
+ hint: "ready to process"
9188
9623
  }),
9189
9624
  /* @__PURE__ */ jsx(StatCard, {
9190
9625
  label: "Active",
@@ -9192,8 +9627,9 @@ var queues_$name_default = UNSAFE_withComponentProps(function QueueDetail({ load
9192
9627
  accent: "primary"
9193
9628
  }),
9194
9629
  /* @__PURE__ */ jsx(StatCard, {
9195
- label: "Deferred",
9196
- value: queue.deferredCount.toLocaleString()
9630
+ label: "Failed",
9631
+ value: queue.failedCount.toLocaleString(),
9632
+ hint: "recent failures"
9197
9633
  }),
9198
9634
  /* @__PURE__ */ jsx(StatCard, {
9199
9635
  label: "Total",
@@ -9539,12 +9975,12 @@ function ConfirmDialog({ title, description, confirmLabel, confirmVariant = "pri
9539
9975
  //#endregion
9540
9976
  //#region app/routes/queues.$name.jobs.$jobId.tsx
9541
9977
  var queues_$name_jobs_$jobId_exports = /* @__PURE__ */ __exportAll({
9542
- ErrorBoundary: () => ErrorBoundary$5,
9978
+ ErrorBoundary: () => ErrorBoundary$6,
9543
9979
  action: () => action$3,
9544
9980
  default: () => queues_$name_jobs_$jobId_default,
9545
- loader: () => loader$5
9981
+ loader: () => loader$6
9546
9982
  });
9547
- async function loader$5({ params, context }) {
9983
+ async function loader$6({ params, context }) {
9548
9984
  const { DB_URL, SCHEMA } = context.get(dbContext);
9549
9985
  const job = await getJobById(DB_URL, SCHEMA, params.name, params.jobId);
9550
9986
  if (!job) throw new Response("Job not found", { status: 404 });
@@ -9598,7 +10034,7 @@ async function action$3({ params, request, context }) {
9598
10034
  message
9599
10035
  };
9600
10036
  }
9601
- var ErrorBoundary$5 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
10037
+ var ErrorBoundary$6 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
9602
10038
  return /* @__PURE__ */ jsx(ErrorCard, {
9603
10039
  title: "Failed to load job",
9604
10040
  backTo: {
@@ -9851,11 +10287,11 @@ function ConfigItem({ label, value, mono = false }) {
9851
10287
  //#endregion
9852
10288
  //#region app/routes/schedules.tsx
9853
10289
  var schedules_exports = /* @__PURE__ */ __exportAll({
9854
- ErrorBoundary: () => ErrorBoundary$4,
10290
+ ErrorBoundary: () => ErrorBoundary$5,
9855
10291
  default: () => schedules_default,
9856
- loader: () => loader$4
10292
+ loader: () => loader$5
9857
10293
  });
9858
- async function loader$4({ request, context }) {
10294
+ async function loader$5({ request, context }) {
9859
10295
  const { DB_URL, SCHEMA } = context.get(dbContext);
9860
10296
  const page = parsePageNumber(new URL(request.url).searchParams.get("page"));
9861
10297
  const limit = 20;
@@ -9873,7 +10309,7 @@ async function loader$4({ request, context }) {
9873
10309
  hasPrevPage: page > 1
9874
10310
  };
9875
10311
  }
9876
- var ErrorBoundary$4 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
10312
+ var ErrorBoundary$5 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
9877
10313
  return /* @__PURE__ */ jsx(ErrorCard, { title: "Failed to load schedules" });
9878
10314
  });
9879
10315
  function cronHuman(cron) {
@@ -9983,12 +10419,12 @@ var schedules_default = UNSAFE_withComponentProps(function Schedules({ loaderDat
9983
10419
  //#endregion
9984
10420
  //#region app/routes/schedules.$name.$key.tsx
9985
10421
  var schedules_$name_$key_exports = /* @__PURE__ */ __exportAll({
9986
- ErrorBoundary: () => ErrorBoundary$3,
10422
+ ErrorBoundary: () => ErrorBoundary$4,
9987
10423
  action: () => action$2,
9988
10424
  default: () => schedules_$name_$key_default,
9989
- loader: () => loader$3
10425
+ loader: () => loader$4
9990
10426
  });
9991
- async function loader$3({ params, context }) {
10427
+ async function loader$4({ params, context }) {
9992
10428
  const { DB_URL, SCHEMA } = context.get(dbContext);
9993
10429
  const key = params.key === "__default__" ? "" : params.key;
9994
10430
  const schedule = await getSchedule(DB_URL, SCHEMA, params.name, key);
@@ -10006,7 +10442,7 @@ async function action$2({ params, request, context }) {
10006
10442
  }
10007
10443
  return { error: "Invalid action" };
10008
10444
  }
10009
- var ErrorBoundary$3 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
10445
+ var ErrorBoundary$4 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
10010
10446
  return /* @__PURE__ */ jsx(ErrorCard, {
10011
10447
  title: "Failed to load schedule",
10012
10448
  backTo: {
@@ -10166,12 +10602,12 @@ var schedules_$name_$key_default = UNSAFE_withComponentProps(function ScheduleDe
10166
10602
  //#endregion
10167
10603
  //#region app/routes/schedules.new.tsx
10168
10604
  var schedules_new_exports = /* @__PURE__ */ __exportAll({
10169
- ErrorBoundary: () => ErrorBoundary$2,
10605
+ ErrorBoundary: () => ErrorBoundary$3,
10170
10606
  action: () => action$1,
10171
10607
  default: () => schedules_new_default,
10172
- loader: () => loader$2
10608
+ loader: () => loader$3
10173
10609
  });
10174
- async function loader$2({ context }) {
10610
+ async function loader$3({ context }) {
10175
10611
  const { DB_URL, SCHEMA } = context.get(dbContext);
10176
10612
  return { queues: await getQueues(DB_URL, SCHEMA) };
10177
10613
  }
@@ -10224,7 +10660,7 @@ async function action$1({ request, context }) {
10224
10660
  const dbParam = new URL(request.url).searchParams.get("db");
10225
10661
  return redirect(dbParam ? `/schedules?db=${encodeURIComponent(dbParam)}` : `/schedules`);
10226
10662
  }
10227
- var ErrorBoundary$2 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
10663
+ var ErrorBoundary$3 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
10228
10664
  return /* @__PURE__ */ jsx(ErrorCard, {
10229
10665
  title: "Failed to load schedule creation",
10230
10666
  backTo: {
@@ -10490,12 +10926,12 @@ var schedules_new_default = UNSAFE_withComponentProps(function CreateSchedule({
10490
10926
  //#endregion
10491
10927
  //#region app/routes/send.tsx
10492
10928
  var send_exports = /* @__PURE__ */ __exportAll({
10493
- ErrorBoundary: () => ErrorBoundary$1,
10929
+ ErrorBoundary: () => ErrorBoundary$2,
10494
10930
  action: () => action,
10495
10931
  default: () => send_default,
10496
- loader: () => loader$1
10932
+ loader: () => loader$2
10497
10933
  });
10498
- async function loader$1({ context }) {
10934
+ async function loader$2({ context }) {
10499
10935
  const { DB_URL, SCHEMA } = context.get(dbContext);
10500
10936
  return { queues: await getQueues(DB_URL, SCHEMA) };
10501
10937
  }
@@ -10545,7 +10981,7 @@ async function action({ request, context }) {
10545
10981
  const dbParam = new URL(request.url).searchParams.get("db");
10546
10982
  return redirect(dbParam ? `/queues/${encodeURIComponent(queueName.trim())}?db=${encodeURIComponent(dbParam)}` : `/queues/${encodeURIComponent(queueName.trim())}`);
10547
10983
  }
10548
- var ErrorBoundary$1 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
10984
+ var ErrorBoundary$2 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
10549
10985
  return /* @__PURE__ */ jsx(ErrorCard, {
10550
10986
  title: "Failed to load job sending page",
10551
10987
  backTo: {
@@ -10777,6 +11213,182 @@ var send_default = UNSAFE_withComponentProps(function SendJob({ loaderData }) {
10777
11213
  });
10778
11214
  });
10779
11215
  //#endregion
11216
+ //#region app/routes/migrations.tsx
11217
+ var migrations_exports = /* @__PURE__ */ __exportAll({
11218
+ ErrorBoundary: () => ErrorBoundary$1,
11219
+ default: () => migrations_default,
11220
+ loader: () => loader$1
11221
+ });
11222
+ var PAGE_SIZE = 50;
11223
+ var STATUS_ACCENT = {
11224
+ pending: "warning",
11225
+ in_progress: "primary",
11226
+ completed: "success",
11227
+ failed: "error"
11228
+ };
11229
+ async function loader$1({ request, context }) {
11230
+ const { DB_URL, SCHEMA } = context.get(dbContext);
11231
+ const url = new URL(request.url);
11232
+ const statusParam = url.searchParams.get("status");
11233
+ const statusFilter = isValidBamStatus(statusParam) ? statusParam : null;
11234
+ const page = parsePageNumber(url.searchParams.get("page"));
11235
+ const offset = (page - 1) * PAGE_SIZE;
11236
+ const [entries, totalCount, summary] = await Promise.all([
11237
+ getBamEntries(DB_URL, SCHEMA, {
11238
+ status: statusFilter,
11239
+ limit: PAGE_SIZE,
11240
+ offset
11241
+ }),
11242
+ getBamCount(DB_URL, SCHEMA, statusFilter),
11243
+ getBamStatusSummary(DB_URL, SCHEMA)
11244
+ ]);
11245
+ return {
11246
+ entries,
11247
+ summary,
11248
+ statusFilter,
11249
+ page,
11250
+ totalPages: Math.ceil(totalCount / PAGE_SIZE)
11251
+ };
11252
+ }
11253
+ var ErrorBoundary$1 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary() {
11254
+ return /* @__PURE__ */ jsx(ErrorCard, {
11255
+ title: "Failed to load migrations",
11256
+ backTo: {
11257
+ href: "/",
11258
+ label: "Back to Dashboard"
11259
+ }
11260
+ });
11261
+ });
11262
+ var migrations_default = UNSAFE_withComponentProps(function Migrations({ loaderData }) {
11263
+ const { entries, summary, statusFilter, page, totalPages } = loaderData;
11264
+ const [searchParams, setSearchParams] = useSearchParams();
11265
+ const counts = countByStatus(summary);
11266
+ const handleFilterChange = (key, value) => {
11267
+ const params = new URLSearchParams(searchParams);
11268
+ if (value) params.set(key, value);
11269
+ else params.delete(key);
11270
+ params.delete("page");
11271
+ setSearchParams(params);
11272
+ };
11273
+ const handlePageChange = (newPage) => {
11274
+ const params = new URLSearchParams(searchParams);
11275
+ params.set("page", newPage.toString());
11276
+ setSearchParams(params);
11277
+ };
11278
+ return /* @__PURE__ */ jsxs("div", {
11279
+ className: "space-y-4",
11280
+ children: [
11281
+ /* @__PURE__ */ jsx(PageHeader, {
11282
+ title: "Migrations",
11283
+ subtitle: "Background async migrations (BAM) — schema changes such as concurrent index builds that run outside the install transaction"
11284
+ }),
11285
+ /* @__PURE__ */ jsx("div", {
11286
+ className: "grid grid-cols-2 gap-4 lg:grid-cols-4",
11287
+ children: BAM_STATUSES.map((status) => /* @__PURE__ */ jsx(StatCard, {
11288
+ label: BAM_STATUS_LABELS[status],
11289
+ value: counts[status].toLocaleString(),
11290
+ accent: counts[status] > 0 ? STATUS_ACCENT[status] : "neutral"
11291
+ }, status))
11292
+ }),
11293
+ /* @__PURE__ */ jsxs(Card, { children: [
11294
+ /* @__PURE__ */ jsxs(CardHeader, { children: [/* @__PURE__ */ jsx(CardTitle, { children: "Migration commands" }), /* @__PURE__ */ jsx(FilterSelect, {
11295
+ value: statusFilter,
11296
+ options: BAM_STATUS_OPTIONS,
11297
+ onChange: (value) => handleFilterChange("status", value)
11298
+ })] }),
11299
+ /* @__PURE__ */ jsx(CardContent, {
11300
+ className: "p-0",
11301
+ children: /* @__PURE__ */ jsxs(Table, { children: [/* @__PURE__ */ jsx(TableHeader, { children: /* @__PURE__ */ jsxs(TableRow, { children: [
11302
+ /* @__PURE__ */ jsx(TableHead, { children: "Name" }),
11303
+ /* @__PURE__ */ jsx(TableHead, {
11304
+ className: "text-right",
11305
+ children: "Ver"
11306
+ }),
11307
+ /* @__PURE__ */ jsx(TableHead, { children: "Status" }),
11308
+ /* @__PURE__ */ jsx(TableHead, { children: "Table" }),
11309
+ /* @__PURE__ */ jsx(TableHead, { children: "Created" }),
11310
+ /* @__PURE__ */ jsx(TableHead, { children: "Started" }),
11311
+ /* @__PURE__ */ jsx(TableHead, { children: "Completed" }),
11312
+ /* @__PURE__ */ jsx(TableHead, { children: "Command / Error" })
11313
+ ] }) }), /* @__PURE__ */ jsx(TableBody, { children: entries.length === 0 ? /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(TableCell, {
11314
+ className: "text-center text-[var(--text-tertiary)] py-8",
11315
+ colSpan: 8,
11316
+ children: statusFilter ? `No ${BAM_STATUS_LABELS[statusFilter].toLowerCase()} migrations found` : "No async migrations recorded."
11317
+ }) }) : entries.map((entry) => /* @__PURE__ */ jsxs(TableRow, { children: [
11318
+ /* @__PURE__ */ jsx(TableCell, {
11319
+ className: "text-[var(--text-primary)] font-medium",
11320
+ children: entry.name
11321
+ }),
11322
+ /* @__PURE__ */ jsx(TableCell, {
11323
+ className: "text-right pgb-num text-[var(--text-tertiary)]",
11324
+ children: entry.version
11325
+ }),
11326
+ /* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsx(Badge, {
11327
+ variant: BAM_STATUS_VARIANTS[entry.status],
11328
+ size: "sm",
11329
+ dot: true,
11330
+ children: BAM_STATUS_LABELS[entry.status]
11331
+ }) }),
11332
+ /* @__PURE__ */ jsxs(TableCell, {
11333
+ className: "font-mono text-xs text-[var(--text-secondary)] whitespace-nowrap",
11334
+ children: [entry.table, entry.queue ? /* @__PURE__ */ jsxs("span", {
11335
+ className: "text-[var(--text-tertiary)]",
11336
+ children: [" · ", entry.queue]
11337
+ }) : null]
11338
+ }),
11339
+ /* @__PURE__ */ jsx(TableCell, {
11340
+ className: "pgb-num text-[var(--text-tertiary)] whitespace-nowrap",
11341
+ children: formatTimestamp(entry.createdOn)
11342
+ }),
11343
+ /* @__PURE__ */ jsx(TableCell, {
11344
+ className: "pgb-num text-[var(--text-tertiary)] whitespace-nowrap",
11345
+ children: formatTimestamp(entry.startedOn)
11346
+ }),
11347
+ /* @__PURE__ */ jsx(TableCell, {
11348
+ className: "pgb-num text-[var(--text-tertiary)] whitespace-nowrap",
11349
+ children: formatTimestamp(entry.completedOn)
11350
+ }),
11351
+ /* @__PURE__ */ jsxs(TableCell, {
11352
+ className: "max-w-md",
11353
+ children: [entry.error ? /* @__PURE__ */ jsx("p", {
11354
+ className: "mb-1 font-mono text-xs text-[var(--error-600)] break-words whitespace-pre-wrap",
11355
+ children: entry.error
11356
+ }) : null, /* @__PURE__ */ jsxs("details", { children: [/* @__PURE__ */ jsx("summary", {
11357
+ className: "cursor-pointer text-xs text-[var(--text-tertiary)] select-none",
11358
+ children: "View command"
11359
+ }), /* @__PURE__ */ jsx("pre", {
11360
+ className: "mt-1 overflow-x-auto rounded bg-[var(--surface-sunken)] p-2 font-mono text-xs text-[var(--text-secondary)] whitespace-pre-wrap",
11361
+ children: entry.command
11362
+ })] })]
11363
+ })
11364
+ ] }, entry.id)) })] })
11365
+ }),
11366
+ /* @__PURE__ */ jsx(Pagination, {
11367
+ page,
11368
+ totalPages,
11369
+ hasNextPage: page < totalPages,
11370
+ hasPrevPage: page > 1,
11371
+ onPageChange: handlePageChange
11372
+ })
11373
+ ] })
11374
+ ]
11375
+ });
11376
+ });
11377
+ function formatTimestamp(value) {
11378
+ if (!value) return "—";
11379
+ return formatDateWithSeconds(new Date(value));
11380
+ }
11381
+ function countByStatus(summary) {
11382
+ const counts = {
11383
+ pending: 0,
11384
+ in_progress: 0,
11385
+ completed: 0,
11386
+ failed: 0
11387
+ };
11388
+ for (const row of summary) if (row.status in counts) counts[row.status] += row.count;
11389
+ return counts;
11390
+ }
11391
+ //#endregion
10780
11392
  //#region app/routes/warnings.tsx
10781
11393
  var warnings_exports = /* @__PURE__ */ __exportAll({
10782
11394
  ErrorBoundary: () => ErrorBoundary,
@@ -10888,8 +11500,8 @@ function WarningTypeBadge({ type }) {
10888
11500
  //#region \0virtual:react-router/server-manifest
10889
11501
  var server_manifest_default = {
10890
11502
  "entry": {
10891
- "module": "/assets/entry.client-DL_oPh96.js",
10892
- "imports": ["/assets/jsx-runtime-BgbGXvsu.js", "/assets/react-dom-QnGHOQwT.js"],
11503
+ "module": "/assets/entry.client-CqyjuPDB.js",
11504
+ "imports": ["/assets/jsx-runtime-RQyiN6Nr.js", "/assets/react-dom-D_m_Zgd3.js"],
10893
11505
  "css": []
10894
11506
  },
10895
11507
  "routes": {
@@ -10906,16 +11518,16 @@ var server_manifest_default = {
10906
11518
  "hasClientMiddleware": false,
10907
11519
  "hasDefaultExport": true,
10908
11520
  "hasErrorBoundary": true,
10909
- "module": "/assets/root-Df70GAY3.js",
11521
+ "module": "/assets/root-qxoeL6W3.js",
10910
11522
  "imports": [
10911
- "/assets/jsx-runtime-BgbGXvsu.js",
10912
- "/assets/react-dom-QnGHOQwT.js",
10913
- "/assets/db-link-BWWnHM0k.js",
10914
- "/assets/MenuTrigger-BhalG0aG.js",
10915
- "/assets/createLucideIcon-DVP_i62f.js",
10916
- "/assets/useOpenInteractionType-D3JsvupP.js"
11523
+ "/assets/jsx-runtime-RQyiN6Nr.js",
11524
+ "/assets/react-dom-D_m_Zgd3.js",
11525
+ "/assets/db-link-BajQ1v8I.js",
11526
+ "/assets/createLucideIcon-C-LI4enx.js",
11527
+ "/assets/MenuTrigger-BNvpjhsQ.js",
11528
+ "/assets/useOpenInteractionType-BQ1arb0B.js"
10917
11529
  ],
10918
- "css": ["/assets/root-C0MdPLOa.css"],
11530
+ "css": ["/assets/root-B0MB8jZH.css"],
10919
11531
  "clientActionModule": void 0,
10920
11532
  "clientLoaderModule": void 0,
10921
11533
  "clientMiddlewareModule": void 0,
@@ -10934,15 +11546,15 @@ var server_manifest_default = {
10934
11546
  "hasClientMiddleware": false,
10935
11547
  "hasDefaultExport": true,
10936
11548
  "hasErrorBoundary": true,
10937
- "module": "/assets/_index-D1-nZ7Th.js",
11549
+ "module": "/assets/_index-DqpFaaQw.js",
10938
11550
  "imports": [
10939
- "/assets/jsx-runtime-BgbGXvsu.js",
10940
- "/assets/db-link-BWWnHM0k.js",
10941
- "/assets/error-card-B0ANyjh3.js",
10942
- "/assets/badge-DCQvSdiR.js",
10943
- "/assets/button-BxLcuaPM.js",
10944
- "/assets/stat-card-DLtQnscf.js",
10945
- "/assets/table-DqqzSNik.js"
11551
+ "/assets/jsx-runtime-RQyiN6Nr.js",
11552
+ "/assets/db-link-BajQ1v8I.js",
11553
+ "/assets/stat-card-dyg1wY5p.js",
11554
+ "/assets/button-9NpSS9Ow.js",
11555
+ "/assets/badge-CMnQO7Lq.js",
11556
+ "/assets/table-Cz7ujmH_.js",
11557
+ "/assets/error-card-BH7i86fH.js"
10946
11558
  ],
10947
11559
  "css": [],
10948
11560
  "clientActionModule": void 0,
@@ -10963,24 +11575,24 @@ var server_manifest_default = {
10963
11575
  "hasClientMiddleware": false,
10964
11576
  "hasDefaultExport": true,
10965
11577
  "hasErrorBoundary": true,
10966
- "module": "/assets/jobs-D0a6Lwq0.js",
11578
+ "module": "/assets/jobs-CAd_qqLH.js",
10967
11579
  "imports": [
10968
- "/assets/jsx-runtime-BgbGXvsu.js",
10969
- "/assets/db-link-BWWnHM0k.js",
10970
- "/assets/error-card-B0ANyjh3.js",
10971
- "/assets/badge-DCQvSdiR.js",
10972
- "/assets/button-BxLcuaPM.js",
10973
- "/assets/filter-select--qLjbs9m.js",
10974
- "/assets/pagination-Bzx8wbXG.js",
10975
- "/assets/table-DqqzSNik.js",
10976
- "/assets/MenuTrigger-BhalG0aG.js",
10977
- "/assets/useOpenInteractionType-D3JsvupP.js",
10978
- "/assets/createLucideIcon-DVP_i62f.js",
10979
- "/assets/check-Ch42cXMT.js",
10980
- "/assets/chevron-down-Byq-CYG9.js",
10981
- "/assets/chevron-right-CKAGD7DJ.js",
10982
- "/assets/x-BPKZwOn9.js",
10983
- "/assets/react-dom-QnGHOQwT.js"
11580
+ "/assets/jsx-runtime-RQyiN6Nr.js",
11581
+ "/assets/db-link-BajQ1v8I.js",
11582
+ "/assets/createLucideIcon-C-LI4enx.js",
11583
+ "/assets/check-7jwc5sb1.js",
11584
+ "/assets/chevron-down-BFFjfYD4.js",
11585
+ "/assets/chevron-right-DGk5QFJF.js",
11586
+ "/assets/x-AhXI_F1j.js",
11587
+ "/assets/MenuTrigger-BNvpjhsQ.js",
11588
+ "/assets/useOpenInteractionType-BQ1arb0B.js",
11589
+ "/assets/button-9NpSS9Ow.js",
11590
+ "/assets/badge-CMnQO7Lq.js",
11591
+ "/assets/table-Cz7ujmH_.js",
11592
+ "/assets/error-card-BH7i86fH.js",
11593
+ "/assets/pagination-C-ohiBmY.js",
11594
+ "/assets/filter-select-Bn_oSiip.js",
11595
+ "/assets/react-dom-D_m_Zgd3.js"
10984
11596
  ],
10985
11597
  "css": [],
10986
11598
  "clientActionModule": void 0,
@@ -11001,14 +11613,14 @@ var server_manifest_default = {
11001
11613
  "hasClientMiddleware": false,
11002
11614
  "hasDefaultExport": true,
11003
11615
  "hasErrorBoundary": true,
11004
- "module": "/assets/queues._index-D8903DTa.js",
11616
+ "module": "/assets/queues._index-8YriSqbQ.js",
11005
11617
  "imports": [
11006
- "/assets/jsx-runtime-BgbGXvsu.js",
11007
- "/assets/db-link-BWWnHM0k.js",
11008
- "/assets/badge-DCQvSdiR.js",
11009
- "/assets/button-BxLcuaPM.js",
11010
- "/assets/filter-select--qLjbs9m.js",
11011
- "/assets/table-DqqzSNik.js"
11618
+ "/assets/jsx-runtime-RQyiN6Nr.js",
11619
+ "/assets/db-link-BajQ1v8I.js",
11620
+ "/assets/button-9NpSS9Ow.js",
11621
+ "/assets/badge-CMnQO7Lq.js",
11622
+ "/assets/table-Cz7ujmH_.js",
11623
+ "/assets/filter-select-Bn_oSiip.js"
11012
11624
  ],
11013
11625
  "css": [],
11014
11626
  "clientActionModule": void 0,
@@ -11029,14 +11641,14 @@ var server_manifest_default = {
11029
11641
  "hasClientMiddleware": false,
11030
11642
  "hasDefaultExport": true,
11031
11643
  "hasErrorBoundary": true,
11032
- "module": "/assets/queues.create-CMqQVLup.js",
11644
+ "module": "/assets/queues.create-DsY0Sc19.js",
11033
11645
  "imports": [
11034
- "/assets/jsx-runtime-BgbGXvsu.js",
11035
- "/assets/db-link-BWWnHM0k.js",
11036
- "/assets/error-card-B0ANyjh3.js",
11037
- "/assets/button-BxLcuaPM.js",
11038
- "/assets/chevron-down-Byq-CYG9.js",
11039
- "/assets/createLucideIcon-DVP_i62f.js"
11646
+ "/assets/jsx-runtime-RQyiN6Nr.js",
11647
+ "/assets/db-link-BajQ1v8I.js",
11648
+ "/assets/chevron-down-BFFjfYD4.js",
11649
+ "/assets/button-9NpSS9Ow.js",
11650
+ "/assets/error-card-BH7i86fH.js",
11651
+ "/assets/createLucideIcon-C-LI4enx.js"
11040
11652
  ],
11041
11653
  "css": [],
11042
11654
  "clientActionModule": void 0,
@@ -11057,25 +11669,25 @@ var server_manifest_default = {
11057
11669
  "hasClientMiddleware": false,
11058
11670
  "hasDefaultExport": true,
11059
11671
  "hasErrorBoundary": true,
11060
- "module": "/assets/queues._name-BVt_4pav.js",
11672
+ "module": "/assets/queues._name-Cb17IB2u.js",
11061
11673
  "imports": [
11062
- "/assets/jsx-runtime-BgbGXvsu.js",
11063
- "/assets/db-link-BWWnHM0k.js",
11064
- "/assets/error-card-B0ANyjh3.js",
11065
- "/assets/badge-DCQvSdiR.js",
11066
- "/assets/button-BxLcuaPM.js",
11067
- "/assets/dialog-Bik519zD.js",
11068
- "/assets/filter-select--qLjbs9m.js",
11069
- "/assets/pagination-Bzx8wbXG.js",
11070
- "/assets/stat-card-DLtQnscf.js",
11071
- "/assets/table-DqqzSNik.js",
11072
- "/assets/MenuTrigger-BhalG0aG.js",
11073
- "/assets/createLucideIcon-DVP_i62f.js",
11074
- "/assets/chevron-down-Byq-CYG9.js",
11075
- "/assets/chevron-right-CKAGD7DJ.js",
11076
- "/assets/useOpenInteractionType-D3JsvupP.js",
11077
- "/assets/x-BPKZwOn9.js",
11078
- "/assets/react-dom-QnGHOQwT.js"
11674
+ "/assets/jsx-runtime-RQyiN6Nr.js",
11675
+ "/assets/db-link-BajQ1v8I.js",
11676
+ "/assets/createLucideIcon-C-LI4enx.js",
11677
+ "/assets/chevron-down-BFFjfYD4.js",
11678
+ "/assets/chevron-right-DGk5QFJF.js",
11679
+ "/assets/MenuTrigger-BNvpjhsQ.js",
11680
+ "/assets/dialog-D-oczDM2.js",
11681
+ "/assets/stat-card-dyg1wY5p.js",
11682
+ "/assets/button-9NpSS9Ow.js",
11683
+ "/assets/badge-CMnQO7Lq.js",
11684
+ "/assets/table-Cz7ujmH_.js",
11685
+ "/assets/error-card-BH7i86fH.js",
11686
+ "/assets/pagination-C-ohiBmY.js",
11687
+ "/assets/filter-select-Bn_oSiip.js",
11688
+ "/assets/react-dom-D_m_Zgd3.js",
11689
+ "/assets/useOpenInteractionType-BQ1arb0B.js",
11690
+ "/assets/x-AhXI_F1j.js"
11079
11691
  ],
11080
11692
  "css": [],
11081
11693
  "clientActionModule": void 0,
@@ -11096,19 +11708,19 @@ var server_manifest_default = {
11096
11708
  "hasClientMiddleware": false,
11097
11709
  "hasDefaultExport": true,
11098
11710
  "hasErrorBoundary": true,
11099
- "module": "/assets/queues._name.jobs._jobId-BkG9y75k.js",
11711
+ "module": "/assets/queues._name.jobs._jobId-Bkv8POBj.js",
11100
11712
  "imports": [
11101
- "/assets/jsx-runtime-BgbGXvsu.js",
11102
- "/assets/db-link-BWWnHM0k.js",
11103
- "/assets/error-card-B0ANyjh3.js",
11104
- "/assets/badge-DCQvSdiR.js",
11105
- "/assets/button-BxLcuaPM.js",
11106
- "/assets/dialog-Bik519zD.js",
11107
- "/assets/createLucideIcon-DVP_i62f.js",
11108
- "/assets/check-Ch42cXMT.js",
11109
- "/assets/useOpenInteractionType-D3JsvupP.js",
11110
- "/assets/x-BPKZwOn9.js",
11111
- "/assets/react-dom-QnGHOQwT.js"
11713
+ "/assets/jsx-runtime-RQyiN6Nr.js",
11714
+ "/assets/db-link-BajQ1v8I.js",
11715
+ "/assets/createLucideIcon-C-LI4enx.js",
11716
+ "/assets/check-7jwc5sb1.js",
11717
+ "/assets/dialog-D-oczDM2.js",
11718
+ "/assets/button-9NpSS9Ow.js",
11719
+ "/assets/badge-CMnQO7Lq.js",
11720
+ "/assets/error-card-BH7i86fH.js",
11721
+ "/assets/x-AhXI_F1j.js",
11722
+ "/assets/useOpenInteractionType-BQ1arb0B.js",
11723
+ "/assets/react-dom-D_m_Zgd3.js"
11112
11724
  ],
11113
11725
  "css": [],
11114
11726
  "clientActionModule": void 0,
@@ -11129,15 +11741,15 @@ var server_manifest_default = {
11129
11741
  "hasClientMiddleware": false,
11130
11742
  "hasDefaultExport": true,
11131
11743
  "hasErrorBoundary": true,
11132
- "module": "/assets/schedules-DPXQoaEE.js",
11744
+ "module": "/assets/schedules-iYfIJxOD.js",
11133
11745
  "imports": [
11134
- "/assets/jsx-runtime-BgbGXvsu.js",
11135
- "/assets/db-link-BWWnHM0k.js",
11136
- "/assets/error-card-B0ANyjh3.js",
11137
- "/assets/badge-DCQvSdiR.js",
11138
- "/assets/button-BxLcuaPM.js",
11139
- "/assets/pagination-Bzx8wbXG.js",
11140
- "/assets/table-DqqzSNik.js"
11746
+ "/assets/jsx-runtime-RQyiN6Nr.js",
11747
+ "/assets/db-link-BajQ1v8I.js",
11748
+ "/assets/button-9NpSS9Ow.js",
11749
+ "/assets/badge-CMnQO7Lq.js",
11750
+ "/assets/table-Cz7ujmH_.js",
11751
+ "/assets/error-card-BH7i86fH.js",
11752
+ "/assets/pagination-C-ohiBmY.js"
11141
11753
  ],
11142
11754
  "css": [],
11143
11755
  "clientActionModule": void 0,
@@ -11158,17 +11770,17 @@ var server_manifest_default = {
11158
11770
  "hasClientMiddleware": false,
11159
11771
  "hasDefaultExport": true,
11160
11772
  "hasErrorBoundary": true,
11161
- "module": "/assets/schedules._name._key-B_luxy1w.js",
11773
+ "module": "/assets/schedules._name._key-CJVu73XY.js",
11162
11774
  "imports": [
11163
- "/assets/jsx-runtime-BgbGXvsu.js",
11164
- "/assets/db-link-BWWnHM0k.js",
11165
- "/assets/error-card-B0ANyjh3.js",
11166
- "/assets/button-BxLcuaPM.js",
11167
- "/assets/dialog-Bik519zD.js",
11168
- "/assets/useOpenInteractionType-D3JsvupP.js",
11169
- "/assets/x-BPKZwOn9.js",
11170
- "/assets/react-dom-QnGHOQwT.js",
11171
- "/assets/createLucideIcon-DVP_i62f.js"
11775
+ "/assets/jsx-runtime-RQyiN6Nr.js",
11776
+ "/assets/db-link-BajQ1v8I.js",
11777
+ "/assets/dialog-D-oczDM2.js",
11778
+ "/assets/button-9NpSS9Ow.js",
11779
+ "/assets/error-card-BH7i86fH.js",
11780
+ "/assets/x-AhXI_F1j.js",
11781
+ "/assets/useOpenInteractionType-BQ1arb0B.js",
11782
+ "/assets/createLucideIcon-C-LI4enx.js",
11783
+ "/assets/react-dom-D_m_Zgd3.js"
11172
11784
  ],
11173
11785
  "css": [],
11174
11786
  "clientActionModule": void 0,
@@ -11189,12 +11801,12 @@ var server_manifest_default = {
11189
11801
  "hasClientMiddleware": false,
11190
11802
  "hasDefaultExport": true,
11191
11803
  "hasErrorBoundary": true,
11192
- "module": "/assets/schedules.new-BQV7GWzs.js",
11804
+ "module": "/assets/schedules.new-Cq0Mxa7G.js",
11193
11805
  "imports": [
11194
- "/assets/jsx-runtime-BgbGXvsu.js",
11195
- "/assets/db-link-BWWnHM0k.js",
11196
- "/assets/error-card-B0ANyjh3.js",
11197
- "/assets/button-BxLcuaPM.js"
11806
+ "/assets/jsx-runtime-RQyiN6Nr.js",
11807
+ "/assets/db-link-BajQ1v8I.js",
11808
+ "/assets/button-9NpSS9Ow.js",
11809
+ "/assets/error-card-BH7i86fH.js"
11198
11810
  ],
11199
11811
  "css": [],
11200
11812
  "clientActionModule": void 0,
@@ -11215,12 +11827,43 @@ var server_manifest_default = {
11215
11827
  "hasClientMiddleware": false,
11216
11828
  "hasDefaultExport": true,
11217
11829
  "hasErrorBoundary": true,
11218
- "module": "/assets/send-DJBsfnx_.js",
11830
+ "module": "/assets/send-8X9ZisG-.js",
11831
+ "imports": [
11832
+ "/assets/jsx-runtime-RQyiN6Nr.js",
11833
+ "/assets/db-link-BajQ1v8I.js",
11834
+ "/assets/button-9NpSS9Ow.js",
11835
+ "/assets/error-card-BH7i86fH.js"
11836
+ ],
11837
+ "css": [],
11838
+ "clientActionModule": void 0,
11839
+ "clientLoaderModule": void 0,
11840
+ "clientMiddlewareModule": void 0,
11841
+ "hydrateFallbackModule": void 0
11842
+ },
11843
+ "routes/migrations": {
11844
+ "id": "routes/migrations",
11845
+ "parentId": "root",
11846
+ "path": "migrations",
11847
+ "index": void 0,
11848
+ "caseSensitive": void 0,
11849
+ "hasAction": false,
11850
+ "hasLoader": true,
11851
+ "hasClientAction": false,
11852
+ "hasClientLoader": false,
11853
+ "hasClientMiddleware": false,
11854
+ "hasDefaultExport": true,
11855
+ "hasErrorBoundary": true,
11856
+ "module": "/assets/migrations-D5l0n4Jn.js",
11219
11857
  "imports": [
11220
- "/assets/jsx-runtime-BgbGXvsu.js",
11221
- "/assets/db-link-BWWnHM0k.js",
11222
- "/assets/error-card-B0ANyjh3.js",
11223
- "/assets/button-BxLcuaPM.js"
11858
+ "/assets/jsx-runtime-RQyiN6Nr.js",
11859
+ "/assets/db-link-BajQ1v8I.js",
11860
+ "/assets/stat-card-dyg1wY5p.js",
11861
+ "/assets/button-9NpSS9Ow.js",
11862
+ "/assets/badge-CMnQO7Lq.js",
11863
+ "/assets/table-Cz7ujmH_.js",
11864
+ "/assets/error-card-BH7i86fH.js",
11865
+ "/assets/pagination-C-ohiBmY.js",
11866
+ "/assets/filter-select-Bn_oSiip.js"
11224
11867
  ],
11225
11868
  "css": [],
11226
11869
  "clientActionModule": void 0,
@@ -11241,16 +11884,16 @@ var server_manifest_default = {
11241
11884
  "hasClientMiddleware": false,
11242
11885
  "hasDefaultExport": true,
11243
11886
  "hasErrorBoundary": true,
11244
- "module": "/assets/warnings-CHKaRfIW.js",
11887
+ "module": "/assets/warnings-C1R_RzIe.js",
11245
11888
  "imports": [
11246
- "/assets/jsx-runtime-BgbGXvsu.js",
11247
- "/assets/db-link-BWWnHM0k.js",
11248
- "/assets/error-card-B0ANyjh3.js",
11249
- "/assets/badge-DCQvSdiR.js",
11250
- "/assets/button-BxLcuaPM.js",
11251
- "/assets/filter-select--qLjbs9m.js",
11252
- "/assets/pagination-Bzx8wbXG.js",
11253
- "/assets/table-DqqzSNik.js"
11889
+ "/assets/jsx-runtime-RQyiN6Nr.js",
11890
+ "/assets/db-link-BajQ1v8I.js",
11891
+ "/assets/button-9NpSS9Ow.js",
11892
+ "/assets/badge-CMnQO7Lq.js",
11893
+ "/assets/table-Cz7ujmH_.js",
11894
+ "/assets/error-card-BH7i86fH.js",
11895
+ "/assets/pagination-C-ohiBmY.js",
11896
+ "/assets/filter-select-Bn_oSiip.js"
11254
11897
  ],
11255
11898
  "css": [],
11256
11899
  "clientActionModule": void 0,
@@ -11259,8 +11902,8 @@ var server_manifest_default = {
11259
11902
  "hydrateFallbackModule": void 0
11260
11903
  }
11261
11904
  },
11262
- "url": "/assets/manifest-ef81a0f9.js",
11263
- "version": "ef81a0f9",
11905
+ "url": "/assets/manifest-27e8e133.js",
11906
+ "version": "27e8e133",
11264
11907
  "sri": void 0
11265
11908
  };
11266
11909
  //#endregion
@@ -11366,6 +12009,14 @@ var routes = {
11366
12009
  caseSensitive: void 0,
11367
12010
  module: send_exports
11368
12011
  },
12012
+ "routes/migrations": {
12013
+ id: "routes/migrations",
12014
+ parentId: "root",
12015
+ path: "migrations",
12016
+ index: void 0,
12017
+ caseSensitive: void 0,
12018
+ module: migrations_exports
12019
+ },
11369
12020
  "routes/warnings": {
11370
12021
  id: "routes/warnings",
11371
12022
  parentId: "root",