Skip to content

Commit

Permalink
feat(experimental): Replace invoke timeout with AbortSignal
Browse files Browse the repository at this point in the history
  • Loading branch information
manzt committed Apr 18, 2024
1 parent dcbf443 commit f0aa55e
Show file tree
Hide file tree
Showing 3 changed files with 50 additions and 10 deletions.
29 changes: 29 additions & 0 deletions .changeset/three-snails-give.md
Original file line number Diff line number Diff line change
@@ -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}`;
});
},
};
```
26 changes: 17 additions & 9 deletions packages/anywidget/src/widget.js
Original file line number Diff line number Diff line change
Expand Up @@ -238,46 +238,54 @@ 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
* @param {DataView[]} buffers
*/
function handler(msg, buffers) {
if (!(msg.id === id)) return;
clearTimeout(timer);
resolve([msg.response, buffers]);
model.off("msg:custom", handler);
}
model.on("msg:custom", handler);
model.send(
{ id, kind: "anywidget-command", name, msg },
undefined,
buffers,
options.buffers ?? [],
);
});
}
Expand Down
5 changes: 4 additions & 1 deletion packages/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,10 @@ export type Experimental = {
invoke: <T>(
name: string,
msg?: any,
buffers?: DataView[],
options?: {
buffers?: DataView[];
signal?: AbortSignal;
},
) => Promise<[T, DataView[]]>;
};

Expand Down

0 comments on commit f0aa55e

Please sign in to comment.