1Hosted script
Add the current hosted build before code that constructs the client.
<script src="https://update.pmblue.us/resources/pm_socket/js"></script>
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.
Load the script, configure callbacks, then connect. The matching Python PmSocket server adapter sends the required ::connect:: handshake automatically.
<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>
Pages loaded over HTTPS should connect with wss://. Browsers normally block insecure ws:// connections from a secure page.
Add the current hosted build before code that constructs the client.
<script src="https://update.pmblue.us/resources/pm_socket/js"></script>
Serve resources/pm_socket.js from your application, then reference its public path.
<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.
connect() creates the native WebSocket and attaches its event handlers.open, connect_handler() runs. At this point the transport is open, but alive may still be false.::connect::. The client consumes it and sets alive = true.handler(message) and any active waitFor() listener.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.
Assign one global handler before calling connect(). It receives event.data, not the browser MessageEvent.
socket.handler = (message) => {
const payload = JSON.parse(message);
console.log(payload.type, payload.data);
};
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.
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().
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.
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 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.
ping() sends ::ping::, waits for ::pong::, and resolves with round-trip latency in milliseconds.
if (socket.isConnected()) {
const milliseconds = await socket.ping();
console.log(`Latency: ${milliseconds} ms`);
}
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.
const socket = new WebSocketConnection("wss://example.com/events");
socket.auto_ping_interval = 30; // seconds; set before connect()
socket.connect();
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.
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.
new WebSocketConnection(url)Creates a disconnected client. url must be a WebSocket URL accepted by the browser, normally wss://….
connect()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()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()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)Sends a string, Blob, ArrayBuffer, or typed-array view when connected. Logs WebSocket is not connected. when closed.
send(data)Calls the native socket's send() directly, with no existence or state guard. Native errors can propagate.
ping()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)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.
| Property | Default | Purpose | Notes |
|---|---|---|---|
url | constructor value | WebSocket endpoint. | Use wss:// on secure sites. |
socket | null | Current native WebSocket. | Managed internally; cleared by disconnect(). |
alive | false | PmSocket handshake state. | Set true only by ::connect::. |
handler | null | Application message callback. | Signature: (message) => void. |
connect_handler | null | Native-open callback. | Runs on initial connection and reconnections. |
auto_ping_interval | null | Auto-ping cadence in seconds. | Set before connect(); later becomes a timer ID. |
maxAttempts | 5 | Nominal reconnect limit. | See reconnect caveat below. |
attempts | 0 | Reconnect attempt counter. | Internal; reset by every connect() call. |
waiting | {} | Legacy/internal wait state. | Current waitFor() uses event listeners instead. |
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 function | Arguments / return | State and callback contract |
|---|---|---|
url, socket, alive | URL 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_handler | Assign 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. |
handler | Assign 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.
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();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();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());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.
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.
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.
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.
const scheme = location.protocol === "https:" ? "wss" : "ws";
const socket = new WebSocketConnection(
`${scheme}://${location.host}/record/ws`
);
socket.connect();
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.
window.addEventListener("pagehide", () => {
socket.disconnect();
});
WebSocketConnection.::connect:: and responds to ::ping::.handler and connect_handler before connect().JSON.stringify().sendData() for guarded sends. It drops messages while disconnected; there is no send queue.disconnect() for deliberate shutdown.