@gradio/client 2.3.1 → 2.5.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.
Files changed (48) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +4 -2
  3. package/dist/__vite-browser-external-Cgmn0awE.js +7 -0
  4. package/dist/browser.js +1949 -1963
  5. package/dist/client.d.ts +5 -0
  6. package/dist/client.d.ts.map +1 -1
  7. package/dist/constants.d.ts +4 -0
  8. package/dist/constants.d.ts.map +1 -1
  9. package/dist/helpers/api_info.d.ts.map +1 -1
  10. package/dist/helpers/data.d.ts.map +1 -1
  11. package/dist/helpers/init_helpers.d.ts +11 -2
  12. package/dist/helpers/init_helpers.d.ts.map +1 -1
  13. package/dist/index.cjs +2584 -0
  14. package/dist/index.d.ts +1 -0
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +2471 -2466
  17. package/dist/index.min.js +1962 -0
  18. package/dist/types.d.ts +29 -0
  19. package/dist/types.d.ts.map +1 -1
  20. package/dist/utils/duplicate.d.ts.map +1 -1
  21. package/dist/utils/predict.d.ts.map +1 -1
  22. package/dist/utils/run_history.d.ts +64 -0
  23. package/dist/utils/run_history.d.ts.map +1 -0
  24. package/dist/utils/stream.d.ts.map +1 -1
  25. package/dist/utils/submit.d.ts.map +1 -1
  26. package/package.json +11 -4
  27. package/scripts/verify_dist.mjs +46 -0
  28. package/src/client.ts +54 -5
  29. package/src/constants.ts +10 -0
  30. package/src/helpers/api_info.ts +46 -19
  31. package/src/helpers/data.ts +8 -3
  32. package/src/helpers/init_helpers.ts +54 -7
  33. package/src/index.ts +12 -0
  34. package/src/test/api_info.test.ts +77 -12
  35. package/src/test/data.test.ts +26 -3
  36. package/src/test/init.test.ts +21 -3
  37. package/src/test/init_helpers.test.ts +67 -1
  38. package/src/test/refresh.test.ts +31 -0
  39. package/src/test/run_history.test.ts +345 -0
  40. package/src/test/submit.test.ts +1 -3
  41. package/src/types.ts +29 -0
  42. package/src/utils/duplicate.ts +2 -0
  43. package/src/utils/predict.ts +14 -1
  44. package/src/utils/run_history.ts +520 -0
  45. package/src/utils/stream.ts +17 -4
  46. package/src/utils/submit.ts +105 -12
  47. package/vite.config.ts +20 -4
  48. package/dist/__vite-browser-external-DYxpcVy9.js +0 -4
