npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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.

Readme

gluer-nodejs

npm version license

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/:action

where :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 functions

Requirements

  • Node.js 18.17+ (the package is ESM-only: "type": "module"). register_directory() relies on fs.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-nodejs

60-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.js

Call 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 development

connect(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 carries output: "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 / sendToChannel log to stderr and 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

Known limitations

Documented so they are not mistaken for undocumented features:

  • register_directory() uses Array#forEach with 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 constructed WebSocketManager instances can be stopped with stopReconnecting().
  • The default project key is the placeholder "sm"; connect() with a falsy key silently keeps it instead of failing.
  • lib/redis.js is legacy, not used by the SDK, and is not part of the supported API.

Contributing

See CONTRIBUTING.md. Changes are tracked in CHANGELOG.md.

License

ISC