pg-boss 10.3.3 → 10.4.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 (48) hide show
  1. package/.claude/settings.local.json +25 -0
  2. package/dist/attorney.d.ts +19 -0
  3. package/dist/attorney.d.ts.map +1 -0
  4. package/dist/attorney.js +227 -0
  5. package/dist/bam.d.ts +14 -0
  6. package/dist/bam.d.ts.map +1 -0
  7. package/dist/bam.js +114 -0
  8. package/dist/boss.d.ts +16 -0
  9. package/dist/boss.d.ts.map +1 -0
  10. package/dist/boss.js +163 -0
  11. package/dist/cli.d.ts +3 -0
  12. package/dist/cli.d.ts.map +1 -0
  13. package/dist/cli.js +333 -0
  14. package/dist/contractor.d.ts +22 -0
  15. package/dist/contractor.d.ts.map +1 -0
  16. package/dist/contractor.js +81 -0
  17. package/dist/db.d.ts +16 -0
  18. package/dist/db.d.ts.map +1 -0
  19. package/dist/db.js +46 -0
  20. package/dist/index.d.ts +72 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.js +266 -0
  23. package/dist/manager.d.ts +93 -0
  24. package/dist/manager.d.ts.map +1 -0
  25. package/dist/manager.js +746 -0
  26. package/dist/migrationStore.d.ts +7 -0
  27. package/dist/migrationStore.d.ts.map +1 -0
  28. package/dist/migrationStore.js +425 -0
  29. package/dist/plans.d.ts +102 -0
  30. package/dist/plans.d.ts.map +1 -0
  31. package/dist/plans.js +1220 -0
  32. package/dist/spy.d.ts +23 -0
  33. package/dist/spy.d.ts.map +1 -0
  34. package/dist/spy.js +73 -0
  35. package/dist/timekeeper.d.ts +34 -0
  36. package/dist/timekeeper.d.ts.map +1 -0
  37. package/dist/timekeeper.js +158 -0
  38. package/dist/tools.d.ts +18 -0
  39. package/dist/tools.d.ts.map +1 -0
  40. package/dist/tools.js +45 -0
  41. package/dist/types.d.ts +301 -0
  42. package/dist/types.d.ts.map +1 -0
  43. package/dist/types.js +1 -0
  44. package/dist/worker.d.ts +43 -0
  45. package/dist/worker.d.ts.map +1 -0
  46. package/dist/worker.js +113 -0
  47. package/package.json +1 -1
  48. package/src/plans.js +22 -7
