pg-boss 12.37.0 → 12.38.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 (46) hide show
  1. package/dist/activity.d.ts.map +1 -1
  2. package/dist/activity.js +2 -1
  3. package/dist/attorney.d.ts +4 -1
  4. package/dist/attorney.d.ts.map +1 -1
  5. package/dist/attorney.js +28 -1
  6. package/dist/bam.d.ts +1 -1
  7. package/dist/bam.d.ts.map +1 -1
  8. package/dist/bam.js +154 -15
  9. package/dist/boss.d.ts.map +1 -1
  10. package/dist/boss.js +113 -30
  11. package/dist/cli.js +14 -3
  12. package/dist/db.d.ts +4 -1
  13. package/dist/db.d.ts.map +1 -1
  14. package/dist/db.js +43 -1
  15. package/dist/index.d.ts +12 -12
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +44 -20
  18. package/dist/manager.d.ts +12 -11
  19. package/dist/manager.d.ts.map +1 -1
  20. package/dist/manager.js +294 -57
  21. package/dist/migrationStore.d.ts.map +1 -1
  22. package/dist/migrationStore.js +219 -45
  23. package/dist/notifier.d.ts.map +1 -1
  24. package/dist/notifier.js +11 -4
  25. package/dist/plans.d.ts +53 -20
  26. package/dist/plans.d.ts.map +1 -1
  27. package/dist/plans.js +758 -208
  28. package/dist/registrar.d.ts +1 -1
  29. package/dist/registrar.d.ts.map +1 -1
  30. package/dist/registrar.js +10 -2
  31. package/dist/schema.json +84 -2
  32. package/dist/timekeeper.d.ts +1 -1
  33. package/dist/timekeeper.d.ts.map +1 -1
  34. package/dist/timekeeper.js +10 -5
  35. package/dist/tools.d.ts +2 -1
  36. package/dist/tools.d.ts.map +1 -1
  37. package/dist/tools.js +7 -4
  38. package/dist/types.d.ts +55 -13
  39. package/dist/types.d.ts.map +1 -1
  40. package/dist/warning.d.ts +5 -0
  41. package/dist/warning.d.ts.map +1 -1
  42. package/dist/warning.js +18 -4
  43. package/dist/worker.d.ts +3 -0
  44. package/dist/worker.d.ts.map +1 -1
  45. package/dist/worker.js +10 -0
  46. package/package.json +8 -8
