Replay (SSR gap)
When you combine SSR with WebSocket live updates, there’s a gap between server-side data loading and the moment the client’s WebSocket connects. Messages published during that window are lost.
The replay plugin solves this without touching the adapter core. It’s opt-in - if you don’t import it, it doesn’t exist.
How it works
- Server: publish through a replay buffer instead of
platform.publish()directly - messages get a sequence number and are stored in a ring buffer - SSR: pass the current sequence number to the client via your
load()function - Client:
onReplay()connects, requests missed messages, and switches to live mode once caught up
Setup
Create a shared replay instance:
// src/lib/server/replay.js
import { createReplay } from 'svelte-adapter-uws/plugins/replay';
export const replay = createReplay({ size: 500 }); Use it when publishing:
// src/routes/chat/+page.server.js
import { replay } from '$lib/server/replay';
export async function load() {
const messages = await db.getRecentMessages();
return { messages, seq: replay.seq('chat') };
}
export const actions = {
send: async ({ request, platform }) => {
const form = await request.formData();
const msg = await db.createMessage(Object.fromEntries(form));
replay.publish(platform, 'chat', 'created', msg);
}
}; Handle replay requests in your WebSocket handler:
// src/hooks.ws.js
import { replay } from '$lib/server/replay';
export function message(ws, { data, platform }) {
const msg = JSON.parse(Buffer.from(data).toString());
if (msg.type === 'replay') {
replay.replay(ws, msg.topic, msg.since, platform, msg.reqId);
return;
}
} Options
| Option | Default | Description |
|---|---|---|
size | 1000 | Max messages per topic in the ring buffer |
maxTopics | 100 | Max tracked topics, LRU evicted |
Server API
| Method | Description |
|---|---|
replay.publish(platform, topic, event, data) | Publish and buffer the message |
replay.seq(topic) | Current sequence number for a topic |
replay.since(topic, seq) | Buffered messages after a sequence number |
replay.replay(ws, topic, sinceSeq, platform, reqId) | Send missed messages to one client |
replay.clear() | Reset everything |
replay.clearTopic(topic) | Reset one topic |
Client API
import { onReplay } from 'svelte-adapter-uws/plugins/replay/client';
// Works exactly like on() but bridges the SSR gap
const store = onReplay('chat', { since: data.seq });
// .scan() works the same as on().scan()
const messages = onReplay('chat', { since: data.seq }).scan([], reducer); Each onReplay() call generates a unique request ID that is sent with the replay request and matched against the server’s responses. Multiple onReplay('chat', ...) instances on the same page each receive only their own replay stream. The server must pass msg.reqId to replay.replay() as shown above for this to work.
Client example
<!-- src/routes/chat/+page.svelte -->
<script>
import { onReplay } from 'svelte-adapter-uws/plugins/replay/client';
let { data } = $props();
const messages = onReplay('chat', { since: data.seq }).scan(
data.messages,
(list, { event, data }) => {
if (event === 'created') return [...list, data];
return list;
}
);
</script>
{#each $messages as msg}
<p>{msg.text}</p>
{/each} Buffer overflow: If more than size messages were published before the client connected and the ring buffer wrapped around, the store emits a synthetic { event: 'truncated', data: null } event after the replayed messages. Check for it in your reducer to decide whether to reload from the server:
const messages = onReplay('chat', { since: data.seq }).scan(data.messages, (list, { event, data }) => {
if (event === 'truncated') return []; // buffer overflow - reload from server
if (event === 'created') return [...list, data];
return list;
}); Limitations
- In-memory only. The ring buffer lives in the server process. A restart loses the buffer. For most apps this is fine - the gap is typically under a second, and a page reload after a server restart gives fresh SSR data anyway.
- Single-worker only. In clustered mode, each worker has its own buffer. If the SSR load runs on worker A and the WebSocket connects to worker B, the replay won’t have the right messages. If you need replay with clustering, stick to a single worker or use an external store.
- Buffer overflow. If more than
sizemessages are published to a topic before a client requests replay, the oldest are gone. Size the buffer for your expected throughput during the SSR-to-connect window (usually well under 100 messages).
Was this page helpful?