Client APIs

Virtual imports

Every file in src/live/ generates a $live/ import. The Vite plugin reads your server code and generates lightweight client stubs.

// src/live/chat.js exports sendMessage (live) and messages (stream)
import { sendMessage, messages } from '$live/chat';
  • live() exports become async functions
  • live.stream() exports become Svelte stores
  • live.validated() exports become async functions (validation runs server-side)

Store subscription

Stream stores are standard Svelte stores. Use the $ prefix:

<script>
  import { messages } from '$live/chat';
</script>

{#each $messages as msg (msg.id)}
  <p>{msg.text}</p>
{/each}

Pagination

Return { data, hasMore, cursor } from your stream init to enable cursor pagination:

// Server
export const posts = live.stream('posts', async (ctx) => {
  const limit = 20;
  const rows = await db.posts.list({ limit: limit + 1, after: ctx.cursor });
  const hasMore = rows.length > limit;
  const data = hasMore ? rows.slice(0, limit) : rows;
  const cursor = data.at(-1)?.id ?? null;
  return { data, hasMore, cursor };
}, { merge: 'crud', key: 'id' });
<!-- Client -->
<script>
  import { posts } from '$live/feed';
</script>

{#each $posts as post (post.id)}
  <p>{post.title}</p>
{/each}

{#if posts.hasMore}
  <button onclick={() => posts.loadMore()}>Load more</button>
{/if}

Optimistic updates

Apply changes instantly, roll back on failure:

<script>
  import { todos, addTodo } from '$live/todos';

  async function add(text) {
    const tempId = 'temp-' + Date.now();
    const rollback = todos.optimistic('created', { id: tempId, text });
    try {
      await addTodo(text);
    } catch {
      rollback();
    }
  }
</script>

Undo and redo

<script>
  import { todos } from '$live/todos';
  todos.enableHistory(100);
</script>

<button onclick={() => todos.undo()} disabled={!todos.canUndo}>Undo</button>
<button onclick={() => todos.redo()} disabled={!todos.canRedo}>Redo</button>

SSR hydration

Load data server-side, then hydrate the client store:

// src/routes/chat/+page.server.js
export async function load({ platform }) {
  const { messages } = await import('$live/chat');
  return { messages: await messages.load(platform) };
}
<script>
  import { messages } from '$live/chat';
  let { data } = $props();
  const msgs = messages.hydrate(data.messages);
</script>

Connection hooks

<script>
  import { configure } from 'svelte-realtime/client';

  configure({
    onConnect() { /* reconnected */ },
    onDisconnect() { /* connection lost */ }
  });
</script>

Combine stores

<script>
  import { combine } from 'svelte-realtime/client';
  import { orders, inventory } from '$live/dashboard';

  const dashboard = combine(orders, inventory, (o, i) => ({
    pending: o?.filter(x => x.status === 'pending').length ?? 0,
    lowStock: i?.filter(x => x.qty < 10) ?? []
  }));
</script>

Offline queue

import { configure } from 'svelte-realtime/client';

configure({
  offline: {
    queue: true,
    maxQueue: 100,
    maxAge: 60000
  }
});

RPCs called while disconnected queue up and replay on reconnect.

Delta sync and replay

Delta sync

Enable delta sync for efficient reconnection on streams with large datasets. Instead of refetching all data, the server sends only what changed since the client’s last known version.

export const inventory = live.stream('inventory', async (ctx) => {
  return db.inventory.all();
}, {
  merge: 'crud',
  key: 'sku',
  delta: {
    version: () => db.inventory.lastModified(),
    diff: async (sinceVersion) => {
      const changes = await db.inventory.changedSince(sinceVersion);
      return changes; // null to force full refetch
    }
  }
});

How it works:

  • On first connect, the client gets the full dataset plus a version value
  • On reconnect, the client sends its last known version
  • If versions match: server responds with { unchanged: true } (nearly zero bytes)
  • If versions differ: server calls diff(sinceVersion) and sends only the changes
  • If diff returns null: falls back to full refetch

Replay

Enable seq-based replay for gap-free stream reconnection. When a client reconnects, it sends its last known sequence number. If the server has the missed events buffered, it sends only those instead of a full refetch.

export const feed = live.stream('feed', async (ctx) => {
  return db.feed.latest(50);
}, { merge: 'latest', max: 50, replay: true });

Replay requires the replay extension from svelte-adapter-uws-extensions. When replay is not available or the gap is too large, the client falls back to a full refetch automatically.

With adapter 0.4.0+, the replay end marker sends { reqId } (replay complete) or { reqId, truncated: true } (cache miss). When truncated, the client automatically resets its sequence number and triggers a full refetch.

Was this page helpful?