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

  1. Server: publish through a replay buffer instead of platform.publish() directly - messages get a sequence number and are stored in a ring buffer
  2. SSR: pass the current sequence number to the client via your load() function
  3. 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

OptionDefaultDescription
size1000Max messages per topic in the ring buffer
maxTopics100Max tracked topics, LRU evicted

Server API

MethodDescription
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 size messages 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?