Salus Docs
Browser module · API guide

pm_socket.js

WebSocketConnection is a small, dependency-free wrapper around the browser WebSocket API. It adds a server handshake, reconnect behavior, application-level ping/pong latency checks, message callbacks, and one-shot response matching.

Browser JavaScript No dependencies Callback-based Works with PmSocket
On this page

Quickstart

Load the script, configure callbacks, then connect. The matching Python PmSocket server adapter sends the required ::connect:: handshake automatically.

HTML + JavaScript
<script src="https://update.pmblue.us/resources/pm_socket/js"></script>
<script>
  const socket = new WebSocketConnection("wss://example.com/events");

  socket.connect_handler = () => {
    console.log("WebSocket transport is open");
  };

  socket.handler = (message) => {
    console.log("Received:", message);
  };

  socket.connect();
</script>
Use a secure URL in production.

Pages loaded over HTTPS should connect with wss://. Browsers normally block insecure ws:// connections from a secure page.

Installation

1Hosted script

Add the current hosted build before code that constructs the client.

HTML
<script src="https://update.pmblue.us/resources/pm_socket/js"></script>

2Local script

Serve resources/pm_socket.js from your application, then reference its public path.

HTML
<script src="/static/resources/pm_socket.js"></script>

The script defines WebSocketConnection in the global scope. It is not currently packaged as an ES module or npm package.

Connection lifecycle

  1. connect() creates the native WebSocket and attaches its event handlers.
  2. When the browser fires open, connect_handler() runs. At this point the transport is open, but alive may still be false.
  3. The compatible server sends ::connect::. The client consumes it and sets alive = true.
  4. Application messages are passed to handler(message) and any active waitFor() listener.
  5. On an unexpected close after the handshake, the client tries to reconnect.
  6. disconnect() marks the connection inactive, closes it, and prevents reconnect handling for that close.
alive and isConnected() mean different things.

alive records the PmSocket application handshake. isConnected() checks the native socket's OPEN state. Use isConnected() before sending.

Sending and receiving messages

Receive application messages

Assign one global handler before calling connect(). It receives event.data, not the browser MessageEvent.

JavaScript
socket.handler = (message) => {
  const payload = JSON.parse(message);
  console.log(payload.type, payload.data);
};

Send safely

sendData(data) sends only while the socket is open; otherwise it logs an error. Prefer it for normal application traffic. Like the native WebSocket API, objects must be serialized first.

JavaScript
socket.sendData(JSON.stringify({
  type: "subscribe",
  channel: "orders"
}));

send(data) forwards directly to socket.send(data) without checking state. Use it only when you have already confirmed isConnected().

Match a one-time response

waitFor(condition, callback, timeout) adds a temporary message listener. The first message for which condition(message) returns true is passed to the callback, then the listener removes itself.

JavaScript
const requestId = crypto.randomUUID();

socket.waitFor(
  (raw) => {
    try {
      return JSON.parse(raw).requestId === requestId;
    } catch {
      return false;
    }
  },
  (raw) => console.log("Matched response:", JSON.parse(raw)),
  5000
);

socket.sendData(JSON.stringify({ type: "lookup", requestId }));
The timeout is silent.

The listener is attached to the current native WebSocket object and does not follow reconnections. Install it before sending the corresponding request, and register it again for a new connection when needed. Because it uses a native event listener, its condition can see reserved control strings even when the main handler does not. When time expires, the listener is removed, but the callback is not called and no error is raised. waitFor() also returns undefined; it is not a Promise.

Latency and keepalive

Measure round-trip latency

ping() sends ::ping::, waits for ::pong::, and resolves with round-trip latency in milliseconds.

JavaScript
if (socket.isConnected()) {
  const milliseconds = await socket.ping();
  console.log(`Latency: ${milliseconds} ms`);
}

Enable automatic pinging

Set auto_ping_interval to a number of seconds before connecting. Each interval runs ping(); if the matching ping has not completed after two seconds, the socket is closed.

JavaScript
const socket = new WebSocketConnection("wss://example.com/events");
socket.auto_ping_interval = 30; // seconds; set before connect()
socket.connect();
Current implementation detail.

After connection, auto_ping_interval is overwritten with the timer ID. A reconnect then uses that timer ID as the interval value, and previous timers are not reliably cleared during reconnect. Avoid enabling automatic pinging across reconnects until the application handles timer cleanup and interval restoration; ping() itself has no timeout.

Reserved protocol messages

This page follows resources/pm_socket.js, the file served by /resources/pm_socket/js. The alternate pm_socket_ and pm_socket_v1 filenames are separate resources; they are not selected by this URL. The served file has no version declaration.

These exact strings are used internally. The client consumes ::connect::, ::disconnect::, and ::ping:: before its application handler; ::pong:: also completes active ping listeners and can still reach handler or waitFor(). Do not use any of them as application payloads.

::connect::

Server → client. Marks the PmSocket session alive.

::disconnect::

