@spfn/core 0.2.0-beta.45 → 0.2.0-beta.48

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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/event/ws/client.ts"],"names":[],"mappings":"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","file":"client.js","sourcesContent":["/**\n * WebSocket Client\n *\n * Type-safe browser WebSocket wrapper for event subscription and bidirectional messaging.\n *\n * @example\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 * const unsubscribe = client.subscribe({\n * events: ['userUpdated', 'notification'],\n * handlers: {\n * userUpdated: ({ userId }) => console.log(userId),\n * notification: ({ message }) => console.log(message),\n * },\n * onOpen: () => console.log('connected'),\n * });\n *\n * // Send message to server\n * client.send('ping', {});\n *\n * // Cleanup\n * unsubscribe();\n * ```\n */\n\nimport type { WSRouterDef, WSClientConfig, WSSubscribeOptions, WSUnsubscribe, WSConnectionState } from './types';\n\n// ============================================================================\n// Public Interface\n// ============================================================================\n\nexport interface WSClient<TRouter extends WSRouterDef<any, any>>\n{\n /**\n * Subscribe to server-push events\n * Returns an unsubscribe function\n */\n subscribe(options: WSSubscribeOptions<TRouter>): WSUnsubscribe;\n\n /**\n * Send a message to the server\n */\n send<TType extends string>(\n type: TType,\n payload: unknown\n ): void;\n\n /**\n * Get current connection state\n */\n getState(): WSConnectionState;\n\n /**\n * Close the connection permanently\n */\n close(): void;\n}\n\n// ============================================================================\n// Defaults\n// ============================================================================\n\nfunction deriveWSHost(): string\n{\n const apiUrl = typeof process !== 'undefined'\n ? (process.env.NEXT_PUBLIC_SPFN_API_URL || 'http://localhost:8790')\n : 'http://localhost:8790';\n\n return apiUrl\n .replace(/^https:\\/\\//, 'wss://')\n .replace(/^http:\\/\\//, 'ws://');\n}\n\nconst WS_DEFAULTS = {\n get host() { return deriveWSHost(); },\n pathname: '/ws',\n} as const;\n\n// ============================================================================\n// Factory\n// ============================================================================\n\n/**\n * Create a type-safe WebSocket client\n */\nexport function createWSClient<TRouter extends WSRouterDef<any, any>>(\n config: WSClientConfig = {}\n): WSClient<TRouter>\n{\n const {\n host = WS_DEFAULTS.host,\n pathname = WS_DEFAULTS.pathname,\n reconnect = true,\n reconnectDelay = 3000,\n maxReconnectAttempts = 0,\n acquireToken,\n } = config;\n\n const baseUrl = `${host}${pathname}`;\n\n let socket: WebSocket | null = null;\n let state: WSConnectionState = 'closed';\n let reconnectAttempts = 0;\n let reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n let destroyed = false;\n // Set before intentionally closing socket (new events merge) — skips delay/counter in onClose\n let intentionalReconnect = false;\n // Events actually sent in the current/last connect() URL\n let sentEvents: Set<string> = new Set();\n // Events the server confirmed it has subscribed (set on onOpen)\n let connectedEvents: Set<string> = new Set();\n\n // Active subscriptions: each entry is one subscribe() call\n type Subscription = {\n options: WSSubscribeOptions<TRouter>;\n active: boolean;\n };\n const subscriptions: Set<Subscription> = new Set();\n\n // ── Internal helpers ──\n\n function setState(next: WSConnectionState)\n {\n state = next;\n }\n\n function buildURL(events: string[], token?: string): string\n {\n const params = new URLSearchParams();\n params.set('events', events.join(','));\n if (token) params.set('token', token);\n return `${baseUrl}?${params.toString()}`;\n }\n\n function mergeEventNames(): string[]\n {\n const names = new Set<string>();\n for (const sub of subscriptions)\n {\n if (sub.active)\n {\n (sub.options.events as string[]).forEach(e => names.add(e));\n }\n }\n return [...names];\n }\n\n function dispatch(type: string, payload: unknown)\n {\n for (const sub of subscriptions)\n {\n if (!sub.active) continue;\n const handler = (sub.options.handlers as Record<string, ((p: unknown) => void) | undefined>)[type];\n if (handler) handler(payload);\n }\n }\n\n function onOpen()\n {\n setState('open');\n reconnectAttempts = 0;\n // Use sentEvents (what was in the URL), not mergeEventNames()\n // New subscriptions added during CONNECTING are NOT yet on the server\n connectedEvents = new Set(sentEvents);\n\n // Check reconnect need BEFORE firing callbacks to avoid spurious onOpen calls\n const current = mergeEventNames();\n const hasNewEvents = current.some(e => !connectedEvents.has(e));\n if (hasNewEvents)\n {\n intentionalReconnect = true;\n socket?.close();\n return;\n }\n\n for (const sub of subscriptions)\n {\n if (sub.active) sub.options.onOpen?.();\n }\n }\n\n function onError(evt: Event)\n {\n setState('error');\n for (const sub of subscriptions)\n {\n if (sub.active) sub.options.onError?.(evt);\n }\n }\n\n function onClose()\n {\n socket = null;\n connectedEvents = new Set();\n sentEvents = new Set();\n\n if (destroyed)\n {\n setState('closed');\n for (const sub of subscriptions)\n {\n if (sub.active)\n {\n sub.options.onClose?.();\n sub.active = false;\n }\n }\n subscriptions.clear();\n return;\n }\n\n // Intentional reconnect (new events merged): skip delay, counter, and callbacks\n if (intentionalReconnect)\n {\n intentionalReconnect = false;\n connect();\n return;\n }\n\n const hasActive = [...subscriptions].some(s => s.active);\n if (!reconnect || !hasActive)\n {\n setState('closed');\n if (!reconnect && hasActive)\n {\n for (const sub of subscriptions)\n {\n if (sub.active)\n {\n sub.options.onClose?.();\n sub.active = false;\n }\n }\n subscriptions.clear();\n }\n return;\n }\n\n if (maxReconnectAttempts > 0 && reconnectAttempts >= maxReconnectAttempts)\n {\n setState('closed');\n for (const sub of subscriptions)\n {\n if (sub.active)\n {\n sub.options.onClose?.();\n sub.active = false;\n }\n }\n subscriptions.clear();\n return;\n }\n\n setState('closed');\n reconnectAttempts++;\n for (const sub of subscriptions)\n {\n if (sub.active) sub.options.onReconnect?.(reconnectAttempts);\n }\n\n reconnectTimer = setTimeout(() => connect(), reconnectDelay);\n }\n\n function onMessage(evt: MessageEvent)\n {\n let msg: { type?: string; data?: unknown };\n try\n {\n msg = JSON.parse(evt.data as string);\n }\n catch\n {\n return;\n }\n\n const { type, data } = msg;\n if (!type || type === '__connected') return;\n\n dispatch(type, data);\n }\n\n async function connect()\n {\n if (destroyed) return;\n if (state === 'connecting') return;\n\n const events = mergeEventNames();\n if (events.length === 0) return;\n\n sentEvents = new Set(events); // record what we're sending in the URL\n setState('connecting');\n\n let token: string | undefined;\n if (acquireToken)\n {\n try\n {\n token = await acquireToken();\n }\n catch\n {\n setState('error');\n const errorEvent = new Event('error');\n for (const sub of subscriptions)\n {\n if (sub.active) sub.options.onError?.(errorEvent);\n }\n\n const permanentlyClose = destroyed || !reconnect\n || (maxReconnectAttempts > 0 && reconnectAttempts >= maxReconnectAttempts);\n\n if (permanentlyClose)\n {\n setState('closed');\n sentEvents = new Set();\n for (const sub of subscriptions)\n {\n if (sub.active) { sub.options.onClose?.(); sub.active = false; }\n }\n subscriptions.clear();\n return;\n }\n\n reconnectAttempts++;\n for (const sub of subscriptions)\n {\n if (sub.active) sub.options.onReconnect?.(reconnectAttempts);\n }\n if (reconnectTimer) clearTimeout(reconnectTimer);\n reconnectTimer = setTimeout(() => connect(), reconnectDelay);\n return;\n }\n\n // close() may have been called while awaiting the token\n if (destroyed)\n {\n setState('closed');\n sentEvents = new Set();\n for (const sub of subscriptions)\n {\n if (sub.active)\n {\n sub.options.onClose?.();\n sub.active = false;\n }\n }\n subscriptions.clear();\n return;\n }\n\n // Subscriptions may have changed while awaiting the token — recompute\n const currentEvents = mergeEventNames();\n if (currentEvents.length === 0)\n {\n setState('closed');\n sentEvents = new Set();\n return;\n }\n // Update sentEvents to reflect what we're actually connecting with\n if (currentEvents.some(e => !sentEvents.has(e)) || sentEvents.size !== currentEvents.length)\n {\n sentEvents = new Set(currentEvents);\n }\n }\n\n const url = buildURL([...sentEvents], token);\n const thisSocket = new WebSocket(url);\n socket = thisSocket;\n socket.onopen = onOpen;\n socket.onerror = onError;\n // Guard against a stale close event from a previous socket overwriting the\n // current socket reference. This can happen when onError fires (state→'error'),\n // subscribe() immediately calls connect() creating a new socket, and then the\n // old socket's onclose finally arrives and sets socket = null on the new one.\n socket.onclose = () => { if (socket === thisSocket) onClose(); };\n socket.onmessage = onMessage;\n }\n\n // ── Public API ──\n\n function subscribe(options: WSSubscribeOptions<TRouter>): WSUnsubscribe\n {\n const sub: Subscription = { options, active: true };\n subscriptions.add(sub);\n\n if (!destroyed && (state === 'closed' || state === 'error') && !socket)\n {\n if (reconnectTimer)\n {\n clearTimeout(reconnectTimer);\n reconnectTimer = null;\n }\n connect();\n }\n else if (state === 'open')\n {\n // Reconnect if the new subscription requests events not yet subscribed\n const hasNewEvents = (options.events as string[]).some(e => !connectedEvents.has(e));\n if (hasNewEvents)\n {\n intentionalReconnect = true;\n socket!.close(); // triggers onClose → connect() immediately, no delay/counter\n }\n }\n // state === 'connecting': onOpen will detect new events and do intentionalReconnect\n\n return () =>\n {\n if (!sub.active) return;\n sub.active = false;\n subscriptions.delete(sub);\n\n if (!destroyed)\n {\n const hasActive = [...subscriptions].some(s => s.active);\n if (!hasActive && socket)\n {\n socket.close();\n }\n }\n\n options.onClose?.();\n };\n }\n\n function send(type: string, payload: unknown): void\n {\n if (destroyed || !socket || socket.readyState !== WebSocket.OPEN) return;\n socket.send(JSON.stringify({ type, data: payload }));\n }\n\n function close(): void\n {\n destroyed = true;\n if (reconnectTimer)\n {\n clearTimeout(reconnectTimer);\n reconnectTimer = null;\n }\n if (socket)\n {\n socket.close(); // onClose() will fire and clean up\n }\n else\n {\n // No socket (e.g. waiting for reconnect timer) — onClose won't fire, clean up directly\n setState('closed');\n sentEvents = new Set();\n for (const sub of subscriptions)\n {\n if (sub.active)\n {\n sub.options.onClose?.();\n sub.active = false;\n }\n }\n subscriptions.clear();\n }\n }\n\n return {\n subscribe,\n send,\n getState: () => state,\n close,\n };\n}\n"]}
@@ -0,0 +1,94 @@
1
+ import { a as EventDef, S as SSETokenManager } from '../../token-manager-CyG7la3p.js';
2
+ import { b as WSMessageHandlers, W as WSRouterDef, a as WSHandlerConfig } from '../../types-Cfj--lfr.js';
3
+ export { c as WSAuthConfig, h as WSClientConfig, i as WSConnectionState, j as WSEventHandlers, d as WSHandlerAuthConfig, e as WSMessageContext, f as WSMessageHandlerFn, g as WSRawConnection, k as WSSubscribeOptions, l as WSUnsubscribe } from '../../types-Cfj--lfr.js';
4
+ import { Server } from 'node:http';
5
+ import '@sinclair/typebox';
6
+ import 'hono';
7
+
8
+ /**
9
+ * WebSocket Handler
10
+ *
11
+ * Attaches a WebSocket server to an existing Node.js http.Server.
12
+ * Handles authentication, event subscription, and client message routing.
13
+ */
14
+
15
+ /**
16
+ * Attach a WebSocket server to a Node.js http.Server
17
+ *
18
+ * @returns cleanup function that closes the WebSocket server
19
+ */
20
+ declare function attachWSHandler<TEvents extends Record<string, EventDef<any>>, TMessages extends WSMessageHandlers>(server: Server, router: WSRouterDef<TEvents, TMessages>, config?: WSHandlerConfig & {
21
+ path?: string;
22
+ }, tokenManager?: SSETokenManager): Promise<() => Promise<void>>;
23
+
24
+ /**
25
+ * WebSocket Module
26
+ *
27
+ * Type-safe WebSocket server with event-based pub/sub and bidirectional messaging.
28
+ *
29
+ * @example Server setup
30
+ * ```typescript
31
+ * // src/server/ws.ts
32
+ * import { defineWSRouter } from '@spfn/core/event/ws';
33
+ * import { defineEvent } from '@spfn/core/event';
34
+ * import { Type } from '@sinclair/typebox';
35
+ *
36
+ * const userUpdated = defineEvent('user.updated', Type.Object({ userId: Type.String() }));
37
+ * const notification = defineEvent('notification', Type.Object({ message: Type.String() }));
38
+ *
39
+ * export const wsRouter = defineWSRouter({
40
+ * events: { userUpdated, notification },
41
+ * messages: {
42
+ * ping: ({ ws }) => ws.send('pong', {}),
43
+ * },
44
+ * });
45
+ *
46
+ * export type WSRouter = typeof wsRouter;
47
+ *
48
+ * // server.config.ts
49
+ * defineServerConfig()
50
+ * .websockets(wsRouter)
51
+ * .build();
52
+ * ```
53
+ *
54
+ * @example Client usage
55
+ * ```typescript
56
+ * import { createWSClient } from '@spfn/core/event/ws/client';
57
+ * import type { WSRouter } from '@/server/ws';
58
+ *
59
+ * const client = createWSClient<WSRouter>();
60
+ *
61
+ * client.subscribe({
62
+ * events: ['userUpdated', 'notification'],
63
+ * handlers: {
64
+ * userUpdated: ({ userId }) => console.log(userId),
65
+ * notification: ({ message }) => console.log(message),
66
+ * },
67
+ * });
68
+ *
69
+ * client.send('ping', {});
70
+ * ```
71
+ */
72
+
73
+ /**
74
+ * Define a WebSocket router
75
+ *
76
+ * Combines server→client event push with client→server message handlers.
77
+ *
78
+ * @example
79
+ * ```typescript
80
+ * export const wsRouter = defineWSRouter({
81
+ * events: { userUpdated, notification },
82
+ * messages: {
83
+ * ping: ({ ws }) => ws.send('pong', {}),
84
+ * 'chat.send': ({ payload, subject }) => handleChat(payload, subject),
85
+ * },
86
+ * });
87
+ * ```
88
+ */
89
+ declare function defineWSRouter<TEvents extends Record<string, EventDef<any>>, TMessages extends WSMessageHandlers = WSMessageHandlers>(def: {
90
+ events: TEvents;
91
+ messages?: TMessages;
92
+ }): WSRouterDef<TEvents, TMessages>;
93
+
94
+ export { WSHandlerConfig, WSMessageHandlers, WSRouterDef, attachWSHandler, defineWSRouter };
@@ -0,0 +1,213 @@
1
+ import { logger } from '@spfn/core/logger';
2
+
3
+ // src/event/ws/handler.ts
4
+ var wsLogger = logger.child("@spfn/core:ws");
5
+ async function attachWSHandler(server, router, config = {}, tokenManager) {
6
+ const WebSocketServer = await loadWSServer();
7
+ const {
8
+ pingInterval = 3e4,
9
+ path = "/ws",
10
+ auth: authConfig
11
+ } = config;
12
+ if (authConfig?.enabled && !tokenManager) {
13
+ throw new Error(
14
+ "WebSocket auth.enabled=true requires a tokenManager. Pass tokenManager or use .websockets(router, { auth: { enabled: true } }) via startServer."
15
+ );
16
+ }
17
+ const wss = new WebSocketServer({ server, path });
18
+ const clients = /* @__PURE__ */ new Set();
19
+ wss.on("connection", (ws, req) => {
20
+ clients.add(ws);
21
+ ws.on("close", () => clients.delete(ws));
22
+ handleConnection(ws, req, router, authConfig, tokenManager, pingInterval).catch((err) => {
23
+ wsLogger.error("WebSocket connection handler error", err);
24
+ if (ws.readyState === 1) ws.close(1011, "Internal server error");
25
+ });
26
+ });
27
+ wss.on("error", (err) => {
28
+ wsLogger.error("WebSocket server error", err);
29
+ });
30
+ wsLogger.info(`\u2713 WebSocket endpoint registered at ${path}`, {
31
+ events: router.eventNames,
32
+ auth: !!authConfig?.enabled
33
+ });
34
+ return () => new Promise((resolve, reject) => {
35
+ for (const client of clients) {
36
+ client.close(1001, "Server shutting down");
37
+ }
38
+ clients.clear();
39
+ wss.close((err) => {
40
+ if (err) reject(err);
41
+ else resolve();
42
+ });
43
+ });
44
+ }
45
+ async function handleConnection(ws, req, router, authConfig, tokenManager, pingInterval) {
46
+ let pingTimer;
47
+ let connectionUnsubscribes = [];
48
+ let subscribedEvents = [];
49
+ ws.on("close", () => {
50
+ clearInterval(pingTimer);
51
+ connectionUnsubscribes.forEach((fn) => fn());
52
+ if (subscribedEvents.length > 0)
53
+ wsLogger.info("WebSocket connection closed", { events: subscribedEvents });
54
+ });
55
+ const url = parseURL(req);
56
+ if (!url) {
57
+ ws.close(1002, "Invalid request URL");
58
+ return;
59
+ }
60
+ const subject = await resolveSubject(url, authConfig?.enabled ? tokenManager : void 0);
61
+ if (subject === false) {
62
+ ws.close(4001, "Missing token");
63
+ return;
64
+ }
65
+ if (subject === null) {
66
+ ws.close(4001, "Invalid or expired token");
67
+ return;
68
+ }
69
+ const requestedEvents = parseRequestedEvents(url, router.eventNames);
70
+ if (requestedEvents.length === 0) {
71
+ ws.close(4e3, "No valid event names specified");
72
+ return;
73
+ }
74
+ const allowedEvents = await resolveAllowedEvents(subject, requestedEvents, authConfig);
75
+ if (allowedEvents === null) {
76
+ ws.close(4003, "Not authorized for any requested events");
77
+ return;
78
+ }
79
+ subscribedEvents = allowedEvents;
80
+ wsLogger.info("WebSocket connection established", {
81
+ events: allowedEvents,
82
+ subject: subject ?? void 0
83
+ });
84
+ const connection = createConnection(ws);
85
+ connectionUnsubscribes = subscribeEvents(ws, router, allowedEvents, subject, authConfig);
86
+ if (ws.readyState !== 1) {
87
+ connectionUnsubscribes.forEach((fn) => fn());
88
+ connectionUnsubscribes = [];
89
+ return;
90
+ }
91
+ ws.on("message", (data) => {
92
+ onClientMessage(data, router, connection, subject).catch((err) => wsLogger.error("Unhandled message error", err));
93
+ });
94
+ if (pingInterval > 0) {
95
+ pingTimer = setInterval(() => {
96
+ if (ws.readyState === 1) ws.ping();
97
+ }, pingInterval);
98
+ }
99
+ connection.send("__connected", {
100
+ subscribedEvents: allowedEvents,
101
+ timestamp: Date.now()
102
+ });
103
+ }
104
+ function parseURL(req) {
105
+ try {
106
+ return new URL(req.url ?? "/", "ws://localhost");
107
+ } catch {
108
+ return null;
109
+ }
110
+ }
111
+ async function resolveSubject(url, tokenManager) {
112
+ if (!tokenManager) {
113
+ return void 0;
114
+ }
115
+ const token = url.searchParams.get("token");
116
+ if (!token) {
117
+ return false;
118
+ }
119
+ return await tokenManager.verify(token);
120
+ }
121
+ function parseRequestedEvents(url, validEventNames) {
122
+ const eventsParam = url.searchParams.get("events");
123
+ if (!eventsParam) {
124
+ return [];
125
+ }
126
+ return eventsParam.split(",").map((e) => e.trim()).filter((e) => validEventNames.includes(e));
127
+ }
128
+ async function resolveAllowedEvents(subject, requestedEvents, authConfig) {
129
+ if (!subject || !authConfig?.authorize) {
130
+ return requestedEvents;
131
+ }
132
+ const allowed = await authConfig.authorize(subject, requestedEvents);
133
+ return allowed.length === 0 ? null : allowed;
134
+ }
135
+ function createConnection(ws) {
136
+ return {
137
+ send: (type, payload) => {
138
+ if (ws.readyState !== 1) return;
139
+ ws.send(JSON.stringify({ type, data: payload }));
140
+ },
141
+ close: (code, reason) => ws.close(code, reason)
142
+ };
143
+ }
144
+ function subscribeEvents(ws, router, allowedEvents, subject, authConfig) {
145
+ const unsubscribes = [];
146
+ for (const eventName of allowedEvents) {
147
+ const eventDef = router.events[eventName];
148
+ if (!eventDef) continue;
149
+ const unsubscribe = eventDef.subscribe((payload) => {
150
+ if (ws.readyState !== 1) return;
151
+ if (subject && authConfig?.filter?.[eventName]) {
152
+ if (!authConfig.filter[eventName](subject, payload)) return;
153
+ }
154
+ try {
155
+ ws.send(JSON.stringify({ type: eventName, data: payload }));
156
+ } catch {
157
+ }
158
+ });
159
+ unsubscribes.push(unsubscribe);
160
+ }
161
+ return unsubscribes;
162
+ }
163
+ async function onClientMessage(data, router, connection, subject) {
164
+ let message;
165
+ try {
166
+ message = JSON.parse(data.toString());
167
+ } catch {
168
+ return;
169
+ }
170
+ const { type, data: payload } = message;
171
+ if (!type) return;
172
+ const handler = router.messages[type];
173
+ if (!handler) return;
174
+ try {
175
+ await handler({ payload, subject, ws: connection });
176
+ } catch (err) {
177
+ wsLogger.error(`WebSocket message handler error: ${type}`, err);
178
+ }
179
+ }
180
+ async function loadWSServer() {
181
+ try {
182
+ const mod = await import('ws');
183
+ const WS = mod.default ?? mod;
184
+ const WSS = WS.WebSocketServer ?? WS.Server;
185
+ if (typeof WSS !== "function") {
186
+ throw new Error(
187
+ "WebSocketServer not found in ws module. Ensure ws@^8 is installed: pnpm add ws"
188
+ );
189
+ }
190
+ return WSS;
191
+ } catch (err) {
192
+ if (err instanceof Error && err.message.includes("WebSocketServer not found")) {
193
+ throw err;
194
+ }
195
+ throw new Error(
196
+ '@spfn/core WebSocket support requires the "ws" package.\nInstall it with: pnpm add ws'
197
+ );
198
+ }
199
+ }
200
+
201
+ // src/event/ws/index.ts
202
+ function defineWSRouter(def) {
203
+ return {
204
+ events: def.events,
205
+ eventNames: Object.keys(def.events),
206
+ messages: def.messages ?? {},
207
+ _types: {}
208
+ };
209
+ }
210
+
211
+ export { attachWSHandler, defineWSRouter };
212
+ //# sourceMappingURL=index.js.map
213
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/event/ws/handler.ts","../../../src/event/ws/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\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 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 return;\n }\n if (subject === null)\n {\n ws.close(4001, 'Invalid or expired token');\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 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 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 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 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 // eslint-disable-next-line @typescript-eslint/no-explicit-any\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"]}
@@ -5,8 +5,9 @@ import { serve } from '@hono/node-server';
5
5
  import { NamedMiddleware, Router } from '@spfn/core/route';
6
6
  import { OnErrorContext } from '@spfn/core/middleware';
7
7
  import { J as JobRouter, B as BossOptions } from '../boss-Cxqc-Oiw.js';
8
- import { E as EventRouterDef } from '../router-Di7ENoah.js';
9
- import { S as SSEHandlerConfig, a as SSEAuthConfig } from '../types-DKQ90YL7.js';
8
+ import { E as EventRouterDef } from '../token-manager-CyG7la3p.js';
9
+ import { S as SSEHandlerConfig, a as SSEAuthConfig } from '../types-C1jMLGwK.js';
10
+ import { W as WSRouterDef, a as WSHandlerConfig, b as WSMessageHandlers, c as WSAuthConfig } from '../types-Cfj--lfr.js';
10
11
  import '@sinclair/typebox';
11
12
  import 'pg-boss';
12
13
 
@@ -193,6 +194,30 @@ interface ServerConfig {
193
194
  */
194
195
  path?: string;
195
196
  };
197
+ /**
198
+ * WebSocket router for bidirectional real-time communication
199
+ *
200
+ * @example
201
+ * ```typescript
202
+ * import { defineWSRouter } from '@spfn/core/event/ws';
203
+ *
204
+ * export default defineServerConfig()
205
+ * .websockets(wsRouter) // → WS /ws
206
+ * .build();
207
+ * ```
208
+ */
209
+ websockets?: WSRouterDef<any, any>;
210
+ /**
211
+ * WebSocket configuration options
212
+ * Only used if websockets router is provided
213
+ */
214
+ websocketsConfig?: WSHandlerConfig & {
215
+ /**
216
+ * WebSocket endpoint path
217
+ * @default '/ws'
218
+ */
219
+ path?: string;
220
+ };
196
221
  /**
197
222
  * Enable debug mode (default: NODE_ENV === 'development')
198
223
  */
@@ -836,6 +861,37 @@ declare class ServerConfigBuilder {
836
861
  path?: string;
837
862
  auth?: SSEAuthConfig<TRouter>;
838
863
  }): this;
