@spfn/core 0.1.0-alpha.88 → 0.2.0-beta.10
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 +298 -466
- package/dist/boss-DI1r4kTS.d.ts +244 -0
- package/dist/cache/index.d.ts +13 -33
- package/dist/cache/index.js +14 -703
- package/dist/cache/index.js.map +1 -1
- package/dist/codegen/index.d.ts +214 -17
- package/dist/codegen/index.js +231 -1420
- package/dist/codegen/index.js.map +1 -1
- package/dist/config/index.d.ts +1227 -0
- package/dist/config/index.js +273 -0
- package/dist/config/index.js.map +1 -0
- package/dist/db/index.d.ts +741 -59
- package/dist/db/index.js +1063 -1226
- package/dist/db/index.js.map +1 -1
- package/dist/env/index.d.ts +658 -308
- package/dist/env/index.js +503 -928
- package/dist/env/index.js.map +1 -1
- package/dist/env/loader.d.ts +87 -0
- package/dist/env/loader.js +70 -0
- package/dist/env/loader.js.map +1 -0
- package/dist/errors/index.d.ts +417 -29
- package/dist/errors/index.js +359 -98
- package/dist/errors/index.js.map +1 -1
- package/dist/event/index.d.ts +41 -0
- package/dist/event/index.js +131 -0
- package/dist/event/index.js.map +1 -0
- package/dist/event/sse/client.d.ts +82 -0
- package/dist/event/sse/client.js +115 -0
- package/dist/event/sse/client.js.map +1 -0
- package/dist/event/sse/index.d.ts +40 -0
- package/dist/event/sse/index.js +92 -0
- package/dist/event/sse/index.js.map +1 -0
- package/dist/job/index.d.ts +218 -0
- package/dist/job/index.js +410 -0
- package/dist/job/index.js.map +1 -0
- package/dist/logger/index.d.ts +20 -79
- package/dist/logger/index.js +82 -387
- package/dist/logger/index.js.map +1 -1
- package/dist/middleware/index.d.ts +102 -20
- package/dist/middleware/index.js +51 -705
- package/dist/middleware/index.js.map +1 -1
- package/dist/nextjs/index.d.ts +120 -0
- package/dist/nextjs/index.js +448 -0
- package/dist/nextjs/index.js.map +1 -0
- package/dist/{client/nextjs/index.d.ts → nextjs/server.d.ts} +335 -262
- package/dist/nextjs/server.js +637 -0
- package/dist/nextjs/server.js.map +1 -0
- package/dist/route/index.d.ts +879 -25
- package/dist/route/index.js +697 -1271
- package/dist/route/index.js.map +1 -1
- package/dist/route/types.d.ts +9 -0
- package/dist/route/types.js +3 -0
- package/dist/route/types.js.map +1 -0
- package/dist/router-Di7ENoah.d.ts +151 -0
- package/dist/server/index.d.ts +345 -64
- package/dist/server/index.js +1174 -3233
- package/dist/server/index.js.map +1 -1
- package/dist/types-B-e_f2dQ.d.ts +121 -0
- package/dist/types-BGl4QL1w.d.ts +77 -0
- package/dist/types-BOPTApC2.d.ts +245 -0
- package/docs/cache.md +133 -0
- package/docs/codegen.md +74 -0
- package/docs/database.md +346 -0
- package/docs/entity.md +539 -0
- package/docs/env.md +477 -0
- package/docs/errors.md +319 -0
- package/docs/event.md +116 -0
- package/docs/file-upload.md +717 -0
- package/docs/job.md +131 -0
- package/docs/logger.md +108 -0
- package/docs/middleware.md +337 -0
- package/docs/nextjs.md +241 -0
- package/docs/repository.md +496 -0
- package/docs/route.md +497 -0
- package/docs/server.md +307 -0
- package/package.json +68 -48
- package/dist/auto-loader-JFaZ9gON.d.ts +0 -80
- package/dist/client/index.d.ts +0 -358
- package/dist/client/index.js +0 -357
- package/dist/client/index.js.map +0 -1
- package/dist/client/nextjs/index.js +0 -371
- package/dist/client/nextjs/index.js.map +0 -1
- package/dist/codegen/generators/index.d.ts +0 -19
- package/dist/codegen/generators/index.js +0 -1404
- package/dist/codegen/generators/index.js.map +0 -1
- package/dist/database-errors-BNNmLTJE.d.ts +0 -86
- package/dist/events/index.d.ts +0 -183
- package/dist/events/index.js +0 -77
- package/dist/events/index.js.map +0 -1
- package/dist/index-DHiAqhKv.d.ts +0 -101
- package/dist/index.d.ts +0 -8
- package/dist/index.js +0 -3674
- package/dist/index.js.map +0 -1
- package/dist/types/index.d.ts +0 -121
- package/dist/types/index.js +0 -38
- package/dist/types/index.js.map +0 -1
- package/dist/types-BXibIEyj.d.ts +0 -60
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { a as JobOptions, C as CompensateHandler, b as JobHandler, c as JobDef, d as JobRouterEntry, J as JobRouter } from '../boss-DI1r4kTS.js';
|
|
2
|
+
export { k as BossConfig, B as BossOptions, I as InferJobInput, f as InferJobOutput, e as JobSendOptions, g as getBoss, i as initBoss, h as isBossRunning, j as shouldClearOnStart, s as stopBoss } from '../boss-DI1r4kTS.js';
|
|
3
|
+
import * as _sinclair_typebox from '@sinclair/typebox';
|
|
4
|
+
import { Static } from '@sinclair/typebox';
|
|
5
|
+
import { EventDef, InferEventPayload } from '@spfn/core/event';
|
|
6
|
+
import 'pg-boss';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Job builder class with fluent API
|
|
10
|
+
*/
|
|
11
|
+
declare class JobBuilder<TInput = void, TOutput = void> {
|
|
12
|
+
private readonly _name;
|
|
13
|
+
private _inputSchema?;
|
|
14
|
+
private _outputSchema?;
|
|
15
|
+
private _cronExpression?;
|
|
16
|
+
private _runOnce?;
|
|
17
|
+
private _subscribedEvent?;
|
|
18
|
+
private _subscribedEventDef?;
|
|
19
|
+
private _options?;
|
|
20
|
+
private _handler?;
|
|
21
|
+
private _compensate?;
|
|
22
|
+
constructor(name: string);
|
|
23
|
+
/**
|
|
24
|
+
* Define input schema with TypeBox
|
|
25
|
+
*/
|
|
26
|
+
input<TSchema extends _sinclair_typebox.TSchema>(schema: TSchema): JobBuilder<Static<TSchema>, TOutput>;
|
|
27
|
+
/**
|
|
28
|
+
* Define output schema with TypeBox (for workflow integration)
|
|
29
|
+
*/
|
|
30
|
+
output<TSchema extends _sinclair_typebox.TSchema>(schema: TSchema): JobBuilder<TInput, Static<TSchema>>;
|
|
31
|
+
/**
|
|
32
|
+
* Subscribe to an event (decoupled triggering)
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```typescript
|
|
36
|
+
* const userCreated = defineEvent('user.created', Type.Object({
|
|
37
|
+
* userId: Type.String(),
|
|
38
|
+
* }));
|
|
39
|
+
*
|
|
40
|
+
* const sendWelcomeEmail = job('send-welcome-email')
|
|
41
|
+
* .on(userCreated)
|
|
42
|
+
* .handler(async (payload) => {
|
|
43
|
+
* // payload is typed as { userId: string }
|
|
44
|
+
* });
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
on<TEvent extends EventDef<any>>(event: TEvent): JobBuilder<InferEventPayload<TEvent>, TOutput>;
|
|
48
|
+
/**
|
|
49
|
+
* Set cron expression for scheduled execution
|
|
50
|
+
*/
|
|
51
|
+
cron(expression: string): this;
|
|
52
|
+
/**
|
|
53
|
+
* Mark job to run once on server start
|
|
54
|
+
*/
|
|
55
|
+
runOnce(): this;
|
|
56
|
+
/**
|
|
57
|
+
* Set job options (retry, expiration, etc.)
|
|
58
|
+
*/
|
|
59
|
+
options(options: JobOptions): this;
|
|
60
|
+
/**
|
|
61
|
+
* Set job timeout in milliseconds
|
|
62
|
+
* (Converts to expireInSeconds for pg-boss)
|
|
63
|
+
*/
|
|
64
|
+
timeout(ms: number): this;
|
|
65
|
+
/**
|
|
66
|
+
* Define compensate handler for rollback (workflow integration)
|
|
67
|
+
*/
|
|
68
|
+
compensate(fn: CompensateHandler<TInput, TOutput>): this;
|
|
69
|
+
/**
|
|
70
|
+
* Define the job handler and finalize the job definition
|
|
71
|
+
*/
|
|
72
|
+
handler(fn: JobHandler<TInput, TOutput>): JobDef<TInput, TOutput>;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Create a new job definition
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```typescript
|
|
79
|
+
* // Simple job without input
|
|
80
|
+
* export const cleanupJob = job('cleanup')
|
|
81
|
+
* .handler(async () => {
|
|
82
|
+
* await db.cleanup();
|
|
83
|
+
* });
|
|
84
|
+
*
|
|
85
|
+
* // Job with typed input
|
|
86
|
+
* export const sendEmailJob = job('send-email')
|
|
87
|
+
* .input(Type.Object({
|
|
88
|
+
* to: Type.String(),
|
|
89
|
+
* subject: Type.String(),
|
|
90
|
+
* body: Type.String(),
|
|
91
|
+
* }))
|
|
92
|
+
* .handler(async (input) => {
|
|
93
|
+
* await emailService.send(input.to, input.subject, input.body);
|
|
94
|
+
* });
|
|
95
|
+
*
|
|
96
|
+
* // Cron job
|
|
97
|
+
* export const dailyReportJob = job('daily-report')
|
|
98
|
+
* .cron('0 9 * * *')
|
|
99
|
+
* .handler(async () => {
|
|
100
|
+
* await reportService.generateDaily();
|
|
101
|
+
* });
|
|
102
|
+
*
|
|
103
|
+
* // Run once on server start
|
|
104
|
+
* export const initCacheJob = job('init-cache')
|
|
105
|
+
* .runOnce()
|
|
106
|
+
* .handler(async () => {
|
|
107
|
+
* await cache.warmup();
|
|
108
|
+
* });
|
|
109
|
+
*
|
|
110
|
+
* // With options
|
|
111
|
+
* export const importantJob = job('important-task')
|
|
112
|
+
* .input(Type.Object({ id: Type.String() }))
|
|
113
|
+
* .options({
|
|
114
|
+
* retryLimit: 5,
|
|
115
|
+
* retryDelay: 5000,
|
|
116
|
+
* priority: 10,
|
|
117
|
+
* })
|
|
118
|
+
* .handler(async (input) => {
|
|
119
|
+
* await processImportant(input.id);
|
|
120
|
+
* });
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
declare function job(name: string): JobBuilder<void>;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Job Router
|
|
127
|
+
*
|
|
128
|
+
* Groups job definitions for registration with the server
|
|
129
|
+
*/
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Type guard to check if value is a JobDef
|
|
133
|
+
*/
|
|
134
|
+
declare function isJobDef(value: unknown): value is JobDef<any>;
|
|
135
|
+
/**
|
|
136
|
+
* Type guard to check if value is a JobRouter
|
|
137
|
+
*/
|
|
138
|
+
declare function isJobRouter(value: unknown): value is JobRouter<any>;
|
|
139
|
+
/**
|
|
140
|
+
* Define a job router to group jobs together
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```typescript
|
|
144
|
+
* // Flat structure
|
|
145
|
+
* export const jobRouter = defineJobRouter({
|
|
146
|
+
* sendWelcomeEmail,
|
|
147
|
+
* dailyReport,
|
|
148
|
+
* initCache,
|
|
149
|
+
* });
|
|
150
|
+
*
|
|
151
|
+
* // Nested structure
|
|
152
|
+
* export const jobRouter = defineJobRouter({
|
|
153
|
+
* email: defineJobRouter({
|
|
154
|
+
* sendWelcome: sendWelcomeEmailJob,
|
|
155
|
+
* sendReset: sendResetPasswordJob,
|
|
156
|
+
* }),
|
|
157
|
+
* reports: defineJobRouter({
|
|
158
|
+
* daily: dailyReportJob,
|
|
159
|
+
* weekly: weeklyReportJob,
|
|
160
|
+
* }),
|
|
161
|
+
* });
|
|
162
|
+
*
|
|
163
|
+
* // Mixed
|
|
164
|
+
* export const jobRouter = defineJobRouter({
|
|
165
|
+
* initCache, // flat
|
|
166
|
+
* email: defineJobRouter({ ... }), // nested
|
|
167
|
+
* });
|
|
168
|
+
* ```
|
|
169
|
+
*/
|
|
170
|
+
declare function defineJobRouter<TJobs extends Record<string, JobRouterEntry>>(jobs: TJobs): JobRouter<TJobs>;
|
|
171
|
+
/**
|
|
172
|
+
* Collect all JobDefs from a JobRouter (including nested)
|
|
173
|
+
*/
|
|
174
|
+
declare function collectJobs(router: JobRouter<any>, prefix?: string): JobDef<any>[];
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Job Registration
|
|
178
|
+
*
|
|
179
|
+
* Registers jobs with pg-boss
|
|
180
|
+
*/
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Register all jobs from a JobRouter with pg-boss
|
|
184
|
+
*
|
|
185
|
+
* This function:
|
|
186
|
+
* 1. Collects all jobs from the router (including nested routers)
|
|
187
|
+
* 2. Optionally clears existing jobs (if clearOnStart is enabled)
|
|
188
|
+
* 3. Registers each job's worker handler with pg-boss
|
|
189
|
+
* 4. Sets up cron schedules for scheduled jobs
|
|
190
|
+
* 5. Queues runOnce jobs
|
|
191
|
+
* 6. Connects event subscriptions to job queues
|
|
192
|
+
*
|
|
193
|
+
* @param router - JobRouter containing job definitions
|
|
194
|
+
*
|
|
195
|
+
* @example
|
|
196
|
+
* ```typescript
|
|
197
|
+
* // Define jobs
|
|
198
|
+
* const sendEmail = job('send-email')
|
|
199
|
+
* .input(Type.Object({ to: Type.String() }))
|
|
200
|
+
* .handler(async (input) => { ... });
|
|
201
|
+
*
|
|
202
|
+
* const dailyReport = job('daily-report')
|
|
203
|
+
* .cron('0 9 * * *')
|
|
204
|
+
* .handler(async () => { ... });
|
|
205
|
+
*
|
|
206
|
+
* // Create router
|
|
207
|
+
* const jobRouter = defineJobRouter({ sendEmail, dailyReport });
|
|
208
|
+
*
|
|
209
|
+
* // Initialize pg-boss first
|
|
210
|
+
* await initBoss({ connectionString: process.env.DATABASE_URL! });
|
|
211
|
+
*
|
|
212
|
+
* // Register jobs
|
|
213
|
+
* await registerJobs(jobRouter);
|
|
214
|
+
* ```
|
|
215
|
+
*/
|
|
216
|
+
declare function registerJobs(router: JobRouter<any>): Promise<void>;
|
|
217
|
+
|
|
218
|
+
export { CompensateHandler, JobDef, JobHandler, JobOptions, JobRouter, JobRouterEntry, collectJobs, defineJobRouter, isJobDef, isJobRouter, job, registerJobs };
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import PgBoss from 'pg-boss';
|
|
2
|
+
import { logger } from '@spfn/core/logger';
|
|
3
|
+
|
|
4
|
+
// src/job/boss.ts
|
|
5
|
+
var jobLogger = logger.child("@spfn/core:job");
|
|
6
|
+
var bossInstance = null;
|
|
7
|
+
var bossConfig = null;
|
|
8
|
+
async function initBoss(options) {
|
|
9
|
+
if (bossInstance) {
|
|
10
|
+
jobLogger.warn("pg-boss already initialized, returning existing instance");
|
|
11
|
+
return bossInstance;
|
|
12
|
+
}
|
|
13
|
+
jobLogger.info("Initializing pg-boss...");
|
|
14
|
+
bossConfig = options;
|
|
15
|
+
const pgBossOptions = {
|
|
16
|
+
connectionString: options.connectionString,
|
|
17
|
+
schema: options.schema ?? "spfn_queue",
|
|
18
|
+
maintenanceIntervalSeconds: options.maintenanceIntervalSeconds ?? 120
|
|
19
|
+
};
|
|
20
|
+
if (options.monitorIntervalSeconds !== void 0 && options.monitorIntervalSeconds >= 1) {
|
|
21
|
+
pgBossOptions.monitorIntervalSeconds = options.monitorIntervalSeconds;
|
|
22
|
+
}
|
|
23
|
+
bossInstance = new PgBoss(pgBossOptions);
|
|
24
|
+
bossInstance.on("error", (error) => {
|
|
25
|
+
jobLogger.error("pg-boss error:", error);
|
|
26
|
+
});
|
|
27
|
+
await bossInstance.start();
|
|
28
|
+
jobLogger.info("pg-boss started successfully");
|
|
29
|
+
return bossInstance;
|
|
30
|
+
}
|
|
31
|
+
function getBoss() {
|
|
32
|
+
return bossInstance;
|
|
33
|
+
}
|
|
34
|
+
async function stopBoss() {
|
|
35
|
+
if (!bossInstance) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
jobLogger.info("Stopping pg-boss...");
|
|
39
|
+
try {
|
|
40
|
+
await bossInstance.stop({ graceful: true, timeout: 3e4 });
|
|
41
|
+
jobLogger.info("pg-boss stopped gracefully");
|
|
42
|
+
} catch (error) {
|
|
43
|
+
jobLogger.error("Error stopping pg-boss:", error);
|
|
44
|
+
throw error;
|
|
45
|
+
} finally {
|
|
46
|
+
bossInstance = null;
|
|
47
|
+
bossConfig = null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function isBossRunning() {
|
|
51
|
+
return bossInstance !== null;
|
|
52
|
+
}
|
|
53
|
+
function shouldClearOnStart() {
|
|
54
|
+
return bossConfig?.clearOnStart ?? false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// src/job/job-builder.ts
|
|
58
|
+
function buildPgBossOptions(defaults, sendOptions) {
|
|
59
|
+
const options = {};
|
|
60
|
+
if (sendOptions?.startAfter) {
|
|
61
|
+
options.startAfter = sendOptions.startAfter;
|
|
62
|
+
}
|
|
63
|
+
if (sendOptions?.singletonKey) {
|
|
64
|
+
options.singletonKey = sendOptions.singletonKey;
|
|
65
|
+
}
|
|
66
|
+
if (sendOptions?.priority !== void 0) {
|
|
67
|
+
options.priority = sendOptions.priority;
|
|
68
|
+
}
|
|
69
|
+
if (defaults?.retryLimit !== void 0) {
|
|
70
|
+
options.retryLimit = defaults.retryLimit;
|
|
71
|
+
}
|
|
72
|
+
if (defaults?.retryDelay !== void 0) {
|
|
73
|
+
options.retryDelay = defaults.retryDelay;
|
|
74
|
+
}
|
|
75
|
+
if (defaults?.expireInSeconds !== void 0) {
|
|
76
|
+
options.expireInSeconds = defaults.expireInSeconds;
|
|
77
|
+
}
|
|
78
|
+
if (defaults?.priority !== void 0 && sendOptions?.priority === void 0) {
|
|
79
|
+
options.priority = defaults.priority;
|
|
80
|
+
}
|
|
81
|
+
if (defaults?.singletonKey && !sendOptions?.singletonKey) {
|
|
82
|
+
options.singletonKey = defaults.singletonKey;
|
|
83
|
+
}
|
|
84
|
+
if (defaults?.retentionSeconds !== void 0) {
|
|
85
|
+
options.retentionSeconds = defaults.retentionSeconds;
|
|
86
|
+
}
|
|
87
|
+
return options;
|
|
88
|
+
}
|
|
89
|
+
var JobBuilder = class _JobBuilder {
|
|
90
|
+
_name;
|
|
91
|
+
_inputSchema;
|
|
92
|
+
_outputSchema;
|
|
93
|
+
_cronExpression;
|
|
94
|
+
_runOnce;
|
|
95
|
+
_subscribedEvent;
|
|
96
|
+
_subscribedEventDef;
|
|
97
|
+
_options;
|
|
98
|
+
_handler;
|
|
99
|
+
_compensate;
|
|
100
|
+
constructor(name) {
|
|
101
|
+
this._name = name;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Define input schema with TypeBox
|
|
105
|
+
*/
|
|
106
|
+
input(schema) {
|
|
107
|
+
const builder = new _JobBuilder(this._name);
|
|
108
|
+
builder._inputSchema = schema;
|
|
109
|
+
builder._outputSchema = this._outputSchema;
|
|
110
|
+
builder._cronExpression = this._cronExpression;
|
|
111
|
+
builder._runOnce = this._runOnce;
|
|
112
|
+
builder._subscribedEvent = this._subscribedEvent;
|
|
113
|
+
builder._options = this._options;
|
|
114
|
+
return builder;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Define output schema with TypeBox (for workflow integration)
|
|
118
|
+
*/
|
|
119
|
+
output(schema) {
|
|
120
|
+
const builder = new _JobBuilder(this._name);
|
|
121
|
+
builder._inputSchema = this._inputSchema;
|
|
122
|
+
builder._outputSchema = schema;
|
|
123
|
+
builder._cronExpression = this._cronExpression;
|
|
124
|
+
builder._runOnce = this._runOnce;
|
|
125
|
+
builder._subscribedEvent = this._subscribedEvent;
|
|
126
|
+
builder._options = this._options;
|
|
127
|
+
return builder;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Subscribe to an event (decoupled triggering)
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```typescript
|
|
134
|
+
* const userCreated = defineEvent('user.created', Type.Object({
|
|
135
|
+
* userId: Type.String(),
|
|
136
|
+
* }));
|
|
137
|
+
*
|
|
138
|
+
* const sendWelcomeEmail = job('send-welcome-email')
|
|
139
|
+
* .on(userCreated)
|
|
140
|
+
* .handler(async (payload) => {
|
|
141
|
+
* // payload is typed as { userId: string }
|
|
142
|
+
* });
|
|
143
|
+
* ```
|
|
144
|
+
*/
|
|
145
|
+
on(event) {
|
|
146
|
+
const builder = new _JobBuilder(this._name);
|
|
147
|
+
builder._inputSchema = event.schema;
|
|
148
|
+
builder._outputSchema = this._outputSchema;
|
|
149
|
+
builder._subscribedEvent = event.name;
|
|
150
|
+
builder._subscribedEventDef = event;
|
|
151
|
+
builder._cronExpression = this._cronExpression;
|
|
152
|
+
builder._runOnce = this._runOnce;
|
|
153
|
+
builder._options = this._options;
|
|
154
|
+
return builder;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Set cron expression for scheduled execution
|
|
158
|
+
*/
|
|
159
|
+
cron(expression) {
|
|
160
|
+
this._cronExpression = expression;
|
|
161
|
+
return this;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Mark job to run once on server start
|
|
165
|
+
*/
|
|
166
|
+
runOnce() {
|
|
167
|
+
this._runOnce = true;
|
|
168
|
+
return this;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Set job options (retry, expiration, etc.)
|
|
172
|
+
*/
|
|
173
|
+
options(options) {
|
|
174
|
+
this._options = options;
|
|
175
|
+
return this;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Set job timeout in milliseconds
|
|
179
|
+
* (Converts to expireInSeconds for pg-boss)
|
|
180
|
+
*/
|
|
181
|
+
timeout(ms) {
|
|
182
|
+
this._options = {
|
|
183
|
+
...this._options,
|
|
184
|
+
expireInSeconds: Math.ceil(ms / 1e3)
|
|
185
|
+
};
|
|
186
|
+
return this;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Define compensate handler for rollback (workflow integration)
|
|
190
|
+
*/
|
|
191
|
+
compensate(fn) {
|
|
192
|
+
this._compensate = fn;
|
|
193
|
+
return this;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Define the job handler and finalize the job definition
|
|
197
|
+
*/
|
|
198
|
+
handler(fn) {
|
|
199
|
+
this._handler = fn;
|
|
200
|
+
const name = this._name;
|
|
201
|
+
const inputSchema = this._inputSchema;
|
|
202
|
+
const outputSchema = this._outputSchema;
|
|
203
|
+
const cronExpression = this._cronExpression;
|
|
204
|
+
const runOnce = this._runOnce;
|
|
205
|
+
const subscribedEvent = this._subscribedEvent;
|
|
206
|
+
const subscribedEventDef = this._subscribedEventDef;
|
|
207
|
+
const options = this._options;
|
|
208
|
+
const handler = this._handler;
|
|
209
|
+
const compensate = this._compensate;
|
|
210
|
+
const send = async (inputOrOptions, maybeOptions) => {
|
|
211
|
+
const boss = getBoss();
|
|
212
|
+
if (!boss) {
|
|
213
|
+
throw new Error(
|
|
214
|
+
`[Job:${name}] pg-boss not initialized. Ensure jobs are registered with defineServerConfig().jobs()`
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
const [input, sendOptions] = inputSchema ? [inputOrOptions, maybeOptions] : [void 0, inputOrOptions];
|
|
218
|
+
return await boss.send(
|
|
219
|
+
name,
|
|
220
|
+
input ?? {},
|
|
221
|
+
buildPgBossOptions(options, sendOptions)
|
|
222
|
+
);
|
|
223
|
+
};
|
|
224
|
+
const run = async (input) => {
|
|
225
|
+
if (inputSchema) {
|
|
226
|
+
return await handler(input);
|
|
227
|
+
} else {
|
|
228
|
+
return await handler();
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
return {
|
|
232
|
+
name,
|
|
233
|
+
inputSchema,
|
|
234
|
+
outputSchema,
|
|
235
|
+
cronExpression,
|
|
236
|
+
runOnce,
|
|
237
|
+
subscribedEvent,
|
|
238
|
+
_subscribedEventDef: subscribedEventDef,
|
|
239
|
+
options,
|
|
240
|
+
handler,
|
|
241
|
+
compensate,
|
|
242
|
+
send,
|
|
243
|
+
run,
|
|
244
|
+
_input: void 0,
|
|
245
|
+
_output: void 0
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
function job(name) {
|
|
250
|
+
return new JobBuilder(name);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// src/job/job-router.ts
|
|
254
|
+
function isJobDef(value) {
|
|
255
|
+
return value !== null && typeof value === "object" && "name" in value && "handler" in value && "send" in value && "run" in value;
|
|
256
|
+
}
|
|
257
|
+
function isJobRouter(value) {
|
|
258
|
+
return value !== null && typeof value === "object" && "jobs" in value && "_jobs" in value;
|
|
259
|
+
}
|
|
260
|
+
function defineJobRouter(jobs) {
|
|
261
|
+
return {
|
|
262
|
+
jobs,
|
|
263
|
+
_jobs: jobs
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
function collectJobs(router, prefix = "") {
|
|
267
|
+
const jobs = [];
|
|
268
|
+
for (const [key, value] of Object.entries(router.jobs)) {
|
|
269
|
+
const name = prefix ? `${prefix}.${key}` : key;
|
|
270
|
+
if (isJobRouter(value)) {
|
|
271
|
+
jobs.push(...collectJobs(value, name));
|
|
272
|
+
} else if (isJobDef(value)) {
|
|
273
|
+
jobs.push(value);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return jobs;
|
|
277
|
+
}
|
|
278
|
+
var jobLogger2 = logger.child("@spfn/core:job");
|
|
279
|
+
function getEventQueueName(eventName) {
|
|
280
|
+
return `event:${eventName}`;
|
|
281
|
+
}
|
|
282
|
+
function getDefaultJobOptions(options) {
|
|
283
|
+
return {
|
|
284
|
+
retryLimit: options?.retryLimit ?? 3,
|
|
285
|
+
retryDelay: options?.retryDelay ?? 1e3,
|
|
286
|
+
expireInSeconds: options?.expireInSeconds ?? 300
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
async function registerJobs(router) {
|
|
290
|
+
const boss = getBoss();
|
|
291
|
+
if (!boss) {
|
|
292
|
+
throw new Error(
|
|
293
|
+
"pg-boss not initialized. Call initBoss() before registerJobs()"
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
const jobs = collectJobs(router);
|
|
297
|
+
const clearOnStart = shouldClearOnStart();
|
|
298
|
+
jobLogger2.info(`Registering ${jobs.length} job(s)...`);
|
|
299
|
+
if (clearOnStart) {
|
|
300
|
+
jobLogger2.info("Clearing existing jobs before registration...");
|
|
301
|
+
for (const job2 of jobs) {
|
|
302
|
+
await boss.deleteAllJobs(job2.name);
|
|
303
|
+
if (job2.subscribedEvent) {
|
|
304
|
+
const eventQueue = getEventQueueName(job2.subscribedEvent);
|
|
305
|
+
await boss.deleteAllJobs(eventQueue);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
jobLogger2.info("Existing jobs cleared");
|
|
309
|
+
}
|
|
310
|
+
for (const job2 of jobs) {
|
|
311
|
+
await registerJob(job2);
|
|
312
|
+
}
|
|
313
|
+
jobLogger2.info("All jobs registered successfully");
|
|
314
|
+
}
|
|
315
|
+
async function ensureQueue(boss, queueName) {
|
|
316
|
+
await boss.createQueue(queueName);
|
|
317
|
+
}
|
|
318
|
+
async function registerWorker(boss, job2, queueName) {
|
|
319
|
+
await ensureQueue(boss, queueName);
|
|
320
|
+
await boss.work(
|
|
321
|
+
queueName,
|
|
322
|
+
{ batchSize: 1 },
|
|
323
|
+
async (jobs) => {
|
|
324
|
+
for (const pgBossJob of jobs) {
|
|
325
|
+
jobLogger2.debug(`[Job:${job2.name}] Executing...`, { jobId: pgBossJob.id });
|
|
326
|
+
const startTime = Date.now();
|
|
327
|
+
try {
|
|
328
|
+
if (job2.inputSchema) {
|
|
329
|
+
await job2.handler(pgBossJob.data);
|
|
330
|
+
} else {
|
|
331
|
+
await job2.handler();
|
|
332
|
+
}
|
|
333
|
+
const duration = Date.now() - startTime;
|
|
334
|
+
jobLogger2.info(`[Job:${job2.name}] Completed in ${duration}ms`, {
|
|
335
|
+
jobId: pgBossJob.id,
|
|
336
|
+
duration
|
|
337
|
+
});
|
|
338
|
+
} catch (error) {
|
|
339
|
+
const duration = Date.now() - startTime;
|
|
340
|
+
jobLogger2.error(`[Job:${job2.name}] Failed after ${duration}ms`, {
|
|
341
|
+
jobId: pgBossJob.id,
|
|
342
|
+
duration,
|
|
343
|
+
error: error instanceof Error ? error.message : String(error)
|
|
344
|
+
});
|
|
345
|
+
throw error;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
function connectEventToQueue(boss, job2, queueName) {
|
|
352
|
+
if (!job2._subscribedEventDef) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
const eventDef = job2._subscribedEventDef;
|
|
356
|
+
eventDef._registerJobQueue(queueName, async (queue, payload) => {
|
|
357
|
+
await boss.send(queue, payload, getDefaultJobOptions(job2.options));
|
|
358
|
+
});
|
|
359
|
+
jobLogger2.debug(`[Job:${job2.name}] Connected to event: ${job2.subscribedEvent}`);
|
|
360
|
+
}
|
|
361
|
+
async function registerCronSchedule(boss, job2) {
|
|
362
|
+
if (!job2.cronExpression) {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
jobLogger2.debug(`[Job:${job2.name}] Scheduling cron: ${job2.cronExpression}`);
|
|
366
|
+
await ensureQueue(boss, job2.name);
|
|
367
|
+
await boss.schedule(
|
|
368
|
+
job2.name,
|
|
369
|
+
job2.cronExpression,
|
|
370
|
+
{},
|
|
371
|
+
getDefaultJobOptions(job2.options)
|
|
372
|
+
);
|
|
373
|
+
jobLogger2.info(`[Job:${job2.name}] Cron scheduled: ${job2.cronExpression}`);
|
|
374
|
+
}
|
|
375
|
+
async function queueRunOnceJob(boss, job2) {
|
|
376
|
+
if (!job2.runOnce) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
jobLogger2.debug(`[Job:${job2.name}] Queuing runOnce job`);
|
|
380
|
+
await ensureQueue(boss, job2.name);
|
|
381
|
+
await boss.send(
|
|
382
|
+
job2.name,
|
|
383
|
+
{},
|
|
384
|
+
{
|
|
385
|
+
...getDefaultJobOptions(job2.options),
|
|
386
|
+
singletonKey: `runOnce:${job2.name}`
|
|
387
|
+
}
|
|
388
|
+
);
|
|
389
|
+
jobLogger2.info(`[Job:${job2.name}] runOnce job queued`);
|
|
390
|
+
}
|
|
391
|
+
async function registerJob(job2) {
|
|
392
|
+
const boss = getBoss();
|
|
393
|
+
if (!boss) {
|
|
394
|
+
throw new Error("pg-boss not initialized");
|
|
395
|
+
}
|
|
396
|
+
const queueName = job2.subscribedEvent ? getEventQueueName(job2.subscribedEvent) : job2.name;
|
|
397
|
+
jobLogger2.debug(`Registering job: ${job2.name}`, {
|
|
398
|
+
queueName,
|
|
399
|
+
subscribedEvent: job2.subscribedEvent
|
|
400
|
+
});
|
|
401
|
+
await registerWorker(boss, job2, queueName);
|
|
402
|
+
connectEventToQueue(boss, job2, queueName);
|
|
403
|
+
await registerCronSchedule(boss, job2);
|
|
404
|
+
await queueRunOnceJob(boss, job2);
|
|
405
|
+
jobLogger2.debug(`Job registered: ${job2.name}`);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export { collectJobs, defineJobRouter, getBoss, initBoss, isBossRunning, isJobDef, isJobRouter, job, registerJobs, shouldClearOnStart, stopBoss };
|
|
409
|
+
//# sourceMappingURL=index.js.map
|
|
410
|
+
//# sourceMappingURL=index.js.map
|