From f0aa55e14126698a8271d2094cc193d75c6c39d7 Mon Sep 17 00:00:00 2001 From: Trevor Manz Date: Thu, 18 Apr 2024 15:01:32 -0400 Subject: [PATCH] feat(experimental): Replace invoke timeout with AbortSignal --- .changeset/three-snails-give.md | 29 +++++++++++++++++++++++++++++ packages/anywidget/src/widget.js | 26 +++++++++++++++++--------- packages/types/index.ts | 5 ++++- 3 files changed, 50 insertions(+), 10 deletions(-) create mode 100644 .changeset/three-snails-give.md diff --git a/.changeset/three-snails-give.md b/.changeset/three-snails-give.md new file mode 100644 index 00000000..8dfbf7a7 --- /dev/null +++ b/.changeset/three-snails-give.md @@ -0,0 +1,29 @@ +--- +"anywidget": patch +"@anywidget/types": patch +--- + +Experimental: Replace invoke timeout with more flexible `AbortSignal` + +This allows more flexible control over aborting the invoke request, including delegating to third-party libraries that manage cancellation. + +```js +export default { + async render({ model, el }) { + const controller = new AbortController(); + + // Randomly abort the request after 1 second + setTimeout(() => Math.random() < 0.5 && controller.abort(), 1000); + + const signal = controller.signal; + model + .invoke("echo", "Hello, world", { signal }) + .then((result) => { + el.innerHTML = result; + }) + .catch((err) => { + el.innerHTML = `Error: ${err.message}`; + }); + }, +}; +``` diff --git a/packages/anywidget/src/widget.js b/packages/anywidget/src/widget.js index 328d2535..018d9dfc 100644 --- a/packages/anywidget/src/widget.js +++ b/packages/anywidget/src/widget.js @@ -238,30 +238,39 @@ function throw_anywidget_error(source) { throw source; } +/** + * @typedef InvokeOptions + * @prop {DataView[]} [buffers] + * @prop {AbortSignal} [signal] + */ + /** * @template T * @param {import("@anywidget/types").AnyModel} model * @param {string} name * @param {any} [msg] - * @param {DataView[]} [buffers] - * @param {{ timeout?: number }} [options] + * @param {InvokeOptions} [options] * @return {Promise<[T, DataView[]]>} */ export function invoke( model, name, msg, - buffers = [], - { timeout = 3000 } = {}, + options = {}, ) { // crypto.randomUUID() is not available in non-secure contexts (i.e., http://) // so we use simple (non-secure) polyfill. let id = uuid.v4(); + let signal = options.signal ?? AbortSignal.timeout(3000); + return new Promise((resolve, reject) => { - let timer = setTimeout(() => { - reject(new Error(`Promise timed out after ${timeout} ms`)); + if (signal.aborted) { + reject(signal.reason); + } + signal.addEventListener("abort", () => { model.off("msg:custom", handler); - }, timeout); + reject(signal.reason); + }); /** * @param {{ id: string, kind: "anywidget-command-response", response: T }} msg @@ -269,7 +278,6 @@ export function invoke( */ function handler(msg, buffers) { if (!(msg.id === id)) return; - clearTimeout(timer); resolve([msg.response, buffers]); model.off("msg:custom", handler); } @@ -277,7 +285,7 @@ export function invoke( model.send( { id, kind: "anywidget-command", name, msg }, undefined, - buffers, + options.buffers ?? [], ); }); } diff --git a/packages/types/index.ts b/packages/types/index.ts index 7dbd2ebc..317a8b2d 100644 --- a/packages/types/index.ts +++ b/packages/types/index.ts @@ -47,7 +47,10 @@ export type Experimental = { invoke: ( name: string, msg?: any, - buffers?: DataView[], + options?: { + buffers?: DataView[]; + signal?: AbortSignal; + }, ) => Promise<[T, DataView[]]>; };