@pg-boss/dashboard 1.3.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/build/server/index.js +383 -95
  2. package/package.json +2 -2
@@ -1568,7 +1568,8 @@ function createTableVersion(schema) {
1568
1568
  CREATE TABLE ${schema}.version (
1569
1569
  version int primary key,
1570
1570
  cron_on timestamp with time zone,
1571
- bam_on timestamp with time zone
1571
+ bam_on timestamp with time zone,
1572
+ flow_on timestamp with time zone
1572
1573
  )
1573
1574
  `;
1574
1575
  }
@@ -1843,6 +1844,7 @@ function createTableJobCommon(schema) {
1843
1844
  SELECT ${schema}.job_table_run($cmd$${createIndexJobThrottle(schema)}$cmd$, '${COMMON_JOB_TABLE}');
1844
1845
  SELECT ${schema}.job_table_run($cmd$${createIndexJobFetch(schema)}$cmd$, '${COMMON_JOB_TABLE}');
1845
1846
  SELECT ${schema}.job_table_run($cmd$${createIndexJobGroupConcurrency(schema)}$cmd$, '${COMMON_JOB_TABLE}');
1847
+ SELECT ${schema}.job_table_run($cmd$${createIndexJobBlocking(schema)}$cmd$, '${COMMON_JOB_TABLE}');
1846
1848
 
1847
1849
  ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.${COMMON_JOB_TABLE} DEFAULT;
1848
1850
  `;
@@ -1860,6 +1862,7 @@ function createTableJobIndexes(schema, noDeferrableConstraints = false, noCoveri
1860
1862
  ${createIndexJobThrottle(schema)};
1861
1863
  ${createIndexJobFetch(schema, noCoveringIndex)};
1862
1864
  ${createIndexJobGroupConcurrency(schema)};
1865
+ ${createIndexJobBlocking(schema)};
1863
1866
  `;
1864
1867
  }
1865
1868
  function createQueueFunction(schema, noPartitioning = false) {
@@ -1970,6 +1973,7 @@ function createQueueFunction(schema, noPartitioning = false) {
1970
1973
  EXECUTE ${schema}.job_table_format($cmd$${createIndexJobFetch(schema)}$cmd$, tablename);
1971
1974
  EXECUTE ${schema}.job_table_format($cmd$${createIndexJobThrottle(schema)}$cmd$, tablename);
1972
1975
  EXECUTE ${schema}.job_table_format($cmd$${createIndexJobGroupConcurrency(schema)}$cmd$, tablename);
1976
+ EXECUTE ${schema}.job_table_format($cmd$${createIndexJobBlocking(schema)}$cmd$, tablename);
1973
1977
 
1974
1978
  IF options->>'policy' = 'short' THEN
1975
1979
  EXECUTE ${schema}.job_table_format($cmd$${createIndexJobPolicyShort(schema)}$cmd$, tablename);
@@ -2053,7 +2057,7 @@ function createIndexJobThrottle(schema) {
2053
2057
  return `CREATE UNIQUE INDEX job_i4 ON ${schema}.job (name, singleton_on, COALESCE(singleton_key, '')) WHERE state <> '${JOB_STATES.cancelled}' AND singleton_on IS NOT NULL`;
2054
2058
  }
2055
2059
  function createIndexJobFetch(schema, noCoveringIndex = false) {
2056
- return `CREATE INDEX job_i5 ON ${schema}.job (name, start_after) ${noCoveringIndex ? "" : "INCLUDE (priority, created_on, id) "}WHERE state < '${JOB_STATES.active}' AND NOT blocked`;
2060
+ return `CREATE INDEX job_i5 ON ${schema}.job (name, start_after) WHERE state < '${JOB_STATES.active}' AND NOT blocked`;
2057
2061
  }
2058
2062
  function createIndexJobPolicyExclusive(schema) {
2059
2063
  return `CREATE UNIQUE INDEX job_i6 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state <= '${JOB_STATES.active}' AND policy = '${QUEUE_POLICIES.exclusive}'`;
@@ -2067,6 +2071,9 @@ function createCheckConstraintKeyStrictFifo(schema) {
2067
2071
  function createIndexJobGroupConcurrency(schema) {
2068
2072
  return `CREATE INDEX job_i7 ON ${schema}.job (name, group_id) WHERE state = '${JOB_STATES.active}' AND group_id IS NOT NULL`;
2069
2073
  }
2074
+ function createIndexJobBlocking(schema) {
2075
+ return `CREATE INDEX job_i9 ON ${schema}.job (name, id) WHERE blocking AND state = '${JOB_STATES.completed}'`;
2076
+ }
2070
2077
  function trySetQueueMonitorTime(schema, queues, seconds) {
2071
2078
  return trySetQueueTimestamp(schema, queues, "monitor_on", seconds);
2072
2079
  }
@@ -2079,6 +2086,9 @@ function trySetCronTime(schema, seconds) {
2079
2086
  function trySetBamTime(schema, seconds) {
2080
2087
  return trySetTimestamp(schema, "bam_on", seconds);
2081
2088
  }
2089
+ function trySetFlowTime(schema, seconds) {
2090
+ return trySetTimestamp(schema, "flow_on", seconds);
2091
+ }
2082
2092
  function trySetTimestamp(schema, column, seconds) {
2083
2093
  return `
2084
2094
  UPDATE ${schema}.version
@@ -2163,7 +2173,7 @@ function deleteJobsById(schema, table) {
2163
2173
  WITH results as (
2164
2174
  DELETE FROM ${schema}.${table}
2165
2175
  WHERE name = $1
2166
- AND id IN (SELECT UNNEST($2::uuid[]))
2176
+ AND id = ANY($2::uuid[])
2167
2177
  RETURNING 1
2168
2178
  )
2169
2179
  SELECT COUNT(*) from results
@@ -2426,7 +2436,7 @@ function completeJobsUpdate(schema, table, includeQueued) {
2426
2436
  blocked = ${includeQueued ? "false" : "blocked"},
2427
2437
  pending_dependencies = ${includeQueued ? "0" : "pending_dependencies"}
2428
2438
  WHERE name = $1
2429
- AND id IN (SELECT UNNEST($2::uuid[]))
2439
+ AND id = ANY($2::uuid[])
2430
2440
  AND ${includeQueued ? `state < '${JOB_STATES.completed}'` : `state = '${JOB_STATES.active}'`}`;
2431
2441
  }
2432
2442
  function lockedChildrenCte(schema) {
@@ -2450,24 +2460,11 @@ function unblockChildrenUpdate(schema) {
2450
2460
  }
2451
2461
  function completeJobs(schema, table, includeQueued) {
2452
2462
  return `
2453
- WITH completed AS (
2463
+ WITH results AS (
2454
2464
  ${completeJobsUpdate(schema, table, includeQueued)}
2455
- RETURNING name, id, blocking
2456
- ),
2457
- decremented AS (
2458
- SELECT d.child_name, d.child_id, COUNT(*)::int AS n
2459
- FROM ${schema}.job_dependency d
2460
- JOIN completed c ON c.blocking
2461
- AND d.parent_name = c.name
2462
- AND d.parent_id = c.id
2463
- GROUP BY d.child_name, d.child_id
2464
- ),
2465
- ${lockedChildrenCte(schema)},
2466
- unblocked AS (
2467
- ${unblockChildrenUpdate(schema)}
2468
2465
  RETURNING 1
2469
2466
  )
2470
- SELECT COUNT(*) FROM completed
2467
+ SELECT COUNT(*) FROM results
2471
2468
  `;
2472
2469
  }
2473
2470
  function completeJobsWithOutputs(schema, table) {
@@ -2475,7 +2472,7 @@ function completeJobsWithOutputs(schema, table) {
2475
2472
  WITH input AS (
2476
2473
  SELECT * FROM json_to_recordset($2::json) AS x (id uuid, output jsonb)
2477
2474
  ),
2478
- completed AS (
2475
+ results AS (
2479
2476
  UPDATE ${schema}.${table} j
2480
2477
  SET completed_on = now(),
2481
2478
  state = '${JOB_STATES.completed}',
@@ -2484,22 +2481,9 @@ function completeJobsWithOutputs(schema, table) {
2484
2481
  WHERE j.name = $1
2485
2482
  AND j.id = i.id
2486
2483
  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
2484
  RETURNING 1
2501
2485
  )
2502
- SELECT COUNT(*) FROM completed
2486
+ SELECT COUNT(*) FROM results
2503
2487
  `;
2504
2488
  }
2505
2489
  function completeJobsWithOutputsDistributed(schema, table) {
@@ -2515,7 +2499,7 @@ function completeJobsWithOutputsDistributed(schema, table) {
2515
2499
  WHERE j.name = $1
2516
2500
  AND j.id = i.id
2517
2501
  AND j.state = '${JOB_STATES.active}'
2518
- RETURNING j.id, j.blocking
2502
+ RETURNING j.id
2519
2503
  `;
2520
2504
  }
2521
2505
  function cancelJobs(schema, table) {
@@ -2525,7 +2509,7 @@ function cancelJobs(schema, table) {
2525
2509
  SET completed_on = now(),
2526
2510
  state = '${JOB_STATES.cancelled}'
2527
2511
  WHERE name = $1
2528
- AND id IN (SELECT UNNEST($2::uuid[]))
2512
+ AND id = ANY($2::uuid[])
2529
2513
  AND state < '${JOB_STATES.completed}'
2530
2514
  RETURNING 1
2531
2515
  )
@@ -2539,7 +2523,7 @@ function resumeJobs(schema, table) {
2539
2523
  SET completed_on = NULL,
2540
2524
  state = '${JOB_STATES.created}'
2541
2525
  WHERE name = $1
2542
- AND id IN (SELECT UNNEST($2::uuid[]))
2526
+ AND id = ANY($2::uuid[])
2543
2527
  AND state = '${JOB_STATES.cancelled}'
2544
2528
  RETURNING 1
2545
2529
  )
@@ -2553,7 +2537,7 @@ function restoreJobs(schema, table) {
2553
2537
  started_on = NULL,
2554
2538
  heartbeat_on = NULL
2555
2539
  WHERE name = $1
2556
- AND id IN (SELECT UNNEST($2::uuid[]))
2540
+ AND id = ANY($2::uuid[])
2557
2541
  `;
2558
2542
  }
2559
2543
  function insertJobs(schema, { table, name, returnId = true, notify = false }) {
@@ -2668,7 +2652,7 @@ function insertFlowJobs(schema, { table, name }, jobs) {
2668
2652
  `;
2669
2653
  }
2670
2654
  function failJobsById(schema, table) {
2671
- return failJobs(schema, table, `name = $1 AND id IN (SELECT UNNEST($2::uuid[])) AND state < '${JOB_STATES.completed}'`, "$3::jsonb");
2655
+ return failJobs(schema, table, `name = $1 AND id = ANY($2::uuid[]) AND state < '${JOB_STATES.completed}'`, "$3::jsonb");
2672
2656
  }
2673
2657
  function failJobsByTimeout(schema, table, queues, noAdvisoryLocks) {
2674
2658
  return locked(schema, failJobs(schema, table, `state = '${JOB_STATES.active}'
@@ -2687,7 +2671,7 @@ function touchJobs(schema, table) {
2687
2671
  UPDATE ${schema}.${table}
2688
2672
  SET heartbeat_on = now()
2689
2673
  WHERE name = $1
2690
- AND id IN (SELECT UNNEST($2::uuid[]))
2674
+ AND id = ANY($2::uuid[])
2691
2675
  AND state = '${JOB_STATES.active}'
2692
2676
  RETURNING 1
2693
2677
  )
@@ -2891,13 +2875,13 @@ function deadLetterJobsByIdWithOutputs(schema, table) {
2891
2875
  }
2892
2876
  function selectJobsToFailById(schema, table) {
2893
2877
  return {
2894
- text: `SELECT * FROM ${schema}.${table} WHERE name = $1 AND id IN (SELECT UNNEST($2::uuid[])) AND state < '${JOB_STATES.completed}'`,
2878
+ text: `SELECT * FROM ${schema}.${table} WHERE name = $1 AND id = ANY($2::uuid[]) AND state < '${JOB_STATES.completed}'`,
2895
2879
  values: []
2896
2880
  };
2897
2881
  }
2898
2882
  function deleteJobsToFail(schema, table) {
2899
2883
  return {
2900
- text: `DELETE FROM ${schema}.${table} WHERE name = $1 AND id IN (SELECT UNNEST($2::uuid[]))`,
2884
+ text: `DELETE FROM ${schema}.${table} WHERE name = $1 AND id = ANY($2::uuid[])`,
2901
2885
  values: []
2902
2886
  };
2903
2887
  }
@@ -2922,14 +2906,14 @@ function selectJobsToFailByHeartbeat(schema, table, queues) {
2922
2906
  }
2923
2907
  function deleteJobsByIds(schema, table) {
2924
2908
  return {
2925
- text: `DELETE FROM ${schema}.${table} WHERE id IN (SELECT UNNEST($1::uuid[]))`,
2909
+ text: `DELETE FROM ${schema}.${table} WHERE id = ANY($1::uuid[])`,
2926
2910
  values: []
2927
2911
  };
2928
2912
  }
2929
2913
  function completeJobsDistributed(schema, table, includeQueued) {
2930
2914
  return `
2931
2915
  ${completeJobsUpdate(schema, table, includeQueued)}
2932
- RETURNING id, blocking
2916
+ RETURNING id
2933
2917
  `;
2934
2918
  }
2935
2919
  function decrementDependents(schema) {
@@ -2938,13 +2922,75 @@ function decrementDependents(schema) {
2938
2922
  SELECT d.child_name, d.child_id, COUNT(*)::int AS n
2939
2923
  FROM ${schema}.job_dependency d
2940
2924
  WHERE d.parent_name = $1
2941
- AND d.parent_id IN (SELECT UNNEST($2::uuid[]))
2925
+ AND d.parent_id = ANY($2::uuid[])
2942
2926
  GROUP BY d.child_name, d.child_id
2943
2927
  ),
2944
2928
  ${lockedChildrenCte(schema)}
2945
2929
  ${unblockChildrenUpdate(schema)}
2946
2930
  `;
2947
2931
  }
2932
+ var FLOW_BATCH_SIZE = 1e3;
2933
+ function resolveFlowJobs(schema, table, names) {
2934
+ return {
2935
+ text: `
2936
+ WITH locked_parents AS (
2937
+ SELECT j.name, j.id
2938
+ FROM ${schema}.${table} j
2939
+ WHERE j.blocking
2940
+ AND j.state = '${JOB_STATES.completed}'
2941
+ AND j.name = ANY($1::text[])
2942
+ ORDER BY j.name, j.id
2943
+ FOR UPDATE OF j SKIP LOCKED
2944
+ LIMIT ${FLOW_BATCH_SIZE}
2945
+ ),
2946
+ decremented AS (
2947
+ SELECT d.child_name, d.child_id, COUNT(*)::int AS n
2948
+ FROM ${schema}.job_dependency d
2949
+ JOIN locked_parents p ON d.parent_name = p.name
2950
+ AND d.parent_id = p.id
2951
+ GROUP BY d.child_name, d.child_id
2952
+ ),
2953
+ ${lockedChildrenCte(schema)},
2954
+ unblocked AS (
2955
+ ${unblockChildrenUpdate(schema)}
2956
+ RETURNING 1
2957
+ ),
2958
+ cleared AS (
2959
+ UPDATE ${schema}.${table} j
2960
+ SET blocking = false
2961
+ FROM locked_parents p
2962
+ WHERE j.name = p.name
2963
+ AND j.id = p.id
2964
+ RETURNING 1
2965
+ )
2966
+ SELECT COUNT(*)::int AS resolved FROM cleared
2967
+ `,
2968
+ values: [names]
2969
+ };
2970
+ }
2971
+ function selectBlockingParents(schema, table, names, noSkipLocked) {
2972
+ return {
2973
+ text: `
2974
+ SELECT name, id
2975
+ FROM ${schema}.${table}
2976
+ WHERE blocking
2977
+ AND state = '${JOB_STATES.completed}'
2978
+ AND name = ANY($1::text[])
2979
+ ORDER BY name, id
2980
+ FOR UPDATE${noSkipLocked ? "" : " SKIP LOCKED"}
2981
+ LIMIT ${FLOW_BATCH_SIZE}
2982
+ `,
2983
+ values: [names]
2984
+ };
2985
+ }
2986
+ function clearBlocking(schema) {
2987
+ return `
2988
+ UPDATE ${schema}.job
2989
+ SET blocking = false
2990
+ WHERE name = $1
2991
+ AND id = ANY($2::uuid[])
2992
+ `;
2993
+ }
2948
2994
  function insertRetryJob(schema, table) {
2949
2995
  return `
2950
2996
  INSERT INTO ${schema}.${table} (
@@ -2986,7 +3032,7 @@ function retryJobs(schema, table) {
2986
3032
  SET state = '${JOB_STATES.retry}',
2987
3033
  retry_limit = retry_limit + 1
2988
3034
  WHERE name = $1
2989
- AND id IN (SELECT UNNEST($2::uuid[]))
3035
+ AND id = ANY($2::uuid[])
2990
3036
  AND state = '${JOB_STATES.failed}'
2991
3037
  RETURNING 1
2992
3038
  )
@@ -3464,6 +3510,7 @@ function getConfig(value) {
3464
3510
  applyOpsConfig(config);
3465
3511
  applyScheduleConfig(config);
3466
3512
  applyBamConfig(config);
3513
+ applyFlowConfig(config);
3467
3514
  validateWarningConfig(config);
3468
3515
  return config;
3469
3516
  }
@@ -3566,6 +3613,11 @@ function applyBamConfig(config) {
3566
3613
  assert(!("bamIntervalSeconds" in config) || config.bamIntervalSeconds >= minInterval, `configuration assert: bamIntervalSeconds must be at least ${minInterval} seconds`);
3567
3614
  config.bamIntervalSeconds = config.bamIntervalSeconds || 60;
3568
3615
  }
3616
+ function applyFlowConfig(config) {
3617
+ const minInterval = config.__test__bypass_flow_interval_check ? .5 : 1;
3618
+ assert(!("flowIntervalSeconds" in config) || config.flowIntervalSeconds >= minInterval, `configuration assert: flowIntervalSeconds must be at least ${minInterval} seconds`);
3619
+ config.flowIntervalSeconds = config.flowIntervalSeconds || 5;
3620
+ }
3569
3621
  //#endregion
3570
3622
  //#region ../../src/migrationStore.ts
3571
3623
  function formatJobTable(command, table) {
@@ -4113,6 +4165,92 @@ var createQueueFn = {
4113
4165
  END;
4114
4166
  $$
4115
4167
  LANGUAGE plpgsql;
4168
+ `,
4169
+ 33: (schema) => `
4170
+ CREATE OR REPLACE FUNCTION ${schema}.create_queue(queue_name text, options jsonb)
4171
+ RETURNS VOID AS
4172
+ $$
4173
+ DECLARE
4174
+ tablename varchar := CASE WHEN options->>'partition' = 'true'
4175
+ THEN 'j' || encode(sha224(queue_name::bytea), 'hex')
4176
+ ELSE 'job_common'
4177
+ END;
4178
+ queue_created_on timestamptz;
4179
+ BEGIN
4180
+
4181
+ WITH q as (
4182
+ INSERT INTO ${schema}.queue (
4183
+ name,
4184
+ policy,
4185
+ retry_limit,
4186
+ retry_delay,
4187
+ retry_backoff,
4188
+ retry_delay_max,
4189
+ expire_seconds,
4190
+ retention_seconds,
4191
+ deletion_seconds,
4192
+ warning_queued,
4193
+ dead_letter,
4194
+ partition,
4195
+ table_name,
4196
+ heartbeat_seconds,
4197
+ notify
4198
+ )
4199
+ VALUES (
4200
+ queue_name,
4201
+ options->>'policy',
4202
+ COALESCE((options->>'retryLimit')::int, 2),
4203
+ COALESCE((options->>'retryDelay')::int, 0),
4204
+ COALESCE((options->>'retryBackoff')::bool, false),
4205
+ (options->>'retryDelayMax')::int,
4206
+ COALESCE((options->>'expireInSeconds')::int, 900),
4207
+ COALESCE((options->>'retentionSeconds')::int, 1209600),
4208
+ COALESCE((options->>'deleteAfterSeconds')::int, 604800),
4209
+ COALESCE((options->>'warningQueueSize')::int, 0),
4210
+ options->>'deadLetter',
4211
+ COALESCE((options->>'partition')::bool, false),
4212
+ tablename,
4213
+ (options->>'heartbeatSeconds')::int,
4214
+ COALESCE((options->>'notify')::bool, false)
4215
+ )
4216
+ ON CONFLICT DO NOTHING
4217
+ RETURNING created_on
4218
+ )
4219
+ SELECT created_on into queue_created_on from q;
4220
+
4221
+ IF queue_created_on IS NULL OR options->>'partition' IS DISTINCT FROM 'true' THEN
4222
+ RETURN;
4223
+ END IF;
4224
+
4225
+ EXECUTE format('CREATE TABLE ${schema}.%I (LIKE ${schema}.job INCLUDING DEFAULTS)', tablename);
4226
+
4227
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD PRIMARY KEY (name, id)$cmd$, tablename);
4228
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT q_fkey FOREIGN KEY (name) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
4229
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT dlq_fkey FOREIGN KEY (dead_letter) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
4230
+
4231
+ EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i5 ON ${schema}.job (name, start_after) WHERE state < 'active' AND NOT blocked$cmd$, tablename);
4232
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i4 ON ${schema}.job (name, singleton_on, COALESCE(singleton_key, '')) WHERE state <> 'cancelled' AND singleton_on IS NOT NULL$cmd$, tablename);
4233
+ EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i7 ON ${schema}.job (name, group_id) WHERE state = 'active' AND group_id IS NOT NULL$cmd$, tablename);
4234
+ EXECUTE ${schema}.job_table_format($cmd$CREATE INDEX job_i9 ON ${schema}.job (name, id) WHERE blocking AND state = 'completed'$cmd$, tablename);
4235
+
4236
+ IF options->>'policy' = 'short' THEN
4237
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i1 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = 'created' AND policy = 'short'$cmd$, tablename);
4238
+ ELSIF options->>'policy' = 'singleton' THEN
4239
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i2 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = 'active' AND policy = 'singleton'$cmd$, tablename);
4240
+ ELSIF options->>'policy' = 'stately' THEN
4241
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i3 ON ${schema}.job (name, state, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'stately'$cmd$, tablename);
4242
+ ELSIF options->>'policy' = 'exclusive' THEN
4243
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i6 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'exclusive'$cmd$, tablename);
4244
+ ELSIF options->>'policy' = 'key_strict_fifo' THEN
4245
+ EXECUTE ${schema}.job_table_format($cmd$CREATE UNIQUE INDEX job_i8 ON ${schema}.job (name, singleton_key) WHERE state IN ('active', 'retry', 'failed') AND policy = 'key_strict_fifo'$cmd$, tablename);
4246
+ EXECUTE ${schema}.job_table_format($cmd$ALTER TABLE ${schema}.job ADD CONSTRAINT job_key_strict_fifo_singleton_key_check CHECK (NOT (policy = 'key_strict_fifo' AND singleton_key IS NULL))$cmd$, tablename);
4247
+ END IF;
4248
+
4249
+ EXECUTE format('ALTER TABLE ${schema}.%I ADD CONSTRAINT cjc CHECK (name=%L)', tablename, queue_name);
4250
+ EXECUTE format('ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.%I FOR VALUES IN (%L)', tablename, queue_name);
4251
+ END;
4252
+ $$
4253
+ LANGUAGE plpgsql;
4116
4254
  `
4117
4255
  };
4118
4256
  function getAll(schema) {
@@ -4361,6 +4499,25 @@ function getAll(schema) {
4361
4499
  createQueueFn[31](schema),
4362
4500
  `ALTER TABLE ${schema}.queue DROP COLUMN notify`
4363
4501
  ]
4502
+ },
4503
+ {
4504
+ release: "12.22.0",
4505
+ version: 33,
4506
+ previous: 32,
4507
+ install: [
4508
+ `ALTER TABLE ${schema}.version ADD COLUMN IF NOT EXISTS flow_on timestamp with time zone`,
4509
+ `SELECT ${schema}.job_table_run($cmd$CREATE INDEX job_i9 ON ${schema}.job (name, id) WHERE blocking AND state = 'completed'$cmd$)`,
4510
+ `SELECT ${schema}.job_table_run($cmd$DROP INDEX IF EXISTS ${schema}.job_i5$cmd$)`,
4511
+ `SELECT ${schema}.job_table_run($cmd$CREATE INDEX job_i5 ON ${schema}.job (name, start_after) WHERE state < 'active' AND NOT blocked$cmd$)`,
4512
+ createQueueFn[33](schema)
4513
+ ],
4514
+ uninstall: [
4515
+ createQueueFn[32](schema),
4516
+ `SELECT ${schema}.job_table_run($cmd$DROP INDEX IF EXISTS ${schema}.job_i5$cmd$)`,
4517
+ `SELECT ${schema}.job_table_run($cmd$CREATE INDEX job_i5 ON ${schema}.job (name, start_after) INCLUDE (priority, created_on, id) WHERE state < 'active' AND NOT blocked$cmd$)`,
4518
+ `SELECT ${schema}.job_table_run($cmd$DROP INDEX IF EXISTS ${schema}.job_i9$cmd$)`,
4519
+ `ALTER TABLE ${schema}.version DROP COLUMN flow_on`
4520
+ ]
4364
4521
  }
4365
4522
  ];
4366
4523
  }
@@ -4368,7 +4525,7 @@ function getAll(schema) {
4368
4525
  //#region ../../src/contractor.ts
4369
4526
  var schemaVersion = {
4370
4527
  name: "pg-boss",
4371
- version: "12.21.0",
4528
+ version: "12.22.0",
4372
4529
  description: "Queueing jobs in Postgres from Node.js like a boss",
4373
4530
  type: "module",
4374
4531
  main: "./dist/index.js",
@@ -4376,7 +4533,7 @@ var schemaVersion = {
4376
4533
  bin: { "pg-boss": "./dist/cli.js" },
4377
4534
  engines: { "node": ">=22.12.0" },
4378
4535
  dependencies: {
4379
- "cron-parser": "^5.6.0",
4536
+ "cron-parser": "^5.6.1",
4380
4537
  "pg": "^8.22.0",
4381
4538
  "serialize-error": "^13.0.1"
4382
4539
  },
@@ -4424,7 +4581,7 @@ var schemaVersion = {
4424
4581
  "docs": "npm run docs:dev --prefix docs",
4425
4582
  "docs:readme": "node ./scripts/sync-readme.js"
4426
4583
  },
4427
- pgboss: { "schema": 32 },
4584
+ pgboss: { "schema": 33 },
4428
4585
  repository: {
4429
4586
  "type": "git",
4430
4587
  "url": "git+https://github.com/timgit/pg-boss.git"
@@ -4938,7 +5095,7 @@ var NUMERIC_QUEUE_FIELDS = [
4938
5095
  "activeCount",
4939
5096
  "totalCount"
4940
5097
  ];
4941
- var events$4 = {
5098
+ var events$5 = {
4942
5099
  error: "error",
4943
5100
  wip: "wip"
4944
5101
  };
@@ -4947,7 +5104,7 @@ function rethrowWriteError(err) {
4947
5104
  throw err;
4948
5105
  }
4949
5106
  var Manager = class extends EventEmitter {
4950
- events = events$4;
5107
+ events = events$5;
4951
5108
  db;
4952
5109
  config;
4953
5110
  wipTs;
@@ -5110,17 +5267,15 @@ var Manager = class extends EventEmitter {
5110
5267
  output: this.mapCompletionDataArg(item.output)
5111
5268
  }));
5112
5269
  const ids = items.map((item) => item.id);
5113
- if (this.config.noMultiMutationCte) return this.withDistributedTransaction(this.db, async (tx) => {
5270
+ if (this.config.noMultiMutationCte) {
5114
5271
  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]);
5272
+ const { rows } = await this.db.executeSql(sql, [name, JSON.stringify(payload)]);
5118
5273
  return {
5119
5274
  jobs: ids,
5120
5275
  requested: ids.length,
5121
5276
  affected: rows.length
5122
5277
  };
5123
- });
5278
+ }
5124
5279
  const sql = completeJobsWithOutputs(this.config.schema, table);
5125
5280
  const result = await this.db.executeSql(sql, [name, JSON.stringify(payload)]);
5126
5281
  return this.mapCommandResponse(ids, result);
@@ -5208,7 +5363,7 @@ var Manager = class extends EventEmitter {
5208
5363
  try {
5209
5364
  await this.touch(name, jobIds);
5210
5365
  } catch (err) {
5211
- this.emit(events$4.error, err);
5366
+ this.emit(events$5.error, err);
5212
5367
  }
5213
5368
  }, intervalMs);
5214
5369
  }
@@ -5245,7 +5400,7 @@ var Manager = class extends EventEmitter {
5245
5400
  if (now - this.wipTs < 2e3) return;
5246
5401
  const wip = this.getWipData();
5247
5402
  if (wip.some((w) => w.count > 0)) {
5248
- this.emit(events$4.wip, wip);
5403
+ this.emit(events$5.wip, wip);
5249
5404
  this.wipTs = now;
5250
5405
  }
5251
5406
  }, 2e3);
@@ -5260,7 +5415,7 @@ var Manager = class extends EventEmitter {
5260
5415
  return acc;
5261
5416
  }, {});
5262
5417
  } catch (error) {
5263
- emit && this.emit(events$4.error, {
5418
+ emit && this.emit(events$5.error, {
5264
5419
  ...error,
5265
5420
  message: error.message,
5266
5421
  stack: error.stack
@@ -5340,7 +5495,7 @@ var Manager = class extends EventEmitter {
5340
5495
  this.emitWip(name);
5341
5496
  };
5342
5497
  const onError = (error) => {
5343
- this.emit(events$4.error, {
5498
+ this.emit(events$5.error, {
5344
5499
  ...error,
5345
5500
  message: error.message,
5346
5501
  stack: error.stack,
@@ -5379,7 +5534,7 @@ var Manager = class extends EventEmitter {
5379
5534
  if (!INTERNAL_QUEUES[name]) {
5380
5535
  const now = Date.now();
5381
5536
  if (now - this.wipTs > 2e3) {
5382
- this.emit(events$4.wip, this.getWipData());
5537
+ this.emit(events$5.wip, this.getWipData());
5383
5538
  this.wipTs = now;
5384
5539
  }
5385
5540
  }
@@ -5714,24 +5869,17 @@ var Manager = class extends EventEmitter {
5714
5869
  return fn(db);
5715
5870
  }
5716
5871
  async completeDistributed(name, ids, outputData, table, db, includeQueued) {
5717
- return this.withDistributedTransaction(db, async (tx) => {
5718
- const completeSql = completeJobsDistributed(this.config.schema, table, includeQueued);
5719
- const { rows } = await tx.executeSql(completeSql, [
5720
- name,
5721
- ids,
5722
- outputData
5723
- ]);
5724
- const blockingIds = rows.filter((row) => row.blocking).map((row) => row.id);
5725
- if (blockingIds.length > 0) {
5726
- const decrementSql = decrementDependents(this.config.schema);
5727
- await tx.executeSql(decrementSql, [name, blockingIds]);
5728
- }
5729
- return {
5730
- jobs: ids,
5731
- requested: ids.length,
5732
- affected: rows.length
5733
- };
5734
- });
5872
+ const sql = completeJobsDistributed(this.config.schema, table, includeQueued);
5873
+ const { rows } = await db.executeSql(sql, [
5874
+ name,
5875
+ ids,
5876
+ outputData
5877
+ ]);
5878
+ return {
5879
+ jobs: ids,
5880
+ requested: ids.length,
5881
+ affected: rows.length
5882
+ };
5735
5883
  }
5736
5884
  async fail(name, id, data, options = {}) {
5737
5885
  assertQueueName(name);
@@ -5775,6 +5923,26 @@ var Manager = class extends EventEmitter {
5775
5923
  const select = selectJobsToFailByHeartbeat(this.config.schema, table, queues);
5776
5924
  return this.expireJobsDistributed(table, select, { value: { message: "job heartbeat timeout" } });
5777
5925
  }
5926
+ async resolveFlowJobsDistributed(table, names) {
5927
+ const select = selectBlockingParents(this.config.schema, table, names, this.config.noSkipLocked);
5928
+ return this.withDistributedTransaction(this.db, async (tx) => {
5929
+ const { rows } = await tx.executeSql(select.text, select.values);
5930
+ if (rows.length === 0) return 0;
5931
+ const idsByName = /* @__PURE__ */ new Map();
5932
+ for (const row of rows) {
5933
+ const list = idsByName.get(row.name) || [];
5934
+ list.push(row.id);
5935
+ idsByName.set(row.name, list);
5936
+ }
5937
+ const decrementSql = decrementDependents(this.config.schema);
5938
+ const clearSql = clearBlocking(this.config.schema);
5939
+ for (const [name, ids] of idsByName) {
5940
+ await tx.executeSql(decrementSql, [name, ids]);
5941
+ await tx.executeSql(clearSql, [name, ids]);
5942
+ }
5943
+ return rows.length;
5944
+ });
5945
+ }
5778
5946
  async expireJobsDistributed(table, select, outputData) {
5779
5947
  return this.withDistributedTransaction(this.db, async (tx) => {
5780
5948
  const { rows: jobs } = await tx.executeSql(select.text, []);
@@ -6099,7 +6267,7 @@ var Manager = class extends EventEmitter {
6099
6267
  };
6100
6268
  //#endregion
6101
6269
  //#region ../../src/boss.ts
6102
- var events$3 = {
6270
+ var events$4 = {
6103
6271
  error: "error",
6104
6272
  warning: "warning"
6105
6273
  };
@@ -6125,7 +6293,7 @@ var Boss = class extends EventEmitter {
6125
6293
  #db;
6126
6294
  #config;
6127
6295
  #manager;
6128
- events = events$3;
6296
+ events = events$4;
6129
6297
  constructor(db, manager, config) {
6130
6298
  super();
6131
6299
  this.#db = db;
@@ -6160,8 +6328,8 @@ var Boss = class extends EventEmitter {
6160
6328
  db: this.#db,
6161
6329
  schema: this.#config.schema,
6162
6330
  persistWarnings: this.#config.persistWarnings,
6163
- warningEvent: events$3.warning,
6164
- errorEvent: events$3.error
6331
+ warningEvent: events$4.warning,
6332
+ errorEvent: events$4.error
6165
6333
  };
6166
6334
  }
6167
6335
  async #executeQuery(query) {
@@ -6190,7 +6358,7 @@ var Boss = class extends EventEmitter {
6190
6358
  !this.#stopped && await this.supervise(queues);
6191
6359
  !this.#stopped && await this.#maintainWarnings();
6192
6360
  } catch (err) {
6193
- this.emit(events$3.error, err);
6361
+ this.emit(events$4.error, err);
6194
6362
  } finally {
6195
6363
  this.#maintaining = false;
6196
6364
  }
@@ -6268,7 +6436,7 @@ var Boss = class extends EventEmitter {
6268
6436
  };
6269
6437
  //#endregion
6270
6438
  //#region ../../src/bam.ts
6271
- var events$2 = {
6439
+ var events$3 = {
6272
6440
  error: "error",
6273
6441
  bam: "bam"
6274
6442
  };
@@ -6278,7 +6446,7 @@ var Bam = class extends EventEmitter {
6278
6446
  #pollInterval;
6279
6447
  #db;
6280
6448
  #config;
6281
- events = events$2;
6449
+ events = events$3;
6282
6450
  constructor(db, config) {
6283
6451
  super();
6284
6452
  this.#db = db;
@@ -6314,7 +6482,7 @@ var Bam = class extends EventEmitter {
6314
6482
  const { rows } = await this.#db.executeSql(sql);
6315
6483
  if (rows.length === 1) await this.#processCommands();
6316
6484
  } catch (err) {
6317
- this.emit(events$2.error, err);
6485
+ this.emit(events$3.error, err);
6318
6486
  } finally {
6319
6487
  this.#working = false;
6320
6488
  }
@@ -6323,7 +6491,7 @@ var Bam = class extends EventEmitter {
6323
6491
  if (this.#stopped) return;
6324
6492
  const entry = await this.#getNextCommand();
6325
6493
  if (!entry || this.#stopped) return;
6326
- this.emit(events$2.bam, {
6494
+ this.emit(events$3.bam, {
6327
6495
  id: entry.id,
6328
6496
  name: entry.name,
6329
6497
  status: "in_progress",
@@ -6334,7 +6502,7 @@ var Bam = class extends EventEmitter {
6334
6502
  await this.#db.executeSql(entry.command);
6335
6503
  if (this.#stopped) return;
6336
6504
  await this.#markCompleted(entry.id);
6337
- this.emit(events$2.bam, {
6505
+ this.emit(events$3.bam, {
6338
6506
  id: entry.id,
6339
6507
  name: entry.name,
6340
6508
  status: "completed",
@@ -6344,8 +6512,8 @@ var Bam = class extends EventEmitter {
6344
6512
  } catch (err) {
6345
6513
  if (this.#stopped) return;
6346
6514
  await this.#markFailed(entry.id, err);
6347
- this.emit(events$2.error, err);
6348
- this.emit(events$2.bam, {
6515
+ this.emit(events$3.error, err);
6516
+ this.emit(events$3.bam, {
6349
6517
  id: entry.id,
6350
6518
  name: entry.name,
6351
6519
  status: "failed",
@@ -6370,6 +6538,111 @@ var Bam = class extends EventEmitter {
6370
6538
  }
6371
6539
  };
6372
6540
  //#endregion
6541
+ //#region ../../src/navigator.ts
6542
+ var events$2 = {
6543
+ error: "error",
6544
+ flow: "flow"
6545
+ };
6546
+ var MAX_BATCHES_PER_PASS = 100;
6547
+ var Navigator = class extends EventEmitter {
6548
+ #stopped;
6549
+ #stopping;
6550
+ #working;
6551
+ #pollInterval;
6552
+ #db;
6553
+ #manager;
6554
+ #config;
6555
+ events = events$2;
6556
+ constructor(db, manager, config) {
6557
+ super();
6558
+ this.#db = db;
6559
+ this.#manager = manager;
6560
+ this.#config = config;
6561
+ this.#stopped = true;
6562
+ this.#stopping = false;
6563
+ this.#working = false;
6564
+ }
6565
+ get working() {
6566
+ return this.#working;
6567
+ }
6568
+ async start() {
6569
+ if (!this.#stopped) return;
6570
+ this.#stopped = false;
6571
+ this.#stopping = false;
6572
+ setImmediate(() => this.#onPoll());
6573
+ this.#pollInterval = setInterval(() => this.#onPoll(), this.#config.flowIntervalSeconds * 1e3);
6574
+ }
6575
+ async stop() {
6576
+ if (this.#stopped) return;
6577
+ this.#stopping = true;
6578
+ this.#stopped = true;
6579
+ if (this.#pollInterval) {
6580
+ clearInterval(this.#pollInterval);
6581
+ this.#pollInterval = void 0;
6582
+ }
6583
+ while (this.#working) await delay(10);
6584
+ }
6585
+ async #onPoll() {
6586
+ if (this.#stopped || this.#working) return;
6587
+ this.#working = true;
6588
+ try {
6589
+ if (this.#config.__test__throw_flow) throw new Error(this.#config.__test__throw_flow);
6590
+ if (this.#config.__test__delay_flow_ms) await delay(this.#config.__test__delay_flow_ms);
6591
+ const gate = trySetFlowTime(this.#config.schema, this.#config.flowIntervalSeconds);
6592
+ const { rows } = await this.#db.executeSql(gate);
6593
+ if (rows.length === 1) await this.#resolve();
6594
+ } catch (err) {
6595
+ this.emit(events$2.error, err);
6596
+ } finally {
6597
+ this.#working = false;
6598
+ }
6599
+ }
6600
+ async resolveNow() {
6601
+ while (this.#working) await delay(10);
6602
+ if (this.#stopping) return;
6603
+ this.#working = true;
6604
+ try {
6605
+ await this.#resolve();
6606
+ } finally {
6607
+ this.#working = false;
6608
+ }
6609
+ }
6610
+ async #resolve() {
6611
+ const queueGroups = (await this.#manager.getQueues()).reduce((acc, q) => {
6612
+ acc[q.table] = acc[q.table] || {
6613
+ table: q.table,
6614
+ names: []
6615
+ };
6616
+ acc[q.table].names.push(q.name);
6617
+ return acc;
6618
+ }, {});
6619
+ for (const group of Object.values(queueGroups)) {
6620
+ if (this.#stopping) return;
6621
+ const { table } = group;
6622
+ const names = [...group.names];
6623
+ while (names.length) {
6624
+ if (this.#stopping) return;
6625
+ const chunk = names.splice(0, 100);
6626
+ let batches = 0;
6627
+ let resolved = 0;
6628
+ do {
6629
+ if (this.#stopping) return;
6630
+ resolved = this.#config.noMultiMutationCte ? await this.#manager.resolveFlowJobsDistributed(table, chunk) : await this.#resolveStandard(table, chunk);
6631
+ if (resolved > 0) this.emit(events$2.flow, {
6632
+ table,
6633
+ resolved
6634
+ });
6635
+ } while (resolved >= 1e3 && ++batches < MAX_BATCHES_PER_PASS && !this.#stopping);
6636
+ }
6637
+ }
6638
+ }
6639
+ async #resolveStandard(table, names) {
6640
+ const query = resolveFlowJobs(this.#config.schema, table, names);
6641
+ const { rows } = await this.#db.executeSql(query.text, query.values);
6642
+ return Number(rows[0]?.resolved ?? 0);
6643
+ }
6644
+ };
6645
+ //#endregion
6373
6646
  //#region ../../src/notifier.ts
6374
6647
  var events$1 = {
6375
6648
  error: "error",
@@ -6450,7 +6723,7 @@ var Db = class extends EventEmitter {
6450
6723
  constructor(config) {
6451
6724
  super();
6452
6725
  config.application_name = config.application_name || "pgboss";
6453
- config.connectionTimeoutMillis = config.connectionTimeoutMillis || 1e4;
6726
+ config.connectionTimeoutMillis ??= 1e4;
6454
6727
  this.config = config;
6455
6728
  this._pgbdb = true;
6456
6729
  this.opened = false;
@@ -6551,7 +6824,8 @@ var events = Object.freeze({
6551
6824
  warning: "warning",
6552
6825
  wip: "wip",
6553
6826
  stopped: "stopped",
6554
- bam: "bam"
6827
+ bam: "bam",
6828
+ flow: "flow"
6555
6829
  });
6556
6830
  var PgBoss = class extends EventEmitter {
6557
6831
  #stoppingOn;
@@ -6565,6 +6839,7 @@ var PgBoss = class extends EventEmitter {
6565
6839
  #manager;
6566
6840
  #timekeeper;
6567
6841
  #bam;
6842
+ #navigator;
6568
6843
  #notifier;
6569
6844
  constructor(value) {
6570
6845
  super();
@@ -6581,18 +6856,21 @@ var PgBoss = class extends EventEmitter {
6581
6856
  const timekeeper = new Timekeeper(db, manager, config);
6582
6857
  manager.timekeeper = timekeeper;
6583
6858
  const bam = new Bam(db, config);
6859
+ const navigator = new Navigator(db, manager, config);
6584
6860
  const notifier = new Notifier(db, manager, config);
6585
6861
  manager.notifier = notifier;
6586
6862
  this.#promoteEvents(manager);
6587
6863
  this.#promoteEvents(boss);
6588
6864
  this.#promoteEvents(timekeeper);
6589
6865
  this.#promoteEvents(bam);
6866
+ this.#promoteEvents(navigator);
6590
6867
  this.#promoteEvents(notifier);
6591
6868
  this.#boss = boss;
6592
6869
  this.#contractor = contractor;
6593
6870
  this.#manager = manager;
6594
6871
  this.#timekeeper = timekeeper;
6595
6872
  this.#bam = bam;
6873
+ this.#navigator = navigator;
6596
6874
  this.#notifier = notifier;
6597
6875
  }
6598
6876
  #promoteEvents(emitter) {
@@ -6608,7 +6886,10 @@ var PgBoss = class extends EventEmitter {
6608
6886
  else await this.#contractor.check();
6609
6887
  await this.#manager.start();
6610
6888
  if (this.#config.useListenNotify) await this.#notifier.start();
6611
- if (this.#config.supervise) await this.#boss.start();
6889
+ if (this.#config.supervise) {
6890
+ await this.#boss.start();
6891
+ await this.#navigator.start();
6892
+ }
6612
6893
  if (this.#config.schedule) await this.#timekeeper.start();
6613
6894
  if (this.#config.migrate) await this.#bam.start();
6614
6895
  } catch (err) {
@@ -6641,6 +6922,7 @@ var PgBoss = class extends EventEmitter {
6641
6922
  await this.#manager.stop();
6642
6923
  await this.#timekeeper.stop();
6643
6924
  await this.#boss.stop();
6925
+ await this.#navigator.stop();
6644
6926
  await this.#bam.stop();
6645
6927
  const shutdown = async () => {
6646
6928
  await this.#manager.failWip();
@@ -6754,7 +7036,7 @@ var PgBoss = class extends EventEmitter {
6754
7036
  return this.#manager.deleteQueue(name);
6755
7037
  }
6756
7038
  getQueues(names) {
6757
- return this.#manager.getQueues();
7039
+ return this.#manager.getQueues(names);
6758
7040
  }
6759
7041
  getQueue(name) {
6760
7042
  return this.#manager.getQueue(name);
@@ -6768,12 +7050,18 @@ var PgBoss = class extends EventEmitter {
6768
7050
  isBamWorking() {
6769
7051
  return this.#bam.working;
6770
7052
  }
7053
+ isResolvingFlow() {
7054
+ return this.#navigator.working;
7055
+ }
6771
7056
  isCheckingSkew() {
6772
7057
  return this.#timekeeper.checkingSkew;
6773
7058
  }
6774
7059
  supervise(name) {
6775
7060
  return this.#boss.supervise(name);
6776
7061
  }
7062
+ resolveFlow() {
7063
+ return this.#navigator.resolveNow();
7064
+ }
6777
7065
  getWipData(options) {
6778
7066
  return this.#manager.getWipData(options);
6779
7067
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pg-boss/dashboard",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "Web dashboard for monitoring and managing pg-boss job queues",
5
5
  "keywords": [
6
6
  "pg-boss",
@@ -53,7 +53,7 @@
53
53
  "isbot": "^5.1.44",
54
54
  "lucide-react": "^1.21.0",
55
55
  "pg": "^8.22.0",
56
- "pg-boss": "^12.21.0",
56
+ "pg-boss": "^12.22.0",
57
57
  "react": "^19.2.7",
58
58
  "react-dom": "^19.2.7",
59
59
  "react-router": "^8.0.1",