Either direction. Requests a deliberate close without reconnecting.

::ping::

Either direction. Requests an application-level pong.

::pong::

Either direction. Completes a latency measurement; may also reach application listeners.

These are text-frame application messages, separate from native WebSocket ping and pong control frames.

API reference

new WebSocketConnection(url)

→ WebSocketConnection

Creates a disconnected client. url must be a WebSocket URL accepted by the browser, normally wss://….

connect()

→ undefined

Creates a native socket, installs open/close/error/message handlers, and starts the connection. Configure callbacks and options first. Calling it more than once can create multiple live sockets; do not call it again while connected.

disconnect()

→ undefined

If a socket exists, sends ::disconnect:: when alive is true, closes the transport, clears socket, and sets alive to false. Safe to call when no socket exists.

isConnected()

→ boolean | null

Returns a truthy value only when a socket exists and its native state is WebSocket.OPEN. Because the implementation uses this.socket && …, it returns null before a socket exists rather than the literal boolean false.

sendData(data)

→ undefined

Sends a string, Blob, ArrayBuffer, or typed-array view when connected. Logs WebSocket is not connected. when closed.

send(data)

→ undefined

Calls the native socket's send() directly, with no existence or state guard. Native errors can propagate.

ping()

→ Promise<number | undefined>

When connected, resolves with latency in milliseconds after receiving ::pong::. When disconnected, logs an error and resolves to undefined. The current method has no timeout, so a missing pong leaves its Promise pending.

waitFor(condition, callback, timeout = 0)

→ undefined

Adds a one-time matching listener. condition receives message data and must return a truthy value to call callback(message). A positive timeout removes the listener silently. A timeout of 0 leaves it active until a match.

Properties and configuration

PropertyDefaultPurposeNotes
urlconstructor valueWebSocket endpoint.Use wss:// on secure sites.
socketnullCurrent native WebSocket.Managed internally; cleared by disconnect().
alivefalsePmSocket handshake state.Set true only by ::connect::.
handlernullApplication message callback.Signature: (message) => void.
connect_handlernullNative-open callback.Runs on initial connection and reconnections.
auto_ping_intervalnullAuto-ping cadence in seconds.Set before connect(); later becomes a timer ID.
maxAttempts5Nominal reconnect limit.See reconnect caveat below.
attempts0Reconnect attempt counter.Internal; reset by every connect() call.
waiting{}Legacy/internal wait state.Current waitFor() uses event listeners instead.

Wrapper and native WebSocket objects

new WebSocketConnection(url) creates application state only. After connect(), its socket property holds the browser's native WebSocket. A reconnect replaces that native object; listeners and properties attached directly to the previous socket are not transferred automatically.

Member or functionArguments / returnState and callback contract
url, socket, aliveURL string, native WebSocket or null, protocol-state boolean.alive becomes true on ::connect::. It is distinct from native readyState; after an unexpected close it can remain true while reconnecting.
connect()Synchronous; returns undefined.Creates a socket, installs native handlers, resets attempts, and starts connecting. It is not a Promise and must not be awaited as a readiness signal. Calling it twice can create overlapping sockets.
connect_handlerAssign a function with no arguments; return value is ignored.Called on native open, before the PmSocket handshake may have arrived. Access the wrapper through the closure. An async callback is not awaited by the library; handle rejected promises inside it.
handlerAssign a function receiving raw event.data; return value is ignored.Text frames arrive as strings. Binary frames use the native socket's binaryType, normally Blob. The callback receives neither a parsed JSON object nor a native MessageEvent.
isConnected()Returns true/false when a socket exists, otherwise null.Checks native readyState === WebSocket.OPEN; it does not require alive.
send(data) / sendData(data)Synchronous; return undefined.Forward data to native send. Serialize objects explicitly. send assumes a socket; sendData logs and drops the message if not open. Neither waits for delivery or an acknowledgement.
waitFor(condition, callback, timeout=0)Both callbacks receive raw data. Returns undefined, not a Promise or cancellation handle.Attaches a one-shot listener to the current native socket. Condition must synchronously return a boolean; an async condition returns a truthy Promise and matches incorrectly. Positive timeout is milliseconds and silently removes the listener.
ping()Async; resolves with elapsed milliseconds, or undefined if not connected.Creates a native message listener for ::pong::. There is no per-call timeout or cancellation; all outstanding pings use the same pong token.
disconnect()Synchronous; returns undefined.Requests a deliberate close, clears the current socket reference, and resets alive. It does not expose a Promise for final transport closure. Reconnect timers already scheduled elsewhere are not explicitly cancelled.

Keep business state in your application rather than the legacy waiting dictionary. Use connect_handler to reapply native socket settings after each reconnect. The Python partner receives native messages through PmSocket objects.

Browser workflows

1. Wrap a matched response in a Promise with a deadline

The Python state-server example echoes request_id. This helper supplies the Promise and timeout rejection that waitFor() itself does not provide. A connection loss will eventually reject at the deadline.

