History and recovery
Use Protocol V2 stream continuity, replay buffers, channel history, presence history, rewind, and mutable messages.
Sockudo separates connection recovery from durable history. Recovery keeps a connection continuous after a short interruption. History is an application feature for reading and reconstructing past state.
Recovery
Protocol V2 broadcasts carry stream metadata:
{
"event": "order.updated",
"channel": "orders",
"data": { "id": "ord_123" },
"message_id": "msg_01HX",
"stream_id": "orders:main",
"serial": 42
}Clients store recovery positions and provide them during reconnect. Sockudo replays missed messages if continuity can be proven.
For V2 clients, the subscription acknowledgement carries the first recovery position when recovery is enabled. Keep that position even before the channel receives any application event. A reconnect can then resume from the subscribed-but-idle checkpoint and replay messages published while the client was disconnected.
const client = new Sockudo("app-key", {
wsHost: "127.0.0.1",
wsPort: 6001,
forceTLS: false,
protocolVersion: 2,
connectionRecovery: true,
});
client.bind("sockudo:resume_success", (payload) => {
console.log(payload.recovered, payload.failed);
});If a channel emits sockudo:resume_failed with code: "position_expired", the hot replay buffer and durable recovery window could not prove continuity. Resubscribe, then backfill with V2 channel history using until_attach: true so the history page is bounded at the subscription's attach serial.
code: "continuity_unverifiable" means Sockudo rejected an ahead, non-contiguous, duplicate, or non-progressing recovery position. Treat it the same way: resubscribe and backfill instead of advancing the client cursor.
Recovery is two-tier:
- Hot replay uses the bounded per-channel replay buffer.
- Durable recovery uses
HistoryStorewhenstream_id, serial bounds, and retention prove continuity.
Both tiers replay the Protocol V2 wire message, not Sockudo's durable storage wrapper. Durable rows are decoded through the shared history projection first, so recovery and ordinary history reads preserve the same IDs, publisher fields, version projection, stream ID, and serial.
Versioned mutations keep the delivery position reserved by the native version service. The hot replay buffer adopts that position instead of allocating a second serial. Recovery validates every serial in the returned range; a missing position or mixed stream generation falls through to durable recovery and then fails closed if the durable tier cannot prove the same continuity.
The native V2 attach gate registers the subscriber before capturing the durable high-water mark. Concurrent publishes are held in a count-and-byte-bounded gate, shared across subscribers with immutable message references, then drained after rewind without a lock held across delivery. Overflow closes the affected connection because the live-plus-replay sequence can no longer be proven.
If the durable stream is degraded or reset-required, Sockudo fails closed instead of pretending the stream is continuous.
Rewind
Subscribe-time rewind asks Sockudo to send recent history when a client joins:
const channel = client.subscribe("orders", {
rewind: { seconds: 30 },
});
channel.bind("sockudo:rewind_complete", (payload) => {
console.log(payload.historical_count, payload.complete);
});When a V2 subscription has a compound predicate, rewind scans durable history in bounded pages until it fills the requested matching count or reaches the scan/retention boundary. Suppressed rows still advance the channel's canonical continuity position. Hot and durable recovery apply the same predicate after the client has reattached that subscription; a legacy resume sent before reattachment retains the unfiltered recovery contract. Each recovered-channel entry in sockudo:resume_success includes the terminal position so clients advance continuity even when the predicate suppresses the tail.
Durable channel history
Server SDKs expose app-key channel history helpers. Use signed app-key HTTP history for backend jobs, administrative reads, and server SDKs.
page = sockudo.get_channel_history(
"orders",
HistoryParams(limit=50, direction="newest_first"),
)Opaque cursors make storage implementation details private. Store the cursor as a token and pass it back unchanged.
Protocol V2 clients use WebSocket channel_history frames instead of direct HTTP history. Token-authenticated clients need the history capability for the requested channel. The existing app-key HTTP endpoint remains server-only; there is no client-token HTTP history surface in v1.
{
"event": "sockudo:channel_history",
"data": {
"channel": "orders",
"limit": 100,
"direction": "backwards",
"cursor": null,
"start_serial": 1,
"end_serial": 100,
"until_attach": true
}
}The response uses the same history payload shape as app-key HTTP history: items, direction, limit, has_more, next_cursor, bounds, continuity, and stream_state. Limits are capped at 1000 and may be lowered by channel policy. direction defaults to backwards/newest-first. until_attach: true bounds results to history_serial <= attach_serial, while live delivery after subscribe continues above that attach serial. Mutable messages are returned as the latest aggregated V2 message, not raw operation log rows.
until_attach is the late-join gaplessness rule: the history page stops at the serial captured when
the subscription succeeded, and live fanout continues above that serial. Clients should reduce
history first, then live messages. In a cluster, route this request to the node that owns the live
attachment. If Sockudo cannot find that authoritative attachment position, it rejects the read
instead of silently removing the upper bound and returning a page that could overlap live delivery.
Presence history
Presence history records joins, leaves, causes, and continuity metadata. It is different from the current presence member list.
Protocol projections must write transitions through the native presence service. Current membership remains available if the durable tier fails, but the tracking store marks that channel degraded. Continuity-sensitive history reads then fail closed until the durable stream is healthy or explicitly reset; they must not return a partial page as complete.
const channel = client.subscribe("presence-lobby");
const page = await channel.history({
limit: 50,
direction: "newest_first",
});
const snapshot = await channel.snapshot({ atSerial: 4 });Client helpers call your backend proxy. They do not sign Sockudo REST requests directly.
Mutable messages
Protocol V2 mutable messages use action events:
sockudo:message.updatesockudo:message.deletesockudo:message.append
Clients reduce those events into local state. Server SDKs can fetch the latest visible state or list preserved versions. When a V2 subscription uses rewind, historical rows that point at mutable messages are delivered as the latest visible version, so late joiners see accumulated appended content rather than only the original create payload.
const latest = await channel.getMessage("42");
const versions = await channel.getMessageVersions("42", {
limit: 20,
direction: "oldest_first",
});Push and history
Push notifications should include stable identifiers that let the app fetch authoritative state after the user opens the notification.
{
"title": "Order updated",
"body": "Order ord_123 is now packed",
"data": {
"channel": "orders",
"message_serial": "42",
"order_id": "ord_123"
}
}Do not encode the entire durable state into the push payload. Use push to wake the app, then read the latest message or application API state.