864
+ /**
865
+ * Register WebSocket router for bidirectional real-time communication
866
+ *
867
+ * Enables type-safe WebSocket connections with:
868
+ * - Server→client event push (via defineEvent + emit)
869
+ * - Client→server message handling (via messages in defineWSRouter)
870
+ *
871
+ * @example
872
+ * ```typescript
873
+ * // src/server/ws.ts
874
+ * export const wsRouter = defineWSRouter({
875
+ * events: { userUpdated, notification },
876
+ * messages: {
877
+ * ping: ({ ws }) => ws.send('pong', {}),
878
+ * },
879
+ * });
880
+ *
881
+ * // server.config.ts
882
+ * export default defineServerConfig()
883
+ * .websockets(wsRouter) // → WS /ws
884
+ * .websockets(wsRouter, {
885
+ * path: '/realtime', // custom path
886
+ * auth: { enabled: true }, // token authentication
887
+ * })
888
+ * .build();
889
+ * ```
890
+ */
891
+ websockets<TEvents extends Record<string, any>, TMessages extends WSMessageHandlers, TRouter extends WSRouterDef<TEvents, TMessages>>(router: TRouter, config?: Omit<WSHandlerConfig, 'auth'> & {
892
+ path?: string;
893
+ auth?: WSAuthConfig<TRouter>;
894
+ }): this;
839
895
  /**
840
896
  * Enable/disable debug mode
841
897
  */