@spfn/core 0.2.0-beta.1 → 0.2.0-beta.11

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.
Files changed (64) hide show
  1. package/README.md +262 -1092
  2. package/dist/{boss-D-fGtVgM.d.ts → boss-DI1r4kTS.d.ts} +68 -11
  3. package/dist/codegen/index.d.ts +55 -8
  4. package/dist/codegen/index.js +179 -5
  5. package/dist/codegen/index.js.map +1 -1
  6. package/dist/config/index.d.ts +36 -0
  7. package/dist/config/index.js +15 -6
  8. package/dist/config/index.js.map +1 -1
  9. package/dist/db/index.d.ts +13 -0
  10. package/dist/db/index.js +40 -6
  11. package/dist/db/index.js.map +1 -1
  12. package/dist/env/index.d.ts +82 -3
  13. package/dist/env/index.js +81 -14
  14. package/dist/env/index.js.map +1 -1
  15. package/dist/env/loader.d.ts +87 -0
  16. package/dist/env/loader.js +70 -0
  17. package/dist/env/loader.js.map +1 -0
  18. package/dist/event/index.d.ts +3 -70
  19. package/dist/event/index.js +10 -1
  20. package/dist/event/index.js.map +1 -1
  21. package/dist/event/sse/client.d.ts +82 -0
  22. package/dist/event/sse/client.js +115 -0
  23. package/dist/event/sse/client.js.map +1 -0
  24. package/dist/event/sse/index.d.ts +40 -0
  25. package/dist/event/sse/index.js +92 -0
  26. package/dist/event/sse/index.js.map +1 -0
  27. package/dist/job/index.d.ts +54 -8
  28. package/dist/job/index.js +61 -12
  29. package/dist/job/index.js.map +1 -1
  30. package/dist/middleware/index.d.ts +102 -11
  31. package/dist/middleware/index.js +2 -2
  32. package/dist/middleware/index.js.map +1 -1
  33. package/dist/nextjs/index.d.ts +2 -2
  34. package/dist/nextjs/index.js +36 -4
  35. package/dist/nextjs/index.js.map +1 -1
  36. package/dist/nextjs/server.d.ts +62 -15
  37. package/dist/nextjs/server.js +102 -33
  38. package/dist/nextjs/server.js.map +1 -1
  39. package/dist/route/index.d.ts +227 -15
  40. package/dist/route/index.js +307 -31
  41. package/dist/route/index.js.map +1 -1
  42. package/dist/route/types.d.ts +2 -31
  43. package/dist/router-Di7ENoah.d.ts +151 -0
  44. package/dist/server/index.d.ts +153 -6
  45. package/dist/server/index.js +221 -44
  46. package/dist/server/index.js.map +1 -1
  47. package/dist/types-B-e_f2dQ.d.ts +121 -0
  48. package/dist/{types-DRG2XMTR.d.ts → types-BOPTApC2.d.ts} +91 -3
  49. package/docs/cache.md +133 -0
  50. package/docs/codegen.md +74 -0
  51. package/docs/database.md +346 -0
  52. package/docs/entity.md +539 -0
  53. package/docs/env.md +477 -0
  54. package/docs/errors.md +319 -0
  55. package/docs/event.md +116 -0
  56. package/docs/file-upload.md +717 -0
  57. package/docs/job.md +131 -0
  58. package/docs/logger.md +108 -0
  59. package/docs/middleware.md +337 -0
  60. package/docs/nextjs.md +241 -0
  61. package/docs/repository.md +496 -0
  62. package/docs/route.md +497 -0
  63. package/docs/server.md +307 -0
  64. package/package.json +18 -3
@@ -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 BossConfig } from '../boss-D-fGtVgM.js';
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<BossConfig, 'connectionString'>;
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<BossConfig, 'connectionString'>): this;
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 { id } = await c.data();
598
- * return c.success({ id, name: 'John' });
744
+ * const { params } = await c.data();
745
+ * return { id: params.id, name: 'John' };
599
746
  * }),
