index.ts167 lines · main
1/**
2 * @briven/svelte — Svelte stores for briven.
3 *
4 * import { setBrivenClient, query, mutation } from '@briven/svelte';
5 * import { createBrivenClient } from '@briven/client';
6 *
7 * // once at app boot
8 * setBrivenClient(createBrivenClient({
9 * projectId: 'p_...',
10 * apiOrigin: 'https://api.briven.tech',
11 * wsOrigin: 'wss://ws.briven.tech',
12 * token: () => session.token,
13 * }));
14 *
15 * <script lang="ts">
16 * import { query, mutation } from '@briven/svelte';
17 *
18 * const notes = query<Note[]>('listNotes', { userId });
19 * const addNote = mutation<{ body: string }, Note>('addNote');
20 * </script>
21 *
22 * {#if $notes.isLoading}loading…
23 * {:else if $notes.error}{$notes.error.message}
24 * {:else}
25 * {#each $notes.data ?? [] as n}<li>{n.body}</li>{/each}
26 * {/if}
27 *
28 * Re-fetch + unsubscribe lifecycle is automatic — the store closes its
29 * subscription when no Svelte component is subscribed (reference-counted
30 * by Svelte's store contract).
31 */
32
33import { readable, writable, type Readable } from 'svelte/store';
34
35import type { BrivenClient, InvokeFrame, SubscribeHandle } from '@briven/client';
36
37let activeClient: BrivenClient | null = null;
38
39/**
40 * Set the BrivenClient instance every store will use. Call once at app
41 * boot. Throws if called more than once with a different client (avoids
42 * accidental re-init in HMR).
43 */
44export function setBrivenClient(client: BrivenClient): void {
45 if (activeClient && activeClient !== client) {
46 throw new Error(
47 'setBrivenClient called twice with different clients — call it once at boot',
48 );
49 }
50 activeClient = client;
51}
52
53export function getBrivenClient(): BrivenClient {
54 if (!activeClient) {
55 throw new Error(
56 'no BrivenClient set — call setBrivenClient(createBrivenClient({...})) at app boot',
57 );
58 }
59 return activeClient;
60}
61
62export interface QueryState<T> {
63 readonly data: T | undefined;
64 readonly error: { code: string; message: string } | undefined;
65 readonly isLoading: boolean;
66 readonly durationMs: number | undefined;
67}
68
69/**
70 * Subscribe to a briven function. Returns a Svelte readable store that
71 * starts subscribing on first $-bind and unsubscribes when the last
72 * subscriber drops away.
73 */
74export function query<TResult = unknown>(
75 functionName: string,
76 args: unknown = {},
77): Readable<QueryState<TResult>> {
78 const client = getBrivenClient();
79 return readable<QueryState<TResult>>(
80 { data: undefined, error: undefined, isLoading: true, durationMs: undefined },
81 (set) => {
82 let cancelled = false;
83 const handle: SubscribeHandle = client.subscribe(
84 functionName,
85 args,
86 (frame: InvokeFrame) => {
87 if (cancelled) return;
88 if (frame.ok) {
89 set({
90 data: frame.value as TResult,
91 error: undefined,
92 isLoading: false,
93 durationMs: frame.durationMs,
94 });
95 } else {
96 set({
97 data: undefined,
98 error: { code: frame.code, message: frame.message },
99 isLoading: false,
100 durationMs: frame.durationMs,
101 });
102 }
103 },
104 );
105 return () => {
106 cancelled = true;
107 handle.close();
108 };
109 },
110 );
111}
112
113export interface MutationState<TResult> {
114 readonly isPending: boolean;
115 readonly error: { code: string; message: string } | undefined;
116 readonly data: TResult | undefined;
117}
118
119export interface MutationStore<TArgs, TResult> extends Readable<MutationState<TResult>> {
120 mutate(args: TArgs): Promise<TResult | undefined>;
121 reset(): void;
122}
123
124/**
125 * One-shot mutation over HTTP. Use the returned store's `.mutate(args)`
126 * to fire the call; its state stream gives you `isPending` / `error` /
127 * `data` for the UI. Mutations don't auto-subscribe — reactive queries
128 * affected by the write receive their own fresh frames via the
129 * realtime LISTEN/NOTIFY pipeline.
130 */
131export function mutation<TArgs = unknown, TResult = unknown>(
132 functionName: string,
133): MutationStore<TArgs, TResult> {
134 const client = getBrivenClient();
135 const initial: MutationState<TResult> = {
136 isPending: false,
137 error: undefined,
138 data: undefined,
139 };
140 const store = writable<MutationState<TResult>>(initial);
141
142 async function mutate(args: TArgs): Promise<TResult | undefined> {
143 store.set({ isPending: true, error: undefined, data: undefined });
144 const frame = await client.invoke(functionName, args);
145 if (frame.ok) {
146 const value = frame.value as TResult;
147 store.set({ isPending: false, error: undefined, data: value });
148 return value;
149 }
150 store.set({
151 isPending: false,
152 error: { code: frame.code, message: frame.message },
153 data: undefined,
154 });
155 return undefined;
156 }
157
158 function reset(): void {
159 store.set(initial);
160 }
161
162 return {
163 subscribe: store.subscribe,
164 mutate,
165 reset,
166 };
167}