JavaScript SDK

The official NoLag SDK for JavaScript and TypeScript. Full TypeScript support with comprehensive type definitions.

Installation

# npm
npm install @nolag/js-sdk

# yarn
yarn add @nolag/js-sdk

# pnpm
pnpm add @nolag/js-sdk

Quick Start

import { NoLag } from '@nolag/js-sdk'

// Create client with access token
const client = NoLag('your-access-token')

// Connect to NoLag
await client.connect()

// Subscribe to a topic
client.subscribe('chat/room-1')

// Listen for messages
client.on('chat/room-1', (data, meta) => {
  console.log('Received:', data)
})

// Publish a message
client.emit('chat/room-1', { text: 'Hello!' })

Connection Options

const client = NoLag('your-access-token', {
  // Connection
  url: 'wss://broker.nolag.app/ws',  // WebSocket URL (default)
  reconnect: true,                    // Auto-reconnect (default: true)
  reconnectInterval: 5000,            // Initial reconnect delay ms (default: 5000)

  // Messaging
  qos: 1,                             // Default QoS level: 0, 1, or 2 (default: 1)

  // Load Balancing
  loadBalance: false,                 // Enable load balancing (default: false)
  loadBalanceGroup: 'worker-pool',    // Load balance group name

  // Browser-specific
  disconnectOnHidden: false,          // Disconnect when tab hidden (default: false)
  heartbeatInterval: 30000,           // Heartbeat interval ms (default: 30000)

  // Debugging
  debug: false,                       // Enable debug logging (default: false)
})

Connection Management

// Connect to NoLag
await client.connect()

// Check connection status
console.log(client.status)     // 'disconnected' | 'connecting' | 'connected' | 'reconnecting'
console.log(client.connected)  // true or false

// Disconnect (prevents auto-reconnect)
client.disconnect()

// Access client info (available after connect)
console.log(client.actorId)    // Your actor token ID
console.log(client.actorType)  // 'device' | 'user' | 'server'
console.log(client.projectId)  // Project ID

Client Events

// Connection established
client.on('connect', () => {
  console.log('Connected to NoLag')
})

// Connection lost
client.on('disconnect', (reason) => {
  console.log('Disconnected:', reason)
})

// Reconnection starting
client.on('reconnect', () => {
  console.log('Attempting to reconnect...')
})

// Error occurred
client.on('error', (error) => {
  console.error('Error:', error)
})

// Replay events — fired when missed messages are replayed on reconnect
client.on('replay:start', (event) => {
  // event: { count, oldestTimestamp?, newestTimestamp? }
  console.log(`Replaying ${event.count} missed messages...`)
})

client.on('replay:end', (event) => {
  // event: { replayed }
  console.log(`Replay complete: ${event.replayed} messages`)
})

// During replay, individual messages have meta.isReplay = true
client.on('chat/room-1', (data, meta) => {
  if (meta.isReplay) {
    console.log('Replayed message:', data)
  } else {
    console.log('Live message:', data)
  }
})

Subscribing to Topics

// Basic subscription
client.subscribe('chat/room-1')

// With options
client.subscribe('chat/room-1', {
  qos: 2,                        // Override default QoS
  loadBalance: true,             // Enable load balancing for this topic
  loadBalanceGroup: 'workers',   // Specific load balance group
})

// With acknowledgment callback
client.subscribe('chat/room-1', (error) => {
  if (error) {
    console.error('Subscribe failed:', error)
  } else {
    console.log('Subscribed successfully')
  }
})

// Wildcard subscriptions
client.subscribe('chat/+/messages')  // Single level (+)
client.subscribe('users/123/#')      // Multi level (#)

Listening for Messages

// Listen to a specific topic
client.on('chat/room-1', (data, meta) => {
  console.log('Data:', data)
  console.log('Message ID:', meta.msgId)     // Unique message ID
  console.log('Is replay:', meta.isReplay)   // true if replayed from history
  console.log('Filter:', meta.filter)        // Filter value (if published with one)
})

// Listen to all subscribed topics (wildcard handler)
client.onAny((topic: string, data: unknown, meta: MessageMeta) => {
  console.log(`[${topic}]`, data, meta)
})

// Remove a specific handler
const handler = (data, meta) => console.log(data)
client.on('chat/room-1', handler)
client.off('chat/room-1', handler)

// Remove all handlers for a topic
client.off('chat/room-1')

Publishing Messages

// Basic publish
client.emit('chat/room-1', { text: 'Hello!' })

// With options
client.emit('chat/room-1', { text: 'Important' }, {
  qos: 2,           // Override default QoS
  echo: false,      // Don't receive your own message (default: true)
  filter: 'room-1', // Route to subscribers with this filter
})

// With acknowledgment callback
client.emit('chat/room-1', { text: 'Hello' }, (error) => {
  if (error) {
    console.error('Publish failed:', error)
  } else {
    console.log('Message sent')
  }
})

