Skip to content

TanStack Query recipes

Feed shared updates into your existing query cache.

Open recipe

Socket.IO · SvelteKit

Start with an existing SvelteKit app. Keep its renderer, routes and plugins. These files add one live view; Spinetab does not create your API.

Your Socket.IO server emits socket.emit("queue", { open: 12 }) in the default namespace, using the default /socket.io path. Each event contains the complete queue. The example chooses sharing: "shared": use it only if merging tabs onto one socket suits your server's presence and membership rules.

The examples use same-origin URLs and declare anonymous: true: no token is supplied, although cookies still flow. For a separate API origin, use its URL and configure CORS or your application's proxy. For private data, add credentials and user scopes; never put a secret in these files.

Terminal window
pnpm add spinetab socket.io-client@^4.8.4 @tanstack/query-core@^5.104.0 @tanstack/svelte-query@^6.3.0

Keep your framework's existing dependencies. See compatible versions if upgrading an older app.

Merge this addition into your configuration; preserve existing plugins and options. Restart the dev server afterwards. The plugin bundles the worker and discovers the adapters imported below.

vite.config.ts
import { sveltekit } from "@sveltejs/kit/vite";
import { spinetab } from "spinetab/vite";
import { defineConfig } from "vite";
export default defineConfig({ plugins: [sveltekit(), spinetab()] });

Save these files together in src/lib/recipe/.

The provider owns a fresh QueryClient. If your app already has a provider, render Queue beneath it instead of creating another one. The query uses skipToken to read live cache updates without fetching a competing snapshot. The bridge uses that same client's ["queue"] key.

reconcile: "latest" fits this full-state contract: the next complete event restores the displayed value after a gap. Use a refresh/merge policy for deltas or partial GraphQL results. Reconnecting alone does not reconstruct missed state.

src/lib/recipe/live.ts
import { createSpinetab } from "spinetab";
export const spinetab = createSpinetab({ anonymous: true });
src/lib/recipe/queue-types.ts
export type Queue = { open: number };
export type QueueView = { open?: number; problem?: string };
src/lib/recipe/source.ts
import { socketIo } from "spinetab/socket-io";
import type { Queue, QueueView } from "./queue-types";
export const queueSource = socketIo("/", { sharing: "shared" }).subscription<
[Queue]
>({ event: "queue" });
export const selectQueue = ([queue]: [Queue]): QueueView => queue;
src/lib/recipe/feed.ts
import type { QueryClient } from "@tanstack/query-core";
import { bindQuery } from "spinetab/tanstack-query";
import { spinetab } from "./live";
import { queueSource, selectQueue } from "./source";
export function startQueue(
queryClient: QueryClient,
onError: (message: string) => void,
) {
return bindQuery(spinetab, queueSource, {
queryClient,
queryKey: ["queue"],
map: selectQueue,
reconcile: "latest",
onError: (error) => onError(error.message),
onEvent: () => onError(""),
});
}
src/lib/recipe/Queue.svelte
<script lang="ts">
import { createQuery, skipToken, useQueryClient } from "@tanstack/svelte-query";
import { onMount } from "svelte";
import { startQueue } from "./feed";
import type { QueueView } from "./queue-types";
const client = useQueryClient();
const queue = createQuery<QueueView>(() => ({
queryKey: ["queue"],
queryFn: skipToken,
}));
let error = $state("");
onMount(() => {
const binding = startQueue(client, (message) => {
error = message;
});
return () => binding.unsubscribe();
});
</script>
{#if error || queue.data?.problem}
<p role="alert">{error || queue.data?.problem}</p>
{:else if queue.data?.open === undefined}
<p role="status">Loading queue…</p>
{:else}
<p><output>{queue.data.open}</output> open</p>
{/if}
src/lib/recipe/Recipe.svelte
<script lang="ts">
import { QueryClient, QueryClientProvider } from "@tanstack/svelte-query";
import Queue from "./Queue.svelte";
const client = new QueryClient();
</script>
<QueryClientProvider {client}><Queue /></QueryClientProvider>
src/routes/queue/+page.svelte
<script lang="ts">
import Recipe from "../../lib/recipe/Recipe.svelte";
</script>
<main><Recipe /></main>

Keep your existing SvelteKit deployment adapter. Component lifecycle keeps live work out of server rendering; do not disable SSR or subscribe in a server load function.

Open /queue in two tabs of the same browser profile. Both should show advancing queue values. Matching subscriptions share upstream work when SharedWorker is available; fallback runs independently in each tab. Remove one view and the other should keep updating. Removing the last view releases its subscription; connection closure can follow the adapter's idle delay.

API options and advanced recovery · Authentication