From 261f3bdb77799009344aab4a60686b7186ebd3b0 Mon Sep 17 00:00:00 2001 From: "netop://ウィビ" Date: Fri, 24 Apr 2026 14:17:38 -0700 Subject: Implement v0.4 subscriptions + v0.5 theming and plugin slots - SSE and WebSocket fetchers via graphql-sse and graphql-ws, returning AsyncIterable results - SessionStore.run consumes AsyncIterable streams with subscriptionMode "append" (timestamped) or "replace" and honors an AbortSignal for cancellation - Chrome CSS variables documented in styles/theme.scss with prefers-color-scheme light/dark gating and .graphiql-light override - Svelte 5 snippet slots on GraphiQL: toolbarExtras, tabExtras, resultFooter --- source/library/fetcher/websocket.ts | 97 ++++++++++++++++++++++++++++++++++--- 1 file changed, 89 insertions(+), 8 deletions(-) (limited to 'source/library/fetcher/websocket.ts') diff --git a/source/library/fetcher/websocket.ts b/source/library/fetcher/websocket.ts index 6376e76..6c6bfe8 100644 --- a/source/library/fetcher/websocket.ts +++ b/source/library/fetcher/websocket.ts @@ -1,18 +1,99 @@ +/*** IMPORT ------------------------------------------- ***/ + +import { createClient, type Client, type ClientOptions } from "graphql-ws"; + /*** UTILITY ------------------------------------------ ***/ -import type { Fetcher, FetcherOptions } from "./types.ts"; +import type { Fetcher, FetcherOptions, FetcherResult } from "./types.ts"; /*** EXPORT ------------------------------------------- ***/ -/** - * WebSocket fetcher for graphql-ws protocol. - * Stub implementation — see PLAN.md stage v0.4 for full implementation. - */ -export function createWsFetcher(_options: FetcherOptions): Fetcher { - return () => { - throw new Error("WebSocket fetcher not yet implemented — see PLAN.md v0.4"); +export type WsFetcherOptions = Omit & { + client?: Partial>; +}; + +export function createWsFetcher(options: WsFetcherOptions): Fetcher { + const client: Client = createClient({ + connectionParams: options.headers, + url: options.url, + ...options.client + }); + + return (req) => { + return { + [Symbol.asyncIterator](): AsyncIterator { + const queue: FetcherResult[] = []; + const resolvers: ((step: IteratorResult) => void)[] = []; + let done = false; + let error: unknown = null; + + const dispose = client.subscribe>( + { + extensions: req.headers as Record | undefined, + operationName: req.operationName ?? undefined, + query: req.query, + variables: req.variables + }, + { + complete() { + done = true; + flush(); + }, + error(err) { + error = err; + done = true; + flush(); + }, + next(value) { + queue.push(value as FetcherResult); + flush(); + } + } + ); + + function flush() { + while (resolvers.length > 0 && (queue.length > 0 || done)) { + const resolve = resolvers.shift(); + + if (!resolve) + break; + + if (queue.length > 0) + resolve({ done: false, value: queue.shift() as FetcherResult }); + else + resolve({ done: true, value: undefined }); + } + } + + return { + next(): Promise> { + if (error) { + const err = error; + error = null; + return Promise.reject(err); + } + + if (queue.length > 0) + return Promise.resolve({ done: false, value: queue.shift() as FetcherResult }); + + if (done) + return Promise.resolve({ done: true, value: undefined }); + + return new Promise>((resolve) => { + resolvers.push(resolve); + }); + }, + return(): Promise> { + dispose(); + done = true; + flush(); + return Promise.resolve({ done: true, value: undefined }); + } + }; + } + }; }; } -- cgit v1.2.3