const scheme = location.protocol === 'https:' ? 'wss' : 'ws';
const stateSocket = new WebSocketConnection(`${scheme}://${location.host}/state/ws`);
let requestSequence = 0;

function requestState() {
  if (!stateSocket.isConnected()) {
    return Promise.reject(new Error('Socket is not open'));
  }
  const requestId = `state-${++requestSequence}`;
  return new Promise((resolve, reject) => {
    const deadline = setTimeout(() => reject(new Error('State request timed out')), 5000);
    stateSocket.waitFor(
      raw => {
        try { return JSON.parse(raw).request_id === requestId; }
        catch (_) { return false; }
      },
      raw => {
        clearTimeout(deadline);
        resolve(JSON.parse(raw).data);
      },
      5000,
    );
    stateSocket.sendData(JSON.stringify({operation: 'get_state', request_id: requestId}));
  });
}

stateSocket.connect_handler = async () => {
  try { console.log('Server state:', await requestState()); }
  catch (error) { console.error(error.message); }
};
stateSocket.connect();

2. Send binary data and decode acknowledgement text

Register the binary-ingest handler from the Python guide at /upload/ws. The callback configures each newly opened native socket. This sends one binary frame; the server's acknowledgement is still a JSON text frame.

const binaryScheme = location.protocol === 'https:' ? 'wss' : 'ws';
const transfer = new WebSocketConnection(`${binaryScheme}://${location.host}/upload/ws`);
transfer.connect_handler = () => {
  transfer.socket.binaryType = 'arraybuffer';
  transfer.sendData(new Uint8Array([10, 20, 30]).buffer);
};
transfer.handler = raw => {
  if (typeof raw === 'string') {
    if (raw.startsWith('::')) return;
    try { console.log('Acknowledgement:', JSON.parse(raw)); }
    catch (_) { console.log('Text:', raw); }
  } else if (raw instanceof ArrayBuffer) {
    console.log('Binary response:', new Uint8Array(raw));
  }
};
transfer.connect();

3. Keep an application queue for temporary disconnects

sendData() does not queue messages. This small bounded queue adds that behavior and flushes on native open. It provides no exactly-once guarantee: acknowledgements and replay protection would belong to the application protocol.

const queueScheme = location.protocol === 'https:' ? 'wss' : 'ws';
const queuedSocket = new WebSocketConnection(`${queueScheme}://${location.host}/chat/ws`);
const pending = [];

function sendWhenOpen(text) {
  if (queuedSocket.isConnected()) {
    queuedSocket.sendData(text);
    return true;
  }
  if (pending.length >= 100) return false;
  pending.push(text);
  return true;
}

queuedSocket.connect_handler = () => {
  queuedSocket.sendData('join');
  while (pending.length > 0 && queuedSocket.isConnected()) {
    queuedSocket.sendData(pending.shift());
  }
};
queuedSocket.handler = text => console.log(text);
queuedSocket.connect();
sendWhenOpen('hello');
window.addEventListener('pagehide', () => queuedSocket.disconnect());

Errors, reconnection, and caveats

Reconnect behavior

A close triggers reconnection only when alive is true. The source defines delays of 100 ms, 1 second, and 10 seconds plus maxAttempts, but each reconnect calls connect(), which resets attempts to zero. In the current build, repeated post-handshake failures therefore normally retry after 100 ms and do not progress toward the configured maximum.

Close before handshake

If the socket closes before the server sends ::connect::, alive is still false and no reconnect is scheduled. This includes endpoints that speak standard WebSocket but not the PmSocket application protocol.

Error reporting

Native socket errors are written to console.error. There is no configurable error callback, message queue, or thrown custom error. Add application-level status UI in connect_handler and your message protocol as needed.

Concurrent pings

Every active ping() listens for the same ::pong:: token, so one pong can resolve multiple concurrent calls. Await one ping at a time if individual measurements matter.

Practical recipes

Connect to the current host

JavaScript
const scheme = location.protocol === "https:" ? "wss" : "ws";
const socket = new WebSocketConnection(
  `${scheme}://${location.host}/record/ws`
);
socket.connect();

Send after the transport opens

JavaScript
socket.connect_handler = () => {
  socket.sendData(JSON.stringify({ type: "subscribe" }));
};
socket.connect();

connect_handler runs again after a reconnect, which makes it useful for restoring subscriptions. Make the server operation idempotent so a repeated subscription is safe.

Clean up during page teardown

JavaScript
window.addEventListener("pagehide", () => {
  socket.disconnect();
});

Integration checklist

  • Load the script before constructing WebSocketConnection.
  • Use a PmSocket-compatible server that sends ::connect:: and responds to ::ping::.
  • Assign handler and connect_handler before connect().
  • Serialize objects with JSON.stringify().
  • Use sendData() for guarded sends. It drops messages while disconnected; there is no send queue.
  • Call disconnect() for deliberate shutdown.
  • Avoid using the four reserved protocol strings as business messages.