@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.
Files changed (64) hide show
  1. package/dist/client/client-connection.d.ts +48 -0
  2. package/dist/client/client-connection.d.ts.map +1 -0
  3. package/dist/client/client-connection.js +160 -0
  4. package/dist/client/index.d.ts +10 -0
  5. package/dist/client/index.d.ts.map +1 -0
  6. package/dist/client/index.js +7 -0
  7. package/dist/core/connection.d.ts +59 -0
  8. package/dist/core/connection.d.ts.map +1 -0
  9. package/dist/core/connection.js +165 -0
  10. package/dist/core/index.d.ts +16 -0
  11. package/dist/core/index.d.ts.map +1 -0
  12. package/dist/core/index.js +13 -0
  13. package/dist/core/protocol.d.ts +206 -0
  14. package/dist/core/protocol.d.ts.map +1 -0
  15. package/dist/core/protocol.js +93 -0
  16. package/dist/core/result.d.ts +26 -0
  17. package/dist/core/result.d.ts.map +1 -0
  18. package/dist/core/result.js +20 -0
  19. package/dist/core/router.d.ts +79 -0
  20. package/dist/core/router.d.ts.map +1 -0
  21. package/dist/core/router.js +119 -0
  22. package/dist/core/types.d.ts +44 -0
  23. package/dist/core/types.d.ts.map +1 -0
  24. package/dist/core/types.js +14 -0
  25. package/dist/index.d.ts +24 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +26 -0
  28. package/dist/platform/browser.d.ts +14 -0
  29. package/dist/platform/browser.d.ts.map +1 -0
  30. package/dist/platform/browser.js +65 -0
  31. package/dist/platform/bun.d.ts +69 -0
  32. package/dist/platform/bun.d.ts.map +1 -0
  33. package/dist/platform/bun.js +152 -0
  34. package/dist/platform/types.d.ts +83 -0
  35. package/dist/platform/types.d.ts.map +1 -0
  36. package/dist/platform/types.js +14 -0
  37. package/dist/server/connection-manager.d.ts +106 -0
  38. package/dist/server/connection-manager.d.ts.map +1 -0
  39. package/dist/server/connection-manager.js +234 -0
  40. package/dist/server/index.d.ts +12 -0
  41. package/dist/server/index.d.ts.map +1 -0
  42. package/dist/server/index.js +8 -0
  43. package/dist/server/server-connection.d.ts +50 -0
  44. package/dist/server/server-connection.d.ts.map +1 -0
  45. package/dist/server/server-connection.js +84 -0
  46. package/package.json +52 -0
  47. package/src/__tests__/integration.test.ts +443 -0
  48. package/src/client/client-connection.ts +207 -0
  49. package/src/client/index.ts +24 -0
  50. package/src/core/connection.ts +203 -0
  51. package/src/core/index.ts +52 -0
  52. package/src/core/protocol.test.ts +235 -0
  53. package/src/core/protocol.ts +348 -0
  54. package/src/core/result.ts +35 -0
  55. package/src/core/router.test.ts +248 -0
  56. package/src/core/router.ts +186 -0
  57. package/src/core/types.ts +85 -0
  58. package/src/index.ts +80 -0
  59. package/src/platform/browser.ts +79 -0
  60. package/src/platform/bun.ts +220 -0
  61. package/src/platform/types.ts +102 -0
  62. package/src/server/connection-manager.ts +298 -0
  63. package/src/server/index.ts +26 -0
  64. package/src/server/server-connection.ts +122 -0
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Platform Abstraction Types
3
+ *
4
+ * Common interfaces for WebSocket implementations across platforms (browser + Bun).
5
+ */
6
+
7
+ /**
8
+ * WebSocket ready state values.
9
+ */
10
+ export const WebSocketReadyState = {
11
+ CONNECTING: 0,
12
+ OPEN: 1,
13
+ CLOSING: 2,
14
+ CLOSED: 3,
15
+ } as const
16
+
17
+ export type WebSocketReadyState = (typeof WebSocketReadyState)[keyof typeof WebSocketReadyState]
18
+
19
+ /**
20
+ * Close event structure.
21
+ */
22
+ export interface ICloseEvent {
23
+ code: number
24
+ reason: string
25
+ }
26
+
27
+ /**
28
+ * Platform-agnostic WebSocket interface.
29
+ */
30
+ export interface IWebSocket {
31
+ /** Current ready state */
32
+ readonly readyState: WebSocketReadyState
33
+
34
+ /** Send a text message */
35
+ send(data: string): void
36
+
37
+ /** Close the connection */
38
+ close(code?: number, reason?: string): void
39
+
40
+ /** Called when connection opens */
41
+ onopen: (() => void) | null
42
+
43
+ /** Called when connection closes */
44
+ onclose: ((event: ICloseEvent) => void) | null
45
+
46
+ /** Called on error */
47
+ onerror: ((error: Error) => void) | null
48
+
49
+ /** Called when a message is received */
50
+ onmessage: ((data: string) => void) | null
51
+ }
52
+
53
+ /**
54
+ * Factory for creating WebSocket client connections.
55
+ */
56
+ export interface IWebSocketFactory {
57
+ /** Create a new WebSocket connection to the given URL */
58
+ create(url: string): IWebSocket
59
+ }
60
+
61
+ /**
62
+ * Server-side WebSocket interface (for handling incoming connections).
63
+ * Extended from IWebSocket with server-specific capabilities.
64
+ */
65
+ export interface IServerWebSocket<T = unknown> extends IWebSocket {
66
+ /** Attached data for this connection */
67
+ data: T
68
+
69
+ /** Subscribe to a topic for pub/sub messaging */
70
+ subscribe(topic: string): void
71
+
72
+ /** Unsubscribe from a topic */
73
+ unsubscribe(topic: string): void
74
+
75
+ /** Publish to a topic (message sent to all subscribers except sender) */
76
+ publish(topic: string, message: string): void
77
+
78
+ /** Check if subscribed to a topic */
79
+ isSubscribed(topic: string): boolean
80
+ }
81
+
82
+ /**
83
+ * WebSocket server upgrade request data.
84
+ */
85
+ export interface IUpgradeData<T = unknown> {
86
+ /** Data to attach to the WebSocket connection */
87
+ data: T
88
+ }
89
+
90
+ /**
91
+ * WebSocket server interface for handling multiple connections.
92
+ */
93
+ export interface IWebSocketServer<T = unknown> {
94
+ /** Handle WebSocket upgrade from HTTP request */
95
+ upgrade(request: Request, data: T): boolean
96
+
97
+ /** Publish to all connections subscribed to a topic */
98
+ publish(topic: string, message: string): void
99
+
100
+ /** Get all active connections */
101
+ getConnections(): Iterable<IServerWebSocket<T>>
102
+ }
@@ -0,0 +1,298 @@
1
+ /**
2
+ * Connection Manager
3
+ *
4
+ * Manages multiple WebSocket connections and session subscriptions.
5
+ */
6
+
7
+ import type { ProtocolDef } from '../core/protocol.js'
8
+ import type { ServerConnection } from './server-connection.js'
9
+
10
+ // ============================================================================
11
+ // Types
12
+ // ============================================================================
13
+
14
+ /**
15
+ * Connection statistics.
16
+ */
17
+ export interface ConnectionStats {
18
+ /** Total number of connections */
19
+ totalConnections: number
20
+ /** Number of active (connected) connections */
21
+ activeConnections: number
22
+ /** Number of unique sessions with subscribers */
23
+ activeSessions: number
24
+ /** Connections by client type */
25
+ byClientType: Record<string, number>
26
+ }
27
+
28
+ // ============================================================================
29
+ // Connection Manager
30
+ // ============================================================================
31
+
32
+ /**
33
+ * Manages multiple server connections and session subscriptions.
34
+ *
35
+ * Features:
36
+ * - Track all active connections
37
+ * - Manage session subscriptions
38
+ * - Broadcast to subscribed connections
39
+ * - Connection statistics
40
+ */
41
+ export class ConnectionManager<
42
+ TReceive extends ProtocolDef = ProtocolDef,
43
+ TSend extends ProtocolDef = ProtocolDef,
44
+ > {
45
+ private connections = new Map<string, ServerConnection<TReceive, TSend>>()
46
+ private sessionSubscriptions = new Map<string, Set<string>>() // sessionId -> connectionIds
47
+
48
+ // ============================================================================
49
+ // Connection Management
50
+ // ============================================================================
51
+
52
+ /**
53
+ * Add a connection.
54
+ */
55
+ add(connection: ServerConnection<TReceive, TSend>): void {
56
+ this.connections.set(connection.getConnectionId(), connection)
57
+ }
58
+
59
+ /**
60
+ * Remove a connection.
61
+ */
62
+ remove(connectionId: string): void {
63
+ const connection = this.connections.get(connectionId)
64
+ if (!connection) return
65
+
66
+ // Clean up subscriptions
67
+ for (const sessionId of connection.getSubscribedSessions()) {
68
+ this.unsubscribeInternal(connectionId, sessionId)
69
+ }
70
+
71
+ this.connections.delete(connectionId)
72
+ }
73
+
74
+ /**
75
+ * Get a connection by ID.
76
+ */
77
+ get(connectionId: string): ServerConnection<TReceive, TSend> | undefined {
78
+ return this.connections.get(connectionId)
79
+ }
80
+
81
+ /**
82
+ * Check if a connection exists.
83
+ */
84
+ has(connectionId: string): boolean {
85
+ return this.connections.has(connectionId)
86
+ }
87
+
88
+ /**
89
+ * Get all connections.
90
+ */
91
+ getAll(): Iterable<ServerConnection<TReceive, TSend>> {
92
+ return this.connections.values()
93
+ }
94
+
95
+ /**
96
+ * Get all connection IDs.
97
+ */
98
+ getAllIds(): Iterable<string> {
99
+ return this.connections.keys()
100
+ }
101
+
102
+ /**
103
+ * Get connection count.
104
+ */
105
+ get size(): number {
106
+ return this.connections.size
107
+ }
108
+
109
+ // ============================================================================
110
+ // Session Subscriptions
111
+ // ============================================================================
112
+
113
+ /**
114
+ * Subscribe a connection to a session.
115
+ */
116
+ subscribe(connectionId: string, sessionId: string): boolean {
117
+ const connection = this.connections.get(connectionId)
118
+ if (!connection) return false
119
+
120
+ connection.subscribe(sessionId)
121
+
122
+ let subscribers = this.sessionSubscriptions.get(sessionId)
123
+ if (!subscribers) {
124
+ subscribers = new Set()
125
+ this.sessionSubscriptions.set(sessionId, subscribers)
126
+ }
127
+ subscribers.add(connectionId)
128
+
129
+ return true
130
+ }
131
+
132
+ /**
133
+ * Unsubscribe a connection from a session.
134
+ */
135
+ unsubscribe(connectionId: string, sessionId: string): boolean {
136
+ const connection = this.connections.get(connectionId)
137
+ if (!connection) return false
138
+
139
+ connection.unsubscribe(sessionId)
140
+ this.unsubscribeInternal(connectionId, sessionId)
141
+
142
+ return true
143
+ }
144
+
145
+ /**
146
+ * Get all subscribers for a session.
147
+ */
148
+ getSubscribers(sessionId: string): ServerConnection<TReceive, TSend>[] {
149
+ const subscriberIds = this.sessionSubscriptions.get(sessionId)
150
+ if (!subscriberIds) return []
151
+
152
+ const subscribers: ServerConnection<TReceive, TSend>[] = []
153
+ const deadIds: string[] = []
154
+ for (const connectionId of subscriberIds) {
155
+ const connection = this.connections.get(connectionId)
156
+ if (connection?.isConnected()) {
157
+ subscribers.push(connection)
158
+ } else {
159
+ deadIds.push(connectionId)
160
+ }
161
+ }
162
+
163
+ // Clean up dead subscriptions
164
+ for (const id of deadIds) {
165
+ subscriberIds.delete(id)
166
+ }
167
+ if (subscriberIds.size === 0) {
168
+ this.sessionSubscriptions.delete(sessionId)
169
+ }
170
+
171
+ return subscribers
172
+ }
173
+
174
+ /**
175
+ * Get subscriber count for a session.
176
+ */
177
+ getSubscriberCount(sessionId: string): number {
178
+ return this.sessionSubscriptions.get(sessionId)?.size ?? 0
179
+ }
180
+
181
+ /**
182
+ * Check if a session has any subscribers.
183
+ */
184
+ hasSubscribers(sessionId: string): boolean {
185
+ return this.getSubscriberCount(sessionId) > 0
186
+ }
187
+
188
+ // ============================================================================
189
+ // Broadcasting
190
+ // ============================================================================
191
+
192
+ /**
193
+ * Broadcast a raw message to all subscribers of a session.
194
+ */
195
+ broadcast(sessionId: string, message: string): number {
196
+ const subscribers = this.getSubscribers(sessionId)
197
+ let sent = 0
198
+
199
+ for (const connection of subscribers) {
200
+ if (connection.send(message)) {
201
+ sent++
202
+ }
203
+ }
204
+
205
+ return sent
206
+ }
207
+
208
+ /**
209
+ * Broadcast to all connections.
210
+ */
211
+ broadcastToAll(message: string): number {
212
+ let sent = 0
213
+
214
+ for (const connection of this.connections.values()) {
215
+ if (connection.isConnected() && connection.send(message)) {
216
+ sent++
217
+ }
218
+ }
219
+
220
+ return sent
221
+ }
222
+
223
+ // ============================================================================
224
+ // Statistics
225
+ // ============================================================================
226
+
227
+ /**
228
+ * Get connection statistics.
229
+ */
230
+ getStats(): ConnectionStats {
231
+ const byClientType: Record<string, number> = {}
232
+ let activeConnections = 0
233
+
234
+ for (const connection of this.connections.values()) {
235
+ const clientType = connection.getClientType()
236
+ byClientType[clientType] = (byClientType[clientType] ?? 0) + 1
237
+
238
+ if (connection.isConnected()) {
239
+ activeConnections++
240
+ }
241
+ }
242
+
243
+ return {
244
+ totalConnections: this.connections.size,
245
+ activeConnections,
246
+ activeSessions: this.sessionSubscriptions.size,
247
+ byClientType,
248
+ }
249
+ }
250
+
251
+ // ============================================================================
252
+ // Cleanup
253
+ // ============================================================================
254
+
255
+ /**
256
+ * Clear all connections.
257
+ */
258
+ clear(): void {
259
+ for (const connection of this.connections.values()) {
260
+ connection.disconnect()
261
+ }
262
+ this.connections.clear()
263
+ this.sessionSubscriptions.clear()
264
+ }
265
+
266
+ /**
267
+ * Remove disconnected connections.
268
+ */
269
+ cleanup(): number {
270
+ let removed = 0
271
+
272
+ for (const [connectionId, connection] of this.connections) {
273
+ if (!connection.isConnected()) {
274
+ this.remove(connectionId)
275
+ removed++
276
+ }
277
+ }
278
+
279
+ return removed
280
+ }
281
+
282
+ // ============================================================================
283
+ // Private Methods
284
+ // ============================================================================
285
+
286
+ /**
287
+ * Internal unsubscribe (updates subscription map only).
288
+ */
289
+ private unsubscribeInternal(connectionId: string, sessionId: string): void {
290
+ const subscribers = this.sessionSubscriptions.get(sessionId)
291
+ if (subscribers) {
292
+ subscribers.delete(connectionId)
293
+ if (subscribers.size === 0) {
294
+ this.sessionSubscriptions.delete(sessionId)
295
+ }
296
+ }
297
+ }
298
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Server Connection Module
3
+ *
4
+ * WebSocket server for handling incoming client connections.
5
+ */
6
+
7
+ export { createServerConnection, ServerConnection } from './server-connection.js'
8
+ export type { ServerConnectionConfig, ServerHandlerContext } from './server-connection.js'
9
+
10
+ export { ConnectionManager } from './connection-manager.js'
11
+ export type { ConnectionStats } from './connection-manager.js'
12
+
13
+ // Re-export core types commonly used with server
14
+ export type {
15
+ ConnectionState,
16
+ HandlerContext,
17
+ Protocol,
18
+ ProtocolCaller,
19
+ ProtocolDef,
20
+ ProtocolHandlers,
21
+ ProtocolNotifier,
22
+ TransportEvent,
23
+ TransportEventListener,
24
+ } from '../core/index.js'
25
+
26
+ export { defineProtocol, method, notification } from '../core/index.js'
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Server Connection
3
+ *
4
+ * Represents a single client connection on the server side.
5
+ * Simplified: no handshake - connection is ready immediately when attached.
6
+ */
7
+
8
+ import { Connection } from '../core/connection.js'
9
+ import type { ConnectionConfig } from '../core/connection.js'
10
+ import type { HandlerContext, ProtocolDef } from '../core/protocol.js'
11
+ import type { IServerWebSocket } from '../platform/types.js'
12
+ import { WebSocketReadyState } from '../platform/types.js'
13
+
14
+ /**
15
+ * Server connection configuration.
16
+ */
17
+ export interface ServerConnectionConfig<TReceive extends ProtocolDef, TSend extends ProtocolDef> extends ConnectionConfig<TReceive, TSend> {
18
+ connectionId?: string
19
+ }
20
+
21
+ /**
22
+ * Extended handler context for server connections.
23
+ */
24
+ export interface ServerHandlerContext extends HandlerContext {
25
+ clientType?: string
26
+ subscribedSessions: Set<string>
27
+ }
28
+
29
+ /**
30
+ * Server-side WebSocket connection representing a single client.
31
+ */
32
+ export class ServerConnection<TReceive extends ProtocolDef, TSend extends ProtocolDef> extends Connection<TReceive, TSend> {
33
+ private serverWs: IServerWebSocket | null = null
34
+ private clientType: string = 'unknown'
35
+ private subscribedSessions = new Set<string>()
36
+
37
+ constructor(config: ServerConnectionConfig<TReceive, TSend>) {
38
+ super(config)
39
+ if (config.connectionId) {
40
+ this.connectionId = config.connectionId
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Attach an already-accepted WebSocket connection.
46
+ * Connection is immediately ready - no handshake needed.
47
+ */
48
+ attach(ws: IServerWebSocket): void {
49
+ this.serverWs = ws
50
+ this.setupServerWebSocket(ws)
51
+ this.setState('connected')
52
+ }
53
+
54
+ async connect(): Promise<void> {
55
+ throw new Error('Server connections use attach() instead of connect()')
56
+ }
57
+
58
+ async disconnect(): Promise<void> {
59
+ this.cleanup()
60
+
61
+ if (this.serverWs && this.serverWs.readyState !== WebSocketReadyState.CLOSED) {
62
+ this.serverWs.close(1000, 'Server disconnect')
63
+ }
64
+
65
+ this.serverWs = null
66
+ this.ws = null
67
+ this.setState('disconnected')
68
+ }
69
+
70
+ getClientType(): string {
71
+ return this.clientType
72
+ }
73
+
74
+ setClientType(clientType: string): void {
75
+ this.clientType = clientType
76
+ }
77
+
78
+ getSubscribedSessions(): ReadonlySet<string> {
79
+ return this.subscribedSessions
80
+ }
81
+
82
+ subscribe(sessionId: string): void {
83
+ this.subscribedSessions.add(sessionId)
84
+ this.serverWs?.subscribe(`session:${sessionId}`)
85
+ }
86
+
87
+ unsubscribe(sessionId: string): void {
88
+ this.subscribedSessions.delete(sessionId)
89
+ this.serverWs?.unsubscribe(`session:${sessionId}`)
90
+ }
91
+
92
+ isSubscribedTo(sessionId: string): boolean {
93
+ return this.subscribedSessions.has(sessionId)
94
+ }
95
+
96
+ publishToSession(sessionId: string, message: string): void {
97
+ this.serverWs?.publish(`session:${sessionId}`, message)
98
+ }
99
+
100
+ getHandlerContext(): ServerHandlerContext {
101
+ return {
102
+ connectionId: this.connectionId,
103
+ clientType: this.clientType,
104
+ subscribedSessions: this.subscribedSessions,
105
+ }
106
+ }
107
+
108
+ // Use server context for handlers
109
+ private setupServerWebSocket(ws: IServerWebSocket): void {
110
+ this.ws = ws
111
+ this.router.setSender((message) => this.send(message))
112
+ ws.onmessage = (data) => this.router.handleMessage(data, this.getHandlerContext())
113
+ ws.onerror = (error) => this.emit('error', error)
114
+ ws.onclose = (event) => this.handleClose(event.code, event.reason)
115
+ }
116
+ }
117
+
118
+ export function createServerConnection<TReceive extends ProtocolDef, TSend extends ProtocolDef>(
119
+ config: ServerConnectionConfig<TReceive, TSend>,
120
+ ): ServerConnection<TReceive, TSend> {
121
+ return new ServerConnection(config)
122
+ }