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.

See Also