Shared API
Shared types, utilities, and protocol definitions used by both client and server.
defineEvents()
Define a type-safe event schema for your WebSocket communication.
import { defineEvents } from '@mdrv/wsx/shared'
import { z } from 'zod'
const events = defineEvents({
eventName: {
request: z.object({ ... }),
response: z.object({ ... }), // Optional for one-way messages
},
})
Parameters
An object where each key is an event name, and each value defines the event schema:
{
[eventName: string]: {
request: ZodType // Request payload schema (required)
response?: ZodType // Response payload schema (optional)
}
}
Event Types
Request/Response Events - Client sends request, server responds:
const events = defineEvents({
getData: {
request: z.object({ id: z.string() }),
response: z.object({ name: z.string(), age: z.number() }),
},
})
One-Way Messages - No response expected:
const events = defineEvents({
notify: {
request: z.object({ message: z.string() }),
// No response field = one-way message
},
})
Server-to-Client Messages - Server pushes to client:
const events = defineEvents({
serverUpdate: {
request: z.object({ data: z.any() }),
// Client listens with client.on('serverUpdate', ...)
},
})
Type Inference
defineEvents() automatically infers TypeScript types from Zod schemas:
const events = defineEvents({
ping: {
request: z.object({ timestamp: z.number() }),
response: z.object({ pong: z.string() }),
},
})
// TypeScript knows the types:
// events.ping.request => { timestamp: number }
// events.ping.response => { pong: string }
Complex Schemas
Use any Zod schema features:
const events = defineEvents({
createUser: {
request: z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
age: z.number().int().positive().optional(),
role: z.enum(['admin', 'user', 'guest']),
metadata: z.record(z.string()).optional(),
}),
response: z.object({
id: z.string().uuid(),
createdAt: z.number(),
}),
},
})
Shared Types
Extract types from event definitions:
import type { InferEventPayload, InferEventResponse } from '@mdrv/wsx/shared'
const events = defineEvents({
getData: {
request: z.object({ id: z.string() }),
response: z.object({ name: z.string() }),
},
})
// Extract types
type GetDataRequest = InferEventPayload<typeof events, 'getData'>
// => { id: string }
type GetDataResponse = InferEventResponse<typeof events, 'getData'>
// => { name: string }
Protocol Types
The library uses a versioned binary protocol for communication.
Message Types
type MessageType =
| 'send' // One-way message
| 'request' // Request (expects response)
| 'response_ok' // Successful response
| 'response_error' // Error response
Message Structure
Send Message:
{
v: '1.0', // Protocol version
t: 'send', // Message type
x: 'eventName', // Event name
p?: { ... } // Payload (optional)
}
Request Message:
{
v: '1.0',
t: 'request',
x: 'eventName',
id?: 'unique-id', // Request ID (optional)
w: 1234567890, // Timestamp (fallback for matching)
p?: { ... }
}
Response (Success):
{
v: '1.0',
t: 'response_ok',
x: 'eventName',
id?: 'unique-id',
w: 1234567890,
p?: { ... }
}
Response (Error):
{
v: '1.0',
t: 'response_error',
x: 'eventName',
id?: 'unique-id',
w: 1234567890,
e: {
message: 'Error message',
cause?: { ... }
}
}
Serializers
The library supports multiple serialization formats.
CBOR Serializer (Default)
Fast binary encoding using cbor-x:
import { cborSerializer } from '@mdrv/wsx/shared'
const client = createClient(url, events, {
serializer: cborSerializer,
})
Benefits:
- Faster than JSON
- Smaller payload size
- Supports binary data
- Type preservation (Dates, typed arrays, etc.)
JSON Serializer
Standard JSON encoding:
import { jsonSerializer } from '@mdrv/wsx/shared'
const client = createClient(url, events, {
serializer: jsonSerializer,
})
Use when:
- You need human-readable messages
- Debugging WebSocket traffic
- Integration with non-binary systems
Custom Serializer
Implement your own serializer:
import type { Serializer } from '@mdrv/wsx/shared'
const mySerializer: Serializer = {
encode: (data: any) => {
// Return ArrayBuffer or string
return new TextEncoder().encode(JSON.stringify(data))
},
decode: (data: ArrayBuffer | string) => {
// Parse data back to object
if (typeof data === 'string') {
return JSON.parse(data)
}
return JSON.parse(new TextDecoder().decode(data))
},
}
const client = createClient(url, events, {
serializer: mySerializer,
})
Utility Functions
generateId()
Generate a unique request ID:
import { generateId } from '@mdrv/wsx/shared'
const id = generateId() // => "1234567890-abcd"
Used internally for request/response matching.
Type Guards
Check message types at runtime:
import {
isSendMessage,
isRequestMessage,
isResponseOk,
isResponseError
} from '@mdrv/wsx/shared'
if (isRequestMessage(msg)) {
// msg is RequestMessage
console.log(msg.id, msg.x, msg.p)
}
TypeScript Types
EventDefinitions
type EventDefinitions = Record<string, {
request: ZodType
response?: ZodType
}>
Connection
interface Connection {
ws: ElysiaWebSocket
send(event: string, payload?: any): void
respond(event: string, id: string | undefined, timestamp: number, result: any): void
error(event: string, id: string | undefined, timestamp: number, error: Error): void
}
Serializer
interface Serializer {
encode: (data: any) => ArrayBuffer | string
decode: (data: ArrayBuffer | string) => any
}
Protocol Version
Current protocol version: 1.0
The protocol version is included in every message to ensure compatibility. Future versions will maintain backward compatibility when possible.