@spfn/core 0.2.0-beta.51 → 0.2.0-beta.53
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 +72 -0
- package/dist/config/index.js +18 -1
- package/dist/config/index.js.map +1 -1
- package/dist/event/index.d.ts +3 -3
- package/dist/event/index.js +7 -2
- package/dist/event/index.js.map +1 -1
- package/dist/event/sse/client.d.ts +2 -2
- package/dist/event/sse/index.d.ts +4 -4
- package/dist/event/sse/index.js +71 -25
- package/dist/event/sse/index.js.map +1 -1
- package/dist/event/ws/client.d.ts +2 -2
- package/dist/event/ws/index.d.ts +3 -3
- package/dist/middleware/index.d.ts +109 -2
- package/dist/middleware/index.js +244 -2
- package/dist/middleware/index.js.map +1 -1
- package/dist/nextjs/server.d.ts +10 -0
- package/dist/nextjs/server.js +98 -1
- package/dist/nextjs/server.js.map +1 -1
- package/dist/server/index.d.ts +30 -4
- package/dist/server/index.js +392 -28
- package/dist/server/index.js.map +1 -1
- package/dist/{token-manager-CyG7la3p.d.ts → token-manager-C2Ag5-s8.d.ts} +6 -0
- package/dist/{types-Cfj--lfr.d.ts → types-CKsmzaB8.d.ts} +19 -1
- package/dist/{types-C1jMLGwK.d.ts → types-VpVQIsyB.d.ts} +27 -2
- package/package.json +1 -1
package/dist/config/index.d.ts
CHANGED
|
@@ -469,6 +469,30 @@ declare const coreEnvSchema: {
|
|
|
469
469
|
} & {
|
|
470
470
|
key: "RPC_PROXY_TIMEOUT";
|
|
471
471
|
};
|
|
472
|
+
SPFN_PROXY_SECRET: {
|
|
473
|
+
description: string;
|
|
474
|
+
required: boolean;
|
|
475
|
+
sensitive: boolean;
|
|
476
|
+
nextjs: boolean;
|
|
477
|
+
examples: string[];
|
|
478
|
+
} & {
|
|
479
|
+
type: "string";
|
|
480
|
+
validator: (value: string) => string;
|
|
481
|
+
} & {
|
|
482
|
+
key: "SPFN_PROXY_SECRET";
|
|
483
|
+
};
|
|
484
|
+
SPFN_PROXY_SECRET_PREVIOUS: {
|
|
485
|
+
description: string;
|
|
486
|
+
required: boolean;
|
|
487
|
+
sensitive: boolean;
|
|
488
|
+
nextjs: boolean;
|
|
489
|
+
examples: string[];
|
|
490
|
+
} & {
|
|
491
|
+
type: "string";
|
|
492
|
+
validator: (value: string) => string;
|
|
493
|
+
} & {
|
|
494
|
+
key: "SPFN_PROXY_SECRET_PREVIOUS";
|
|
495
|
+
};
|
|
472
496
|
};
|
|
473
497
|
|
|
474
498
|
/**
|
|
@@ -930,6 +954,30 @@ declare const registry: _spfn_core_env.EnvRegistry<{
|
|
|
930
954
|
} & {
|
|
931
955
|
key: "RPC_PROXY_TIMEOUT";
|
|
932
956
|
};
|
|
957
|
+
SPFN_PROXY_SECRET: {
|
|
958
|
+
description: string;
|
|
959
|
+
required: boolean;
|
|
960
|
+
sensitive: boolean;
|
|
961
|
+
nextjs: boolean;
|
|
962
|
+
examples: string[];
|
|
963
|
+
} & {
|
|
964
|
+
type: "string";
|
|
965
|
+
validator: (value: string) => string;
|
|
966
|
+
} & {
|
|
967
|
+
key: "SPFN_PROXY_SECRET";
|
|
968
|
+
};
|
|
969
|
+
SPFN_PROXY_SECRET_PREVIOUS: {
|
|
970
|
+
description: string;
|
|
971
|
+
required: boolean;
|
|
972
|
+
sensitive: boolean;
|
|
973
|
+
nextjs: boolean;
|
|
974
|
+
examples: string[];
|
|
975
|
+
} & {
|
|
976
|
+
type: "string";
|
|
977
|
+
validator: (value: string) => string;
|
|
978
|
+
} & {
|
|
979
|
+
key: "SPFN_PROXY_SECRET_PREVIOUS";
|
|
980
|
+
};
|
|
933
981
|
}>;
|
|
934
982
|
/**
|
|
935
983
|
* Validated environment configuration
|
|
@@ -1384,6 +1432,30 @@ declare const env: _spfn_core_env.InferEnvType<{
|
|
|
1384
1432
|
} & {
|
|
1385
1433
|
key: "RPC_PROXY_TIMEOUT";
|
|
1386
1434
|
};
|
|
1435
|
+
SPFN_PROXY_SECRET: {
|
|
1436
|
+
description: string;
|
|
1437
|
+
required: boolean;
|
|
1438
|
+
sensitive: boolean;
|
|
1439
|
+
nextjs: boolean;
|
|
1440
|
+
examples: string[];
|
|
1441
|
+
} & {
|
|
1442
|
+
type: "string";
|
|
1443
|
+
validator: (value: string) => string;
|
|
1444
|
+
} & {
|
|
1445
|
+
key: "SPFN_PROXY_SECRET";
|
|
1446
|
+
};
|
|
1447
|
+
SPFN_PROXY_SECRET_PREVIOUS: {
|
|
1448
|
+
description: string;
|
|
1449
|
+
required: boolean;
|
|
1450
|
+
sensitive: boolean;
|
|
1451
|
+
nextjs: boolean;
|
|
1452
|
+
examples: string[];
|
|
1453
|
+
} & {
|
|
1454
|
+
type: "string";
|
|
1455
|
+
validator: (value: string) => string;
|
|
1456
|
+
} & {
|
|
1457
|
+
key: "SPFN_PROXY_SECRET_PREVIOUS";
|
|
1458
|
+
};
|
|
1387
1459
|
}>;
|
|
1388
1460
|
|
|
1389
1461
|
export { env, coreEnvSchema as envSchema, registry };
|
package/dist/config/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineEnvSchema, envNumber, envUrl,
|
|
1
|
+
import { defineEnvSchema, envString, envNumber, envUrl, envBoolean, envEnum, parseRedisUrl, parsePostgresUrl, createEnvRegistry } from '@spfn/core/env';
|
|
2
2
|
|
|
3
3
|
// src/config/index.ts
|
|
4
4
|
var coreEnvSchema = defineEnvSchema({
|
|
@@ -285,6 +285,23 @@ var coreEnvSchema = defineEnvSchema({
|
|
|
285
285
|
default: 12e4,
|
|
286
286
|
nextjs: true,
|
|
287
287
|
examples: [6e4, 12e4, 28e4]
|
|
288
|
+
}),
|
|
289
|
+
// ========================================================================
|
|
290
|
+
// Proxy → Backend trust (HMAC signing)
|
|
291
|
+
// ========================================================================
|
|
292
|
+
SPFN_PROXY_SECRET: envString({
|
|
293
|
+
description: "Shared secret for signing proxy\u2192backend requests (HMAC-SHA256). Read by BOTH processes \u2014 the Next.js proxy (to sign) and the SPFN backend (to verify) \u2014 so it belongs in .env.local (loaded by both; the backend reads it via loadEnv, Next.js reads it server-side without exposing it to the browser). Set the SAME value on both. Leave unset to disable proxy-guard signing.",
|
|
294
|
+
required: false,
|
|
295
|
+
sensitive: true,
|
|
296
|
+
nextjs: true,
|
|
297
|
+
examples: ["<32+ byte random hex>", "v2:<32+ byte random hex>"]
|
|
298
|
+
}),
|
|
299
|
+
SPFN_PROXY_SECRET_PREVIOUS: envString({
|
|
300
|
+
description: "Previous (grace) proxy keys still accepted for verification during rotation \u2014 comma-separated <keyId>:<secret>. The proxy never signs with these; they only keep requests signed with the prior key verifying until a rollout settles. Backend-only (verification), so it belongs in .env.server, NOT exposed to the Next.js process.",
|
|
301
|
+
required: false,
|
|
302
|
+
sensitive: true,
|
|
303
|
+
nextjs: false,
|
|
304
|
+
examples: ["v1:<old secret>", "v1:<old>,v0:<older>"]
|
|
288
305
|
})
|
|
289
306
|
});
|
|
290
307
|
|
package/dist/config/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/config/schema.ts","../../src/config/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Core Package Environment Variable Schema\n *\n * Centralized schema definition for all environment variables used in @spfn/core.\n * This provides type safety, validation, and documentation for environment configuration.\n *\n * @module config/schema\n */\n\nimport {\n defineEnvSchema,\n envEnum,\n envNumber,\n envBoolean,\n envUrl,\n envString,\n parsePostgresUrl,\n parseRedisUrl,\n} from '@spfn/core/env';\n\n/**\n * Core package environment variable schema\n *\n * Defines all environment variables with:\n * - Type information\n * - Default values\n * - Validation rules\n * - Documentation\n *\n * @example\n * ```typescript\n * import { coreEnvSchema } from '@spfn/core/config';\n *\n * // Access schema information\n * console.log(coreEnvSchema.DB_POOL_MAX.description);\n * console.log(coreEnvSchema.DB_POOL_MAX.default);\n * ```\n */\nexport const coreEnvSchema = defineEnvSchema({\n // ========================================================================\n // Core Environment\n // ========================================================================\n\n NODE_ENV: envEnum(['local', 'development', 'staging', 'production', 'test'] as const, {\n description: 'Node.js runtime environment',\n default: 'local',\n nextjs: true,\n }),\n\n // ========================================================================\n // Database - Connection\n // ========================================================================\n\n DATABASE_URL: envString({\n description: 'Primary database connection URL',\n required: false,\n sensitive: true,\n validator: parsePostgresUrl,\n examples: ['postgresql://user:password@localhost:5432/dbname'],\n }),\n\n DATABASE_WRITE_URL: envString({\n description: 'Write database URL (master-replica pattern)',\n required: false,\n sensitive: true,\n validator: parsePostgresUrl,\n examples: ['postgresql://user:password@master:5432/dbname'],\n }),\n\n DATABASE_READ_URL: envString({\n description: 'Read database URL (master-replica pattern)',\n required: false,\n sensitive: true,\n validator: parsePostgresUrl,\n examples: ['postgresql://user:password@replica:5432/dbname'],\n }),\n\n // ========================================================================\n // Database - Connection Pool\n // ========================================================================\n\n DB_POOL_MAX: envNumber({\n description: 'Maximum number of database connections in pool',\n default: 10,\n examples: [10, 20, 50],\n }),\n\n DB_POOL_IDLE_TIMEOUT: envNumber({\n description: 'Database connection idle timeout in seconds',\n default: 30,\n examples: [20, 30, 60],\n }),\n\n // ========================================================================\n // Database - Retry Configuration\n // ========================================================================\n\n DB_RETRY_MAX: envNumber({\n description: 'Maximum number of database connection retry attempts',\n default: 3,\n examples: [3, 5, 10],\n }),\n\n DB_RETRY_INITIAL_DELAY: envNumber({\n description: 'Initial delay between database retry attempts (milliseconds)',\n default: 100,\n examples: [50, 100, 200],\n }),\n\n DB_RETRY_MAX_DELAY: envNumber({\n description: 'Maximum delay cap for database retry attempts (milliseconds)',\n default: 10000,\n examples: [5000, 10000, 30000],\n }),\n\n DB_RETRY_FACTOR: envNumber({\n description: 'Exponential backoff factor for database retry delays',\n default: 2,\n examples: [2, 1.5, 3],\n }),\n\n // ========================================================================\n // Database - Health Check\n // ========================================================================\n\n DB_HEALTH_CHECK_ENABLED: envBoolean({\n description: 'Enable periodic database health checks',\n default: true,\n examples: [true, false],\n }),\n\n DB_HEALTH_CHECK_INTERVAL: envNumber({\n description: 'Database health check interval (milliseconds)',\n default: 60000,\n examples: [30000, 60000, 120000],\n }),\n\n DB_HEALTH_CHECK_RECONNECT: envBoolean({\n description: 'Reconnect to database on health check failure',\n default: true,\n examples: [true, false],\n }),\n\n DB_HEALTH_CHECK_MAX_RETRIES: envNumber({\n description: 'Maximum health check retry attempts before marking as failed',\n default: 3,\n examples: [3, 5, 10],\n }),\n\n DB_HEALTH_CHECK_RETRY_INTERVAL: envNumber({\n description: 'Interval between health check retry attempts (milliseconds)',\n default: 5000,\n examples: [5000, 10000, 15000],\n }),\n\n // ========================================================================\n // Database - Monitoring\n // ========================================================================\n\n DB_MONITORING_ENABLED: envBoolean({\n description: 'Enable database query performance monitoring',\n default: false,\n examples: [true, false],\n }),\n\n DB_MONITORING_SLOW_THRESHOLD: envNumber({\n description: 'Slow query threshold for monitoring (milliseconds)',\n default: 1000,\n examples: [500, 1000, 2000],\n }),\n\n DB_MONITORING_LOG_QUERIES: envBoolean({\n description: 'Log all database queries (not just slow queries)',\n default: false,\n examples: [true, false],\n }),\n\n // ========================================================================\n // Database - Transaction\n // ========================================================================\n\n TRANSACTION_TIMEOUT: envNumber({\n description: 'Transaction timeout in milliseconds',\n default: 30000,\n examples: [10000, 30000, 60000],\n }),\n\n // ========================================================================\n // Database - Development\n // ========================================================================\n\n DB_DEBUG_TRACE: envBoolean({\n description: 'Enable detailed debug tracing for database operations',\n default: false,\n examples: [true, false],\n }),\n\n // ========================================================================\n // Drizzle ORM\n // ========================================================================\n\n DRIZZLE_SCHEMA_PATH: envString({\n description: 'Path to Drizzle schema configuration',\n required: false,\n default: './src/server/entities/config.ts',\n examples: ['./src/db/schema.ts', './src/server/entities/config.ts'],\n }),\n\n DRIZZLE_OUT_DIR: envString({\n description: 'Output directory for Drizzle migrations',\n required: false,\n default: './drizzle',\n examples: ['./drizzle', './migrations'],\n }),\n\n // ========================================================================\n // Logger - Core\n // ========================================================================\n\n SPFN_LOG_LEVEL: envEnum(['debug', 'info', 'warn', 'error', 'fatal'] as const, {\n description: 'Minimum log level to output',\n default: 'info',\n }),\n\n // ========================================================================\n // Cache (Redis/Valkey)\n // ========================================================================\n\n CACHE_URL: envString({\n description: 'Single Redis/Valkey instance URL',\n required: false,\n sensitive: true,\n validator: parseRedisUrl,\n examples: ['redis://localhost:6379', 'rediss://secure.cache.com:6380'],\n }),\n\n CACHE_WRITE_URL: envString({\n description: 'Master Redis/Valkey URL for writes (master-replica pattern)',\n required: false,\n sensitive: true,\n validator: parseRedisUrl,\n examples: ['redis://master:6379'],\n }),\n\n CACHE_READ_URL: envString({\n description: 'Replica Redis/Valkey URL for reads (master-replica pattern)',\n required: false,\n sensitive: true,\n validator: parseRedisUrl,\n examples: ['redis://replica:6379'],\n }),\n\n CACHE_SENTINEL_HOSTS: envString({\n description: 'Comma-separated Redis Sentinel hosts',\n required: false,\n examples: ['sentinel1:26379,sentinel2:26379'],\n }),\n\n CACHE_CLUSTER_NODES: envString({\n description: 'Comma-separated Redis Cluster nodes',\n required: false,\n examples: ['node1:6379,node2:6379,node3:6379'],\n }),\n\n CACHE_MASTER_NAME: envString({\n description: 'Redis Sentinel master name',\n required: false,\n examples: ['mymaster'],\n }),\n\n CACHE_PASSWORD: envString({\n description: 'Redis/Valkey authentication password',\n required: false,\n sensitive: true,\n examples: ['your-redis-password'],\n }),\n\n CACHE_TLS_REJECT_UNAUTHORIZED: envBoolean({\n description: 'Verify TLS certificates for secure Redis connections',\n default: true,\n examples: [true, false],\n }),\n\n // ========================================================================\n // Server - Core\n // ========================================================================\n\n PORT: envNumber({\n description: 'Server port number',\n default: 4000,\n examples: [3000, 4000, 8080],\n }),\n\n HOST: envString({\n description: 'Server hostname',\n default: 'localhost',\n required: false,\n examples: ['localhost', '0.0.0.0', '127.0.0.1'],\n }),\n\n // ========================================================================\n // Server - Timeout\n // ========================================================================\n\n SERVER_TIMEOUT: envNumber({\n description: 'Request timeout in milliseconds',\n default: 120000,\n examples: [60000, 120000, 300000],\n }),\n\n SERVER_KEEPALIVE_TIMEOUT: envNumber({\n description: 'Keep-alive timeout in milliseconds',\n default: 65000,\n examples: [30000, 65000, 120000],\n }),\n\n SERVER_HEADERS_TIMEOUT: envNumber({\n description: 'Headers timeout in milliseconds',\n default: 60000,\n examples: [30000, 60000, 120000],\n }),\n\n SHUTDOWN_TIMEOUT: envNumber({\n description: 'Graceful shutdown timeout in milliseconds (must be less than k8s terminationGracePeriodSeconds minus preStop sleep, with safety margin)',\n default: 280000,\n examples: [30000, 120000, 280000],\n }),\n\n // ========================================================================\n // Fetch (Node.js undici) - outbound HTTP request timeout\n // ========================================================================\n\n FETCH_CONNECT_TIMEOUT: envNumber({\n description: 'Fetch TCP connection timeout in milliseconds (time to establish socket connection to upstream server)',\n default: 10000,\n examples: [5000, 10000, 30000],\n }),\n\n FETCH_HEADERS_TIMEOUT: envNumber({\n description: 'Fetch headers timeout in milliseconds (time to receive response headers after request sent)',\n default: 300000,\n examples: [120000, 300000, 600000],\n }),\n\n FETCH_BODY_TIMEOUT: envNumber({\n description: 'Fetch body timeout in milliseconds (time between body data chunks from upstream server)',\n default: 300000,\n examples: [120000, 300000, 600000],\n }),\n\n // ========================================================================\n // Next.js Integration\n // ========================================================================\n\n SPFN_API_URL: envUrl({\n description: 'SPFN API URL (used by Next.js to call backend)',\n required: true,\n nextjs: true,\n examples: ['http://localhost:8790', 'https://api.your-app.com'],\n }),\n\n NEXT_PUBLIC_SPFN_API_URL: envUrl({\n description: 'SPFN API URL (used by Next.js to call backend)',\n required: true,\n nextjs: true,\n examples: ['http://localhost:8790', 'https://api.your-app.com'],\n }),\n\n SPFN_APP_URL: envUrl({\n description: 'Next.js application URL (used by SPFN server)',\n required: false,\n nextjs: true,\n examples: ['http://localhost:3790', 'https://your-app.com'],\n }),\n\n RPC_PROXY_TIMEOUT: envNumber({\n description: 'RPC proxy request timeout in milliseconds (AbortController timeout for proxied requests to backend, should be shorter than FETCH_HEADERS_TIMEOUT)',\n default: 120000,\n nextjs: true,\n examples: [60000, 120000, 280000],\n }),\n});\n","/**\n * Core Package Configuration\n *\n * @example\n * ```typescript\n * import { registry } from '@spfn/core/config';\n *\n * const env = registry.validate();\n * console.log(env.DB_POOL_MAX);\n * ```\n *\n * @module config\n */\n\nimport { createEnvRegistry } from '@spfn/core/env';\nimport { coreEnvSchema } from './schema';\n\n/**\n * Core environment schema\n */\nexport { coreEnvSchema as envSchema } from './schema';\n\n/**\n * Environment registry\n *\n * @example\n * ```typescript\n * // Reset for testing\n * registry.reset();\n * ```\n */\nexport const registry = createEnvRegistry(coreEnvSchema);\n\n/**\n * Validated environment configuration\n */\nexport const env = registry.validate();\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/config/schema.ts","../../src/config/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Core Package Environment Variable Schema\n *\n * Centralized schema definition for all environment variables used in @spfn/core.\n * This provides type safety, validation, and documentation for environment configuration.\n *\n * @module config/schema\n */\n\nimport {\n defineEnvSchema,\n envEnum,\n envNumber,\n envBoolean,\n envUrl,\n envString,\n parsePostgresUrl,\n parseRedisUrl,\n} from '@spfn/core/env';\n\n/**\n * Core package environment variable schema\n *\n * Defines all environment variables with:\n * - Type information\n * - Default values\n * - Validation rules\n * - Documentation\n *\n * @example\n * ```typescript\n * import { coreEnvSchema } from '@spfn/core/config';\n *\n * // Access schema information\n * console.log(coreEnvSchema.DB_POOL_MAX.description);\n * console.log(coreEnvSchema.DB_POOL_MAX.default);\n * ```\n */\nexport const coreEnvSchema = defineEnvSchema({\n // ========================================================================\n // Core Environment\n // ========================================================================\n\n NODE_ENV: envEnum(['local', 'development', 'staging', 'production', 'test'] as const, {\n description: 'Node.js runtime environment',\n default: 'local',\n nextjs: true,\n }),\n\n // ========================================================================\n // Database - Connection\n // ========================================================================\n\n DATABASE_URL: envString({\n description: 'Primary database connection URL',\n required: false,\n sensitive: true,\n validator: parsePostgresUrl,\n examples: ['postgresql://user:password@localhost:5432/dbname'],\n }),\n\n DATABASE_WRITE_URL: envString({\n description: 'Write database URL (master-replica pattern)',\n required: false,\n sensitive: true,\n validator: parsePostgresUrl,\n examples: ['postgresql://user:password@master:5432/dbname'],\n }),\n\n DATABASE_READ_URL: envString({\n description: 'Read database URL (master-replica pattern)',\n required: false,\n sensitive: true,\n validator: parsePostgresUrl,\n examples: ['postgresql://user:password@replica:5432/dbname'],\n }),\n\n // ========================================================================\n // Database - Connection Pool\n // ========================================================================\n\n DB_POOL_MAX: envNumber({\n description: 'Maximum number of database connections in pool',\n default: 10,\n examples: [10, 20, 50],\n }),\n\n DB_POOL_IDLE_TIMEOUT: envNumber({\n description: 'Database connection idle timeout in seconds',\n default: 30,\n examples: [20, 30, 60],\n }),\n\n // ========================================================================\n // Database - Retry Configuration\n // ========================================================================\n\n DB_RETRY_MAX: envNumber({\n description: 'Maximum number of database connection retry attempts',\n default: 3,\n examples: [3, 5, 10],\n }),\n\n DB_RETRY_INITIAL_DELAY: envNumber({\n description: 'Initial delay between database retry attempts (milliseconds)',\n default: 100,\n examples: [50, 100, 200],\n }),\n\n DB_RETRY_MAX_DELAY: envNumber({\n description: 'Maximum delay cap for database retry attempts (milliseconds)',\n default: 10000,\n examples: [5000, 10000, 30000],\n }),\n\n DB_RETRY_FACTOR: envNumber({\n description: 'Exponential backoff factor for database retry delays',\n default: 2,\n examples: [2, 1.5, 3],\n }),\n\n // ========================================================================\n // Database - Health Check\n // ========================================================================\n\n DB_HEALTH_CHECK_ENABLED: envBoolean({\n description: 'Enable periodic database health checks',\n default: true,\n examples: [true, false],\n }),\n\n DB_HEALTH_CHECK_INTERVAL: envNumber({\n description: 'Database health check interval (milliseconds)',\n default: 60000,\n examples: [30000, 60000, 120000],\n }),\n\n DB_HEALTH_CHECK_RECONNECT: envBoolean({\n description: 'Reconnect to database on health check failure',\n default: true,\n examples: [true, false],\n }),\n\n DB_HEALTH_CHECK_MAX_RETRIES: envNumber({\n description: 'Maximum health check retry attempts before marking as failed',\n default: 3,\n examples: [3, 5, 10],\n }),\n\n DB_HEALTH_CHECK_RETRY_INTERVAL: envNumber({\n description: 'Interval between health check retry attempts (milliseconds)',\n default: 5000,\n examples: [5000, 10000, 15000],\n }),\n\n // ========================================================================\n // Database - Monitoring\n // ========================================================================\n\n DB_MONITORING_ENABLED: envBoolean({\n description: 'Enable database query performance monitoring',\n default: false,\n examples: [true, false],\n }),\n\n DB_MONITORING_SLOW_THRESHOLD: envNumber({\n description: 'Slow query threshold for monitoring (milliseconds)',\n default: 1000,\n examples: [500, 1000, 2000],\n }),\n\n DB_MONITORING_LOG_QUERIES: envBoolean({\n description: 'Log all database queries (not just slow queries)',\n default: false,\n examples: [true, false],\n }),\n\n // ========================================================================\n // Database - Transaction\n // ========================================================================\n\n TRANSACTION_TIMEOUT: envNumber({\n description: 'Transaction timeout in milliseconds',\n default: 30000,\n examples: [10000, 30000, 60000],\n }),\n\n // ========================================================================\n // Database - Development\n // ========================================================================\n\n DB_DEBUG_TRACE: envBoolean({\n description: 'Enable detailed debug tracing for database operations',\n default: false,\n examples: [true, false],\n }),\n\n // ========================================================================\n // Drizzle ORM\n // ========================================================================\n\n DRIZZLE_SCHEMA_PATH: envString({\n description: 'Path to Drizzle schema configuration',\n required: false,\n default: './src/server/entities/config.ts',\n examples: ['./src/db/schema.ts', './src/server/entities/config.ts'],\n }),\n\n DRIZZLE_OUT_DIR: envString({\n description: 'Output directory for Drizzle migrations',\n required: false,\n default: './drizzle',\n examples: ['./drizzle', './migrations'],\n }),\n\n // ========================================================================\n // Logger - Core\n // ========================================================================\n\n SPFN_LOG_LEVEL: envEnum(['debug', 'info', 'warn', 'error', 'fatal'] as const, {\n description: 'Minimum log level to output',\n default: 'info',\n }),\n\n // ========================================================================\n // Cache (Redis/Valkey)\n // ========================================================================\n\n CACHE_URL: envString({\n description: 'Single Redis/Valkey instance URL',\n required: false,\n sensitive: true,\n validator: parseRedisUrl,\n examples: ['redis://localhost:6379', 'rediss://secure.cache.com:6380'],\n }),\n\n CACHE_WRITE_URL: envString({\n description: 'Master Redis/Valkey URL for writes (master-replica pattern)',\n required: false,\n sensitive: true,\n validator: parseRedisUrl,\n examples: ['redis://master:6379'],\n }),\n\n CACHE_READ_URL: envString({\n description: 'Replica Redis/Valkey URL for reads (master-replica pattern)',\n required: false,\n sensitive: true,\n validator: parseRedisUrl,\n examples: ['redis://replica:6379'],\n }),\n\n CACHE_SENTINEL_HOSTS: envString({\n description: 'Comma-separated Redis Sentinel hosts',\n required: false,\n examples: ['sentinel1:26379,sentinel2:26379'],\n }),\n\n CACHE_CLUSTER_NODES: envString({\n description: 'Comma-separated Redis Cluster nodes',\n required: false,\n examples: ['node1:6379,node2:6379,node3:6379'],\n }),\n\n CACHE_MASTER_NAME: envString({\n description: 'Redis Sentinel master name',\n required: false,\n examples: ['mymaster'],\n }),\n\n CACHE_PASSWORD: envString({\n description: 'Redis/Valkey authentication password',\n required: false,\n sensitive: true,\n examples: ['your-redis-password'],\n }),\n\n CACHE_TLS_REJECT_UNAUTHORIZED: envBoolean({\n description: 'Verify TLS certificates for secure Redis connections',\n default: true,\n examples: [true, false],\n }),\n\n // ========================================================================\n // Server - Core\n // ========================================================================\n\n PORT: envNumber({\n description: 'Server port number',\n default: 4000,\n examples: [3000, 4000, 8080],\n }),\n\n HOST: envString({\n description: 'Server hostname',\n default: 'localhost',\n required: false,\n examples: ['localhost', '0.0.0.0', '127.0.0.1'],\n }),\n\n // ========================================================================\n // Server - Timeout\n // ========================================================================\n\n SERVER_TIMEOUT: envNumber({\n description: 'Request timeout in milliseconds',\n default: 120000,\n examples: [60000, 120000, 300000],\n }),\n\n SERVER_KEEPALIVE_TIMEOUT: envNumber({\n description: 'Keep-alive timeout in milliseconds',\n default: 65000,\n examples: [30000, 65000, 120000],\n }),\n\n SERVER_HEADERS_TIMEOUT: envNumber({\n description: 'Headers timeout in milliseconds',\n default: 60000,\n examples: [30000, 60000, 120000],\n }),\n\n SHUTDOWN_TIMEOUT: envNumber({\n description: 'Graceful shutdown timeout in milliseconds (must be less than k8s terminationGracePeriodSeconds minus preStop sleep, with safety margin)',\n default: 280000,\n examples: [30000, 120000, 280000],\n }),\n\n // ========================================================================\n // Fetch (Node.js undici) - outbound HTTP request timeout\n // ========================================================================\n\n FETCH_CONNECT_TIMEOUT: envNumber({\n description: 'Fetch TCP connection timeout in milliseconds (time to establish socket connection to upstream server)',\n default: 10000,\n examples: [5000, 10000, 30000],\n }),\n\n FETCH_HEADERS_TIMEOUT: envNumber({\n description: 'Fetch headers timeout in milliseconds (time to receive response headers after request sent)',\n default: 300000,\n examples: [120000, 300000, 600000],\n }),\n\n FETCH_BODY_TIMEOUT: envNumber({\n description: 'Fetch body timeout in milliseconds (time between body data chunks from upstream server)',\n default: 300000,\n examples: [120000, 300000, 600000],\n }),\n\n // ========================================================================\n // Next.js Integration\n // ========================================================================\n\n SPFN_API_URL: envUrl({\n description: 'SPFN API URL (used by Next.js to call backend)',\n required: true,\n nextjs: true,\n examples: ['http://localhost:8790', 'https://api.your-app.com'],\n }),\n\n NEXT_PUBLIC_SPFN_API_URL: envUrl({\n description: 'SPFN API URL (used by Next.js to call backend)',\n required: true,\n nextjs: true,\n examples: ['http://localhost:8790', 'https://api.your-app.com'],\n }),\n\n SPFN_APP_URL: envUrl({\n description: 'Next.js application URL (used by SPFN server)',\n required: false,\n nextjs: true,\n examples: ['http://localhost:3790', 'https://your-app.com'],\n }),\n\n RPC_PROXY_TIMEOUT: envNumber({\n description: 'RPC proxy request timeout in milliseconds (AbortController timeout for proxied requests to backend, should be shorter than FETCH_HEADERS_TIMEOUT)',\n default: 120000,\n nextjs: true,\n examples: [60000, 120000, 280000],\n }),\n\n // ========================================================================\n // Proxy → Backend trust (HMAC signing)\n // ========================================================================\n\n SPFN_PROXY_SECRET: envString({\n description: 'Shared secret for signing proxy→backend requests (HMAC-SHA256). Read by BOTH processes — the Next.js proxy (to sign) and the SPFN backend (to verify) — so it belongs in .env.local (loaded by both; the backend reads it via loadEnv, Next.js reads it server-side without exposing it to the browser). Set the SAME value on both. Leave unset to disable proxy-guard signing.',\n required: false,\n sensitive: true,\n nextjs: true,\n examples: ['<32+ byte random hex>', 'v2:<32+ byte random hex>'],\n }),\n\n SPFN_PROXY_SECRET_PREVIOUS: envString({\n description: 'Previous (grace) proxy keys still accepted for verification during rotation — comma-separated <keyId>:<secret>. The proxy never signs with these; they only keep requests signed with the prior key verifying until a rollout settles. Backend-only (verification), so it belongs in .env.server, NOT exposed to the Next.js process.',\n required: false,\n sensitive: true,\n nextjs: false,\n examples: ['v1:<old secret>', 'v1:<old>,v0:<older>'],\n }),\n});\n","/**\n * Core Package Configuration\n *\n * @example\n * ```typescript\n * import { registry } from '@spfn/core/config';\n *\n * const env = registry.validate();\n * console.log(env.DB_POOL_MAX);\n * ```\n *\n * @module config\n */\n\nimport { createEnvRegistry } from '@spfn/core/env';\nimport { coreEnvSchema } from './schema';\n\n/**\n * Core environment schema\n */\nexport { coreEnvSchema as envSchema } from './schema';\n\n/**\n * Environment registry\n *\n * @example\n * ```typescript\n * // Reset for testing\n * registry.reset();\n * ```\n */\nexport const registry = createEnvRegistry(coreEnvSchema);\n\n/**\n * Validated environment configuration\n */\nexport const env = registry.validate();\n"]}
|
package/dist/event/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { TSchema, Static } from '@sinclair/typebox';
|
|
2
|
-
import { a as EventDef } from '../token-manager-
|
|
3
|
-
export { e as EventHandler, E as EventRouterDef, I as InferEventNames, f as InferEventPayload, c as InferEventPayloads, b as InferRouterEventPayload, J as JobQueueSender, P as PubSubCache, d as defineEventRouter } from '../token-manager-
|
|
2
|
+
import { a as EventDef } from '../token-manager-C2Ag5-s8.js';
|
|
3
|
+
export { e as EventHandler, E as EventRouterDef, I as InferEventNames, f as InferEventPayload, c as InferEventPayloads, b as InferRouterEventPayload, J as JobQueueSender, P as PubSubCache, d as defineEventRouter } from '../token-manager-C2Ag5-s8.js';
|
|
4
4
|
export { defineWSRouter } from './ws/index.js';
|
|
5
|
-
import '../types-
|
|
5
|
+
import '../types-CKsmzaB8.js';
|
|
6
6
|
import 'hono';
|
|
7
7
|
import 'node:http';
|
|
8
8
|
|
package/dist/event/index.js
CHANGED
|
@@ -89,15 +89,19 @@ function createEventImpl(name, schema) {
|
|
|
89
89
|
eventLogger.warn(`Cache already configured for event: ${name}`);
|
|
90
90
|
return self;
|
|
91
91
|
}
|
|
92
|
-
cache = newCache;
|
|
93
|
-
cacheSubscribed = true;
|
|
94
92
|
await newCache.subscribe(name, async (message) => {
|
|
95
93
|
eventLogger.debug(`Received event from cache: ${name}`);
|
|
96
94
|
await handlerManager.trigger(message);
|
|
97
95
|
});
|
|
96
|
+
cache = newCache;
|
|
97
|
+
cacheSubscribed = true;
|
|
98
98
|
eventLogger.debug(`Cache subscription ready for event: ${name}`);
|
|
99
99
|
return self;
|
|
100
100
|
};
|
|
101
|
+
const resetCache = () => {
|
|
102
|
+
cache = void 0;
|
|
103
|
+
cacheSubscribed = false;
|
|
104
|
+
};
|
|
101
105
|
const self = {
|
|
102
106
|
name,
|
|
103
107
|
schema,
|
|
@@ -106,6 +110,7 @@ function createEventImpl(name, schema) {
|
|
|
106
110
|
emit,
|
|
107
111
|
useCache,
|
|
108
112
|
_registerJobQueue: jobQueueManager.register,
|
|
113
|
+
_resetCache: resetCache,
|
|
109
114
|
_payload: void 0
|
|
110
115
|
};
|
|
111
116
|
return self;
|
package/dist/event/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/event/event.ts","../../src/event/sse/route-map.ts","../../src/event/router.ts","../../src/event/ws/handler.ts","../../src/event/ws/index.ts"],"names":["logger"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Event System\n *\n * Decoupled pub/sub event system with optional cache integration for multi-instance support\n *\n * @example\n * ```typescript\n * // Define event\n * const userCreated = defineEvent('user.created', Type.Object({\n * userId: Type.String(),\n * }));\n *\n * // Subscribe (in-memory)\n * userCreated.subscribe((payload) => {\n * console.log('User created:', payload.userId);\n * });\n *\n * // Emit\n * await userCreated.emit({ userId: '123' });\n *\n * // With cache for multi-instance\n * const event = defineEvent('user.created', schema);\n * await event.useCache(cache); // Must await before emitting\n * await event.emit({ userId: '123' }); // Broadcast to all instances\n * ```\n */\n\nimport type { TSchema, Static } from '@sinclair/typebox';\nimport { logger } from '@spfn/core/logger';\nimport type { EventDef, EventHandler, JobQueueSender, PubSubCache } from './types';\n\nconst eventLogger = logger.child('@spfn/core:event');\n\n/**\n * Log handler error with consistent format\n */\nfunction logHandlerError(eventName: string, error: unknown): void\n{\n eventLogger.error(`Event handler error: ${eventName}`, {\n error: error instanceof Error ? error.message : String(error),\n });\n}\n\n/**\n * Log job queue error with consistent format\n */\nfunction logJobQueueError(queueName: string, error: unknown): void\n{\n eventLogger.error(`Failed to send event to job queue: ${queueName}`, {\n error: error instanceof Error ? error.message : String(error),\n });\n}\n\n/**\n * Create handler subscription manager\n */\nfunction createHandlerManager<TPayload>(name: string)\n{\n const handlers: Set<EventHandler<TPayload>> = new Set();\n\n return {\n add: (handler: EventHandler<TPayload>): (() => void) =>\n {\n handlers.add(handler);\n eventLogger.debug(`Subscribed to event: ${name}`, { handlerCount: handlers.size });\n\n return () =>\n {\n handlers.delete(handler);\n eventLogger.debug(`Unsubscribed from event: ${name}`, { handlerCount: handlers.size });\n };\n },\n\n clear: (): void =>\n {\n handlers.clear();\n eventLogger.debug(`Unsubscribed all from event: ${name}`);\n },\n\n trigger: async (payload: TPayload): Promise<void> =>\n {\n const results = await Promise.allSettled(\n [...handlers].map((handler) => handler(payload)),\n );\n\n for (const result of results)\n {\n if (result.status === 'rejected')\n {\n logHandlerError(name, result.reason);\n }\n }\n },\n };\n}\n\n/**\n * Create job queue manager\n */\nfunction createJobQueueManager(name: string)\n{\n const jobQueues: Map<string, JobQueueSender> = new Map();\n\n return {\n register: (queueName: string, sender: JobQueueSender): void =>\n {\n jobQueues.set(queueName, sender);\n eventLogger.debug(`Registered job queue for event: ${name}`, { queueName });\n },\n\n send: async (payload: unknown): Promise<void> =>\n {\n if (jobQueues.size === 0)\n {\n return;\n }\n\n const entries = [...jobQueues.entries()];\n const results = await Promise.allSettled(\n entries.map(([queueName, sender]) => sender(queueName, payload)),\n );\n\n for (const [i, result] of results.entries())\n {\n if (result.status === 'rejected')\n {\n logJobQueueError(entries[i][0], result.reason);\n }\n }\n },\n\n get size(): number\n {\n return jobQueues.size;\n },\n };\n}\n\n/**\n * Internal: Create event implementation\n */\nfunction createEventImpl<TPayload>(\n name: string,\n schema?: TSchema,\n): EventDef<TPayload>\n{\n const handlerManager = createHandlerManager<TPayload>(name);\n const jobQueueManager = createJobQueueManager(name);\n let cache: PubSubCache | undefined;\n let cacheSubscribed = false;\n\n const emit = async (payload?: TPayload): Promise<void> =>\n {\n eventLogger.debug(`Emitting event: ${name}`, {\n payload,\n hasCache: !!cache,\n jobQueueCount: jobQueueManager.size,\n });\n\n if (cache)\n {\n await cache.publish(name, payload);\n }\n else\n {\n await handlerManager.trigger(payload as TPayload);\n }\n\n await jobQueueManager.send(payload);\n eventLogger.debug(`Event emitted: ${name}`);\n };\n\n const useCache = async (newCache: PubSubCache): Promise<EventDef<TPayload>> =>\n {\n if (cacheSubscribed)\n {\n eventLogger.warn(`Cache already configured for event: ${name}`);\n\n return self;\n }\n\n cache = newCache;\n cacheSubscribed = true;\n\n await newCache.subscribe(name, async (message: unknown) =>\n {\n eventLogger.debug(`Received event from cache: ${name}`);\n await handlerManager.trigger(message as TPayload);\n });\n\n eventLogger.debug(`Cache subscription ready for event: ${name}`);\n\n return self;\n };\n\n const self: EventDef<TPayload> = {\n name,\n schema,\n subscribe: handlerManager.add,\n unsubscribeAll: handlerManager.clear,\n emit: emit as EventDef<TPayload>['emit'],\n useCache,\n _registerJobQueue: jobQueueManager.register,\n _payload: undefined as unknown as TPayload,\n };\n\n return self;\n}\n\n/**\n * Define an event without payload\n */\nexport function defineEvent(name: string): EventDef<void>;\n\n/**\n * Define an event with typed payload\n */\nexport function defineEvent<T extends TSchema>(\n name: string,\n schema: T,\n): EventDef<Static<T>>;\n\n/**\n * Define an event for decoupled pub/sub\n *\n * @example\n * ```typescript\n * // Define event with payload\n * export const userCreated = defineEvent('user.created', Type.Object({\n * userId: Type.String(),\n * }));\n *\n * // Subscribe to event (in-memory)\n * const unsubscribe = userCreated.subscribe((payload) => {\n * console.log('User created:', payload.userId);\n * });\n *\n * // Emit event\n * await userCreated.emit({ userId: '123' });\n *\n * // Unsubscribe when done\n * unsubscribe();\n *\n * // Multi-instance with cache\n * await userCreated.useCache(cache);\n * await userCreated.emit({ userId: '123' }); // Broadcast to all instances\n * ```\n */\nexport function defineEvent<T extends TSchema>(\n name: string,\n schema?: T,\n): EventDef<Static<T>> | EventDef\n{\n if (schema)\n {\n return createEventImpl<Static<T>>(name, schema);\n }\n\n return createEventImpl<void>(name);\n}\n","/**\n * SSE Event Route Map\n *\n * Static route map for SSE token endpoint.\n * Merge into RPC proxy routeMap so `eventsToken` resolves to `POST /events/token`.\n *\n * @example\n * ```typescript\n * // app/api/rpc/[routeName]/route.ts\n * import { createRpcProxy } from '@spfn/core/nextjs/server';\n * import { eventRouteMap } from '@spfn/core/event';\n * import { authRouteMap } from '@spfn/auth';\n * import { routeMap } from '@/generated/route-map';\n *\n * export const { GET, POST } = createRpcProxy({\n * routeMap: { ...routeMap, ...authRouteMap, ...eventRouteMap },\n * });\n * ```\n */\nexport const eventRouteMap = {\n eventsToken: { method: 'POST' as const, path: '/events/token' },\n};\n","/**\n * Event Router\n *\n * Type-safe event router for SSE subscription\n *\n * @example\n * ```typescript\n * import { defineEvent, defineEventRouter } from '@spfn/core/event';\n * import { Type } from '@sinclair/typebox';\n *\n * const userCreated = defineEvent('user.created', Type.Object({\n * userId: Type.String(),\n * }));\n *\n * const orderPlaced = defineEvent('order.placed', Type.Object({\n * orderId: Type.String(),\n * amount: Type.Number(),\n * }));\n *\n * export const eventRouter = defineEventRouter({\n * userCreated,\n * orderPlaced,\n * });\n *\n * export type EventRouter = typeof eventRouter;\n * ```\n */\n\nimport type { EventDef } from './types';\n\n/**\n * Event Router Definition\n */\nexport interface EventRouterDef<TEvents extends Record<string, EventDef<any>>>\n{\n /**\n * Event definitions\n */\n readonly events: TEvents;\n\n /**\n * Event names as array\n */\n readonly eventNames: (keyof TEvents)[];\n\n /**\n * Type inference helper - payload types by event name\n */\n readonly _types: {\n [K in keyof TEvents]: TEvents[K]['_payload'];\n };\n}\n\n/**\n * Infer event names from EventRouter\n */\nexport type InferEventNames<T> = T extends EventRouterDef<infer E>\n ? keyof E & string\n : never;\n\n/**\n * Infer payload type for specific event\n */\nexport type InferEventPayload<\n T extends EventRouterDef<any>,\n K extends InferEventNames<T>,\n> = T['_types'][K];\n\n/**\n * Infer all event payloads map\n */\nexport type InferEventPayloads<T extends EventRouterDef<any>> = T['_types'];\n\n/**\n * Define an event router for SSE subscription\n *\n * @example\n * ```typescript\n * export const eventRouter = defineEventRouter({\n * userCreated,\n * orderPlaced,\n * });\n *\n * // Type inference\n * type Names = InferEventNames<typeof eventRouter>;\n * // 'userCreated' | 'orderPlaced'\n *\n * type Payload = InferEventPayload<typeof eventRouter, 'userCreated'>;\n * // { userId: string }\n * ```\n */\nexport function defineEventRouter<\n TEvents extends Record<string, EventDef<any>>,\n>(events: TEvents): EventRouterDef<TEvents>\n{\n return {\n events,\n eventNames: Object.keys(events) as (keyof TEvents)[],\n _types: {} as EventRouterDef<TEvents>['_types'],\n };\n}\n","/**\n * WebSocket Handler\n *\n * Attaches a WebSocket server to an existing Node.js http.Server.\n * Handles authentication, event subscription, and client message routing.\n */\n\nimport type { Server } from 'node:http';\nimport type { EventDef } from '../types';\nimport type {\n WSRouterDef,\n WSHandlerConfig,\n WSHandlerAuthConfig,\n WSMessageHandlers,\n WSRawConnection,\n} from './types';\nimport type { SSETokenManager } from '../sse/token-manager';\nimport { logger } from '@spfn/core/logger';\n\nconst wsLogger = logger.child('@spfn/core:ws');\n\n// ============================================================================\n// Public API\n// ============================================================================\n\n/**\n * Attach a WebSocket server to a Node.js http.Server\n *\n * @returns cleanup function that closes the WebSocket server\n */\nexport async function attachWSHandler<\n TEvents extends Record<string, EventDef<any>>,\n TMessages extends WSMessageHandlers,\n>(\n server: Server,\n router: WSRouterDef<TEvents, TMessages>,\n config: WSHandlerConfig & { path?: string } = {},\n tokenManager?: SSETokenManager,\n): Promise<() => Promise<void>>\n{\n const WebSocketServer = await loadWSServer();\n\n const {\n pingInterval = 30000,\n path = '/ws',\n auth: authConfig,\n } = config;\n\n if (authConfig?.enabled && !tokenManager)\n {\n throw new Error(\n 'WebSocket auth.enabled=true requires a tokenManager. ' +\n 'Pass tokenManager or use .websockets(router, { auth: { enabled: true } }) via startServer.',\n );\n }\n\n const wss = new WebSocketServer({ server, path });\n\n // Track live connections for graceful shutdown\n const clients = new Set<any>();\n\n wss.on('connection', (ws: any, req: any) =>\n {\n clients.add(ws);\n ws.on('close', () => clients.delete(ws));\n handleConnection(ws, req, router, authConfig, tokenManager, pingInterval)\n .catch((err: Error) =>\n {\n wsLogger.error('WebSocket connection handler error', err);\n if (ws.readyState === 1) ws.close(1011, 'Internal server error');\n });\n });\n\n wss.on('error', (err: Error) =>\n {\n wsLogger.error('WebSocket server error', err);\n });\n\n wsLogger.info(`✓ WebSocket endpoint registered at ${path}`, {\n events: router.eventNames,\n auth: !!authConfig?.enabled,\n });\n\n return () => new Promise<void>((resolve, reject) =>\n {\n // Close all existing connections with 1001 Going Away\n for (const client of clients)\n {\n client.close(1001, 'Server shutting down');\n }\n clients.clear();\n\n wss.close((err?: Error) =>\n {\n if (err) reject(err);\n else resolve();\n });\n });\n}\n\n// ============================================================================\n// Connection Handler\n// ============================================================================\n\nasync function handleConnection(\n ws: any,\n req: any,\n router: WSRouterDef<any, any>,\n authConfig: WSHandlerAuthConfig | undefined,\n tokenManager: SSETokenManager | undefined,\n pingInterval: number,\n): Promise<void>\n{\n // Register close handler before any await — ensures we never miss the event even during auth\n let pingTimer: ReturnType<typeof setInterval> | undefined;\n let connectionUnsubscribes: (() => void)[] = [];\n let subscribedEvents: string[] = [];\n ws.on('close', () =>\n {\n clearInterval(pingTimer);\n connectionUnsubscribes.forEach(fn => fn());\n if (subscribedEvents.length > 0)\n wsLogger.info('WebSocket connection closed', { events: subscribedEvents });\n });\n\n const url = parseURL(req);\n if (!url)\n {\n ws.close(1002, 'Invalid request URL');\n\n return;\n }\n\n // ── 1. Authenticate ──\n const subject = await resolveSubject(url, authConfig?.enabled ? tokenManager : undefined);\n if (subject === false)\n {\n ws.close(4001, 'Missing token');\n\n return;\n }\n if (subject === null)\n {\n ws.close(4001, 'Invalid or expired token');\n\n return;\n }\n\n // ── 2. Resolve subscribed events ──\n const requestedEvents = parseRequestedEvents(url, router.eventNames as string[]);\n if (requestedEvents.length === 0)\n {\n ws.close(4000, 'No valid event names specified');\n\n return;\n }\n\n // ── 3. Authorize ──\n const allowedEvents = await resolveAllowedEvents(subject, requestedEvents, authConfig);\n if (allowedEvents === null)\n {\n ws.close(4003, 'Not authorized for any requested events');\n\n return;\n }\n\n subscribedEvents = allowedEvents;\n wsLogger.info('WebSocket connection established', {\n events: allowedEvents,\n subject: subject ?? undefined,\n });\n\n // ── 4. Build connection wrapper ──\n const connection = createConnection(ws);\n\n // ── 5. Subscribe to server-push events ──\n connectionUnsubscribes = subscribeEvents(ws, router, allowedEvents, subject, authConfig);\n\n // If socket closed during auth awaits, clean up and bail\n if (ws.readyState !== 1)\n {\n connectionUnsubscribes.forEach(fn => fn());\n connectionUnsubscribes = [];\n\n return;\n }\n\n // ── 6. Handle incoming messages ──\n ws.on('message', (data: Buffer | string) =>\n {\n onClientMessage(data, router, connection, subject)\n .catch((err: Error) => wsLogger.error('Unhandled message error', err));\n });\n\n // ── 7. Keep-alive ping ──\n if (pingInterval > 0)\n {\n pingTimer = setInterval(() =>\n {\n if (ws.readyState === 1) ws.ping();\n }, pingInterval);\n }\n\n // ── 9. Send connected ack ──\n connection.send('__connected', {\n subscribedEvents: allowedEvents,\n timestamp: Date.now(),\n });\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction parseURL(req: any): URL | null\n{\n try\n {\n return new URL(req.url ?? '/', 'ws://localhost');\n }\n catch\n {\n return null;\n }\n}\n\n/**\n * Resolve subject from token\n * - undefined: no auth required\n * - false: token param missing (when required)\n * - null: token invalid/expired\n * - string: authenticated subject\n */\nasync function resolveSubject(\n url: URL,\n tokenManager?: SSETokenManager,\n): Promise<string | undefined | false | null>\n{\n if (!tokenManager)\n {\n return undefined;\n }\n\n const token = url.searchParams.get('token');\n if (!token)\n {\n return false;\n }\n\n return await tokenManager.verify(token);\n}\n\nfunction parseRequestedEvents(url: URL, validEventNames: string[]): string[]\n{\n const eventsParam = url.searchParams.get('events');\n if (!eventsParam)\n {\n return [];\n }\n\n return eventsParam\n .split(',')\n .map(e => e.trim())\n .filter(e => validEventNames.includes(e));\n}\n\nasync function resolveAllowedEvents(\n subject: string | undefined,\n requestedEvents: string[],\n authConfig?: WSHandlerAuthConfig,\n): Promise<string[] | null>\n{\n if (!subject || !authConfig?.authorize)\n {\n return requestedEvents;\n }\n\n const allowed = await authConfig.authorize(subject, requestedEvents);\n\n return allowed.length === 0 ? null : allowed;\n}\n\nfunction createConnection(ws: any): WSRawConnection\n{\n return {\n send: (type, payload) =>\n {\n if (ws.readyState !== 1) return;\n ws.send(JSON.stringify({ type, data: payload }));\n },\n close: (code, reason) => ws.close(code, reason),\n };\n}\n\nfunction subscribeEvents(\n ws: any,\n router: WSRouterDef<any, any>,\n allowedEvents: string[],\n subject: string | undefined,\n authConfig?: WSHandlerAuthConfig,\n): (() => void)[]\n{\n const unsubscribes: (() => void)[] = [];\n\n for (const eventName of allowedEvents)\n {\n const eventDef = router.events[eventName];\n if (!eventDef) continue;\n\n const unsubscribe = eventDef.subscribe((payload: unknown) =>\n {\n if (ws.readyState !== 1) return;\n\n if (subject && authConfig?.filter?.[eventName])\n {\n if (!authConfig.filter[eventName](subject, payload)) return;\n }\n\n try\n {\n ws.send(JSON.stringify({ type: eventName, data: payload }));\n }\n catch\n {\n // Socket closed between readyState check and send — ignore\n }\n });\n\n unsubscribes.push(unsubscribe);\n }\n\n return unsubscribes;\n}\n\nasync function onClientMessage(\n data: Buffer | string,\n router: WSRouterDef<any, any>,\n connection: WSRawConnection,\n subject: string | undefined,\n): Promise<void>\n{\n let message: { type?: string; data?: unknown };\n\n try\n {\n message = JSON.parse(data.toString());\n }\n catch\n {\n return;\n }\n\n const { type, data: payload } = message;\n if (!type) return;\n\n const handler = router.messages[type];\n if (!handler) return;\n\n try\n {\n await handler({ payload, subject, ws: connection });\n }\n catch (err)\n {\n wsLogger.error(`WebSocket message handler error: ${type}`, err as Error);\n }\n}\n\n// ============================================================================\n// Dynamic import for optional 'ws' dependency\n// ============================================================================\n\nasync function loadWSServer(): Promise<any>\n{\n try\n {\n // ws is a CJS package: module.exports = WebSocket, WebSocket.WebSocketServer is set on it.\n // ESM dynamic import wraps CJS default export under .default\n \n const mod = await import('ws') as any;\n const WS = mod.default ?? mod;\n const WSS = WS.WebSocketServer ?? WS.Server;\n\n if (typeof WSS !== 'function')\n {\n throw new Error(\n 'WebSocketServer not found in ws module. ' +\n 'Ensure ws@^8 is installed: pnpm add ws',\n );\n }\n\n return WSS;\n }\n catch (err)\n {\n if (err instanceof Error && err.message.includes('WebSocketServer not found'))\n {\n throw err;\n }\n throw new Error(\n '@spfn/core WebSocket support requires the \"ws\" package.\\n' +\n 'Install it with: pnpm add ws',\n );\n }\n}\n","/**\n * WebSocket Module\n *\n * Type-safe WebSocket server with event-based pub/sub and bidirectional messaging.\n *\n * @example Server setup\n * ```typescript\n * // src/server/ws.ts\n * import { defineWSRouter } from '@spfn/core/event/ws';\n * import { defineEvent } from '@spfn/core/event';\n * import { Type } from '@sinclair/typebox';\n *\n * const userUpdated = defineEvent('user.updated', Type.Object({ userId: Type.String() }));\n * const notification = defineEvent('notification', Type.Object({ message: Type.String() }));\n *\n * export const wsRouter = defineWSRouter({\n * events: { userUpdated, notification },\n * messages: {\n * ping: ({ ws }) => ws.send('pong', {}),\n * },\n * });\n *\n * export type WSRouter = typeof wsRouter;\n *\n * // server.config.ts\n * defineServerConfig()\n * .websockets(wsRouter)\n * .build();\n * ```\n *\n * @example Client usage\n * ```typescript\n * import { createWSClient } from '@spfn/core/event/ws/client';\n * import type { WSRouter } from '@/server/ws';\n *\n * const client = createWSClient<WSRouter>();\n *\n * client.subscribe({\n * events: ['userUpdated', 'notification'],\n * handlers: {\n * userUpdated: ({ userId }) => console.log(userId),\n * notification: ({ message }) => console.log(message),\n * },\n * });\n *\n * client.send('ping', {});\n * ```\n */\n\nimport type { EventDef } from '../types';\nimport type { WSRouterDef, WSMessageHandlers } from './types';\n\nexport { attachWSHandler } from './handler';\nexport type {\n WSRouterDef,\n WSHandlerConfig,\n WSAuthConfig,\n WSHandlerAuthConfig,\n WSMessageContext,\n WSMessageHandlerFn,\n WSMessageHandlers,\n WSRawConnection,\n WSClientConfig,\n WSConnectionState,\n WSEventHandlers,\n WSSubscribeOptions,\n WSUnsubscribe,\n} from './types';\n\n/**\n * Define a WebSocket router\n *\n * Combines server→client event push with client→server message handlers.\n *\n * @example\n * ```typescript\n * export const wsRouter = defineWSRouter({\n * events: { userUpdated, notification },\n * messages: {\n * ping: ({ ws }) => ws.send('pong', {}),\n * 'chat.send': ({ payload, subject }) => handleChat(payload, subject),\n * },\n * });\n * ```\n */\nexport function defineWSRouter<\n TEvents extends Record<string, EventDef<any>>,\n TMessages extends WSMessageHandlers = WSMessageHandlers,\n>(def: {\n events: TEvents;\n messages?: TMessages;\n}): WSRouterDef<TEvents, TMessages>\n{\n return {\n events: def.events,\n eventNames: Object.keys(def.events) as (keyof TEvents)[],\n messages: (def.messages ?? {}) as TMessages,\n _types: {} as WSRouterDef<TEvents, TMessages>['_types'],\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/event/event.ts","../../src/event/sse/route-map.ts","../../src/event/router.ts","../../src/event/ws/handler.ts","../../src/event/ws/index.ts"],"names":["logger"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Event System\n *\n * Decoupled pub/sub event system with optional cache integration for multi-instance support\n *\n * @example\n * ```typescript\n * // Define event\n * const userCreated = defineEvent('user.created', Type.Object({\n * userId: Type.String(),\n * }));\n *\n * // Subscribe (in-memory)\n * userCreated.subscribe((payload) => {\n * console.log('User created:', payload.userId);\n * });\n *\n * // Emit\n * await userCreated.emit({ userId: '123' });\n *\n * // With cache for multi-instance\n * const event = defineEvent('user.created', schema);\n * await event.useCache(cache); // Must await before emitting\n * await event.emit({ userId: '123' }); // Broadcast to all instances\n * ```\n */\n\nimport type { TSchema, Static } from '@sinclair/typebox';\nimport { logger } from '@spfn/core/logger';\nimport type { EventDef, EventHandler, JobQueueSender, PubSubCache } from './types';\n\nconst eventLogger = logger.child('@spfn/core:event');\n\n/**\n * Log handler error with consistent format\n */\nfunction logHandlerError(eventName: string, error: unknown): void\n{\n eventLogger.error(`Event handler error: ${eventName}`, {\n error: error instanceof Error ? error.message : String(error),\n });\n}\n\n/**\n * Log job queue error with consistent format\n */\nfunction logJobQueueError(queueName: string, error: unknown): void\n{\n eventLogger.error(`Failed to send event to job queue: ${queueName}`, {\n error: error instanceof Error ? error.message : String(error),\n });\n}\n\n/**\n * Create handler subscription manager\n */\nfunction createHandlerManager<TPayload>(name: string)\n{\n const handlers: Set<EventHandler<TPayload>> = new Set();\n\n return {\n add: (handler: EventHandler<TPayload>): (() => void) =>\n {\n handlers.add(handler);\n eventLogger.debug(`Subscribed to event: ${name}`, { handlerCount: handlers.size });\n\n return () =>\n {\n handlers.delete(handler);\n eventLogger.debug(`Unsubscribed from event: ${name}`, { handlerCount: handlers.size });\n };\n },\n\n clear: (): void =>\n {\n handlers.clear();\n eventLogger.debug(`Unsubscribed all from event: ${name}`);\n },\n\n trigger: async (payload: TPayload): Promise<void> =>\n {\n const results = await Promise.allSettled(\n [...handlers].map((handler) => handler(payload)),\n );\n\n for (const result of results)\n {\n if (result.status === 'rejected')\n {\n logHandlerError(name, result.reason);\n }\n }\n },\n };\n}\n\n/**\n * Create job queue manager\n */\nfunction createJobQueueManager(name: string)\n{\n const jobQueues: Map<string, JobQueueSender> = new Map();\n\n return {\n register: (queueName: string, sender: JobQueueSender): void =>\n {\n jobQueues.set(queueName, sender);\n eventLogger.debug(`Registered job queue for event: ${name}`, { queueName });\n },\n\n send: async (payload: unknown): Promise<void> =>\n {\n if (jobQueues.size === 0)\n {\n return;\n }\n\n const entries = [...jobQueues.entries()];\n const results = await Promise.allSettled(\n entries.map(([queueName, sender]) => sender(queueName, payload)),\n );\n\n for (const [i, result] of results.entries())\n {\n if (result.status === 'rejected')\n {\n logJobQueueError(entries[i][0], result.reason);\n }\n }\n },\n\n get size(): number\n {\n return jobQueues.size;\n },\n };\n}\n\n/**\n * Internal: Create event implementation\n */\nfunction createEventImpl<TPayload>(\n name: string,\n schema?: TSchema,\n): EventDef<TPayload>\n{\n const handlerManager = createHandlerManager<TPayload>(name);\n const jobQueueManager = createJobQueueManager(name);\n let cache: PubSubCache | undefined;\n let cacheSubscribed = false;\n\n const emit = async (payload?: TPayload): Promise<void> =>\n {\n eventLogger.debug(`Emitting event: ${name}`, {\n payload,\n hasCache: !!cache,\n jobQueueCount: jobQueueManager.size,\n });\n\n if (cache)\n {\n await cache.publish(name, payload);\n }\n else\n {\n await handlerManager.trigger(payload as TPayload);\n }\n\n await jobQueueManager.send(payload);\n eventLogger.debug(`Event emitted: ${name}`);\n };\n\n const useCache = async (newCache: PubSubCache): Promise<EventDef<TPayload>> =>\n {\n if (cacheSubscribed)\n {\n eventLogger.warn(`Cache already configured for event: ${name}`);\n\n return self;\n }\n\n // Subscribe FIRST, then flip to cache mode. If subscribe rejects, the\n // event stays in-process (cache undefined) and cacheSubscribed stays\n // false — so emit keeps delivering locally and a later useCache can retry.\n // Setting these before the await would latch a half-broken state: emit\n // would publish to a channel this instance never subscribed to.\n await newCache.subscribe(name, async (message: unknown) =>\n {\n eventLogger.debug(`Received event from cache: ${name}`);\n await handlerManager.trigger(message as TPayload);\n });\n\n cache = newCache;\n cacheSubscribed = true;\n\n eventLogger.debug(`Cache subscription ready for event: ${name}`);\n\n return self;\n };\n\n // Drop the cache binding so a later useCache can rebind a fresh transport.\n // Without this, the cacheSubscribed latch would make a second server start in\n // the same process (tests, hot-reload) a no-op while the event still points at\n // a torn-down cache — cross-pod fan-out would silently stay dead.\n const resetCache = (): void =>\n {\n cache = undefined;\n cacheSubscribed = false;\n };\n\n const self: EventDef<TPayload> = {\n name,\n schema,\n subscribe: handlerManager.add,\n unsubscribeAll: handlerManager.clear,\n emit: emit as EventDef<TPayload>['emit'],\n useCache,\n _registerJobQueue: jobQueueManager.register,\n _resetCache: resetCache,\n _payload: undefined as unknown as TPayload,\n };\n\n return self;\n}\n\n/**\n * Define an event without payload\n */\nexport function defineEvent(name: string): EventDef<void>;\n\n/**\n * Define an event with typed payload\n */\nexport function defineEvent<T extends TSchema>(\n name: string,\n schema: T,\n): EventDef<Static<T>>;\n\n/**\n * Define an event for decoupled pub/sub\n *\n * @example\n * ```typescript\n * // Define event with payload\n * export const userCreated = defineEvent('user.created', Type.Object({\n * userId: Type.String(),\n * }));\n *\n * // Subscribe to event (in-memory)\n * const unsubscribe = userCreated.subscribe((payload) => {\n * console.log('User created:', payload.userId);\n * });\n *\n * // Emit event\n * await userCreated.emit({ userId: '123' });\n *\n * // Unsubscribe when done\n * unsubscribe();\n *\n * // Multi-instance with cache\n * await userCreated.useCache(cache);\n * await userCreated.emit({ userId: '123' }); // Broadcast to all instances\n * ```\n */\nexport function defineEvent<T extends TSchema>(\n name: string,\n schema?: T,\n): EventDef<Static<T>> | EventDef\n{\n if (schema)\n {\n return createEventImpl<Static<T>>(name, schema);\n }\n\n return createEventImpl<void>(name);\n}\n","/**\n * SSE Event Route Map\n *\n * Static route map for SSE token endpoint.\n * Merge into RPC proxy routeMap so `eventsToken` resolves to `POST /events/token`.\n *\n * @example\n * ```typescript\n * // app/api/rpc/[routeName]/route.ts\n * import { createRpcProxy } from '@spfn/core/nextjs/server';\n * import { eventRouteMap } from '@spfn/core/event';\n * import { authRouteMap } from '@spfn/auth';\n * import { routeMap } from '@/generated/route-map';\n *\n * export const { GET, POST } = createRpcProxy({\n * routeMap: { ...routeMap, ...authRouteMap, ...eventRouteMap },\n * });\n * ```\n */\nexport const eventRouteMap = {\n eventsToken: { method: 'POST' as const, path: '/events/token' },\n};\n","/**\n * Event Router\n *\n * Type-safe event router for SSE subscription\n *\n * @example\n * ```typescript\n * import { defineEvent, defineEventRouter } from '@spfn/core/event';\n * import { Type } from '@sinclair/typebox';\n *\n * const userCreated = defineEvent('user.created', Type.Object({\n * userId: Type.String(),\n * }));\n *\n * const orderPlaced = defineEvent('order.placed', Type.Object({\n * orderId: Type.String(),\n * amount: Type.Number(),\n * }));\n *\n * export const eventRouter = defineEventRouter({\n * userCreated,\n * orderPlaced,\n * });\n *\n * export type EventRouter = typeof eventRouter;\n * ```\n */\n\nimport type { EventDef } from './types';\n\n/**\n * Event Router Definition\n */\nexport interface EventRouterDef<TEvents extends Record<string, EventDef<any>>>\n{\n /**\n * Event definitions\n */\n readonly events: TEvents;\n\n /**\n * Event names as array\n */\n readonly eventNames: (keyof TEvents)[];\n\n /**\n * Type inference helper - payload types by event name\n */\n readonly _types: {\n [K in keyof TEvents]: TEvents[K]['_payload'];\n };\n}\n\n/**\n * Infer event names from EventRouter\n */\nexport type InferEventNames<T> = T extends EventRouterDef<infer E>\n ? keyof E & string\n : never;\n\n/**\n * Infer payload type for specific event\n */\nexport type InferEventPayload<\n T extends EventRouterDef<any>,\n K extends InferEventNames<T>,\n> = T['_types'][K];\n\n/**\n * Infer all event payloads map\n */\nexport type InferEventPayloads<T extends EventRouterDef<any>> = T['_types'];\n\n/**\n * Define an event router for SSE subscription\n *\n * @example\n * ```typescript\n * export const eventRouter = defineEventRouter({\n * userCreated,\n * orderPlaced,\n * });\n *\n * // Type inference\n * type Names = InferEventNames<typeof eventRouter>;\n * // 'userCreated' | 'orderPlaced'\n *\n * type Payload = InferEventPayload<typeof eventRouter, 'userCreated'>;\n * // { userId: string }\n * ```\n */\nexport function defineEventRouter<\n TEvents extends Record<string, EventDef<any>>,\n>(events: TEvents): EventRouterDef<TEvents>\n{\n return {\n events,\n eventNames: Object.keys(events) as (keyof TEvents)[],\n _types: {} as EventRouterDef<TEvents>['_types'],\n };\n}\n","/**\n * WebSocket Handler\n *\n * Attaches a WebSocket server to an existing Node.js http.Server.\n * Handles authentication, event subscription, and client message routing.\n */\n\nimport type { Server } from 'node:http';\nimport type { EventDef } from '../types';\nimport type {\n WSRouterDef,\n WSHandlerConfig,\n WSHandlerAuthConfig,\n WSMessageHandlers,\n WSRawConnection,\n} from './types';\nimport type { SSETokenManager } from '../sse/token-manager';\nimport { logger } from '@spfn/core/logger';\n\nconst wsLogger = logger.child('@spfn/core:ws');\n\n// ============================================================================\n// Public API\n// ============================================================================\n\n/**\n * Attach a WebSocket server to a Node.js http.Server\n *\n * @returns cleanup function that closes the WebSocket server\n */\nexport async function attachWSHandler<\n TEvents extends Record<string, EventDef<any>>,\n TMessages extends WSMessageHandlers,\n>(\n server: Server,\n router: WSRouterDef<TEvents, TMessages>,\n config: WSHandlerConfig & { path?: string } = {},\n tokenManager?: SSETokenManager,\n): Promise<() => Promise<void>>\n{\n const WebSocketServer = await loadWSServer();\n\n const {\n pingInterval = 30000,\n path = '/ws',\n auth: authConfig,\n } = config;\n\n if (authConfig?.enabled && !tokenManager)\n {\n throw new Error(\n 'WebSocket auth.enabled=true requires a tokenManager. ' +\n 'Pass tokenManager or use .websockets(router, { auth: { enabled: true } }) via startServer.',\n );\n }\n\n const wss = new WebSocketServer({ server, path });\n\n // Track live connections for graceful shutdown\n const clients = new Set<any>();\n\n wss.on('connection', (ws: any, req: any) =>\n {\n clients.add(ws);\n ws.on('close', () => clients.delete(ws));\n handleConnection(ws, req, router, authConfig, tokenManager, pingInterval)\n .catch((err: Error) =>\n {\n wsLogger.error('WebSocket connection handler error', err);\n if (ws.readyState === 1) ws.close(1011, 'Internal server error');\n });\n });\n\n wss.on('error', (err: Error) =>\n {\n wsLogger.error('WebSocket server error', err);\n });\n\n wsLogger.info(`✓ WebSocket endpoint registered at ${path}`, {\n events: router.eventNames,\n auth: !!authConfig?.enabled,\n });\n\n return () => new Promise<void>((resolve, reject) =>\n {\n // Close all existing connections with 1001 Going Away\n for (const client of clients)\n {\n client.close(1001, 'Server shutting down');\n }\n clients.clear();\n\n wss.close((err?: Error) =>\n {\n if (err) reject(err);\n else resolve();\n });\n });\n}\n\n// ============================================================================\n// Connection Handler\n// ============================================================================\n\nasync function handleConnection(\n ws: any,\n req: any,\n router: WSRouterDef<any, any>,\n authConfig: WSHandlerAuthConfig | undefined,\n tokenManager: SSETokenManager | undefined,\n pingInterval: number,\n): Promise<void>\n{\n // Register close handler before any await — ensures we never miss the event even during auth\n let pingTimer: ReturnType<typeof setInterval> | undefined;\n let connectionUnsubscribes: (() => void)[] = [];\n let subscribedEvents: string[] = [];\n ws.on('close', () =>\n {\n clearInterval(pingTimer);\n connectionUnsubscribes.forEach(fn => fn());\n if (subscribedEvents.length > 0)\n wsLogger.info('WebSocket connection closed', { events: subscribedEvents });\n });\n\n const url = parseURL(req);\n if (!url)\n {\n ws.close(1002, 'Invalid request URL');\n\n return;\n }\n\n // ── 1. Authenticate ──\n const subject = await resolveSubject(url, authConfig?.enabled ? tokenManager : undefined);\n if (subject === false)\n {\n ws.close(4001, 'Missing token');\n\n return;\n }\n if (subject === null)\n {\n ws.close(4001, 'Invalid or expired token');\n\n return;\n }\n\n // ── 2. Resolve subscribed events ──\n const requestedEvents = parseRequestedEvents(url, router.eventNames as string[]);\n if (requestedEvents.length === 0)\n {\n ws.close(4000, 'No valid event names specified');\n\n return;\n }\n\n // ── 3. Authorize ──\n const allowedEvents = await resolveAllowedEvents(subject, requestedEvents, authConfig);\n if (allowedEvents === null)\n {\n ws.close(4003, 'Not authorized for any requested events');\n\n return;\n }\n\n subscribedEvents = allowedEvents;\n wsLogger.info('WebSocket connection established', {\n events: allowedEvents,\n subject: subject ?? undefined,\n });\n\n // ── 4. Build connection wrapper ──\n const connection = createConnection(ws);\n\n // ── 5. Subscribe to server-push events ──\n connectionUnsubscribes = subscribeEvents(ws, router, allowedEvents, subject, authConfig);\n\n // If socket closed during auth awaits, clean up and bail\n if (ws.readyState !== 1)\n {\n connectionUnsubscribes.forEach(fn => fn());\n connectionUnsubscribes = [];\n\n return;\n }\n\n // ── 6. Handle incoming messages ──\n ws.on('message', (data: Buffer | string) =>\n {\n onClientMessage(data, router, connection, subject)\n .catch((err: Error) => wsLogger.error('Unhandled message error', err));\n });\n\n // ── 7. Keep-alive ping ──\n if (pingInterval > 0)\n {\n pingTimer = setInterval(() =>\n {\n if (ws.readyState === 1) ws.ping();\n }, pingInterval);\n }\n\n // ── 9. Send connected ack ──\n connection.send('__connected', {\n subscribedEvents: allowedEvents,\n timestamp: Date.now(),\n });\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction parseURL(req: any): URL | null\n{\n try\n {\n return new URL(req.url ?? '/', 'ws://localhost');\n }\n catch\n {\n return null;\n }\n}\n\n/**\n * Resolve subject from token\n * - undefined: no auth required\n * - false: token param missing (when required)\n * - null: token invalid/expired\n * - string: authenticated subject\n */\nasync function resolveSubject(\n url: URL,\n tokenManager?: SSETokenManager,\n): Promise<string | undefined | false | null>\n{\n if (!tokenManager)\n {\n return undefined;\n }\n\n const token = url.searchParams.get('token');\n if (!token)\n {\n return false;\n }\n\n return await tokenManager.verify(token);\n}\n\nfunction parseRequestedEvents(url: URL, validEventNames: string[]): string[]\n{\n const eventsParam = url.searchParams.get('events');\n if (!eventsParam)\n {\n return [];\n }\n\n return eventsParam\n .split(',')\n .map(e => e.trim())\n .filter(e => validEventNames.includes(e));\n}\n\nasync function resolveAllowedEvents(\n subject: string | undefined,\n requestedEvents: string[],\n authConfig?: WSHandlerAuthConfig,\n): Promise<string[] | null>\n{\n if (!subject || !authConfig?.authorize)\n {\n return requestedEvents;\n }\n\n const allowed = await authConfig.authorize(subject, requestedEvents);\n\n return allowed.length === 0 ? null : allowed;\n}\n\nfunction createConnection(ws: any): WSRawConnection\n{\n return {\n send: (type, payload) =>\n {\n if (ws.readyState !== 1) return;\n ws.send(JSON.stringify({ type, data: payload }));\n },\n close: (code, reason) => ws.close(code, reason),\n };\n}\n\nfunction subscribeEvents(\n ws: any,\n router: WSRouterDef<any, any>,\n allowedEvents: string[],\n subject: string | undefined,\n authConfig?: WSHandlerAuthConfig,\n): (() => void)[]\n{\n const unsubscribes: (() => void)[] = [];\n\n for (const eventName of allowedEvents)\n {\n const eventDef = router.events[eventName];\n if (!eventDef) continue;\n\n const unsubscribe = eventDef.subscribe((payload: unknown) =>\n {\n if (ws.readyState !== 1) return;\n\n if (subject && authConfig?.filter?.[eventName])\n {\n if (!authConfig.filter[eventName](subject, payload)) return;\n }\n\n try\n {\n ws.send(JSON.stringify({ type: eventName, data: payload }));\n }\n catch\n {\n // Socket closed between readyState check and send — ignore\n }\n });\n\n unsubscribes.push(unsubscribe);\n }\n\n return unsubscribes;\n}\n\nasync function onClientMessage(\n data: Buffer | string,\n router: WSRouterDef<any, any>,\n connection: WSRawConnection,\n subject: string | undefined,\n): Promise<void>\n{\n let message: { type?: string; data?: unknown };\n\n try\n {\n message = JSON.parse(data.toString());\n }\n catch\n {\n return;\n }\n\n const { type, data: payload } = message;\n if (!type) return;\n\n const handler = router.messages[type];\n if (!handler) return;\n\n try\n {\n await handler({ payload, subject, ws: connection });\n }\n catch (err)\n {\n wsLogger.error(`WebSocket message handler error: ${type}`, err as Error);\n }\n}\n\n// ============================================================================\n// Dynamic import for optional 'ws' dependency\n// ============================================================================\n\nasync function loadWSServer(): Promise<any>\n{\n try\n {\n // ws is a CJS package: module.exports = WebSocket, WebSocket.WebSocketServer is set on it.\n // ESM dynamic import wraps CJS default export under .default\n \n const mod = await import('ws') as any;\n const WS = mod.default ?? mod;\n const WSS = WS.WebSocketServer ?? WS.Server;\n\n if (typeof WSS !== 'function')\n {\n throw new Error(\n 'WebSocketServer not found in ws module. ' +\n 'Ensure ws@^8 is installed: pnpm add ws',\n );\n }\n\n return WSS;\n }\n catch (err)\n {\n if (err instanceof Error && err.message.includes('WebSocketServer not found'))\n {\n throw err;\n }\n throw new Error(\n '@spfn/core WebSocket support requires the \"ws\" package.\\n' +\n 'Install it with: pnpm add ws',\n );\n }\n}\n","/**\n * WebSocket Module\n *\n * Type-safe WebSocket server with event-based pub/sub and bidirectional messaging.\n *\n * @example Server setup\n * ```typescript\n * // src/server/ws.ts\n * import { defineWSRouter } from '@spfn/core/event/ws';\n * import { defineEvent } from '@spfn/core/event';\n * import { Type } from '@sinclair/typebox';\n *\n * const userUpdated = defineEvent('user.updated', Type.Object({ userId: Type.String() }));\n * const notification = defineEvent('notification', Type.Object({ message: Type.String() }));\n *\n * export const wsRouter = defineWSRouter({\n * events: { userUpdated, notification },\n * messages: {\n * ping: ({ ws }) => ws.send('pong', {}),\n * },\n * });\n *\n * export type WSRouter = typeof wsRouter;\n *\n * // server.config.ts\n * defineServerConfig()\n * .websockets(wsRouter)\n * .build();\n * ```\n *\n * @example Client usage\n * ```typescript\n * import { createWSClient } from '@spfn/core/event/ws/client';\n * import type { WSRouter } from '@/server/ws';\n *\n * const client = createWSClient<WSRouter>();\n *\n * client.subscribe({\n * events: ['userUpdated', 'notification'],\n * handlers: {\n * userUpdated: ({ userId }) => console.log(userId),\n * notification: ({ message }) => console.log(message),\n * },\n * });\n *\n * client.send('ping', {});\n * ```\n */\n\nimport type { EventDef } from '../types';\nimport type { WSRouterDef, WSMessageHandlers } from './types';\n\nexport { attachWSHandler } from './handler';\nexport type {\n WSRouterDef,\n WSHandlerConfig,\n WSAuthConfig,\n WSHandlerAuthConfig,\n WSMessageContext,\n WSMessageHandlerFn,\n WSMessageHandlers,\n WSRawConnection,\n WSClientConfig,\n WSConnectionState,\n WSEventHandlers,\n WSSubscribeOptions,\n WSUnsubscribe,\n} from './types';\n\n/**\n * Define a WebSocket router\n *\n * Combines server→client event push with client→server message handlers.\n *\n * @example\n * ```typescript\n * export const wsRouter = defineWSRouter({\n * events: { userUpdated, notification },\n * messages: {\n * ping: ({ ws }) => ws.send('pong', {}),\n * 'chat.send': ({ payload, subject }) => handleChat(payload, subject),\n * },\n * });\n * ```\n */\nexport function defineWSRouter<\n TEvents extends Record<string, EventDef<any>>,\n TMessages extends WSMessageHandlers = WSMessageHandlers,\n>(def: {\n events: TEvents;\n messages?: TMessages;\n}): WSRouterDef<TEvents, TMessages>\n{\n return {\n events: def.events,\n eventNames: Object.keys(def.events) as (keyof TEvents)[],\n messages: (def.messages ?? {}) as TMessages,\n _types: {} as WSRouterDef<TEvents, TMessages>['_types'],\n };\n}\n"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { E as EventRouterDef, I as InferEventNames } from '../../token-manager-
|
|
2
|
-
import { g as SSESubscribeOptions, i as SSEUnsubscribe, h as SSEConnectionState, d as SSEClientConfig } from '../../types-
|
|
1
|
+
import { E as EventRouterDef, I as InferEventNames } from '../../token-manager-C2Ag5-s8.js';
|
|
2
|
+
import { g as SSESubscribeOptions, i as SSEUnsubscribe, h as SSEConnectionState, d as SSEClientConfig } from '../../types-VpVQIsyB.js';
|
|
3
3
|
import '@sinclair/typebox';
|
|
4
4
|
import 'hono';
|
|
5
5
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Context } from 'hono';
|
|
2
|
-
import { E as EventRouterDef, S as SSETokenManager } from '../../token-manager-
|
|
3
|
-
export { C as CacheTokenStore, g as SSEToken, i as SSETokenManagerConfig, h as SSETokenStore } from '../../token-manager-
|
|
4
|
-
import { S as SSEHandlerConfig } from '../../types-
|
|
5
|
-
export { a as SSEAuthConfig, d as SSEClientConfig, h as SSEConnectionState, e as SSEEventHandler, f as SSEEventHandlers, c as SSEHandlerAuthConfig, b as SSEMessage, g as SSESubscribeOptions, i as SSEUnsubscribe } from '../../types-
|
|
2
|
+
import { E as EventRouterDef, S as SSETokenManager } from '../../token-manager-C2Ag5-s8.js';
|
|
3
|
+
export { C as CacheTokenStore, g as SSEToken, i as SSETokenManagerConfig, h as SSETokenStore } from '../../token-manager-C2Ag5-s8.js';
|
|
4
|
+
import { S as SSEHandlerConfig } from '../../types-VpVQIsyB.js';
|
|
5
|
+
export { a as SSEAuthConfig, d as SSEClientConfig, h as SSEConnectionState, e as SSEEventHandler, f as SSEEventHandlers, c as SSEHandlerAuthConfig, b as SSEMessage, g as SSESubscribeOptions, i as SSEUnsubscribe } from '../../types-VpVQIsyB.js';
|
|
6
6
|
import '@sinclair/typebox';
|
|
7
7
|
|
|
8
8
|
/**
|
package/dist/event/sse/index.js
CHANGED
|
@@ -2,12 +2,63 @@ import { streamSSE } from 'hono/streaming';
|
|
|
2
2
|
import { logger } from '@spfn/core/logger';
|
|
3
3
|
import { randomBytes } from 'crypto';
|
|
4
4
|
|
|
5
|
+
// src/event/sse/handler.ts
|
|
6
|
+
|
|
7
|
+
// src/event/sse/bounded-writer.ts
|
|
8
|
+
function createBoundedWriter(stream, maxQueue, onClose) {
|
|
9
|
+
const pending = [];
|
|
10
|
+
let flushing = false;
|
|
11
|
+
let closed = false;
|
|
12
|
+
const flush = async () => {
|
|
13
|
+
if (flushing || closed) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
flushing = true;
|
|
17
|
+
while (pending.length > 0 && !closed) {
|
|
18
|
+
const frame = pending.shift();
|
|
19
|
+
try {
|
|
20
|
+
await stream.writeSSE(frame);
|
|
21
|
+
} catch (err) {
|
|
22
|
+
flushing = false;
|
|
23
|
+
onClose(err instanceof Error ? err.message : String(err));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
flushing = false;
|
|
28
|
+
};
|
|
29
|
+
return {
|
|
30
|
+
enqueue(frame) {
|
|
31
|
+
if (closed) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
pending.push(frame);
|
|
35
|
+
if (pending.length > maxQueue) {
|
|
36
|
+
onClose("outbound queue overflow (slow consumer)");
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
void flush();
|
|
40
|
+
},
|
|
41
|
+
close() {
|
|
42
|
+
closed = true;
|
|
43
|
+
pending.length = 0;
|
|
44
|
+
},
|
|
45
|
+
get queued() {
|
|
46
|
+
return pending.length;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
5
51
|
// src/event/sse/handler.ts
|
|
6
52
|
var sseLogger = logger.child("@spfn/core:sse");
|
|
53
|
+
var DEFAULT_MAX_QUEUE = 1e3;
|
|
7
54
|
function createSSEHandler(router, config = {}, tokenManager) {
|
|
8
55
|
const {
|
|
9
|
-
|
|
10
|
-
|
|
56
|
+
// 10s — keep-alive가 중간 프록시(GCP LB/nginx)의 idle 타임아웃(흔히 ~15-30s)보다 자주 와야
|
|
57
|
+
// 첫 ping 전 침묵 구간에 연결이 끊기지 않는다. 30s 기본은 idle 타임아웃보다 길어 SSE가
|
|
58
|
+
// 데이터 없는 동안 끊기던 문제가 있었다.
|
|
59
|
+
pingInterval = 1e4,
|
|
60
|
+
auth: authConfig,
|
|
61
|
+
maxQueue = DEFAULT_MAX_QUEUE
|
|
11
62
|
} = config;
|
|
12
63
|
return async (c) => {
|
|
13
64
|
const subject = await authenticateToken(c, tokenManager);
|
|
@@ -48,15 +99,29 @@ function createSSEHandler(router, config = {}, tokenManager) {
|
|
|
48
99
|
let messageId = 0;
|
|
49
100
|
let connectionDead = false;
|
|
50
101
|
let pingTimer;
|
|
51
|
-
|
|
102
|
+
let writer;
|
|
103
|
+
const cleanup = (reason) => {
|
|
52
104
|
if (connectionDead) return;
|
|
53
105
|
connectionDead = true;
|
|
54
106
|
clearInterval(pingTimer);
|
|
55
107
|
unsubscribes.forEach((fn) => fn());
|
|
108
|
+
writer?.close();
|
|
56
109
|
sseLogger.info("SSE dead connection cleaned up", {
|
|
57
|
-
events: allowedEvents
|
|
110
|
+
events: allowedEvents,
|
|
111
|
+
reason
|
|
58
112
|
});
|
|
59
113
|
};
|
|
114
|
+
writer = createBoundedWriter(stream, maxQueue, (reason) => {
|
|
115
|
+
sseLogger.warn("SSE connection closed", { events: allowedEvents, reason });
|
|
116
|
+
cleanup(reason);
|
|
117
|
+
});
|
|
118
|
+
writer.enqueue({
|
|
119
|
+
event: "connected",
|
|
120
|
+
data: JSON.stringify({
|
|
121
|
+
subscribedEvents: allowedEvents,
|
|
122
|
+
timestamp: Date.now()
|
|
123
|
+
})
|
|
124
|
+
});
|
|
60
125
|
for (const eventName of allowedEvents) {
|
|
61
126
|
const eventDef = router.events[eventName];
|
|
62
127
|
if (!eventDef) {
|
|
@@ -78,17 +143,10 @@ function createSSEHandler(router, config = {}, tokenManager) {
|
|
|
78
143
|
event: eventName,
|
|
79
144
|
messageId
|
|
80
145
|
});
|
|
81
|
-
|
|
146
|
+
writer.enqueue({
|
|
82
147
|
id: String(messageId),
|
|
83
148
|
event: eventName,
|
|
84
149
|
data: JSON.stringify(message)
|
|
85
|
-
}).catch((err) => {
|
|
86
|
-
sseLogger.warn("SSE write failed", {
|
|
87
|
-
event: eventName,
|
|
88
|
-
messageId,
|
|
89
|
-
error: err.message
|
|
90
|
-
});
|
|
91
|
-
cleanup();
|
|
92
150
|
});
|
|
93
151
|
});
|
|
94
152
|
unsubscribes.push(unsubscribe);
|
|
@@ -97,23 +155,11 @@ function createSSEHandler(router, config = {}, tokenManager) {
|
|
|
97
155
|
events: allowedEvents,
|
|
98
156
|
subscriptionCount: unsubscribes.length
|
|
99
157
|
});
|
|
100
|
-
await stream.writeSSE({
|
|
101
|
-
event: "connected",
|
|
102
|
-
data: JSON.stringify({
|
|
103
|
-
subscribedEvents: allowedEvents,
|
|
104
|
-
timestamp: Date.now()
|
|
105
|
-
})
|
|
106
|
-
});
|
|
107
158
|
pingTimer = setInterval(() => {
|
|
108
159
|
if (connectionDead) return;
|
|
109
|
-
|
|
160
|
+
writer.enqueue({
|
|
110
161
|
event: "ping",
|
|
111
162
|
data: JSON.stringify({ timestamp: Date.now() })
|
|
112
|
-
}).catch((err) => {
|
|
113
|
-
sseLogger.warn("SSE ping failed", {
|
|
114
|
-
error: err.message
|
|
115
|
-
});
|
|
116
|
-
cleanup();
|
|
117
163
|
});
|
|
118
164
|
}, pingInterval);
|
|
119
165
|
const abortSignal = c.req.raw.signal;
|