@@ -0,0 +1,43 @@
1
+ import type * as types from './types.ts';
2
+ interface WorkerOptions<T> {
3
+ id: string;
4
+ name: string;
5
+ options: types.WorkOptions;
6
+ interval: number;
7
+ fetch: () => Promise<types.Job<T>[]>;
8
+ onFetch: (jobs: types.Job<T>[]) => Promise<void>;
9
+ onError: (err: any) => void;
10
+ }
11
+ declare class Worker<T = unknown> {
12
+ readonly id: string;
13
+ readonly name: string;
14
+ readonly options: types.WorkOptions;
15
+ readonly fetch: () => Promise<types.Job<T>[]>;
16
+ readonly onFetch: (jobs: types.Job<T>[]) => Promise<void>;
17
+ readonly onError: (err: any) => void;
18
+ readonly interval: number;
19
+ jobs: types.Job<T>[];
20
+ createdOn: number;
21
+ state: types.WorkerState;
22
+ lastFetchedOn: number | null;
23
+ lastJobStartedOn: number | null;
24
+ lastJobEndedOn: number | null;
25
+ lastJobDuration: number | null;
26
+ lastError: any;
27
+ lastErrorOn: number | null;
28
+ stopping: boolean;
29
+ stopped: boolean;
30
+ abortController: AbortController | null;
31
+ private loopDelayPromise;
32
+ private beenNotified;
33
+ private runPromise;
34
+ constructor({ id, name, options, interval, fetch, onFetch, onError }: WorkerOptions<T>);
35
+ start(): void;
36
+ private run;
37
+ notify(): void;
38
+ stop(): Promise<void>;
39
+ abort(): void;
40
+ toWipData(): types.WipData;
41
+ }
42
+ export default Worker;
43
+ //# sourceMappingURL=worker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../src/worker.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,KAAK,MAAM,YAAY,CAAA;AASxC,UAAU,aAAa,CAAC,CAAC;IACvB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,KAAK,CAAC,WAAW,CAAA;IAC1B,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;IACpC,OAAO,EAAE,CAAC,IAAI,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IAChD,OAAO,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;CAC5B;AAED,cAAM,MAAM,CAAC,CAAC,GAAG,OAAO;IACtB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,WAAW,CAAA;IACnC,QAAQ,CAAC,KAAK,EAAE,MAAM,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;IAC7C,QAAQ,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACzD,QAAQ,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IACpC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IAEzB,IAAI,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAK;IACzB,SAAS,SAAa;IACtB,KAAK,EAAE,KAAK,CAAC,WAAW,CAAwB;IAChD,aAAa,EAAE,MAAM,GAAG,IAAI,CAAO;IACnC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAO;IACtC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAO;IACpC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAO;IACrC,SAAS,EAAE,GAAG,CAAO;IACrB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAO;IACjC,QAAQ,UAAQ;IAChB,OAAO,UAAQ;IACf,eAAe,EAAE,eAAe,GAAG,IAAI,CAAO;IAC9C,OAAO,CAAC,gBAAgB,CAAsC;IAC9D,OAAO,CAAC,YAAY,CAAQ;IAC5B,OAAO,CAAC,UAAU,CAA6B;gBAElC,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,aAAa,CAAC,CAAC,CAAC;IAUvF,KAAK;YAIS,GAAG;IAgDjB,MAAM;IAQA,IAAI,IAAK,OAAO,CAAC,IAAI,CAAC;IAW5B,KAAK,IAAK,IAAI;IAMd,SAAS,IAAK,KAAK,CAAC,OAAO;CAgB5B;AAED,eAAe,MAAM,CAAA"}
package/dist/worker.js ADDED
@@ -0,0 +1,113 @@
1
+ import { delay } from "./tools.js";
2
+ const WORKER_STATES = {
3
+ created: 'created',
4
+ active: 'active',
5
+ stopping: 'stopping',
6
+ stopped: 'stopped'
7
+ };
8
+ class Worker {
9
+ id;
10
+ name;
11
+ options;
12
+ fetch;
13
+ onFetch;
14
+ onError;
15
+ interval;
16
+ jobs = [];
17
+ createdOn = Date.now();
18
+ state = WORKER_STATES.created;
19
+ lastFetchedOn = null;
20
+ lastJobStartedOn = null;
21
+ lastJobEndedOn = null;
22
+ lastJobDuration = null;
23
+ lastError = null;
24
+ lastErrorOn = null;
25
+ stopping = false;
26
+ stopped = false;
27
+ abortController = null;
28
+ loopDelayPromise = null;
29
+ beenNotified = false;
30
+ runPromise = null;
31
+ constructor({ id, name, options, interval, fetch, onFetch, onError }) {
32
+ this.id = id;
33
+ this.name = name;
34
+ this.options = options;
35
+ this.fetch = fetch;
36
+ this.onFetch = onFetch;
37
+ this.onError = onError;
38
+ this.interval = interval;
39
+ }
40
+ start() {
41
+ this.runPromise = this.run();
42
+ }
43
+ async run() {
44
+ this.state = WORKER_STATES.active;
45
+ while (!this.stopping) {
46
+ const started = Date.now();
47
+ try {
48
+ this.beenNotified = false;
49
+ const jobs = await this.fetch();
50
+ this.lastFetchedOn = Date.now();
51
+ if (jobs) {
52
+ this.jobs = jobs;
53
+ this.lastJobStartedOn = this.lastFetchedOn;
54
+ await this.onFetch(jobs);
55
+ this.lastJobEndedOn = Date.now();
56
+ this.jobs = [];
57
+ }
58
+ }
59
+ catch (err) {
60
+ this.lastErrorOn = Date.now();
61
+ this.lastError = err;
62
+ err.message = `${err.message} (Queue: ${this.name}, Worker: ${this.id})`;
63
+ this.onError(err);
64
+ }
65
+ const duration = Date.now() - started;
66
+ this.lastJobDuration = duration;
67
+ if (!this.stopping && !this.beenNotified && (this.interval - duration) > 100) {
68
+ this.loopDelayPromise = delay(this.interval - duration);
69
+ await this.loopDelayPromise;
70
+ this.loopDelayPromise = null;
71
+ }
72
+ }
73
+ this.stopping = false;
74
+ this.stopped = true;
75
+ this.state = WORKER_STATES.stopped;
76
+ }
77
+ notify() {
78
+ this.beenNotified = true;
79
+ if (this.loopDelayPromise) {
80
+ this.loopDelayPromise.abort();
81
+ }
82
+ }
83
+ async stop() {
84
+ this.stopping = true;
85
+ this.state = WORKER_STATES.stopping;
86
+ if (this.loopDelayPromise) {
87
+ this.loopDelayPromise.abort();
88
+ }
89
+ await this.runPromise;
90
+ }
91
+ abort() {
92
+ if (this.abortController && !this.abortController.signal.aborted) {
93
+ this.abortController.abort();
94
+ }
95
+ }
96
+ toWipData() {
97
+ return {
98
+ id: this.id,
99
+ name: this.name,
100
+ options: this.options,
101
+ state: this.state,
102
+ count: this.jobs.length,
103
+ createdOn: this.createdOn,
104
+ lastFetchedOn: this.lastFetchedOn,
105
+ lastJobStartedOn: this.lastJobStartedOn,
106
+ lastJobEndedOn: this.lastJobEndedOn,
107
+ lastError: this.lastError,
108
+ lastErrorOn: this.lastErrorOn,
109
+ lastJobDuration: this.lastJobDuration
110
+ };
111
+ }
112
+ }
113
+ export default Worker;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pg-boss",
3
- "version": "10.3.3",
3
+ "version": "10.4.1",
4
4
  "description": "Queueing jobs in Postgres from Node.js like a boss",
