Skip to main content

Overview

The @proj-airi/server-sdk package provides a client SDK for connecting to the AIRI server runtime. It handles WebSocket communication, authentication, heartbeat management, and event routing.

Installation

npm install @proj-airi/server-sdk

Quick Start

Connect to a server runtime:
import { Client } from '@proj-airi/server-sdk'

const client = new Client({
  url: 'ws://localhost:6121/ws',
  name: 'my-module',
  token: 'your-auth-token'
})

// Listen for events
client.onEvent('registry:modules:sync', (event) => {
  console.log('Modules:', event.data.modules)
})

// Send events
client.send({
  type: 'custom:event',
  data: { message: 'Hello' }
})

Client Class

Constructor

Create a new client instance.
const client = new Client<CustomData>(options)
options.url
string
default:"ws://localhost:6121/ws"
WebSocket server URL
options.name
string
required
Module name for identification
options.token
string
Authentication token (if server requires auth)
options.identity
MetadataEventSource
Custom module identity (auto-generated if not provided)
options.possibleEvents
string[]
List of event types this module can emit
options.dependencies
ModuleDependency[]
Module dependencies
options.configSchema
ModuleConfigSchema
Configuration schema for this module
options.autoConnect
boolean
default:"true"
Connect automatically on construction
options.autoReconnect
boolean
default:"true"
Reconnect automatically on disconnect
options.maxReconnectAttempts
number
default:"-1"
Maximum reconnection attempts (-1 for unlimited)
options.heartbeat.readTimeout
number
default:"30000"
Heartbeat interval in milliseconds
options.heartbeat.message
MessageHeartbeat | string
Custom heartbeat message
options.onError
(error: unknown) => void
Error handler callback
options.onClose
() => void
Connection close callback
options.onAnyMessage
(data: WebSocketEvent) => void
Handler for all incoming messages
options.onAnySend
(data: WebSocketEvent) => void
Handler for all outgoing messages

Methods

connect()

Manually connect to the server.
await client.connect()

send()

Send an event to the server.
client.send({
  type: 'custom:event',
  data: { message: 'Hello' },
  route: {
    destinations: ['target-module']
  }
})
data.type
string
required
Event type identifier
data.data
T
required
Event payload data
data.route
object
Routing information
data.metadata
object
Event metadata (auto-populated if not provided)

sendRaw()

Send raw data without serialization.
client.sendRaw('raw string data')
client.sendRaw(new Uint8Array([1, 2, 3]))

onEvent()

Register an event handler.
client.onEvent('module:configure', async (event) => {
  const config = event.data.config
  // Handle configuration
})
event
string
required
Event type to listen for
callback
function
required
Event handler function

offEvent()

Unregister an event handler.
const handler = (event) => {
  // Handle event
}

client.onEvent('custom:event', handler)

// Later: remove specific handler
client.offEvent('custom:event', handler)

// Or remove all handlers for an event
client.offEvent('custom:event')

close()

Close the connection.
client.close()

TypeScript Generics

Type your custom event data:
interface CustomEvents {
  'custom:greeting': { name: string }
  'custom:data': { value: number }
}

const client = new Client<CustomEvents>({
  url: 'ws://localhost:6121/ws',
  name: 'typed-module'
})

// Type-safe event handling
client.onEvent('custom:greeting', (event) => {
  const name = event.data.name  // TypeScript knows this is a string
})

// Type-safe sending
client.send({
  type: 'custom:greeting',
  data: { name: 'Alice' }  // TypeScript enforces correct shape
})

Event Types

The SDK re-exports event types from @proj-airi/server-shared:
import type {
  WebSocketEvent,
  MetadataEventSource,
  ModuleDependency,
  ModuleConfigSchema
} from '@proj-airi/server-sdk'

import {
  WebSocketEventSource,
  ContextUpdateStrategy,
  MessageHeartbeat,
  MessageHeartbeatKind
} from '@proj-airi/server-sdk'

Connection Lifecycle

The client manages connection lifecycle automatically:

Authentication Flow

const client = new Client({
  url: 'ws://localhost:6121/ws',
  name: 'secure-module',
  token: 'secret-token'
})

// Client automatically:
// 1. Connects to server
// 2. Sends module:authenticate with token
// 3. Waits for module:authenticated response
// 4. Announces module to registry
// 5. Receives registry:modules:sync

Heartbeat Management

Heartbeats maintain connection health:
const client = new Client({
  url: 'ws://localhost:6121/ws',
  name: 'monitored-module',
  heartbeat: {
    readTimeout: 30000,  // Send ping every 30s
    message: MessageHeartbeat.Ping
  }
})

// Client automatically sends heartbeat pings
// Server responds with pongs
// Connection is maintained

Error Handling

const client = new Client({
  url: 'ws://localhost:6121/ws',
  name: 'resilient-module',
  onError: (error) => {
    console.error('Client error:', error)
  },
  onClose: () => {
    console.log('Connection closed')
  },
  autoReconnect: true,
  maxReconnectAttempts: 10
})

