aboutsummaryrefslogtreecommitdiff
path: root/source/library/fetcher/apq.ts
diff options
context:
space:
mode:
authornetop://ウィビ <paul@webb.page>2026-04-24 16:37:33 -0700
committernetop://ウィビ <paul@webb.page>2026-04-24 16:37:33 -0700
commit510fd8cbe53abb39cba2c7cbaaefcf2783dc0066 (patch)
tree8f753a33c475b285f2a297785d34cda3b0a8faed /source/library/fetcher/apq.ts
parent261f3bdb77799009344aab4a60686b7186ebd3b0 (diff)
downloadgraphiql-510fd8cbe53abb39cba2c7cbaaefcf2783dc0066.tar.gz
graphiql-510fd8cbe53abb39cba2c7cbaaefcf2783dc0066.zip
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
Diffstat (limited to 'source/library/fetcher/apq.ts')
-rw-r--r--source/library/fetcher/apq.ts101
1 files changed, 101 insertions, 0 deletions
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<string, string>();
+ 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<string, string>, query: string): Promise<string> {
+ 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<string, unknown>): 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<string, unknown>;
+
+ if (err.message === "PersistedQueryNotFound")
+ return true;
+
+ const ext = err.extensions;
+
+ if (ext !== null && typeof ext === "object") {
+ const code = (ext as Record<string, unknown>).code;
+
+ if (code === "PERSISTED_QUERY_NOT_FOUND")
+ return true;
+ }
+ }
+
+ return false;
+}
+
+async function sha256Hex(input: string): Promise<string> {
+ 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("");
+}