@spfn/core 0.2.0-beta.1 → 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 +262 -1092
- package/dist/{boss-D-fGtVgM.d.ts → boss-DI1r4kTS.d.ts} +68 -11
- package/dist/codegen/index.d.ts +55 -8
- package/dist/codegen/index.js +159 -5
- package/dist/codegen/index.js.map +1 -1
- package/dist/config/index.d.ts +36 -0
- package/dist/config/index.js +15 -6
- package/dist/config/index.js.map +1 -1
- package/dist/db/index.d.ts +13 -0
- package/dist/db/index.js +40 -6
- package/dist/db/index.js.map +1 -1
- package/dist/env/index.d.ts +82 -3
- package/dist/env/index.js +81 -14
- 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/event/index.d.ts +3 -70
- package/dist/event/index.js +10 -1
- package/dist/event/index.js.map +1 -1
- 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 +54 -8
- package/dist/job/index.js +61 -12
- package/dist/job/index.js.map +1 -1
- package/dist/middleware/index.d.ts +102 -11
- package/dist/middleware/index.js +2 -2
- package/dist/middleware/index.js.map +1 -1
- package/dist/nextjs/index.d.ts +2 -2
- package/dist/nextjs/index.js +36 -4
- package/dist/nextjs/index.js.map +1 -1
- package/dist/nextjs/server.d.ts +62 -15
- package/dist/nextjs/server.js +102 -33
- package/dist/nextjs/server.js.map +1 -1
- package/dist/route/index.d.ts +227 -15
- package/dist/route/index.js +307 -31
- package/dist/route/index.js.map +1 -1
- package/dist/route/types.d.ts +2 -31
- package/dist/router-Di7ENoah.d.ts +151 -0
- package/dist/server/index.d.ts +153 -6
- package/dist/server/index.js +216 -14
- package/dist/server/index.js.map +1 -1
- package/dist/types-B-e_f2dQ.d.ts +121 -0
- package/dist/{types-DRG2XMTR.d.ts → types-BOPTApC2.d.ts} +91 -3
- 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 +18 -3
package/dist/server/index.d.ts
CHANGED
|
@@ -2,12 +2,42 @@ import { MiddlewareHandler, Hono } from 'hono';
|
|
|
2
2
|
import { cors } from 'hono/cors';
|
|
3
3
|
import { serve } from '@hono/node-server';
|
|
4
4
|
import { NamedMiddleware, Router } from '@spfn/core/route';
|
|
5
|
-
import { J as JobRouter, B as
|
|
5
|
+
import { J as JobRouter, B as BossOptions } from '../boss-DI1r4kTS.js';
|
|
6
|
+
import { E as EventRouterDef } from '../router-Di7ENoah.js';
|
|
7
|
+
import { S as SSEHandlerConfig } from '../types-B-e_f2dQ.js';
|
|
6
8
|
import '@sinclair/typebox';
|
|
7
9
|
import 'pg-boss';
|
|
8
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Load environment files for SPFN server
|
|
13
|
+
*
|
|
14
|
+
* Priority (high → low, later files don't override):
|
|
15
|
+
* 1. .env.server.local - Server-only secrets (gitignored)
|
|
16
|
+
* 2. .env.server - Server-only defaults
|
|
17
|
+
* 3. .env.{NODE_ENV}.local
|
|
18
|
+
* 4. .env.local - Local overrides (gitignored)
|
|
19
|
+
* 5. .env.{NODE_ENV}
|
|
20
|
+
* 6. .env - Defaults
|
|
21
|
+
*/
|
|
9
22
|
declare function loadEnvFiles(): void;
|
|
10
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Workflow router interface for @spfn/core integration
|
|
26
|
+
*
|
|
27
|
+
* This is a minimal interface that avoids circular dependency with @spfn/workflow.
|
|
28
|
+
* The actual WorkflowRouter from @spfn/workflow implements this interface.
|
|
29
|
+
*/
|
|
30
|
+
interface WorkflowRouterLike {
|
|
31
|
+
/**
|
|
32
|
+
* Initialize the workflow engine
|
|
33
|
+
* Called by server during infrastructure initialization
|
|
34
|
+
*
|
|
35
|
+
* @internal
|
|
36
|
+
*/
|
|
37
|
+
_init: (db: any, options?: {
|
|
38
|
+
largeOutputThreshold?: number;
|
|
39
|
+
}) => void;
|
|
40
|
+
}
|
|
11
41
|
/**
|
|
12
42
|
* CORS configuration options - inferred from hono/cors
|
|
13
43
|
*/
|
|
@@ -117,7 +147,39 @@ interface ServerConfig {
|
|
|
117
147
|
* pg-boss configuration options
|
|
118
148
|
* Only used if jobs router is provided
|
|
119
149
|
*/
|
|
120
|
-
jobsConfig?: Omit<
|
|
150
|
+
jobsConfig?: Omit<BossOptions, 'connectionString'>;
|
|
151
|
+
/**
|
|
152
|
+
* Event router for SSE (Server-Sent Events) subscription
|
|
153
|
+
* Enables real-time event streaming to frontend clients
|
|
154
|
+
*
|
|
155
|
+
* @example
|
|
156
|
+
* ```typescript
|
|
157
|
+
* import { defineEvent, defineEventRouter } from '@spfn/core/event';
|
|
158
|
+
*
|
|
159
|
+
* const userCreated = defineEvent('user.created', Type.Object({
|
|
160
|
+
* userId: Type.String(),
|
|
161
|
+
* }));
|
|
162
|
+
*
|
|
163
|
+
* const eventRouter = defineEventRouter({ userCreated });
|
|
164
|
+
*
|
|
165
|
+
* export default defineServerConfig()
|
|
166
|
+
* .routes(appRouter)
|
|
167
|
+
* .events(eventRouter) // → GET /events/stream
|
|
168
|
+
* .build();
|
|
169
|
+
* ```
|
|
170
|
+
*/
|
|
171
|
+
events?: EventRouterDef<any>;
|
|
172
|
+
/**
|
|
173
|
+
* SSE configuration options
|
|
174
|
+
* Only used if events router is provided
|
|
175
|
+
*/
|
|
176
|
+
eventsConfig?: SSEHandlerConfig & {
|
|
177
|
+
/**
|
|
178
|
+
* SSE endpoint path
|
|
179
|
+
* @default '/events/stream'
|
|
180
|
+
*/
|
|
181
|
+
path?: string;
|
|
182
|
+
};
|
|
121
183
|
/**
|
|
122
184
|
* Enable debug mode (default: NODE_ENV === 'development')
|
|
123
185
|
*/
|
|
@@ -293,6 +355,39 @@ interface ServerConfig {
|
|
|
293
355
|
*/
|
|
294
356
|
redis?: boolean;
|
|
295
357
|
};
|
|
358
|
+
/**
|
|
359
|
+
* Workflow router for workflow orchestration
|
|
360
|
+
*
|
|
361
|
+
* Automatically initializes the workflow engine after database is ready.
|
|
362
|
+
* Workflows are defined using @spfn/workflow package.
|
|
363
|
+
*
|
|
364
|
+
* @example
|
|
365
|
+
* ```typescript
|
|
366
|
+
* import { defineWorkflowRouter } from '@spfn/workflow';
|
|
367
|
+
*
|
|
368
|
+
* const workflowRouter = defineWorkflowRouter([
|
|
369
|
+
* provisionTenant,
|
|
370
|
+
* deprovisionTenant,
|
|
371
|
+
* ]);
|
|
372
|
+
*
|
|
373
|
+
* export default defineServerConfig()
|
|
374
|
+
* .workflows(workflowRouter)
|
|
375
|
+
* .build();
|
|
376
|
+
* ```
|
|
377
|
+
*/
|
|
378
|
+
workflows?: WorkflowRouterLike;
|
|
379
|
+
/**
|
|
380
|
+
* Workflow engine configuration
|
|
381
|
+
* Only used if workflows router is provided
|
|
382
|
+
*/
|
|
383
|
+
workflowsConfig?: {
|
|
384
|
+
/**
|
|
385
|
+
* Large output threshold in bytes
|
|
386
|
+
* Outputs larger than this will be stored in external storage
|
|
387
|
+
* @default 1024 * 1024 (1MB)
|
|
388
|
+
*/
|
|
389
|
+
largeOutputThreshold?: number;
|
|
390
|
+
};
|
|
296
391
|
/**
|
|
297
392
|
* Server lifecycle hooks for custom infrastructure setup and management
|
|
298
393
|
* Allows initialization of custom services and resources at different stages
|
|
@@ -545,7 +640,38 @@ declare class ServerConfigBuilder {
|
|
|
545
640
|
* .build();
|
|
546
641
|
* ```
|
|
547
642
|
*/
|
|
548
|
-
jobs(router: JobRouter<any>, config?: Omit<
|
|
643
|
+
jobs(router: JobRouter<any>, config?: Omit<BossOptions, 'connectionString'>): this;
|
|
644
|
+
/**
|
|
645
|
+
* Register event router for SSE (Server-Sent Events)
|
|
646
|
+
*
|
|
647
|
+
* Enables real-time event streaming to frontend clients.
|
|
648
|
+
* Events defined with defineEvent() can be subscribed by:
|
|
649
|
+
* - Backend: .subscribe() for internal handlers
|
|
650
|
+
* - Jobs: .on(event) for background processing
|
|
651
|
+
* - Frontend: SSE stream for real-time updates
|
|
652
|
+
*
|
|
653
|
+
* @example
|
|
654
|
+
* ```typescript
|
|
655
|
+
* import { defineEvent, defineEventRouter } from '@spfn/core/event';
|
|
656
|
+
*
|
|
657
|
+
* const userCreated = defineEvent('user.created', Type.Object({
|
|
658
|
+
* userId: Type.String(),
|
|
659
|
+
* }));
|
|
660
|
+
*
|
|
661
|
+
* const eventRouter = defineEventRouter({ userCreated });
|
|
662
|
+
*
|
|
663
|
+
* export default defineServerConfig()
|
|
664
|
+
* .routes(appRouter)
|
|
665
|
+
* .events(eventRouter) // → GET /events/stream
|
|
666
|
+
* .build();
|
|
667
|
+
*
|
|
668
|
+
* // Custom path
|
|
669
|
+
* .events(eventRouter, { path: '/sse' })
|
|
670
|
+
* ```
|
|
671
|
+
*/
|
|
672
|
+
events(router: EventRouterDef<any>, config?: SSEHandlerConfig & {
|
|
673
|
+
path?: string;
|
|
674
|
+
}): this;
|
|
549
675
|
/**
|
|
550
676
|
* Enable/disable debug mode
|
|
551
677
|
*/
|
|
@@ -570,6 +696,27 @@ declare class ServerConfigBuilder {
|
|
|
570
696
|
* Configure infrastructure initialization
|
|
571
697
|
*/
|
|
572
698
|
infrastructure(infrastructure: ServerConfig['infrastructure']): this;
|
|
699
|
+
/**
|
|
700
|
+
* Register workflow router for workflow orchestration
|
|
701
|
+
*
|
|
702
|
+
* Automatically initializes the workflow engine after database is ready.
|
|
703
|
+
*
|
|
704
|
+
* @example
|
|
705
|
+
* ```typescript
|
|
706
|
+
* import { defineWorkflowRouter } from '@spfn/workflow';
|
|
707
|
+
*
|
|
708
|
+
* const workflowRouter = defineWorkflowRouter([
|
|
709
|
+
* provisionTenant,
|
|
710
|
+
* deprovisionTenant,
|
|
711
|
+
* ]);
|
|
712
|
+
*
|
|
713
|
+
* export default defineServerConfig()
|
|
714
|
+
* .routes(appRouter)
|
|
715
|
+
* .workflows(workflowRouter)
|
|
716
|
+
* .build();
|
|
717
|
+
* ```
|
|
718
|
+
*/
|
|
719
|
+
workflows(router: ServerConfig['workflows'], config?: ServerConfig['workflowsConfig']): this;
|
|
573
720
|
/**
|
|
574
721
|
* Configure lifecycle hooks
|
|
575
722
|
* Can be called multiple times - hooks will be executed in registration order
|
|
@@ -592,10 +739,10 @@ declare class ServerConfigBuilder {
|
|
|
592
739
|
*
|
|
593
740
|
* const appRouter = defineRouter({
|
|
594
741
|
* getUser: route.get('/users/:id')
|
|
595
|
-
* .input(Type.Object({ id: Type.String() }))
|
|
742
|
+
* .input({ params: Type.Object({ id: Type.String() }) })
|
|
596
743
|
* .handler(async (c) => {
|
|
597
|
-
* const {
|
|
598
|
-
* return
|
|
744
|
+
* const { params } = await c.data();
|
|
745
|
+
* return { id: params.id, name: 'John' };
|
|
599
746
|
* }),
|
|
600
747
|
* });
|
|
601
748
|
*
|
package/dist/server/index.js
CHANGED
|
@@ -6,9 +6,10 @@ import { Hono } from 'hono';
|
|
|
6
6
|
import { cors } from 'hono/cors';
|
|
7
7
|
import { registerRoutes } from '@spfn/core/route';
|
|
8
8
|
import { ErrorHandler, RequestLogger } from '@spfn/core/middleware';
|
|
9
|
+
import { streamSSE } from 'hono/streaming';
|
|
10
|
+
import { logger } from '@spfn/core/logger';
|
|
9
11
|
import { initDatabase, getDatabase, closeDatabase } from '@spfn/core/db';
|
|
10
12
|
import { initCache, getCache, closeCache } from '@spfn/core/cache';
|
|
11
|
-
import { logger } from '@spfn/core/logger';
|
|
12
13
|
import { serve } from '@hono/node-server';
|
|
13
14
|
import PgBoss from 'pg-boss';
|
|
14
15
|
import { networkInterfaces } from 'os';
|
|
@@ -317,6 +318,8 @@ function loadEnvFiles() {
|
|
|
317
318
|
const cwd = process.cwd();
|
|
318
319
|
const nodeEnv = process.env.NODE_ENV || "development";
|
|
319
320
|
const envFiles = [
|
|
321
|
+
".env.server.local",
|
|
322
|
+
".env.server",
|
|
320
323
|
`.env.${nodeEnv}.local`,
|
|
321
324
|
nodeEnv !== "test" ? ".env.local" : null,
|
|
322
325
|
`.env.${nodeEnv}`,
|
|
@@ -329,6 +332,90 @@ function loadEnvFiles() {
|
|
|
329
332
|
}
|
|
330
333
|
}
|
|
331
334
|
}
|
|
335
|
+
var sseLogger = logger.child("@spfn/core:sse");
|
|
336
|
+
function createSSEHandler(router, config2 = {}) {
|
|
337
|
+
const {
|
|
338
|
+
pingInterval = 3e4
|
|
339
|
+
// headers: customHeaders = {}, // Reserved for future use
|
|
340
|
+
} = config2;
|
|
341
|
+
return async (c) => {
|
|
342
|
+
const eventsParam = c.req.query("events");
|
|
343
|
+
if (!eventsParam) {
|
|
344
|
+
return c.json({ error: "Missing events parameter" }, 400);
|
|
345
|
+
}
|
|
346
|
+
const requestedEvents = eventsParam.split(",").map((e) => e.trim());
|
|
347
|
+
const validEventNames = router.eventNames;
|
|
348
|
+
const invalidEvents = requestedEvents.filter((e) => !validEventNames.includes(e));
|
|
349
|
+
if (invalidEvents.length > 0) {
|
|
350
|
+
return c.json({
|
|
351
|
+
error: "Invalid event names",
|
|
352
|
+
invalidEvents,
|
|
353
|
+
validEvents: validEventNames
|
|
354
|
+
}, 400);
|
|
355
|
+
}
|
|
356
|
+
sseLogger.debug("SSE connection requested", {
|
|
357
|
+
events: requestedEvents,
|
|
358
|
+
clientIp: c.req.header("x-forwarded-for") || c.req.header("x-real-ip")
|
|
359
|
+
});
|
|
360
|
+
return streamSSE(c, async (stream) => {
|
|
361
|
+
const unsubscribes = [];
|
|
362
|
+
let messageId = 0;
|
|
363
|
+
for (const eventName of requestedEvents) {
|
|
364
|
+
const eventDef = router.events[eventName];
|
|
365
|
+
if (!eventDef) {
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
const unsubscribe = eventDef.subscribe((payload) => {
|
|
369
|
+
messageId++;
|
|
370
|
+
const message = {
|
|
371
|
+
event: eventName,
|
|
372
|
+
data: payload
|
|
373
|
+
};
|
|
374
|
+
sseLogger.debug("SSE sending event", {
|
|
375
|
+
event: eventName,
|
|
376
|
+
messageId
|
|
377
|
+
});
|
|
378
|
+
void stream.writeSSE({
|
|
379
|
+
id: String(messageId),
|
|
380
|
+
event: eventName,
|
|
381
|
+
data: JSON.stringify(message)
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
unsubscribes.push(unsubscribe);
|
|
385
|
+
}
|
|
386
|
+
sseLogger.info("SSE connection established", {
|
|
387
|
+
events: requestedEvents,
|
|
388
|
+
subscriptionCount: unsubscribes.length
|
|
389
|
+
});
|
|
390
|
+
await stream.writeSSE({
|
|
391
|
+
event: "connected",
|
|
392
|
+
data: JSON.stringify({
|
|
393
|
+
subscribedEvents: requestedEvents,
|
|
394
|
+
timestamp: Date.now()
|
|
395
|
+
})
|
|
396
|
+
});
|
|
397
|
+
const pingTimer = setInterval(() => {
|
|
398
|
+
void stream.writeSSE({
|
|
399
|
+
event: "ping",
|
|
400
|
+
data: JSON.stringify({ timestamp: Date.now() })
|
|
401
|
+
});
|
|
402
|
+
}, pingInterval);
|
|
403
|
+
const abortSignal = c.req.raw.signal;
|
|
404
|
+
while (!abortSignal.aborted) {
|
|
405
|
+
await stream.sleep(pingInterval);
|
|
406
|
+
}
|
|
407
|
+
clearInterval(pingTimer);
|
|
408
|
+
unsubscribes.forEach((fn) => fn());
|
|
409
|
+
sseLogger.info("SSE connection closed", {
|
|
410
|
+
events: requestedEvents
|
|
411
|
+
});
|
|
412
|
+
}, async (err) => {
|
|
413
|
+
sseLogger.error("SSE stream error", {
|
|
414
|
+
error: err.message
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
};
|
|
418
|
+
}
|
|
332
419
|
function createHealthCheckHandler(detailed) {
|
|
333
420
|
return async (c) => {
|
|
334
421
|
const response = {
|
|
@@ -465,7 +552,8 @@ async function loadCustomApp(appPath, appJsPath, config2) {
|
|
|
465
552
|
}
|
|
466
553
|
const app = await appFactory();
|
|
467
554
|
if (config2?.routes) {
|
|
468
|
-
registerRoutes(app, config2.routes, config2.middlewares);
|
|
555
|
+
const routes = registerRoutes(app, config2.routes, config2.middlewares);
|
|
556
|
+
logRegisteredRoutes(routes, config2?.debug ?? false);
|
|
469
557
|
}
|
|
470
558
|
return app;
|
|
471
559
|
}
|
|
@@ -488,6 +576,7 @@ async function createAutoConfiguredApp(config2) {
|
|
|
488
576
|
registerHealthCheckEndpoint(app, config2);
|
|
489
577
|
await executeBeforeRoutesHook(app, config2);
|
|
490
578
|
await loadAppRoutes(app, config2);
|
|
579
|
+
registerSSEEndpoint(app, config2);
|
|
491
580
|
await executeAfterRoutesHook(app, config2);
|
|
492
581
|
if (enableErrorHandler) {
|
|
493
582
|
app.onError(ErrorHandler());
|
|
@@ -521,38 +610,69 @@ async function executeBeforeRoutesHook(app, config2) {
|
|
|
521
610
|
async function loadAppRoutes(app, config2) {
|
|
522
611
|
const debug = isDebugMode(config2);
|
|
523
612
|
if (config2?.routes) {
|
|
524
|
-
registerRoutes(app, config2.routes, config2.middlewares);
|
|
525
|
-
|
|
526
|
-
serverLogger.info("\u2713 Routes registered");
|
|
527
|
-
}
|
|
613
|
+
const routes = registerRoutes(app, config2.routes, config2.middlewares);
|
|
614
|
+
logRegisteredRoutes(routes, debug);
|
|
528
615
|
} else if (debug) {
|
|
529
616
|
serverLogger.warn("\u26A0\uFE0F No routes configured. Use defineServerConfig().routes() to register routes.");
|
|
530
617
|
}
|
|
531
618
|
}
|
|
619
|
+
function logRegisteredRoutes(routes, debug) {
|
|
620
|
+
if (routes.length === 0) {
|
|
621
|
+
if (debug) {
|
|
622
|
+
serverLogger.warn("\u26A0\uFE0F No routes registered");
|
|
623
|
+
}
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
const sortedRoutes = [...routes].sort((a, b) => a.path.localeCompare(b.path));
|
|
627
|
+
const maxMethodLen = Math.max(...sortedRoutes.map((r) => r.method.length));
|
|
628
|
+
const routeLines = sortedRoutes.map(
|
|
629
|
+
(r) => ` ${r.method.padEnd(maxMethodLen)} ${r.path}`
|
|
630
|
+
).join("\n");
|
|
631
|
+
serverLogger.info(`\u2713 Routes registered (${routes.length}):
|
|
632
|
+
${routeLines}`);
|
|
633
|
+
}
|
|
532
634
|
async function executeAfterRoutesHook(app, config2) {
|
|
533
635
|
if (config2?.lifecycle?.afterRoutes) {
|
|
534
636
|
await config2.lifecycle.afterRoutes(app);
|
|
535
637
|
}
|
|
536
638
|
}
|
|
639
|
+
function registerSSEEndpoint(app, config2) {
|
|
640
|
+
if (!config2?.events) {
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
const eventsConfig = config2.eventsConfig ?? {};
|
|
644
|
+
const path = eventsConfig.path ?? "/events/stream";
|
|
645
|
+
const debug = isDebugMode(config2);
|
|
646
|
+
app.get(path, createSSEHandler(config2.events, eventsConfig));
|
|
647
|
+
if (debug) {
|
|
648
|
+
const eventNames = config2.events.eventNames;
|
|
649
|
+
serverLogger.info(`\u2713 SSE endpoint registered at ${path}`, {
|
|
650
|
+
events: eventNames
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
}
|
|
537
654
|
function isDebugMode(config2) {
|
|
538
655
|
return config2?.debug ?? process.env.NODE_ENV === "development";
|
|
539
656
|
}
|
|
540
657
|
var jobLogger = logger.child("@spfn/core:job");
|
|
541
658
|
var bossInstance = null;
|
|
542
659
|
var bossConfig = null;
|
|
543
|
-
async function initBoss(
|
|
660
|
+
async function initBoss(options) {
|
|
544
661
|
if (bossInstance) {
|
|
545
662
|
jobLogger.warn("pg-boss already initialized, returning existing instance");
|
|
546
663
|
return bossInstance;
|
|
547
664
|
}
|
|
548
665
|
jobLogger.info("Initializing pg-boss...");
|
|
549
|
-
bossConfig =
|
|
550
|
-
|
|
551
|
-
connectionString:
|
|
552
|
-
schema:
|
|
553
|
-
maintenanceIntervalSeconds:
|
|
554
|
-
|
|
555
|
-
|
|
666
|
+
bossConfig = options;
|
|
667
|
+
const pgBossOptions = {
|
|
668
|
+
connectionString: options.connectionString,
|
|
669
|
+
schema: options.schema ?? "spfn_queue",
|
|
670
|
+
maintenanceIntervalSeconds: options.maintenanceIntervalSeconds ?? 120
|
|
671
|
+
};
|
|
672
|
+
if (options.monitorIntervalSeconds !== void 0 && options.monitorIntervalSeconds >= 1) {
|
|
673
|
+
pgBossOptions.monitorIntervalSeconds = options.monitorIntervalSeconds;
|
|
674
|
+
}
|
|
675
|
+
bossInstance = new PgBoss(pgBossOptions);
|
|
556
676
|
bossInstance.on("error", (error) => {
|
|
557
677
|
jobLogger.error("pg-boss error:", error);
|
|
558
678
|
});
|
|
@@ -639,7 +759,11 @@ async function registerJobs(router) {
|
|
|
639
759
|
}
|
|
640
760
|
jobLogger2.info("All jobs registered successfully");
|
|
641
761
|
}
|
|
762
|
+
async function ensureQueue(boss, queueName) {
|
|
763
|
+
await boss.createQueue(queueName);
|
|
764
|
+
}
|
|
642
765
|
async function registerWorker(boss, job2, queueName) {
|
|
766
|
+
await ensureQueue(boss, queueName);
|
|
643
767
|
await boss.work(
|
|
644
768
|
queueName,
|
|
645
769
|
{ batchSize: 1 },
|
|
@@ -686,6 +810,7 @@ async function registerCronSchedule(boss, job2) {
|
|
|
686
810
|
return;
|
|
687
811
|
}
|
|
688
812
|
jobLogger2.debug(`[Job:${job2.name}] Scheduling cron: ${job2.cronExpression}`);
|
|
813
|
+
await ensureQueue(boss, job2.name);
|
|
689
814
|
await boss.schedule(
|
|
690
815
|
job2.name,
|
|
691
816
|
job2.cronExpression,
|
|
@@ -699,6 +824,7 @@ async function queueRunOnceJob(boss, job2) {
|
|
|
699
824
|
return;
|
|
700
825
|
}
|
|
701
826
|
jobLogger2.debug(`[Job:${job2.name}] Queuing runOnce job`);
|
|
827
|
+
await ensureQueue(boss, job2.name);
|
|
702
828
|
await boss.send(
|
|
703
829
|
job2.name,
|
|
704
830
|
{},
|
|
@@ -947,6 +1073,20 @@ async function initializeInfrastructure(config2) {
|
|
|
947
1073
|
serverLogger.debug("Registering jobs...");
|
|
948
1074
|
await registerJobs(config2.jobs);
|
|
949
1075
|
}
|
|
1076
|
+
if (config2.workflows) {
|
|
1077
|
+
const infraConfig2 = getInfrastructureConfig(config2);
|
|
1078
|
+
if (!infraConfig2.database) {
|
|
1079
|
+
throw new Error(
|
|
1080
|
+
"Workflows require database connection. Ensure database is enabled in infrastructure config."
|
|
1081
|
+
);
|
|
1082
|
+
}
|
|
1083
|
+
serverLogger.debug("Initializing workflow engine...");
|
|
1084
|
+
config2.workflows._init(
|
|
1085
|
+
getDatabase(),
|
|
1086
|
+
config2.workflowsConfig
|
|
1087
|
+
);
|
|
1088
|
+
serverLogger.info("Workflow engine initialized");
|
|
1089
|
+
}
|
|
950
1090
|
}
|
|
951
1091
|
function startHttpServer(app, host, port) {
|
|
952
1092
|
serverLogger.debug(`Starting server on ${host}:${port}...`);
|
|
@@ -1314,6 +1454,41 @@ var ServerConfigBuilder = class {
|
|
|
1314
1454
|
}
|
|
1315
1455
|
return this;
|
|
1316
1456
|
}
|
|
1457
|
+
/**
|
|
1458
|
+
* Register event router for SSE (Server-Sent Events)
|
|
1459
|
+
*
|
|
1460
|
+
* Enables real-time event streaming to frontend clients.
|
|
1461
|
+
* Events defined with defineEvent() can be subscribed by:
|
|
1462
|
+
* - Backend: .subscribe() for internal handlers
|
|
1463
|
+
* - Jobs: .on(event) for background processing
|
|
1464
|
+
* - Frontend: SSE stream for real-time updates
|
|
1465
|
+
*
|
|
1466
|
+
* @example
|
|
1467
|
+
* ```typescript
|
|
1468
|
+
* import { defineEvent, defineEventRouter } from '@spfn/core/event';
|
|
1469
|
+
*
|
|
1470
|
+
* const userCreated = defineEvent('user.created', Type.Object({
|
|
1471
|
+
* userId: Type.String(),
|
|
1472
|
+
* }));
|
|
1473
|
+
*
|
|
1474
|
+
* const eventRouter = defineEventRouter({ userCreated });
|
|
1475
|
+
*
|
|
1476
|
+
* export default defineServerConfig()
|
|
1477
|
+
* .routes(appRouter)
|
|
1478
|
+
* .events(eventRouter) // → GET /events/stream
|
|
1479
|
+
* .build();
|
|
1480
|
+
*
|
|
1481
|
+
* // Custom path
|
|
1482
|
+
* .events(eventRouter, { path: '/sse' })
|
|
1483
|
+
* ```
|
|
1484
|
+
*/
|
|
1485
|
+
events(router, config2) {
|
|
1486
|
+
this.config.events = router;
|
|
1487
|
+
if (config2) {
|
|
1488
|
+
this.config.eventsConfig = config2;
|
|
1489
|
+
}
|
|
1490
|
+
return this;
|
|
1491
|
+
}
|
|
1317
1492
|
/**
|
|
1318
1493
|
* Enable/disable debug mode
|
|
1319
1494
|
*/
|
|
@@ -1356,6 +1531,33 @@ var ServerConfigBuilder = class {
|
|
|
1356
1531
|
this.config.infrastructure = infrastructure;
|
|
1357
1532
|
return this;
|
|
1358
1533
|
}
|
|
1534
|
+
/**
|
|
1535
|
+
* Register workflow router for workflow orchestration
|
|
1536
|
+
*
|
|
1537
|
+
* Automatically initializes the workflow engine after database is ready.
|
|
1538
|
+
*
|
|
1539
|
+
* @example
|
|
1540
|
+
* ```typescript
|
|
1541
|
+
* import { defineWorkflowRouter } from '@spfn/workflow';
|
|
1542
|
+
*
|
|
1543
|
+
* const workflowRouter = defineWorkflowRouter([
|
|
1544
|
+
* provisionTenant,
|
|
1545
|
+
* deprovisionTenant,
|
|
1546
|
+
* ]);
|
|
1547
|
+
*
|
|
1548
|
+
* export default defineServerConfig()
|
|
1549
|
+
* .routes(appRouter)
|
|
1550
|
+
* .workflows(workflowRouter)
|
|
1551
|
+
* .build();
|
|
1552
|
+
* ```
|
|
1553
|
+
*/
|
|
1554
|
+
workflows(router, config2) {
|
|
1555
|
+
this.config.workflows = router;
|
|
1556
|
+
if (config2) {
|
|
1557
|
+
this.config.workflowsConfig = config2;
|
|
1558
|
+
}
|
|
1559
|
+
return this;
|
|
1560
|
+
}
|
|
1359
1561
|
/**
|
|
1360
1562
|
* Configure lifecycle hooks
|
|
1361
1563
|
* Can be called multiple times - hooks will be executed in registration order
|