From 22df61a26adf8023f6dd49c051979990e8d3879a Mon Sep 17 00:00:00 2001 From: pngwn Date: Thu, 9 May 2024 12:06:28 +0100 Subject: [PATCH] Client node fix (#8252) * fix client in node * run all client tests in ci * add changeset * fix types * add changeset * format * types * add changeset * format --------- Co-authored-by: gradio-pr-bot --- .changeset/upset-lands-add.md | 18 ++++++++++ .github/workflows/tests-js.yml | 2 ++ client/js/src/client.ts | 21 ++++++------ client/js/src/test/mock_eventsource.ts | 20 ++++++----- client/js/src/test/stream.test.ts | 34 +++++++++++++------ client/js/src/utils/stream.ts | 4 +-- client/js/src/utils/submit.ts | 6 ++-- js/app/src/lite/index.ts | 2 +- js/audio/Index.svelte | 2 +- js/audio/interactive/InteractiveAudio.svelte | 2 +- js/dataframe/Index.svelte | 2 +- js/dataframe/shared/Table.svelte | 2 +- js/file/Index.svelte | 2 +- js/file/shared/FileUpload.svelte | 2 +- js/gallery/Index.svelte | 2 +- js/image/Index.svelte | 2 +- js/image/shared/ImageUploader.svelte | 2 +- js/imageeditor/Index.svelte | 2 +- .../shared/InteractiveImageEditor.svelte | 2 +- js/imageeditor/shared/tools/Sources.svelte | 2 +- js/model3D/Index.svelte | 2 +- js/model3D/shared/Model3DUpload.svelte | 2 +- js/multimodaltextbox/Index.svelte | 2 +- .../shared/MultimodalTextbox.svelte | 2 +- js/simpleimage/Index.svelte | 2 +- js/simpleimage/shared/ImageUploader.svelte | 2 +- js/upload/src/Upload.svelte | 2 +- js/upload/src/UploadProgress.svelte | 8 ++--- js/video/Index.svelte | 2 +- js/video/shared/InteractiveVideo.svelte | 2 +- 30 files changed, 95 insertions(+), 62 deletions(-) create mode 100644 .changeset/upset-lands-add.md diff --git a/.changeset/upset-lands-add.md b/.changeset/upset-lands-add.md new file mode 100644 index 0000000000000..7814375dd3f04 --- /dev/null +++ b/.changeset/upset-lands-add.md @@ -0,0 +1,18 @@ +--- +"@gradio/app": patch +"@gradio/audio": patch +"@gradio/client": patch +"@gradio/dataframe": patch +"@gradio/file": patch +"@gradio/gallery": patch +"@gradio/image": patch +"@gradio/imageeditor": patch +"@gradio/model3d": patch +"@gradio/multimodaltextbox": patch +"@gradio/simpleimage": patch +"@gradio/upload": patch +"@gradio/video": patch +"gradio": patch +--- + +fix:Client node fix diff --git a/.github/workflows/tests-js.yml b/.github/workflows/tests-js.yml index 49218d587e440..1f8deed1bbc14 100644 --- a/.github/workflows/tests-js.yml +++ b/.github/workflows/tests-js.yml @@ -65,6 +65,8 @@ jobs: run: pnpm ts:check - name: unit tests run: pnpm test:run + - name: client tests + run: pnpm --filter @gradio/client test - name: do check if: always() uses: "gradio-app/github/actions/commit-status@main" diff --git a/client/js/src/client.ts b/client/js/src/client.ts index 24d3c8c212cd7..637ee2cf3bc3c 100644 --- a/client/js/src/client.ts +++ b/client/js/src/client.ts @@ -59,19 +59,18 @@ export class Client { return fetch(input, init); } - stream_factory(url: URL): EventSource | null { + async stream(url: URL): Promise { if (typeof window === "undefined" || typeof EventSource === "undefined") { - import("eventsource") - .then((EventSourceModule) => { - return new EventSourceModule.default(url.toString()); - }) - .catch((error) => - console.error("Failed to load EventSource module:", error) - ); + try { + const EventSourceModule = await import("eventsource"); + return new EventSourceModule.default(url.toString()) as EventSource; + } catch (error) { + console.error("Failed to load EventSource module:", error); + throw error; + } } else { return new EventSource(url.toString()); } - return null; } view_api: () => Promise>; @@ -107,7 +106,7 @@ export class Client { data?: unknown[], event_data?: unknown ) => Promise; - open_stream: () => void; + open_stream: () => Promise; private resolve_config: (endpoint: string) => Promise; constructor(app_reference: string, options: ClientOptions = {}) { this.app_reference = app_reference; @@ -144,7 +143,7 @@ export class Client { const heartbeat_url = new URL( `${this.config.root}/heartbeat/${this.session_hash}` ); - this.heartbeat_event = this.stream_factory(heartbeat_url); // Just connect to the endpoint without parsing the response. Ref: https://github.com/gradio-app/gradio/pull/7974#discussion_r1557717540 + this.heartbeat_event = await this.stream(heartbeat_url); // Just connect to the endpoint without parsing the response. Ref: https://github.com/gradio-app/gradio/pull/7974#discussion_r1557717540 if (this.config.space_id && this.options.hf_token) { this.jwt = await get_jwt( diff --git a/client/js/src/test/mock_eventsource.ts b/client/js/src/test/mock_eventsource.ts index 5b5981ad5a01b..1f47258cd3b90 100644 --- a/client/js/src/test/mock_eventsource.ts +++ b/client/js/src/test/mock_eventsource.ts @@ -1,11 +1,13 @@ import { vi } from "vitest"; -Object.defineProperty(window, "EventSource", { - writable: true, - value: vi.fn().mockImplementation(() => ({ - close: vi.fn(() => {}), - addEventListener: vi.fn(), - onmessage: vi.fn((_event: MessageEvent) => {}), - onerror: vi.fn((_event: Event) => {}) - })) -}); +if (process.env.TEST_MODE !== "node") { + Object.defineProperty(window, "EventSource", { + writable: true, + value: vi.fn().mockImplementation(() => ({ + close: vi.fn(() => {}), + addEventListener: vi.fn(), + onmessage: vi.fn((_event: MessageEvent) => {}), + onerror: vi.fn((_event: Event) => {}) + })) + }); +} diff --git a/client/js/src/test/stream.test.ts b/client/js/src/test/stream.test.ts index aba5bfd9c3432..adb050249c967 100644 --- a/client/js/src/test/stream.test.ts +++ b/client/js/src/test/stream.test.ts @@ -1,11 +1,21 @@ -import { vi } from "vitest"; +import { vi, type Mock } from "vitest"; import { Client } from "../client"; import { initialise_server } from "./server"; -import { describe, it, expect, afterEach } from "vitest"; +import { + describe, + it, + expect, + afterEach, + beforeAll, + afterAll, + beforeEach +} from "vitest"; import "./mock_eventsource.ts"; +import NodeEventSource from "eventsource"; const server = initialise_server(); +const IS_NODE = process.env.TEST_MODE === "node"; beforeAll(() => server.listen()); afterEach(() => server.resetHandlers()); @@ -13,12 +23,14 @@ afterAll(() => server.close()); describe("open_stream", () => { let mock_eventsource: any; - let app: any; + let app: Client; beforeEach(async () => { app = await Client.connect("hmb/hello_world"); - app.stream_factory = vi.fn().mockImplementation(() => { - mock_eventsource = new EventSource(""); + app.stream = vi.fn().mockImplementation(() => { + mock_eventsource = IS_NODE + ? new NodeEventSource("") + : new EventSource(""); return mock_eventsource; }); }); @@ -30,21 +42,21 @@ describe("open_stream", () => { it("should throw an error if config is not defined", () => { app.config = undefined; - expect(() => { - app.open_stream(); - }).toThrow("Could not resolve app config"); + expect(async () => { + await app.open_stream(); + }).rejects.toThrow("Could not resolve app config"); }); it("should connect to the SSE endpoint and handle messages", async () => { - app.open_stream(); + await app.open_stream(); - const eventsource_mock_call = app.stream_factory.mock.calls[0][0]; + const eventsource_mock_call = (app.stream as Mock).mock.calls[0][0]; expect(eventsource_mock_call.href).toMatch( /https:\/\/hmb-hello-world\.hf\.space\/queue\/data\?session_hash/ ); - expect(app.stream_factory).toHaveBeenCalledWith(eventsource_mock_call); + expect(app.stream).toHaveBeenCalledWith(eventsource_mock_call); const onMessageCallback = mock_eventsource.onmessage; const onErrorCallback = mock_eventsource.onerror; diff --git a/client/js/src/utils/stream.ts b/client/js/src/utils/stream.ts index c4d7f481b4475..02df1a968919d 100644 --- a/client/js/src/utils/stream.ts +++ b/client/js/src/utils/stream.ts @@ -1,7 +1,7 @@ import { BROKEN_CONNECTION_MSG } from "../constants"; import type { Client } from "../client"; -export function open_stream(this: Client): void { +export async function open_stream(this: Client): Promise { let { event_callbacks, unclosed_events, @@ -28,7 +28,7 @@ export function open_stream(this: Client): void { url.searchParams.set("__sign", jwt); } - stream = this.stream_factory(url); + stream = await this.stream(url); if (!stream) { console.warn("Cannot connect to SSE endpoint: " + url.toString()); diff --git a/client/js/src/utils/submit.ts b/client/js/src/utils/submit.ts index 55e49e6824432..c93cc68e98cc0 100644 --- a/client/js/src/utils/submit.ts +++ b/client/js/src/utils/submit.ts @@ -372,7 +372,7 @@ export function submit( url.searchParams.set("__sign", this.jwt); } - stream = this.stream_factory(url); + stream = await this.stream(url); if (!stream) { return Promise.reject( @@ -503,7 +503,7 @@ export function submit( headers ); }); - post_data_promise.then(([response, status]: any) => { + post_data_promise.then(async ([response, status]: any) => { if (status === 503) { fire_event({ type: "status", @@ -655,7 +655,7 @@ export function submit( event_callbacks[event_id] = callback; unclosed_events.add(event_id); if (!stream_status.open) { - this.open_stream(); + await this.open_stream(); } } }); diff --git a/js/app/src/lite/index.ts b/js/app/src/lite/index.ts index 7363fbc87b6dd..a03a3078e21ee 100644 --- a/js/app/src/lite/index.ts +++ b/js/app/src/lite/index.ts @@ -131,7 +131,7 @@ export function create(options: Options): GradioAppController { return wasm_proxied_fetch(worker_proxy, input, init); } - stream_factory(url: URL): EventSource { + async stream(url: URL): Promise { return wasm_proxied_stream_factory(worker_proxy, url); } } diff --git a/js/audio/Index.svelte b/js/audio/Index.svelte index c7e989167c509..184b82dffecde 100644 --- a/js/audio/Index.svelte +++ b/js/audio/Index.svelte @@ -226,7 +226,7 @@ {waveform_options} {trim_region_settings} upload={gradio.client.upload} - stream_handler={gradio.client.stream_factory} + stream_handler={gradio.client.stream} > diff --git a/js/audio/interactive/InteractiveAudio.svelte b/js/audio/interactive/InteractiveAudio.svelte index 876dfcd25ea3f..67e4c19745b97 100644 --- a/js/audio/interactive/InteractiveAudio.svelte +++ b/js/audio/interactive/InteractiveAudio.svelte @@ -35,7 +35,7 @@ export let editable = true; export let max_file_size: number | null = null; export let upload: Client["upload"]; - export let stream_handler: Client["stream_factory"]; + export let stream_handler: Client["stream"]; $: dispatch("drag", dragging); diff --git a/js/dataframe/Index.svelte b/js/dataframe/Index.svelte index b2107b65a2e9e..c6d27f997e6a5 100644 --- a/js/dataframe/Index.svelte +++ b/js/dataframe/Index.svelte @@ -151,6 +151,6 @@ {line_breaks} {column_widths} upload={gradio.client.upload} - stream_handler={gradio.client.stream_factory} + stream_handler={gradio.client.stream} /> diff --git a/js/dataframe/shared/Table.svelte b/js/dataframe/shared/Table.svelte index ec8e878d7e8b3..9d5fa161be425 100644 --- a/js/dataframe/shared/Table.svelte +++ b/js/dataframe/shared/Table.svelte @@ -40,7 +40,7 @@ export let line_breaks = true; export let column_widths: string[] = []; export let upload: Client["upload"]; - export let stream_handler: Client["stream_factory"]; + export let stream_handler: Client["stream"]; let selected: false | [number, number] = false; export let display_value: string[][] | null = null; diff --git a/js/file/Index.svelte b/js/file/Index.svelte index 6c0fc0f66b931..805e8454f757c 100644 --- a/js/file/Index.svelte +++ b/js/file/Index.svelte @@ -88,7 +88,7 @@ {:else} { const files = Array.isArray(e.detail) ? e.detail : [e.detail]; value = files.map((x) => ({ image: x, caption: null })); diff --git a/js/image/Index.svelte b/js/image/Index.svelte index 6596f50d24676..da031f3539578 100644 --- a/js/image/Index.svelte +++ b/js/image/Index.svelte @@ -157,7 +157,7 @@ max_file_size={gradio.max_file_size} i18n={gradio.i18n} upload={gradio.client.upload} - stream_handler={gradio.client.stream_factory} + stream_handler={gradio.client.stream} > {#if active_source === "upload" || !active_source} diff --git a/js/image/shared/ImageUploader.svelte b/js/image/shared/ImageUploader.svelte index 27b8e388cb188..38c254426206c 100644 --- a/js/image/shared/ImageUploader.svelte +++ b/js/image/shared/ImageUploader.svelte @@ -27,7 +27,7 @@ export let i18n: I18nFormatter; export let max_file_size: number | null = null; export let upload: Client["upload"]; - export let stream_handler: Client["stream_factory"]; + export let stream_handler: Client["stream"]; let upload_input: Upload; let uploading = false; diff --git a/js/imageeditor/Index.svelte b/js/imageeditor/Index.svelte index 820b4c4710b83..6e0a73bd8dc6b 100644 --- a/js/imageeditor/Index.svelte +++ b/js/imageeditor/Index.svelte @@ -208,7 +208,7 @@ {layers} status={loading_status?.status} upload={gradio.client.upload} - stream_handler={gradio.client.stream_factory} + stream_handler={gradio.client.stream} > {/if} diff --git a/js/imageeditor/shared/InteractiveImageEditor.svelte b/js/imageeditor/shared/InteractiveImageEditor.svelte index a37aa335a4902..1699353724cff 100644 --- a/js/imageeditor/shared/InteractiveImageEditor.svelte +++ b/js/imageeditor/shared/InteractiveImageEditor.svelte @@ -47,7 +47,7 @@ export let canvas_size: [number, number] | undefined; export let realtime: boolean; export let upload: Client["upload"]; - export let stream_handler: Client["stream_factory"]; + export let stream_handler: Client["stream"]; const dispatch = createEventDispatcher<{ clear?: never; diff --git a/js/imageeditor/shared/tools/Sources.svelte b/js/imageeditor/shared/tools/Sources.svelte index e565b0c919604..4303d2640c16c 100644 --- a/js/imageeditor/shared/tools/Sources.svelte +++ b/js/imageeditor/shared/tools/Sources.svelte @@ -26,7 +26,7 @@ export let mirror_webcam = true; export let i18n: I18nFormatter; export let upload: Client["upload"]; - export let stream_handler: Client["stream_factory"]; + export let stream_handler: Client["stream"]; const { active_tool } = getContext(TOOL_KEY); const { pixi, dimensions, register_context, reset, editor_box } = diff --git a/js/model3D/Index.svelte b/js/model3D/Index.svelte index f5433dead8e5a..2fd3f6ee51180 100644 --- a/js/model3D/Index.svelte +++ b/js/model3D/Index.svelte @@ -128,7 +128,7 @@ i18n={gradio.i18n} max_file_size={gradio.max_file_size} upload={gradio.client.upload} - stream_handler={gradio.client.stream_factory} + stream_handler={gradio.client.stream} > diff --git a/js/model3D/shared/Model3DUpload.svelte b/js/model3D/shared/Model3DUpload.svelte index 24ca7dbda4096..e1aaf5549b29f 100644 --- a/js/model3D/shared/Model3DUpload.svelte +++ b/js/model3D/shared/Model3DUpload.svelte @@ -25,7 +25,7 @@ null ]; export let upload: Client["upload"]; - export let stream_handler: Client["stream_factory"]; + export let stream_handler: Client["stream"]; async function handle_upload({ detail diff --git a/js/multimodaltextbox/Index.svelte b/js/multimodaltextbox/Index.svelte index b2032d16b428e..60faecc0d940f 100644 --- a/js/multimodaltextbox/Index.svelte +++ b/js/multimodaltextbox/Index.svelte @@ -98,6 +98,6 @@ }} disabled={!interactive} upload={gradio.client.upload} - stream_handler={gradio.client.stream_factory} + stream_handler={gradio.client.stream} /> diff --git a/js/multimodaltextbox/shared/MultimodalTextbox.svelte b/js/multimodaltextbox/shared/MultimodalTextbox.svelte index c1d9b5b9decb6..6cc49bc461e28 100644 --- a/js/multimodaltextbox/shared/MultimodalTextbox.svelte +++ b/js/multimodaltextbox/shared/MultimodalTextbox.svelte @@ -36,7 +36,7 @@ export let file_types: string[] | null = null; export let max_file_size: number | null = null; export let upload: Client["upload"]; - export let stream_handler: Client["stream_factory"]; + export let stream_handler: Client["stream"]; let upload_component: Upload; let hidden_upload: HTMLInputElement; diff --git a/js/simpleimage/Index.svelte b/js/simpleimage/Index.svelte index 36970d4491fb4..2b31388c85880 100644 --- a/js/simpleimage/Index.svelte +++ b/js/simpleimage/Index.svelte @@ -91,7 +91,7 @@ gradio.dispatch("clear")} diff --git a/js/simpleimage/shared/ImageUploader.svelte b/js/simpleimage/shared/ImageUploader.svelte index 692daa3516f33..e5690bee620ba 100644 --- a/js/simpleimage/shared/ImageUploader.svelte +++ b/js/simpleimage/shared/ImageUploader.svelte @@ -12,7 +12,7 @@ export let show_label: boolean; export let root: string; export let upload: Client["upload"]; - export let stream_handler: Client["stream_factory"]; + export let stream_handler: Client["stream"]; let upload_component: Upload; let uploading = false; diff --git a/js/upload/src/Upload.svelte b/js/upload/src/Upload.svelte index 1d4eae23dff99..0ce9ce930681d 100644 --- a/js/upload/src/Upload.svelte +++ b/js/upload/src/Upload.svelte @@ -20,7 +20,7 @@ export let show_progress = true; export let max_file_size: number | null = null; export let upload: Client["upload"]; - export let stream_handler: Client["stream_factory"]; + export let stream_handler: Client["stream"]; let upload_id: string; let file_data: FileData[]; diff --git a/js/upload/src/UploadProgress.svelte b/js/upload/src/UploadProgress.svelte index e3145d5876575..b10dc90d59915 100644 --- a/js/upload/src/UploadProgress.svelte +++ b/js/upload/src/UploadProgress.svelte @@ -7,9 +7,9 @@ export let upload_id: string; export let root: string; export let files: FileData[]; - export let stream_handler: Client["stream_factory"]; + export let stream_handler: Client["stream"]; - let stream: ReturnType; + let stream: Awaited>; let progress = false; let current_file_upload: FileDataWithProgress; let file_to_display: FileDataWithProgress; @@ -37,8 +37,8 @@ return (file.progress * 100) / (file.size || 0) || 0; } - onMount(() => { - stream = stream_handler( + onMount(async () => { + stream = await stream_handler( new URL(`${root}/upload_progress?upload_id=${upload_id}`) ); diff --git a/js/video/Index.svelte b/js/video/Index.svelte index 54bbeefc17fe5..8d598a982a463 100644 --- a/js/video/Index.svelte +++ b/js/video/Index.svelte @@ -208,7 +208,7 @@ i18n={gradio.i18n} max_file_size={gradio.max_file_size} upload={gradio.client.upload} - stream_handler={gradio.client.stream_factory} + stream_handler={gradio.client.stream} > diff --git a/js/video/shared/InteractiveVideo.svelte b/js/video/shared/InteractiveVideo.svelte index ea55b7e9088aa..56fb0be94df9d 100644 --- a/js/video/shared/InteractiveVideo.svelte +++ b/js/video/shared/InteractiveVideo.svelte @@ -30,7 +30,7 @@ export let handle_reset_value: () => void = () => {}; export let max_file_size: number | null = null; export let upload: Client["upload"]; - export let stream_handler: Client["stream_factory"]; + export let stream_handler: Client["stream"]; const dispatch = createEventDispatcher<{ change: FileData | null;