diff options
Diffstat (limited to 'source/library/state/session.svelte.ts')
| -rw-r--r-- | source/library/state/session.svelte.ts | 142 |
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 ?? "{}" |