aboutsummaryrefslogtreecommitdiff
path: root/source/library/state/session.svelte.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/state/session.svelte.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/state/session.svelte.ts')
-rw-r--r--source/library/state/session.svelte.ts142
1 files changed, 142 insertions, 0 deletions
diff --git a/source/library/state/session.svelte.ts b/source/library/state/session.svelte.ts
index 9345d29..76777e5 100644
--- a/source/library/state/session.svelte.ts
+++ b/source/library/state/session.svelte.ts
@@ -4,11 +4,18 @@
/*** UTILITY ------------------------------------------ ***/
import type { Fetcher, FetcherResult } from "../fetcher/types.ts";
+import { format } from "../graphql/format.ts";
import {
deriveTitle,
parseOperations,
type OperationInfo
} from "../graphql/operations.ts";
+import {
+ tabToExport,
+ type ImportResult,
+ type SessionExport,
+ type TabExport
+} from "./session-io.ts";
import type { Storage } from "./storage.ts";
const STORAGE_KEY = "session";
@@ -26,6 +33,12 @@ type Snapshot = {
/*** EXPORT ------------------------------------------- ***/
+export type TabTiming = {
+ endMs: number;
+ firstByteMs: number;
+ startMs: number;
+};
+
export type Tab = {
headers: string;
id: string;
@@ -33,6 +46,8 @@ export type Tab = {
operations: OperationInfo[];
query: string;
result: string;
+ streamIntervals: number[];
+ timing: TabTiming | null;
title: string;
titleDirty: boolean;
variables: string;
@@ -103,6 +118,85 @@ export class SessionStore {
this.activeId = this.tabs[Math.max(0, idx - 1)].id;
}
+ exportAll(): SessionExport {
+ return {
+ exportedAt: new Date().toISOString(),
+ tabs: this.tabs.map(tabToExport),
+ version: 1
+ };
+ }
+
+ exportTab(id: string): TabExport | null {
+ const tab = this.tabs.find((t) => t.id === id);
+
+ if (!tab)
+ return null;
+
+ return tabToExport(tab);
+ }
+
+ formatActive() {
+ const tab = this.active;
+
+ if (!tab)
+ return;
+
+ const next = format(tab.query);
+
+ if (next === tab.query)
+ return;
+
+ tab.query = next;
+ tab.operations = parseOperations(next);
+
+ if (!tab.titleDirty)
+ tab.title = deriveTitle(next, tab.operations);
+ }
+
+ importTabs(data: SessionExport, opts: { mode: "append" | "replace" }): ImportResult {
+ const errors: string[] = [];
+ const capped = data.tabs.slice(0, 50);
+ const skipped = data.tabs.length - capped.length;
+
+ if (opts.mode === "replace") {
+ this.tabs = [];
+
+ for (const t of capped)
+ this.tabs.push(this.#seeded(t));
+
+ if (this.tabs.length === 0) {
+ const fresh = this.#blank();
+ this.tabs = [fresh];
+ this.activeId = fresh.id;
+ } else {
+ this.activeId = this.tabs[0].id;
+ }
+
+ return { added: capped.length, errors, skipped };
+ }
+
+ for (const t of capped)
+ this.tabs.push(this.#seeded(t));
+
+ if (capped.length > 0)
+ this.activeId = this.tabs[this.tabs.length - 1].id;
+
+ return { added: capped.length, errors, skipped };
+ }
+
+ nextTab() {
+ if (this.tabs.length <= 1)
+ return;
+
+ const idx = this.tabs.findIndex((t) => t.id === this.activeId);
+
+ if (idx === -1)
+ return;
+
+ const nextIdx = (idx + 1) % this.tabs.length;
+ this.activeId = this.tabs[nextIdx].id;
+ }
+
persist() {
this.#storage.set<Snapshot>(STORAGE_KEY, {
activeId: this.activeId,
@@ -110,6 +204,19 @@ export class SessionStore {
});
}
+ prevTab() {
+ if (this.tabs.length <= 1)
+ return;
+
+ const idx = this.tabs.findIndex((t) => t.id === this.activeId);
+
+ if (idx === -1)
+ return;
+
+ const prevIdx = (idx - 1 + this.tabs.length) % this.tabs.length;
+ this.activeId = this.tabs[prevIdx].id;
+ }
+
renameActive(title: string) {
if (!this.active)
return;
@@ -137,6 +244,10 @@ export class SessionStore {
const mode = options.subscriptionMode ?? "append";
const signal = options.signal;
+ tab.streamIntervals = [];
+ const startMs = performance.now();
+ tab.timing = { endMs: startMs, firstByteMs: startMs, startMs };
+
try {
const variables = tab.variables.trim() ? JSON.parse(tab.variables) : {};
const headers = tab.headers.trim() ? JSON.parse(tab.headers) : {};
@@ -151,6 +262,8 @@ export class SessionStore {
if (isAsyncIterable<FetcherResult>(result)) {
tab.result = "";
const iterator = result[Symbol.asyncIterator]();
+ let firstByteRecorded = false;
+ let previousMs = startMs;
try {
while (true) {
@@ -164,6 +277,23 @@ export class SessionStore {
if (step.done)
break;
+ const now = performance.now();
+
+ if (!firstByteRecorded) {
+ firstByteRecorded = true;
+ previousMs = now;
+ tab.timing = { ...tab.timing, endMs: now, firstByteMs: now };
+ } else {
+ const delta = now - previousMs;
+ previousMs = now;
+
+ if (tab.streamIntervals.length >= 500)
+ tab.streamIntervals.shift();
+
+ tab.streamIntervals = [...tab.streamIntervals, delta];
+ tab.timing = { ...tab.timing, endMs: now };
+ }
+
const payload = JSON.stringify(step.value, null, 2);
if (mode === "append") {
@@ -181,10 +311,14 @@ export class SessionStore {
return true;
}
+ const firstByteMs = performance.now();
+ tab.timing = { ...tab.timing, firstByteMs };
tab.result = JSON.stringify(result, null, 2);
+ tab.timing = { ...tab.timing, endMs: performance.now() };
return true;
} catch(err) {
tab.result = JSON.stringify({ error: String(err) }, null, 2);
+ tab.timing = { ...tab.timing, endMs: performance.now() };
return false;
}
}
@@ -201,6 +335,8 @@ export class SessionStore {
tab.operationName = seed.operationName ?? null;
tab.query = query;
tab.result = "";
+ tab.streamIntervals = [];
+ tab.timing = null;
tab.variables = seed.variables ?? "{}";
if (seed.title && !tab.titleDirty)
@@ -251,6 +387,8 @@ export class SessionStore {
operations: [],
query: "",
result: "",
+ streamIntervals: [],
+ timing: null,
title: "untitled",
titleDirty: false,
variables: "{}"
@@ -265,6 +403,8 @@ export class SessionStore {
operations: raw.operations ?? parseOperations(raw.query ?? ""),
query: raw.query ?? "",
result: raw.result ?? "",
+ streamIntervals: [],
+ timing: null,
title: raw.title ?? "untitled",
titleDirty: raw.titleDirty ?? raw.title !== "untitled",
variables: raw.variables ?? "{}"
@@ -283,6 +423,8 @@ export class SessionStore {
operations,
query,
result: "",
+ streamIntervals: [],
+ timing: null,
title,
titleDirty: Boolean(seed.title),
variables: seed.variables ?? "{}"