@spfn/core 0.3.0-beta.6 → 0.3.0-beta.8
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.
- package/dist/{boss-D16fO2oG.d.ts → boss-BiDCbbAQ.d.ts} +60 -7
- package/dist/job/index.d.ts +48 -3
- package/dist/job/index.js +118 -40
- package/dist/job/index.js.map +1 -1
- package/dist/server/index.d.ts +19 -2
- package/dist/server/index.js +129 -39
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
- package/src/job/README.md +108 -9
|
@@ -7,6 +7,23 @@ import PgBoss from 'pg-boss';
|
|
|
7
7
|
* Type definitions for the pg-boss based job system
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* pg-boss queue policy
|
|
12
|
+
*
|
|
13
|
+
* The policy decides which unique index pg-boss puts on
|
|
14
|
+
* `(queue name, singletonKey)`, and therefore what `singletonKey` actually
|
|
15
|
+
* deduplicates. A `standard` queue has no such index: the key is stored and
|
|
16
|
+
* ignored.
|
|
17
|
+
*
|
|
18
|
+
* | policy | one job per `(queue, singletonKey)` among states… |
|
|
19
|
+
* |---|---|
|
|
20
|
+
* | `standard` | none — the key is ignored |
|
|
21
|
+
* | `short` | `created` (queued, not yet picked up) |
|
|
22
|
+
* | `singleton` | `active` (running) — many may queue, one runs at a time |
|
|
23
|
+
* | `stately` | one per state among `created`, `retry`, `active` |
|
|
24
|
+
* | `exclusive` | `created` + `retry` + `active` together |
|
|
25
|
+
*/
|
|
26
|
+
type JobQueuePolicy = 'standard' | 'short' | 'singleton' | 'stately' | 'exclusive';
|
|
10
27
|
/**
|
|
11
28
|
* Job options passed to pg-boss
|
|
12
29
|
*/
|
|
@@ -39,9 +56,32 @@ interface JobOptions {
|
|
|
39
56
|
*/
|
|
40
57
|
priority?: number;
|
|
41
58
|
/**
|
|
42
|
-
*
|
|
59
|
+
* Deduplication key. What it deduplicates depends entirely on `policy`:
|
|
60
|
+
* on a `standard` queue (the default for a plain job) pg-boss stores the
|
|
61
|
+
* key and never conflicts on it, so nothing is deduplicated.
|
|
62
|
+
*
|
|
63
|
+
* Setting this without an explicit `policy` makes the queue `exclusive`.
|
|
43
64
|
*/
|
|
44
65
|
singletonKey?: string;
|
|
66
|
+
/**
|
|
67
|
+
* pg-boss queue policy — what `singletonKey` deduplicates on this queue.
|
|
68
|
+
*
|
|
69
|
+
* | policy | one job per `(queue, singletonKey)` among states… |
|
|
70
|
+
* |---|---|
|
|
71
|
+
* | `standard` | none — the key is ignored |
|
|
72
|
+
* | `short` | `created` |
|
|
73
|
+
* | `singleton` | `active` |
|
|
74
|
+
* | `stately` | one per state among `created`, `retry`, `active` |
|
|
75
|
+
* | `exclusive` | `created` + `retry` + `active` together |
|
|
76
|
+
*
|
|
77
|
+
* Deduplication is scoped by queue name: different queues never
|
|
78
|
+
* deduplicate against each other. `exclusive` with no `singletonKey`
|
|
79
|
+
* collapses the whole queue to one pending job, which is why plain jobs
|
|
80
|
+
* stay `standard`.
|
|
81
|
+
*
|
|
82
|
+
* @default 'exclusive' when the job is `.runOnce()` or sets `singletonKey`, otherwise 'standard'
|
|
83
|
+
*/
|
|
84
|
+
policy?: JobQueuePolicy;
|
|
45
85
|
/**
|
|
46
86
|
* Keep completed jobs for this many seconds
|
|
47
87
|
* @default 604800 (7 days)
|
|
@@ -71,7 +111,11 @@ interface JobSendOptions {
|
|
|
71
111
|
*/
|
|
72
112
|
startAfter?: number | Date;
|
|
73
113
|
/**
|
|
74
|
-
*
|
|
114
|
+
* Deduplication key for this specific job instance.
|
|
115
|
+
*
|
|
116
|
+
* Only takes effect when the job's queue has a non-standard `policy`: see
|
|
117
|
+
* {@link JobOptions.policy}. On a `standard` queue pg-boss stores the key
|
|
118
|
+
* and never conflicts on it, and the first such send logs a warning.
|
|
75
119
|
*/
|
|
76
120
|
singletonKey?: string;
|
|
77
121
|
/**
|
|
@@ -81,8 +125,17 @@ interface JobSendOptions {
|
|
|
81
125
|
}
|
|
82
126
|
/**
|
|
83
127
|
* Job handler function type
|
|
128
|
+
*
|
|
129
|
+
* The check is wrapped in a tuple — `[TInput] extends [void]`, not
|
|
130
|
+
* `TInput extends void` — to stop the conditional from distributing. A bare
|
|
131
|
+
* type parameter in the check position makes the conditional distributive, so a
|
|
132
|
+
* union input `A | B | C` is evaluated per member and the results are unioned
|
|
133
|
+
* into three function types whose parameters intersect to `never`: no caller
|
|
134
|
+
* can then pass any member. The tuple makes the union one opaque type, which is
|
|
135
|
+
* what a handler taking `A | B | C` needs. The same wrap is on `send`, `run`
|
|
136
|
+
* and `sendBatch` below, for the same reason. Do not "simplify" it away.
|
|
84
137
|
*/
|
|
85
|
-
type JobHandler<TInput, TOutput = void> = TInput extends void ? () => Promise<TOutput> : (input: TInput) => Promise<TOutput>;
|
|
138
|
+
type JobHandler<TInput, TOutput = void> = [TInput] extends [void] ? () => Promise<TOutput> : (input: TInput) => Promise<TOutput>;
|
|
86
139
|
/**
|
|
87
140
|
* Compensate handler function type (for rollback)
|
|
88
141
|
*/
|
|
@@ -134,17 +187,17 @@ interface JobDef<TInput = void, TOutput = void> {
|
|
|
134
187
|
/**
|
|
135
188
|
* Send job to queue (returns immediately, executes in background)
|
|
136
189
|
*/
|
|
137
|
-
send: TInput extends void ? (options?: JobSendOptions) => Promise<string | null> : (input: TInput, options?: JobSendOptions) => Promise<string | null>;
|
|
190
|
+
send: [TInput] extends [void] ? (options?: JobSendOptions) => Promise<string | null> : (input: TInput, options?: JobSendOptions) => Promise<string | null>;
|
|
138
191
|
/**
|
|
139
192
|
* Run job synchronously (for testing/debugging)
|
|
140
193
|
*/
|
|
141
|
-
run: TInput extends void ? () => Promise<TOutput> : (input: TInput) => Promise<TOutput>;
|
|
194
|
+
run: [TInput] extends [void] ? () => Promise<TOutput> : (input: TInput) => Promise<TOutput>;
|
|
142
195
|
/**
|
|
143
196
|
* Bulk insert jobs into the queue (pg-boss insert).
|
|
144
197
|
* Much faster than calling send() in a loop.
|
|
145
198
|
* Only available for jobs with input schema.
|
|
146
199
|
*/
|
|
147
|
-
sendBatch: TInput extends void ? (options?: JobSendOptions) => Promise<void> : (inputs: TInput[], options?: JobSendOptions) => Promise<void>;
|
|
200
|
+
sendBatch: [TInput] extends [void] ? (options?: JobSendOptions) => Promise<void> : (inputs: TInput[], options?: JobSendOptions) => Promise<void>;
|
|
148
201
|
/**
|
|
149
202
|
* Type inference helpers
|
|
150
203
|
*/
|
|
@@ -281,4 +334,4 @@ declare function isBossRunning(): boolean;
|
|
|
281
334
|
*/
|
|
282
335
|
declare function shouldClearOnStart(): boolean;
|
|
283
336
|
|
|
284
|
-
export { type BossConfig as B, type CompensateHandler as C, type InferJobInput as I, type JobOptions as J, type JobHandler as a, type JobDef as b, type JobRouter as c, type JobRouterEntry as d, type
|
|
337
|
+
export { type BossConfig as B, type CompensateHandler as C, type InferJobInput as I, type JobOptions as J, type JobHandler as a, type JobDef as b, type JobRouter as c, type JobRouterEntry as d, type JobQueuePolicy as e, type BossOptions as f, type InferJobOutput as g, type JobSendOptions as h, getBoss as i, initBoss as j, isBossRunning as k, stopBoss as l, shouldClearOnStart as s };
|
package/dist/job/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { J as JobOptions, C as CompensateHandler, a as JobHandler, b as JobDef, c as JobRouter, d as JobRouterEntry } from '../boss-
|
|
2
|
-
export { B as BossConfig,
|
|
1
|
+
import { J as JobOptions, C as CompensateHandler, a as JobHandler, b as JobDef, c as JobRouter, d as JobRouterEntry, e as JobQueuePolicy } from '../boss-BiDCbbAQ.js';
|
|
2
|
+
export { B as BossConfig, f as BossOptions, I as InferJobInput, g as InferJobOutput, h as JobSendOptions, i as getBoss, j as initBoss, k as isBossRunning, s as shouldClearOnStart, l as stopBoss } from '../boss-BiDCbbAQ.js';
|
|
3
3
|
import * as _sinclair_typebox from '@sinclair/typebox';
|
|
4
4
|
import { Static } from '@sinclair/typebox';
|
|
5
5
|
import { EventDef, InferEventPayload } from '@spfn/core/event';
|
|
@@ -168,11 +168,56 @@ declare function isJobRouter(value: unknown): value is JobRouter<any>;
|
|
|
168
168
|
* ```
|
|
169
169
|
*/
|
|
170
170
|
declare function defineJobRouter<TJobs extends Record<string, JobRouterEntry>>(jobs: TJobs): JobRouter<TJobs>;
|
|
171
|
+
/**
|
|
172
|
+
* Merge two job routers into a new one
|
|
173
|
+
*
|
|
174
|
+
* `defineServerConfig().jobs()` may be called once per domain router, the way
|
|
175
|
+
* `.routes()` is; each call merges into what is already registered instead of
|
|
176
|
+
* replacing it. Keys are the router's own namespace, so a key present in both
|
|
177
|
+
* routers is a mistake the app cannot recover from silently — one of the two
|
|
178
|
+
* would disappear.
|
|
179
|
+
*
|
|
180
|
+
* Neither input is mutated: a fresh router is returned.
|
|
181
|
+
*
|
|
182
|
+
* @throws if a top-level key exists in both routers
|
|
183
|
+
*/
|
|
184
|
+
declare function mergeJobRouters(base: JobRouter<any>, extra: JobRouter<any>): JobRouter<any>;
|
|
171
185
|
/**
|
|
172
186
|
* Collect all JobDefs from a JobRouter (including nested)
|
|
173
187
|
*/
|
|
174
188
|
declare function collectJobs(router: JobRouter<any>, prefix?: string): JobDef<any>[];
|
|
175
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Queue Policy Resolution
|
|
192
|
+
*
|
|
193
|
+
* Lives apart from register-jobs.ts and job-builder.ts so both can read the
|
|
194
|
+
* policy of a job definition without importing each other.
|
|
195
|
+
*/
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* The part of a job definition that decides its queue policy
|
|
199
|
+
*
|
|
200
|
+
* `JobDef` satisfies this; the builder can also pass its own fields before the
|
|
201
|
+
* definition object exists.
|
|
202
|
+
*/
|
|
203
|
+
interface QueuePolicySource {
|
|
204
|
+
readonly runOnce?: boolean;
|
|
205
|
+
readonly options?: JobOptions;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Resolve the pg-boss queue policy for a job definition
|
|
209
|
+
*
|
|
210
|
+
* An explicit `.options({ policy })` always wins. Otherwise a job that asks
|
|
211
|
+
* for deduplication — `.runOnce()` or an `options.singletonKey` — gets
|
|
212
|
+
* `exclusive`, the only policy under which a second enqueue is dropped while
|
|
213
|
+
* the first is still pending or running.
|
|
214
|
+
*
|
|
215
|
+
* Everything else stays `standard`: `exclusive` without a `singletonKey`
|
|
216
|
+
* deduplicates on `COALESCE(singleton_key, '')` and would collapse a plain
|
|
217
|
+
* queue to a single pending job.
|
|
218
|
+
*/
|
|
219
|
+
declare function resolveQueuePolicy(job: QueuePolicySource): JobQueuePolicy;
|
|
220
|
+
|
|
176
221
|
/**
|
|
177
222
|
* Job Registration
|
|
178
223
|
*
|
|
@@ -215,4 +260,4 @@ declare function collectJobs(router: JobRouter<any>, prefix?: string): JobDef<an
|
|
|
215
260
|
*/
|
|
216
261
|
declare function registerJobs(router: JobRouter<any>): Promise<void>;
|
|
217
262
|
|
|
218
|
-
export { CompensateHandler, JobDef, JobHandler, JobOptions, JobRouter, JobRouterEntry, collectJobs, defineJobRouter, isJobDef, isJobRouter, job, registerJobs };
|
|
263
|
+
export { CompensateHandler, JobDef, JobHandler, JobOptions, JobQueuePolicy, JobRouter, JobRouterEntry, collectJobs, defineJobRouter, isJobDef, isJobRouter, job, mergeJobRouters, registerJobs, resolveQueuePolicy };
|
package/dist/job/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import PgBoss from 'pg-boss';
|
|
2
1
|
import { logger } from '@spfn/core/logger';
|
|
2
|
+
import PgBoss from 'pg-boss';
|
|
3
3
|
import { env } from '@spfn/core/config';
|
|
4
4
|
|
|
5
|
-
// src/job/
|
|
5
|
+
// src/job/job-builder.ts
|
|
6
6
|
var jobLogger = logger.child("@spfn/core:job");
|
|
7
7
|
function requiresSSLWithoutVerification(connectionString) {
|
|
8
8
|
try {
|
|
@@ -94,7 +94,29 @@ function shouldSweepOrphanSchedules() {
|
|
|
94
94
|
return getBossConfig()?.sweepOrphanSchedules ?? false;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
// src/job/queue-policy.ts
|
|
98
|
+
function resolveQueuePolicy(job2) {
|
|
99
|
+
if (job2.options?.policy) {
|
|
100
|
+
return job2.options.policy;
|
|
101
|
+
}
|
|
102
|
+
if (job2.runOnce || job2.options?.singletonKey) {
|
|
103
|
+
return "exclusive";
|
|
104
|
+
}
|
|
105
|
+
return "standard";
|
|
106
|
+
}
|
|
107
|
+
|
|
97
108
|
// src/job/job-builder.ts
|
|
109
|
+
var jobLogger2 = logger.child("@spfn/core:job");
|
|
110
|
+
var warnedIgnoredSingletonKeys = /* @__PURE__ */ new Set();
|
|
111
|
+
function warnIfSingletonKeyIgnored(name, source) {
|
|
112
|
+
if (resolveQueuePolicy(source) !== "standard" || warnedIgnoredSingletonKeys.has(name)) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
warnedIgnoredSingletonKeys.add(name);
|
|
116
|
+
jobLogger2.warn(
|
|
117
|
+
`[Job:${name}] singletonKey ignored: queue policy is "standard"; set .options({ policy: 'exclusive' }) (or another non-standard policy) to deduplicate`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
98
120
|
function buildPgBossOptions(defaults, sendOptions) {
|
|
99
121
|
const options = {};
|
|
100
122
|
if (sendOptions?.startAfter) {
|
|
@@ -255,6 +277,9 @@ var JobBuilder = class _JobBuilder {
|
|
|
255
277
|
);
|
|
256
278
|
}
|
|
257
279
|
const [input, sendOptions] = inputSchema ? [inputOrOptions, maybeOptions] : [void 0, inputOrOptions];
|
|
280
|
+
if (sendOptions?.singletonKey) {
|
|
281
|
+
warnIfSingletonKeyIgnored(name, { runOnce, options });
|
|
282
|
+
}
|
|
258
283
|
return await boss.send(
|
|
259
284
|
name,
|
|
260
285
|
input ?? {},
|
|
@@ -276,6 +301,9 @@ var JobBuilder = class _JobBuilder {
|
|
|
276
301
|
);
|
|
277
302
|
}
|
|
278
303
|
const [inputs, sendOptions] = inputSchema ? [inputsOrOptions, maybeOptions] : [void 0, inputsOrOptions];
|
|
304
|
+
if (sendOptions?.singletonKey) {
|
|
305
|
+
warnIfSingletonKeyIgnored(name, { runOnce, options });
|
|
306
|
+
}
|
|
279
307
|
const pgBossOptions = buildPgBossOptions(options, sendOptions);
|
|
280
308
|
const jobs = (inputs ?? [{}]).map((data) => ({
|
|
281
309
|
name,
|
|
@@ -320,19 +348,32 @@ function defineJobRouter(jobs) {
|
|
|
320
348
|
_jobs: jobs
|
|
321
349
|
};
|
|
322
350
|
}
|
|
323
|
-
function
|
|
324
|
-
const jobs
|
|
351
|
+
function mergeJobRouters(base, extra) {
|
|
352
|
+
for (const key of Object.keys(extra.jobs)) {
|
|
353
|
+
if (key in base.jobs) {
|
|
354
|
+
throw new Error(
|
|
355
|
+
`jobs(): key "${key}" is already registered by an earlier .jobs() call; rename it or nest it under a domain router`
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return defineJobRouter({ ...base.jobs, ...extra.jobs });
|
|
360
|
+
}
|
|
361
|
+
function collectJobEntries(router, prefix = "") {
|
|
362
|
+
const entries = [];
|
|
325
363
|
for (const [key, value] of Object.entries(router.jobs)) {
|
|
326
|
-
const
|
|
364
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
327
365
|
if (isJobRouter(value)) {
|
|
328
|
-
|
|
366
|
+
entries.push(...collectJobEntries(value, path));
|
|
329
367
|
} else if (isJobDef(value)) {
|
|
330
|
-
|
|
368
|
+
entries.push({ key: path, job: value });
|
|
331
369
|
}
|
|
332
370
|
}
|
|
333
|
-
return
|
|
371
|
+
return entries;
|
|
334
372
|
}
|
|
335
|
-
|
|
373
|
+
function collectJobs(router, prefix = "") {
|
|
374
|
+
return collectJobEntries(router, prefix).map((entry) => entry.job);
|
|
375
|
+
}
|
|
376
|
+
var jobLogger3 = logger.child("@spfn/core:job");
|
|
336
377
|
function getEventQueueName(eventName) {
|
|
337
378
|
return `event:${eventName}`;
|
|
338
379
|
}
|
|
@@ -353,11 +394,12 @@ async function registerJobs(router) {
|
|
|
353
394
|
"pg-boss not initialized. Call initBoss() before registerJobs()"
|
|
354
395
|
);
|
|
355
396
|
}
|
|
397
|
+
assertUniqueJobNames(router);
|
|
356
398
|
const jobs = collectJobs(router);
|
|
357
399
|
const clearOnStart = shouldClearOnStart();
|
|
358
|
-
|
|
400
|
+
jobLogger3.info(`Registering ${jobs.length} job(s)...`);
|
|
359
401
|
if (clearOnStart) {
|
|
360
|
-
|
|
402
|
+
jobLogger3.info("Clearing existing jobs before registration...");
|
|
361
403
|
for (const job2 of jobs) {
|
|
362
404
|
await boss.deleteAllJobs(job2.name);
|
|
363
405
|
if (job2.subscribedEvent) {
|
|
@@ -365,7 +407,7 @@ async function registerJobs(router) {
|
|
|
365
407
|
await boss.deleteAllJobs(eventQueue);
|
|
366
408
|
}
|
|
367
409
|
}
|
|
368
|
-
|
|
410
|
+
jobLogger3.info("Existing jobs cleared");
|
|
369
411
|
}
|
|
370
412
|
await Promise.all(jobs.map((job2) => registerJob(job2)));
|
|
371
413
|
for (const job2 of jobs) {
|
|
@@ -376,42 +418,77 @@ async function registerJobs(router) {
|
|
|
376
418
|
if (shouldSweepOrphanSchedules()) {
|
|
377
419
|
await sweepOrphanSchedules(boss);
|
|
378
420
|
}
|
|
379
|
-
|
|
421
|
+
jobLogger3.info("All jobs registered successfully");
|
|
422
|
+
}
|
|
423
|
+
function assertUniqueJobNames(router) {
|
|
424
|
+
const keysByName = /* @__PURE__ */ new Map();
|
|
425
|
+
for (const { key, job: job2 } of collectJobEntries(router)) {
|
|
426
|
+
const existingKey = keysByName.get(job2.name);
|
|
427
|
+
if (existingKey) {
|
|
428
|
+
throw new Error(
|
|
429
|
+
`registerJobs(): job name "${job2.name}" is defined twice (keys "${existingKey}" and "${key}"); job names are pg-boss queue names and must be unique`
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
keysByName.set(job2.name, key);
|
|
433
|
+
}
|
|
380
434
|
}
|
|
381
435
|
var registeredCronNames = /* @__PURE__ */ new Set();
|
|
382
436
|
async function sweepOrphanSchedules(boss) {
|
|
383
437
|
if (registeredCronNames.size === 0) {
|
|
384
|
-
|
|
438
|
+
jobLogger3.debug("No cron jobs declared; skipping orphan schedule sweep");
|
|
385
439
|
return;
|
|
386
440
|
}
|
|
387
441
|
try {
|
|
388
442
|
const schedules = await boss.getSchedules();
|
|
389
443
|
const orphans = schedules.filter((schedule) => !registeredCronNames.has(schedule.name));
|
|
390
444
|
if (orphans.length === 0) {
|
|
391
|
-
|
|
445
|
+
jobLogger3.debug("No orphan schedules found");
|
|
392
446
|
return;
|
|
393
447
|
}
|
|
394
448
|
await Promise.all(orphans.map(async (orphan) => {
|
|
395
449
|
try {
|
|
396
450
|
await boss.unschedule(orphan.name, orphan.key);
|
|
397
|
-
|
|
451
|
+
jobLogger3.info(`Removed orphan schedule: ${orphan.name}`);
|
|
398
452
|
} catch (error) {
|
|
399
|
-
|
|
453
|
+
jobLogger3.error(`Failed to remove orphan schedule: ${orphan.name}`, {
|
|
400
454
|
error: error instanceof Error ? error.message : String(error)
|
|
401
455
|
});
|
|
402
456
|
}
|
|
403
457
|
}));
|
|
404
458
|
} catch (error) {
|
|
405
|
-
|
|
459
|
+
jobLogger3.error("Orphan schedule sweep failed", {
|
|
406
460
|
error: error instanceof Error ? error.message : String(error)
|
|
407
461
|
});
|
|
408
462
|
}
|
|
409
463
|
}
|
|
410
|
-
async function ensureQueue(boss, queueName) {
|
|
411
|
-
await boss.createQueue(queueName);
|
|
464
|
+
async function ensureQueue(boss, queueName, policy) {
|
|
465
|
+
await boss.createQueue(queueName, { policy });
|
|
466
|
+
const existing = await boss.getQueue(queueName);
|
|
467
|
+
if (!existing || existing.policy === policy) {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (shouldClearOnStart()) {
|
|
471
|
+
await boss.deleteQueue(queueName);
|
|
472
|
+
await boss.createQueue(queueName, { policy });
|
|
473
|
+
jobLogger3.info(`[Queue:${queueName}] policy ${existing.policy} \u2192 ${policy} (recreated: clearOnStart)`);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
jobLogger3.error(
|
|
477
|
+
`[Queue:${queueName}] policy mismatch: the queue exists as "${existing.policy}" but its jobs require "${policy}". Delete the queue in a quiet window (getBoss().deleteQueue('${queueName}') drops its job rows) or set clearOnStart in development; until then singletonKey/runOnce on this queue do not deduplicate`
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
function createQueueEnsurer(boss, policy) {
|
|
481
|
+
const ensured = /* @__PURE__ */ new Set();
|
|
482
|
+
return async (queueName) => {
|
|
483
|
+
if (ensured.has(queueName)) {
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
ensured.add(queueName);
|
|
487
|
+
await ensureQueue(boss, queueName, policy);
|
|
488
|
+
};
|
|
412
489
|
}
|
|
413
490
|
async function executeJobHandler(job2, pgBossJob) {
|
|
414
|
-
|
|
491
|
+
jobLogger3.debug(`[Job:${job2.name}] Executing...`, { jobId: pgBossJob.id });
|
|
415
492
|
const startTime = Date.now();
|
|
416
493
|
try {
|
|
417
494
|
if (job2.inputSchema) {
|
|
@@ -420,13 +497,13 @@ async function executeJobHandler(job2, pgBossJob) {
|
|
|
420
497
|
await job2.handler();
|
|
421
498
|
}
|
|
422
499
|
const duration = Date.now() - startTime;
|
|
423
|
-
|
|
500
|
+
jobLogger3.info(`[Job:${job2.name}] Completed in ${duration}ms`, {
|
|
424
501
|
jobId: pgBossJob.id,
|
|
425
502
|
duration
|
|
426
503
|
});
|
|
427
504
|
} catch (error) {
|
|
428
505
|
const duration = Date.now() - startTime;
|
|
429
|
-
|
|
506
|
+
jobLogger3.error(`[Job:${job2.name}] Failed after ${duration}ms`, {
|
|
430
507
|
jobId: pgBossJob.id,
|
|
431
508
|
duration,
|
|
432
509
|
error: error instanceof Error ? error.message : String(error)
|
|
@@ -434,8 +511,8 @@ async function executeJobHandler(job2, pgBossJob) {
|
|
|
434
511
|
throw error;
|
|
435
512
|
}
|
|
436
513
|
}
|
|
437
|
-
async function registerWorker(boss, job2, queueName) {
|
|
438
|
-
await
|
|
514
|
+
async function registerWorker(boss, job2, queueName, ensureQueueOnce) {
|
|
515
|
+
await ensureQueueOnce(queueName);
|
|
439
516
|
const batchSize = job2.options?.batchSize ?? 1;
|
|
440
517
|
const pollingIntervalSeconds = job2.options?.pollingIntervalSeconds ?? env.JOB_POLLING_INTERVAL_SECONDS;
|
|
441
518
|
await boss.work(
|
|
@@ -469,28 +546,28 @@ function connectEventToQueue(boss, job2, queueName) {
|
|
|
469
546
|
eventDef._registerJobQueue(queueName, async (queue, payload) => {
|
|
470
547
|
await boss.send(queue, payload, getDefaultJobOptions(job2.options));
|
|
471
548
|
});
|
|
472
|
-
|
|
549
|
+
jobLogger3.debug(`[Job:${job2.name}] Connected to event: ${job2.subscribedEvent}`);
|
|
473
550
|
}
|
|
474
|
-
async function registerCronSchedule(boss, job2) {
|
|
551
|
+
async function registerCronSchedule(boss, job2, ensureQueueOnce) {
|
|
475
552
|
if (!job2.cronExpression) {
|
|
476
553
|
return;
|
|
477
554
|
}
|
|
478
|
-
|
|
479
|
-
await
|
|
555
|
+
jobLogger3.debug(`[Job:${job2.name}] Scheduling cron: ${job2.cronExpression}`);
|
|
556
|
+
await ensureQueueOnce(job2.name);
|
|
480
557
|
await boss.schedule(
|
|
481
558
|
job2.name,
|
|
482
559
|
job2.cronExpression,
|
|
483
560
|
{},
|
|
484
561
|
getDefaultJobOptions(job2.options)
|
|
485
562
|
);
|
|
486
|
-
|
|
563
|
+
jobLogger3.info(`[Job:${job2.name}] Cron scheduled: ${job2.cronExpression}`);
|
|
487
564
|
}
|
|
488
|
-
async function queueRunOnceJob(boss, job2) {
|
|
565
|
+
async function queueRunOnceJob(boss, job2, ensureQueueOnce) {
|
|
489
566
|
if (!job2.runOnce) {
|
|
490
567
|
return;
|
|
491
568
|
}
|
|
492
|
-
|
|
493
|
-
await
|
|
569
|
+
jobLogger3.debug(`[Job:${job2.name}] Queuing runOnce job`);
|
|
570
|
+
await ensureQueueOnce(job2.name);
|
|
494
571
|
await boss.send(
|
|
495
572
|
job2.name,
|
|
496
573
|
{},
|
|
@@ -499,7 +576,7 @@ async function queueRunOnceJob(boss, job2) {
|
|
|
499
576
|
singletonKey: `runOnce:${job2.name}`
|
|
500
577
|
}
|
|
501
578
|
);
|
|
502
|
-
|
|
579
|
+
jobLogger3.info(`[Job:${job2.name}] runOnce job queued`);
|
|
503
580
|
}
|
|
504
581
|
async function registerJob(job2) {
|
|
505
582
|
const boss = getBoss();
|
|
@@ -507,17 +584,18 @@ async function registerJob(job2) {
|
|
|
507
584
|
throw new Error("pg-boss not initialized");
|
|
508
585
|
}
|
|
509
586
|
const queueName = job2.subscribedEvent ? getEventQueueName(job2.subscribedEvent) : job2.name;
|
|
510
|
-
|
|
587
|
+
jobLogger3.debug(`Registering job: ${job2.name}`, {
|
|
511
588
|
queueName,
|
|
512
589
|
subscribedEvent: job2.subscribedEvent
|
|
513
590
|
});
|
|
514
|
-
|
|
591
|
+
const ensureQueueOnce = createQueueEnsurer(boss, resolveQueuePolicy(job2));
|
|
592
|
+
await registerWorker(boss, job2, queueName, ensureQueueOnce);
|
|
515
593
|
connectEventToQueue(boss, job2, queueName);
|
|
516
|
-
await registerCronSchedule(boss, job2);
|
|
517
|
-
await queueRunOnceJob(boss, job2);
|
|
518
|
-
|
|
594
|
+
await registerCronSchedule(boss, job2, ensureQueueOnce);
|
|
595
|
+
await queueRunOnceJob(boss, job2, ensureQueueOnce);
|
|
596
|
+
jobLogger3.debug(`Job registered: ${job2.name}`);
|
|
519
597
|
}
|
|
520
598
|
|
|
521
|
-
export { collectJobs, defineJobRouter, getBoss, initBoss, isBossRunning, isJobDef, isJobRouter, job, registerJobs, shouldClearOnStart, stopBoss };
|
|
599
|
+
export { collectJobs, defineJobRouter, getBoss, initBoss, isBossRunning, isJobDef, isJobRouter, job, mergeJobRouters, registerJobs, resolveQueuePolicy, shouldClearOnStart, stopBoss };
|
|
522
600
|
//# sourceMappingURL=index.js.map
|
|
523
601
|
//# sourceMappingURL=index.js.map
|