@gradio/client 2.3.0 → 2.4.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 (46) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +4 -2
  3. package/dist/__vite-browser-external-Cgmn0awE.js +7 -0
  4. package/dist/browser.js +1672 -1902
  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 +12 -3
  12. package/dist/helpers/init_helpers.d.ts.map +1 -1
  13. package/dist/index.cjs +2274 -0
  14. package/dist/index.js +2155 -2449
  15. package/dist/index.min.js +1735 -0
  16. package/dist/types.d.ts +16 -0
  17. package/dist/types.d.ts.map +1 -1
  18. package/dist/utils/duplicate.d.ts.map +1 -1
  19. package/dist/utils/predict.d.ts.map +1 -1
  20. package/dist/utils/stream.d.ts.map +1 -1
  21. package/dist/utils/submit.d.ts.map +1 -1
  22. package/dist/utils/view_api.d.ts.map +1 -1
  23. package/package.json +11 -4
  24. package/scripts/verify_dist.mjs +46 -0
  25. package/src/client.ts +46 -7
  26. package/src/constants.ts +10 -0
  27. package/src/helpers/api_info.ts +47 -19
  28. package/src/helpers/data.ts +8 -3
  29. package/src/helpers/init_helpers.ts +61 -14
  30. package/src/test/api_info.test.ts +105 -12
  31. package/src/test/data.test.ts +26 -3
  32. package/src/test/init.test.ts +21 -3
  33. package/src/test/init_helpers.test.ts +94 -1
  34. package/src/test/post_data.test.ts +21 -0
  35. package/src/test/refresh.test.ts +31 -0
  36. package/src/test/submit.test.ts +14 -0
  37. package/src/types.ts +16 -0
  38. package/src/utils/duplicate.ts +5 -1
  39. package/src/utils/post_data.ts +1 -1
  40. package/src/utils/predict.ts +34 -19
  41. package/src/utils/stream.ts +17 -4
  42. package/src/utils/submit.ts +43 -10
  43. package/src/utils/upload_files.ts +1 -1
  44. package/src/utils/view_api.ts +2 -3
  45. package/vite.config.ts +20 -4
  46. package/dist/__vite-browser-external-DYxpcVy9.js +0 -4
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  INVALID_URL_MSG,
3
3
  QUEUE_FULL_MSG,
4
- SPACE_METADATA_ERROR_MSG
4
+ SPACE_NOT_FOUND_MSG
5
5
  } from "../constants";
6
6
  import { beforeAll, afterEach, afterAll, it, expect, describe } from "vitest";
7
7
  import {
@@ -10,7 +10,8 @@ import {
10
10
  get_type,
11
11
  process_endpoint,
12
12
  join_urls,
13
- map_data_to_params
13
+ map_data_to_params,
14
+ transform_api_info
14
15
  } from "../helpers/api_info";
15
16
  import { initialise_server } from "./server";
16
17
  import { transformed_api_info } from "./test_data";
@@ -184,6 +185,34 @@ describe("handle_message", () => {
184
185
  });
185
186
  });
186
187
 
