diff options
| author | netop://ウィビ <paul@webb.page> | 2026-04-24 14:17:38 -0700 |
|---|---|---|
| committer | netop://ウィビ <paul@webb.page> | 2026-04-24 14:17:38 -0700 |
| commit | 261f3bdb77799009344aab4a60686b7186ebd3b0 (patch) | |
| tree | 8e87c6610a307f15f0c4b32f68b19424273fc6ad /source/library/fetcher | |
| parent | 8a59f92d031963e23ecc84b75feecf43eb4dd146 (diff) | |
| download | graphiql-261f3bdb77799009344aab4a60686b7186ebd3b0.tar.gz graphiql-261f3bdb77799009344aab4a60686b7186ebd3b0.zip | |
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
Diffstat (limited to 'source/library/fetcher')
| -rw-r--r-- | source/library/fetcher/sse.ts | 100 | ||||
| -rw-r--r-- | source/library/fetcher/websocket.ts | 97 |
2 files changed, 181 insertions, 16 deletions
diff --git a/source/library/fetcher/sse.ts b/source/library/fetcher/sse.ts index b6805dc..c28436b 100644 --- a/source/library/fetcher/sse.ts +++ b/source/library/fetcher/sse.ts @@ -1,18 +1,102 @@ +/*** IMPORT ------------------------------------------- ***/ + +import { createClient, type Client, type ClientOptions } from "graphql-sse"; + /*** UTILITY ------------------------------------------ ***/ -import type { Fetcher, FetcherOptions } from "./types.ts"; +import type { Fetcher, FetcherOptions, FetcherResult } from "./types.ts"; /*** EXPORT ------------------------------------------- ***/ -/** - * Server-Sent Events fetcher for graphql-sse protocol. - * Stub implementation — see PLAN.md stage v0.4 for full implementation. - */ -export function createSseFetcher(_options: FetcherOptions): Fetcher { - return () => { - throw new Error("SSE fetcher not yet implemented — see PLAN.md v0.4"); +export type SseFetcherOptions = FetcherOptions & { + client?: Partial<Omit<ClientOptions, "url">>; +}; + +export function createSseFetcher(options: SseFetcherOptions): Fetcher { + const client: Client = createClient({ + fetchFn: options.fetch, + headers: options.headers, + url: options.url, + ...options.client + }); + + return (req) => { + return { + [Symbol.asyncIterator](): AsyncIterator<FetcherResult> { + const queue: FetcherResult[] = []; + const resolvers: ((step: IteratorResult<FetcherResult>) => void)[] = []; + let done = false; + let error: unknown = null; + + const dispose = client.subscribe<FetcherResult, Record<string, unknown>>( + { + extensions: req.headers as Record<string, unknown> | 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 if (error) + resolve({ done: true, value: undefined }); + else + resolve({ done: true, value: undefined }); + } + } + + return { + next(): Promise<IteratorResult<FetcherResult>> { + 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<IteratorResult<FetcherResult>>((resolve) => { + resolvers.push(resolve); + }); + }, + return(): Promise<IteratorResult<FetcherResult>> { + dispose(); + done = true; + flush(); + return Promise.resolve({ done: true, value: undefined }); + } + }; + } + }; }; } 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<FetcherOptions, "fetch"> & { + client?: Partial<Omit<ClientOptions, "url">>; +}; + +export function createWsFetcher(options: WsFetcherOptions): Fetcher { + const client: Client = createClient({ + connectionParams: options.headers, + url: options.url, + ...options.client + }); + + return (req) => { + return { + [Symbol.asyncIterator](): AsyncIterator<FetcherResult> { + const queue: FetcherResult[] = []; + const resolvers: ((step: IteratorResult<FetcherResult>) => void)[] = []; + let done = false; + let error: unknown = null; + + const dispose = client.subscribe<FetcherResult, Record<string, unknown>>( + { + extensions: req.headers as Record<string, unknown> | 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<IteratorResult<FetcherResult>> { + 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<IteratorResult<FetcherResult>>((resolve) => { + resolvers.push(resolve); + }); + }, + return(): Promise<IteratorResult<FetcherResult>> { + dispose(); + done = true; + flush(); + return Promise.resolve({ done: true, value: undefined }); + } + }; + } + }; }; } |