Skip to content

AgoraIO-Community/signaling_nodejs

Repository files navigation

rtm_nodejs

RTM (signaling) client for Node.js wrapping the Agora RTM C SDK via koffi FFI — no native compilation required.

Why this package

  • Pure-JS FFI binding via koffi — no node-gyp, no build toolchain
  • Works on macOS and Linux
  • Includes SDK auto-install flow for native libs
  • Supports multiple clients per process via worker_threads

Platform support

  • macOS arm64
  • Linux x64

Prerequisites

  • Node.js 18+

No C++ compiler or build tools needed.

Install

npm install

Import

CommonJS:

const { createRtmClient, RtmConfig } = require('rtm_nodejs');

ESM:

import { createRtmClient, RtmConfig } from 'rtm_nodejs';

Quick start

import { createRtmClient, RtmConfig, SubscribeOptions } from 'rtm_nodejs';

const client = await createRtmClient(
  new RtmConfig({ appId: 'YOUR_APP_ID', userId: '1001' })
);

client.on('message', (e) => {
  console.log('incoming', e.channelName, e.publisher, e.message);
});

client.on('linkState', (e) => {
  console.log('link state', e.previousState, '→', e.currentState);
});

await client.login('');
await client.subscribe('test-channel', new SubscribeOptions({ withMessage: true }));
await client.sendChannelMessage('test-channel', 'hello from node');

Multi-client (worker_threads)

The native SDK limits each process to one reliable client. To run multiple clients in a single process, use createWorkerRtmClient which runs each additional client in its own worker_thread:

import { createRtmClient, createWorkerRtmClient } from 'rtm_nodejs';

// First client — runs in the main thread (as before)
const client1 = await createRtmClient({ appId: APP_ID, userId: 'user1' });

// Second client — runs in a worker thread
const client2 = await createWorkerRtmClient({ appId: APP_ID, userId: 'user2' });

// Same API from here
client2.on('message', (e) => console.log(e));
await client2.login('');

The WorkerRTMClient API mirrors RTMClient with two differences:

  • createStreamChannel(name) is async (returns Promise<WorkerStreamChannel>)
  • getVersion() / getErrorReason(code) are async (returns Promise<string>)

See examples/rtm_multi_client.js for a full working example.

Demo

Set your app ID, then run:

export AGORA_APP_ID=your_app_id_here
npm run example

Custom user IDs:

AGORA_APP_ID=your_app_id_here node examples/rtm_basic.js 1001 1002

The demo:

  1. logs in
  2. subscribes test-channel
  3. sends one channel message
  4. sends one peer message
  5. waits and prints incoming messages
  6. cleanly logs out on Ctrl+C

SDK native library

This package wraps Agora RTM native SDK 2.2.8.

The native libs are auto-assembled on first use:

  1. The Agora RTM SDK (libAgoraRtmKit.dylib / libagora_rtm_sdk.so plus libaosl) is downloaded from Agora's CDN.
  2. The matching C ABI shim (libagora_rtm_sdk_c.{dylib,so}) for your platform is copied from the npm package's prebuilds/ directory. The shim is built from the C++ source in native/ and translates the koffi-friendly C ABI used by this package into calls to Agora's C++ SDK.

Default behavior:

  • Native libs land in agora_rtm/agora/agora_sdk
  • A signature is stored in agora_rtm/agora/version.txt so re-installs don't re-download

Overrides:

  • AGORA_SDK_LIBRARY_DIR: use an existing local SDK directory (skip auto-download)
  • AGORA_SDK_URL: force a specific SDK zip URL

Files placed in agora_sdk/:

  • macOS: libagora_rtm_sdk_c.dylib (shim) + libAgoraRtmKit.dylib + libaosl.dylib
  • Linux: libagora_rtm_sdk_c.so (shim) + libagora_rtm_sdk.so + libaosl.so

Building the shim from source

If you're cloning the repo for development, prebuilt shims live under prebuilds/<platform>-<arch>/. To rebuild for your host:

# macOS — build from the host
cd native && cmake -S . -B build && cmake --build build

# Linux x64 — uses Docker
bash native/scripts/build-linux.sh x64

CI (.github/workflows/build-shim.yml) produces all three platform shims on tagged releases and uploads them as the prebuilds-all artifact.

API reference

createRtmClient(config, options?)

  • config: RtmConfig
  • options.libraryDir?: explicit SDK lib directory
  • returns: Promise<RTMClient>

createWorkerRtmClient(config, options?)

  • config: RtmConfig (plain object — eventHandler is ignored)
  • options.libraryDir?: explicit SDK lib directory
  • options.timeout?: RPC timeout in ms (default 10000)
  • returns: Promise<WorkerRTMClient>

RtmConfig

Main fields:

  • appId, userId
  • useStringUserId (true for string IDs, false for numeric IDs; default true)
  • areaCode: RtmAreaCode value (default RTM_AREA_CODE_GLOB)
  • logConfig, proxyConfig, encryptionConfig, privateConfig

Events

RTMClient extends EventEmitter. Listen with client.on(event, handler).

