pg-boss 12.4.0 → 12.5.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.
@@ -0,0 +1,547 @@
1
+ import assert, { notStrictEqual } from 'node:assert';
2
+ import { randomUUID } from 'node:crypto';
3
+ import EventEmitter from 'node:events';
4
+ import { serializeError as stringify } from 'serialize-error';
5
+ import * as Attorney from "./attorney.js";
6
+ import * as plans from "./plans.js";
7
+ import * as timekeeper from "./timekeeper.js";
8
+ import { resolveWithinSeconds } from "./tools.js";
9
+ import * as types from "./types.js";
10
+ import Worker from "./worker.js";
11
+ import { JobSpy } from "./spy.js";
12
+ const INTERNAL_QUEUES = Object.values(timekeeper.QUEUES).reduce((acc, i) => ({ ...acc, [i]: i }), {});
13
+ const events = {
14
+ error: 'error',
15
+ wip: 'wip'
16
+ };
17
+ class Manager extends EventEmitter {
18
+ events = events;
19
+ db;
20
+ config;
21
+ wipTs;
22
+ workers;
23
+ stopped;
24
+ queueCacheInterval;
25
+ timekeeper;
26
+ queues;
27
+ pendingOffWorkCleanups;
28
+ #spies;
29
+ constructor(db, config) {
30
+ super();
31
+ this.config = config;
32
+ this.db = db;
33
+ this.wipTs = Date.now();
34
+ this.workers = new Map();
35
+ this.queues = null;
36
+ this.pendingOffWorkCleanups = new Set();
37
+ this.#spies = new Map();
38
+ }
39
+ getSpy(name) {
40
+ if (!this.config.__test__enableSpies) {
41
+ throw new Error('Spy is not enabled. Set __test__enableSpies: true in constructor options to use spies.');
42
+ }
43
+ let spy = this.#spies.get(name);
44
+ if (!spy) {
45
+ spy = new JobSpy();
46
+ this.#spies.set(name, spy);
47
+ }
48
+ return spy;
49
+ }
50
+ clearSpies() {
51
+ for (const spy of this.#spies.values()) {
52
+ spy.clear();
53
+ }
54
+ this.#spies.clear();
55
+ }
56
+ async start() {
57
+ this.stopped = false;
58
+ this.queueCacheInterval = setInterval(() => this.onCacheQueues({ emit: true }), this.config.queueCacheIntervalSeconds * 1000);
59
+ await this.onCacheQueues();
60
+ }
61
+ async onCacheQueues({ emit = false } = {}) {
62
+ try {
63
+ assert(!this.config.__test__throw_queueCache, 'test error');
64
+ const queues = await this.getQueues();
65
+ this.queues = queues.reduce((acc, i) => { acc[i.name] = i; return acc; }, {});
66
+ }
67
+ catch (error) {
68
+ emit && this.emit(events.error, { ...error, message: error.message, stack: error.stack });
69
+ }
70
+ }
71
+ async getQueueCache(name) {
72
+ assert(this.queues, 'Queue cache is not initialized');
73
+ let queue = this.queues[name];
74
+ if (queue) {
75
+ return queue;
76
+ }
77
+ queue = await this.getQueue(name);
78
+ if (!queue) {
79
+ throw new Error(`Queue ${name} does not exist`);
80
+ }
81
+ this.queues[name] = queue;
82
+ return queue;
83
+ }
84
+ async stop() {
85
+ this.stopped = true;
86
+ clearInterval(this.queueCacheInterval);
87
+ await Promise.allSettled([...this.workers.values()]
88
+ .filter(worker => !INTERNAL_QUEUES[worker.name])
89
+ .map(async (worker) => await this.offWork(worker.name, { wait: false })));
90
+ }
91
+ async failWip() {
92
+ for (const worker of this.workers.values()) {
93
+ const jobIds = worker.jobs.map(j => j.id);
94
+ if (jobIds.length) {
95
+ await this.fail(worker.name, jobIds, 'pg-boss shut down while active');
96
+ }
97
+ }
98
+ }
99
+ async work(name, ...args) {
100
+ const { options, callback } = Attorney.checkWorkArgs(name, args);
101
+ if (this.stopped) {
102
+ throw new Error('Workers are disabled. pg-boss is stopped');
103
+ }
104
+ const { pollingInterval: interval, batchSize = 1, includeMetadata = false, priority = true } = options;
105
+ const id = randomUUID({ disableEntropyCache: true });
106
+ const fetch = () => this.fetch(name, { batchSize, includeMetadata, priority });
107
+ const onFetch = async (jobs) => {
108
+ if (!jobs.length) {
109
+ return;
110
+ }
111
+ if (this.config.__test__throw_worker) {
112
+ throw new Error('__test__throw_worker');
113
+ }
114
+ this.emitWip(name);
115
+ const spy = this.config.__test__enableSpies ? this.#spies.get(name) : undefined;
116
+ if (spy) {
117
+ for (const job of jobs) {
118
+ spy.addJob(job.id, name, job.data, 'active');
119
+ }
120
+ }
121
+ const maxExpiration = jobs.reduce((acc, i) => Math.max(acc, i.expireInSeconds), 0);
122
+ const jobIds = jobs.map(job => job.id);
123
+ const ac = new AbortController();
124
+ jobs.forEach(job => { job.signal = ac.signal; });
125
+ try {
126
+ const result = await resolveWithinSeconds(callback(jobs), maxExpiration, `handler execution exceeded ${maxExpiration}s`, ac);
127
+ await this.complete(name, jobIds, jobIds.length === 1 ? result : undefined);
128
+ if (spy) {
129
+ for (const job of jobs) {
130
+ spy.addJob(job.id, name, job.data, 'completed', jobIds.length === 1 ? result : undefined);
131
+ }
132
+ }
133
+ }
134
+ catch (err) {
135
+ await this.fail(name, jobIds, err);
136
+ if (spy) {
137
+ for (const job of jobs) {
138
+ spy.addJob(job.id, name, job.data, 'failed', { message: err?.message, stack: err?.stack });
139
+ }
140
+ }
141
+ }
142
+ this.emitWip(name);
143
+ };
144
+ const onError = (error) => {
145
+ this.emit(events.error, { ...error, message: error.message, stack: error.stack, queue: name, worker: id });
146
+ };
147
+ const worker = new Worker({ id, name, options, interval, fetch, onFetch, onError });
148
+ this.addWorker(worker);
149
+ worker.start();
150
+ return id;
151
+ }
152
+ addWorker(worker) {
153
+ this.workers.set(worker.id, worker);
154
+ }
155
+ removeWorker(worker) {
156
+ this.workers.delete(worker.id);
157
+ }
158
+ getWorkers() {
159
+ return Array.from(this.workers.values());
160
+ }
161
+ emitWip(name) {
162
+ if (!INTERNAL_QUEUES[name]) {
163
+ const now = Date.now();
164
+ if (now - this.wipTs > 2000) {
165
+ this.emit(events.wip, this.getWipData());
166
+ this.wipTs = now;
167
+ }
168
+ }
169
+ }
170
+ getWipData(options = {}) {
171
+ const { includeInternal = false } = options;
172
+ const data = this.getWorkers()
173
+ .map(i => i.toWipData())
174
+ .filter(i => i.state !== 'stopped' && (!INTERNAL_QUEUES[i.name] || includeInternal));
175
+ return data;
176
+ }
177
+ hasPendingCleanups() {
178
+ return this.pendingOffWorkCleanups.size > 0;
179
+ }
180
+ async offWork(name, options = { wait: true }) {
181
+ assert(name, 'queue name is required');
182
+ assert(typeof name === 'string', 'queue name must be a string');
183
+ const query = (i) => options?.id ? i.id === options.id : i.name === name;
184
+ const workers = this.getWorkers().filter(i => query(i) && !i.stopping && !i.stopped);
185
+ if (workers.length === 0) {
186
+ return;
187
+ }
188
+ const cleanupPromise = Promise.allSettled(workers.map(async (worker) => {
189
+ await worker.stop();
190
+ this.removeWorker(worker);
191
+ }));
192
+ if (options.wait) {
193
+ await cleanupPromise;
194
+ }
195
+ else {
196
+ this.pendingOffWorkCleanups.add(cleanupPromise);
197
+ cleanupPromise.finally(() => this.pendingOffWorkCleanups.delete(cleanupPromise));
198
+ }
199
+ }
200
+ notifyWorker(workerId) {
201
+ this.workers.get(workerId)?.notify();
202
+ }
203
+ async subscribe(event, name) {
204
+ assert(event, 'Missing required argument');
205
+ assert(name, 'Missing required argument');
206
+ const sql = plans.subscribe(this.config.schema);
207
+ await this.db.executeSql(sql, [event, name]);
208
+ }
209
+ async unsubscribe(event, name) {
210
+ assert(event, 'Missing required argument');
211
+ assert(name, 'Missing required argument');
212
+ const sql = plans.unsubscribe(this.config.schema);
213
+ await this.db.executeSql(sql, [event, name]);
214
+ }
215
+ async publish(event, data, options) {
216
+ assert(event, 'Missing required argument');
217
+ const sql = plans.getQueuesForEvent(this.config.schema);
218
+ const { rows } = await this.db.executeSql(sql, [event]);
219
+ await Promise.allSettled(rows.map(({ name }) => this.send(name, data, options)));
220
+ }
221
+ async send(...args) {
222
+ const result = Attorney.checkSendArgs(args);
223
+ return await this.createJob(result);
224
+ }
225
+ async sendAfter(name, data, options, after) {
226
+ options = options ? { ...options } : {};
227
+ options.startAfter = after;
228
+ const result = Attorney.checkSendArgs([name, data, options]);
229
+ return await this.createJob(result);
230
+ }
231
+ async sendThrottled(name, data, options, seconds, key) {
232
+ options = options ? { ...options } : {};
233
+ options.singletonSeconds = seconds;
234
+ options.singletonNextSlot = false;
235
+ options.singletonKey = key;
236
+ const result = Attorney.checkSendArgs([name, data, options]);
237
+ return await this.createJob(result);
238
+ }
239
+ async sendDebounced(name, data, options, seconds, key) {
240
+ options = options ? { ...options } : {};
241
+ options.singletonSeconds = seconds;
242
+ options.singletonNextSlot = true;
243
+ options.singletonKey = key;
244
+ const result = Attorney.checkSendArgs([name, data, options]);
245
+ return await this.createJob(result);
246
+ }
247
+ async createJob(request) {
248
+ const { name, data = null, options = {} } = request;
249
+ const { id = null, db: wrapper, priority, startAfter, singletonKey = null, singletonSeconds, singletonNextSlot, expireInSeconds, deleteAfterSeconds, retentionSeconds, keepUntil, retryLimit, retryDelay, retryBackoff, retryDelayMax } = options;
250
+ const job = {
251
+ id,
252
+ name,
253
+ data,
254
+ priority,
255
+ startAfter,
256
+ singletonKey,
257
+ singletonSeconds,
258
+ singletonOffset: 0,
259
+ expireInSeconds,
260
+ deleteAfterSeconds,
261
+ retentionSeconds,
262
+ keepUntil,
263
+ retryLimit,
264
+ retryDelay,
265
+ retryBackoff,
266
+ retryDelayMax
267
+ };
268
+ const db = wrapper || this.db;
269
+ const { table } = await this.getQueueCache(name);
270
+ const sql = plans.insertJobs(this.config.schema, { table, name, returnId: true });
271
+ const { rows: try1 } = await db.executeSql(sql, [JSON.stringify([job])]);
272
+ if (try1.length === 1) {
273
+ const jobId = try1[0].id;
274
+ if (this.config.__test__enableSpies) {
275
+ const spy = this.#spies.get(name);
276
+ if (spy) {
277
+ spy.addJob(jobId, name, data || {}, 'created');
278
+ }
279
+ }
280
+ return jobId;
281
+ }
282
+ if (singletonNextSlot) {
283
+ // delay starting by the offset to honor throttling config
284
+ job.startAfter = this.getDebounceStartAfter(singletonSeconds, this.timekeeper.clockSkew);
285
+ job.singletonOffset = singletonSeconds;
286
+ const { rows: try2 } = await db.executeSql(sql, [JSON.stringify([job])]);
287
+ if (try2.length === 1) {
288
+ const jobId = try2[0].id;
289
+ if (this.config.__test__enableSpies) {
290
+ const spy = this.#spies.get(name);
291
+ if (spy) {
292
+ spy.addJob(jobId, name, data || {}, 'created');
293
+ }
294
+ }
295
+ return jobId;
296
+ }
297
+ }
298
+ return null;
299
+ }
300
+ async insert(name, jobs, options = {}) {
301
+ assert(Array.isArray(jobs), 'jobs argument should be an array');
302
+ const { table } = await this.getQueueCache(name);
303
+ const db = this.assertDb(options);
304
+ const spy = this.config.__test__enableSpies ? this.#spies.get(name) : undefined;
305
+ // Return IDs if spy is active for this queue (needed for job tracking)
306
+ const returnId = !!spy;
307
+ const sql = plans.insertJobs(this.config.schema, { table, name, returnId });
308
+ const { rows } = await db.executeSql(sql, [JSON.stringify(jobs)]);
309
+ if (rows.length) {
310
+ if (spy) {
311
+ for (let i = 0; i < rows.length; i++) {
312
+ spy.addJob(rows[i].id, name, jobs[i].data || {}, 'created');
313
+ }
314
+ }
315
+ return rows.map((i) => i.id);
316
+ }
317
+ return null;
318
+ }
319
+ getDebounceStartAfter(singletonSeconds, clockOffset) {
320
+ const debounceInterval = singletonSeconds * 1000;
321
+ const now = Date.now() + clockOffset;
322
+ const slot = Math.floor(now / debounceInterval) * debounceInterval;
323
+ // prevent startAfter=0 during debouncing
324
+ let startAfter = (singletonSeconds - Math.floor((now - slot) / 1000)) || 1;
325
+ if (singletonSeconds > 1) {
326
+ startAfter++;
327
+ }
328
+ return startAfter;
329
+ }
330
+ async fetch(name, options = {}) {
331
+ Attorney.checkFetchArgs(name, options);
332
+ const db = this.assertDb(options);
333
+ const { table, policy, singletonsActive } = await this.getQueueCache(name);
334
+ const fetchOptions = {
335
+ ...options,
336
+ schema: this.config.schema,
337
+ table,
338
+ name,
339
+ policy,
340
+ limit: options.batchSize || 1,
341
+ ignoreSingletons: singletonsActive
342
+ };
343
+ const sql = plans.fetchNextJob(fetchOptions);
344
+ let result;
345
+ try {
346
+ result = await db.executeSql(sql);
347
+ }
348
+ catch (err) {
349
+ // errors from fetchquery should only be unique constraint violations
350
+ }
351
+ return result?.rows || [];
352
+ }
353
+ mapCompletionIdArg(id, funcName) {
354
+ const errorMessage = `${funcName}() requires an id`;
355
+ assert(id, errorMessage);
356
+ const ids = Array.isArray(id) ? id : [id];
357
+ assert(ids.length, errorMessage);
358
+ return ids;
359
+ }
360
+ mapCompletionDataArg(data) {
361
+ if (data === null || typeof data === 'undefined' || typeof data === 'function') {
362
+ return null;
363
+ }
364
+ const result = (typeof data === 'object' && !Array.isArray(data))
365
+ ? data
366
+ : { value: data };
367
+ return stringify(result);
368
+ }
369
+ mapCommandResponse(ids, result) {
370
+ return {
371
+ jobs: ids,
372
+ requested: ids.length,
373
+ affected: result && result.rows ? parseInt(result.rows[0].count) : 0
374
+ };
375
+ }
376
+ async complete(name, id, data, options = {}) {
377
+ Attorney.assertQueueName(name);
378
+ const db = this.assertDb(options);
379
+ const ids = this.mapCompletionIdArg(id, 'complete');
380
+ const { table } = await this.getQueueCache(name);
381
+ const sql = plans.completeJobs(this.config.schema, table);
382
+ const result = await db.executeSql(sql, [name, ids, this.mapCompletionDataArg(data)]);
383
+ return this.mapCommandResponse(ids, result);
384
+ }
385
+ async fail(name, id, data, options = {}) {
386
+ Attorney.assertQueueName(name);
387
+ const db = this.assertDb(options);
388
+ const ids = this.mapCompletionIdArg(id, 'fail');
389
+ const { table } = await this.getQueueCache(name);
390
+ const sql = plans.failJobsById(this.config.schema, table);
391
+ const result = await db.executeSql(sql, [name, ids, this.mapCompletionDataArg(data)]);
392
+ return this.mapCommandResponse(ids, result);
393
+ }
394
+ async cancel(name, id, options = {}) {
395
+ Attorney.assertQueueName(name);
396
+ const db = this.assertDb(options);
397
+ const ids = this.mapCompletionIdArg(id, 'cancel');
398
+ const { table } = await this.getQueueCache(name);
399
+ const sql = plans.cancelJobs(this.config.schema, table);
400
+ const result = await db.executeSql(sql, [name, ids]);
401
+ return this.mapCommandResponse(ids, result);
402
+ }
403
+ async deleteJob(name, id, options = {}) {
404
+ Attorney.assertQueueName(name);
405
+ const db = this.assertDb(options);
406
+ const ids = this.mapCompletionIdArg(id, 'deleteJob');
407
+ const { table } = await this.getQueueCache(name);
408
+ const sql = plans.deleteJobsById(this.config.schema, table);
409
+ const result = await db.executeSql(sql, [name, ids]);
410
+ return this.mapCommandResponse(ids, result);
411
+ }
412
+ async resume(name, id, options = {}) {
413
+ Attorney.assertQueueName(name);
414
+ const db = this.assertDb(options);
415
+ const ids = this.mapCompletionIdArg(id, 'resume');
416
+ const { table } = await this.getQueueCache(name);
417
+ const sql = plans.resumeJobs(this.config.schema, table);
418
+ const result = await db.executeSql(sql, [name, ids]);
419
+ return this.mapCommandResponse(ids, result);
420
+ }
421
+ async retry(name, id, options = {}) {
422
+ Attorney.assertQueueName(name);
423
+ const db = options.db || this.db;
424
+ const ids = this.mapCompletionIdArg(id, 'retry');
425
+ const { table } = await this.getQueueCache(name);
426
+ const sql = plans.retryJobs(this.config.schema, table);
427
+ const result = await db.executeSql(sql, [name, ids]);
428
+ return this.mapCommandResponse(ids, result);
429
+ }
430
+ async createQueue(name, options = {}) {
431
+ name = name || options.name;
432
+ Attorney.assertQueueName(name);
433
+ const policy = options.policy || plans.QUEUE_POLICIES.standard;
434
+ assert(policy in plans.QUEUE_POLICIES, `${policy} is not a valid queue policy`);
435
+ Attorney.validateQueueArgs(options);
436
+ if (options.deadLetter) {
437
+ Attorney.assertQueueName(options.deadLetter);
438
+ notStrictEqual(name, options.deadLetter, 'deadLetter cannot be itself');
439
+ await this.getQueueCache(options.deadLetter);
440
+ }
441
+ const sql = plans.createQueue(this.config.schema, name, { ...options, policy });
442
+ await this.db.executeSql(sql);
443
+ }
444
+ async getQueues(names) {
445
+ names = Array.isArray(names) ? names : typeof names === 'string' ? [names] : undefined;
446
+ if (names) {
447
+ for (const name of names) {
448
+ Attorney.assertQueueName(name);
449
+ }
450
+ }
451
+ const sql = plans.getQueues(this.config.schema, names);
452
+ const { rows } = await this.db.executeSql(sql);
453
+ return rows;
454
+ }
455
+ async updateQueue(name, options = {}) {
456
+ Attorney.assertQueueName(name);
457
+ assert(Object.keys(options).length > 0, 'no properties found to update');
458
+ if ('policy' in options) {
459
+ throw new Error('queue policy cannot be changed after creation');
460
+ }
461
+ if ('partition' in options) {
462
+ throw new Error('queue partitioning cannot be changed after creation');
463
+ }
464
+ Attorney.validateQueueArgs(options);
465
+ const { deadLetter } = options;
466
+ if (deadLetter) {
467
+ Attorney.assertQueueName(deadLetter);
468
+ notStrictEqual(name, deadLetter, 'deadLetter cannot be itself');
469
+ }
470
+ const sql = plans.updateQueue(this.config.schema, { deadLetter });
471
+ await this.db.executeSql(sql, [name, options]);
472
+ }
473
+ async getQueue(name) {
474
+ Attorney.assertQueueName(name);
475
+ const sql = plans.getQueues(this.config.schema, [name]);
476
+ const { rows } = await this.db.executeSql(sql);
477
+ return rows[0] || null;
478
+ }
479
+ async deleteQueue(name) {
480
+ Attorney.assertQueueName(name);
481
+ try {
482
+ await this.getQueueCache(name);
483
+ const sql = plans.deleteQueue(this.config.schema, name);
484
+ await this.db.executeSql(sql);
485
+ }
486
+ catch { }
487
+ }
488
+ async deleteQueuedJobs(name) {
489
+ Attorney.assertQueueName(name);
490
+ const { table } = await this.getQueueCache(name);
491
+ const sql = plans.deleteQueuedJobs(this.config.schema, table);
492
+ await this.db.executeSql(sql, [name]);
493
+ }
494
+ async deleteStoredJobs(name) {
495
+ Attorney.assertQueueName(name);
496
+ const { table } = await this.getQueueCache(name);
497
+ const sql = plans.deleteStoredJobs(this.config.schema, table);
498
+ await this.db.executeSql(sql, [name]);
499
+ }
500
+ async deleteAllJobs(name) {
501
+ if (!name) {
502
+ const sql = plans.truncateTable(this.config.schema, 'job');
503
+ await this.db.executeSql(sql);
504
+ return;
505
+ }
506
+ Attorney.assertQueueName(name);
507
+ const { table, partition } = await this.getQueueCache(name);
508
+ if (partition) {
509
+ const sql = plans.truncateTable(this.config.schema, table);
510
+ await this.db.executeSql(sql);
511
+ }
512
+ else {
513
+ const sql = plans.deleteAllJobs(this.config.schema, table);
514
+ await this.db.executeSql(sql, [name]);
515
+ }
516
+ }
517
+ async getQueueStats(name) {
518
+ Attorney.assertQueueName(name);
519
+ const queue = await this.getQueueCache(name);
520
+ const sql = plans.getQueueStats(this.config.schema, queue.table, [name]);
521
+ const { rows } = await this.db.executeSql(sql);
522
+ return Object.assign(queue, rows.at(0) || {});
523
+ }
524
+ async getJobById(name, id, options = {}) {
525
+ Attorney.assertQueueName(name);
526
+ const db = this.assertDb(options);
527
+ const { table } = await this.getQueueCache(name);
528
+ const sql = plans.getJobById(this.config.schema, table);
529
+ const result1 = await db.executeSql(sql, [name, id]);
530
+ if (result1?.rows?.length === 1) {
531
+ return result1.rows[0];
532
+ }
533
+ else {
534
+ return null;
535
+ }
536
+ }
537
+ assertDb(options) {
538
+ if (options.db) {
539
+ return options.db;
540
+ }
541
+ if (this.db._pgbdb) {
542
+ assert(this.db.opened, 'Database connection is not opened');
543
+ }
544
+ return this.db;
545
+ }
546
+ }
547
+ export default Manager;
@@ -0,0 +1,7 @@
1
+ import * as types from './types.ts';
2
+ declare function rollback(schema: string, version: number, migrations?: types.Migration[]): string;
3
+ declare function next(schema: string, version: number, migrations: types.Migration[] | undefined): string;
4
+ declare function migrate(schema: string, version: number, migrations?: types.Migration[]): string;
5
+ declare function getAll(schema: string): types.Migration[];
6
+ export { rollback, next, migrate, getAll, };
7
+ //# sourceMappingURL=migrationStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrationStore.d.ts","sourceRoot":"","sources":["../src/migrationStore.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,YAAY,CAAA;AASnC,iBAAS,QAAQ,CAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,KAAK,CAAC,SAAS,EAAE,UAQjF;AAED,iBAAS,IAAI,CAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,CAAC,SAAS,EAAE,GAAG,SAAS,UAQxF;AAED,iBAAS,OAAO,CAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,KAAK,CAAC,SAAS,EAAE,UAehF;AAED,iBAAS,MAAM,CAAE,MAAM,EAAE,MAAM,GAAG,KAAK,CAAC,SAAS,EAAE,CA0FlD;AAED,OAAO,EACL,QAAQ,EACR,IAAI,EACJ,OAAO,EACP,MAAM,GACP,CAAA"}
@@ -0,0 +1,126 @@
1
+ import assert from 'node:assert';
2
+ import * as plans from "./plans.js";
3
+ import * as types from "./types.js";
4
+ function flatten(schema, commands, version) {
5
+ commands.unshift(plans.assertMigration(schema, version));
6
+ commands.push(plans.setVersion(schema, version));
7
+ return plans.locked(schema, commands);
8
+ }
9
+ function rollback(schema, version, migrations) {
10
+ migrations = migrations || getAll(schema);
11
+ const result = migrations.find(i => i.version === version);
12
+ assert(result, `Version ${version} not found.`);
13
+ return flatten(schema, result.uninstall || [], result.previous);
14
+ }
15
+ function next(schema, version, migrations) {
16
+ migrations = migrations || getAll(schema);
17
+ const result = migrations.find(i => i.previous === version);
18
+ assert(result, `Version ${version} not found.`);
19
+ return flatten(schema, result.install, result.version);
20
+ }
21
+ function migrate(schema, version, migrations) {
22
+ migrations = migrations || getAll(schema);
23
+ const result = migrations
24
+ .filter(i => i.previous >= version)
25
+ .sort((a, b) => a.version - b.version)
26
+ .reduce((acc, i) => {
27
+ acc.install = acc.install.concat(i.install);
28
+ acc.version = i.version;
29
+ return acc;
30
+ }, { install: [], version });
31
+ assert(result.install.length > 0, `Version ${version} not found.`);
32
+ return flatten(schema, result.install, result.version);
33
+ }
34
+ function getAll(schema) {
35
+ return [
36
+ {
37
+ release: '11.1.0',
38
+ version: 26,
39
+ previous: 25,
40
+ install: [
41
+ `
42
+ CREATE OR REPLACE FUNCTION ${schema}.create_queue(queue_name text, options jsonb)
43
+ RETURNS VOID AS
44
+ $$
45
+ DECLARE
46
+ tablename varchar := CASE WHEN options->>'partition' = 'true'
47
+ THEN 'j' || encode(sha224(queue_name::bytea), 'hex')
48
+ ELSE 'job_common'
49
+ END;
50
+ queue_created_on timestamptz;
51
+ BEGIN
52
+
53
+ WITH q as (
54
+ INSERT INTO ${schema}.queue (
55
+ name,
56
+ policy,
57
+ retry_limit,
58
+ retry_delay,
59
+ retry_backoff,
60
+ retry_delay_max,
61
+ expire_seconds,
62
+ retention_seconds,
63
+ deletion_seconds,
64
+ warning_queued,
65
+ dead_letter,
66
+ partition,
67
+ table_name
68
+ )
69
+ VALUES (
70
+ queue_name,
71
+ options->>'policy',
72
+ COALESCE((options->>'retryLimit')::int, 2),
73
+ COALESCE((options->>'retryDelay')::int, 0),
74
+ COALESCE((options->>'retryBackoff')::bool, false),
75
+ (options->>'retryDelayMax')::int,
76
+ COALESCE((options->>'expireInSeconds')::int, 900),
77
+ COALESCE((options->>'retentionSeconds')::int, 1209600),
78
+ COALESCE((options->>'deleteAfterSeconds')::int, 604800),
79
+ COALESCE((options->>'warningQueueSize')::int, 0),
80
+ options->>'deadLetter',
81
+ COALESCE((options->>'partition')::bool, false),
82
+ tablename
83
+ )
84
+ ON CONFLICT DO NOTHING
85
+ RETURNING created_on
86
+ )
87
+ SELECT created_on into queue_created_on from q;
88
+
89
+ IF queue_created_on IS NULL OR options->>'partition' IS DISTINCT FROM 'true' THEN
90
+ RETURN;
91
+ END IF;
92
+
93
+ EXECUTE format('CREATE TABLE ${schema}.%I (LIKE ${schema}.job INCLUDING DEFAULTS)', tablename);
94
+
95
+ EXECUTE format('ALTER TABLE ${schema}.%1$I ADD PRIMARY KEY (name, id)', tablename);
96
+ 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);
97
+ 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);
98
+
99
+ EXECUTE format('CREATE INDEX %1$s_i5 ON ${schema}.%1$I (name, start_after) INCLUDE (priority, created_on, id) WHERE state < ''active''', tablename);
100
+ 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);
101
+
102
+ IF options->>'policy' = 'short' THEN
103
+ EXECUTE format('CREATE UNIQUE INDEX %1$s_i1 ON ${schema}.%1$I (name, COALESCE(singleton_key, '''')) WHERE state = ''created'' AND policy = ''short''', tablename);
104
+ ELSIF options->>'policy' = 'singleton' THEN
105
+ EXECUTE format('CREATE UNIQUE INDEX %1$s_i2 ON ${schema}.%1$I (name, COALESCE(singleton_key, '''')) WHERE state = ''active'' AND policy = ''singleton''', tablename);
106
+ ELSIF options->>'policy' = 'stately' THEN
107
+ EXECUTE format('CREATE UNIQUE INDEX %1$s_i3 ON ${schema}.%1$I (name, state, COALESCE(singleton_key, '''')) WHERE state <= ''active'' AND policy = ''stately''', tablename);
108
+ ELSIF options->>'policy' = 'exclusive' THEN
109
+ EXECUTE format('CREATE UNIQUE INDEX %1$s_i6 ON ${schema}.%1$I (name, COALESCE(singleton_key, '''')) WHERE state <= ''active'' AND policy = ''exclusive''', tablename);
110
+ END IF;
111
+
112
+ EXECUTE format('ALTER TABLE ${schema}.%I ADD CONSTRAINT cjc CHECK (name=%L)', tablename, queue_name);
113
+ EXECUTE format('ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.%I FOR VALUES IN (%L)', tablename, queue_name);
114
+ END;
115
+ $$
116
+ LANGUAGE plpgsql;
117
+ `,
118
+ `CREATE UNIQUE INDEX job_i6 ON ${schema}.job_common (name, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'exclusive'`
119
+ ],
120
+ uninstall: [
121
+ `DROP INDEX ${schema}.job_i6`
122
+ ]
123
+ },
124
+ ];
125
+ }
126
+ export { rollback, next, migrate, getAll, };