@spfn/core 0.2.0-beta.3 → 0.2.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config/index.d.ts +36 -0
- package/dist/config/index.js +15 -6
- package/dist/config/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.js +13 -4
- package/dist/job/index.js.map +1 -1
- package/dist/nextjs/index.d.ts +2 -2
- package/dist/nextjs/index.js +1 -1
- package/dist/nextjs/index.js.map +1 -1
- package/dist/nextjs/server.d.ts +1 -1
- package/dist/nextjs/server.js +4 -1
- package/dist/nextjs/server.js.map +1 -1
- package/dist/route/index.d.ts +11 -2
- package/dist/route/index.js +32 -6
- package/dist/route/index.js.map +1 -1
- package/dist/router-Di7ENoah.d.ts +151 -0
- package/dist/server/index.d.ts +76 -0
- package/dist/server/index.js +171 -10
- package/dist/server/index.js.map +1 -1
- package/dist/types-B-e_f2dQ.d.ts +121 -0
- package/dist/{types-BVxUIkcU.d.ts → types-D_N_U-Py.d.ts} +47 -1
- package/package.json +17 -2
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,19 +610,47 @@ 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
|
}
|
|
@@ -547,12 +664,15 @@ async function initBoss(options) {
|
|
|
547
664
|
}
|
|
548
665
|
jobLogger.info("Initializing pg-boss...");
|
|
549
666
|
bossConfig = options;
|
|
550
|
-
|
|
667
|
+
const pgBossOptions = {
|
|
551
668
|
connectionString: options.connectionString,
|
|
552
669
|
schema: options.schema ?? "spfn_queue",
|
|
553
|
-
maintenanceIntervalSeconds: options.maintenanceIntervalSeconds ?? 120
|
|
554
|
-
|
|
555
|
-
|
|
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
|
{},
|
|
@@ -1314,6 +1440,41 @@ var ServerConfigBuilder = class {
|
|
|
1314
1440
|
}
|
|
1315
1441
|
return this;
|
|
1316
1442
|
}
|
|
1443
|
+
/**
|
|
1444
|
+
* Register event router for SSE (Server-Sent Events)
|
|
1445
|
+
*
|
|
1446
|
+
* Enables real-time event streaming to frontend clients.
|
|
1447
|
+
* Events defined with defineEvent() can be subscribed by:
|
|
1448
|
+
* - Backend: .subscribe() for internal handlers
|
|
1449
|
+
* - Jobs: .on(event) for background processing
|
|
1450
|
+
* - Frontend: SSE stream for real-time updates
|
|
1451
|
+
*
|
|
1452
|
+
* @example
|
|
1453
|
+
* ```typescript
|
|
1454
|
+
* import { defineEvent, defineEventRouter } from '@spfn/core/event';
|
|
1455
|
+
*
|
|
1456
|
+
* const userCreated = defineEvent('user.created', Type.Object({
|
|
1457
|
+
* userId: Type.String(),
|
|
1458
|
+
* }));
|
|
1459
|
+
*
|
|
1460
|
+
* const eventRouter = defineEventRouter({ userCreated });
|
|
1461
|
+
*
|
|
1462
|
+
* export default defineServerConfig()
|
|
1463
|
+
* .routes(appRouter)
|
|
1464
|
+
* .events(eventRouter) // → GET /events/stream
|
|
1465
|
+
* .build();
|
|
1466
|
+
*
|
|
1467
|
+
* // Custom path
|
|
1468
|
+
* .events(eventRouter, { path: '/sse' })
|
|
1469
|
+
* ```
|
|
1470
|
+
*/
|
|
1471
|
+
events(router, config2) {
|
|
1472
|
+
this.config.events = router;
|
|
1473
|
+
if (config2) {
|
|
1474
|
+
this.config.eventsConfig = config2;
|
|
1475
|
+
}
|
|
1476
|
+
return this;
|
|
1477
|
+
}
|
|
1317
1478
|
/**
|
|
1318
1479
|
* Enable/disable debug mode
|
|
1319
1480
|
*/
|