Skip to content

Direct subscriptions recipes

Render a live feed with your framework's component binding.

Open recipe

Fetch stream · Nuxt

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

Your endpoint supplies complete { "open": 12 } values: one JSON value per line from /api/queue/stream (NDJSON). Opening this request again must be safe.

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

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.

nuxt.config.ts
import { defineNuxtConfig } from "nuxt/config";
export default defineNuxtConfig({ modules: ["spinetab/nuxt"] });

Save these files together in app/components/recipe/.

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.

app/components/recipe/live.ts
import { createSpinetab } from "spinetab";
export const spinetab = createSpinetab({ anonymous: true });
app/components/recipe/queue-types.ts
export type Queue = { open: number };
export type QueueView = { open?: number; problem?: string };
app/components/recipe/source.ts
import { stream } from "spinetab/stream";
import type { Queue, QueueView } from "./queue-types";
export const queueSource = stream<Queue>("/api/queue/stream", {
repeatable: true,
});
export const selectQueue = (queue: Queue): QueueView => queue;
app/components/recipe/Recipe.vue
<script setup lang="ts">
import { bindClient } from "spinetab/vue";
import { spinetab } from "./live";
import { queueSource, selectQueue } from "./source";
const { useLive } = bindClient(spinetab);
const { data, error } = useLive(queueSource, {
map: selectQueue,
reconcile: "latest",
});
</script>
<template>
<p v-if="error || data?.problem" role="alert">
{{ error?.message ?? data?.problem }}
</p>
<p v-else-if="data?.open === undefined" role="status">Loading queue…</p>
<p v-else><output>{{ data.open }}</output> open</p>
</template>
app/pages/queue.vue
<script setup lang="ts">
import Recipe from "../components/recipe/Recipe.vue";
</script>
<template
><main><Recipe /></main></template
>

Your existing app/app.vue must render <NuxtPage />. Work starts on browser mount; no ClientOnly wrapper is needed. KeepAlive retains subscriptions until the component is disposed.

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