// Handle specific error events
client.onEvent('error', (event) => {
  console.error('Server error:', event.data.message)
  
  if (event.data.message === 'not authenticated') {
    // Handle authentication failure
  }
})

Reconnection with Backoff

The client implements exponential backoff:
// Attempt 1: 1s delay
// Attempt 2: 2s delay
// Attempt 3: 4s delay
// Attempt 4: 8s delay
// Attempt 5: 16s delay
// Attempt 6+: 30s delay (capped)
Configure reconnection:
const client = new Client({
  url: 'ws://localhost:6121/ws',
  name: 'persistent-module',
  autoReconnect: true,
  maxReconnectAttempts: -1  // Unlimited attempts
})

Event Routing

Route events to specific destinations:
// Route to specific modules
client.send({
  type: 'custom:request',
  data: { query: 'data' },
  route: {
    destinations: ['processor-module', 'logger-module']
  }
})

// Route by module instance
client.send({
  type: 'custom:request',
  data: { query: 'data' },
  route: {
    destinations: [
      { name: 'worker-module', index: 0 },
      { name: 'worker-module', index: 1 }
    ]
  }
})

// Route by labels
client.send({
  type: 'custom:broadcast',
  data: { message: 'Hello' },
  route: {
    destinations: [
      { labels: { role: 'processor' } }
    ]
  }
})

// Broadcast to all (default)
client.send({
  type: 'custom:broadcast',
  data: { message: 'Everyone' }
})

Module Registry

Receive and track connected modules:
const modules = new Map()

client.onEvent('registry:modules:sync', (event) => {
  for (const module of event.data.modules) {
    modules.set(module.identity.id, module)
  }
  
  console.log('Known modules:', Array.from(modules.keys()))
})

Configuration Handling

client.onEvent('module:configure', async (event) => {
  const config = event.data.config
  
  try {
    // Validate configuration
    if (!config.apiKey) {
      throw new Error('API key required')
    }
    
    // Apply configuration
    await applyConfig(config)
    
    // Acknowledge
    client.send({
      type: 'module:configuration:configured',
      data: {
        identity: client.identity,
        config
      }
    })
  } catch (error) {
    // Report error
    client.send({
      type: 'error',
      data: {
        message: error.message
      }
    })
  }
})

Message Interception

Intercept all messages for logging or debugging:
const client = new Client({
  url: 'ws://localhost:6121/ws',
  name: 'debug-module',
  onAnyMessage: (event) => {
    console.log('Received:', event.type, event.data)
  },
  onAnySend: (event) => {
    console.log('Sending:', event.type, event.data)
  }
})

Node.js Utilities

For Node.js environments:
import { /* node utilities */ } from '@proj-airi/server-sdk/utils/node'

// Node-specific utilities (implementation details in source)

Complete Example

AI module connecting to server:
import { Client } from '@proj-airi/server-sdk'

interface AIModuleEvents {
  'ai:generate': { prompt: string, model: string }
  'ai:response': { text: string, tokens: number }
}

const client = new Client<AIModuleEvents>({
  url: 'ws://localhost:6121/ws',
  name: 'ai-module',
  token: process.env.AUTH_TOKEN,
  identity: {
    kind: 'plugin',
    plugin: {
      id: 'ai-module',
      version: '1.0.0',
      labels: { role: 'processor', tier: 'premium' }
    },
    id: 'ai-module-1'
  },
  possibleEvents: ['ai:response'],
  onError: (error) => {
    console.error('Connection error:', error)
  },
  autoReconnect: true
})

// Handle generation requests
client.onEvent('ai:generate', async (event) => {
  const { prompt, model } = event.data
  
  try {
    // Generate response
    const response = await generateText(prompt, model)
    
    // Send response
    client.send({
      type: 'ai:response',
      data: {
        text: response.text,
        tokens: response.tokens
      },
      metadata: {
        event: {
          parentId: event.metadata.event.id
        }
      }
    })
  } catch (error) {
    // Send error
    client.send({
      type: 'error',
      data: {
        message: error.message
      },
      metadata: {
        event: {
          parentId: event.metadata.event.id
        }
      }
    })
  }
})

// Graceful shutdown
process.on('SIGTERM', () => {
  console.log('Shutting down...')
  client.close()
  process.exit(0)
})

Best Practices

Always enable autoReconnect for production services to handle network issues gracefully.
Include parentId in responses to enable request-response correlation and distributed tracing.
Configure appropriate heartbeat intervals based on your network conditions (default: 30s).
Use TypeScript generics to type-check event data and catch errors at compile time.
Listen for authentication errors and handle them appropriately (retry with new token, notify user, etc.).

Next Steps

Server Runtime

Learn about the server runtime

WebSocket Protocol

Understand the WebSocket protocol

Build docs developers (and LLMs) love