// With options and callback
client.emit('chat/room-1', { text: 'Hello' }, { qos: 2 }, (error) => {
  if (error) console.error(error)
})

Room Presence

Presence is scoped to rooms. Use the fluent API to set and observe presence:

// Join a room and set presence
const room = client.setApp('chat').setRoom('general')

room.setPresence({
  username: 'Alice',
  status: 'online',
  avatar: '/img/alice.png'
})

// Get all actors in this room (from local cache)
const actors = room.getPresence()
console.log('In room:', Object.keys(actors).length)

// Fetch presence list from server (async)
const freshList = await room.fetchPresence()

// Listen for presence events in this room
room.on('presence:join', (actor) => {
  console.log(`${actor.presence.username} joined`)
})

room.on('presence:leave', (actor) => {
  console.log(`${actor.presence.username} left`)
})

room.on('presence:update', (actor) => {
  console.log(`${actor.presence.username} updated status`)
})

Lobbies (Multi-Room Presence)

Lobbies let you observe presence across multiple rooms. Use them for dashboards and monitoring interfaces:

// Subscribe to a lobby to observe all rooms
const lobby = client.setApp('rides').setLobby('active-trips')

// Subscribe returns a snapshot of current presence
const snapshot = await lobby.subscribe()
// snapshot: { roomId -> { actorId -> presenceData } }
console.log('Active trips:', Object.keys(snapshot).length)

// Listen for presence events (includes room context)
lobby.on('presence:join', (event) => {
  // event: { lobbyId, roomId, actorId, data }
  console.log(`${event.actorId} joined room ${event.roomId}`)
  addToMap(event.roomId, event.actorId, event.data)
})

lobby.on('presence:update', (event) => {
  updateOnMap(event.roomId, event.actorId, event.data)
})

lobby.on('presence:leave', (event) => {
  removeFromMap(event.roomId, event.actorId)
})

// Fetch fresh presence at any time
const freshPresence = await lobby.fetchPresence()

// Unsubscribe when done
lobby.unsubscribe()
Read-only: Lobbies are for observation only. Actors set presence on rooms, not lobbies. See Lobbies documentation for details.

Fluent API (Scoped Pub/Sub)

Use the fluent API to scope subscriptions and messages to a specific app and room:

// Create a room context
const room = client.setApp('chat').setRoom('general')

// Subscribe (topic is auto-prefixed to 'chat/general/messages')
room.subscribe('messages')

// Listen for messages
room.on('messages', (data, meta) => {
  console.log('Message:', data)
})

// Publish (also auto-prefixed)
room.emit('messages', { text: 'Hello room!' })

// Get the full topic prefix
console.log(room.prefix) // 'chat/general'

// Unsubscribe
room.unsubscribe('messages')

Unsubscribing

// Unsubscribe from a topic
client.unsubscribe('chat/room-1')

// With callback
client.unsubscribe('chat/room-1', (error) => {
  if (error) {
    console.error('Unsubscribe failed:', error)
  }
})

Topic Filters

Filters let you narrow message delivery to specific entities within a topic. Instead of receiving all updates on a topic, subscribe with filters to only get messages for the entities you care about.

// Subscribe with filters — only receive updates for these bookings
client.subscribe('bookings', {
  filters: ['booking_1', 'booking_2']
})

// Listen for filtered messages
client.on('bookings', (data, meta) => {
  console.log('Booking update:', data)
  console.log('Filter:', meta.filter)  // e.g. 'booking_1'
})

// Publish to a specific filter
client.emit('bookings', { status: 'confirmed' }, {
  filter: 'booking_1'  // Only subscribers with this filter receive it
})

Dynamic Filter Management

Change filters at any time without resubscribing:

// Replace all filters for a topic
client.setFilters('bookings', ['booking_3', 'booking_4'])

// Add filters to existing set
client.addFilters('bookings', ['booking_5'])

// Remove specific filters
client.removeFilters('bookings', ['booking_3'])

// Filters work with the Room API too
const room = client.setApp('dashboard').setRoom('ops')
room.subscribe('bookings', { filters: ['booking_1'] })
room.setFilters('bookings', ['booking_2', 'booking_3'])
room.addFilters('bookings', ['booking_4'])
room.removeFilters('bookings', ['booking_2'])
No filters = wildcard: Subscribing without filters receives all messages on the topic. Subscribing with filters only receives messages published with a matching filter. Messages published without a filter are only delivered to wildcard (no-filter) subscribers.

Quality of Service (QoS)

// QoS 0: At most once (fire and forget)
client.emit('telemetry/data', data, { qos: 0 })

// QoS 1: At least once (guaranteed delivery, may duplicate)
client.emit('orders/new', order, { qos: 1 })

// QoS 2: Exactly once (guaranteed single delivery)
client.emit('payments/process', payment, { qos: 2 })

// Set default QoS for all messages
const client = NoLag(token, { qos: 1 })

Load Balancing

Distribute messages across multiple clients in a group using round-robin:

// Enable for all subscriptions
const client = NoLag(token, {
  loadBalance: true,
  loadBalanceGroup: 'worker-pool-1'
})

// Or per-subscription
client.subscribe('jobs/process', {
  loadBalance: true,
  loadBalanceGroup: 'job-workers'
})

// Only ONE client in the group receives each message

REST API Client

Manage apps, rooms, and actors programmatically:

import { NoLagApi } from '@nolag/js-sdk'
import type { PaginatedResult, App } from '@nolag/js-sdk'

const api = new NoLagApi('your-api-key', {
  baseUrl: 'https://api.nolag.app/v1',  // Optional
  timeout: 30000,                        // Optional
})

// Apps — list() returns PaginatedResult<App>
const result: PaginatedResult<App> = await api.apps.list()
console.log(result.data)        // App[]
console.log(result.total)       // Total count
console.log(result.totalPages)  // Total pages

const singleApp = await api.apps.get(appId)            // Get by ID
const app = await api.apps.create({ name: 'My App' })  // Create
await api.apps.update(app.appId, { name: 'Updated' })  // Update
await api.apps.delete(app.appId)                        // Delete

// Rooms
const rooms = await api.rooms.list(appId)                // List rooms in an app
const singleRoom = await api.rooms.get(appId, roomId)    // Get by ID
const room = await api.rooms.create(appId, {             // Create
  name: 'General', slug: 'general'
})
await api.rooms.update(appId, room.roomId, {             // Update
  name: 'Updated Room'
})
await api.rooms.delete(appId, room.roomId)               // Delete

// Actors
const actors = await api.actors.list()                   // List all actors
const singleActor = await api.actors.get(actorTokenId)   // Get by ID
const actor = await api.actors.create({                  // Create
  name: 'Device 1',
  actorType: 'device'
})
console.log('Access Token:', actor.accessToken)  // Save this! Only shown once

await api.actors.update(actor.actorTokenId, { name: 'Updated Device' })
await api.actors.delete(actor.actorTokenId)

TypeScript Support

The SDK includes full TypeScript definitions:

import {
  NoLag,
  NoLagApi,
  NoLagOptions,
  ConnectionStatus,
  MessageMeta,
  ActorPresence,
  PresenceData,
  LobbyPresenceEvent,
  LobbyPresenceState,
  ReplayStartEvent,
  ReplayEndEvent,
  QoS,
  SubscribeOptions,
  EmitOptions,
  RoomContext,
  LobbyContext,
  PaginatedResult,
} from '@nolag/js-sdk'

// Type your message data
interface ChatMessage {
  text: string
  sender: string
  timestamp: number
}

client.on('chat/room-1', (data: ChatMessage, meta: MessageMeta) => {
  console.log(`[${data.sender}]: ${data.text}`)
})

Error Handling

// Connection errors
client.on('error', (error) => {
  console.error('Client error:', error.message)
})

// Subscribe/emit callbacks
client.subscribe('topic', (error) => {
  if (error) {
    console.error('Subscribe failed:', error.message)
  }
})

client.emit('topic', data, (error) => {
  if (error) {
    console.error('Emit failed:', error.message)
  }
})

// REST API errors
import { NoLagApiError } from '@nolag/js-sdk'

try {
  await api.apps.get('invalid-id')
} catch (error) {
  if (error instanceof NoLagApiError) {
    console.error('API Error:', error.statusCode, error.message)
  }
}

Browser Usage

<script type="module">
  import { NoLag } from 'https://unpkg.com/@nolag/js-sdk/dist/browser.js'

  const client = NoLag('your-access-token')
  await client.connect()

  client.subscribe('updates')
  client.on('updates', (data) => {
    document.getElementById('output').textContent = JSON.stringify(data)
  })
</script>

Complete Example

import { NoLag, NoLagApi } from '@nolag/js-sdk'

// Create an actor via REST API (server-side)
const api = new NoLagApi('your-api-key')
const actor = await api.actors.create({
  name: 'chat-client',
  actorType: 'user'
})

// Connect with the actor's access token (client-side)
const client = NoLag(actor.accessToken, {
  reconnect: true,
  qos: 1
})

client.on('connect', () => {
  console.log('Connected as:', client.actorId)

  // Set presence
  client.setPresence({ username: 'Alice', status: 'online' })

  // Subscribe to chat room
  const room = client.setApp('chat').setRoom('general')
  room.subscribe('messages')

  room.on('messages', (data, meta) => {
    console.log('Message:', data, meta.isReplay ? '(replay)' : '(live)')
  })
})

client.on('presence:join', (actor) => {
  console.log(`${actor.presence.username} joined`)
})

client.on('disconnect', (reason) => {
  console.log('Disconnected:', reason)
})

client.on('error', (error) => {
  console.error('Error:', error)
})

await client.connect()

// Send a message
const room = client.setApp('chat').setRoom('general')
room.emit('messages', { text: 'Hello everyone!' })

Next Steps