188
+ it("should carry changed_state_ids in the streaming status when msg is 'process_streaming'", () => {
189
+ const data = {
190
+ msg: "process_streaming",
191
+ success: true,
192
+ code: 200,
193
+ time_limit: 30,
194
+ eta: 5,
195
+ progress_data: { current: 50, total: 100 },
196
+ output: { data: [1], changed_state_ids: [3] }
197
+ };
198
+ const last_status = "pending";
199
+ const result = handle_message(data, last_status);
200
+ expect(result).toEqual({
201
+ type: "streaming",
202
+ status: {
203
+ queue: true,
204
+ message: undefined,
205
+ stage: "streaming",
206
+ time_limit: 30,
207
+ code: 200,
208
+ progress_data: { current: 50, total: 100 },
209
+ changed_state_ids: [3],
210
+ eta: 5
211
+ },
212
+ data: { data: [1], changed_state_ids: [3] }
213
+ });
214
+ });
215
+
187
216
  it("should return type 'complete' with success status when msg is 'process_completed' and success is true", () => {
188
217
  const data = {
189
218
  msg: "process_completed",
@@ -443,19 +472,13 @@ describe("process_endpoint", () => {
443
472
  expect(result).toEqual(expected);
444
473
  });
445
474
 
446
- it("should throw an error when fetching space metadata fails", async () => {
475
+ it("should throw a clear error when the space does not exist or is private", async () => {
447
476
  const app_reference = "hmb/bye_world";
448
477
  const token = "hf_token";
449
478
 
450
- try {
451
- await process_endpoint(app_reference, token);
452
- } catch (error) {
453
- if (error instanceof Error) {
454
- expect(error.message).toEqual(SPACE_METADATA_ERROR_MSG);
455
- } else {
456
- expect.fail("Error should not be unknown.");
457
- }
458
- }
479
+ await expect(process_endpoint(app_reference, token)).rejects.toThrow(
480
+ SPACE_NOT_FOUND_MSG(app_reference, 404)
481
+ );
459
482
  });
460
483
 
461
484
  it("should return the correct data when app_reference is a valid space domain", async () => {
@@ -671,3 +694,73 @@ describe("map_data_params", () => {
671
694
  );
672
695
  });
673
696
  });
697
+
698
+ describe("transform_api_info", () => {
699
+ it("defaults parameters and returns to empty arrays when an endpoint entry is malformed", () => {
700
+ const api_info = {
701
+ named_endpoints: {
702
+ // missing `parameters` and `returns`, as returned by some legacy
703
+ // or misbehaving apps (see issue #10945)
704
+ "/predict": {}
705
+ },
706
+ unnamed_endpoints: {}
707
+ } as any;
708
+ const config = {
709
+ dependencies: [
710
+ {
711
+ id: 0,
712
+ api_name: "predict",
713
+ inputs: [1],
714
+ outputs: [2],
715
+ types: { generator: false, cancel: false }
716
+ }
717
+ ],
718
+ components: [
719
+ { id: 1, type: "textbox", props: {} },
720
+ { id: 2, type: "textbox", props: {} }
721
+ ]
722
+ } as any;
723
+
724
+ const result = transform_api_info(api_info, config, { predict: 0 });
725
+
726
+ expect(result.named_endpoints["/predict"].parameters).toEqual([]);
727
+ expect(result.named_endpoints["/predict"].returns).toEqual([]);
728
+ });
729
+
730
+ it("keeps oauth_token, which submit() needs to decide where a token may be sent", () => {
731
+ const api_info = {
732
+ named_endpoints: {
733
+ "/report": { parameters: [], returns: [], oauth_token: "optional" },
734
+ "/calculator": { parameters: [], returns: [] }
735
+ },
736
+ unnamed_endpoints: {}
737
+ } as any;
738
+ const config = {
739
+ dependencies: [
740
+ {
741
+ id: 0,
742
+ api_name: "report",
743
+ inputs: [],
744
+ outputs: [],
745
+ types: { generator: false, cancel: false }
746
+ },
747
+ {
748
+ id: 1,
749
+ api_name: "calculator",
750
+ inputs: [],
751
+ outputs: [],
752
+ types: { generator: false, cancel: false }
753
+ }
754
+ ],
755
+ components: []
756
+ } as any;
757
+
758
+ const result = transform_api_info(api_info, config, {
759
+ report: 0,
760
+ calculator: 1
761
+ });
762
+
763
+ expect(result.named_endpoints["/report"].oauth_token).toBe("optional");
764
+ expect(result.named_endpoints["/calculator"].oauth_token).toBeUndefined();
765
+ });
766
+ });
@@ -97,6 +97,27 @@ describe("walk_and_store_blobs", () => {
97
97
  expect(parts[0].path).toEqual(["0"]);
98
98
  });
99
99
 
100
+ it("should preserve File instances (and their filenames)", async () => {
101
+ const file = new File(["test data"], "report.txt", {
102
+ type: "text/plain"
103
+ });
104
+ const parts = await walk_and_store_blobs([file]);
105
+
106
+ expect(parts).toHaveLength(1);
107
+ expect(parts[0].blob).toBe(file);
108
+ expect((parts[0].blob as File).name).toBe("report.txt");
109
+ expect((parts[0].blob as File).type).toBe("text/plain");
110
+ });
111
+
112
+ it("should preserve the MIME type of Blob instances", async () => {
113
+ const blob = new Blob(["test data"], { type: "image/png" });
114
+ const parts = await walk_and_store_blobs([blob]);
115
+
116
+ expect(parts).toHaveLength(1);
117
+ expect(parts[0].blob).toBe(blob);
118
+ expect(parts[0].blob && parts[0].blob.type).toBe("image/png");
119
+ });
120
+
100
121
  it("should handle deep structures", async () => {
101
122
  const image = new Blob([]);
102
123
  const parts = await walk_and_store_blobs({ a: { b: { data: { image } } } });
@@ -371,11 +392,13 @@ describe("handle_file", () => {
371
392
  );
372
393
 
373
394
  it.skipIf(IS_NODE)(
374
- "should handle a File object and return it as FileData",
395
+ "should handle a File object and return it unchanged, preserving its name and type",
375
396
  () => {
376
397
  const file = new File(["test image"], "test.png", { type: "image/png" });
377
- const result = handle_file(file) as FileData;
378
- expect(result).toBeInstanceOf(Blob);
398
+ const result = handle_file(file) as File;
399
+ expect(result).toBe(file);
400
+ expect(result.name).toBe("test.png");
401
+ expect(result.type).toBe("image/png");
379
402
  }
380
403
  );
381
404
 
@@ -15,7 +15,7 @@ import {
15
15
  response_api_info
16
16
  } from "./test_data";
17
17
  import { initialise_server } from "./server";
18
- import { SPACE_METADATA_ERROR_MSG } from "../constants";
18
+ import { SPACE_NOT_FOUND_MSG } from "../constants";
19
19
 
20
20
  const app_reference = "hmb/hello_world";
21
21
  const broken_app_reference = "hmb/bye_world";
@@ -87,12 +87,28 @@ describe("Client class", () => {
87
87
  });
88
88
  });
89
89
 
90
+ test("connecting successfully to a private running app with the deprecated hf_token option", async () => {
91
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
92
+ const app = await Client.connect("hmb/secret_world", {
93
+ hf_token: "hf_123"
94
+ });
95
+
96
+ expect(app.config).toEqual({
97
+ ...config_response,
98
+ root: "https://hmb-secret-world.hf.space"
99
+ });
100
+ expect(warn).toHaveBeenCalledWith(
101
+ expect.stringContaining("`hf_token` option has been renamed")
102
+ );
103
+ warn.mockRestore();
104
+ });
105
+
90
106
  test("unsuccessfully attempting to connect to a private running app", async () => {
91
107
  await expect(
92
108
  Client.connect("hmb/secret_world", {
93
109
  token: "hf_bad_token"
94
110
  })
95
- ).rejects.toThrowError(SPACE_METADATA_ERROR_MSG);
111
+ ).rejects.toThrowError(SPACE_NOT_FOUND_MSG("hmb/secret_world", 401));
96
112
  });
97
113
 
98
114
  test("viewing the api info of a running app", async () => {
@@ -139,7 +155,9 @@ describe("Client class", () => {
139
155
  test("creating a duplicate of a broken app", async () => {
140
156
  const duplicate = Client.duplicate(broken_app_reference);
141
157
 
142
- await expect(duplicate).rejects.toThrow(SPACE_METADATA_ERROR_MSG);
158
+ await expect(duplicate).rejects.toThrow(
159
+ SPACE_NOT_FOUND_MSG(broken_app_reference, 404)
160
+ );
143
161
  });
144
162
  });
145
163
 
@@ -2,12 +2,15 @@ import {
2
2
  resolve_root,
3
3
  get_jwt,
4
4
  determine_protocol,
5
- parse_and_set_cookies
5
+ parse_and_set_cookies,
6
+ resolve_config,
7
+ resolve_config_root
6
8
  } from "../helpers/init_helpers";
7
9
  import { initialise_server } from "./server";
8
10
  import { beforeAll, afterEach, afterAll, it, expect, describe } from "vitest";
9
11
  import { Client } from "../client";
10
12
  import { INVALID_CREDENTIALS_MSG, MISSING_CREDENTIALS_MSG } from "../constants";
13
+ import { config_response } from "./test_data";
11
14
 
12
15
  let server: Awaited<ReturnType<typeof initialise_server>>;
13
16
 
@@ -18,6 +21,96 @@ beforeAll(async () => {
18
21
  afterEach(() => server.resetHandlers());
19
22
  afterAll(() => server.stop());
20
23
 
24
+ describe("resolve_config", () => {
25
+ it("uses the public protocol and port for a same-host proxy root", () => {
26
+ expect(
27
+ resolve_config_root(
28
+ "http://machine.local:7862/gradio",
29
+ "https://machine.local:20443/gradio"
30
+ )
31
+ ).toBe("https://machine.local:20443/gradio");
32
+ });
33
+
34
+ it("keeps the configured origin for a remote root", () => {
35
+ expect(
36
+ resolve_config_root(
37
+ "https://remote.example/gradio",
38
+ "https://host.example/page"
39
+ )
40
+ ).toBe("https://remote.example/gradio");
41
+ });
42
+
43
+ it("requests /config without a Content-Type header and with same-origin credentials, so the cross-origin embed fetch is not blocked by CORS", async () => {
44
+ let captured_init: RequestInit | undefined;
45
+ const fake_client = {
46
+ options: {},
47
+ deep_link: null,
48
+ fetch: (_url: string, init: RequestInit) => {
49
+ captured_init = init;
50
+ return Promise.resolve(
51
+ new Response(JSON.stringify(config_response), { status: 200 })
52
+ );
53
+ }
54
+ } as unknown as Client;
55
+
56
+ await resolve_config.call(fake_client, "https://hmb-hello-world.hf.space");
57
+
58
+ expect(captured_init).toBeDefined();
59
+ const header_names = Object.keys(
60
+ captured_init?.headers as Record<string, string>
61
+ ).map((h) => h.toLowerCase());
62
+ expect(header_names).not.toContain("content-type");
63
+ expect(captured_init?.credentials).toBe("same-origin");
64
+ });
65
+
66
+ const in_browser = typeof window !== "undefined";
67
+
68
+ it.skipIf(!in_browser)(
69
+ "uses the browser origin for a same-host config root behind a proxy",
70
+ async () => {
71
+ const page = new URL(window.location.href);
72
+ const internal_root = `${page.protocol}//${page.hostname}:7862/gradio`;
73
+ window.gradio_config = {
74
+ ...config_response,
75
+ root: internal_root
76
+ };
77
+ const fake_client = {
78
+ options: {},
79
+ deep_link: null
80
+ } as unknown as Client;
81
+
82
+ try {
83
+ const config = await resolve_config.call(fake_client, internal_root);
84
+ expect(config?.root).toBe(`${page.origin}/gradio`);
85
+ } finally {
86
+ delete (window as Partial<Window>).gradio_config;
87
+ }
88
+ }
89
+ );
90
+
91
+ it.skipIf(!in_browser)(
92
+ "keeps a remote config root when the page and backend hostnames differ",
93
+ async () => {
94
+ const remote_root = "https://remote.example/gradio";
95
+ window.gradio_config = {
96
+ ...config_response,
97
+ root: remote_root
98
+ };
99
+ const fake_client = {
100
+ options: {},
101
+ deep_link: null
102
+ } as unknown as Client;
103
+
104
+ try {
105
+ const config = await resolve_config.call(fake_client, remote_root);
106
+ expect(config?.root).toBe(remote_root);
107
+ } finally {
108
+ delete (window as Partial<Window>).gradio_config;
109
+ }
110
+ }
111
+ );
112
+ });
113
+
21
114
  describe("resolve_root", () => {
22
115
  it('should return the base URL if the root path starts with "http://"', () => {
23
116
  const base_url = "https://huggingface.co";
@@ -1,4 +1,5 @@
1
1
  import { Client } from "../client";
2
+ import { post_data } from "../utils/post_data";
2
3
 
3
4
  import { initialise_server } from "./server";
4
5
  import { BROKEN_CONNECTION_MSG } from "../constants";
@@ -46,4 +47,24 @@ describe("post_data", () => {
46
47
  expect(response).toEqual(BROKEN_CONNECTION_MSG);
47
48
  expect(status).toBe(500);
48
49
  });
50
+
51
+ it("honors the credentials client option, so authenticated cross-origin deployments can opt back into cookies", async () => {
52
+ let captured_init: RequestInit | undefined;
53
+ const fake_client = {
54
+ options: { credentials: "include" },
55
+ fetch: (_url: string, init: RequestInit) => {
56
+ captured_init = init;
57
+ return Promise.resolve(new Response("{}", { status: 200 }));
58
+ }
59
+ } as unknown as Client;
60
+
61
+ const [, status] = await post_data.call(
62
+ fake_client,
63
+ "https://hmb-hello-world.hf.space/gradio_api/queue/join",
64
+ { data: "test" }
65
+ );
66
+
67
+ expect(status).toBe(200);
68
+ expect(captured_init?.credentials).toBe("include");
69
+ });
49
70
  });
@@ -0,0 +1,31 @@
1
+ import { describe, expect, test, vi } from "vitest";
2
+ import { Client } from "../client";
3
+ import type { Config } from "../types";
4
+
5
+ describe("Client.refresh", () => {
6
+ test("refreshes from the app root without stripping the current page again", async () => {
7
+ const client = new Client("https://example.test/app/page");
8
+ client.config = {
9
+ root: "https://example.test/app",
10
+ dependencies: []
11
+ } as unknown as Config;
12
+
13
+ const refreshed_config = {
14
+ root: "https://example.test/app",
15
+ api_prefix: "/gradio_api",
16
+ dependencies: [{ id: 4, api_name: "generate" }]
17
+ } as unknown as Config;
18
+ const resolve_config = vi.fn().mockResolvedValue(refreshed_config);
19
+ Reflect.set(client, "resolve_config", resolve_config);
20
+ client.view_api = vi.fn().mockResolvedValue({});
21
+ client.get_url_config = vi.fn().mockReturnValue(refreshed_config);
22
+
23
+ await client.refresh();
24
+
25
+ expect(resolve_config).toHaveBeenCalledWith(
26
+ "https://example.test/app",
27
+ false
28
+ );
29
+ expect(client.api_map).toEqual({ generate: 4 });
30
+ });
31
+ });
@@ -151,3 +151,17 @@ describe("submit iterator", () => {
151
151
  expect(error_event).toBeDefined();
152
152
  });
153
153
  });
154
+
155
+ describe("predict error handling", () => {
156
+ test("predict() rejects its returned promise when the endpoint does not exist, so the error is catchable", async () => {
157
+ const app = await Client.connect("hmb/hello_world");
158
+
159
+ await expect(
160
+ race_with_timeout(
161
+ app.predict("nonexistent_endpoint", ["hi"]),
162
+ 1000,
163
+ "predict() never settled for an unknown endpoint"
164
+ )
165
+ ).rejects.toThrow('No endpoint matching "nonexistent_endpoint" was found');
166
+ });
167
+ });
package/src/types.ts CHANGED
@@ -36,6 +36,8 @@ export interface EndpointInfo<T extends ApiData | JsApiData> {
36
36
  parameters: T[];
37
37
  returns: T[];
38
38
  type?: DependencyTypes;
39
+ /** Set when the endpoint's function takes a `gr.OAuthToken`. */
40
+ oauth_token?: "required" | "optional";
39
41
  }
40
42
 
41
43
  export interface ApiInfo<T extends ApiData | JsApiData> {
@@ -301,6 +303,7 @@ export interface Payload {
301
303
  time?: Date;
302
304
  event_data?: unknown;
303
305
  trigger_id?: number | null;
306
+ oauth_token?: string;
304
307
  }
305
308
 
306
309
  export interface PostResponse {
@@ -323,6 +326,11 @@ export interface DuplicateOptions extends ClientOptions {
323
326
 
324
327
  export interface ClientOptions {
325
328
  token?: `hf_${string}`;
329
+ /**
330
+ * @deprecated Use `token` instead. Kept as an alias so that code written
331
+ * for older versions of the client keeps working.
332
+ */
333
+ hf_token?: `hf_${string}`;
326
334
  status_callback?: SpaceStatusCallback | null;
327
335
  auth?: [string, string] | null;
328
336
  with_null_state?: boolean;
@@ -331,6 +339,14 @@ export interface ClientOptions {
331
339
  query_params?: Record<string, string>;
332
340
  session_hash?: string;
333
341
  cookies?: string;
342
+ credentials?: RequestCredentials;
343
+ /**
344
+ * A Hugging Face token passed to the app's own code, for endpoints whose
345
+ * function takes a `gr.OAuthToken`. Unlike `token`, which only authenticates
346
+ * you to the app, this lets the app act on your behalf, so it is sent only to
347
+ * endpoints that declare they need it.
348
+ */
349
+ oauth_token?: string;
334
350
  }
335
351
 
336
352
  export interface FileData {
@@ -8,6 +8,7 @@ import { Client } from "../client";
8
8
  import { SPACE_METADATA_ERROR_MSG } from "../constants";
9
9
  import {
10
10
  get_cookie_header,
11
+ normalise_token_option,
11
12
  parse_and_set_cookies
12
13
  } from "../helpers/init_helpers";
13
14
  import { process_endpoint } from "../helpers/api_info";
@@ -16,6 +17,7 @@ export async function duplicate(
16
17
  app_reference: string,
17
18
  options: DuplicateOptions
18
19
  ): Promise<Client> {
20
+ normalise_token_option(options);
19
21
  const { token, private: _private, hardware, timeout, auth } = options;
20
22
 
21
23
  if (hardware && !hardware_types.includes(hardware)) {
@@ -35,7 +37,9 @@ export async function duplicate(
35
37
  http_protocol,
36
38
  host,
37
39
  auth,
38
- fetch
40
+ fetch,
41
+ undefined,
42
+ options.credentials
39
43
  );
40
44
 
41
45
  if (cookie_header) cookies = parse_and_set_cookies(cookie_header);
@@ -20,7 +20,7 @@ export async function post_data(
20
20
  method: "POST",
21
21
  body: JSON.stringify(body),
22
22
  headers: { ...headers, ...additional_headers },
23
- credentials: "include"
23
+ credentials: this.options.credentials ?? "same-origin"
24
24
  });
25
25
  } catch (e) {
26
26
  return [{ error: BROKEN_CONNECTION_MSG }, 500];
@@ -23,29 +23,44 @@ export async function predict<T = unknown>(
23
23
  )!;
24
24
  }
25
25
 
26
- return new Promise(async (resolve, reject) => {
27
- const app = this.submit(endpoint, data, null, null, true);
28
- let result: unknown;
26
+ const app = this.submit(endpoint, data, null, null, true);
27
+ let result: unknown;
29
28
 
30
- for await (const message of app) {
31
- if (message.type === "data") {
32
- if (status_complete) {
33
- resolve(result as PredictReturn<T>);
34
- }
35
- data_returned = true;
36
- result = message;
29
+ for await (const message of app) {
30
+ if (message.type === "data") {
31
+ data_returned = true;
32
+ result = message;
33
+ if (status_complete) {
34
+ return result as PredictReturn<T>;
37
35
  }
36
+ }
38
37
 
39
- if (message.type === "status") {
40
- if (message.stage === "error") reject(message);
41
- if (message.stage === "complete") {
42
- status_complete = true;
43
- // if complete message comes after data, resolve here
44
- if (data_returned) {
45
- resolve(result as PredictReturn<T>);
46
- }
38
+ if (message.type === "status") {
39
+ if (message.stage === "error") {
40
+ // Throw a real `Error` (rather than the raw status object) so that
41
+ // uncaught failures surface a readable message instead of
42
+ // crashing Node with `ERR_UNHANDLED_REJECTION ... reason "#<Object>"`.
43
+ // The status fields are preserved on the error for callers that
44
+ // inspect them.
45
+ const { message: error_message, ...status } = message;
46
+ const error = new Error(
47
+ (typeof error_message === "string"
48
+ ? error_message
49
+ : error_message && JSON.stringify(error_message)) ||
50
+ "An unknown error occurred while making a prediction."
51
+ );
52
+ Object.assign(error, status);
53
+ throw error;
54
+ }
55
+ if (message.stage === "complete") {
56
+ status_complete = true;
57
+ // if complete message comes after data, resolve here
58
+ if (data_returned) {
59
+ return result as PredictReturn<T>;
47
60
  }
48
61
  }
49
62
  }
50
- });
63
+ }
64
+
65
+ return result as PredictReturn<T>;
51
66
  }
@@ -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
  });