600
747
  * });
601
748
  *
@@ -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
- if (debug) {
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(config2) {
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 = config2;
550
- bossInstance = new PgBoss({
551
- connectionString: config2.connectionString,
552
- schema: config2.schema ?? "spfn_queue",
553
- maintenanceIntervalSeconds: config2.maintenanceIntervalSeconds ?? 120,
554
- monitorIntervalSeconds: config2.monitorIntervalSeconds
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
  {},
@@ -805,9 +931,7 @@ var DEFAULT_MAX_LISTENERS = 15;
805
931
  var TIMEOUTS = {
806
932
  SERVER_CLOSE: 5e3,
807
933
  DATABASE_CLOSE: 5e3,
808
- REDIS_CLOSE: 5e3,
809
- PRODUCTION_ERROR_SHUTDOWN: 1e4
810
- };
934
+ REDIS_CLOSE: 5e3};
811
935
  var CONFIG_FILE_PATHS = [
812
936
  ".spfn/server/server.config.mjs",
813
937
  ".spfn/server/server.config",
@@ -947,6 +1071,20 @@ async function initializeInfrastructure(config2) {
947
1071
  serverLogger.debug("Registering jobs...");
948
1072
  await registerJobs(config2.jobs);
949
1073
  }
1074
+ if (config2.workflows) {
1075
+ const infraConfig2 = getInfrastructureConfig(config2);
1076
+ if (!infraConfig2.database) {
1077
+ throw new Error(
1078
+ "Workflows require database connection. Ensure database is enabled in infrastructure config."
1079
+ );
1080
+ }
1081
+ serverLogger.debug("Initializing workflow engine...");
1082
+ config2.workflows._init(
1083
+ getDatabase(),
1084
+ config2.workflowsConfig
1085
+ );
1086
+ serverLogger.info("Workflow engine initialized");
1087
+ }
950
1088
  }
951
1089
  function startHttpServer(app, host, port) {
952
1090
  serverLogger.debug(`Starting server on ${host}:${port}...`);
@@ -1091,31 +1229,8 @@ function createGracefulShutdown(shutdownServer, config2, shutdownState) {
1091
1229
  }
1092
1230
  };
1093
1231
  }
1094
- function handleProcessError(errorType, shutdown) {
1095
- const isProduction = env.NODE_ENV === "production";
1096
- const isDevelopment = env.NODE_ENV === "development";
1097
- if (isDevelopment || process.env.WATCH_MODE === "true") {
1098
- serverLogger.info("Exiting immediately for clean restart");
1099
- process.exit(1);
1100
- } else if (isProduction) {
1101
- serverLogger.info(`Attempting graceful shutdown after ${errorType}`);
1102
- const forceExitTimer = setTimeout(() => {
1103
- serverLogger.error(`Forced exit after ${TIMEOUTS.PRODUCTION_ERROR_SHUTDOWN}ms - graceful shutdown did not complete`);
1104
- process.exit(1);
1105
- }, TIMEOUTS.PRODUCTION_ERROR_SHUTDOWN);
1106
- shutdown(errorType).then(() => {
1107
- clearTimeout(forceExitTimer);
1108
- serverLogger.info("Graceful shutdown completed, exiting");
1109
- process.exit(0);
1110
- }).catch((shutdownError) => {
1111
- clearTimeout(forceExitTimer);
1112
- serverLogger.error("Graceful shutdown failed", shutdownError);
1113
- process.exit(1);
1114
- });
1115
- } else {
1116
- serverLogger.info("Exiting immediately");
1117
- process.exit(1);
1118
- }
1232
+ function handleProcessError(errorType) {
1233
+ serverLogger.warn(`${errorType} occurred - server continues running. Check logs above for details.`);
1119
1234
  }
1120
1235
  function registerProcessHandlers(shutdown) {
1121
1236
  if (processHandlersRegistered) {
@@ -1150,7 +1265,7 @@ function registerProcessHandlers(shutdown) {
1150
1265
  } else {
1151
1266
  serverLogger.error("Uncaught exception", error);
1152
1267
  }
1153
- handleProcessError("UNCAUGHT_EXCEPTION", shutdown);
1268
+ handleProcessError("UNCAUGHT_EXCEPTION");
1154
1269
  });
1155
1270
  process.on("unhandledRejection", (reason, promise) => {
1156
1271
  if (reason instanceof Error) {
@@ -1168,7 +1283,7 @@ function registerProcessHandlers(shutdown) {
1168
1283
  promise
1169
1284
  });
1170
1285
  }
1171
- handleProcessError("UNHANDLED_REJECTION", shutdown);
1286
+ handleProcessError("UNHANDLED_REJECTION");
1172
1287
  });
1173
1288
  serverLogger.debug("Process-level shutdown handlers registered successfully");
1174
1289
  }
@@ -1314,6 +1429,41 @@ var ServerConfigBuilder = class {
1314
1429
  }
1315
1430
  return this;
1316
1431
  }
