Skip to content

Direct subscriptions recipes

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

Open recipe

GraphQL over WebSocket · Vite · Vue

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

Your schema exposes Subscription.queue: Queue! with Queue.open: Int!. Each event contains the complete queue value. Serve the graphql-transport-ws protocol at /graphql. Queries and mutations, if used, go to /graphql. This is a GraphQL endpoint, not a plain SSE or WebSocket feed.

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 graphql@^17.0.2 graphql-ws@^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 vue from "@vitejs/plugin-vue";
import { spinetab } from "spinetab/vite";
import { defineConfig } from "vite";
export default defineConfig({ plugins: [vue(), spinetab()] });

Save these files together in src/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.

src/recipe/live.ts
import { createSpinetab } from "spinetab";
export const spinetab = createSpinetab({ anonymous: true });
src/recipe/queue-types.ts
export type Queue = { open: number };
export type QueueView = { open?: number; problem?: string };
src/recipe/source.ts
import { parse } from "graphql";
import {
type GraphqlDocument,
type GraphqlResult,
graphqlWs,
} from "spinetab/graphql-ws";
import type { Queue, QueueView } from "./queue-types";
const document: GraphqlDocument<
{ queue: Queue },
Record<string, never>
> = parse("subscription Queue { queue { open } }");
export const queueSource = graphqlWs("/graphql").subscription({
query: document,
});
export function selectQueue(
result: GraphqlResult<{ queue: Queue }>,
): QueueView {
return {
open: result.data?.queue?.open,
problem:
result.errors?.map((error) => error.message).join("; ") ||
(result.data?.queue ? undefined : "The server returned no queue."),
};
}
src/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>
src/App.vue
<script setup lang="ts">
import Recipe from "./recipe/Recipe.vue";
</script>
<template
><Recipe /></template
>

Your existing entry point mounts App as usual. Keep its renderer plugin; no additional application provider is required beyond those shown.

Open the page 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