Quick Start

Get up and running with @mdrv/wsx in 5 minutes.

Installation

bun add @mdrv/wsx zod cbor-x elysia

1. Define Your Events

Create a shared event schema that both client and server will use:

// events.ts
import { defineEvents } from '@mdrv/wsx/shared'
import { z } from 'zod'

export const events = defineEvents({
  ping: {
    request: z.object({ timestamp: z.number() }),
    response: z.object({ pong: z.string() }),
  },
  notify: {
    request: z.object({ message: z.string() }),
    // No response field = one-way message
  },
})

The defineEvents function creates a type-safe event schema where:

  • request defines the payload structure sent by the client
  • response defines the structure returned by the server (optional for one-way messages)

2. Create the Server

// server.ts
import { createElysiaWS } from '@mdrv/wsx/server'
import { Elysia } from 'elysia'
import { events } from './events'

const { server, handler } = createElysiaWS(events, {
  validate: true, // Enable Zod validation
  debug: true,    // Enable debug logging
})

// Handle request/response
server.onRequest('ping', async (payload) => {
  console.log('Received ping:', payload.timestamp)
  return { pong: 'Hello from server!' }
})

// Handle one-way messages
server.onSend('notify', async (payload) => {
  console.log('Notification:', payload.message)
})

// Attach to Elysia
new Elysia()
  .ws('/ws', handler)
  .listen(3000)

console.log('WebSocket server running on http://localhost:3000/ws')

3. Create the Client

// client.ts
import { createClient } from '@mdrv/wsx/client'
import { events } from './events'

const client = createClient('ws://localhost:3000/ws', events, {
  validate: true,
  debug: true,
})

client.connect()

client.onOpen(async () => {
  console.log('Connected to server!')
  
  // Send request and await response
  const result = await client.request('ping', {
    timestamp: Date.now(),
  })
  console.log('Server responded:', result.pong)
  
  // Send one-way message
  client.send('notify', {
    message: 'Hello from client!',
  })
})

client.onClose(() => {
  console.log('Disconnected from server')
})

client.onError((error) => {
  console.error('Connection error:', error)
})

4. Run Your Application

# Terminal 1: Start the server
bun server.ts

# Terminal 2: Run the client
bun client.ts

You should see:

Connected to server!
Server responded: Hello from server!

What’s Next?

Key Concepts

Type Safety

All events are fully typed. TypeScript will autocomplete event names and payloads:

// ✅ TypeScript knows 'ping' exists and requires { timestamp: number }
await client.request('ping', { timestamp: Date.now() })

// ❌ TypeScript error: 'unknownEvent' doesn't exist
await client.request('unknownEvent', {})

// ❌ TypeScript error: missing required field 'timestamp'
await client.request('ping', {})

Validation

Zod validates all messages at runtime:

// ❌ Runtime error: validation failed
await client.request('ping', { timestamp: 'not a number' })

Auto-Reconnection

The client automatically reconnects with exponential backoff:

const client = createClient(url, events, {
  maxRetries: Infinity,
  minReconnectionDelay: 1000,    // Start at 1 second
  maxReconnectionDelay: 30000,   // Max 30 seconds
  reconnectionDelayGrowFactor: 1.3,
})

Message Queuing

Messages sent while disconnected are queued and sent when reconnected automatically.