Pingerchips LogoPingerchips
Durable ObjectsReference

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

OptionTypeDescription
afterLogIdnumberReplay entries after this log_id.
authstringA pre-minted capability token — skips the auth endpoint.
authEndpointstringOverride 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

EventPayloadFires 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 changeOnly 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.

On this page