aboutsummaryrefslogtreecommitdiff
path: root/source/library/state/session.svelte.ts
diff options
context:
space:
mode:
authornetop://ウィビ <paul@webb.page>2026-04-24 11:33:25 -0700
committernetop://ウィビ <paul@webb.page>2026-04-24 11:33:25 -0700
commit8a59f92d031963e23ecc84b75feecf43eb4dd146 (patch)
tree75de5768885583897061a3b1795e4c987ce90039 /source/library/state/session.svelte.ts
downloadgraphiql-8a59f92d031963e23ecc84b75feecf43eb4dd146.tar.gz
graphiql-8a59f92d031963e23ecc84b75feecf43eb4dd146.zip
Initial commit: @eol/graphiql v0.3
Svelte 5 GraphiQL alternative for JSR. Covers: - HTTP fetcher with injectable fetch; SSE/WS stubs - Session store with tabs, auto-titling, persistence, rename - Operation detection via graphql parse(); Toolbar picker - CodeMirror 6 editor via cm6-graphql with theme prop - Light theme preset (hand-rolled EditorView.theme) - Doc explorer with breadcrumb nav and type guards - History panel with 100-entry cap, favorite pinning - Deno tests for operations, storage, and history eviction
Diffstat (limited to 'source/library/state/session.svelte.ts')
-rw-r--r--source/library/state/session.svelte.ts242
1 files changed, 242 insertions, 0 deletions
diff --git a/source/library/state/session.svelte.ts b/source/library/state/session.svelte.ts
new file mode 100644
index 0000000..d9f52ff
--- /dev/null
+++ b/source/library/state/session.svelte.ts
@@ -0,0 +1,242 @@
+
+
+
+/*** UTILITY ------------------------------------------ ***/
+
+import type { Fetcher } from "../fetcher/types.ts";
+import {
+ deriveTitle,
+ parseOperations,
+ type OperationInfo
+} from "../graphql/operations.ts";
+import type { Storage } from "./storage.ts";
+
+const STORAGE_KEY = "session";
+
+type Snapshot = {
+ activeId: string;
+ tabs: Tab[];
+};
+
+/*** EXPORT ------------------------------------------- ***/
+
+export type Tab = {
+ headers: string;
+ id: string;
+ operationName: string | null;
+ operations: OperationInfo[];
+ query: string;
+ result: string;
+ title: string;
+ titleDirty: boolean;
+ variables: string;
+};
+
+export type TabSeed = {
+ headers?: string;
+ operationName?: string | null;
+ query?: string;
+ title?: string;
+ variables?: string;
+};
+
+export class SessionStore {
+ activeId = $state<string>("");
+ tabs = $state<Tab[]>([]);
+ active = $derived(this.tabs.find((t) => t.id === this.activeId));
+
+ #storage: Storage;
+
+ constructor(storage: Storage) {
+ this.#storage = storage;
+ const restored = storage.get<Snapshot>(STORAGE_KEY);
+
+ if (restored && restored.tabs.length > 0) {
+ this.tabs = restored.tabs.map((t) => this.#hydrate(t));
+
+ this.activeId = this.tabs.some((t) => t.id === restored.activeId) ?
+ restored.activeId :
+ this.tabs[0].id;
+ } else {
+ const tab = this.#blank();
+ this.tabs = [tab];
+ this.activeId = tab.id;
+ }
+ }
+
+ addTab(seed?: TabSeed) {
+ const tab = seed ? this.#seeded(seed) : this.#blank();
+ this.tabs.push(tab);
+ this.activeId = tab.id;
+ }
+
+ closeTab(id: string) {
+ const idx = this.tabs.findIndex((t) => t.id === id);
+
+ if (idx === -1)
+ return;
+
+ if (this.tabs.length === 1) {
+ const fresh = this.#blank();
+ this.tabs = [fresh];
+ this.activeId = fresh.id;
+
+ return;
+ }
+
+ this.tabs.splice(idx, 1);
+
+ if (this.activeId === id)
+ this.activeId = this.tabs[Math.max(0, idx - 1)].id;
+ }
+
+ persist() {
+ this.#storage.set<Snapshot>(STORAGE_KEY, {
+ activeId: this.activeId,
+ tabs: this.tabs
+ });
+ }
+
+ renameActive(title: string) {
+ if (!this.active)
+ return;
+
+ this.active.title = title.trim() || "untitled";
+ this.active.titleDirty = true;
+ }
+
+ renameTab(id: string, title: string) {
+ const tab = this.tabs.find((t) => t.id === id);
+
+ if (!tab)
+ return;
+
+ tab.title = title.trim() || "untitled";
+ tab.titleDirty = true;
+ }
+
+ async run(fetcher: Fetcher): Promise<boolean> {
+ const tab = this.active;
+
+ if (!tab)
+ return false;
+
+ try {
+ const variables = tab.variables.trim() ? JSON.parse(tab.variables) : {};
+ const headers = tab.headers.trim() ? JSON.parse(tab.headers) : {};
+
+ const result = await fetcher({
+ headers,
+ operationName: tab.operationName,
+ query: tab.query,
+ variables
+ });
+
+ tab.result = JSON.stringify(result, null, 2);
+ return true;
+ } catch(err) {
+ tab.result = JSON.stringify({ error: String(err) }, null, 2);
+ return false;
+ }
+ }
+
+ overwriteActive(seed: TabSeed) {
+ const tab = this.active;
+
+ if (!tab)
+ return;
+
+ const query = seed.query ?? "";
+ tab.headers = seed.headers ?? "{}";
+ tab.operations = parseOperations(query);
+ tab.operationName = seed.operationName ?? null;
+ tab.query = query;
+ tab.result = "";
+ tab.variables = seed.variables ?? "{}";
+
+ if (seed.title && !tab.titleDirty)
+ tab.title = seed.title;
+ else if (!tab.titleDirty)
+ tab.title = deriveTitle(query, tab.operations);
+ }
+
+ selectOperation(id: string, name: string | null) {
+ const tab = this.tabs.find((t) => t.id === id);
+
+ if (tab)
+ tab.operationName = name;
+ }
+
+ selectTab(id: string) {
+ if (this.tabs.some((t) => t.id === id))
+ this.activeId = id;
+ }
+
+ updateQuery(id: string, query: string) {
+ const tab = this.tabs.find((t) => t.id === id);
+
+ if (!tab)
+ return;
+
+ tab.query = query;
+
+ const ops = parseOperations(query);
+ tab.operations = ops;
+
+ if (ops.length === 0)
+ tab.operationName = null;
+ else if (ops.length === 1)
+ tab.operationName = ops[0].name;
+ else if (tab.operationName && !ops.some((o) => o.name === tab.operationName))
+ tab.operationName = null;
+
+ if (!tab.titleDirty)
+ tab.title = deriveTitle(query, ops);
+ }
+
+ #blank(): Tab {
+ return {
+ headers: "{}",
+ id: crypto.randomUUID(),
+ operationName: null,
+ operations: [],
+ query: "",
+ result: "",
+ title: "untitled",
+ titleDirty: false,
+ variables: "{}"
+ };
+ }
+
+ #hydrate(raw: Tab): Tab {
+ return {
+ headers: raw.headers ?? "{}",
+ id: raw.id,
+ operationName: raw.operationName ?? null,
+ operations: raw.operations ?? parseOperations(raw.query ?? ""),
+ query: raw.query ?? "",
+ result: raw.result ?? "",
+ title: raw.title ?? "untitled",
+ titleDirty: raw.titleDirty ?? raw.title !== "untitled",
+ variables: raw.variables ?? "{}"
+ };
+ }
+
+ #seeded(seed: TabSeed): Tab {
+ const query = seed.query ?? "";
+ const operations = parseOperations(query);
+ const title = seed.title ?? deriveTitle(query, operations);
+
+ return {
+ headers: seed.headers ?? "{}",
+ id: crypto.randomUUID(),
+ operationName: seed.operationName ?? null,
+ operations,
+ query,
+ result: "",
+ title,
+ titleDirty: Boolean(seed.title),
+ variables: seed.variables ?? "{}"
+ };
+ }
+}