@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,348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Protocol Definition API
|
|
3
|
+
*
|
|
4
|
+
* Provides a type-safe way to define RPC-like protocols for WebSocket communication.
|
|
5
|
+
* Inspired by tRPC but designed for bidirectional WebSocket protocols.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { z } from 'zod'
|
|
9
|
+
import type { Result } from './result.js'
|
|
10
|
+
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// Method/Notification Definition Types
|
|
13
|
+
// ============================================================================
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Definition for a request-response method.
|
|
17
|
+
*/
|
|
18
|
+
export interface MethodDef<
|
|
19
|
+
TInput extends z.ZodTypeAny = z.ZodTypeAny,
|
|
20
|
+
TOutput extends z.ZodTypeAny = z.ZodTypeAny,
|
|
21
|
+
TError extends z.ZodTypeAny = z.ZodTypeAny,
|
|
22
|
+
> {
|
|
23
|
+
_type: 'method'
|
|
24
|
+
input: TInput
|
|
25
|
+
output: TOutput
|
|
26
|
+
error: TError
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Definition for a fire-and-forget notification.
|
|
31
|
+
*/
|
|
32
|
+
export interface NotificationDef<TInput extends z.ZodTypeAny = z.ZodTypeAny> {
|
|
33
|
+
_type: 'notification'
|
|
34
|
+
input: TInput
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A single endpoint definition (method or notification).
|
|
39
|
+
*/
|
|
40
|
+
export type EndpointDef = MethodDef | NotificationDef
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Protocol definition - a collection of named endpoints.
|
|
44
|
+
*/
|
|
45
|
+
export interface ProtocolDef {
|
|
46
|
+
[key: string]: EndpointDef
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ============================================================================
|
|
50
|
+
// Type Inference Utilities
|
|
51
|
+
// ============================================================================
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Extract input type from an endpoint definition.
|
|
55
|
+
*/
|
|
56
|
+
export type InferInput<T extends EndpointDef> = T extends MethodDef<infer I, infer _O, infer _E> ? z.infer<I>
|
|
57
|
+
: T extends NotificationDef<infer I> ? z.infer<I>
|
|
58
|
+
: never
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Extract output type from a method definition.
|
|
62
|
+
*/
|
|
63
|
+
export type InferOutput<T extends MethodDef> = T extends MethodDef<infer _I, infer O, infer _E> ? z.infer<O>
|
|
64
|
+
: never
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Extract error type from a method definition.
|
|
68
|
+
*/
|
|
69
|
+
export type InferError<T extends MethodDef> = T extends MethodDef<infer _I, infer _O, infer E> ? z.infer<E>
|
|
70
|
+
: never
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Check if endpoint is a method.
|
|
74
|
+
*/
|
|
75
|
+
export type IsMethod<T extends EndpointDef> = T extends MethodDef ? true : false
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Check if endpoint is a notification.
|
|
79
|
+
*/
|
|
80
|
+
export type IsNotification<T extends EndpointDef> = T extends NotificationDef ? true : false
|
|
81
|
+
|
|
82
|
+
// ============================================================================
|
|
83
|
+
// Handler Types
|
|
84
|
+
// ============================================================================
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Handler context passed to method/notification handlers.
|
|
88
|
+
*/
|
|
89
|
+
export interface HandlerContext {
|
|
90
|
+
/** Connection identifier */
|
|
91
|
+
connectionId: string
|
|
92
|
+
/** Additional context data (platform-specific) */
|
|
93
|
+
[key: string]: unknown
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Handler for a method endpoint.
|
|
98
|
+
*/
|
|
99
|
+
export type MethodHandler<T extends MethodDef> = (
|
|
100
|
+
input: InferInput<T>,
|
|
101
|
+
ctx: HandlerContext,
|
|
102
|
+
) => Promise<Result<InferOutput<T>, InferError<T>>>
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Handler for a notification endpoint.
|
|
106
|
+
*/
|
|
107
|
+
export type NotificationHandler<T extends NotificationDef> = (
|
|
108
|
+
input: InferInput<T>,
|
|
109
|
+
ctx: HandlerContext,
|
|
110
|
+
) => Promise<void>
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Handler for an endpoint (method or notification).
|
|
114
|
+
*/
|
|
115
|
+
export type EndpointHandler<T extends EndpointDef> = T extends MethodDef ? MethodHandler<T>
|
|
116
|
+
: T extends NotificationDef ? NotificationHandler<T>
|
|
117
|
+
: never
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Handlers object for a protocol.
|
|
121
|
+
*/
|
|
122
|
+
export type ProtocolHandlers<T extends ProtocolDef> = {
|
|
123
|
+
[K in keyof T]: EndpointHandler<T[K]>
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ============================================================================
|
|
127
|
+
// Caller Types (for making outbound calls)
|
|
128
|
+
// ============================================================================
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Caller function for a method endpoint.
|
|
132
|
+
*/
|
|
133
|
+
export type MethodCaller<T extends MethodDef> = (
|
|
134
|
+
input: InferInput<T>,
|
|
135
|
+
) => Promise<Result<InferOutput<T>, InferError<T>>>
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Notifier function for a notification endpoint.
|
|
139
|
+
*/
|
|
140
|
+
export type NotificationNotifier<T extends NotificationDef> = (
|
|
141
|
+
input: InferInput<T>,
|
|
142
|
+
) => void
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Caller object for methods in a protocol.
|
|
146
|
+
* Only includes method endpoints.
|
|
147
|
+
*/
|
|
148
|
+
export type ProtocolCaller<T extends ProtocolDef> = {
|
|
149
|
+
[K in keyof T as T[K] extends MethodDef ? K : never]: T[K] extends MethodDef ? MethodCaller<T[K]>
|
|
150
|
+
: never
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Notifier object for notifications in a protocol.
|
|
155
|
+
* Only includes notification endpoints.
|
|
156
|
+
*/
|
|
157
|
+
export type ProtocolNotifier<T extends ProtocolDef> = {
|
|
158
|
+
[K in keyof T as T[K] extends NotificationDef ? K : never]: T[K] extends NotificationDef ? NotificationNotifier<T[K]>
|
|
159
|
+
: never
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ============================================================================
|
|
163
|
+
// Definition Functions
|
|
164
|
+
// ============================================================================
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Options for method definition.
|
|
168
|
+
*/
|
|
169
|
+
export interface MethodOptions<
|
|
170
|
+
TInput extends z.ZodTypeAny,
|
|
171
|
+
TOutput extends z.ZodTypeAny,
|
|
172
|
+
TError extends z.ZodTypeAny,
|
|
173
|
+
> {
|
|
174
|
+
/** Input schema (what the caller sends) */
|
|
175
|
+
input: TInput
|
|
176
|
+
/** Output schema (what the handler returns on success) */
|
|
177
|
+
output: TOutput
|
|
178
|
+
/** Error schema (what the handler returns on failure) */
|
|
179
|
+
error: TError
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Define a request-response method endpoint.
|
|
184
|
+
*/
|
|
185
|
+
export function method<
|
|
186
|
+
TInput extends z.ZodTypeAny,
|
|
187
|
+
TOutput extends z.ZodTypeAny,
|
|
188
|
+
TError extends z.ZodTypeAny,
|
|
189
|
+
>(options: MethodOptions<TInput, TOutput, TError>): MethodDef<TInput, TOutput, TError> {
|
|
190
|
+
return {
|
|
191
|
+
_type: 'method',
|
|
192
|
+
input: options.input,
|
|
193
|
+
output: options.output,
|
|
194
|
+
error: options.error,
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Options for notification definition.
|
|
200
|
+
*/
|
|
201
|
+
export interface NotificationOptions<TInput extends z.ZodTypeAny> {
|
|
202
|
+
/** Input schema (what is sent) */
|
|
203
|
+
input: TInput
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Define a fire-and-forget notification endpoint.
|
|
208
|
+
*/
|
|
209
|
+
export function notification<TInput extends z.ZodTypeAny>(
|
|
210
|
+
options: NotificationOptions<TInput>,
|
|
211
|
+
): NotificationDef<TInput> {
|
|
212
|
+
return {
|
|
213
|
+
_type: 'notification',
|
|
214
|
+
input: options.input,
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ============================================================================
|
|
219
|
+
// Protocol Definition
|
|
220
|
+
// ============================================================================
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Safe parse result type (matches Zod's structure).
|
|
224
|
+
*/
|
|
225
|
+
export type SafeParseResult<T> =
|
|
226
|
+
| { success: true; data: T }
|
|
227
|
+
| { success: false; error: { message: string } }
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Protocol object with definition and metadata.
|
|
231
|
+
*/
|
|
232
|
+
export interface Protocol<T extends ProtocolDef> {
|
|
233
|
+
/** The protocol definition */
|
|
234
|
+
_def: T
|
|
235
|
+
/** Get endpoint definition by name */
|
|
236
|
+
getEndpoint<K extends keyof T>(name: K): T[K]
|
|
237
|
+
/** Check if endpoint is a method */
|
|
238
|
+
isMethod(name: keyof T): boolean
|
|
239
|
+
/** Check if endpoint is a notification */
|
|
240
|
+
isNotification(name: keyof T): boolean
|
|
241
|
+
/** Get all method names */
|
|
242
|
+
getMethodNames(): (keyof T)[]
|
|
243
|
+
/** Get all notification names */
|
|
244
|
+
getNotificationNames(): (keyof T)[]
|
|
245
|
+
/** Validate input for an endpoint */
|
|
246
|
+
validateInput<K extends keyof T>(name: K, input: unknown): SafeParseResult<InferInput<T[K]>>
|
|
247
|
+
/** Validate output for a method endpoint */
|
|
248
|
+
validateOutput<K extends keyof T>(name: K, output: unknown): SafeParseResult<unknown>
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Define a protocol with typed endpoints.
|
|
253
|
+
*
|
|
254
|
+
* @example
|
|
255
|
+
* ```typescript
|
|
256
|
+
* const clientProtocol = defineProtocol({
|
|
257
|
+
* subscribe: method({
|
|
258
|
+
* input: z.object({ sessionId: z.string() }),
|
|
259
|
+
* output: z.void(),
|
|
260
|
+
* error: z.object({ code: z.literal('SESSION_NOT_FOUND') }),
|
|
261
|
+
* }),
|
|
262
|
+
*
|
|
263
|
+
* userMessage: method({
|
|
264
|
+
* input: z.object({ sessionId: z.string(), content: z.string() }),
|
|
265
|
+
* output: z.object({ messageId: z.string() }),
|
|
266
|
+
* error: z.object({ code: z.literal('AGENT_BUSY') }),
|
|
267
|
+
* }),
|
|
268
|
+
* });
|
|
269
|
+
*
|
|
270
|
+
* const serverProtocol = defineProtocol({
|
|
271
|
+
* agentMessage: notification({
|
|
272
|
+
* input: z.object({ sessionId: z.string(), content: z.string() }),
|
|
273
|
+
* }),
|
|
274
|
+
* });
|
|
275
|
+
* ```
|
|
276
|
+
*/
|
|
277
|
+
export function defineProtocol<T extends ProtocolDef>(def: T): Protocol<T> {
|
|
278
|
+
return {
|
|
279
|
+
_def: def,
|
|
280
|
+
|
|
281
|
+
getEndpoint<K extends keyof T>(name: K): T[K] {
|
|
282
|
+
return def[name]
|
|
283
|
+
},
|
|
284
|
+
|
|
285
|
+
isMethod(name: keyof T): boolean {
|
|
286
|
+
const endpoint = def[name]
|
|
287
|
+
return endpoint?._type === 'method'
|
|
288
|
+
},
|
|
289
|
+
|
|
290
|
+
isNotification(name: keyof T): boolean {
|
|
291
|
+
const endpoint = def[name]
|
|
292
|
+
return endpoint?._type === 'notification'
|
|
293
|
+
},
|
|
294
|
+
|
|
295
|
+
getMethodNames(): (keyof T)[] {
|
|
296
|
+
return Object.keys(def).filter((key) => def[key]._type === 'method') as (keyof T)[]
|
|
297
|
+
},
|
|
298
|
+
|
|
299
|
+
getNotificationNames(): (keyof T)[] {
|
|
300
|
+
return Object.keys(def).filter((key) => def[key]._type === 'notification') as (keyof T)[]
|
|
301
|
+
},
|
|
302
|
+
|
|
303
|
+
validateInput<K extends keyof T>(name: K, input: unknown): SafeParseResult<InferInput<T[K]>> {
|
|
304
|
+
const endpoint = def[name]
|
|
305
|
+
const result = endpoint.input.safeParse(input)
|
|
306
|
+
if (result.success) {
|
|
307
|
+
return { success: true, data: result.data as InferInput<T[K]> }
|
|
308
|
+
}
|
|
309
|
+
return { success: false, error: { message: result.error.message } }
|
|
310
|
+
},
|
|
311
|
+
|
|
312
|
+
validateOutput<K extends keyof T>(name: K, output: unknown): SafeParseResult<unknown> {
|
|
313
|
+
const endpoint = def[name]
|
|
314
|
+
if (endpoint._type !== 'method') {
|
|
315
|
+
throw new Error(`Endpoint ${String(name)} is not a method`)
|
|
316
|
+
}
|
|
317
|
+
const result = endpoint.output.safeParse(output)
|
|
318
|
+
if (result.success) {
|
|
319
|
+
return { success: true, data: result.data }
|
|
320
|
+
}
|
|
321
|
+
return { success: false, error: { message: result.error.message } }
|
|
322
|
+
},
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// ============================================================================
|
|
327
|
+
// Type Extraction Helpers
|
|
328
|
+
// ============================================================================
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Extract the protocol definition type from a Protocol.
|
|
332
|
+
*/
|
|
333
|
+
export type ExtractProtocolDef<T> = T extends Protocol<infer P> ? P : never
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Create a typed handlers object type for a protocol.
|
|
337
|
+
*/
|
|
338
|
+
export type HandlersFor<T extends Protocol<ProtocolDef>> = ProtocolHandlers<ExtractProtocolDef<T>>
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Create a typed caller object type for a protocol.
|
|
342
|
+
*/
|
|
343
|
+
export type CallerFor<T extends Protocol<ProtocolDef>> = ProtocolCaller<ExtractProtocolDef<T>>
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Create a typed notifier object type for a protocol.
|
|
347
|
+
*/
|
|
348
|
+
export type NotifierFor<T extends Protocol<ProtocolDef>> = ProtocolNotifier<ExtractProtocolDef<T>>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Result type for explicit error handling without exceptions.
|
|
3
|
+
* Inspired by Rust/Go approach.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type Result<T, E = Error> =
|
|
7
|
+
| { ok: true; value: T }
|
|
8
|
+
| { ok: false; error: E }
|
|
9
|
+
|
|
10
|
+
// Constructors
|
|
11
|
+
export const Ok = <T>(value: T): Result<T, never> => ({ ok: true, value })
|
|
12
|
+
export const Err = <E>(error: E): Result<never, E> => ({ ok: false, error })
|
|
13
|
+
|
|
14
|
+
// Type guards
|
|
15
|
+
export const isOk = <T, E>(result: Result<T, E>): result is { ok: true; value: T } => result.ok
|
|
16
|
+
export const isErr = <T, E>(result: Result<T, E>): result is { ok: false; error: E } => !result.ok
|
|
17
|
+
|
|
18
|
+
// Helper functions
|
|
19
|
+
export const mapResult = <T, U, E>(
|
|
20
|
+
result: Result<T, E>,
|
|
21
|
+
fn: (value: T) => U,
|
|
22
|
+
): Result<U, E> => result.ok ? Ok(fn(result.value)) : result
|
|
23
|
+
|
|
24
|
+
export const flatMapResult = <T, U, E>(
|
|
25
|
+
result: Result<T, E>,
|
|
26
|
+
fn: (value: T) => Result<U, E>,
|
|
27
|
+
): Result<U, E> => result.ok ? fn(result.value) : result
|
|
28
|
+
|
|
29
|
+
// Unwrap functions
|
|
30
|
+
export const unwrapOr = <T, E>(result: Result<T, E>, defaultValue: T): T => result.ok ? result.value : defaultValue
|
|
31
|
+
|
|
32
|
+
export const unwrapOrThrow = <T, E>(result: Result<T, E>): T => {
|
|
33
|
+
if (result.ok) return result.value
|
|
34
|
+
throw result.error
|
|
35
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message Router Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for notification-only message router.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test'
|
|
8
|
+
import { z } from 'zod'
|
|
9
|
+
import { defineProtocol, notification } from './protocol.js'
|
|
10
|
+
import { createRouter, MessageRouter } from './router.js'
|
|
11
|
+
|
|
12
|
+
// Test protocols (notification-only)
|
|
13
|
+
const receiveProtocol = defineProtocol({
|
|
14
|
+
agentMessage: notification({
|
|
15
|
+
input: z.object({ sessionId: z.string(), content: z.string() }),
|
|
16
|
+
}),
|
|
17
|
+
error: notification({
|
|
18
|
+
input: z.object({ code: z.string(), message: z.string() }),
|
|
19
|
+
}),
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
const sendProtocol = defineProtocol({
|
|
23
|
+
userMessage: notification({
|
|
24
|
+
input: z.object({ sessionId: z.string(), content: z.string() }),
|
|
25
|
+
}),
|
|
26
|
+
status: notification({
|
|
27
|
+
input: z.object({ status: z.string() }),
|
|
28
|
+
}),
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
describe('MessageRouter', () => {
|
|
32
|
+
let router: MessageRouter<typeof receiveProtocol._def, typeof sendProtocol._def>
|
|
33
|
+
let sender: ReturnType<typeof mock>
|
|
34
|
+
let sentMessages: string[]
|
|
35
|
+
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
router = new MessageRouter(receiveProtocol, sendProtocol)
|
|
38
|
+
sentMessages = []
|
|
39
|
+
sender = mock((msg: string) => {
|
|
40
|
+
sentMessages.push(msg)
|
|
41
|
+
return true
|
|
42
|
+
})
|
|
43
|
+
router.setSender(sender)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
afterEach(() => {
|
|
47
|
+
// No cleanup needed - no pending requests in notification-only router
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
describe('handleMessage - incoming notifications', () => {
|
|
51
|
+
it('should dispatch notification to handler', async () => {
|
|
52
|
+
const handler = mock().mockResolvedValue(undefined)
|
|
53
|
+
router.setHandlers({ agentMessage: handler })
|
|
54
|
+
|
|
55
|
+
await router.handleMessage(
|
|
56
|
+
JSON.stringify({
|
|
57
|
+
type: 'agentMessage',
|
|
58
|
+
payload: { sessionId: 'sess-1', content: 'Hello' },
|
|
59
|
+
ts: Date.now(),
|
|
60
|
+
}),
|
|
61
|
+
{ connectionId: 'conn-1' },
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
expect(handler).toHaveBeenCalledWith(
|
|
65
|
+
{ sessionId: 'sess-1', content: 'Hello' },
|
|
66
|
+
{ connectionId: 'conn-1' },
|
|
67
|
+
)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('should not send any response for notifications', async () => {
|
|
71
|
+
const handler = mock().mockResolvedValue(undefined)
|
|
72
|
+
router.setHandlers({ agentMessage: handler })
|
|
73
|
+
|
|
74
|
+
await router.handleMessage(
|
|
75
|
+
JSON.stringify({
|
|
76
|
+
type: 'agentMessage',
|
|
77
|
+
payload: { sessionId: 'sess-1', content: 'Hello' },
|
|
78
|
+
ts: Date.now(),
|
|
79
|
+
}),
|
|
80
|
+
{ connectionId: 'conn-1' },
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
// Notifications don't send responses
|
|
84
|
+
expect(sentMessages.length).toBe(0)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('should ignore invalid input without error', async () => {
|
|
88
|
+
const handler = mock().mockResolvedValue(undefined)
|
|
89
|
+
router.setHandlers({ agentMessage: handler })
|
|
90
|
+
|
|
91
|
+
await router.handleMessage(
|
|
92
|
+
JSON.stringify({
|
|
93
|
+
type: 'agentMessage',
|
|
94
|
+
payload: { sessionId: 123 }, // Should be string, missing content
|
|
95
|
+
ts: Date.now(),
|
|
96
|
+
}),
|
|
97
|
+
{ connectionId: 'conn-1' },
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
// Handler should not be called for invalid input
|
|
101
|
+
expect(handler).not.toHaveBeenCalled()
|
|
102
|
+
// No error response sent (notification-only)
|
|
103
|
+
expect(sentMessages.length).toBe(0)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('should ignore messages when no handler registered', async () => {
|
|
107
|
+
// No handlers set
|
|
108
|
+
|
|
109
|
+
await router.handleMessage(
|
|
110
|
+
JSON.stringify({
|
|
111
|
+
type: 'agentMessage',
|
|
112
|
+
payload: { sessionId: 'test', content: 'Hello' },
|
|
113
|
+
ts: Date.now(),
|
|
114
|
+
}),
|
|
115
|
+
{ connectionId: 'conn-1' },
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
// Should not throw and not send any response
|
|
119
|
+
expect(sentMessages.length).toBe(0)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('should silently ignore handler errors', async () => {
|
|
123
|
+
const handler = mock().mockRejectedValue(new Error('Handler failed'))
|
|
124
|
+
router.setHandlers({ agentMessage: handler })
|
|
125
|
+
|
|
126
|
+
// Should not throw
|
|
127
|
+
await router.handleMessage(
|
|
128
|
+
JSON.stringify({
|
|
129
|
+
type: 'agentMessage',
|
|
130
|
+
payload: { sessionId: 'test', content: 'Hello' },
|
|
131
|
+
ts: Date.now(),
|
|
132
|
+
}),
|
|
133
|
+
{ connectionId: 'conn-1' },
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
expect(handler).toHaveBeenCalled()
|
|
137
|
+
// No error response sent (notification-only)
|
|
138
|
+
expect(sentMessages.length).toBe(0)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('should handle multiple notification types', async () => {
|
|
142
|
+
const agentHandler = mock().mockResolvedValue(undefined)
|
|
143
|
+
const errorHandler = mock().mockResolvedValue(undefined)
|
|
144
|
+
router.setHandlers({
|
|
145
|
+
agentMessage: agentHandler,
|
|
146
|
+
error: errorHandler,
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
await router.handleMessage(
|
|
150
|
+
JSON.stringify({
|
|
151
|
+
type: 'agentMessage',
|
|
152
|
+
payload: { sessionId: 'sess-1', content: 'Hello' },
|
|
153
|
+
ts: Date.now(),
|
|
154
|
+
}),
|
|
155
|
+
{ connectionId: 'conn-1' },
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
await router.handleMessage(
|
|
159
|
+
JSON.stringify({
|
|
160
|
+
type: 'error',
|
|
161
|
+
payload: { code: 'ERR_001', message: 'Something failed' },
|
|
162
|
+
ts: Date.now(),
|
|
163
|
+
}),
|
|
164
|
+
{ connectionId: 'conn-1' },
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
expect(agentHandler).toHaveBeenCalledTimes(1)
|
|
168
|
+
expect(errorHandler).toHaveBeenCalledTimes(1)
|
|
169
|
+
})
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
describe('notify - outbound notifications', () => {
|
|
173
|
+
it('should send notification', () => {
|
|
174
|
+
router.notify('userMessage', { sessionId: 'test', content: 'Hello' })
|
|
175
|
+
|
|
176
|
+
expect(sentMessages.length).toBe(1)
|
|
177
|
+
const message = JSON.parse(sentMessages[0])
|
|
178
|
+
expect(message.type).toBe('userMessage')
|
|
179
|
+
expect(message.payload).toEqual({ sessionId: 'test', content: 'Hello' })
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('should include timestamp', () => {
|
|
183
|
+
const before = Date.now()
|
|
184
|
+
router.notify('status', { status: 'online' })
|
|
185
|
+
const after = Date.now()
|
|
186
|
+
|
|
187
|
+
const message = JSON.parse(sentMessages[0])
|
|
188
|
+
expect(message.ts).toBeGreaterThanOrEqual(before)
|
|
189
|
+
expect(message.ts).toBeLessThanOrEqual(after)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('should return true on success', () => {
|
|
193
|
+
const result = router.notify('userMessage', { sessionId: 'test', content: 'Hello' })
|
|
194
|
+
expect(result).toBe(true)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('should return false if no sender configured', () => {
|
|
198
|
+
const noSenderRouter = new MessageRouter(receiveProtocol, sendProtocol)
|
|
199
|
+
const result = noSenderRouter.notify('userMessage', { sessionId: 'test', content: 'Hello' })
|
|
200
|
+
expect(result).toBe(false)
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it('should return false if sender returns false', () => {
|
|
204
|
+
const failingRouter = new MessageRouter(receiveProtocol, sendProtocol)
|
|
205
|
+
failingRouter.setSender(() => false)
|
|
206
|
+
|
|
207
|
+
const result = failingRouter.notify('userMessage', { sessionId: 'test', content: 'Hello' })
|
|
208
|
+
expect(result).toBe(false)
|
|
209
|
+
})
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
describe('createRouter factory', () => {
|
|
213
|
+
it('should create a router instance', () => {
|
|
214
|
+
const router = createRouter(receiveProtocol, sendProtocol)
|
|
215
|
+
expect(router).toBeInstanceOf(MessageRouter)
|
|
216
|
+
})
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
describe('invalid JSON handling', () => {
|
|
220
|
+
it('should ignore invalid JSON', async () => {
|
|
221
|
+
const handler = mock()
|
|
222
|
+
router.setHandlers({ agentMessage: handler })
|
|
223
|
+
|
|
224
|
+
await router.handleMessage('not valid json', { connectionId: 'test' })
|
|
225
|
+
|
|
226
|
+
expect(handler).not.toHaveBeenCalled()
|
|
227
|
+
})
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
describe('unknown message types', () => {
|
|
231
|
+
it('should ignore unknown message types', async () => {
|
|
232
|
+
const handler = mock()
|
|
233
|
+
router.setHandlers({ agentMessage: handler })
|
|
234
|
+
|
|
235
|
+
await router.handleMessage(
|
|
236
|
+
JSON.stringify({
|
|
237
|
+
type: 'unknownType',
|
|
238
|
+
payload: {},
|
|
239
|
+
ts: Date.now(),
|
|
240
|
+
}),
|
|
241
|
+
{ connectionId: 'test' },
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
expect(handler).not.toHaveBeenCalled()
|
|
245
|
+
expect(sentMessages.length).toBe(0)
|
|
246
|
+
})
|
|
247
|
+
})
|
|
248
|
+
})
|