@roj-ai/transport 0.0.2
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/client/client-connection.d.ts +48 -0
- package/dist/client/client-connection.d.ts.map +1 -0
- package/dist/client/client-connection.js +160 -0
- package/dist/client/index.d.ts +10 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +7 -0
- package/dist/core/connection.d.ts +59 -0
- package/dist/core/connection.d.ts.map +1 -0
- package/dist/core/connection.js +165 -0
- package/dist/core/index.d.ts +16 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +13 -0
- package/dist/core/protocol.d.ts +206 -0
- package/dist/core/protocol.d.ts.map +1 -0
- package/dist/core/protocol.js +93 -0
- package/dist/core/result.d.ts +26 -0
- package/dist/core/result.d.ts.map +1 -0
- package/dist/core/result.js +20 -0
- package/dist/core/router.d.ts +79 -0
- package/dist/core/router.d.ts.map +1 -0
- package/dist/core/router.js +119 -0
- package/dist/core/types.d.ts +44 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +14 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/platform/browser.d.ts +14 -0
- package/dist/platform/browser.d.ts.map +1 -0
- package/dist/platform/browser.js +65 -0
- package/dist/platform/bun.d.ts +69 -0
- package/dist/platform/bun.d.ts.map +1 -0
- package/dist/platform/bun.js +152 -0
- package/dist/platform/types.d.ts +83 -0
- package/dist/platform/types.d.ts.map +1 -0
- package/dist/platform/types.js +14 -0
- package/dist/server/connection-manager.d.ts +106 -0
- package/dist/server/connection-manager.d.ts.map +1 -0
- package/dist/server/connection-manager.js +234 -0
- package/dist/server/index.d.ts +12 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +8 -0
- package/dist/server/server-connection.d.ts +50 -0
- package/dist/server/server-connection.d.ts.map +1 -0
- package/dist/server/server-connection.js +84 -0
- package/package.json +52 -0
- package/src/__tests__/integration.test.ts +443 -0
- package/src/client/client-connection.ts +207 -0
- package/src/client/index.ts +24 -0
- package/src/core/connection.ts +203 -0
- package/src/core/index.ts +52 -0
- package/src/core/protocol.test.ts +235 -0
- package/src/core/protocol.ts +348 -0
- package/src/core/result.ts +35 -0
- package/src/core/router.test.ts +248 -0
- package/src/core/router.ts +186 -0
- package/src/core/types.ts +85 -0
- package/src/index.ts +80 -0
- package/src/platform/browser.ts +79 -0
- package/src/platform/bun.ts +220 -0
- package/src/platform/types.ts +102 -0
- package/src/server/connection-manager.ts +298 -0
- package/src/server/index.ts +26 -0
- package/src/server/server-connection.ts +122 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message Router
|
|
3
|
+
*
|
|
4
|
+
* Handles notification dispatching - no request-response, no correlation.
|
|
5
|
+
* Simplified for notification-only protocols.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { HandlerContext, InferInput, Protocol, ProtocolDef, ProtocolHandlers } from './protocol.js'
|
|
9
|
+
import type { WireMessage } from './types.js'
|
|
10
|
+
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// Router Types
|
|
13
|
+
// ============================================================================
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Message send function type.
|
|
17
|
+
*/
|
|
18
|
+
export type MessageSender = (message: string) => boolean
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Router error types for debugging protocol issues.
|
|
22
|
+
*/
|
|
23
|
+
export type RouterErrorType = 'parse' | 'unknown_type' | 'validation' | 'handler'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Router error handler callback.
|
|
27
|
+
*/
|
|
28
|
+
export type RouterErrorHandler = (error: {
|
|
29
|
+
type: RouterErrorType
|
|
30
|
+
message: string
|
|
31
|
+
raw?: string
|
|
32
|
+
details?: unknown
|
|
33
|
+
}) => void
|
|
34
|
+
|
|
35
|
+
// ============================================================================
|
|
36
|
+
// Message Router
|
|
37
|
+
// ============================================================================
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Message router for handling incoming notifications and sending outbound notifications.
|
|
41
|
+
*
|
|
42
|
+
* Simplified notification-only router:
|
|
43
|
+
* - No request-response correlation
|
|
44
|
+
* - No pending requests tracking
|
|
45
|
+
* - Fire-and-forget notifications only
|
|
46
|
+
*/
|
|
47
|
+
/**
|
|
48
|
+
* Raw message listener — receives every parsed message before handler dispatch.
|
|
49
|
+
*/
|
|
50
|
+
export type RawMessageListener = (type: string, payload: unknown) => void
|
|
51
|
+
|
|
52
|
+
export class MessageRouter<TReceive extends ProtocolDef, TSend extends ProtocolDef> {
|
|
53
|
+
private readonly receiveProtocol: Protocol<TReceive> | null
|
|
54
|
+
private readonly sendProtocol: Protocol<TSend> | null
|
|
55
|
+
|
|
56
|
+
private handlers: Partial<ProtocolHandlers<TReceive>> = {}
|
|
57
|
+
private sender: MessageSender | null = null
|
|
58
|
+
private errorHandler: RouterErrorHandler | null = null
|
|
59
|
+
private rawMessageListener: RawMessageListener | null = null
|
|
60
|
+
|
|
61
|
+
constructor(
|
|
62
|
+
receiveProtocol: Protocol<TReceive> | null,
|
|
63
|
+
sendProtocol: Protocol<TSend> | null,
|
|
64
|
+
) {
|
|
65
|
+
this.receiveProtocol = receiveProtocol
|
|
66
|
+
this.sendProtocol = sendProtocol
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Set the message sender function.
|
|
71
|
+
*/
|
|
72
|
+
setSender(sender: MessageSender): void {
|
|
73
|
+
this.sender = sender
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Set handlers for incoming notifications.
|
|
78
|
+
*/
|
|
79
|
+
setHandlers(handlers: Partial<ProtocolHandlers<TReceive>>): void {
|
|
80
|
+
this.handlers = handlers
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Set error handler for debugging protocol issues.
|
|
85
|
+
*/
|
|
86
|
+
setErrorHandler(handler: RouterErrorHandler): void {
|
|
87
|
+
this.errorHandler = handler
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Set a raw message listener that receives every incoming message.
|
|
92
|
+
* When set, bypasses per-type handler dispatch — the listener receives (type, payload) directly.
|
|
93
|
+
*/
|
|
94
|
+
setRawMessageListener(listener: RawMessageListener): void {
|
|
95
|
+
this.rawMessageListener = listener
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Handle an incoming raw message (notification).
|
|
100
|
+
*/
|
|
101
|
+
async handleMessage(raw: string, ctx: HandlerContext): Promise<void> {
|
|
102
|
+
let parsed: WireMessage
|
|
103
|
+
try {
|
|
104
|
+
parsed = JSON.parse(raw)
|
|
105
|
+
} catch (e) {
|
|
106
|
+
this.errorHandler?.({ type: 'parse', message: 'Invalid JSON', raw, details: e })
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const { type, payload } = parsed
|
|
111
|
+
|
|
112
|
+
// Raw listener mode — forward everything without dispatch or validation
|
|
113
|
+
if (this.rawMessageListener) {
|
|
114
|
+
this.rawMessageListener(type, payload)
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (this.receiveProtocol) {
|
|
119
|
+
// Validated path — protocol provided, validate input before dispatching
|
|
120
|
+
const endpoint = this.receiveProtocol._def[type]
|
|
121
|
+
if (!endpoint) {
|
|
122
|
+
this.errorHandler?.({ type: 'unknown_type', message: `Unknown message type: ${type}`, raw })
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const inputResult = this.receiveProtocol.validateInput(type, payload)
|
|
127
|
+
if (!inputResult.success) {
|
|
128
|
+
this.errorHandler?.({ type: 'validation', message: `Validation failed for ${type}`, raw, details: inputResult.error })
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const handler = this.handlers[type]
|
|
133
|
+
if (!handler) return
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
await (handler as (input: unknown, ctx: HandlerContext) => Promise<void>)(inputResult.data, ctx)
|
|
137
|
+
} catch (e) {
|
|
138
|
+
this.errorHandler?.({ type: 'handler', message: `Handler error for ${type}`, raw, details: e })
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
// Unvalidated path — no protocol, dispatch directly (trusted source)
|
|
142
|
+
const handler = this.handlers[type]
|
|
143
|
+
if (!handler) return
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
await (handler as (input: unknown, ctx: HandlerContext) => Promise<void>)(payload, ctx)
|
|
147
|
+
} catch (e) {
|
|
148
|
+
this.errorHandler?.({ type: 'handler', message: `Handler error for ${type}`, raw, details: e })
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Send a notification (fire-and-forget).
|
|
155
|
+
*/
|
|
156
|
+
notify<K extends keyof TSend>(
|
|
157
|
+
name: K,
|
|
158
|
+
input: TSend[K] extends { input: infer I } ? (I extends { _type: string } ? never : InferInput<TSend[K]>) : never,
|
|
159
|
+
): boolean {
|
|
160
|
+
if (!this.sender) {
|
|
161
|
+
return false
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const message: WireMessage = {
|
|
165
|
+
type: String(name),
|
|
166
|
+
payload: input,
|
|
167
|
+
ts: Date.now(),
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return this.sender(JSON.stringify(message))
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ============================================================================
|
|
175
|
+
// Router Builder
|
|
176
|
+
// ============================================================================
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Create a message router for the given protocols.
|
|
180
|
+
*/
|
|
181
|
+
export function createRouter<TReceive extends ProtocolDef, TSend extends ProtocolDef>(
|
|
182
|
+
receiveProtocol: Protocol<TReceive> | null,
|
|
183
|
+
sendProtocol: Protocol<TSend> | null,
|
|
184
|
+
): MessageRouter<TReceive, TSend> {
|
|
185
|
+
return new MessageRouter(receiveProtocol, sendProtocol)
|
|
186
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core Transport Types
|
|
3
|
+
*
|
|
4
|
+
* Platform-agnostic types for WebSocket transport infrastructure.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Connection States
|
|
9
|
+
// ============================================================================
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Connection state machine states.
|
|
13
|
+
*/
|
|
14
|
+
export type ConnectionState =
|
|
15
|
+
| 'disconnected'
|
|
16
|
+
| 'connecting'
|
|
17
|
+
| 'connected'
|
|
18
|
+
| 'reconnecting'
|
|
19
|
+
|
|
20
|
+
// ============================================================================
|
|
21
|
+
// Wire Message Format
|
|
22
|
+
// ============================================================================
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Wire message envelope for all protocol messages.
|
|
26
|
+
* Simplified: notification-only, no correlation ID.
|
|
27
|
+
*/
|
|
28
|
+
export interface WireMessage<T = unknown> {
|
|
29
|
+
/** Notification type name */
|
|
30
|
+
type: string
|
|
31
|
+
/** Message payload */
|
|
32
|
+
payload: T
|
|
33
|
+
/** Timestamp */
|
|
34
|
+
ts: number
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ============================================================================
|
|
38
|
+
// Transport Events
|
|
39
|
+
// ============================================================================
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Transport event types.
|
|
43
|
+
*/
|
|
44
|
+
export type TransportEvent =
|
|
45
|
+
| 'connected'
|
|
46
|
+
| 'disconnected'
|
|
47
|
+
| 'error'
|
|
48
|
+
| 'reconnecting'
|
|
49
|
+
| 'reconnected'
|
|
50
|
+
| 'state_change'
|
|
51
|
+
| 'router_error'
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Transport event listener.
|
|
55
|
+
*/
|
|
56
|
+
export type TransportEventListener = (event: TransportEvent, data?: unknown) => void
|
|
57
|
+
|
|
58
|
+
// ============================================================================
|
|
59
|
+
// Configuration
|
|
60
|
+
// ============================================================================
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Reconnection strategy options.
|
|
64
|
+
*/
|
|
65
|
+
export interface ReconnectOptions {
|
|
66
|
+
/** Base delay in ms (default: 1000) */
|
|
67
|
+
baseDelayMs: number
|
|
68
|
+
/** Maximum delay in ms (default: 30000) */
|
|
69
|
+
maxDelayMs: number
|
|
70
|
+
/** Maximum number of attempts before giving up (default: Infinity) */
|
|
71
|
+
maxAttempts: number
|
|
72
|
+
/** Jitter factor (0-1, default: 0.3) */
|
|
73
|
+
jitterFactor: number
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ============================================================================
|
|
77
|
+
// Default Values
|
|
78
|
+
// ============================================================================
|
|
79
|
+
|
|
80
|
+
export const DEFAULT_RECONNECT_OPTIONS: ReconnectOptions = {
|
|
81
|
+
baseDelayMs: 1000,
|
|
82
|
+
maxDelayMs: 30000,
|
|
83
|
+
maxAttempts: Infinity,
|
|
84
|
+
jitterFactor: 0.3,
|
|
85
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @roj-ai/transport
|
|
3
|
+
*
|
|
4
|
+
* Platform-agnostic WebSocket transport infrastructure with RPC-like protocol API.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Core Exports
|
|
9
|
+
// ============================================================================
|
|
10
|
+
|
|
11
|
+
// Protocol Definition API
|
|
12
|
+
export { defineProtocol, method, notification } from './core/protocol.js'
|
|
13
|
+
export type {
|
|
14
|
+
CallerFor,
|
|
15
|
+
EndpointDef,
|
|
16
|
+
EndpointHandler,
|
|
17
|
+
ExtractProtocolDef,
|
|
18
|
+
HandlerContext,
|
|
19
|
+
HandlersFor,
|
|
20
|
+
InferError,
|
|
21
|
+
InferInput,
|
|
22
|
+
InferOutput,
|
|
23
|
+
IsMethod,
|
|
24
|
+
IsNotification,
|
|
25
|
+
MethodCaller,
|
|
26
|
+
MethodDef,
|
|
27
|
+
MethodHandler,
|
|
28
|
+
MethodOptions,
|
|
29
|
+
NotificationDef,
|
|
30
|
+
NotificationHandler,
|
|
31
|
+
NotificationNotifier,
|
|
32
|
+
NotificationOptions,
|
|
33
|
+
NotifierFor,
|
|
34
|
+
Protocol,
|
|
35
|
+
ProtocolCaller,
|
|
36
|
+
ProtocolDef,
|
|
37
|
+
ProtocolHandlers,
|
|
38
|
+
ProtocolNotifier,
|
|
39
|
+
} from './core/protocol.js'
|
|
40
|
+
|
|
41
|
+
// Result Type
|
|
42
|
+
export { Err, flatMapResult, isErr, isOk, mapResult, Ok, unwrapOr, unwrapOrThrow } from './core/result.js'
|
|
43
|
+
export type { Result } from './core/result.js'
|
|
44
|
+
|
|
45
|
+
// Types
|
|
46
|
+
export type { ConnectionState, ReconnectOptions, TransportEvent, TransportEventListener, WireMessage } from './core/types.js'
|
|
47
|
+
|
|
48
|
+
export { DEFAULT_RECONNECT_OPTIONS } from './core/types.js'
|
|
49
|
+
|
|
50
|
+
// Core Components
|
|
51
|
+
export { createRouter, MessageRouter } from './core/router.js'
|
|
52
|
+
export type { MessageSender, RawMessageListener, RouterErrorHandler, RouterErrorType } from './core/router.js'
|
|
53
|
+
|
|
54
|
+
export { Connection } from './core/connection.js'
|
|
55
|
+
export type { ConnectionConfig } from './core/connection.js'
|
|
56
|
+
|
|
57
|
+
// ============================================================================
|
|
58
|
+
// Platform Exports
|
|
59
|
+
// ============================================================================
|
|
60
|
+
|
|
61
|
+
export type { ICloseEvent, IServerWebSocket, IWebSocket, IWebSocketFactory } from './platform/types.js'
|
|
62
|
+
|
|
63
|
+
export { WebSocketReadyState } from './platform/types.js'
|
|
64
|
+
|
|
65
|
+
// ============================================================================
|
|
66
|
+
// Client Exports
|
|
67
|
+
// ============================================================================
|
|
68
|
+
|
|
69
|
+
export { ClientConnection, createClientConnection } from './client/client-connection.js'
|
|
70
|
+
export type { ClientConnectionConfig } from './client/client-connection.js'
|
|
71
|
+
|
|
72
|
+
// ============================================================================
|
|
73
|
+
// Server Exports
|
|
74
|
+
// ============================================================================
|
|
75
|
+
|
|
76
|
+
export { createServerConnection, ServerConnection } from './server/server-connection.js'
|
|
77
|
+
export type { ServerConnectionConfig, ServerHandlerContext } from './server/server-connection.js'
|
|
78
|
+
|
|
79
|
+
export { ConnectionManager } from './server/connection-manager.js'
|
|
80
|
+
export type { ConnectionStats } from './server/connection-manager.js'
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser WebSocket Adapter
|
|
3
|
+
*
|
|
4
|
+
* Wraps the native browser WebSocket to conform to IWebSocket interface.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ICloseEvent, IWebSocket, IWebSocketFactory } from './types.js'
|
|
8
|
+
import { WebSocketReadyState } from './types.js'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Browser WebSocket adapter that wraps native WebSocket.
|
|
12
|
+
*/
|
|
13
|
+
class BrowserWebSocketAdapter implements IWebSocket {
|
|
14
|
+
private readonly ws: WebSocket
|
|
15
|
+
|
|
16
|
+
onopen: (() => void) | null = null
|
|
17
|
+
onclose: ((event: ICloseEvent) => void) | null = null
|
|
18
|
+
onerror: ((error: Error) => void) | null = null
|
|
19
|
+
onmessage: ((data: string) => void) | null = null
|
|
20
|
+
|
|
21
|
+
constructor(url: string) {
|
|
22
|
+
this.ws = new WebSocket(url)
|
|
23
|
+
|
|
24
|
+
this.ws.onopen = () => {
|
|
25
|
+
this.onopen?.()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
this.ws.onclose = (event) => {
|
|
29
|
+
this.onclose?.({
|
|
30
|
+
code: event.code,
|
|
31
|
+
reason: event.reason,
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
this.ws.onerror = () => {
|
|
36
|
+
// Browser WebSocket doesn't provide error details
|
|
37
|
+
this.onerror?.(new Error('WebSocket error'))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
this.ws.onmessage = (event) => {
|
|
41
|
+
// Handle text data only
|
|
42
|
+
if (typeof event.data === 'string') {
|
|
43
|
+
this.onmessage?.(event.data)
|
|
44
|
+
} else if (event.data instanceof Blob) {
|
|
45
|
+
// Convert Blob to string for text-only protocol
|
|
46
|
+
event.data.text().then((text) => {
|
|
47
|
+
this.onmessage?.(text)
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
get readyState(): WebSocketReadyState {
|
|
54
|
+
return this.ws.readyState as WebSocketReadyState
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
send(data: string): void {
|
|
58
|
+
if (this.ws.readyState === WebSocketReadyState.OPEN) {
|
|
59
|
+
this.ws.send(data)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
close(code?: number, reason?: string): void {
|
|
64
|
+
this.ws.close(code, reason)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Browser WebSocket factory.
|
|
70
|
+
*/
|
|
71
|
+
export const browserWebSocketFactory: IWebSocketFactory = {
|
|
72
|
+
create(url: string): IWebSocket {
|
|
73
|
+
return new BrowserWebSocketAdapter(url)
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Re-export types for convenience
|
|
78
|
+
export { WebSocketReadyState }
|
|
79
|
+
export type { ICloseEvent, IWebSocket, IWebSocketFactory }
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bun WebSocket Adapters
|
|
3
|
+
*
|
|
4
|
+
* Provides both client and server WebSocket adapters for Bun runtime.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ICloseEvent, IServerWebSocket, IWebSocket, IWebSocketFactory } from './types.js'
|
|
8
|
+
import { WebSocketReadyState } from './types.js'
|
|
9
|
+
|
|
10
|
+
// Type for Bun's native WebSocket
|
|
11
|
+
type BunWebSocket = WebSocket
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Bun WebSocket client adapter.
|
|
15
|
+
* Wraps Bun's WebSocket client to conform to IWebSocket interface.
|
|
16
|
+
*/
|
|
17
|
+
class BunWebSocketClientAdapter implements IWebSocket {
|
|
18
|
+
private readonly ws: BunWebSocket
|
|
19
|
+
|
|
20
|
+
onopen: (() => void) | null = null
|
|
21
|
+
onclose: ((event: ICloseEvent) => void) | null = null
|
|
22
|
+
onerror: ((error: Error) => void) | null = null
|
|
23
|
+
onmessage: ((data: string) => void) | null = null
|
|
24
|
+
|
|
25
|
+
constructor(url: string) {
|
|
26
|
+
this.ws = new WebSocket(url)
|
|
27
|
+
|
|
28
|
+
this.ws.onopen = () => {
|
|
29
|
+
this.onopen?.()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
this.ws.onclose = (event) => {
|
|
33
|
+
this.onclose?.({
|
|
34
|
+
code: event.code,
|
|
35
|
+
reason: event.reason,
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
this.ws.onerror = () => {
|
|
40
|
+
this.onerror?.(new Error('WebSocket error'))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
this.ws.onmessage = (event) => {
|
|
44
|
+
if (typeof event.data === 'string') {
|
|
45
|
+
this.onmessage?.(event.data)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
get readyState(): WebSocketReadyState {
|
|
51
|
+
return this.ws.readyState as WebSocketReadyState
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
send(data: string): void {
|
|
55
|
+
if (this.ws.readyState === WebSocketReadyState.OPEN) {
|
|
56
|
+
this.ws.send(data)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
close(code?: number, reason?: string): void {
|
|
61
|
+
this.ws.close(code, reason)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Bun WebSocket client factory.
|
|
67
|
+
*/
|
|
68
|
+
export const bunWebSocketFactory: IWebSocketFactory = {
|
|
69
|
+
create(url: string): IWebSocket {
|
|
70
|
+
return new BunWebSocketClientAdapter(url)
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Server WebSocket data interface.
|
|
76
|
+
*/
|
|
77
|
+
export interface ServerWebSocketData<T = unknown> {
|
|
78
|
+
/** User-attached data */
|
|
79
|
+
userData: T
|
|
80
|
+
/** Topics this connection is subscribed to */
|
|
81
|
+
topics: Set<string>
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Using 'any' here because Bun's ServerWebSocket has complex generics
|
|
85
|
+
// that would require importing Bun types directly
|
|
86
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
87
|
+
type BunServerWebSocket = any
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Bun server WebSocket adapter.
|
|
91
|
+
* Wraps Bun's ServerWebSocket to conform to IServerWebSocket interface.
|
|
92
|
+
*/
|
|
93
|
+
export class BunServerWebSocketAdapter<T = unknown> implements IServerWebSocket<T> {
|
|
94
|
+
private readonly ws: BunServerWebSocket
|
|
95
|
+
private wsData: ServerWebSocketData<T>
|
|
96
|
+
|
|
97
|
+
onopen: (() => void) | null = null
|
|
98
|
+
onclose: ((event: ICloseEvent) => void) | null = null
|
|
99
|
+
onerror: ((error: Error) => void) | null = null
|
|
100
|
+
onmessage: ((data: string) => void) | null = null
|
|
101
|
+
|
|
102
|
+
constructor(ws: BunServerWebSocket, data: T) {
|
|
103
|
+
this.ws = ws
|
|
104
|
+
this.wsData = {
|
|
105
|
+
userData: data,
|
|
106
|
+
topics: new Set(),
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
get data(): T {
|
|
111
|
+
return this.wsData.userData
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
set data(value: T) {
|
|
115
|
+
this.wsData.userData = value
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
get readyState(): WebSocketReadyState {
|
|
119
|
+
return this.ws.readyState as WebSocketReadyState
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
send(data: string): void {
|
|
123
|
+
if (this.ws.readyState === WebSocketReadyState.OPEN) {
|
|
124
|
+
this.ws.send(data)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
close(code?: number, reason?: string): void {
|
|
129
|
+
this.ws.close(code, reason)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
subscribe(topic: string): void {
|
|
133
|
+
this.ws.subscribe(topic)
|
|
134
|
+
this.wsData.topics.add(topic)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
unsubscribe(topic: string): void {
|
|
138
|
+
this.ws.unsubscribe(topic)
|
|
139
|
+
this.wsData.topics.delete(topic)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
publish(topic: string, message: string): void {
|
|
143
|
+
this.ws.publish(topic, message)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
isSubscribed(topic: string): boolean {
|
|
147
|
+
return this.wsData.topics.has(topic)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Get the underlying Bun WebSocket (for advanced use cases).
|
|
152
|
+
*/
|
|
153
|
+
getRawWebSocket(): BunServerWebSocket {
|
|
154
|
+
return this.ws
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Helper to create Bun WebSocket handlers that integrate with the transport layer.
|
|
160
|
+
*/
|
|
161
|
+
export interface BunWebSocketHandlers<T> {
|
|
162
|
+
open: (ws: BunServerWebSocket) => void
|
|
163
|
+
close: (ws: BunServerWebSocket, code: number, reason: string) => void
|
|
164
|
+
message: (ws: BunServerWebSocket, message: string | Buffer) => void
|
|
165
|
+
error: (ws: BunServerWebSocket, error: Error) => void
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Create Bun WebSocket handlers with adapter integration.
|
|
170
|
+
*/
|
|
171
|
+
export function createBunWebSocketHandlers<T>(callbacks: {
|
|
172
|
+
onOpen: (ws: IServerWebSocket<T>) => void
|
|
173
|
+
onClose: (ws: IServerWebSocket<T>, code: number, reason: string) => void
|
|
174
|
+
onMessage: (ws: IServerWebSocket<T>, message: string) => void
|
|
175
|
+
onError: (ws: IServerWebSocket<T>, error: Error) => void
|
|
176
|
+
getData: (ws: BunServerWebSocket) => T
|
|
177
|
+
}): BunWebSocketHandlers<T> {
|
|
178
|
+
// Map to store adapter instances
|
|
179
|
+
const adapters = new WeakMap<BunServerWebSocket, BunServerWebSocketAdapter<T>>()
|
|
180
|
+
|
|
181
|
+
const getOrCreateAdapter = (ws: BunServerWebSocket): BunServerWebSocketAdapter<T> => {
|
|
182
|
+
let adapter = adapters.get(ws)
|
|
183
|
+
if (!adapter) {
|
|
184
|
+
adapter = new BunServerWebSocketAdapter(ws, callbacks.getData(ws))
|
|
185
|
+
adapters.set(ws, adapter)
|
|
186
|
+
}
|
|
187
|
+
return adapter
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
open(ws: BunServerWebSocket) {
|
|
192
|
+
const adapter = getOrCreateAdapter(ws)
|
|
193
|
+
callbacks.onOpen(adapter)
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
close(ws: BunServerWebSocket, code: number, reason: string) {
|
|
197
|
+
const adapter = adapters.get(ws)
|
|
198
|
+
if (adapter) {
|
|
199
|
+
callbacks.onClose(adapter, code, reason)
|
|
200
|
+
adapters.delete(ws)
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
message(ws: BunServerWebSocket, message: string | Buffer) {
|
|
205
|
+
const adapter = getOrCreateAdapter(ws)
|
|
206
|
+
// Convert Buffer to string if needed
|
|
207
|
+
const data = typeof message === 'string' ? message : message.toString('utf-8')
|
|
208
|
+
callbacks.onMessage(adapter, data)
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
error(ws: BunServerWebSocket, error: Error) {
|
|
212
|
+
const adapter = getOrCreateAdapter(ws)
|
|
213
|
+
callbacks.onError(adapter, error)
|
|
214
|
+
},
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Re-export types for convenience
|
|
219
|
+
export { WebSocketReadyState }
|
|
220
|
+
export type { ICloseEvent, IServerWebSocket, IWebSocket, IWebSocketFactory }
|