@@ -1 +1 @@
1
- {"version":3,"file":"activity.d.ts","sourceRoot":"","sources":["../src/activity.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAqB,MAAM,YAAY,CAAA;AAI9D;;;GAGG;AACH,wBAAgB,SAAS,CAAC,CAAC,SAAS,SAAS,EAAG,EAAE,EAAE,CAAC,GAAG,CAAC,CAExD;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,SAAS,EAAG,EAAE,EAAE,CAAC,GAAG;IAAE,EAAE,EAAE,CAAC,CAAC;IAAC,IAAI,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAA;CAAE,CAmFlG"}
1
+ {"version":3,"file":"activity.d.ts","sourceRoot":"","sources":["../src/activity.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAqB,MAAM,YAAY,CAAA;AAI9D;;;GAGG;AACH,wBAAgB,SAAS,CAAC,CAAC,SAAS,SAAS,EAAG,EAAE,EAAE,CAAC,GAAG,CAAC,CAExD;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,SAAS,EAAG,EAAE,EAAE,CAAC,GAAG;IAAE,EAAE,EAAE,CAAC,CAAC;IAAC,IAAI,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAA;CAAE,CAoFlG"}
package/dist/activity.js CHANGED
@@ -51,7 +51,8 @@ export function trackActivity(db) {
51
51
  });
52
52
  const wrapDb = (inner) => {
53
53
  const overrides = {
54
- executeSql: track((text, values) => inner.executeSql(text, values))
54
+ // Every argument, not only text and values: pg-boss's own Db takes BAM's stop signals as a third.
55
+ executeSql: track((...args) => inner.executeSql(...args))
55
56
  };
56
57
  // Only when present: callers detect the capability with typeof.
57
58
  if (typeof inner.beginTransaction === 'function') {
@@ -20,7 +20,10 @@ declare function checkWorkArgs(name: string, args: any[]): {
20
20
  declare function checkFetchArgs(name: string, options: any): void;
21
21
  declare function getConfig(value: string | types.ConstructorOptions): types.ResolvedConstructorOptions;
22
22
  declare function assertPostgresObjectName(name: string): void;
23
+ declare function assertListLimit(method: string, limit?: number): number;
24
+ declare function assertListAfter(method: string, after: unknown, id?: boolean): void;
25
+ declare function assertListAfterRow(method: string, after: unknown, fields: [string, string], id?: string): void;
23
26
  declare function assertQueueName(name: string): void;
24
27
  declare function assertKey(key: string): void;
25
- export { assertKey, COMPATIBILITY_FLAGS, assertPostgresObjectName, assertQueueName, checkFetchArgs, checkSendArgs, checkUpdateArgs, checkWorkArgs, getConfig, pinZonelessDateTime, POLICY, validateFlowJobs, validateGroupConfig, validateQueueArgs };
28
+ export { assertKey, assertListAfter, assertListAfterRow, assertListLimit, COMPATIBILITY_FLAGS, assertPostgresObjectName, assertQueueName, checkFetchArgs, checkSendArgs, checkUpdateArgs, checkWorkArgs, getConfig, pinZonelessDateTime, POLICY, validateFlowJobs, validateGroupConfig, validateQueueArgs };
26
29
  //# sourceMappingURL=attorney.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"attorney.d.ts","sourceRoot":"","sources":["../src/attorney.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,KAAK,KAAK,MAAM,YAAY,CAAA;AAExC,QAAA,MAAM,MAAM;;;;CAIX,CAAA;AAKD,QAAA,MAAM,mBAAmB,6RAcf,CAAA;AA+EV,iBAAS,iBAAiB,CAAE,MAAM,GAAE,GAAQ,QAc3C;AAiBD,iBAAS,mBAAmB,CAAE,KAAK,EAAE,MAAM,UAE1C;AAED,iBAAS,aAAa,CAAE,IAAI,EAAE,GAAG,GAAG,KAAK,CAAC,OAAO,CAgDhD;AAOD,iBAAS,eAAe,CAAE,IAAI,EAAE,GAAG,EAAE,EAAE,MAAc,EAAE;;CAAK,GAAG,KAAK,CAAC,OAAO,CA4D3E;AAED,iBAAS,mBAAmB,CAAE,MAAM,EAAE,GAAG,QAOxC;AAED,iBAAS,gBAAgB,CAAE,IAAI,EAAE,KAAK,CAAC,OAAO,EAAE,QA2D/C;AA2GD,iBAAS,aAAa,CAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG;IAClD,OAAO,EAAE,KAAK,CAAC,mBAAmB,CAAA;IAClC,QAAQ,EAAE,KAAK,CAAC,WAAW,CAAC,GAAG,CAAC,CAAA;CACjC,CAgCA;AAqBD,iBAAS,cAAc,CAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,QAQlD;AAED,iBAAS,SAAS,CAAE,KAAK,EAAE,MAAM,GAAG,KAAK,CAAC,kBAAkB,GAAG,KAAK,CAAC,0BAA0B,CA4B9F;AA4JD,iBAAS,wBAAwB,CAAE,IAAI,EAAE,MAAM,QAiC9C;AAED,iBAAS,eAAe,CAAE,IAAI,EAAE,MAAM,QAIrC;AAED,iBAAS,SAAS,CAAE,GAAG,EAAE,MAAM,QAI9B;AA+LD,OAAO,EACL,SAAS,EACT,mBAAmB,EACnB,wBAAwB,EACxB,eAAe,EACf,cAAc,EACd,aAAa,EACb,eAAe,EACf,aAAa,EACb,SAAS,EACT,mBAAmB,EACnB,MAAM,EACN,gBAAgB,EAChB,mBAAmB,EACnB,iBAAiB,EAClB,CAAA"}
1
+ {"version":3,"file":"attorney.d.ts","sourceRoot":"","sources":["../src/attorney.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,KAAK,KAAK,MAAM,YAAY,CAAA;AAExC,QAAA,MAAM,MAAM;;;;CAIX,CAAA;AAKD,QAAA,MAAM,mBAAmB,6RAcf,CAAA;AA+EV,iBAAS,iBAAiB,CAAE,MAAM,GAAE,GAAQ,QAc3C;AAiBD,iBAAS,mBAAmB,CAAE,KAAK,EAAE,MAAM,UAE1C;AAED,iBAAS,aAAa,CAAE,IAAI,EAAE,GAAG,GAAG,KAAK,CAAC,OAAO,CAgDhD;AAOD,iBAAS,eAAe,CAAE,IAAI,EAAE,GAAG,EAAE,EAAE,MAAc,EAAE;;CAAK,GAAG,KAAK,CAAC,OAAO,CA4D3E;AAED,iBAAS,mBAAmB,CAAE,MAAM,EAAE,GAAG,QAOxC;AAED,iBAAS,gBAAgB,CAAE,IAAI,EAAE,KAAK,CAAC,OAAO,EAAE,QA2D/C;AA2GD,iBAAS,aAAa,CAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG;IAClD,OAAO,EAAE,KAAK,CAAC,mBAAmB,CAAA;IAClC,QAAQ,EAAE,KAAK,CAAC,WAAW,CAAC,GAAG,CAAC,CAAA;CACjC,CAgCA;AAqBD,iBAAS,cAAc,CAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,QAQlD;AAED,iBAAS,SAAS,CAAE,KAAK,EAAE,MAAM,GAAG,KAAK,CAAC,kBAAkB,GAAG,KAAK,CAAC,0BAA0B,CA4B9F;AA4JD,iBAAS,wBAAwB,CAAE,IAAI,EAAE,MAAM,QAiC9C;AAOD,iBAAS,eAAe,CAAE,MAAM,EAAE,MAAM,EAAE,KAAK,GAAE,MAA2B,GAAG,MAAM,CAIpF;AAYD,iBAAS,eAAe,CAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,UAAQ,GAAG,IAAI,CAI1E;AAID,iBAAS,kBAAkB,CAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAKxG;AAED,iBAAS,eAAe,CAAE,IAAI,EAAE,MAAM,QAIrC;AAED,iBAAS,SAAS,CAAE,GAAG,EAAE,MAAM,QAI9B;AA+LD,OAAO,EACL,SAAS,EACT,eAAe,EACf,kBAAkB,EAClB,eAAe,EACf,mBAAmB,EACnB,wBAAwB,EACxB,eAAe,EACf,cAAc,EACd,aAAa,EACb,eAAe,EACf,aAAa,EACb,SAAS,EACT,mBAAmB,EACnB,MAAM,EACN,gBAAgB,EAChB,mBAAmB,EACnB,iBAAiB,EAClB,CAAA"}
package/dist/attorney.js CHANGED
@@ -572,6 +572,33 @@ function assertPostgresObjectName(name) {
572
572
  assert(!/\W/.test(name), `Schema name ${JSON.stringify(name)} can only contain alphanumeric characters or underscores when unquoted.${remedy}`);
573
573
  assert(!/^\d/.test(name), `Schema name ${JSON.stringify(name)} cannot start with a number when unquoted.${remedy}`);
574
574
  }
575
+ const LIST_LIMIT_DEFAULT = 1000;
576
+ const LIST_LIMIT_MAX = 100_000;
577
+ // The limit of a public list read: an integer from 1 to LIST_LIMIT_MAX, LIST_LIMIT_DEFAULT when
578
+ // left out. Internal callers that need every row call below the public method instead.
579
+ function assertListLimit(method, limit = LIST_LIMIT_DEFAULT) {
580
+ assert(Number.isInteger(limit) && limit >= 1 && limit <= LIST_LIMIT_MAX, `${method}: limit must be an integer between 1 and ${LIST_LIMIT_MAX}`);
581
+ return limit;
582
+ }
583
+ // Any spelling the uuid type accepts, in either case: 32 hex digits, a hyphen allowed after any group
584
+ // of four but the last, and the whole optionally inside one pair of braces. A read that pages by id
585
+ // checks it here, so a malformed one is a usage error rather than the database's.
586
+ const UUID_INPUT = /^(?:\{[0-9a-fA-F]{4}(?:-?[0-9a-fA-F]{4}){7}\}|[0-9a-fA-F]{4}(?:-?[0-9a-fA-F]{4}){7})$/;
587
+ function isUuid(value) {
588
+ return UUID_INPUT.test(value);
589
+ }
590
+ // The `after` of a list read that pages by one value: a queue name, a key or, with `id`, an id.
591
+ function assertListAfter(method, after, id = false) {
592
+ assert(after === undefined || (typeof after === 'string' && after.length > 0), `${method}: after must be a non-empty string`);
593
+ assert(!id || after === undefined || isUuid(after), `${method}: after must be a uuid`);
594
+ }
595
+ // The `after` of a list read that pages by a row: the previous page's last row, of which only the
596
+ // two named fields are read. With `id`, that field must be a uuid.
597
+ function assertListAfterRow(method, after, fields, id) {
598
+ assert(after === undefined || (typeof after === 'object' && after !== null &&
599
+ fields.every(field => typeof after[field] === 'string')), `${method}: after must be an object with string ${fields.join(' and ')}`);
600
+ assert(!id || after === undefined || isUuid(after[id]), `${method}: after.${id} must be a uuid`);
601
+ }
575
602
  function assertQueueName(name) {
576
603
  assert(name, 'Name is required');
577
604
  assert(typeof name === 'string', 'Name must be a string');
@@ -694,4 +721,4 @@ function applyFlowConfig(config) {
694
721
  assert(!('flowIntervalSeconds' in config) || config.flowIntervalSeconds >= minInterval, `configuration assert: flowIntervalSeconds must be at least ${minInterval} seconds`);
695
722
  config.flowIntervalSeconds = config.flowIntervalSeconds || 5;
696
723
  }
697
- export { assertKey, COMPATIBILITY_FLAGS, assertPostgresObjectName, assertQueueName, checkFetchArgs, checkSendArgs, checkUpdateArgs, checkWorkArgs, getConfig, pinZonelessDateTime, POLICY, validateFlowJobs, validateGroupConfig, validateQueueArgs };
724
+ export { assertKey, assertListAfter, assertListAfterRow, assertListLimit, COMPATIBILITY_FLAGS, assertPostgresObjectName, assertQueueName, checkFetchArgs, checkSendArgs, checkUpdateArgs, checkWorkArgs, getConfig, pinZonelessDateTime, POLICY, validateFlowJobs, validateGroupConfig, validateQueueArgs };
package/dist/bam.d.ts CHANGED
@@ -9,7 +9,7 @@ declare class Bam extends EventEmitter implements types.EventsMixin {
9
9
  constructor(db: types.IDatabase, config: types.ResolvedConstructorOptions);
10
10
  get working(): boolean;
11
11
  start(): Promise<void>;
12
- stop(): Promise<void>;
12
+ stop(deadline?: Promise<void>): Promise<void>;
13
13
  }
14
14
  export default Bam;
15
15
  //# sourceMappingURL=bam.d.ts.map
package/dist/bam.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"bam.d.ts","sourceRoot":"","sources":["../src/bam.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,MAAM,aAAa,CAAA;AAItC,OAAO,KAAK,KAAK,MAAM,YAAY,CAAA;AAgBnC,cAAM,GAAI,SAAQ,YAAa,YAAW,KAAK,CAAC,WAAW;;IAOzD,MAAM;;;MAAS;gBAGb,EAAE,EAAE,KAAK,CAAC,SAAS,EACnB,MAAM,EAAE,KAAK,CAAC,0BAA0B;IAU1C,IAAI,OAAO,IAAK,OAAO,CAEtB;IAEK,KAAK;IAcL,IAAI;CAwLX;AAED,eAAe,GAAG,CAAA"}
1
+ {"version":3,"file":"bam.d.ts","sourceRoot":"","sources":["../src/bam.ts"],"names":[],"mappings":"AACA,OAAO,YAAY,MAAM,aAAa,CAAA;AAKtC,OAAO,KAAK,KAAK,MAAM,YAAY,CAAA;AAmDnC,cAAM,GAAI,SAAQ,YAAa,YAAW,KAAK,CAAC,WAAW;;IAQzD,MAAM;;;MAAS;gBAGb,EAAE,EAAE,KAAK,CAAC,SAAS,EACnB,MAAM,EAAE,KAAK,CAAC,0BAA0B;IAU1C,IAAI,OAAO,IAAK,OAAO,CAEtB;IAEK,KAAK;IAiBL,IAAI,CAAE,QAAQ,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC;CA+UrC;AAED,eAAe,GAAG,CAAA"}
package/dist/bam.js CHANGED
@@ -1,16 +1,23 @@
1
+ import assert from 'node:assert';
1
2
  import EventEmitter from 'node:events';
2
3
  import { ClaimTimer } from "./claimTimer.js";
4
+ import Db from "./db.js";
3
5
  import * as plans from "./plans.js";
4
- import { delay } from "./tools.js";
6
+ import { delay, unwrapSQLResult } from "./tools.js";
5
7
  import * as types from "./types.js";
6
8
  const events = {
7
9
  error: 'error',
8
10
  bam: 'bam'
9
11
  };
12
+ // How often stop() repeats a cancel that found nothing to cancel. It can land between two statements
13
+ // of one row, or before the statement reaches the server. It also finds nothing when the role cannot
14
+ // see the statement in pg_stat_activity, and then stop() keeps trying until the statement ends.
15
+ const CANCEL_RETRY_MS = 1000;
10
16
  class Bam extends EventEmitter {
11
17
  #stopped;
12
18
  #working;
13
19
  #pollTimer;
20
+ #running;
14
21
  #db;
15
22
  #config;
16
23
  events = events;
@@ -32,16 +39,95 @@ class Bam extends EventEmitter {
32
39
  setImmediate(() => this.#onPoll());
33
40
  this.#pollTimer.start();
34
41
  }
35
- async stop() {
42
+ // Waits for a running command until deadline resolves, then ends it (see Ending) and waits for
43
+ // that to land, so the row is recorded before index.ts closes the pool. Without a deadline it waits
44
+ // for the command to finish.
45
+ async stop(deadline) {
36
46
  if (this.#stopped)
37
47
  return;
38
48
  this.#stopped = true;
39
49
  this.#pollTimer.stop();
40
50
  this.#pollTimer = undefined;
51
+ let expired = false;
52
+ deadline?.then(() => { expired = true; });
53
+ let endedAt = 0;
41
54
  while (this.#working) {
55
+ const running = this.#running;
56
+ if (expired && running) {
57
+ running.stopped = true;
58
+ if (!running.ended && running.ending !== 'wait' && Date.now() - endedAt >= CANCEL_RETRY_MS) {
59
+ endedAt = Date.now();
60
+ await this.#end(running);
61
+ }
62
+ }
42
63
  await delay(10);
43
64
  }
44
65
  }
66
+ async #end(running) {
67
+ if (running.ending === 'abandon' || running.ending === 'drop') {
68
+ running.abort.abort();
69
+ running.ended = true;
70
+ return;
71
+ }
72
+ // Before the cancel reads pg_stat_activity, so a statement that ends after the read closes its
73
+ // connection rather than returning it to the pool with the cancel still on its way.
74
+ running.abort.abort();
75
+ try {
76
+ const { rows } = await this.#db.executeSql(plans.cancelBamCommand(running.id));
77
+ // Only ever set here: a statement that stop() kept from starting may have set it while the
78
+ // cancel was in flight, and the cancel then finds nothing.
79
+ if (rows.some(row => row.cancelled))
80
+ running.ended = true;
81
+ }
82
+ catch (err) {
83
+ // Usually a role that can see the statement but not signal its backend, which a retry will not
84
+ // change. Report it once and wait for the statement.
85
+ running.ending = 'wait';
86
+ this.emit(events.error, err);
87
+ }
88
+ }
89
+ // Runs one of a claimed row's statements so stop() can end it. See Ending.
90
+ async #execute(running, sql) {
91
+ if (running.stopped) {
92
+ this.#notStarted(running);
93
+ }
94
+ const cockroach = this.#config.backend === 'cockroachdb';
95
+ if (!(this.#db instanceof Db)) {
96
+ running.ending = cockroach ? 'wait' : 'cancel';
97
+ }
98
+ else if (cockroach) {
99
+ running.ending = 'drop';
100
+ }
101
+ else {
102
+ running.ending = !this.#config.noIndexProgressView && plans.isConcurrentIndexCommand(sql) ? 'abandon' : 'cancel';
103
+ }
104
+ // CockroachDB drops comments from the statement text it reports, so the tag only serves Postgres.
105
+ const text = cockroach ? sql : `${plans.bamCommandTag(running.id)}\n${sql}`;
106
+ try {
107
+ const result = !(this.#db instanceof Db)
108
+ ? await this.#db.executeSql(text)
109
+ : await this.#db.executeSql(text, undefined, running.ending === 'cancel'
110
+ ? { retire: running.abort.signal }
111
+ : { signal: running.abort.signal });
112
+ // The statement is over. Until the next one starts, which checks stopped first, stop() has
113
+ // nothing to end, and a failure of the row's other statements is not the stop's doing.
114
+ running.ending = 'wait';
115
+ return result;
116
+ }
117
+ catch (err) {
118
+ // Db throws the abort reason when stop() aborted before the statement reached a connection.
119
+ if (running.abort.signal.aborted && err === running.abort.signal.reason) {
120
+ this.#notStarted(running);
121
+ }
122
+ throw err;
123
+ }
124
+ }
125
+ #notStarted(running) {
126
+ // Nothing is left running for the row to wait on, whatever the previous statement was.
127
+ running.ending = 'wait';
128
+ running.ended = true;
129
+ throw new Error('stop() ended the command before it ran');
130
+ }
45
131
  async #onPoll() {
46
132
  if (this.#stopped || this.#working || !this.#config.migrate)
47
133
  return;
@@ -69,12 +155,20 @@ class Bam extends EventEmitter {
69
155
  this.#working = false;
70
156
  }
71
157
  }
158
+ // Runs commands back to back once a poll finds one, rather than one per bamIntervalSeconds, so a
159
+ // migration's builds land one after another. The first claim may retry a failed or stale row; the
160
+ // rest take pending rows only, and the burst ends when none is left or the runner stops.
72
161
  async #processCommands() {
73
- if (this.#stopped)
74
- return;
75
- const entry = await this.#getNextCommand();
162
+ let pendingOnly = false;
163
+ while (!this.#stopped && await this.#processNextCommand(pendingOnly)) {
164
+ pendingOnly = true;
165
+ }
166
+ }
167
+ // Claims and runs one command. Returns whether a command was claimed, whatever its outcome.
168
+ async #processNextCommand(pendingOnly) {
169
+ const entry = await this.#getNextCommand(pendingOnly);
76
170
  if (!entry)
77
- return;
171
+ return false;
78
172
  if (this.#config.__test__delay_bam_claim_ms) {
79
173
  await delay(this.#config.__test__delay_bam_claim_ms);
80
174
  }
@@ -90,8 +184,10 @@ class Bam extends EventEmitter {
90
184
  catch (err) {
91
185
  this.emit(events.error, err);
92
186
  }
93
- return;
187
+ return false;
94
188
  }
189
+ const running = { id: entry.id, stopped: false, ended: false, ending: 'wait', abort: new AbortController() };
190
+ this.#running = running;
95
191
  this.emit(events.bam, {
96
192
  id: entry.id,
97
193
  name: entry.name,
@@ -99,14 +195,19 @@ class Bam extends EventEmitter {
99
195
  queue: entry.queue,
100
196
  table: entry.table
101
197
  });
198
+ // Set when a CockroachDB build returns without its index because an earlier build of it is still
199
+ // running as a schema change job. The row fails until that job ends, which is not an error.
200
+ let buildPending = false;
102
201
  try {
103
202
  let alreadyBuilt = false;
203
+ // Non-null for a CONCURRENTLY index build, which is checked against the catalog before a
204
+ // re-attempt and after the build returns.
205
+ const probeSql = plans.bamHealProbe(this.#config.schema, entry.command);
104
206
  // A re-attempted command (a stale in_progress reclaim, or a retry of a prior 'failed', including
105
207
  // failed rows left by older releases) needs the catalog consulted before the command is re-run,
106
208
  // because the row keeps the text it was enqueued with and that text may not be idempotent.
107
209
  // Probe indisvalid on every backend: pg_index is readable everywhere, and both outcomes matter.
108
210
  if (entry.reattempt) {
109
- const probeSql = plans.bamHealProbe(this.#config.schema, entry.command);
110
211
  if (probeSql) {
111
212
  const { rows } = await this.#db.executeSql(probeSql);
112
213
  if (rows[0]?.invalid) {
@@ -117,7 +218,7 @@ class Bam extends EventEmitter {
117
218
  // DROP ... CONCURRENTLY isn't their model.
118
219
  if (!this.#config.noIndexProgressView) {
119
220
  // Non-null wherever the probe was: both recognise the same CONCURRENTLY commands.
120
- await this.#db.executeSql(plans.bamHealDrop(this.#config.schema, entry.command));
221
+ await this.#execute(running, plans.bamHealDrop(this.#config.schema, entry.command));
121
222
  }
122
223
  }
123
224
  else if (rows[0]) {
@@ -134,7 +235,25 @@ class Bam extends EventEmitter {
134
235
  }
135
236
  }
136
237
  if (!alreadyBuilt) {
137
- await this.#db.executeSql(entry.command);
238
+ await this.#execute(running, entry.command);
239
+ // A build can return without leaving a valid index. On CockroachDB, a retry of a build whose
240
+ // connection stop() dropped returns at once while the first build still runs as a schema
241
+ // change job, because IF NOT EXISTS matches the index it is adding, which pg_index does not
242
+ // list yet. On Postgres, IF NOT EXISTS skips over an INVALID index of the same name. Marking
243
+ // the row completed then would release the commands behind it, and nothing would build the
244
+ // index again if that job failed. Failing the row has a later attempt check again, where the
245
+ // probe completes it or heals the INVALID index. Without liveness there is no heal, so an
246
+ // INVALID index would fail every retry, and the row completes as before.
247
+ if (probeSql) {
248
+ const { rows } = await this.#db.executeSql(probeSql);
249
+ if (!rows[0] || (rows[0].invalid && !this.#config.noIndexProgressView)) {
250
+ buildPending = !rows[0] && this.#config.backend === 'cockroachdb';
251
+ const name = plans.bamCommandIndexName(entry.command);
252
+ throw new Error(rows[0]
253
+ ? `index ${name} is invalid after the build`
254
+ : `index ${name} does not exist after the build. An earlier build of it may still be running.`);
255
+ }
256
+ }
138
257
  }
139
258
  // Record the outcome even when a stop landed while the command was running. stop() waits out
140
259
  // #working and index.ts closes the pool only after #bam.stop() resolves, so the UPDATE is safe
@@ -154,6 +273,11 @@ class Bam extends EventEmitter {
154
273
  });
155
274
  }
156
275
  catch (err) {
276
+ // The one failure that leaves the row in_progress: stop() dropped the connection under a
277
+ // concurrent index statement that the server is still running. See Ending.
278
+ if (running.ended && running.ending === 'abandon') {
279
+ return true;
280
+ }
157
281
  // Same reasoning as the completed path: a stop must not leave the row in_progress. This write
158
282
  // is guarded because it commonly runs on the connection that just failed - a terminated backend
159
283
  // or dropped connection fails the command AND the UPDATE that would record it. Letting that
@@ -165,7 +289,11 @@ class Bam extends EventEmitter {
165
289
  catch (markErr) {
166
290
  this.emit(events.error, markErr);
167
291
  }
168
- this.emit(events.error, err);
292
+ // A failure stop() caused is not an error. The row still records it, so the next attempt runs
293
+ // as a reattempt and heals what the cancel left.
294
+ if (!running.ended && !buildPending) {
295
+ this.emit(events.error, err);
296
+ }
169
297
  this.emit(events.bam, {
170
298
  id: entry.id,
171
299
  name: entry.name,
@@ -175,11 +303,22 @@ class Bam extends EventEmitter {
175
303
  error: String(err)
176
304
  });
177
305
  }
306
+ finally {
307
+ this.#running = undefined;
308
+ }
309
+ return true;
178
310
  }
179
- async #getNextCommand() {
180
- const sql = plans.getNextBamCommand(this.#config.schema, { useLiveness: !this.#config.noIndexProgressView });
181
- const { rows } = await this.#db.executeSql(sql);
182
- return rows[0] || null;
311
+ async #getNextCommand(pendingOnly) {
312
+ const sql = plans.claimBamCommand(this.#config.schema, {
313
+ useLiveness: !this.#config.noIndexProgressView,
314
+ pendingOnly,
315
+ noAdvisoryLocks: this.#config.noAdvisoryLocks
316
+ });
317
+ const { rows } = unwrapSQLResult(await this.#db.executeSql(sql));
318
+ // The lock's own row comes first, so the claim is told apart by a column only it returns. Without
319
+ // the lock's row the db returned only the last statement, COMMIT, and a claim it made is lost.
320
+ assert(this.#config.noAdvisoryLocks || rows.some(row => row && 'pg_advisory_xact_lock' in row), 'bam claim returned no lock row: a custom db must return the rows of every statement in a multi-statement query');
321
+ return rows.find(row => row?.claimedStartedOn !== undefined) ?? null;
183
322
  }
184
323
  async #releaseCommand(entry) {
185
324
  const sql = plans.releaseBamCommand(this.#config.schema, entry.id, entry.priorStatus, entry.priorStartedOn ?? null, entry.claimedStartedOn);
@@ -1 +1 @@
1
- {"version":3,"file":"boss.d.ts","sourceRoot":"","sources":["../src/boss.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,MAAM,aAAa,CAAA;AAEtC,OAAO,KAAK,OAAO,MAAM,cAAc,CAAA;AAGvC,OAAO,KAAK,KAAK,MAAM,YAAY,CAAA;AAkInC,cAAM,IAAK,SAAQ,YAAa,YAAW,KAAK,CAAC,WAAW;;IAsC1D,MAAM;;;MAAS;gBAGb,EAAE,EAAE,KAAK,CAAC,SAAS,EACnB,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,KAAK,CAAC,0BAA0B;IAa1C,IAAI,WAAW,IAAK,OAAO,CAE1B;IAIK,KAAK;IAiBL,IAAI;IAoHJ,SAAS,CAAE,KAAK,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC,WAAW,EAAE,EAAE,OAAO,CAAC,EAAE,KAAK,CAAC,gBAAgB;IA2tBvF;;;;OAIG;IACG,kBAAkB,CAAE,OAAO,CAAC,EAAE,KAAK,CAAC,cAAc,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;CAwB7E;AAED,eAAe,IAAI,CAAA"}
1
+ {"version":3,"file":"boss.d.ts","sourceRoot":"","sources":["../src/boss.ts"],"names":[],"mappings":"AACA,OAAO,YAAY,MAAM,aAAa,CAAA;AAEtC,OAAO,KAAK,OAAO,MAAM,cAAc,CAAA;AAGvC,OAAO,KAAK,KAAK,MAAM,YAAY,CAAA;AAkInC,cAAM,IAAK,SAAQ,YAAa,YAAW,KAAK,CAAC,WAAW;;IAsC1D,MAAM;;;MAAS;gBAGb,EAAE,EAAE,KAAK,CAAC,SAAS,EACnB,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,KAAK,CAAC,0BAA0B;IAa1C,IAAI,WAAW,IAAK,OAAO,CAE1B;IAIK,KAAK;IAiBL,IAAI;IAwHJ,SAAS,CAAE,KAAK,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC,WAAW,EAAE,EAAE,OAAO,CAAC,EAAE,KAAK,CAAC,gBAAgB;IAizBvF;;;;OAIG;IACG,kBAAkB,CAAE,OAAO,CAAC,EAAE,KAAK,CAAC,cAAc,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;CAwB7E;AAED,eAAe,IAAI,CAAA"}
package/dist/boss.js CHANGED
@@ -1,3 +1,4 @@
1
+ import assert from 'node:assert';
1
2
  import EventEmitter from 'node:events';
2
3
  import { ClaimTimer } from "./claimTimer.js";
3
4
  import * as plans from "./plans.js";
@@ -242,10 +243,14 @@ class Boss extends EventEmitter {
242
243
  if (!this.#config.persistQueueStats || !this.#config.queueStatRetentionDays) {
243
244
  return;
244
245
  }
245
- const sql = this.#config.noTablePartitioning
246
- ? plans.deleteOldQueueStats(this.#config.schema, this.#config.queueStatRetentionDays)
247
- : plans.dropOldQueueStatsPartitions(this.#config.schema, this.#config.queueStatRetentionDays);
248
- await this.#executeQuery(sql);
246
+ const { schema, queueStatRetentionDays: days } = this.#config;
247
+ if (this.#config.noTablePartitioning) {
248
+ const batchSize = this.#config.__test__walk_batch_size;
249
+ await this.#walk((after) => plans.deleteOldQueueStats(schema, days, { after, batchSize }));
250
+ }
251
+ else {
252
+ await this.#executeQuery(plans.dropOldQueueStatsPartitions(schema, days));
253
+ }
249
254
  }
250
255
  // Waits for a pass in flight rather than skipping like #onSupervise, since the caller asked for
251
256
  // this one. Holding #maintaining keeps it from overlapping the background pass and makes stop()
@@ -392,38 +397,117 @@ class Boss extends EventEmitter {
392
397
  return;
393
398
  // CockroachDB rejects the multi-mutation failJobs() CTE these use, so under noMultiMutationCte
394
399
  // route expiry through the manager's split select/delete/re-insert variants instead.
395
- if (this.#config.noMultiMutationCte) {
396
- await this.#manager.failJobsByTimeoutDistributed(table, queues);
397
- }
398
- else {
399
- const sql = plans.failJobsByTimeout(this.#config.schema, table, queues, this.#config.noAdvisoryLocks);
400
- await this.#executeQuery(sql);
401
- }
400
+ const limit = this.#config.__test__expiry_batch_size ?? plans.EXPIRY_BATCH_SIZE;
401
+ await this.#repeatWhileFull(limit, () => this.#config.noMultiMutationCte
402
+ ? this.#manager.failJobsByTimeoutDistributed(table, queues, limit)
403
+ : this.#expiryBatch(plans.failJobsByTimeout(this.#config.schema, table, queues, this.#config.noAdvisoryLocks, limit)));
402
404
  if (this.#stopping)
403
405
  return;
404
- if (this.#config.noMultiMutationCte) {
405
- await this.#manager.failJobsByHeartbeatDistributed(table, queues);
406
- }
407
- else {
408
- const heartbeatSql = plans.failJobsByHeartbeat(this.#config.schema, table, queues, this.#config.noAdvisoryLocks);
409
- await this.#executeQuery(heartbeatSql);
410
- }
406
+ await this.#repeatWhileFull(limit, () => this.#config.noMultiMutationCte
407
+ ? this.#manager.failJobsByHeartbeatDistributed(table, queues, limit)
408
+ : this.#expiryBatch(plans.failJobsByHeartbeat(this.#config.schema, table, queues, this.#config.noAdvisoryLocks, limit)));
411
409
  }
412
410
  }
411
+ // Runs a maintenance sweep a batch at a time until a batch picks fewer than `limit` rows. A stop
412
+ // between batches leaves the rest for the next pass.
413
+ async #repeatWhileFull(limit, batch) {
414
+ while (!this.#stopping) {
415
+ if (await batch() < limit)
416
+ return;
417
+ }
418
+ }
419
+ // CockroachDB returns the count as a string.
420
+ async #expiryBatch(sql) {
421
+ const { rows } = await this.#executeQuery(sql);
422
+ return Number(rows.find((row) => row.expiryPicked !== undefined)?.expiryPicked ?? 0);
423
+ }
413
424
  async #maintain(table, names) {
414
425
  if (this.#stopping)
415
426
  return;
416
- const command = plans.trySetQueueDeletionTime(this.#config.schema, names, this.#config.maintenanceIntervalSeconds, plans.queueClaimSkipLocked(this.#config.backend, this.#config.noSkipLocked));
427
+ const skipLocked = plans.queueClaimSkipLocked(this.#config.backend, this.#config.noSkipLocked);
428
+ const command = plans.trySetQueueDeletionTime(this.#config.schema, names, this.#config.maintenanceIntervalSeconds, skipLocked);
417
429
  const { rows } = await this.#executeQuery(command);
430
+ const queues = rows.map((q) => q.name);
431
+ // One statement stamps every queue it claims with the same time.
432
+ const claimedOn = rows[0]?.claimedOn;
433
+ let finished = 0;
434
+ try {
435
+ while (finished < queues.length && await this.#maintainQueue(table, queues[finished])) {
436
+ finished++;
437
+ }
438
+ }
439
+ finally {
440
+ // The claim covers every queue up front, so a pass that a stop or a failure ends early hands
441
+ // back the ones it did not finish. Otherwise they would wait a whole maintenance interval.
442
+ if (finished < queues.length) {
443
+ try {
444
+ await this.#executeQuery(plans.releaseQueueDeletionTime(this.#config.schema, queues.slice(finished), claimedOn, skipLocked));
445
+ }
446
+ catch (err) {
447
+ this.emit(events.error, err);
448
+ }
449
+ }
450
+ }
451
+ }
452
+ // The retention sweep and the dependency cleanup for one queue. Returns whether every walk covered
453
+ // the whole queue.
454
+ async #maintainQueue(table, name) {
418
455
  if (this.#stopping)
419
- return;
420
- if (rows.length) {
421
- const queues = rows.map((q) => q.name);
422
- const sql = plans.deletion(this.#config.schema, table, queues, this.#config.noAdvisoryLocks);
423
- await this.#executeQuery(sql);
424
- const depSql = plans.cleanupDependencies(this.#config.schema, table, queues, this.#config.noAdvisoryLocks);
425
- await this.#executeQuery(depSql);
456
+ return false;
457
+ const { schema, noAdvisoryLocks, backend } = this.#config;
458
+ const batchSize = this.#config.__test__walk_batch_size;
459
+ // The walk counts the completed and cancelled jobs that finished by DELTA_LAG before it
460
+ // started, so a completion committed late is still ahead of the watermark, and the monitor
461
+ // counts the rest after that (see plans.createTableQueue).
462
+ const { rows: [{ time }] } = await this.#executeQuery(plans.getTime(schema));
463
+ const retainedBefore = Number(time) - plans.DELTA_LAG_SECONDS * 1000;
464
+ const retention = { retained: 0, deleted: 0, deletedRetained: 0 };
465
+ let done = false;
466
+ try {
467
+ done = await this.#walk((after) => plans.deletion(schema, table, name, { after, batchSize, backend, noAdvisoryLocks, retainedBefore }), retention);
426
468
  }
469
+ finally {
470
+ // A walk that a stop or a failed batch cut short has not counted what it kept, so it only
471
+ // takes what it deleted off the counts, as a bulk delete does. A failed update is reported so
472
+ // that it does not hide the batch's own error, and the next full walk sets the count.
473
+ if (!done && retention.deleted) {
474
+ try {
475
+ await this.#executeQuery({ text: plans.reduceRetainedCount(schema), values: [name, retention.deletedRetained, retention.deleted] });
476
+ }
477
+ catch (err) {
478
+ this.emit(events.error, err);
479
+ }
480
+ }
481
+ }
482
+ // Only a walk that covered the whole queue has counted all of what it kept.
483
+ if (done) {
484
+ await this.#executeQuery({ text: plans.setRetainedCount(schema, table), values: [name, retention.retained, retainedBefore] });
485
+ }
486
+ const children = await this.#walk((after) => plans.cleanupDependencies(schema, table, name, 'child', { after, batchSize, backend, noAdvisoryLocks }));
487
+ const parents = await this.#walk((after) => plans.cleanupDependencies(schema, table, name, 'parent', { after, batchSize, backend, noAdvisoryLocks }));
488
+ return done && children && parents;
489
+ }
490
+ // Walks one queue's keys a batch at a time (see plans.deletion), carrying each batch's cursor into
491
+ // the next until a batch comes back without one, and returns whether it got there. A stop between
492
+ // batches leaves the rest for the next pass. `tally` totals the batches' counts as they commit, so
493
+ // it holds them even when a batch throws. CockroachDB returns counts as strings. A batch with no
494
+ // result row fails the walk rather than ending it, since a walk taken as done records its count.
495
+ async #walk(batch, tally) {
496
+ let after;
497
+ while (!this.#stopping) {
498
+ const { rows } = await this.#executeQuery(batch(after));
499
+ const row = rows.find((row) => row.walkCursor !== undefined);
500
+ assert(row, 'maintenance batch returned no result row: a custom db must return the rows of every statement in a multi-statement query');
501
+ if (tally) {
502
+ tally.retained += Number(row.retained ?? 0);
503
+ tally.deleted += Number(row.deleted ?? 0);
504
+ tally.deletedRetained += Number(row.deletedRetained ?? 0);
505
+ }
506
+ if (!row.walkCursor)
507
+ return true;
508
+ after = row.walkCursor;
509
+ }
510
+ return false;
427
511
  }
428
512
  /**
429
513
  * Defer the next queue-stats aggregate when this pass spent long enough inside it to threaten
@@ -712,11 +796,10 @@ class Boss extends EventEmitter {
712
796
  // because "no rows" from an unreadable view must not read as "horizon is healthy".
713
797
  const readable = await this.#narrowXminHorizonSources(attempted, lastVacuum);
714
798
  this.#xminHorizonSources = readable;
799
+ // Persisted like the horizon warnings themselves, so a table with none of them does not read
800
+ // as a healthy horizon on an instance that cannot check it.
715
801
  if (!readable.length) {
716
- this.emit(events.warning, {
717
- message: 'Unable to read the transaction horizon; the xmin_horizon check is disabled for this instance.',
718
- data: { type: WARNING_TYPES.XMIN_HORIZON, error: err?.message }
719
- });
802
+ await emitAndPersistWarning(this.#warningContext, WARNING_TYPES.XMIN_HORIZON, 'Unable to read the transaction horizon; the xmin_horizon check is disabled for this instance.', { type: WARNING_TYPES.XMIN_HORIZON, instance: this.#manager.instanceId, error: err?.message });
720
803
  }
721
804
  return null;
722
805
  }
package/dist/cli.js CHANGED
@@ -331,9 +331,20 @@ async function cmdMigrate(args) {
331
331
  const partitionTables = await getPartitionTables(db, schema);
332
332
  const { sql, concurrent } = migrationStore.migrateCommands(schema, version, migrationStore.getAllForConfig(config), config.noAdvisoryLocks, { inlineAsync: true, partitionTables });
333
333
  await db.executeSql(sql);
334
- // CONCURRENTLY index builds must run outside the migration transaction, one at a time.
335
- for (const statement of concurrent) {
336
- await db.executeSql(statement);
334
+ // CONCURRENTLY index builds must run outside the migration transaction, one at a time. The
335
+ // version is already committed by then and nothing enqueues them for BAM, so a failed build
336
+ // would never be retried: hand the rest to the operator instead.
337
+ for (const [i, statement] of concurrent.entries()) {
338
+ try {
339
+ await db.executeSql(statement);
340
+ }
341
+ catch (err) {
342
+ console.error(`pg-boss schema "${schema}" is at version ${schemaVersion}, but an index build failed: ${err.message}`);
343
+ console.error('Drop any invalid index the failed build left, then run these statements one at a time:\n');
344
+ console.error(concurrent.slice(i).map(rest => `${rest};`).join('\n\n'));
345
+ process.exitCode = 1;
346
+ return;
347
+ }
337
348
  }
338
349
  console.log(`Successfully migrated pg-boss schema "${schema}" to version ${schemaVersion}`);
339
350
  }
package/dist/db.d.ts CHANGED
@@ -17,7 +17,10 @@ declare class Db extends EventEmitter implements types.IDatabase, types.EventsMi
17
17
  close(): Promise<void>;
18
18
  poolCounts(): types.PoolCounts | null;
19
19
  setSessionStatements(statements: string[]): Promise<void>;
20
- executeSql(text: string, values?: unknown[]): Promise<import("pg").QueryResult<any>>;
20
+ executeSql(text: string, values?: unknown[], { signal, retire }?: {
21
+ signal?: AbortSignal;
22
+ retire?: AbortSignal;
23
+ }): Promise<import("pg").QueryResult<any>>;
21
24
  listen(channel: string, onNotification: (payload: string) => void, onReconnect: () => void): Promise<types.ListenHandle>;
22
25
  beginTransaction(): Promise<types.TransactionHandle>;
23
26
  withTransaction<T>(fn: (db: types.IDatabase) => Promise<T>): Promise<T>;
package/dist/db.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,MAAM,aAAa,CAAA;AAItC,OAAO,KAAK,KAAK,KAAK,MAAM,YAAY,CAAA;AAaxC,eAAO,MAAM,+BAA+B,OAAO,CAAA;AAEnD,cAAM,EAAG,SAAQ,YAAa,YAAW,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,WAAW;IACzE,OAAO,CAAC,IAAI,CAAU;IACtB,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,KAAK,CAAa;IAG1B,OAAO,CAAC,iBAAiB,CAAe;IAGxC,MAAM,EAAE,OAAO,CAAA;gBAEF,MAAM,EAAE,KAAK,CAAC,eAAe,GAAG;QAAE,KAAK,CAAC,EAAE,KAAK,CAAC,KAAK,CAAA;KAAE;IAcpE,MAAM;;MAEL;IAEK,IAAI;IAgBJ,KAAK;IAQX,UAAU,IAAK,KAAK,CAAC,UAAU,GAAG,IAAI;IAUhC,oBAAoB,CAAE,UAAU,EAAE,MAAM,EAAE;IAoC1C,UAAU,CAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE;IAqB5C,MAAM,CACV,OAAO,EAAE,MAAM,EACf,cAAc,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,EACzC,WAAW,EAAE,MAAM,IAAI,GACtB,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC;IA2JxB,gBAAgB,IAAK,OAAO,CAAC,KAAK,CAAC,iBAAiB,CAAC;IAsFrD,eAAe,CAAC,CAAC,EAAG,EAAE,EAAE,CAAC,EAAE,EAAE,KAAK,CAAC,SAAS,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;CAY/E;AAED,eAAe,EAAE,CAAA"}
1
+ {"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,MAAM,aAAa,CAAA;AAItC,OAAO,KAAK,KAAK,KAAK,MAAM,YAAY,CAAA;AAaxC,eAAO,MAAM,+BAA+B,OAAO,CAAA;AAEnD,cAAM,EAAG,SAAQ,YAAa,YAAW,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,WAAW;IACzE,OAAO,CAAC,IAAI,CAAU;IACtB,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,KAAK,CAAa;IAG1B,OAAO,CAAC,iBAAiB,CAAe;IAGxC,MAAM,EAAE,OAAO,CAAA;gBAEF,MAAM,EAAE,KAAK,CAAC,eAAe,GAAG;QAAE,KAAK,CAAC,EAAE,KAAK,CAAC,KAAK,CAAA;KAAE;IAcpE,MAAM;;MAEL;IAEK,IAAI;IAgBJ,KAAK;IAQX,UAAU,IAAK,KAAK,CAAC,UAAU,GAAG,IAAI;IAUhC,oBAAoB,CAAE,UAAU,EAAE,MAAM,EAAE;IA2C1C,UAAU,CAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,GAAE;QAAE,MAAM,CAAC,EAAE,WAAW,CAAC;QAAC,MAAM,CAAC,EAAE,WAAW,CAAA;KAAO;IA4DrH,MAAM,CACV,OAAO,EAAE,MAAM,EACf,cAAc,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,EACzC,WAAW,EAAE,MAAM,IAAI,GACtB,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC;IA2JxB,gBAAgB,IAAK,OAAO,CAAC,KAAK,CAAC,iBAAiB,CAAC;IAsFrD,eAAe,CAAC,CAAC,EAAG,EAAE,EAAE,CAAC,EAAE,EAAE,KAAK,CAAC,SAAS,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;CAY/E;AAED,eAAe,EAAE,CAAA"}