1432
+ /**
1433
+ * Register event router for SSE (Server-Sent Events)
1434
+ *
1435
+ * Enables real-time event streaming to frontend clients.
1436
+ * Events defined with defineEvent() can be subscribed by:
1437
+ * - Backend: .subscribe() for internal handlers
1438
+ * - Jobs: .on(event) for background processing
1439
+ * - Frontend: SSE stream for real-time updates
1440
+ *
1441
+ * @example
1442
+ * ```typescript
1443
+ * import { defineEvent, defineEventRouter } from '@spfn/core/event';
1444
+ *
1445
+ * const userCreated = defineEvent('user.created', Type.Object({
1446
+ * userId: Type.String(),
1447
+ * }));
1448
+ *
1449
+ * const eventRouter = defineEventRouter({ userCreated });
1450
+ *
1451
+ * export default defineServerConfig()
1452
+ * .routes(appRouter)
1453
+ * .events(eventRouter) // → GET /events/stream
1454
+ * .build();
1455
+ *
1456
+ * // Custom path
1457
+ * .events(eventRouter, { path: '/sse' })
1458
+ * ```
1459
+ */
1460
+ events(router, config2) {
1461
+ this.config.events = router;
1462
+ if (config2) {
1463
+ this.config.eventsConfig = config2;
1464
+ }
1465
+ return this;
1466
+ }
1317
1467
  /**
1318
1468
  * Enable/disable debug mode
1319
1469
  */
@@ -1356,6 +1506,33 @@ var ServerConfigBuilder = class {
1356
1506
  this.config.infrastructure = infrastructure;
1357
1507
  return this;
1358
1508
  }
1509
+ /**
1510
+ * Register workflow router for workflow orchestration
1511
+ *
1512
+ * Automatically initializes the workflow engine after database is ready.
1513
+ *
1514
+ * @example
1515
+ * ```typescript
1516
+ * import { defineWorkflowRouter } from '@spfn/workflow';
1517
+ *
1518
+ * const workflowRouter = defineWorkflowRouter([
1519
+ * provisionTenant,
1520
+ * deprovisionTenant,
1521
+ * ]);
1522
+ *
1523
+ * export default defineServerConfig()
1524
+ * .routes(appRouter)
1525
+ * .workflows(workflowRouter)
1526
+ * .build();
1527
+ * ```
1528
+ */
1529
+ workflows(router, config2) {
1530
+ this.config.workflows = router;
1531
+ if (config2) {
1532
+ this.config.workflowsConfig = config2;
1533
+ }
1534
+ return this;
1535
+ }
1359
1536
  /**
1360
1537
  * Configure lifecycle hooks
1361
1538
  * Can be called multiple times - hooks will be executed in registration order