Event Handler signature Description
'message' (event) Incoming message on a subscribed channel or from a user
'presence' (event) Presence change in a subscribed channel
'topic' (event) Topic publisher change in a stream channel
'lock' (event) Lock state change in a subscribed channel
'storage' (event) Metadata change in a subscribed channel or user
'linkState' (event) SDK connection state changed

RTMClient methods

All async methods return a Promise that resolves on success or rejects with an RtmError on failure.

Method Resolves with Description
login(token) void Log in to RTM
logout() void Log out
renewToken(token) { serviceType, channelName } Renew auth token
publish(target, message, options?) void Publish to a channel or user
sendChannelMessage(channel, message) void Publish string message to a message channel
sendUserMessage(userId, message) void Send string message to a specific user
subscribe(channel, options?) void Subscribe to a channel
unsubscribe(channel) void Unsubscribe from a channel
getPresence() RTMPresence | null Get the presence interface
getStorage() RTMStorage | null Get the storage interface
getLock() RTMLock | null Get the lock interface
getHistory() RTMHistory | null Get the history interface
createStreamChannel(name) RTMStreamChannel | null Create a stream channel instance
getVersion() string SDK version string
getErrorReason(code) string Human-readable error description
setParameters(json) number (sync) Apply a private SDK parameter (JSON string). Returns 0 on success. Must be called before login() for parameters that affect the login handshake.
release() Destroy the client and free resources

setParameters(json) — private SDK parameters

Synchronous escape hatch for SDK parameters that don't have a typed wrapper. Pass a JSON string. Returns 0 on success, negative on failure. Must be called before login() if the parameter affects the login handshake.

Currently known opt-in flag, both verified end-to-end:

// Allow '.' in channel names (default SDK behavior rejects them).
client.setParameters('{"rtm.extend_channel_name": true}');
await client.login('');
await client.subscribe('any.dotted.channel', new SubscribeOptions({ withMessage: true }));

Notes:

  • The flag relaxes only the dot rule for channel names. *, /, \, \0, non-printable ASCII, and leading underscore are still rejected.
  • The flag has no effect on user IDs — those continue to follow the default character ruleset.
  • setParameters keys are private SDK parameters: not part of the public Agora API and may change in future SDK releases.
  • Multiple keys can be combined in one JSON object: '{"rtm.extend_channel_name": true, "rtm.channel.join_limit": [5000, 20]}'.

See docs/DOTTED_NAMES_REPORT.md for the empirical test matrix.

RtmError

Rejected promises throw RtmError (extends Error) with a .code property containing the numeric SDK error code.

try {
  await client.login('');
} catch (err) {
  if (err instanceof RtmError) {
    console.error(err.code, err.message);
  }
}

RTMPresence methods

Obtained via client.getPresence(). All methods return a Promise.

Method Resolves with Description
whoNow(channelName, channelType, options?) { userStateList, nextPage } Query users currently in a channel
whereNow(userId) { channels } Query channels a user is in
getOnlineUsers(channelName, channelType, options?) { userStateList, nextPage } Get paginated list of online users
getUserChannels(userId) { channels } Get all channels a user has joined
setState(channelName, channelType, items) void Set user state key/value pairs
removeState(channelName, channelType, keys) void Remove user state keys
getState(userId, channelName, channelType) { state } Get state of a specific user

channelType defaults to RtmChannelType.RTM_CHANNEL_TYPE_MESSAGE.

options for whoNow / getOnlineUsers (PresenceOptions / GetOnlineUsersOptions):

  • includeUserId — boolean (default true)
  • includeState — boolean (default false)
  • page — string pagination token (default '')

items for setState: [{ key: string, value: string }]

keys for removeState: string[]


Exports

Classes / config: RtmConfig, RtmLogConfig, RtmProxyConfig, RtmPrivateConfig, RtmEncryptionConfig, PublishOptions, SubscribeOptions, PresenceOptions, GetOnlineUsersOptions, MetadataOptions, MetadataItem, JoinChannelOptions, JoinTopicOptions, TopicMessageOptions, GetHistoryMessagesOptions, IRtmEventHandler

Client classes: RTMClient, WorkerRTMClient, RTMPresence, RTMStorage, RTMLock, RTMHistory, RTMStreamChannel, RtmError

Enums: RtmAreaCode, RtmLogLevel, RtmEncryptionMode, RtmServiceType, RtmChannelType, RtmMessageType, RtmStorageType, RtmStorageEventType, RtmLockEventType, RtmProxyType, RtmTopicEventType, RtmPresenceEventType, RtmLinkState, RtmLinkOperation, RtmLinkStateChangeReason, RtmErrorCode

Helper: enumNameByValue(enumObj, value, fallback?)


Troubleshooting

create failed / no callbacks

  • Verify appId, userId, and token policy
  • Make sure both clients use the same channel string
  • Ensure network is available to Agora endpoints

spdlog log file errors on startup

Pre-create the log file path you configured, or disable file logging:

logConfig: new RtmLogConfig({ filePath: '' })

SDK lib not found

export AGORA_SDK_LIBRARY_DIR=/absolute/path/to/libs

License

MIT

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors