From 510fd8cbe53abb39cba2c7cbaaefcf2783dc0066 Mon Sep 17 00:00:00 2001 From: "netop://ウィビ" Date: Fri, 24 Apr 2026 16:37:33 -0700 Subject: Implement v0.6-1.0: shortcuts, format, export/import, splitter, timing, APQ - v0.6: matchShortcut + format(); Cmd+Shift+Enter/W/F + Cmd+Alt+arrows - v0.7: SessionStore.exportAll/importTabs with version-1 validator - v0.8: Splitter component + four resize handles persisted under layout.* - v0.10: createApqFetcher (HTTP-only) wrapping shared http-body helpers - Drop .svelte re-exports from index.ts for multi-entry JSR/npm publishing --- source/library/fetcher/apq.ts | 101 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 source/library/fetcher/apq.ts (limited to 'source/library/fetcher/apq.ts') diff --git a/source/library/fetcher/apq.ts b/source/library/fetcher/apq.ts new file mode 100644 index 0000000..7f9f3c1 --- /dev/null +++ b/source/library/fetcher/apq.ts @@ -0,0 +1,101 @@ + + + +/*** UTILITY ------------------------------------------ ***/ + +import { buildHeaders, postJson } from "./http-body.ts"; +import type { Fetcher, FetcherOptions } from "./types.ts"; + +/*** EXPORT ------------------------------------------- ***/ + +export type ApqOptions = FetcherOptions & { + disable?: boolean; +}; + +export function createApqFetcher(options: ApqOptions): Fetcher { + const cache = new Map(); + const fetchImpl = options.fetch ?? globalThis.fetch; + + return async (req) => { + const headers = buildHeaders(options, req); + const sha256Hash = await getHash(cache, req.query); + + const extensions = { + persistedQuery: { + sha256Hash, + version: 1 + } + }; + + if (options.disable === true) { + return await postJson(fetchImpl, options.url, headers, { + extensions, + operationName: req.operationName, + query: req.query, + variables: req.variables + }); + } + + const firstResponse = await postJson(fetchImpl, options.url, headers, { + extensions, + operationName: req.operationName, + variables: req.variables + }); + + if (!isPersistedQueryNotFound(firstResponse)) + return firstResponse; + + return await postJson(fetchImpl, options.url, headers, { + extensions, + operationName: req.operationName, + query: req.query, + variables: req.variables + }); + }; +} + +/*** INTERNAL ----------------------------------------- ***/ + +async function getHash(cache: Map, query: string): Promise { + const cached = cache.get(query); + + if (cached !== undefined) + return cached; + + const hash = await sha256Hex(query); + cache.set(query, hash); + return hash; +} + +function isPersistedQueryNotFound(response: Record): boolean { + const errors = response.errors; + + if (!Array.isArray(errors)) + return false; + + for (const entry of errors) { + if (entry === null || typeof entry !== "object") + continue; + + const err = entry as Record; + + if (err.message === "PersistedQueryNotFound") + return true; + + const ext = err.extensions; + + if (ext !== null && typeof ext === "object") { + const code = (ext as Record).code; + + if (code === "PERSISTED_QUERY_NOT_FOUND") + return true; + } + } + + return false; +} + +async function sha256Hex(input: string): Promise { + const buf = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(input)); + return Array.from(new Uint8Array(buf)).map((b) => b.toString(16).padStart(2, "0")).join(""); +} -- cgit v1.2.3