gluer-js
v1.3.17
Published
Browser/JavaScript SDK for Gluer, a managed real-time integration layer: WebSocket messaging with RPC, pub/sub, automatic reconnect, offline queue and HTTP fallback.
Maintainers
Readme
gluer-js
Browser/JavaScript SDK for Gluer — a managed real-time integration layer that connects frontends, backends, webhooks and local LLMs through a single message bus.
What problem it solves
Wiring a frontend to a backend usually means inventing plumbing: an HTTP API, a WebSocket server, reconnect logic, a queue for messages sent while offline, and a second code path for when the socket is down.
Gluer replaces that plumbing with a hosted mesh. Every participant — browsers, mobile apps, backend workers in any language, third-party webhooks, locally running LLMs — connects to the same bus and addresses the others by project / plugin / action. gluer-js is the client for the browser side of that bus:
browser (gluer-js) ─┐
mobile app ─┤
webhook / REST call ─┼──► Gluer mesh (wss://ws.gluer.io) ──► backend worker
local LLM ─┘ (gluer-go, gluer-python,
gluer-nodejs, gluer-elx…)This SDK gives you: a persistent WebSocket connection with automatic reconnect and heartbeat, request/response (RPC) calls over that socket, pub/sub channel subscriptions that survive reconnects, an offline message queue, and an automatic HTTP fallback when the socket is unavailable.
Install
npm install gluer-jsThe package ships ESM (dist/gluer-js.module.js), CommonJS (dist/gluer-js.cjs) and UMD (dist/gluer-js.umd.js) builds, so a bundler-based app can simply import it.
The UMD build does not inline dependencies: loading it directly from a CDN also requires
uuidand@msgpack/msgpackto be available as the globalsuuidandmsgpack, and the singleton is then reached asgluerJs.default(the named export isgluerJs.GluerJS). Using a bundler is the supported path.
Requirements: an environment with WebSocket, fetch and EventTarget (modern browsers; see SSR for Node/SSR notes).
Quickstart (60 seconds)
import gluer from "gluer-js";
// 1. Point the client at your project. Use the PUBLIC frontend key here.
gluer.setup({ project: "your-frontend-key" });
// 2. Open the connection.
gluer.connect();
gluer.addEventListener("connection", (e) => {
console.log("connected, session:", e.detail.sessionId);
});
// 3. Call an action on your backend worker and await the response.
const res = await gluer.sendMsgSync("users:list", { page: 1 });
console.log(res.data);
// 4. Listen to a broadcast channel.
gluer.subscribe("orders", (msg) => console.log("order event", msg.data));sendMsgSync resolves with the full response message ({ smh, uuid, data, ... } as sent by the worker), so application data lives under res.data.
Architecture and protocol
Every frame is a MessagePack-encoded object with a routing header in the smh field:
smh = "<direction>:<sessionId>:<project>:<action>"| Direction | Meaning |
| --- | --- |
| > | request sent to the mesh |
| < | response coming back |
| + | subscribe (channels in data.channels) |
| - | unsubscribe |
gluer-js builds these headers for you: sendMsg/sendMsgSync/sendHttp emit >, and subscribe emits +. Requests carry a uuid used to correlate the response. The action segment is whatever string you pass as the first argument, so a plugin-qualified action is written as "plugin:action" (for example sendMsgSync("mailer:send", …)).
Incoming frames are dispatched as follows:
- frames with a
channelfield → the callback registered for that channel viasubscribe, plus a DOM-style event named after the channel; - frames with a
uuidmatching a pending request → that request's promise or callback. A frame withd === "*"is treated as a streaming response: the handler is invoked and kept registered for further frames instead of being removed.
API reference
Exports
import gluer, { GluerJS } from "gluer-js";| Export | Description |
| --- | --- |
| default | A ready-to-use singleton GluerJS instance, created with default configuration at import time. Not connected until you call connect(). |
| GluerJS | The class, in case you need several independent connections. Extends EventTarget. |
The singleton is the normal entry point; use new GluerJS(config) when a page must talk to more than one project or mesh.
new GluerJS(config = {})
Creates a client and applies config through setup(). Does not open a connection.
setup(config = {})
Applies configuration. Every field is optional and only overwrites the current value when present, so setup() can be called repeatedly (for example to set the session id after login). Changing url, project or sessionId takes effect on the next connect().
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| project | string | "sm" | Project key used in the connection URL and in every smh header. Use the public frontend key. |
| url | string | "wss://ws.gluer.io" | Base WebSocket URL. The client connects to `${url}/ws/${project}/${sessionId}`. |
| sessionId | string | "ws-" + uuidv4() | Identifies this client on the mesh. Generated once per instance if not supplied. |
| httpFallback | boolean | true | When true, sendMsg/sendMsgSync fall back to HTTP if the socket is not open. |
| httpUrl | string | "https://api.gluer.io" | Base URL for the HTTP fallback; requests are POSTed to `${httpUrl}/api`. |
Returns: undefined.
connect()
Opens the WebSocket connection, binds the socket handlers and starts the heartbeat.
- If
WebSocketis undefined (SSR), logs a warning and returns without connecting. - If a socket is already
OPENorCONNECTING, it is reused; for an already-open socket aconnectionevent withdetail.reconnected === trueis dispatched and the offline queue is flushed. - If constructing the socket throws, the error is logged and a reconnect is scheduled.
Returns: undefined (it does not return a promise — listen for the connection event or just start sending, since messages are queued until the socket opens).
disconnect()
Closes the socket if there is one. See Reconnect semantics for an important caveat: the automatic reconnect timer is still armed by the socket's close handler, so a manually disconnected client will attempt to reconnect after 5 s.
sendMsgSync(action, data = {}, timeoutMs = 10000)
Sends a request and awaits the correlated response.
| Parameter | Type | Description |
| --- | --- | --- |
| action | string | Action to invoke, e.g. "users:list". Becomes the last smh segment. |
| data | object | Payload. Merged with the context data set via setContextItem. |
| timeoutMs | number | Time budget for the whole call. Default 10000. |
Returns: Promise<object> resolving with the response message.
Rejects with:
- the value of
message.data.error(as sent by the worker) when the response carries an error field — note this is the raw value, not necessarily anError; Error("Gluer Timeout: Sem resposta para '<action>' após <timeoutMs>ms")when no response arrives in time;Error("Connection Lost")if the socket closes while the call is pending;Error("Gluer: Não foi possível enviar (Sem Conexão).")if the client is disconnected andhttpFallbackisfalse;- whatever
fetch/msgpackthrows when the HTTP fallback is used and fails (e.g.Error("HTTP Error: 502")).
If the client is not connected, it first waits for a connection (up to 3 s when httpFallback is enabled, otherwise timeoutMs - 1000) and only then falls back to HTTP.
try {
const res = await gluer.sendMsgSync("users:get", { id: 7 }, 5000);
render(res.data);
} catch (err) {
console.error("call failed", err);
}sendMsg(action, data = {}, callback = null)
Fire-and-forget send, with an optional callback for the response.
| Parameter | Type | Description |
| --- | --- | --- |
| action | string | Action to invoke. |
| data | object | Payload, merged with context data. |
| callback | function \| null | Invoked with the response message. Kept registered while responses carry d === "*" (streaming), otherwise removed after the first response. |
Returns: Promise<void> — it is async because it may wait up to 2 s for a connection, but it never resolves with the response; use the callback or sendMsgSync for that.
Behaviour when offline: waits up to 2 s for the socket; if that fails and httpFallback is on, sends over HTTP and passes the decoded response to callback; if the HTTP attempt also fails, the message is queued and delivered when the socket comes back. With httpFallback disabled the message is simply queued.
gluer.sendMsg("metrics:track", { event: "page_view" });
gluer.sendMsg("users:list", {}, (msg) => console.log(msg.data));subscribe(channels, callback)
Subscribes to one or more broadcast channels.
| Parameter | Type | Description |
| --- | --- | --- |
| channels | string \| string[] | Channel name(s). |
| callback | function | Invoked with each message received on those channels. One callback per channel is stored; subscribing again to the same channel replaces it. |
Returns: undefined. The subscribe frame goes through the normal send path, so it is queued while offline, and all registered channels are re-subscribed automatically on every (re)connect.
Channel messages are additionally dispatched as events named after the channel, so this also works:
gluer.addEventListener("orders", (e) => console.log(e.detail));send(payload)
Low-level send. Merges contextData into payload.data, MessagePack-encodes the payload and writes it to the socket; if the socket is not open (or send throws) the payload is pushed to the offline queue and flushed on the next successful connection. Build the smh header yourself when using this.
Returns: undefined.
sendHttp(action, data = {})
Sends a request over HTTP explicitly, bypassing the WebSocket. Same argument shape as sendMsg.
Returns: Promise<object> with the decoded MessagePack response. Rejects with Error("HTTP Error: <status>") for non-2xx responses, or the underlying fetch error. Dispatches the httpFallback event before performing the request.
setContextItem(key, value) / deleteContextItem(key)
Set or remove a value that is merged into data of every outgoing message (WebSocket and HTTP). Useful for auth tokens and tenant ids.
gluer.setContextItem("token", jwt);
gluer.deleteContextItem("token"); // on logoutBoth return undefined. Context keys win over per-call data keys of the same name.
Events
GluerJS extends EventTarget, so use addEventListener / removeEventListener.
| Event | detail | When |
| --- | --- | --- |
| connection | { sessionId, reconnected } | Socket opened (reconnected: false) or connect() was called on an already-open socket (reconnected: true). |
| disconnect | — | Socket closed, for any reason. |
| error | the raw socket error event | Socket-level error. |
| httpFallback | { command, uuid, url } | Just before a request is sent over HTTP instead of the socket. |
| channel name | the channel message | A message arrived on a subscribed channel. |
Instance state
Read-only in practice, but useful for diagnostics: isConnected, socket, msgQueue, subscriptions (Map channel → callback), callbacks (Map uuid → pending handler), contextData.
Configuration and environment variables
The library reads no environment variables and has no build-time configuration — everything goes through setup(). In a bundler-based app, keep the project key in your framework's public env mechanism, e.g.:
gluer.setup({
project: import.meta.env.VITE_GLUER_PROJECT, // public frontend key
url: import.meta.env.VITE_GLUER_WS_URL, // optional, for staging meshes
});Error handling and reconnect semantics
Reconnect
- The socket's
closehandler marks the client disconnected, stops the heartbeat, rejects all pendingsendMsgSyncpromises withError("Connection Lost"), dispatchesdisconnect, and schedules a reconnect 5 s later (fixed delay, no exponential backoff). The reconnect only runs whenwindowis defined. - On reconnect, all channels in
subscriptionsare re-subscribed and the offline queue is flushed. - A heartbeat
pingrequest is sent every 30 s while connected. disconnect()clears the pending reconnect timer before closing, but theclosehandler then schedules a new one — so a manual disconnect is followed by a reconnect attempt 5 s later. If you need a permanent disconnect today, remove your listeners and stop using the instance, or subclass and override_scheduleReconnect.
Offline behaviour
Messages sent while the socket is down are appended to msgQueue and flushed in order once the connection opens, so sendMsg never throws for being offline. sendMsgSync needs a live round trip, so it either falls back to HTTP or rejects.
HTTP fallback
With httpFallback: true (default), a sendMsg/sendMsgSync issued while the socket is down is POSTed as MessagePack to `${httpUrl}/api` with Content-Type: application/msgpack, and the response is MessagePack-decoded. An httpFallback event is dispatched for observability. Failures are logged and rethrown (sendMsgSync) or queued (sendMsg).
Server-side rendering
connect() is a no-op when WebSocket is undefined and reconnects are skipped when window is undefined, so the client itself is SSR-aware. Call connect() from a client-only lifecycle hook.
Caveat: the published bundles are transpiled to ES5, and the EventTarget subclass in that output throws TypeError: Class constructor EventTarget cannot be invoked without 'new' when imported in Node (Node's EventTarget is not a native function, so the transpiler's native-super shim does not kick in). Browsers are unaffected. If your framework evaluates the import on the server, load the module dynamically on the client (await import("gluer-js") inside a client-only hook / onMount / useEffect).
Security
Gluer projects have two keys, and only one of them belongs in this SDK:
| Key | Where it belongs |
| --- | --- |
| Public frontend key | Safe in browser bundles. This is the key you pass to setup({ project }), and the one used in REST bridge URLs (POST https://ws.gluer.io/api/:project/:plugin/:action) — the request passes through the mesh, which routes it to the connected backend worker. |
| Private backend key | Only for backend workers calling connect() from a server SDK (gluer-go, gluer-python, gluer-nodejs…). It must never appear in client code, a bundle, or a git repository. |
Additional notes:
- Treat anything a browser sends as untrusted; enforce authorization in the backend worker, not in the frontend.
- Values passed to
setContextItem(tokens, ids) are sent with every message — scope them accordingly and delete them on logout. - The transport is MessagePack over TLS (
wss:///https://); never pointurlat a plaintextws://endpoint in production.
Related projects
- gluer.io — product and documentation
- gluer-elx — the Elixir mesh
- gluer-go, gluer-python, gluer-nodejs, gluer-dart — backend/client SDKs
- gluer-web — web console
Development
npm install
npm run build # microbundle → dist/ (cjs, module, umd)
npm run bundle # single UMD bundle
npm run dev # watch modeReleases are cut by ./publish.sh "<commit message>", which commits, runs npm version patch and pushes the tag; GitLab CI publishes tagged builds to npm.
Contributing
See CONTRIBUTING.md. Changes are tracked in CHANGELOG.md.
License
ISC © Thiago Magro
