@gradio/client 2.3.1 → 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.
- package/CHANGELOG.md +15 -0
- package/README.md +4 -2
- package/dist/__vite-browser-external-Cgmn0awE.js +7 -0
- package/dist/browser.js +1672 -1913
- package/dist/client.d.ts +5 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/constants.d.ts +4 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/helpers/api_info.d.ts.map +1 -1
- package/dist/helpers/data.d.ts.map +1 -1
- package/dist/helpers/init_helpers.d.ts +11 -2
- package/dist/helpers/init_helpers.d.ts.map +1 -1
- package/dist/index.cjs +2274 -0
- package/dist/index.js +2154 -2452
- package/dist/index.min.js +1735 -0
- package/dist/types.d.ts +15 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/duplicate.d.ts.map +1 -1
- package/dist/utils/predict.d.ts.map +1 -1
- package/dist/utils/stream.d.ts.map +1 -1
- package/dist/utils/submit.d.ts.map +1 -1
- package/package.json +11 -4
- package/scripts/verify_dist.mjs +46 -0
- package/src/client.ts +44 -5
- package/src/constants.ts +10 -0
- package/src/helpers/api_info.ts +46 -19
- package/src/helpers/data.ts +8 -3
- package/src/helpers/init_helpers.ts +54 -7
- package/src/test/api_info.test.ts +77 -12
- package/src/test/data.test.ts +26 -3
- package/src/test/init.test.ts +21 -3
- package/src/test/init_helpers.test.ts +67 -1
- package/src/test/refresh.test.ts +31 -0
- package/src/test/submit.test.ts +1 -3
- package/src/types.ts +15 -0
- package/src/utils/duplicate.ts +2 -0
- package/src/utils/predict.ts +14 -1
- package/src/utils/stream.ts +17 -4
- package/src/utils/submit.ts +43 -10
- package/vite.config.ts +20 -4
- 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
|
-
|
|
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";
|
|
@@ -471,19 +472,13 @@ describe("process_endpoint", () => {
|
|
|
471
472
|
expect(result).toEqual(expected);
|
|
472
473
|
});
|
|
473
474
|
|
|
474
|
-
it("should throw
|
|
475
|
+
it("should throw a clear error when the space does not exist or is private", async () => {
|
|
475
476
|
const app_reference = "hmb/bye_world";
|
|
476
477
|
const token = "hf_token";
|
|
477
478
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
if (error instanceof Error) {
|
|
482
|
-
expect(error.message).toEqual(SPACE_METADATA_ERROR_MSG);
|
|
483
|
-
} else {
|
|
484
|
-
expect.fail("Error should not be unknown.");
|
|
485
|
-
}
|
|
486
|
-
}
|
|
479
|
+
await expect(process_endpoint(app_reference, token)).rejects.toThrow(
|
|
480
|
+
SPACE_NOT_FOUND_MSG(app_reference, 404)
|
|
481
|
+
);
|
|
487
482
|
});
|
|
488
483
|
|
|
489
484
|
it("should return the correct data when app_reference is a valid space domain", async () => {
|
|
@@ -699,3 +694,73 @@ describe("map_data_params", () => {
|
|
|
699
694
|
);
|
|
700
695
|
});
|
|
701
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
|
+
});
|
package/src/test/data.test.ts
CHANGED
|
@@ -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
|
|
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
|
|
378
|
-
expect(result).
|
|
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
|
|
package/src/test/init.test.ts
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
response_api_info
|
|
16
16
|
} from "./test_data";
|
|
17
17
|
import { initialise_server } from "./server";
|
|
18
|
-
import {
|
|
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(
|
|
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(
|
|
158
|
+
await expect(duplicate).rejects.toThrow(
|
|
159
|
+
SPACE_NOT_FOUND_MSG(broken_app_reference, 404)
|
|
160
|
+
);
|
|
143
161
|
});
|
|
144
162
|
});
|
|
145
163
|
|
|
@@ -3,7 +3,8 @@ import {
|
|
|
3
3
|
get_jwt,
|
|
4
4
|
determine_protocol,
|
|
5
5
|
parse_and_set_cookies,
|
|
6
|
-
resolve_config
|
|
6
|
+
resolve_config,
|
|
7
|
+
resolve_config_root
|
|
7
8
|
} from "../helpers/init_helpers";
|
|
8
9
|
import { initialise_server } from "./server";
|
|
9
10
|
import { beforeAll, afterEach, afterAll, it, expect, describe } from "vitest";
|
|
@@ -21,6 +22,24 @@ afterEach(() => server.resetHandlers());
|
|
|
21
22
|
afterAll(() => server.stop());
|
|
22
23
|
|
|
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
|
+
|
|
24
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 () => {
|
|
25
44
|
let captured_init: RequestInit | undefined;
|
|
26
45
|
const fake_client = {
|
|
@@ -43,6 +62,53 @@ describe("resolve_config", () => {
|
|
|
43
62
|
expect(header_names).not.toContain("content-type");
|
|
44
63
|
expect(captured_init?.credentials).toBe("same-origin");
|
|
45
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
|
+
);
|
|
46
112
|
});
|
|
47
113
|
|
|
48
114
|
describe("resolve_root", () => {
|
|
@@ -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
|
+
});
|
package/src/test/submit.test.ts
CHANGED
|
@@ -162,8 +162,6 @@ describe("predict error handling", () => {
|
|
|
162
162
|
1000,
|
|
163
163
|
"predict() never settled for an unknown endpoint"
|
|
164
164
|
)
|
|
165
|
-
).rejects.toThrow(
|
|
166
|
-
"There is no endpoint matching that name of fn_index matching that number."
|
|
167
|
-
);
|
|
165
|
+
).rejects.toThrow('No endpoint matching "nonexistent_endpoint" was found');
|
|
168
166
|
});
|
|
169
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;
|
|
@@ -332,6 +340,13 @@ export interface ClientOptions {
|
|
|
332
340
|
session_hash?: string;
|
|
333
341
|
cookies?: string;
|
|
334
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;
|
|
335
350
|
}
|
|
336
351
|
|
|
337
352
|
export interface FileData {
|
package/src/utils/duplicate.ts
CHANGED
|
@@ -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)) {
|
package/src/utils/predict.ts
CHANGED
|
@@ -37,7 +37,20 @@ export async function predict<T = unknown>(
|
|
|
37
37
|
|
|
38
38
|
if (message.type === "status") {
|
|
39
39
|
if (message.stage === "error") {
|
|
40
|
-
|
|
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;
|
|
41
54
|
}
|
|
42
55
|
if (message.stage === "complete") {
|
|
43
56
|
status_complete = true;
|
package/src/utils/stream.ts
CHANGED
|
@@ -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 (
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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
|
});
|
package/src/utils/submit.ts
CHANGED
|
@@ -19,11 +19,13 @@ import {
|
|
|
19
19
|
} from "../helpers/api_info";
|
|
20
20
|
import {
|
|
21
21
|
BROKEN_CONNECTION_MSG,
|
|
22
|
+
NO_API_INFO_MSG,
|
|
22
23
|
QUEUE_FULL_MSG,
|
|
23
24
|
SSE_URL,
|
|
24
25
|
SSE_DATA_URL,
|
|
25
26
|
RESET_URL,
|
|
26
|
-
CANCEL_URL
|
|
27
|
+
CANCEL_URL,
|
|
28
|
+
WS_PROTOCOL_MSG
|
|
27
29
|
} from "../constants";
|
|
28
30
|
import { apply_diff_stream, close_stream } from "./stream";
|
|
29
31
|
import { Client } from "../client";
|
|
@@ -60,7 +62,7 @@ export function submit(
|
|
|
60
62
|
|
|
61
63
|
const that = this;
|
|
62
64
|
|
|
63
|
-
if (!api_info) throw new Error(
|
|
65
|
+
if (!api_info) throw new Error(NO_API_INFO_MSG);
|
|
64
66
|
if (!config) throw new Error("Could not resolve app config");
|
|
65
67
|
|
|
66
68
|
let { fn_index, endpoint_info, dependency } = get_endpoint_info(
|
|
@@ -75,7 +77,7 @@ export function submit(
|
|
|
75
77
|
let stream: EventSource | null;
|
|
76
78
|
let protocol = config.protocol ?? "ws";
|
|
77
79
|
if (protocol === "ws") {
|
|
78
|
-
throw new Error(
|
|
80
|
+
throw new Error(WS_PROTOCOL_MSG);
|
|
79
81
|
}
|
|
80
82
|
let event_id_final = "";
|
|
81
83
|
let event_id_cb: () => string = () => event_id_final;
|
|
@@ -182,7 +184,10 @@ export function submit(
|
|
|
182
184
|
data: input_data || [],
|
|
183
185
|
event_data,
|
|
184
186
|
fn_index,
|
|
185
|
-
trigger_id
|
|
187
|
+
trigger_id,
|
|
188
|
+
...(options.oauth_token && endpoint_info?.oauth_token
|
|
189
|
+
? { oauth_token: options.oauth_token }
|
|
190
|
+
: {})
|
|
186
191
|
};
|
|
187
192
|
if (skip_queue(fn_index, config)) {
|
|
188
193
|
fire_event({
|
|
@@ -630,6 +635,22 @@ export function submit(
|
|
|
630
635
|
}
|
|
631
636
|
});
|
|
632
637
|
|
|
638
|
+
// Surface internal failures (e.g. malformed payloads or configs) as an
|
|
639
|
+
// error event instead of leaving the returned iterator hanging forever
|
|
640
|
+
// with an unhandled promise rejection.
|
|
641
|
+
job.catch((e) => {
|
|
642
|
+
fire_event({
|
|
643
|
+
type: "status",
|
|
644
|
+
stage: "error",
|
|
645
|
+
message: e instanceof Error ? e.message : String(e),
|
|
646
|
+
queue: !skip_queue(fn_index, config),
|
|
647
|
+
endpoint: _endpoint,
|
|
648
|
+
fn_index,
|
|
649
|
+
time: new Date()
|
|
650
|
+
});
|
|
651
|
+
close();
|
|
652
|
+
});
|
|
653
|
+
|
|
633
654
|
let done = false;
|
|
634
655
|
const values: (IteratorResult<GradioEvent> | PromiseLike<never>)[] = [];
|
|
635
656
|
const resolvers: ((
|
|
@@ -735,25 +756,37 @@ function get_endpoint_info(
|
|
|
735
756
|
} {
|
|
736
757
|
let fn_index: number;
|
|
737
758
|
let endpoint_info: EndpointInfo<JsApiData>;
|
|
738
|
-
let dependency: Dependency;
|
|
759
|
+
let dependency: Dependency | undefined;
|
|
739
760
|
|
|
740
761
|
if (typeof endpoint === "number") {
|
|
741
762
|
fn_index = endpoint;
|
|
742
763
|
endpoint_info = api_info.unnamed_endpoints[fn_index];
|
|
743
|
-
dependency = config.dependencies.find((dep) => dep.id == endpoint)
|
|
764
|
+
dependency = config.dependencies.find((dep) => dep.id == endpoint);
|
|
744
765
|
} else {
|
|
745
766
|
const trimmed_endpoint = endpoint.replace(/^\//, "");
|
|
746
767
|
|
|
747
768
|
fn_index = api_map[trimmed_endpoint];
|
|
748
|
-
|
|
769
|
+
// named endpoints are keyed with a leading slash in the API info, but
|
|
770
|
+
// accept endpoint names passed without one (e.g. "predict")
|
|
771
|
+
endpoint_info =
|
|
772
|
+
api_info.named_endpoints[endpoint.trim()] ??
|
|
773
|
+
api_info.named_endpoints[`/${trimmed_endpoint}`];
|
|
749
774
|
dependency = config.dependencies.find(
|
|
750
775
|
(dep) => dep.id == api_map[trimmed_endpoint]
|
|
751
|
-
)
|
|
776
|
+
);
|
|
752
777
|
}
|
|
753
778
|
|
|
754
|
-
if (typeof fn_index !== "number") {
|
|
779
|
+
if (typeof fn_index !== "number" || !dependency) {
|
|
780
|
+
const valid_endpoints = config.dependencies
|
|
781
|
+
.filter((dep) => dep.api_name)
|
|
782
|
+
.map((dep) => `"/${dep.api_name}"`)
|
|
783
|
+
.join(", ");
|
|
755
784
|
throw new Error(
|
|
756
|
-
|
|
785
|
+
`No endpoint matching ${JSON.stringify(endpoint)} was found. ` +
|
|
786
|
+
(valid_endpoints
|
|
787
|
+
? `Valid named endpoints are: ${valid_endpoints}. `
|
|
788
|
+
: "This app exposes no named endpoints. ") +
|
|
789
|
+
"An fn_index (number) of an existing dependency can also be used."
|
|
757
790
|
);
|
|
758
791
|
}
|
|
759
792
|
return { fn_index, endpoint_info, dependency };
|
package/vite.config.ts
CHANGED
|
@@ -38,7 +38,12 @@ logger.info = (log, options) => {
|
|
|
38
38
|
};
|
|
39
39
|
export default defineConfig(({ mode }) => {
|
|
40
40
|
const production = mode === "production";
|
|
41
|
-
|
|
41
|
+
// Single-file, minified browser bundle served from CDNs at
|
|
42
|
+
// dist/index.min.js (see https://github.com/gradio-app/gradio/issues/10028)
|
|
43
|
+
const isCdnBuild = process.env.CDN_BUILD === "true";
|
|
44
|
+
// a CDN build is always a browser build (guards against CDN_BUILD=true
|
|
45
|
+
// without BROWSER_BUILD=true emitting es+cjs into the same file name)
|
|
46
|
+
const isBrowserBuild = process.env.BROWSER_BUILD === "true" || isCdnBuild;
|
|
42
47
|
|
|
43
48
|
if (mode === "preview") {
|
|
44
49
|
return {
|
|
@@ -50,15 +55,26 @@ export default defineConfig(({ mode }) => {
|
|
|
50
55
|
customLogger: logger,
|
|
51
56
|
build: {
|
|
52
57
|
emptyOutDir: false,
|
|
58
|
+
...(isCdnBuild ? { minify: true } : {}),
|
|
53
59
|
lib: {
|
|
54
60
|
entry: "src/index.ts",
|
|
55
|
-
|
|
56
|
-
|
|
61
|
+
// the node build also emits a CommonJS bundle (dist/index.cjs)
|
|
62
|
+
// so that require("@gradio/client") works on all supported
|
|
63
|
+
// node versions (see https://github.com/gradio-app/gradio/issues/9214)
|
|
64
|
+
formats: isBrowserBuild ? ["es"] : ["es", "cjs"],
|
|
65
|
+
fileName: (format) => {
|
|
66
|
+
if (isCdnBuild) return "index.min.js";
|
|
67
|
+
if (isBrowserBuild) return "browser.js";
|
|
68
|
+
return format === "cjs" ? "index.cjs" : "index.js";
|
|
69
|
+
}
|
|
57
70
|
},
|
|
58
71
|
rollupOptions: {
|
|
59
72
|
input: "src/index.ts",
|
|
60
73
|
output: {
|
|
61
|
-
dir: "dist"
|
|
74
|
+
dir: "dist",
|
|
75
|
+
// keep the CDN bundle as a single file so the documented
|
|
76
|
+
// <script type="module"> import works without extra chunks
|
|
77
|
+
inlineDynamicImports: isCdnBuild
|
|
62
78
|
}
|
|
63
79
|
}
|
|
64
80
|
},
|