@gradio/client 2.4.0 → 2.5.1
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.
- package/CHANGELOG.md +12 -0
- package/dist/browser.js +586 -341
- package/dist/client.d.ts.map +1 -1
- package/dist/index.cjs +344 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +337 -3
- package/dist/index.min.js +588 -343
- package/dist/types.d.ts +14 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/run_history.d.ts +90 -0
- package/dist/utils/run_history.d.ts.map +1 -0
- package/dist/utils/submit.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +10 -0
- package/src/index.ts +14 -0
- package/src/test/run_history.test.ts +412 -0
- package/src/types.ts +14 -0
- package/src/utils/run_history.ts +589 -0
- package/src/utils/submit.ts +62 -2
|
@@ -0,0 +1,589 @@
|
|
|
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
|
+
/**
|
|
312
|
+
* The parts of an app config a replayed run writes back into. Kept structural
|
|
313
|
+
* so every entry point — the SPA and the SSR app each carry their own `Config`
|
|
314
|
+
* declaration — can hand its config straight over.
|
|
315
|
+
*/
|
|
316
|
+
export interface ReplayTarget {
|
|
317
|
+
components: { id: number; type: string; props: Record<string, any> }[];
|
|
318
|
+
dependencies: {
|
|
319
|
+
id: number;
|
|
320
|
+
api_name?: string | null;
|
|
321
|
+
inputs: number[];
|
|
322
|
+
outputs: number[];
|
|
323
|
+
}[];
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function restore_run_impl(config: ReplayTarget, run: StoredRun): boolean {
|
|
327
|
+
const dependency = config.dependencies.find(
|
|
328
|
+
(item) =>
|
|
329
|
+
item.id === run.fn_index ||
|
|
330
|
+
(typeof item.api_name === "string" &&
|
|
331
|
+
`/${item.api_name.replace(/^\//, "")}` === run.api_name)
|
|
332
|
+
);
|
|
333
|
+
if (!dependency) return false;
|
|
334
|
+
|
|
335
|
+
const inputs = Array.isArray(run.inputs)
|
|
336
|
+
? run.inputs
|
|
337
|
+
: Object.values((run.inputs ?? {}) as Record<string, unknown>);
|
|
338
|
+
const outputs = Array.isArray(run.outputs)
|
|
339
|
+
? run.outputs
|
|
340
|
+
: run.outputs === null || run.outputs === undefined
|
|
341
|
+
? []
|
|
342
|
+
: [run.outputs];
|
|
343
|
+
|
|
344
|
+
const restore = (ids: number[], saved: unknown[]): void => {
|
|
345
|
+
for (const [index, id] of ids.entries()) {
|
|
346
|
+
const component = config.components.find((item) => item.id === id);
|
|
347
|
+
if (!component || index >= saved.length) continue;
|
|
348
|
+
// `gr.State` is held on the server and always saved as null, so
|
|
349
|
+
// writing it back would wipe out the component's real default.
|
|
350
|
+
if (component.type === "state") continue;
|
|
351
|
+
component.props.value = saved[index];
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
restore(dependency.inputs, inputs);
|
|
356
|
+
restore(dependency.outputs, outputs);
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function apply_run_history_replay_impl(
|
|
361
|
+
config: ReplayTarget & RunHistoryScope
|
|
362
|
+
): boolean {
|
|
363
|
+
const run = consume_run_history_replay_impl(config);
|
|
364
|
+
return run ? restore_run_impl(config, run) : false;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function start_run_history_impl(options: StartRunOptions): string | null {
|
|
368
|
+
const key = storage_key(options);
|
|
369
|
+
if (!key) return null;
|
|
370
|
+
|
|
371
|
+
// A new app id means a new key, so clear out the ones left behind before
|
|
372
|
+
// adding to this one.
|
|
373
|
+
prune_apps(key);
|
|
374
|
+
|
|
375
|
+
const run: StoredRun = {
|
|
376
|
+
id: make_id(),
|
|
377
|
+
endpoint: options.endpoint,
|
|
378
|
+
api_name: options.api_name,
|
|
379
|
+
fn_index: options.fn_index,
|
|
380
|
+
page: `${window.location.pathname}${window.location.search}`,
|
|
381
|
+
inputs: clone_for_storage(options.inputs),
|
|
382
|
+
outputs: null,
|
|
383
|
+
...(options.input_components
|
|
384
|
+
? {
|
|
385
|
+
input_components: clone_for_storage(
|
|
386
|
+
options.input_components
|
|
387
|
+
) as (StoredRunComponent | null)[]
|
|
388
|
+
}
|
|
389
|
+
: {}),
|
|
390
|
+
...(options.output_components
|
|
391
|
+
? {
|
|
392
|
+
output_components: clone_for_storage(
|
|
393
|
+
options.output_components
|
|
394
|
+
) as (StoredRunComponent | null)[]
|
|
395
|
+
}
|
|
396
|
+
: {}),
|
|
397
|
+
status: "running",
|
|
398
|
+
started_at: new Date().toISOString()
|
|
399
|
+
};
|
|
400
|
+
write_run_history(options, [run, ...read_run_history_impl(options)]);
|
|
401
|
+
notify_run_history_change();
|
|
402
|
+
return run.id;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function update_run_inputs_impl(
|
|
406
|
+
scope: RunHistoryScope | null | undefined,
|
|
407
|
+
id: string | null,
|
|
408
|
+
inputs: unknown
|
|
409
|
+
): void {
|
|
410
|
+
if (!id) return;
|
|
411
|
+
|
|
412
|
+
const runs = read_run_history_impl(scope);
|
|
413
|
+
const run = runs.find((item) => item.id === id);
|
|
414
|
+
if (!run) return;
|
|
415
|
+
run.inputs = clone_for_storage(inputs);
|
|
416
|
+
write_run_history(scope, runs);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function update_run_history_impl(
|
|
420
|
+
scope: RunHistoryScope | null | undefined,
|
|
421
|
+
id: string | null,
|
|
422
|
+
event: GradioEvent
|
|
423
|
+
): void {
|
|
424
|
+
if (!id) return;
|
|
425
|
+
|
|
426
|
+
const runs = read_run_history_impl(scope);
|
|
427
|
+
const run = runs.find((item) => item.id === id);
|
|
428
|
+
if (!run) return;
|
|
429
|
+
|
|
430
|
+
let finished = false;
|
|
431
|
+
if (event.type === "data") {
|
|
432
|
+
run.outputs = clone_for_storage(event.data);
|
|
433
|
+
deferred_outputs.set(id, run.outputs);
|
|
434
|
+
} else if (
|
|
435
|
+
event.type === "status" &&
|
|
436
|
+
event.original_msg === "process_starts"
|
|
437
|
+
) {
|
|
438
|
+
mark_process_start(run, event.time);
|
|
439
|
+
} else if (
|
|
440
|
+
event.type === "status" &&
|
|
441
|
+
(event.stage === "generating" || event.stage === "streaming")
|
|
442
|
+
) {
|
|
443
|
+
run.streamed = true;
|
|
444
|
+
} else if (event.type === "status" && event.stage === "complete") {
|
|
445
|
+
run.status = "completed";
|
|
446
|
+
mark_complete(run, event);
|
|
447
|
+
finished = true;
|
|
448
|
+
} else if (event.type === "status" && event.stage === "error") {
|
|
449
|
+
run.status = "failed";
|
|
450
|
+
run.error =
|
|
451
|
+
typeof event.message === "string"
|
|
452
|
+
? event.message
|
|
453
|
+
: JSON.stringify(event.message || "Unknown error");
|
|
454
|
+
mark_complete(run, event);
|
|
455
|
+
finished = true;
|
|
456
|
+
} else {
|
|
457
|
+
// Logs, renders and progress updates change nothing worth saving, and
|
|
458
|
+
// writing serialises every run we hold.
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (finished) {
|
|
463
|
+
// Every call re-reads from storage, so a chunk that was held back above is
|
|
464
|
+
// missing from `run`. Put the last one back before this final write.
|
|
465
|
+
if (deferred_outputs.has(id)) run.outputs = deferred_outputs.get(id);
|
|
466
|
+
deferred_outputs.delete(id);
|
|
467
|
+
last_write.delete(id);
|
|
468
|
+
} else if (run.streamed) {
|
|
469
|
+
// A generator emits an event per chunk. Saving each one would stringify
|
|
470
|
+
// the whole history on the main thread over and over, so write at most
|
|
471
|
+
// once an interval and let the run's completion flush the rest.
|
|
472
|
+
const since = Date.now() - (last_write.get(id) ?? 0);
|
|
473
|
+
if (since < STREAM_WRITE_INTERVAL_MS) return;
|
|
474
|
+
last_write.set(id, Date.now());
|
|
475
|
+
deferred_outputs.delete(id);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
write_run_history(scope, runs);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const STREAM_WRITE_INTERVAL_MS = 500;
|
|
482
|
+
/** When each in-flight streamed run was last written. */
|
|
483
|
+
const last_write = new Map<string, number>();
|
|
484
|
+
/** The newest output of a streamed run that has not been written yet. */
|
|
485
|
+
const deferred_outputs = new Map<string, unknown>();
|
|
486
|
+
|
|
487
|
+
function mark_process_start(run: StoredRun, time: Date | undefined): void {
|
|
488
|
+
// The queue tells us when the function actually started, which lets us
|
|
489
|
+
// report a runtime that excludes however long the run sat in the queue.
|
|
490
|
+
if (run.process_started_at) return;
|
|
491
|
+
const started = time || new Date();
|
|
492
|
+
run.process_started_at = started.toISOString();
|
|
493
|
+
run.queued_ms = Math.max(0, started.getTime() - Date.parse(run.started_at));
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function mark_complete(run: StoredRun, event: StatusMessage): void {
|
|
497
|
+
const completed = event.time || new Date();
|
|
498
|
+
run.completed_at = completed.toISOString();
|
|
499
|
+
// `cache_duration` is how long the function took on the server, which the
|
|
500
|
+
// queue reports on every completed run (not just cached ones). A generator
|
|
501
|
+
// reports it per chunk though, so the final value covers only the last one
|
|
502
|
+
// and the elapsed time is the honest number for a streamed run.
|
|
503
|
+
const server_duration =
|
|
504
|
+
!run.streamed &&
|
|
505
|
+
typeof event.cache_duration === "number" &&
|
|
506
|
+
event.cache_duration >= 0
|
|
507
|
+
? event.cache_duration * 1000
|
|
508
|
+
: null;
|
|
509
|
+
run.duration_ms =
|
|
510
|
+
server_duration ??
|
|
511
|
+
Math.max(
|
|
512
|
+
0,
|
|
513
|
+
completed.getTime() - Date.parse(run.process_started_at || run.started_at)
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Public API. Each of these is a no-op if anything goes wrong.
|
|
518
|
+
|
|
519
|
+
export function read_run_history(
|
|
520
|
+
scope: RunHistoryScope | null | undefined
|
|
521
|
+
): StoredRun[] {
|
|
522
|
+
return safely(() => read_run_history_impl(scope), []);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
export function clear_run_history(
|
|
526
|
+
scope: RunHistoryScope | null | undefined
|
|
527
|
+
): void {
|
|
528
|
+
safely(() => clear_run_history_impl(scope), undefined);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
export function delete_run_history(
|
|
532
|
+
scope: RunHistoryScope | null | undefined,
|
|
533
|
+
id: string
|
|
534
|
+
): void {
|
|
535
|
+
safely(() => delete_run_history_impl(scope, id), undefined);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
export function stage_run_history_replay(
|
|
539
|
+
scope: RunHistoryScope | null | undefined,
|
|
540
|
+
run: StoredRun
|
|
541
|
+
): void {
|
|
542
|
+
safely(() => stage_run_history_replay_impl(scope, run), undefined);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
export function consume_run_history_replay(
|
|
546
|
+
scope: RunHistoryScope | null | undefined
|
|
547
|
+
): StoredRun | null {
|
|
548
|
+
return safely(() => consume_run_history_replay_impl(scope), null);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Applies the run staged by the history page, if this page load is the one it
|
|
553
|
+
* was staged for. Every entry point that renders an app has to call this, or
|
|
554
|
+
* "Load run" silently does nothing on that entry point.
|
|
555
|
+
*
|
|
556
|
+
* @returns whether a staged run was found and applied.
|
|
557
|
+
*/
|
|
558
|
+
export function apply_run_history_replay(
|
|
559
|
+
config: ReplayTarget & RunHistoryScope
|
|
560
|
+
): boolean {
|
|
561
|
+
return safely(() => apply_run_history_replay_impl(config), false);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
export function start_run_history(options: StartRunOptions): string | null {
|
|
565
|
+
return safely(() => start_run_history_impl(options), null);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
export function update_run_inputs(
|
|
569
|
+
scope: RunHistoryScope | null | undefined,
|
|
570
|
+
id: string | null,
|
|
571
|
+
inputs: unknown
|
|
572
|
+
): void {
|
|
573
|
+
safely(() => update_run_inputs_impl(scope, id, inputs), undefined);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
export function update_run_history(
|
|
577
|
+
scope: RunHistoryScope | null | undefined,
|
|
578
|
+
id: string | null,
|
|
579
|
+
event: GradioEvent
|
|
580
|
+
): void {
|
|
581
|
+
safely(() => update_run_history_impl(scope, id, event), undefined);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
export function on_run_history_change(listener: () => void): () => void {
|
|
585
|
+
return safely(
|
|
586
|
+
() => on_run_history_change_impl(() => safely(listener, undefined)),
|
|
587
|
+
() => {}
|
|
588
|
+
);
|
|
589
|
+
}
|
package/src/utils/submit.ts
CHANGED
|
@@ -29,6 +29,11 @@ import {
|
|
|
29
29
|
} from "../constants";
|
|
30
30
|
import { apply_diff_stream, close_stream } from "./stream";
|
|
31
31
|
import { Client } from "../client";
|
|
32
|
+
import {
|
|
33
|
+
start_run_history,
|
|
34
|
+
update_run_history,
|
|
35
|
+
update_run_inputs
|
|
36
|
+
} from "./run_history";
|
|
32
37
|
|
|
33
38
|
export function submit(
|
|
34
39
|
this: Client,
|
|
@@ -73,12 +78,65 @@ export function submit(
|
|
|
73
78
|
);
|
|
74
79
|
|
|
75
80
|
let resolved_data = map_data_to_params(data, endpoint_info);
|
|
76
|
-
|
|
77
|
-
let stream: EventSource | null;
|
|
78
81
|
let protocol = config.protocol ?? "ws";
|
|
79
82
|
if (protocol === "ws") {
|
|
80
83
|
throw new Error(WS_PROTOCOL_MSG);
|
|
81
84
|
}
|
|
85
|
+
const history_endpoint =
|
|
86
|
+
typeof dependency.api_name === "string"
|
|
87
|
+
? `/${dependency.api_name}`
|
|
88
|
+
: endpoint;
|
|
89
|
+
const history_api_name =
|
|
90
|
+
typeof dependency.api_name === "string"
|
|
91
|
+
? `/${dependency.api_name}`
|
|
92
|
+
: `Function ${fn_index}`;
|
|
93
|
+
// Kept index-aligned with the stored payloads (null for components we
|
|
94
|
+
// cannot resolve) so every saved value stays matched to its component.
|
|
95
|
+
const component_metadata = (id: number) => {
|
|
96
|
+
const component = config.components.find((item) => item.id === id);
|
|
97
|
+
if (!component) return null;
|
|
98
|
+
return {
|
|
99
|
+
type: component.type,
|
|
100
|
+
component_class_id: component.component_class_id,
|
|
101
|
+
props: component.props
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
// The run history covers the same endpoints the API page documents, which
|
|
105
|
+
// it selects with exactly this predicate (see `ApiDocs.svelte`). That also
|
|
106
|
+
// keeps out the dependencies Gradio wires up for itself, since example
|
|
107
|
+
// loading, flagging and clear buttons are all "undocumented" or "private"
|
|
108
|
+
// and some of them fire on page load. The trade is that a component whose
|
|
109
|
+
// UI submits through an undocumented dependency — `gr.ChatInterface` does
|
|
110
|
+
// — records nothing for its in-app use.
|
|
111
|
+
const is_documented_endpoint = dependency.api_visibility === "public";
|
|
112
|
+
// Either side may opt out: the app for everyone who uses it, and this
|
|
113
|
+
// caller for itself.
|
|
114
|
+
const history_enabled =
|
|
115
|
+
config.run_history !== false && this.options.record_history !== false;
|
|
116
|
+
const history_scope = { app_id: config.app_id, username: config.username };
|
|
117
|
+
const history_run_id =
|
|
118
|
+
!history_enabled || !is_documented_endpoint
|
|
119
|
+
? null
|
|
120
|
+
: start_run_history({
|
|
121
|
+
...history_scope,
|
|
122
|
+
endpoint: history_endpoint,
|
|
123
|
+
api_name: history_api_name,
|
|
124
|
+
fn_index,
|
|
125
|
+
// Aligned to `dependency.inputs` the same way the submitted payload
|
|
126
|
+
// is, so this placeholder matches the components until the uploaded
|
|
127
|
+
// files are swapped in by `update_run_inputs` below.
|
|
128
|
+
inputs: handle_payload(
|
|
129
|
+
resolved_data,
|
|
130
|
+
dependency,
|
|
131
|
+
config.components,
|
|
132
|
+
"input",
|
|
133
|
+
true
|
|
134
|
+
),
|
|
135
|
+
input_components: dependency.inputs.map(component_metadata),
|
|
136
|
+
output_components: dependency.outputs.map(component_metadata)
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
let stream: EventSource | null;
|
|
82
140
|
let event_id_final = "";
|
|
83
141
|
let event_id_cb: () => string = () => event_id_final;
|
|
84
142
|
|
|
@@ -103,6 +161,7 @@ export function submit(
|
|
|
103
161
|
|
|
104
162
|
// event subscription methods
|
|
105
163
|
function fire_event(event: GradioEvent): void {
|
|
164
|
+
update_run_history(history_scope, history_run_id, event);
|
|
106
165
|
if (all_events || events_to_publish[event.type]) {
|
|
107
166
|
push_event(event);
|
|
108
167
|
}
|
|
@@ -180,6 +239,7 @@ export function submit(
|
|
|
180
239
|
"input",
|
|
181
240
|
true
|
|
182
241
|
);
|
|
242
|
+
update_run_inputs(history_scope, history_run_id, input_data || []);
|
|
183
243
|
payload = {
|
|
184
244
|
data: input_data || [],
|
|
185
245
|
event_data,
|