5
5
  "main": "./src/index.js",
6
6
  "engines": {
package/src/plans.js CHANGED
@@ -506,7 +506,7 @@ function insertVersion (schema, version) {
506
506
  function fetchNextJob (schema) {
507
507
  return ({ includeMetadata, priority = true, ignoreStartAfter = false } = {}) => `
508
508
  WITH next as (
509
- SELECT id
509
+ SELECT id, singleton_key, policy
510
510
  FROM ${schema}.job
511
511
  WHERE name = $1
512
512
  AND state < '${JOB_STATES.active}'
@@ -514,13 +514,28 @@ function fetchNextJob (schema) {
514
514
  ORDER BY ${priority ? 'priority desc, ' : ''}created_on, id
515
515
  LIMIT $2
516
516
  FOR UPDATE SKIP LOCKED
517
+ ), grouped as (
518
+ SELECT
519
+ n.id,
520
+ n.policy as grouped_policy,
521
+ row_number() OVER (
522
+ PARTITION BY CASE
523
+ WHEN $2 > 1 AND n.policy in ('${QUEUE_POLICIES.singleton}', '${QUEUE_POLICIES.stately}') THEN n.singleton_key -- gate singleton/stately by key per batch
524
+ ELSE n.id::text
525
+ END
526
+ ) as row_number
527
+ FROM next n
517
528
  )
518
529
  UPDATE ${schema}.job j SET
519
530
  state = '${JOB_STATES.active}',
520
531
  started_on = now(),
521
532
  retry_count = CASE WHEN started_on IS NOT NULL THEN retry_count + 1 ELSE retry_count END
522
- FROM next
523
- WHERE name = $1 AND j.id = next.id
533
+ FROM grouped
534
+ WHERE name = $1 AND j.id = grouped.id
535
+ AND (
536
+ (grouped.grouped_policy IN ('${QUEUE_POLICIES.singleton}', '${QUEUE_POLICIES.stately}') AND grouped.row_number = 1) -- only one per singleton key
537
+ OR COALESCE(grouped.grouped_policy, '') NOT IN ('${QUEUE_POLICIES.singleton}', '${QUEUE_POLICIES.stately}') -- other policies unaffected
538
+ )
524
539
  RETURNING j.${includeMetadata ? allJobColumns : baseJobColumns}
525
540
  `
526
541
  }
@@ -777,9 +792,9 @@ function insertJob (schema) {
777
792
  singleton_on,
778
793
  COALESCE(j.dead_letter, q.dead_letter) as dead_letter,
779
794
  CASE
780
- WHEN expire_in IS NOT NULL THEN CAST(expire_in as interval)
795
+ WHEN expire_in IS NOT NULL THEN expire_in::numeric * interval '1s'
781
796
  WHEN q.expire_seconds IS NOT NULL THEN q.expire_seconds * interval '1s'
782
- WHEN expire_in_default IS NOT NULL THEN CAST(expire_in_default as interval)
797
+ WHEN expire_in_default IS NOT NULL THEN expire_in_default::numeric * interval '1s'
783
798
  ELSE interval '15 minutes'
784
799
  END as expire_in,
785
800
  CASE
@@ -865,9 +880,9 @@ function insertJobs (schema) {
865
880
  END as singleton_on,
866
881
  COALESCE("deadLetter", q.dead_letter) as dead_letter,
867
882
  CASE
868
- WHEN "expireInSeconds" IS NOT NULL THEN "expireInSeconds" * interval '1s'
883
+ WHEN "expireInSeconds" IS NOT NULL THEN "expireInSeconds"::numeric * interval '1s'
869
884
  WHEN q.expire_seconds IS NOT NULL THEN q.expire_seconds * interval '1s'
870
- WHEN defaults.expire_in IS NOT NULL THEN CAST(defaults.expire_in as interval)
885
+ WHEN defaults.expire_in IS NOT NULL THEN defaults.expire_in::numeric * interval '1s'
871
886
  ELSE interval '15 minutes'
872
887
  END as expire_in,
873
888
  CASE