Client SDK
Subscribe to a Durable Object from the browser — pingerchips.object().
Subscribe to a Durable Object from the browser with pingerchips.object()
(pingerchips-js >= 2.2.0). The view is read-only — writes go through your
backend via pingerchips-js-server.
Setup
import Pingerchips from 'pingerchips-js';
const pc = new Pingerchips('YOUR_APP_KEY', {
authEndpoint: '/auth/durable', // your route — see Auth below
});Subscribe
const order = await pc.object('order', 'order-42');Resolves once the initial snapshot has arrived, so order.state and
order.get() are populated immediately. Internally this joins the
durable:{appKey}:order:order-42 channel with a per-object token from your
auth endpoint.
Resume from a checkpoint
const order = await pc.object('order', 'order-42', {
afterLogId: Number(localStorage.getItem('order-42-log')) || undefined,
});The server replays every log entry after afterLogId as a change event
before the snapshot.
Options
| Option | Type | Description |
|---|---|---|
afterLogId | number | Replay entries after this log_id. |
auth | string | A pre-minted capability token — skips the auth endpoint. |
authEndpoint | string | Override the client's authEndpoint for this call. |
Read state
order.state
The current state map. Updated on every snapshot / change / batch.
console.log(order.state);
// { status: "processing", assigned_to: "agent-7", retry_count: 2 }order.get(slot)
Synchronous read of one slot from the cached state — no network call.
order.get('status'); // "processing"order.logId
The last seen log_id. Checkpoint this for resumable reconnects.
Listen for changes
order.on(event, handler) — returns an unsubscribe function
| Event | Payload | Fires on |
|---|---|---|
snapshot | { state, log_id } | Join and reconnect |
change | { key, value, previous?, log_id } | set / increment / append / delete, and once per slot in a batch |
batch | { changes: [{ key, value, previous }], log_id } | setAll / transaction |
change:{slot} | same as change | Only when that slot changes |
order.on('change', ({ key, value, previous, logId }) => {
console.log(`${key}: ${previous} -> ${value} (log ${logId})`);
});
const off = order.on('change:status', ({ value }) => updateBadge(value));
off(); // stop listening
order.on('snapshot', ({ state }) => hydrate(state));A change with value: null means the slot was deleted — order.state drops
it.
Unsubscribe
order.unsubscribe();Leaves the channel and stops all events. On socket reconnect, active
subscriptions re-authenticate and rejoin automatically with their last
logId.
Auth
The browser must never hold the App Secret. Your server mints a token bound to one socket, one object type, and one key.
Client
const pc = new Pingerchips('YOUR_APP_KEY', {
authEndpoint: '/auth/durable',
authInfo: { token: userSessionToken }, // forwarded to your endpoint
});The SDK POSTs { socket_id, object_type, object_key, auth_info } to
authEndpoint and expects { auth: "{appKey}:{signature}" } back.
Server (pingerchips-js-server >= 2.2.0)
import PingerchipsServer from 'pingerchips-js-server';
const pc = new PingerchipsServer(
process.env.PINGERCHIPS_APP_KEY,
process.env.PINGERCHIPS_APP_SECRET,
);
app.post('/auth/durable', (req, res) => {
const { socket_id, object_type, object_key, auth_info } = req.body;
const user = verifyUser(auth_info?.token);
if (!user || !canRead(user, object_type, object_key)) {
return res.status(403).json({ error: 'Forbidden' });
}
res.json(pc.authenticateObject(socket_id, object_type, object_key));
});authenticateObject returns { auth: "{appKey}.{payloadB64}.{sigB64}" } — a
fast-path capability token granting object:read:{type}/{key}, bound to
socket_id. A token for order/order-42 cannot be used for order/order-99.
React example
import { useEffect, useState } from 'react';
function OrderStatus({ pc, orderId }) {
const [status, setStatus] = useState(null);
useEffect(() => {
let order;
(async () => {
order = await pc.object('order', orderId);
setStatus(order.get('status'));
order.on('change:status', ({ value }) => setStatus(value));
})();
return () => order?.unsubscribe();
}, [pc, orderId]);
return <span>{status ?? 'loading…'}</span>;
}Wire protocol
Building an SDK in another language? The full frame-level contract — join payloads, every event shape, the REST API, and errors — is on the Durable Objects wire protocol page.
In brief:
- Topic:
durable:{appKey}:{type}:{key} - Join payload:
{ auth: "<capability token>", after_log_id?: N }(server-side callers may send{ app_secret }instead) - Join reply:
{ socket_id } - Events:
snapshot{state, log_id},change{key, value, previous?, log_id},batch{changes: [...], log_id} - Client pushes are rejected — the channel is read-only.