@gradio/client 2.5.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Client for the durable run history in an HF Hub bucket
3
+ * (`/gradio_api/run-history/*`).
4
+ *
5
+ * Read-only by design. Records are written by the server from the run it
6
+ * actually executed — there is no push function here, and nothing in this
7
+ * module can create a record.
8
+ *
9
+ * Every call names the bucket it is talking about. The server holds no
10
+ * per-session binding, so two tabs (or two apps on one origin) cannot end up
11
+ * reading each other's history.
12
+ */
13
+
14
+ const BUCKET_ID_RE = /^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-][a-zA-Z0-9_./-]*$/;
15
+
16
+ export function is_valid_bucket_id(id: string): boolean {
17
+ if (!BUCKET_ID_RE.test(id)) return false;
18
+ return !id
19
+ .split("/")
20
+ .some((seg) => seg === "" || seg === "." || seg === "..");
21
+ }
22
+
23
+ /** Mirrors `HistoryRecord` in `gradio/history.py`. */
24
+ export interface HistoryRecord {
25
+ record_id: string;
26
+ endpoint: string;
27
+ inputs: unknown;
28
+ outputs: unknown;
29
+ started_at: string;
30
+ schema_version: number;
31
+ }
32
+
33
+ export interface HistoryResult<T> {
34
+ ok: boolean;
35
+ status: number;
36
+ data: T;
37
+ detail?: string;
38
+ }
39
+
40
+ function url(
41
+ root: string,
42
+ path: string,
43
+ params?: Record<string, string>
44
+ ): string {
45
+ const query = params ? `?${new URLSearchParams(params)}` : "";
46
+ const base = root.replace(/\/+$/, "");
47
+ if (base) return `${base}/gradio_api/run-history/${path}${query}`;
48
+ // No configured root. A leading slash would drop any mount subpath
49
+ // (`mount_gradio_app(app, path="/myapp")`), so resolve against the document
50
+ // base, which gradio sets via <base href> on the app shell.
51
+ if (typeof document !== "undefined" && document.baseURI) {
52
+ return new URL(
53
+ `gradio_api/run-history/${path}${query}`,
54
+ document.baseURI
55
+ ).toString();
56
+ }
57
+ return `gradio_api/run-history/${path}${query}`;
58
+ }
59
+
60
+ async function parse_error(res: Response): Promise<string> {
61
+ try {
62
+ const j = await res.json();
63
+ return typeof j?.detail === "string" ? j.detail : `${res.status}`;
64
+ } catch {
65
+ return `${res.status}`;
66
+ }
67
+ }
68
+
69
+ async function request<T>(
70
+ input: string,
71
+ init: RequestInit,
72
+ fallback: T,
73
+ pick: (body: any) => T
74
+ ): Promise<HistoryResult<T>> {
75
+ try {
76
+ const res = await fetch(input, { credentials: "include", ...init });
77
+ if (!res.ok) {
78
+ // An empty list and a failed request are different things: a 401 here
79
+ // means "sign in", not "no runs yet".
80
+ return {
81
+ ok: false,
82
+ status: res.status,
83
+ data: fallback,
84
+ detail: await parse_error(res)
85
+ };
86
+ }
87
+ return { ok: true, status: res.status, data: pick(await res.json()) };
88
+ } catch (e) {
89
+ return { ok: false, status: 0, data: fallback, detail: String(e) };
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Create the bucket if it does not exist and confirm it is writable. Stores
95
+ * nothing server-side — the caller keeps its own choice and names it on every
96
+ * later request.
97
+ */
98
+ export async function connect_bucket(
99
+ root: string,
100
+ bucket_id: string
101
+ ): Promise<HistoryResult<null>> {
102
+ if (!is_valid_bucket_id(bucket_id)) {
103
+ return {
104
+ ok: false,
105
+ status: 422,
106
+ data: null,
107
+ detail: "invalid bucket id"
108
+ };
109
+ }
110
+ return request(
111
+ url(root, "connect"),
112
+ {
113
+ method: "POST",
114
+ headers: { "Content-Type": "application/json" },
115
+ body: JSON.stringify({ bucket_id })
116
+ },
117
+ null,
118
+ () => null
119
+ );
120
+ }
121
+
122
+ export async function list_user_buckets(
123
+ root: string
124
+ ): Promise<HistoryResult<string[]>> {
125
+ return request<string[]>(url(root, "buckets"), {}, [], (b) =>
126
+ Array.isArray(b?.buckets) ? b.buckets : []
127
+ );
128
+ }
129
+
130
+ export async function list_bucket_records(
131
+ root: string,
132
+ bucket: string
133
+ ): Promise<HistoryResult<HistoryRecord[]>> {
134
+ return request(url(root, "records", { bucket }), {}, [], (b) =>
135
+ Array.isArray(b?.records) ? b.records : []
136
+ );
137
+ }
138
+
139
+ export function asset_url(
140
+ root: string,
141
+ bucket: string,
142
+ endpoint: string,
143
+ record_id: string,
144
+ filename: string
145
+ ): string {
146
+ return url(
147
+ root,
148
+ `records/${encodeURIComponent(endpoint)}/${encodeURIComponent(
149
+ record_id
150
+ )}/assets/${encodeURIComponent(filename)}`,
151
+ { bucket }
152
+ );
153
+ }
@@ -11,6 +11,7 @@ import type { GradioEvent, StatusMessage } from "../types";
11
11
  const KEY_ROOT = "gradio:run-history:";
12
12
  const STORAGE_PREFIX = `${KEY_ROOT}v2:`;
13
13
  const REPLAY_PREFIX = `${KEY_ROOT}replay:v2:`;
14
+ const DESTINATION_PREFIX = `${KEY_ROOT}destination:v1:`;
14
15
  const MAX_RUNS = 100;
15
16
  const MAX_APPS = 8;
16
17
 
@@ -26,6 +27,10 @@ export interface RunHistoryScope {
26
27
  username?: string | null;
27
28
  }
28
29
 
30
+ export type RunHistoryStorage =
31
+ | { type: "browser"; bucket_id?: string }
32
+ | { type: "bucket"; bucket_id: string };
33
+
29
34
  /**
30
35
  * Run history is a side effect of submitting, never the point of it, so no
31
36
  * failure in here may propagate into the caller and break the app. Every
@@ -115,6 +120,45 @@ function replay_key(scope: RunHistoryScope | null | undefined): string | null {
115
120
  return key ? key.replace(STORAGE_PREFIX, REPLAY_PREFIX) : null;
116
121
  }
117
122
 
123
+ function destination_key(
124
+ scope: RunHistoryScope | null | undefined
125
+ ): string | null {
126
+ const key = storage_key(scope);
127
+ return key ? key.replace(STORAGE_PREFIX, DESTINATION_PREFIX) : null;
128
+ }
129
+
130
+ function read_run_history_storage_impl(
131
+ scope: RunHistoryScope | null | undefined
132
+ ): RunHistoryStorage {
133
+ const key = destination_key(scope);
134
+ if (!key) return { type: "browser" };
135
+ try {
136
+ const value = JSON.parse(window.localStorage.getItem(key) || "null");
137
+ if (typeof value?.bucket_id === "string") {
138
+ return value.type === "bucket"
139
+ ? { type: "bucket", bucket_id: value.bucket_id }
140
+ : { type: "browser", bucket_id: value.bucket_id };
141
+ }
142
+ return { type: "browser" };
143
+ } catch {
144
+ return { type: "browser" };
145
+ }
146
+ }
147
+
148
+ function set_run_history_storage_impl(
149
+ scope: RunHistoryScope | null | undefined,
150
+ storage: RunHistoryStorage
151
+ ): void {
152
+ const key = destination_key(scope);
153
+ if (!key) return;
154
+ if (storage.type === "browser" && !storage.bucket_id) {
155
+ window.localStorage.removeItem(key);
156
+ } else {
157
+ window.localStorage.setItem(key, JSON.stringify(storage));
158
+ }
159
+ notify_run_history_change();
160
+ }
161
+
118
162
  /** When a run was most recently saved under a key, for deciding what to drop. */
119
163
  function last_saved_at(key: string): number {
120
164
  try {
@@ -140,7 +184,10 @@ function prune_apps(current_key: string): void {
140
184
  const stale = [
141
185
  // Keys written by an older layout can never be read again.
142
186
  ...keys.filter(
143
- (key) => !key.startsWith(STORAGE_PREFIX) && !key.startsWith(REPLAY_PREFIX)
187
+ (key) =>
188
+ !key.startsWith(STORAGE_PREFIX) &&
189
+ !key.startsWith(REPLAY_PREFIX) &&
190
+ !key.startsWith(DESTINATION_PREFIX)
144
191
  ),
145
192
  ...keys
146
193
  .filter((key) => key.startsWith(STORAGE_PREFIX) && key !== current_key)
@@ -308,7 +355,64 @@ function consume_run_history_replay_impl(
308
355
  }
309
356
  }
310
357
 
358
+ /**
359
+ * The parts of an app config a replayed run writes back into. Kept structural
360
+ * so every entry point — the SPA and the SSR app each carry their own `Config`
361
+ * declaration — can hand its config straight over.
362
+ */
363
+ export interface ReplayTarget {
364
+ components: { id: number; type: string; props: Record<string, any> }[];
365
+ dependencies: {
366
+ id: number;
367
+ api_name?: string | null;
368
+ inputs: number[];
369
+ outputs: number[];
370
+ }[];
371
+ }
372
+
373
+ function restore_run_impl(config: ReplayTarget, run: StoredRun): boolean {
374
+ const dependency = config.dependencies.find(
375
+ (item) =>
376
+ item.id === run.fn_index ||
377
+ (typeof item.api_name === "string" &&
378
+ `/${item.api_name.replace(/^\//, "")}` === run.api_name)
379
+ );
380
+ if (!dependency) return false;
381
+
382
+ const inputs = Array.isArray(run.inputs)
383
+ ? run.inputs
384
+ : Object.values((run.inputs ?? {}) as Record<string, unknown>);
385
+ const outputs = Array.isArray(run.outputs)
386
+ ? run.outputs
387
+ : run.outputs === null || run.outputs === undefined
388
+ ? []
389
+ : [run.outputs];
390
+
391
+ const restore = (ids: number[], saved: unknown[]): void => {
392
+ for (const [index, id] of ids.entries()) {
393
+ const component = config.components.find((item) => item.id === id);
394
+ if (!component || index >= saved.length) continue;
395
+ // `gr.State` is held on the server and always saved as null, so
396
+ // writing it back would wipe out the component's real default.
397
+ if (component.type === "state") continue;
398
+ component.props.value = saved[index];
399
+ }
400
+ };
401
+
402
+ restore(dependency.inputs, inputs);
403
+ restore(dependency.outputs, outputs);
404
+ return true;
405
+ }
406
+
407
+ function apply_run_history_replay_impl(
408
+ config: ReplayTarget & RunHistoryScope
409
+ ): boolean {
410
+ const run = consume_run_history_replay_impl(config);
411
+ return run ? restore_run_impl(config, run) : false;
412
+ }
413
+
311
414
  function start_run_history_impl(options: StartRunOptions): string | null {
415
+ if (read_run_history_storage_impl(options).type === "bucket") return null;
312
416
  const key = storage_key(options);
313
417
  if (!key) return null;
314
418
 
@@ -466,6 +570,21 @@ export function read_run_history(
466
570
  return safely(() => read_run_history_impl(scope), []);
467
571
  }
468
572
 
573
+ export function read_run_history_storage(
574
+ scope: RunHistoryScope | null | undefined
575
+ ): RunHistoryStorage {
576
+ return safely(() => read_run_history_storage_impl(scope), {
577
+ type: "browser"
578
+ });
579
+ }
580
+
581
+ export function set_run_history_storage(
582
+ scope: RunHistoryScope | null | undefined,
583
+ storage: RunHistoryStorage
584
+ ): void {
585
+ safely(() => set_run_history_storage_impl(scope, storage), undefined);
586
+ }
587
+
469
588
  export function clear_run_history(
470
589
  scope: RunHistoryScope | null | undefined
471
590
  ): void {
@@ -492,6 +611,19 @@ export function consume_run_history_replay(
492
611
  return safely(() => consume_run_history_replay_impl(scope), null);
493
612
  }
494
613
 
614
+ /**
615
+ * Applies the run staged by the history page, if this page load is the one it
616
+ * was staged for. Every entry point that renders an app has to call this, or
617
+ * "Load run" silently does nothing on that entry point.
618
+ *
619
+ * @returns whether a staged run was found and applied.
620
+ */
621
+ export function apply_run_history_replay(
622
+ config: ReplayTarget & RunHistoryScope
623
+ ): boolean {
624
+ return safely(() => apply_run_history_replay_impl(config), false);
625
+ }
626
+
495
627
  export function start_run_history(options: StartRunOptions): string | null {
496
628
  return safely(() => start_run_history_impl(options), null);
497
629
  }
@@ -30,6 +30,7 @@ import {
30
30
  import { apply_diff_stream, close_stream } from "./stream";
31
31
  import { Client } from "../client";
32
32
  import {
33
+ read_run_history_storage,
33
34
  start_run_history,
34
35
  update_run_history,
35
36
  update_run_inputs
@@ -63,7 +64,7 @@ export function submit(
63
64
  api_prefix
64
65
  } = this;
65
66
 
66
- const addt_headers = additional_headers || { "x-gradio-user": "api" };
67
+ const base_headers = additional_headers || { "x-gradio-user": "api" };
67
68
 
68
69
  const that = this;
69
70
 
@@ -114,6 +115,13 @@ export function submit(
114
115
  const history_enabled =
115
116
  config.run_history !== false && this.options.record_history !== false;
116
117
  const history_scope = { app_id: config.app_id, username: config.username };
118
+ const history_storage = read_run_history_storage(history_scope);
119
+ const addt_headers = {
120
+ ...base_headers,
121
+ ...(history_enabled && history_storage.type === "bucket"
122
+ ? { "x-gradio-history-bucket": history_storage.bucket_id }
123
+ : {})
124
+ };
117
125
  const history_run_id =
118
126
  !history_enabled || !is_documented_endpoint
119
127
  ? null
@@ -385,7 +393,8 @@ export function submit(
385
393
  ...payload,
386
394
  session_hash,
387
395
  event_id
388
- }
396
+ },
397
+ addt_headers
389
398
  );
390
399
  if (status !== 200) {
391
400
  fire_event({