@@ -0,0 +1,520 @@
1
+ import type { GradioEvent, StatusMessage } from "../types";
2
+
3
+ // Keyed on the app id, so two different apps served on the same port never
4
+ // share a history. The id is regenerated whenever an app starts, so a restart
5
+ // begins a fresh history and leaves the previous one orphaned; `prune_apps`
6
+ // keeps those from accumulating.
7
+ //
8
+ // The key also carries the logged-in user, because local storage outlives a
9
+ // session: without it, whoever logs into an authenticated app next on this
10
+ // browser would open the previous user's runs.
11
+ const KEY_ROOT = "gradio:run-history:";
12
+ const STORAGE_PREFIX = `${KEY_ROOT}v2:`;
13
+ const REPLAY_PREFIX = `${KEY_ROOT}replay:v2:`;
14
+ const MAX_RUNS = 100;
15
+ const MAX_APPS = 8;
16
+
17
+ export type AppId = string | number | null | undefined;
18
+
19
+ /**
20
+ * Which history to read or write. `Config` satisfies this shape, so callers
21
+ * that hold one can pass it straight through.
22
+ */
23
+ export interface RunHistoryScope {
24
+ app_id?: AppId;
25
+ /** The authenticated user, when the app uses `auth`. */
26
+ username?: string | null;
27
+ }
28
+
29
+ /**
30
+ * Run history is a side effect of submitting, never the point of it, so no
31
+ * failure in here may propagate into the caller and break the app. Every
32
+ * exported function routes through this.
33
+ */
34
+ function safely<T>(operation: () => T, fallback: T): T {
35
+ try {
36
+ return operation();
37
+ } catch (error) {
38
+ console.warn("Could not update the run history.", error);
39
+ return fallback;
40
+ }
41
+ }
42
+
43
+ export type RunStatus = "running" | "completed" | "failed";
44
+
45
+ export interface StoredRunComponent {
46
+ type: string;
47
+ component_class_id: string;
48
+ props: Record<string, unknown>;
49
+ }
50
+
51
+ export interface StoredRun {
52
+ id: string;
53
+ endpoint: string | number;
54
+ api_name: string;
55
+ fn_index: number;
56
+ page: string;
57
+ inputs: unknown;
58
+ outputs: unknown | null;
59
+ input_components?: (StoredRunComponent | null)[];
60
+ output_components?: (StoredRunComponent | null)[];
61
+ status: RunStatus;
62
+ error?: string;
63
+ started_at: string;
64
+ /** When the server started running the function, if it reported queueing. */
65
+ process_started_at?: string;
66
+ completed_at?: string;
67
+ /** How long the function itself took, server-reported where available. */
68
+ duration_ms?: number;
69
+ /** How long the run waited in the queue before the function started. */
70
+ queued_ms?: number;
71
+ /** Whether the run produced its output in chunks, i.e. came from a generator. */
72
+ streamed?: boolean;
73
+ }
74
+
75
+ interface StartRunOptions extends RunHistoryScope {
76
+ endpoint: string | number;
77
+ api_name: string;
78
+ fn_index: number;
79
+ inputs: unknown;
80
+ input_components?: (StoredRunComponent | null)[];
81
+ output_components?: (StoredRunComponent | null)[];
82
+ }
83
+
84
+ /**
85
+ * The URL of the run history page for an app. `root` never carries a trailing
86
+ * slash, so it cannot simply be concatenated with the path.
87
+ */
88
+ export function run_history_url(
89
+ root: string,
90
+ api_prefix = "/gradio_api"
91
+ ): string {
92
+ return `${root.replace(/\/+$/, "")}${api_prefix}/runs`;
93
+ }
94
+
95
+ function storage_key(scope: RunHistoryScope | null | undefined): string | null {
96
+ if (typeof window === "undefined") return null;
97
+ const app_id = scope?.app_id;
98
+ if (app_id === null || app_id === undefined || app_id === "") return null;
99
+
100
+ try {
101
+ if (!window.localStorage) return null;
102
+ // Encoded so that a username containing the separator cannot be made to
103
+ // collide with another user's key.
104
+ const user = scope?.username
105
+ ? `:user:${encodeURIComponent(scope.username)}`
106
+ : "";
107
+ return `${STORAGE_PREFIX}${app_id}${user}`;
108
+ } catch {
109
+ return null;
110
+ }
111
+ }
112
+
113
+ function replay_key(scope: RunHistoryScope | null | undefined): string | null {
114
+ const key = storage_key(scope);
115
+ return key ? key.replace(STORAGE_PREFIX, REPLAY_PREFIX) : null;
116
+ }
117
+
118
+ /** When a run was most recently saved under a key, for deciding what to drop. */
119
+ function last_saved_at(key: string): number {
120
+ try {
121
+ const runs = JSON.parse(window.localStorage.getItem(key) || "[]");
122
+ // Runs are stored newest first.
123
+ return Array.isArray(runs) && runs.length
124
+ ? Date.parse(runs[0]?.started_at) || 0
125
+ : 0;
126
+ } catch {
127
+ return 0;
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Drops the histories of long-gone app instances. Every restart mints a new app
133
+ * id, so without this the browser would keep every history an app ever had and
134
+ * eventually run out of room for the current one.
135
+ */
136
+ function prune_apps(current_key: string): void {
137
+ const keys = Object.keys(window.localStorage).filter((key) =>
138
+ key.startsWith(KEY_ROOT)
139
+ );
140
+ const stale = [
141
+ // Keys written by an older layout can never be read again.
142
+ ...keys.filter(
143
+ (key) => !key.startsWith(STORAGE_PREFIX) && !key.startsWith(REPLAY_PREFIX)
144
+ ),
145
+ ...keys
146
+ .filter((key) => key.startsWith(STORAGE_PREFIX) && key !== current_key)
147
+ .sort((a, b) => last_saved_at(b) - last_saved_at(a))
148
+ .slice(MAX_APPS - 1)
149
+ ];
150
+ for (const key of stale) {
151
+ try {
152
+ window.localStorage.removeItem(key);
153
+ window.sessionStorage.removeItem(
154
+ key.replace(STORAGE_PREFIX, REPLAY_PREFIX)
155
+ );
156
+ } catch {
157
+ // Nothing to do if the browser will not let us clean up.
158
+ }
159
+ }
160
+ }
161
+
162
+ function make_id(): string {
163
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
164
+ return crypto.randomUUID();
165
+ }
166
+ return `${Date.now()}-${Math.random().toString(36).slice(2)}`;
167
+ }
168
+
169
+ function clone_for_storage(value: unknown): unknown {
170
+ const seen = new WeakSet<object>();
171
+ try {
172
+ return JSON.parse(
173
+ JSON.stringify(value, (_key, item) => {
174
+ if (typeof item === "bigint") return item.toString();
175
+ if (typeof item === "object" && item !== null) {
176
+ if (seen.has(item)) return "[Circular]";
177
+ seen.add(item);
178
+ }
179
+ return item;
180
+ })
181
+ );
182
+ } catch {
183
+ return "[Unserializable value]";
184
+ }
185
+ }
186
+
187
+ function read_run_history_impl(
188
+ scope: RunHistoryScope | null | undefined
189
+ ): StoredRun[] {
190
+ const key = storage_key(scope);
191
+ if (!key) return [];
192
+
193
+ try {
194
+ const value = JSON.parse(window.localStorage.getItem(key) || "[]");
195
+ return Array.isArray(value) ? value : [];
196
+ } catch {
197
+ return [];
198
+ }
199
+ }
200
+
201
+ function write_run_history(
202
+ scope: RunHistoryScope | null | undefined,
203
+ runs: StoredRun[]
204
+ ): void {
205
+ const key = storage_key(scope);
206
+ if (!key) return;
207
+
208
+ let next = runs.slice(0, MAX_RUNS);
209
+ let pruned = false;
210
+ while (next.length > 0) {
211
+ try {
212
+ window.localStorage.setItem(key, JSON.stringify(next));
213
+ return;
214
+ } catch {
215
+ // Reclaim the space held by app instances that are long gone before
216
+ // giving up on the oldest runs of the current one.
217
+ if (!pruned) {
218
+ pruned = true;
219
+ prune_apps(key);
220
+ continue;
221
+ }
222
+ next = next.slice(0, -1);
223
+ }
224
+ }
225
+ try {
226
+ window.localStorage.removeItem(key);
227
+ } catch {
228
+ // Run history must never interfere with an app submission.
229
+ }
230
+ }
231
+
232
+ function clear_run_history_impl(
233
+ scope: RunHistoryScope | null | undefined
234
+ ): void {
235
+ const key = storage_key(scope);
236
+ if (!key) return;
237
+ try {
238
+ window.localStorage.removeItem(key);
239
+ } catch {
240
+ // Storage may be disabled by the browser.
241
+ }
242
+ notify_run_history_change();
243
+ }
244
+
245
+ function delete_run_history_impl(
246
+ scope: RunHistoryScope | null | undefined,
247
+ id: string
248
+ ): void {
249
+ write_run_history(
250
+ scope,
251
+ read_run_history_impl(scope).filter((run) => run.id !== id)
252
+ );
253
+ notify_run_history_change();
254
+ }
255
+
256
+ const CHANGE_EVENT = "gradio:run-history-change";
257
+
258
+ function notify_run_history_change(): void {
259
+ if (typeof window === "undefined") return;
260
+ try {
261
+ window.dispatchEvent(new Event(CHANGE_EVENT));
262
+ } catch {
263
+ // Nothing depends on the notification arriving.
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Subscribes to runs being added, deleted or cleared. `storage` covers other
269
+ * tabs; the custom event covers this one, which `storage` never fires for.
270
+ * Only counts change, not per-run progress, so listeners stay cheap.
271
+ *
272
+ * @returns a function that unsubscribes.
273
+ */
274
+ function on_run_history_change_impl(listener: () => void): () => void {
275
+ if (typeof window === "undefined") return () => {};
276
+ window.addEventListener(CHANGE_EVENT, listener);
277
+ window.addEventListener("storage", listener);
278
+ return () => {
279
+ window.removeEventListener(CHANGE_EVENT, listener);
280
+ window.removeEventListener("storage", listener);
281
+ };
282
+ }
283
+
284
+ function stage_run_history_replay_impl(
285
+ scope: RunHistoryScope | null | undefined,
286
+ run: StoredRun
287
+ ): void {
288
+ const key = replay_key(scope);
289
+ if (!key) return;
290
+ try {
291
+ window.sessionStorage.setItem(key, JSON.stringify(run));
292
+ } catch {
293
+ // Session storage may be disabled by the browser.
294
+ }
295
+ }
296
+
297
+ function consume_run_history_replay_impl(
298
+ scope: RunHistoryScope | null | undefined
299
+ ): StoredRun | null {
300
+ const key = replay_key(scope);
301
+ if (!key) return null;
302
+ try {
303
+ const value = window.sessionStorage.getItem(key);
304
+ window.sessionStorage.removeItem(key);
305
+ return value ? (JSON.parse(value) as StoredRun) : null;
306
+ } catch {
307
+ return null;
308
+ }
309
+ }
310
+
311
+ function start_run_history_impl(options: StartRunOptions): string | null {
312
+ const key = storage_key(options);
313
+ if (!key) return null;
314
+
315
+ // A new app id means a new key, so clear out the ones left behind before
316
+ // adding to this one.
317
+ prune_apps(key);
318
+
319
+ const run: StoredRun = {
320
+ id: make_id(),
321
+ endpoint: options.endpoint,
322
+ api_name: options.api_name,
323
+ fn_index: options.fn_index,
324
+ page: `${window.location.pathname}${window.location.search}`,
325
+ inputs: clone_for_storage(options.inputs),
326
+ outputs: null,
327
+ ...(options.input_components
328
+ ? {
329
+ input_components: clone_for_storage(
330
+ options.input_components
331
+ ) as (StoredRunComponent | null)[]
332
+ }
333
+ : {}),
334
+ ...(options.output_components
335
+ ? {
336
+ output_components: clone_for_storage(
337
+ options.output_components
338
+ ) as (StoredRunComponent | null)[]
339
+ }
340
+ : {}),
341
+ status: "running",
342
+ started_at: new Date().toISOString()
343
+ };
344
+ write_run_history(options, [run, ...read_run_history_impl(options)]);
345
+ notify_run_history_change();
346
+ return run.id;
347
+ }
348
+
349
+ function update_run_inputs_impl(
350
+ scope: RunHistoryScope | null | undefined,
351
+ id: string | null,
352
+ inputs: unknown
353
+ ): void {
354
+ if (!id) return;
355
+
356
+ const runs = read_run_history_impl(scope);
357
+ const run = runs.find((item) => item.id === id);
358
+ if (!run) return;
359
+ run.inputs = clone_for_storage(inputs);
360
+ write_run_history(scope, runs);
361
+ }
362
+
363
+ function update_run_history_impl(
364
+ scope: RunHistoryScope | null | undefined,
365
+ id: string | null,
366
+ event: GradioEvent
367
+ ): void {
368
+ if (!id) return;
369
+
370
+ const runs = read_run_history_impl(scope);
371
+ const run = runs.find((item) => item.id === id);
372
+ if (!run) return;
373
+
374
+ let finished = false;
375
+ if (event.type === "data") {
376
+ run.outputs = clone_for_storage(event.data);
377
+ deferred_outputs.set(id, run.outputs);
378
+ } else if (
379
+ event.type === "status" &&
380
+ event.original_msg === "process_starts"
381
+ ) {
382
+ mark_process_start(run, event.time);
383
+ } else if (
384
+ event.type === "status" &&
385
+ (event.stage === "generating" || event.stage === "streaming")
386
+ ) {
387
+ run.streamed = true;
388
+ } else if (event.type === "status" && event.stage === "complete") {
389
+ run.status = "completed";
390
+ mark_complete(run, event);
391
+ finished = true;
392
+ } else if (event.type === "status" && event.stage === "error") {
393
+ run.status = "failed";
394
+ run.error =
395
+ typeof event.message === "string"
396
+ ? event.message
397
+ : JSON.stringify(event.message || "Unknown error");
398
+ mark_complete(run, event);
399
+ finished = true;
400
+ } else {
401
+ // Logs, renders and progress updates change nothing worth saving, and
402
+ // writing serialises every run we hold.
403
+ return;
404
+ }
405
+
406
+ if (finished) {
407
+ // Every call re-reads from storage, so a chunk that was held back above is
408
+ // missing from `run`. Put the last one back before this final write.
409
+ if (deferred_outputs.has(id)) run.outputs = deferred_outputs.get(id);
410
+ deferred_outputs.delete(id);
411
+ last_write.delete(id);
412
+ } else if (run.streamed) {
413
+ // A generator emits an event per chunk. Saving each one would stringify
414
+ // the whole history on the main thread over and over, so write at most
415
+ // once an interval and let the run's completion flush the rest.
416
+ const since = Date.now() - (last_write.get(id) ?? 0);
417
+ if (since < STREAM_WRITE_INTERVAL_MS) return;
418
+ last_write.set(id, Date.now());
419
+ deferred_outputs.delete(id);
420
+ }
421
+
422
+ write_run_history(scope, runs);
423
+ }
424
+
425
+ const STREAM_WRITE_INTERVAL_MS = 500;
426
+ /** When each in-flight streamed run was last written. */
427
+ const last_write = new Map<string, number>();
428
+ /** The newest output of a streamed run that has not been written yet. */
429
+ const deferred_outputs = new Map<string, unknown>();
430
+
431
+ function mark_process_start(run: StoredRun, time: Date | undefined): void {
432
+ // The queue tells us when the function actually started, which lets us
433
+ // report a runtime that excludes however long the run sat in the queue.
434
+ if (run.process_started_at) return;
435
+ const started = time || new Date();
436
+ run.process_started_at = started.toISOString();
437
+ run.queued_ms = Math.max(0, started.getTime() - Date.parse(run.started_at));
438
+ }
439
+
440
+ function mark_complete(run: StoredRun, event: StatusMessage): void {
441
+ const completed = event.time || new Date();
442
+ run.completed_at = completed.toISOString();
443
+ // `cache_duration` is how long the function took on the server, which the
444
+ // queue reports on every completed run (not just cached ones). A generator
445
+ // reports it per chunk though, so the final value covers only the last one
446
+ // and the elapsed time is the honest number for a streamed run.
447
+ const server_duration =
448
+ !run.streamed &&
449
+ typeof event.cache_duration === "number" &&
450
+ event.cache_duration >= 0
451
+ ? event.cache_duration * 1000
452
+ : null;
453
+ run.duration_ms =
454
+ server_duration ??
455
+ Math.max(
456
+ 0,
457
+ completed.getTime() - Date.parse(run.process_started_at || run.started_at)
458
+ );
459
+ }
460
+
461
+ // Public API. Each of these is a no-op if anything goes wrong.
462
+
463
+ export function read_run_history(
464
+ scope: RunHistoryScope | null | undefined
465
+ ): StoredRun[] {
466
+ return safely(() => read_run_history_impl(scope), []);
467
+ }
468
+
469
+ export function clear_run_history(
470
+ scope: RunHistoryScope | null | undefined
471
+ ): void {
472
+ safely(() => clear_run_history_impl(scope), undefined);
473
+ }
474
+
475
+ export function delete_run_history(
476
+ scope: RunHistoryScope | null | undefined,
477
+ id: string
478
+ ): void {
479
+ safely(() => delete_run_history_impl(scope, id), undefined);
480
+ }
481
+
482
+ export function stage_run_history_replay(
483
+ scope: RunHistoryScope | null | undefined,
484
+ run: StoredRun
485
+ ): void {
486
+ safely(() => stage_run_history_replay_impl(scope, run), undefined);
487
+ }
488
+
489
+ export function consume_run_history_replay(
490
+ scope: RunHistoryScope | null | undefined
491
+ ): StoredRun | null {
492
+ return safely(() => consume_run_history_replay_impl(scope), null);
493
+ }
494
+
495
+ export function start_run_history(options: StartRunOptions): string | null {
496
+ return safely(() => start_run_history_impl(options), null);
497
+ }
498
+
499
+ export function update_run_inputs(
500
+ scope: RunHistoryScope | null | undefined,
501
+ id: string | null,
502
+ inputs: unknown
503
+ ): void {
504
+ safely(() => update_run_inputs_impl(scope, id, inputs), undefined);
505
+ }
506
+
507
+ export function update_run_history(
508
+ scope: RunHistoryScope | null | undefined,
509
+ id: string | null,
510
+ event: GradioEvent
511
+ ): void {
512
+ safely(() => update_run_history_impl(scope, id, event), undefined);
513
+ }
514
+
515
+ export function on_run_history_change(listener: () => void): () => void {
516
+ return safely(
517
+ () => on_run_history_change_impl(() => safely(listener, undefined)),
518
+ () => {}
519
+ );
520
+ }
@@ -62,9 +62,15 @@ export async function open_stream(this: Client): Promise<void> {
62
62
  }
63
63
  let fn: (data: any) => void = event_callbacks[event_id];
64
64
 
65
- if (typeof window !== "undefined" && typeof document !== "undefined") {
66
- // fn(_data); // need to do this to put the event on the end of the event loop, so the browser can refresh between callbacks and not freeze in case of quick generations. See
67
- setTimeout(fn, 0, _data); // need to do this to put the event on the end of the event loop, so the browser can refresh between callbacks and not freeze in case of quick generations. See https://github.com/gradio-app/gradio/pull/7055
65
+ if (
66
+ typeof window !== "undefined" &&
67
+ typeof document !== "undefined" &&
68
+ document.visibilityState !== "hidden"
69
+ ) {
70
+ // Put the event at the end of the event loop so the browser can refresh
71
+ // between callbacks and not freeze during quick generations. Hidden tabs
72
+ // throttle timers, so process those messages immediately instead.
73
+ setTimeout(fn, 0, _data); // See https://github.com/gradio-app/gradio/pull/7055
68
74
  } else {
69
75
  fn(_data);
70
76
  }
@@ -111,7 +117,14 @@ export function apply_diff_stream(
111
117
  });
112
118
  } else {
113
119
  data.data.forEach((value: any, i: number) => {
114
- let new_data = apply_diff(pending_diff_streams[event_id][i], value);
120
+ // A new output can appear mid-stream if the app was hot-reloaded
121
+ // while a generator was running; such outputs are diffed against
122
+ // null on the server, so start from null when we haven't seen it.
123
+ const prev =
124
+ i < pending_diff_streams[event_id].length
125
+ ? pending_diff_streams[event_id][i]
126
+ : null;
127
+ let new_data = apply_diff(prev, value);
115
128
  pending_diff_streams[event_id][i] = new_data;
116
129
  data.data[i] = new_data;
117
130
  });