@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,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connection Base Class
|
|
3
|
+
*
|
|
4
|
+
* Provides symmetric API for WebSocket client and server connections.
|
|
5
|
+
* Simplified: no handshake, no heartbeat - just WebSocket open/close → connected/disconnected.
|
|
6
|
+
* Notification-only: no request-response, no correlation.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { IWebSocket } from '../platform/types.js'
|
|
10
|
+
import { WebSocketReadyState } from '../platform/types.js'
|
|
11
|
+
import type { Protocol, ProtocolDef, ProtocolHandlers, ProtocolNotifier } from './protocol.js'
|
|
12
|
+
import { MessageRouter } from './router.js'
|
|
13
|
+
import type { RawMessageListener } from './router.js'
|
|
14
|
+
import type { ConnectionState, TransportEvent, TransportEventListener } from './types.js'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Connection configuration.
|
|
18
|
+
*/
|
|
19
|
+
export interface ConnectionConfig<TReceive extends ProtocolDef, TSend extends ProtocolDef> {
|
|
20
|
+
receiveProtocol?: Protocol<TReceive>
|
|
21
|
+
sendProtocol?: Protocol<TSend>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Abstract base class for WebSocket connections.
|
|
26
|
+
* State transitions: disconnected ↔ connecting ↔ connected ↔ reconnecting
|
|
27
|
+
*/
|
|
28
|
+
export abstract class Connection<TReceive extends ProtocolDef, TSend extends ProtocolDef> {
|
|
29
|
+
protected readonly config: ConnectionConfig<TReceive, TSend>
|
|
30
|
+
protected readonly router: MessageRouter<TReceive, TSend>
|
|
31
|
+
|
|
32
|
+
// State
|
|
33
|
+
protected state: ConnectionState = 'disconnected'
|
|
34
|
+
protected ws: IWebSocket | null = null
|
|
35
|
+
protected connectionId: string
|
|
36
|
+
protected eventListeners = new Set<TransportEventListener>()
|
|
37
|
+
|
|
38
|
+
constructor(config: ConnectionConfig<TReceive, TSend>) {
|
|
39
|
+
this.config = config
|
|
40
|
+
this.connectionId = `conn-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
|
|
41
|
+
|
|
42
|
+
this.router = new MessageRouter(
|
|
43
|
+
config.receiveProtocol ?? null,
|
|
44
|
+
config.sendProtocol ?? null,
|
|
45
|
+
)
|
|
46
|
+
this.router.setErrorHandler((error) => this.emit('router_error', error))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ============================================================================
|
|
50
|
+
// Public API
|
|
51
|
+
// ============================================================================
|
|
52
|
+
|
|
53
|
+
getState(): ConnectionState {
|
|
54
|
+
return this.state
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
isConnected(): boolean {
|
|
58
|
+
return this.state === 'connected'
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
getConnectionId(): string {
|
|
62
|
+
return this.connectionId
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
setHandlers(handlers: Partial<ProtocolHandlers<TReceive>>): void {
|
|
66
|
+
this.router.setHandlers(handlers)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Set a raw message listener that receives every incoming message as (type, payload).
|
|
71
|
+
* When set, bypasses per-type handler dispatch.
|
|
72
|
+
*/
|
|
73
|
+
setRawMessageListener(listener: RawMessageListener): void {
|
|
74
|
+
this.router.setRawMessageListener(listener)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
get notify(): ProtocolNotifier<TSend> {
|
|
78
|
+
const protocol = this.config.sendProtocol
|
|
79
|
+
if (!protocol) {
|
|
80
|
+
throw new Error('Cannot send notifications without a send protocol')
|
|
81
|
+
}
|
|
82
|
+
const notifier: Record<string, unknown> = {}
|
|
83
|
+
for (const name of protocol.getNotificationNames()) {
|
|
84
|
+
notifier[name as string] = (input: unknown) => this.router.notify(name, input as never)
|
|
85
|
+
}
|
|
86
|
+
return notifier as ProtocolNotifier<TSend>
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ============================================================================
|
|
90
|
+
// Send Buffer (bridges short disconnects)
|
|
91
|
+
// ============================================================================
|
|
92
|
+
|
|
93
|
+
private sendBuffer: string[] = []
|
|
94
|
+
private readonly maxBufferSize: number = 500
|
|
95
|
+
|
|
96
|
+
send(data: string): boolean {
|
|
97
|
+
if (!this.ws || this.ws.readyState !== WebSocketReadyState.OPEN) {
|
|
98
|
+
if (this.sendBuffer.length < this.maxBufferSize) {
|
|
99
|
+
this.sendBuffer.push(data)
|
|
100
|
+
}
|
|
101
|
+
return false
|
|
102
|
+
}
|
|
103
|
+
this.flushSendBuffer()
|
|
104
|
+
try {
|
|
105
|
+
this.ws.send(data)
|
|
106
|
+
return true
|
|
107
|
+
} catch {
|
|
108
|
+
if (this.sendBuffer.length < this.maxBufferSize) {
|
|
109
|
+
this.sendBuffer.push(data)
|
|
110
|
+
}
|
|
111
|
+
return false
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
flushSendBuffer(): void {
|
|
116
|
+
while (this.sendBuffer.length > 0) {
|
|
117
|
+
if (!this.ws || this.ws.readyState !== WebSocketReadyState.OPEN) break
|
|
118
|
+
const msg = this.sendBuffer.shift()!
|
|
119
|
+
try {
|
|
120
|
+
this.ws.send(msg)
|
|
121
|
+
} catch {
|
|
122
|
+
this.sendBuffer.unshift(msg)
|
|
123
|
+
break
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
clearSendBuffer(): void {
|
|
129
|
+
this.sendBuffer.length = 0
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
get bufferedMessageCount(): number {
|
|
133
|
+
return this.sendBuffer.length
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
on(listener: TransportEventListener): () => void {
|
|
137
|
+
this.eventListeners.add(listener)
|
|
138
|
+
return () => this.eventListeners.delete(listener)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
abstract connect(): Promise<void>
|
|
142
|
+
abstract disconnect(): Promise<void>
|
|
143
|
+
|
|
144
|
+
// ============================================================================
|
|
145
|
+
// State Management
|
|
146
|
+
// ============================================================================
|
|
147
|
+
|
|
148
|
+
protected setState(newState: ConnectionState): void {
|
|
149
|
+
if (newState === this.state) return
|
|
150
|
+
const from = this.state
|
|
151
|
+
this.state = newState
|
|
152
|
+
this.emit('state_change', { from, to: newState })
|
|
153
|
+
if (newState === 'connected') this.emit('connected')
|
|
154
|
+
else if (newState === 'disconnected') this.emit('disconnected')
|
|
155
|
+
else if (newState === 'reconnecting') this.emit('reconnecting')
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ============================================================================
|
|
159
|
+
// WebSocket Setup
|
|
160
|
+
// ============================================================================
|
|
161
|
+
|
|
162
|
+
protected setupWebSocket(ws: IWebSocket): void {
|
|
163
|
+
this.ws = ws
|
|
164
|
+
this.router.setSender((message) => this.send(message))
|
|
165
|
+
ws.onmessage = (data) => this.router.handleMessage(data, { connectionId: this.connectionId })
|
|
166
|
+
ws.onerror = (error) => this.emit('error', error)
|
|
167
|
+
ws.onclose = (event) => this.handleClose(event.code, event.reason)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
protected handleOpen(): void {
|
|
171
|
+
// Immediately connected - no handshake needed
|
|
172
|
+
this.setState('connected')
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
protected handleClose(_code: number, _reason: string): void {
|
|
176
|
+
this.cleanup()
|
|
177
|
+
this.setState('disconnected')
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
protected handleConnectionLost(): void {
|
|
181
|
+
this.cleanup()
|
|
182
|
+
this.setState('disconnected')
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
protected cleanup(): void {
|
|
186
|
+
if (this.ws) {
|
|
187
|
+
this.ws.onopen = null
|
|
188
|
+
this.ws.onclose = null
|
|
189
|
+
this.ws.onerror = null
|
|
190
|
+
this.ws.onmessage = null
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
protected emit(event: TransportEvent, data?: unknown): void {
|
|
195
|
+
for (const listener of this.eventListeners) {
|
|
196
|
+
try {
|
|
197
|
+
listener(event, data)
|
|
198
|
+
} catch {
|
|
199
|
+
// Ignore listener errors
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core Transport Module
|
|
3
|
+
*
|
|
4
|
+
* Platform-agnostic WebSocket infrastructure.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Types
|
|
8
|
+
export type { ConnectionState, ReconnectOptions, TransportEvent, TransportEventListener, WireMessage } from './types.js'
|
|
9
|
+
|
|
10
|
+
export { DEFAULT_RECONNECT_OPTIONS } from './types.js'
|
|
11
|
+
|
|
12
|
+
// Result
|
|
13
|
+
export type { Result } from './result.js'
|
|
14
|
+
export { Err, flatMapResult, isErr, isOk, mapResult, Ok, unwrapOr, unwrapOrThrow } from './result.js'
|
|
15
|
+
|
|
16
|
+
// Protocol Definition
|
|
17
|
+
export { defineProtocol, method, notification } from './protocol.js'
|
|
18
|
+
export type {
|
|
19
|
+
CallerFor,
|
|
20
|
+
EndpointDef,
|
|
21
|
+
EndpointHandler,
|
|
22
|
+
ExtractProtocolDef,
|
|
23
|
+
HandlerContext,
|
|
24
|
+
HandlersFor,
|
|
25
|
+
InferError,
|
|
26
|
+
InferInput,
|
|
27
|
+
InferOutput,
|
|
28
|
+
IsMethod,
|
|
29
|
+
IsNotification,
|
|
30
|
+
MethodCaller,
|
|
31
|
+
MethodDef,
|
|
32
|
+
MethodHandler,
|
|
33
|
+
MethodOptions,
|
|
34
|
+
NotificationDef,
|
|
35
|
+
NotificationHandler,
|
|
36
|
+
NotificationNotifier,
|
|
37
|
+
NotificationOptions,
|
|
38
|
+
NotifierFor,
|
|
39
|
+
Protocol,
|
|
40
|
+
ProtocolCaller,
|
|
41
|
+
ProtocolDef,
|
|
42
|
+
ProtocolHandlers,
|
|
43
|
+
ProtocolNotifier,
|
|
44
|
+
} from './protocol.js'
|
|
45
|
+
|
|
46
|
+
// Message Router
|
|
47
|
+
export { createRouter, MessageRouter } from './router.js'
|
|
48
|
+
export type { MessageSender } from './router.js'
|
|
49
|
+
|
|
50
|
+
// Connection Base
|
|
51
|
+
export { Connection } from './connection.js'
|
|
52
|
+
export type { ConnectionConfig } from './connection.js'
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Protocol Definition Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it } from 'bun:test'
|
|
6
|
+
import { z } from 'zod'
|
|
7
|
+
import { defineProtocol, method, notification } from './protocol.js'
|
|
8
|
+
|
|
9
|
+
describe('Protocol Definition', () => {
|
|
10
|
+
describe('method()', () => {
|
|
11
|
+
it('should create a method definition', () => {
|
|
12
|
+
const def = method({
|
|
13
|
+
input: z.object({ id: z.string() }),
|
|
14
|
+
output: z.object({ success: z.boolean() }),
|
|
15
|
+
error: z.object({ code: z.string() }),
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
expect(def._type).toBe('method')
|
|
19
|
+
expect(def.input).toBeDefined()
|
|
20
|
+
expect(def.output).toBeDefined()
|
|
21
|
+
expect(def.error).toBeDefined()
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
describe('notification()', () => {
|
|
26
|
+
it('should create a notification definition', () => {
|
|
27
|
+
const def = notification({
|
|
28
|
+
input: z.object({ message: z.string() }),
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
expect(def._type).toBe('notification')
|
|
32
|
+
expect(def.input).toBeDefined()
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
describe('defineProtocol()', () => {
|
|
37
|
+
const testProtocol = defineProtocol({
|
|
38
|
+
subscribe: method({
|
|
39
|
+
input: z.object({ sessionId: z.string() }),
|
|
40
|
+
output: z.void(),
|
|
41
|
+
error: z.object({ code: z.enum(['SESSION_NOT_FOUND']), message: z.string() }),
|
|
42
|
+
}),
|
|
43
|
+
unsubscribe: method({
|
|
44
|
+
input: z.object({ sessionId: z.string() }),
|
|
45
|
+
output: z.void(),
|
|
46
|
+
error: z.never(),
|
|
47
|
+
}),
|
|
48
|
+
agentMessage: notification({
|
|
49
|
+
input: z.object({
|
|
50
|
+
sessionId: z.string(),
|
|
51
|
+
content: z.string(),
|
|
52
|
+
format: z.enum(['text', 'markdown']),
|
|
53
|
+
}),
|
|
54
|
+
}),
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('should expose the definition', () => {
|
|
58
|
+
expect(testProtocol._def).toBeDefined()
|
|
59
|
+
expect(testProtocol._def.subscribe).toBeDefined()
|
|
60
|
+
expect(testProtocol._def.unsubscribe).toBeDefined()
|
|
61
|
+
expect(testProtocol._def.agentMessage).toBeDefined()
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
describe('getEndpoint()', () => {
|
|
65
|
+
it('should return endpoint definition by name', () => {
|
|
66
|
+
const endpoint = testProtocol.getEndpoint('subscribe')
|
|
67
|
+
expect(endpoint._type).toBe('method')
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
describe('isMethod()', () => {
|
|
72
|
+
it('should return true for methods', () => {
|
|
73
|
+
expect(testProtocol.isMethod('subscribe')).toBe(true)
|
|
74
|
+
expect(testProtocol.isMethod('unsubscribe')).toBe(true)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('should return false for notifications', () => {
|
|
78
|
+
expect(testProtocol.isMethod('agentMessage')).toBe(false)
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
describe('isNotification()', () => {
|
|
83
|
+
it('should return true for notifications', () => {
|
|
84
|
+
expect(testProtocol.isNotification('agentMessage')).toBe(true)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('should return false for methods', () => {
|
|
88
|
+
expect(testProtocol.isNotification('subscribe')).toBe(false)
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
describe('getMethodNames()', () => {
|
|
93
|
+
it('should return all method names', () => {
|
|
94
|
+
const names = testProtocol.getMethodNames()
|
|
95
|
+
expect(names).toContain('subscribe')
|
|
96
|
+
expect(names).toContain('unsubscribe')
|
|
97
|
+
expect(names).not.toContain('agentMessage')
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
describe('getNotificationNames()', () => {
|
|
102
|
+
it('should return all notification names', () => {
|
|
103
|
+
const names = testProtocol.getNotificationNames()
|
|
104
|
+
expect(names).toContain('agentMessage')
|
|
105
|
+
expect(names).not.toContain('subscribe')
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
describe('validateInput()', () => {
|
|
110
|
+
it('should validate correct input', () => {
|
|
111
|
+
const result = testProtocol.validateInput('subscribe', { sessionId: 'test-123' })
|
|
112
|
+
expect(result.success).toBe(true)
|
|
113
|
+
if (result.success) {
|
|
114
|
+
expect(result.data.sessionId).toBe('test-123')
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('should reject invalid input', () => {
|
|
119
|
+
const result = testProtocol.validateInput('subscribe', { sessionId: 123 })
|
|
120
|
+
expect(result.success).toBe(false)
|
|
121
|
+
if (!result.success) {
|
|
122
|
+
expect(result.error.message).toBeDefined()
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('should reject missing required fields', () => {
|
|
127
|
+
const result = testProtocol.validateInput('subscribe', {})
|
|
128
|
+
expect(result.success).toBe(false)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('should validate notification input', () => {
|
|
132
|
+
const result = testProtocol.validateInput('agentMessage', {
|
|
133
|
+
sessionId: 'test',
|
|
134
|
+
content: 'Hello',
|
|
135
|
+
format: 'text',
|
|
136
|
+
})
|
|
137
|
+
expect(result.success).toBe(true)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('should reject invalid notification input', () => {
|
|
141
|
+
const result = testProtocol.validateInput('agentMessage', {
|
|
142
|
+
sessionId: 'test',
|
|
143
|
+
content: 'Hello',
|
|
144
|
+
format: 'invalid',
|
|
145
|
+
})
|
|
146
|
+
expect(result.success).toBe(false)
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
describe('validateOutput()', () => {
|
|
151
|
+
it('should validate method output', () => {
|
|
152
|
+
const result = testProtocol.validateOutput('subscribe', undefined)
|
|
153
|
+
expect(result.success).toBe(true)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('should throw for notification output validation', () => {
|
|
157
|
+
expect(() => testProtocol.validateOutput('agentMessage', {})).toThrow(
|
|
158
|
+
'Endpoint agentMessage is not a method',
|
|
159
|
+
)
|
|
160
|
+
})
|
|
161
|
+
})
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
describe('complex schemas', () => {
|
|
165
|
+
it('should handle nested objects', () => {
|
|
166
|
+
const protocol = defineProtocol({
|
|
167
|
+
createUser: method({
|
|
168
|
+
input: z.object({
|
|
169
|
+
name: z.string(),
|
|
170
|
+
address: z.object({
|
|
171
|
+
street: z.string(),
|
|
172
|
+
city: z.string(),
|
|
173
|
+
}),
|
|
174
|
+
}),
|
|
175
|
+
output: z.object({ id: z.string() }),
|
|
176
|
+
error: z.object({ code: z.string() }),
|
|
177
|
+
}),
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
const valid = protocol.validateInput('createUser', {
|
|
181
|
+
name: 'John',
|
|
182
|
+
address: { street: '123 Main St', city: 'NYC' },
|
|
183
|
+
})
|
|
184
|
+
expect(valid.success).toBe(true)
|
|
185
|
+
|
|
186
|
+
const invalid = protocol.validateInput('createUser', {
|
|
187
|
+
name: 'John',
|
|
188
|
+
address: { street: '123 Main St' }, // Missing city
|
|
189
|
+
})
|
|
190
|
+
expect(invalid.success).toBe(false)
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('should handle arrays', () => {
|
|
194
|
+
const protocol = defineProtocol({
|
|
195
|
+
sendBatch: method({
|
|
196
|
+
input: z.object({
|
|
197
|
+
messages: z.array(z.object({
|
|
198
|
+
id: z.string(),
|
|
199
|
+
content: z.string(),
|
|
200
|
+
})),
|
|
201
|
+
}),
|
|
202
|
+
output: z.object({ processed: z.number() }),
|
|
203
|
+
error: z.object({ code: z.string() }),
|
|
204
|
+
}),
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
const valid = protocol.validateInput('sendBatch', {
|
|
208
|
+
messages: [
|
|
209
|
+
{ id: '1', content: 'Hello' },
|
|
210
|
+
{ id: '2', content: 'World' },
|
|
211
|
+
],
|
|
212
|
+
})
|
|
213
|
+
expect(valid.success).toBe(true)
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it('should handle optional fields', () => {
|
|
217
|
+
const protocol = defineProtocol({
|
|
218
|
+
search: method({
|
|
219
|
+
input: z.object({
|
|
220
|
+
query: z.string(),
|
|
221
|
+
limit: z.number().optional(),
|
|
222
|
+
}),
|
|
223
|
+
output: z.array(z.string()),
|
|
224
|
+
error: z.object({ code: z.string() }),
|
|
225
|
+
}),
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
const withOptional = protocol.validateInput('search', { query: 'test', limit: 10 })
|
|
229
|
+
expect(withOptional.success).toBe(true)
|
|
230
|
+
|
|
231
|
+
const withoutOptional = protocol.validateInput('search', { query: 'test' })
|
|
232
|
+
expect(withoutOptional.success).toBe(true)
|
|
233
|
+
})
|
|
234
|
+
})
|
|
235
|
+
})
|