gluer-nodejs
v1.0.57
Published
Node.js worker SDK for Gluer: expose JavaScript functions as remotely callable actions over the Gluer real-time mesh.
Maintainers
Readme
gluer-nodejs
Node.js worker SDK for Gluer: expose plain JavaScript functions as remotely callable actions over the Gluer real-time mesh.
What problem it solves
Gluer is a managed real-time integration layer between applications. Browsers, mobile apps, backends in any language, third-party webhooks and locally running LLMs all connect to the same message bus (wss://ws.gluer.io) and address each other by name instead of by network location.
This package is the worker side of that picture. A worker:
- opens an outbound WebSocket connection to the hosted mesh — no inbound ports, no public IP, no reverse proxy, so it runs fine behind NAT, in a container, or on a laptop;
- registers one or more plugins (named groups of functions);
- receives requests routed to
plugin:action, runs the function, and the return value is sent back to the caller.
Callers reach your worker either over a WebSocket connection to the mesh, or through the REST bridge:
POST https://ws.gluer.io/api/:project/:plugin/:actionwhere :project is the public frontend key of the project. The request passes through the Elixir mesh, which routes it to whichever worker is connected for that project and has the requested plugin.
browser / mobile / webhook / LLM
│
▼
wss://ws.gluer.io (Gluer mesh, hosted)
│ routes plugin:action
▼
your Node.js process ── gluer-nodejs ── your functionsRequirements
- Node.js 18.17+ (the package is ESM-only:
"type": "module").register_directory()relies onfs.Dirent#path, which is available from Node 18.17 / 20.1 onwards. - A Gluer project with its two keys (see Keys and security).
Install
npm install gluer-nodejs60-second quickstart
// worker.js
import { connect, register_plugin, register_object } from 'gluer-nodejs';
// 1. a single function
register_plugin('math', 'sum', async ({ a, b }) => ({ result: a + b }));
// 2. or every method of an object at once
register_object('greeter', {
hello: async ({ name }) => ({ message: `Hello, ${name}!` }),
bye: async ({ name }) => ({ message: `Bye, ${name}!` }),
});
// 3. connect outbound with the PRIVATE backend key
await connect(process.env.GLUER_PROJECT_KEY, ['math', 'greeter']);Run it:
GLUER_PROJECT_KEY=<private-backend-key> node worker.jsCall it from anywhere with the public frontend key:
curl -X POST https://ws.gluer.io/api/<public-frontend-key>/math/sum \
-H 'Content-Type: application/json' \
-d '{"a": 2, "b": 3}'
# => {"result": 5}API reference
All functions are named exports of the package root (import { ... } from 'gluer-nodejs'). Module-level state (project key, registered plugins, the active connection) is shared per process — one worker process serves one project.
set_websocket_url(url)
Overrides the mesh endpoint used by the next connect() call.
| Parameter | Type | Description |
| --- | --- | --- |
| url | string | Base worker endpoint, without project or session segments. Default: wss://ws.gluer.io/server. |
Returns undefined. Never throws. Must be called before connect(); it has no effect on an already established connection.
import { set_websocket_url } from 'gluer-nodejs';
set_websocket_url('ws://localhost:9001/server'); // local mesh during developmentconnect(project_key, plugins_list)
Opens the outbound WebSocket connection to the mesh and starts serving registered actions. The connection URL is built as:
<websocket_url>/<project_key>/ws-<uuid>?plugins=<comma separated list>| Parameter | Type | Description |
| --- | --- | --- |
| project_key | string | The private backend key of the project. If falsy, the built-in default ("sm") is kept, which is almost certainly not what you want. |
| plugins_list | string[] (optional) | Plugin names advertised to the mesh. Defaults to list_objects(), i.e. everything registered so far. |
Returns a Promise<void>. The promise resolves as soon as the connection attempt has been started — it is not a signal that the socket is open, and connection failures are handled by the reconnect loop rather than by rejecting. Because of that, register your plugins before calling connect(), so the plugins query parameter is complete.
The ?plugins= list is what the mesh uses to decide which worker a request should be routed to, so a plugin missing from that list will not receive traffic even if it is registered locally.
As soon as the socket opens, the worker also declares its actions to the mesh (a gluer:manifest message built from list_manifest()), and it declares them again on every reconnect. A mesh that understands the manifest routes by plugin:action and rejects a request no connected worker declared, instead of drawing a worker that does not implement it.
register_plugin(plugin, fn_name, fn)
Registers a single action.
| Parameter | Type | Description |
| --- | --- | --- |
| plugin | string | Plugin (namespace) name, the :plugin segment of a request. |
| fn_name | string | Action name, the :action segment of a request. |
| fn | function | Handler. Receives the decoded request payload and its return value (awaited) becomes the response payload. |
Returns a Promise<void>. Registering an existing plugin/fn_name pair overwrites the previous handler.
register_plugin('users', 'find', async ({ id }) => db.users.findById(id));register_object(plugin, obj)
Registers every own function property of obj as an action of plugin, using the property names as action names.
| Parameter | Type | Description |
| --- | --- | --- |
| plugin | string | Plugin name. |
| obj | object | Object whose function properties become actions. |
Returns a Promise<void>. Only own properties are inspected (Object.getOwnPropertyNames), so methods inherited from a class prototype are not picked up — pass an object literal of functions, or register class methods individually with register_plugin.
register_directory(dir)
Convention-based registration: recursively scans dir and imports every file whose name contains _plugin.js. Each such module must export a plugin string naming the plugin; all of its other exports are registered as actions of that plugin.
| Parameter | Type | Description |
| --- | --- | --- |
| dir | string | Absolute path of the directory to scan. |
Returns a Promise<void>. Note that the scan is fire-and-forget internally (see Known limitations): awaiting it does not guarantee all modules have finished importing, so allow a tick before calling connect(), or list your plugins explicitly in connect().
// plugins/users_plugin.js
export const plugin = 'users';
export async function find({ id }) { /* ... */ }import path from 'node:path';
await register_directory(path.join(import.meta.dirname, 'plugins'));list_objects()
Returns string[] — the names of all plugins registered so far. Used as the default value of connect()'s plugins_list.
list_manifest()
Returns [{ name, actions: [{ name }] }] — every registered plugin with the actions it serves. This is the payload declared to the mesh on connect; it is exported for inspection and testing.
sendToChannel(channel, command, data)
Pushes a message to every subscriber of a channel, outside of any request/response cycle (server-initiated push).
| Parameter | Type | Description |
| --- | --- | --- |
| channel | string | Channel name to broadcast on. |
| command | string | Command/action name delivered to subscribers. |
| data | object (optional) | Payload, MessagePack-encoded. Defaults to {}. |
Returns undefined. If the socket is not open it logs gluer-nodejs sendToChannel: WebSocket not connected to stderr and drops the message — it does not throw and does not queue.
sendToChannel('prices', 'tick', { symbol: 'PETR4', price: 38.12 });sendMsg(sessionId, command, data)
Same as sendToChannel, but addressed to one specific client session instead of a channel.
| Parameter | Type | Description |
| --- | --- | --- |
| sessionId | string | Target session id (as seen in the header of a request from that client). |
| command | string | Command/action name. |
| data | object (optional) | Payload. Defaults to {}. |
Returns undefined; same not-connected behaviour as sendToChannel.
Wire protocol
You do not need this to use the SDK, but it helps when debugging. Every frame is a MessagePack-encoded map containing a header string smh plus the payload:
smh = direction:session:project:plugin:action| Direction | Meaning |
| --- | --- |
| > | request |
| < | response |
| + | subscribe |
| - | unsubscribe |
Incoming request headers are parsed into { d, session_id, project, plugin, action }; the session segment may be a >-separated hop list. When replying, the SDK reuses the incoming header with the direction replaced by <.
Configuration
The SDK reads no environment variables of its own — everything is passed as arguments. The conventional setup is:
| Variable | Used for | Example |
| --- | --- | --- |
| GLUER_PROJECT_KEY | private backend key passed to connect() | connect(process.env.GLUER_PROJECT_KEY, [...]) |
| GLUER_WS_URL | override the mesh endpoint in development | set_websocket_url(process.env.GLUER_WS_URL) |
Error handling
- Handler throws — the error is caught and the caller receives
{ "error": "<error.message>" }. The worker stays connected. Stack traces are not sent over the wire, so log them yourself inside the handler if you need them. - Unknown
plugin:action— the caller receives{ "error": "no server actions found for <plugin>:<action>" }. If the request carriesoutput: "html", the same message is returned as an HTML fragment (<div id="alerts">...</div>) instead. - Malformed frame — binary frames are decoded as MessagePack, with a fallback attempt at JSON; text frames are decoded as JSON. A frame that is neither will throw inside the message handler.
- Not connected —
sendMsg/sendToChannellog tostderrand drop the message.
Reconnect semantics
WebSocketManager keeps the worker online without any application code:
- Heartbeat — a WebSocket ping every 3s; if the matching pong does not arrive within 5s the beat counts as missed. After 3 consecutive missed beats the socket is torn down and reconnected after 500ms.
- Connect timeout — a WebSocket handshake that does not open within 15s is terminated and scheduled for reconnection.
- Backoff — after a close event, reconnection is scheduled with an initial delay of 1s, multiplied by 1.5 per attempt and capped at 30s. The delay resets to 1s on a successful open.
- Cleanup — timers are cleared and listeners removed on every teardown, so reconnecting does not leak intervals or duplicate handlers.
- Session identity — each
connect()generates a new session id (ws-<uuid>), but reconnects keep the id of the current process, and the?plugins=list is re-sent on every attempt.
The manager created by module-level connect() reconnects for the process lifetime; connect() does not expose that instance to stop (see Known limitations).
Keys and security
Each Gluer project has two keys:
| Key | Where it belongs | Where it must never be |
| --- | --- | --- |
| Public frontend key | browser bundles, mobile apps, REST bridge URLs (POST https://ws.gluer.io/api/:project/...) | — |
| Private backend key | worker processes only, passed to connect() from an environment variable or secret manager | client code, mobile binaries, git history, CI logs |
Additional notes:
- Never hard-code the private key: read it from
process.env(or your secret store) and keep the value out of the repository. - Any registered action is reachable by anyone who can send a request to your project, so validate and authorize inside handlers — registration is not an access-control mechanism.
- Handler return values are sent verbatim to the caller; do not return internal objects containing secrets or credentials.
Related repositories
- gluer.io — product and documentation
- gluer-js — browser/JS client
- gluer-web — web console
- gluer-dart — Dart/Flutter client
- gluer-python — Python worker SDK
- gluer-elx — Elixir mesh
- gluer-ws — WebSocket gateway
Known limitations
Documented so they are not mistaken for undocumented features:
register_directory()usesArray#forEachwith async callbacks, so imports are not actually awaited and recursion into subdirectories is fire-and-forget.connect()does not return its internal manager, so that connection cannot be stopped programmatically; independently constructedWebSocketManagerinstances can be stopped withstopReconnecting().- The default project key is the placeholder
"sm";connect()with a falsy key silently keeps it instead of failing. lib/redis.jsis legacy, not used by the SDK, and is not part of the supported API.
Contributing
See CONTRIBUTING.md. Changes are tracked in CHANGELOG.md.
