@nolag/iot

IoT device telemetry and command dispatch with acknowledgment tracking.

Overview

Connect IoT devices and controllers over real-time channels. Devices push telemetry (sensor readings, status updates, or any structured measurement) while controllers dispatch commands with Promise-based acknowledgment tracking and configurable timeouts. Two roles participate in a device group: devices report data and acknowledge commands, and controllers monitor telemetry streams and issue commands. A single client can act as both. Groups organise devices by fleet, physical location, or function. Your app owns one core NoLag client and injects it into NoLagIoT; the wrapper attaches its behaviour to that connection.

Key Features

  • Real-time telemetry broadcast from devices to all group subscribers
  • Promise-based command dispatch with configurable acknowledgment timeout
  • Ephemeral telemetry for high-frequency data without storage overhead
  • Ephemeral commands with fire-and-forget dispatch and ack tracking
  • Ephemeral acknowledgment channel for command response tracking
  • Device online/offline presence via lobby
  • Multiple groups per connection; join by zone, building floor, vehicle type, etc.

How It Works

NoLagIoT attaches to an injected @nolag/js-sdk client and manages a lobby that reflects device presence. Calling joinGroup(name) returns a DeviceGroup that subscribes to three topics: telemetry for ephemeral sensor readings, commands for ephemeral command payloads, and _cmd_ack for ephemeral acknowledgment messages. All topics are ephemeral (no server-side retention) to handle high-frequency telemetry without storage overhead. When a controller calls sendCommand() the SDK stores a pending Promise keyed on the command ID; when the target device calls ackCommand() the matching Promise resolves or rejects based on the reported status. The app owns the socket lifecycle; the wrapper never opens or closes it.

TopicPurposeReplay
telemetrySensor readings and device status updatesEphemeral
commandsCommands dispatched to devicesEphemeral
_cmd_ackCommand acknowledgment messagesEphemeral

Installation

npm install @nolag/iot @nolag/js-sdk

One core NoLag client can back several wrapper SDKs at once, for example IoT telemetry, a dashboard, and notify on a single socket, as long as each wrapper uses a distinct appName. Each wrapper attaches its handlers on construction and releases them with detach(), and never touches the socket itself. Your app owns connect() and disconnect().

Quick Start

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

// The app owns one core client. In a browser, pass a token provider so the
// SDK can mint fresh short-lived client tokens from your backend.
const client = NoLag(async () => (await (await fetch('/api/nolag-token')).json()).token)

// Inject the client into the IoT wrapper (role: 'device' or 'controller')
const iot = new NoLagIoT({ client, deviceId: 'device-007', role: 'controller' })

await client.connect()   // the app owns the connection
await iot.ready()        // wrapper setup complete

// Join a device group (organised by fleet, location, or function)
const group = await iot.joinGroup('sensors-floor-3')

// ── Device side ───────────────────────────────────────────────────────────────

// Send a telemetry reading
group.sendTelemetry('temperature', 22.4, {
  unit: 'celsius',
  sensorId: 'tmp_01',
})

group.sendTelemetry('humidity', 61, { unit: 'percent' })

// Listen for commands sent by controllers
group.on('command', async ({ command }) => {
  console.log('Received command:', command.name, command.params)

  // Execute the command...
  const result = await handleCommand(command)

  // Acknowledge success
  await group.ackCommand(command.commandId, 'success', result)
})

// ── Controller side ───────────────────────────────────────────────────────────

// Watch all telemetry from the group
group.on('telemetry', ({ deviceId, sensorId, value, opts, timestamp }) => {
  console.log(`[${deviceId}] ${sensorId}: ${value}`)
})

// Retrieve the last known reading for a specific sensor
const reading = await group.getTelemetry('device-007', 'temperature')
console.log('Last temperature:', reading)

// Send a command (returns a Promise that resolves when the device acks)
try {
  const cmd = await group.sendCommand('device-007', 'reboot', {
    delay: 5000,
  })
  // Promise resolves once the device calls ackCommand
  console.log('Command acked by device:', cmd.status)
} catch (err) {
  // Throws if the device does not ack within the configured timeout
  console.error('Command timed out or rejected:', err)
}

// Monitor ack events
group.on('commandAck', ({ commandId, deviceId, status, result }) => {
  console.log(`Command ${commandId} acked by ${deviceId} with status ${status}`)
})

// Presence
iot.on('deviceOnline', ({ deviceId }) => console.log('Online:', deviceId))
iot.on('deviceOffline', ({ deviceId }) => console.log('Offline:', deviceId))

// Teardown: the wrapper releases its handlers; the app closes the socket.
iot.detach()
client.disconnect()

API Reference

NoLagIoT

Constructor Options

OptionTypeDescription
clientNoLagSocketRequired. The injected core NoLag client the app owns and connects.
deviceIdstringStable device identifier (auto-generated if omitted).
deviceNamestringOptional human-readable device name.
role'device' | 'controller'Whether this client acts as a device or a controller (default 'device').
metadataRecord<string, unknown>Optional custom data attached to device presence.
appNamestringNoLag app for topic prefixes (default 'iot').
groupsstring[]Groups to join once the wrapper is ready.
maxTelemetryPointsnumberMax telemetry readings retained per device/sensor key (default 1000).
commandTimeoutnumberCommand acknowledgment timeout in ms (default 30000).
debugbooleanEnable wrapper debug logging (default false).
MethodReturnsDescription
ready()Promise<void>Resolves once wrapper setup completed.
detach()voidRelease this wrapper's handlers and topics; terminal, never closes the socket.
joinGroup(name)Promise<DeviceGroup>Subscribe to a device group. Returns the group instance.
leaveGroup(name)Promise<void>Unsubscribe from a device group and release its resources.
getOnlineDevices()Device[]Return all devices currently present in the lobby.

NoLagIoT Events

EventPayloadDescription
connectednoneWebSocket connection established.
disconnectedreason: stringConnection closed.
reconnectednoneConnection restored; room membership and presence are restored automatically.
errorerror: ErrorA transport or protocol error occurred.
deviceOnlinedevice: DeviceA device joined the lobby.
deviceOfflinedevice: DeviceA device left the lobby.

DeviceGroup

MethodRoleReturnsDescription
sendTelemetry(sensorId, value, opts?)DevicevoidPublish a sensor reading to the group. opts carries metadata such as unit or tags.
getTelemetry(deviceId?, sensorId?)BothPromise<TelemetryReading[]>Fetch the most recent telemetry. Omit arguments to get all devices and sensors.
sendCommand(targetDeviceId, command, params?)ControllerPromise<DeviceCommand>Dispatch a named command to a device. Resolves when the device acknowledges, or rejects on timeout.
ackCommand(commandId, status, result?)DevicevoidSend an acknowledgment for a received command. status is 'success' or 'error'.

DeviceGroup Events

EventPayloadDescription
telemetry{ deviceId, sensorId, value, opts?, timestamp }A telemetry reading was received from a device in the group.
command{ command: DeviceCommand }A command was dispatched to this device (device role only).
commandAck{ commandId, deviceId, status, result? }A device acknowledged a previously dispatched command.
deviceJoineddevice: DeviceA device joined this group.
deviceLeftdevice: DeviceA device left this group.