@spfn/core 0.3.0-beta.5 → 0.3.0-beta.7
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/README.md +132 -4
- package/dist/{boss-D16fO2oG.d.ts → boss-Caj5dGMm.d.ts} +47 -3
- package/dist/db/index.d.ts +173 -27
- package/dist/db/index.js +192 -57
- package/dist/db/index.js.map +1 -1
- 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/nextjs/index.d.ts +18 -1
- package/dist/nextjs/index.js +40 -1
- package/dist/nextjs/index.js.map +1 -1
- package/dist/nextjs/server.d.ts +34 -1
- package/dist/nextjs/server.js +14 -0
- package/dist/nextjs/server.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/docs/file-upload.md +195 -333
- package/package.json +6 -5
- package/src/cache/README.md +330 -0
- package/src/codegen/README.md +516 -0
- package/src/config/README.md +326 -0
- package/src/contract/README.md +326 -0
- package/src/db/README.md +589 -0
- package/src/db/manager/README.md +500 -0
- package/src/db/schema/README.md +344 -0
- package/src/db/transaction/README.md +822 -0
- package/src/env/README.md +651 -0
- package/src/errors/README.md +429 -0
- package/src/event/README.md +736 -0
- package/src/job/README.md +609 -0
- package/src/logger/README.md +321 -0
- package/src/middleware/README.md +634 -0
- package/src/nextjs/README.md +608 -0
- package/src/route/README.md +738 -0
- package/src/security/README.md +100 -0
- package/src/server/README.md +704 -0
package/dist/server/index.js
CHANGED
|
@@ -2275,6 +2275,20 @@ function shouldSweepOrphanSchedules() {
|
|
|
2275
2275
|
return getBossConfig()?.sweepOrphanSchedules ?? false;
|
|
2276
2276
|
}
|
|
2277
2277
|
|
|
2278
|
+
// src/job/queue-policy.ts
|
|
2279
|
+
function resolveQueuePolicy(job2) {
|
|
2280
|
+
if (job2.options?.policy) {
|
|
2281
|
+
return job2.options.policy;
|
|
2282
|
+
}
|
|
2283
|
+
if (job2.runOnce || job2.options?.singletonKey) {
|
|
2284
|
+
return "exclusive";
|
|
2285
|
+
}
|
|
2286
|
+
return "standard";
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
// src/job/job-builder.ts
|
|
2290
|
+
logger.child("@spfn/core:job");
|
|
2291
|
+
|
|
2278
2292
|
// src/job/job-router.ts
|
|
2279
2293
|
function isJobDef(value) {
|
|
2280
2294
|
return value !== null && typeof value === "object" && "name" in value && "handler" in value && "send" in value && "run" in value;
|
|
@@ -2282,19 +2296,38 @@ function isJobDef(value) {
|
|
|
2282
2296
|
function isJobRouter(value) {
|
|
2283
2297
|
return value !== null && typeof value === "object" && "jobs" in value && "_jobs" in value;
|
|
2284
2298
|
}
|
|
2285
|
-
function
|
|
2286
|
-
|
|
2299
|
+
function defineJobRouter(jobs) {
|
|
2300
|
+
return {
|
|
2301
|
+
jobs,
|
|
2302
|
+
_jobs: jobs
|
|
2303
|
+
};
|
|
2304
|
+
}
|
|
2305
|
+
function mergeJobRouters(base, extra) {
|
|
2306
|
+
for (const key of Object.keys(extra.jobs)) {
|
|
2307
|
+
if (key in base.jobs) {
|
|
2308
|
+
throw new Error(
|
|
2309
|
+
`jobs(): key "${key}" is already registered by an earlier .jobs() call; rename it or nest it under a domain router`
|
|
2310
|
+
);
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
return defineJobRouter({ ...base.jobs, ...extra.jobs });
|
|
2314
|
+
}
|
|
2315
|
+
function collectJobEntries(router, prefix = "") {
|
|
2316
|
+
const entries = [];
|
|
2287
2317
|
for (const [key, value] of Object.entries(router.jobs)) {
|
|
2288
|
-
const
|
|
2318
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
2289
2319
|
if (isJobRouter(value)) {
|
|
2290
|
-
|
|
2320
|
+
entries.push(...collectJobEntries(value, path));
|
|
2291
2321
|
} else if (isJobDef(value)) {
|
|
2292
|
-
|
|
2322
|
+
entries.push({ key: path, job: value });
|
|
2293
2323
|
}
|
|
2294
2324
|
}
|
|
2295
|
-
return
|
|
2325
|
+
return entries;
|
|
2296
2326
|
}
|
|
2297
|
-
|
|
2327
|
+
function collectJobs(router, prefix = "") {
|
|
2328
|
+
return collectJobEntries(router, prefix).map((entry) => entry.job);
|
|
2329
|
+
}
|
|
2330
|
+
var jobLogger3 = logger.child("@spfn/core:job");
|
|
2298
2331
|
function getEventQueueName(eventName) {
|
|
2299
2332
|
return `event:${eventName}`;
|
|
2300
2333
|
}
|
|
@@ -2315,11 +2348,12 @@ async function registerJobs(router) {
|
|
|
2315
2348
|
"pg-boss not initialized. Call initBoss() before registerJobs()"
|
|
2316
2349
|
);
|
|
2317
2350
|
}
|
|
2351
|
+
assertUniqueJobNames(router);
|
|
2318
2352
|
const jobs = collectJobs(router);
|
|
2319
2353
|
const clearOnStart = shouldClearOnStart();
|
|
2320
|
-
|
|
2354
|
+
jobLogger3.info(`Registering ${jobs.length} job(s)...`);
|
|
2321
2355
|
if (clearOnStart) {
|
|
2322
|
-
|
|
2356
|
+
jobLogger3.info("Clearing existing jobs before registration...");
|
|
2323
2357
|
for (const job2 of jobs) {
|
|
2324
2358
|
await boss.deleteAllJobs(job2.name);
|
|
2325
2359
|
if (job2.subscribedEvent) {
|
|
@@ -2327,7 +2361,7 @@ async function registerJobs(router) {
|
|
|
2327
2361
|
await boss.deleteAllJobs(eventQueue);
|
|
2328
2362
|
}
|
|
2329
2363
|
}
|
|
2330
|
-
|
|
2364
|
+
jobLogger3.info("Existing jobs cleared");
|
|
2331
2365
|
}
|
|
2332
2366
|
await Promise.all(jobs.map((job2) => registerJob(job2)));
|
|
2333
2367
|
for (const job2 of jobs) {
|
|
@@ -2338,42 +2372,77 @@ async function registerJobs(router) {
|
|
|
2338
2372
|
if (shouldSweepOrphanSchedules()) {
|
|
2339
2373
|
await sweepOrphanSchedules(boss);
|
|
2340
2374
|
}
|
|
2341
|
-
|
|
2375
|
+
jobLogger3.info("All jobs registered successfully");
|
|
2376
|
+
}
|
|
2377
|
+
function assertUniqueJobNames(router) {
|
|
2378
|
+
const keysByName = /* @__PURE__ */ new Map();
|
|
2379
|
+
for (const { key, job: job2 } of collectJobEntries(router)) {
|
|
2380
|
+
const existingKey = keysByName.get(job2.name);
|
|
2381
|
+
if (existingKey) {
|
|
2382
|
+
throw new Error(
|
|
2383
|
+
`registerJobs(): job name "${job2.name}" is defined twice (keys "${existingKey}" and "${key}"); job names are pg-boss queue names and must be unique`
|
|
2384
|
+
);
|
|
2385
|
+
}
|
|
2386
|
+
keysByName.set(job2.name, key);
|
|
2387
|
+
}
|
|
2342
2388
|
}
|
|
2343
2389
|
var registeredCronNames = /* @__PURE__ */ new Set();
|
|
2344
2390
|
async function sweepOrphanSchedules(boss) {
|
|
2345
2391
|
if (registeredCronNames.size === 0) {
|
|
2346
|
-
|
|
2392
|
+
jobLogger3.debug("No cron jobs declared; skipping orphan schedule sweep");
|
|
2347
2393
|
return;
|
|
2348
2394
|
}
|
|
2349
2395
|
try {
|
|
2350
2396
|
const schedules = await boss.getSchedules();
|
|
2351
2397
|
const orphans = schedules.filter((schedule) => !registeredCronNames.has(schedule.name));
|
|
2352
2398
|
if (orphans.length === 0) {
|
|
2353
|
-
|
|
2399
|
+
jobLogger3.debug("No orphan schedules found");
|
|
2354
2400
|
return;
|
|
2355
2401
|
}
|
|
2356
2402
|
await Promise.all(orphans.map(async (orphan) => {
|
|
2357
2403
|
try {
|
|
2358
2404
|
await boss.unschedule(orphan.name, orphan.key);
|
|
2359
|
-
|
|
2405
|
+
jobLogger3.info(`Removed orphan schedule: ${orphan.name}`);
|
|
2360
2406
|
} catch (error) {
|
|
2361
|
-
|
|
2407
|
+
jobLogger3.error(`Failed to remove orphan schedule: ${orphan.name}`, {
|
|
2362
2408
|
error: error instanceof Error ? error.message : String(error)
|
|
2363
2409
|
});
|
|
2364
2410
|
}
|
|
2365
2411
|
}));
|
|
2366
2412
|
} catch (error) {
|
|
2367
|
-
|
|
2413
|
+
jobLogger3.error("Orphan schedule sweep failed", {
|
|
2368
2414
|
error: error instanceof Error ? error.message : String(error)
|
|
2369
2415
|
});
|
|
2370
2416
|
}
|
|
2371
2417
|
}
|
|
2372
|
-
async function ensureQueue(boss, queueName) {
|
|
2373
|
-
await boss.createQueue(queueName);
|
|
2418
|
+
async function ensureQueue(boss, queueName, policy) {
|
|
2419
|
+
await boss.createQueue(queueName, { policy });
|
|
2420
|
+
const existing = await boss.getQueue(queueName);
|
|
2421
|
+
if (!existing || existing.policy === policy) {
|
|
2422
|
+
return;
|
|
2423
|
+
}
|
|
2424
|
+
if (shouldClearOnStart()) {
|
|
2425
|
+
await boss.deleteQueue(queueName);
|
|
2426
|
+
await boss.createQueue(queueName, { policy });
|
|
2427
|
+
jobLogger3.info(`[Queue:${queueName}] policy ${existing.policy} \u2192 ${policy} (recreated: clearOnStart)`);
|
|
2428
|
+
return;
|
|
2429
|
+
}
|
|
2430
|
+
jobLogger3.error(
|
|
2431
|
+
`[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`
|
|
2432
|
+
);
|
|
2433
|
+
}
|
|
2434
|
+
function createQueueEnsurer(boss, policy) {
|
|
2435
|
+
const ensured = /* @__PURE__ */ new Set();
|
|
2436
|
+
return async (queueName) => {
|
|
2437
|
+
if (ensured.has(queueName)) {
|
|
2438
|
+
return;
|
|
2439
|
+
}
|
|
2440
|
+
ensured.add(queueName);
|
|
2441
|
+
await ensureQueue(boss, queueName, policy);
|
|
2442
|
+
};
|
|
2374
2443
|
}
|
|
2375
2444
|
async function executeJobHandler(job2, pgBossJob) {
|
|
2376
|
-
|
|
2445
|
+
jobLogger3.debug(`[Job:${job2.name}] Executing...`, { jobId: pgBossJob.id });
|
|
2377
2446
|
const startTime = Date.now();
|
|
2378
2447
|
try {
|
|
2379
2448
|
if (job2.inputSchema) {
|
|
@@ -2382,13 +2451,13 @@ async function executeJobHandler(job2, pgBossJob) {
|
|
|
2382
2451
|
await job2.handler();
|
|
2383
2452
|
}
|
|
2384
2453
|
const duration = Date.now() - startTime;
|
|
2385
|
-
|
|
2454
|
+
jobLogger3.info(`[Job:${job2.name}] Completed in ${duration}ms`, {
|
|
2386
2455
|
jobId: pgBossJob.id,
|
|
2387
2456
|
duration
|
|
2388
2457
|
});
|
|
2389
2458
|
} catch (error) {
|
|
2390
2459
|
const duration = Date.now() - startTime;
|
|
2391
|
-
|
|
2460
|
+
jobLogger3.error(`[Job:${job2.name}] Failed after ${duration}ms`, {
|
|
2392
2461
|
jobId: pgBossJob.id,
|
|
2393
2462
|
duration,
|
|
2394
2463
|
error: error instanceof Error ? error.message : String(error)
|
|
@@ -2396,8 +2465,8 @@ async function executeJobHandler(job2, pgBossJob) {
|
|
|
2396
2465
|
throw error;
|
|
2397
2466
|
}
|
|
2398
2467
|
}
|
|
2399
|
-
async function registerWorker(boss, job2, queueName) {
|
|
2400
|
-
await
|
|
2468
|
+
async function registerWorker(boss, job2, queueName, ensureQueueOnce) {
|
|
2469
|
+
await ensureQueueOnce(queueName);
|
|
2401
2470
|
const batchSize = job2.options?.batchSize ?? 1;
|
|
2402
2471
|
const pollingIntervalSeconds = job2.options?.pollingIntervalSeconds ?? env.JOB_POLLING_INTERVAL_SECONDS;
|
|
2403
2472
|
await boss.work(
|
|
@@ -2431,28 +2500,28 @@ function connectEventToQueue(boss, job2, queueName) {
|
|
|
2431
2500
|
eventDef._registerJobQueue(queueName, async (queue, payload) => {
|
|
2432
2501
|
await boss.send(queue, payload, getDefaultJobOptions(job2.options));
|
|
2433
2502
|
});
|
|
2434
|
-
|
|
2503
|
+
jobLogger3.debug(`[Job:${job2.name}] Connected to event: ${job2.subscribedEvent}`);
|
|
2435
2504
|
}
|
|
2436
|
-
async function registerCronSchedule(boss, job2) {
|
|
2505
|
+
async function registerCronSchedule(boss, job2, ensureQueueOnce) {
|
|
2437
2506
|
if (!job2.cronExpression) {
|
|
2438
2507
|
return;
|
|
2439
2508
|
}
|
|
2440
|
-
|
|
2441
|
-
await
|
|
2509
|
+
jobLogger3.debug(`[Job:${job2.name}] Scheduling cron: ${job2.cronExpression}`);
|
|
2510
|
+
await ensureQueueOnce(job2.name);
|
|
2442
2511
|
await boss.schedule(
|
|
2443
2512
|
job2.name,
|
|
2444
2513
|
job2.cronExpression,
|
|
2445
2514
|
{},
|
|
2446
2515
|
getDefaultJobOptions(job2.options)
|
|
2447
2516
|
);
|
|
2448
|
-
|
|
2517
|
+
jobLogger3.info(`[Job:${job2.name}] Cron scheduled: ${job2.cronExpression}`);
|
|
2449
2518
|
}
|
|
2450
|
-
async function queueRunOnceJob(boss, job2) {
|
|
2519
|
+
async function queueRunOnceJob(boss, job2, ensureQueueOnce) {
|
|
2451
2520
|
if (!job2.runOnce) {
|
|
2452
2521
|
return;
|
|
2453
2522
|
}
|
|
2454
|
-
|
|
2455
|
-
await
|
|
2523
|
+
jobLogger3.debug(`[Job:${job2.name}] Queuing runOnce job`);
|
|
2524
|
+
await ensureQueueOnce(job2.name);
|
|
2456
2525
|
await boss.send(
|
|
2457
2526
|
job2.name,
|
|
2458
2527
|
{},
|
|
@@ -2461,7 +2530,7 @@ async function queueRunOnceJob(boss, job2) {
|
|
|
2461
2530
|
singletonKey: `runOnce:${job2.name}`
|
|
2462
2531
|
}
|
|
2463
2532
|
);
|
|
2464
|
-
|
|
2533
|
+
jobLogger3.info(`[Job:${job2.name}] runOnce job queued`);
|
|
2465
2534
|
}
|
|
2466
2535
|
async function registerJob(job2) {
|
|
2467
2536
|
const boss = getBoss();
|
|
@@ -2469,15 +2538,16 @@ async function registerJob(job2) {
|
|
|
2469
2538
|
throw new Error("pg-boss not initialized");
|
|
2470
2539
|
}
|
|
2471
2540
|
const queueName = job2.subscribedEvent ? getEventQueueName(job2.subscribedEvent) : job2.name;
|
|
2472
|
-
|
|
2541
|
+
jobLogger3.debug(`Registering job: ${job2.name}`, {
|
|
2473
2542
|
queueName,
|
|
2474
2543
|
subscribedEvent: job2.subscribedEvent
|
|
2475
2544
|
});
|
|
2476
|
-
|
|
2545
|
+
const ensureQueueOnce = createQueueEnsurer(boss, resolveQueuePolicy(job2));
|
|
2546
|
+
await registerWorker(boss, job2, queueName, ensureQueueOnce);
|
|
2477
2547
|
connectEventToQueue(boss, job2, queueName);
|
|
2478
|
-
await registerCronSchedule(boss, job2);
|
|
2479
|
-
await queueRunOnceJob(boss, job2);
|
|
2480
|
-
|
|
2548
|
+
await registerCronSchedule(boss, job2, ensureQueueOnce);
|
|
2549
|
+
await queueRunOnceJob(boss, job2, ensureQueueOnce);
|
|
2550
|
+
jobLogger3.debug(`Job registered: ${job2.name}`);
|
|
2481
2551
|
}
|
|
2482
2552
|
var wsLogger = logger.child("@spfn/core:ws");
|
|
2483
2553
|
function isOriginAllowed(origin, allowList) {
|
|
@@ -3506,6 +3576,20 @@ var ServerConfigBuilder = class {
|
|
|
3506
3576
|
/**
|
|
3507
3577
|
* Register background jobs router
|
|
3508
3578
|
*
|
|
3579
|
+
* May be called once per domain router: repeated calls **merge** into one
|
|
3580
|
+
* router, the way `.routes()` composes. Calling it several times used to
|
|
3581
|
+
* keep only the last router and register nothing else, without warning.
|
|
3582
|
+
*
|
|
3583
|
+
* The pg-boss `config` is per server, so pass it on one call only.
|
|
3584
|
+
*
|
|
3585
|
+
* Two names can collide, and both fail loudly:
|
|
3586
|
+
* - a router **key** present in two calls throws here, at config time;
|
|
3587
|
+
* - two jobs sharing a **`job.name`** (the pg-boss queue name) throw in
|
|
3588
|
+
* `registerJobs()` at boot, before any queue or worker is created.
|
|
3589
|
+
*
|
|
3590
|
+
* @throws if a router key was already registered by an earlier call
|
|
3591
|
+
* @throws if `config` is given when an earlier call already gave one
|
|
3592
|
+
*
|
|
3509
3593
|
* @example
|
|
3510
3594
|
* ```typescript
|
|
3511
3595
|
* import { job, defineJobRouter } from '@spfn/core/job';
|
|
@@ -3518,12 +3602,18 @@ var ServerConfigBuilder = class {
|
|
|
3518
3602
|
*
|
|
3519
3603
|
* export default defineServerConfig()
|
|
3520
3604
|
* .routes(appRouter)
|
|
3521
|
-
* .jobs(jobRouter)
|
|
3605
|
+
* .jobs(jobRouter, { clearOnStart: isDev }) // config on one call only
|
|
3606
|
+
* .jobs(billingJobRouter) // merged, not replaced
|
|
3522
3607
|
* .build();
|
|
3523
3608
|
* ```
|
|
3524
3609
|
*/
|
|
3525
3610
|
jobs(router, config) {
|
|
3526
|
-
this.config.
|
|
3611
|
+
if (config && this.config.jobsConfig) {
|
|
3612
|
+
throw new Error(
|
|
3613
|
+
"jobs(): pg-boss config was already given by an earlier .jobs() call; pass it once"
|
|
3614
|
+
);
|
|
3615
|
+
}
|
|
3616
|
+
this.config.jobs = this.config.jobs ? mergeJobRouters(this.config.jobs, router) : router;
|
|
3527
3617
|
if (config) {
|
|
3528
3618
|
this.config.jobsConfig = config;
|
|
3529
3619
|
}
|