Skip to content

Commit a4b0ec0

Browse files
authored
feat(experimental): Replace invoke timeout with AbortSignal (#540)
1 parent dcbf443 commit a4b0ec0

File tree

3 files changed

+50
-10
lines changed

3 files changed

+50
-10
lines changed

.changeset/three-snails-give.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
"anywidget": patch
3+
"@anywidget/types": patch
4+
---
5+
6+
Experimental: Replace invoke timeout with more flexible `AbortSignal`
7+
8+
This allows more flexible control over aborting the invoke request, including delegating to third-party libraries that manage cancellation.
9+
10+
```js
11+
export default {
12+
async render({ model, el }) {
13+
const controller = new AbortController();
14+
15+
// Randomly abort the request after 1 second
16+
setTimeout(() => Math.random() < 0.5 && controller.abort(), 1000);
17+
18+
const signal = controller.signal;
19+
model
20+
.invoke("echo", "Hello, world", { signal })
21+
.then((result) => {
22+
el.innerHTML = result;
23+
})
24+
.catch((err) => {
25+
el.innerHTML = `Error: ${err.message}`;
26+
});
27+
},
28+
};
29+
```

packages/anywidget/src/widget.js

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -238,46 +238,54 @@ function throw_anywidget_error(source) {
238238
throw source;
239239
}
240240

241+
/**
242+
* @typedef InvokeOptions
243+
* @prop {DataView[]} [buffers]
244+
* @prop {AbortSignal} [signal]
245+
*/
246+
241247
/**
242248
* @template T
243249
* @param {import("@anywidget/types").AnyModel} model
244250
* @param {string} name
245251
* @param {any} [msg]
246-
* @param {DataView[]} [buffers]
247-
* @param {{ timeout?: number }} [options]
252+
* @param {InvokeOptions} [options]
248253
* @return {Promise<[T, DataView[]]>}
249254
*/
250255
export function invoke(
251256
model,
252257
name,
253258
msg,
254-
buffers = [],
255-
{ timeout = 3000 } = {},
259+
options = {},
256260
) {
257261
// crypto.randomUUID() is not available in non-secure contexts (i.e., http://)
258262
// so we use simple (non-secure) polyfill.
259263
let id = uuid.v4();
264+
let signal = options.signal ?? AbortSignal.timeout(3000);
265+
260266
return new Promise((resolve, reject) => {
261-
let timer = setTimeout(() => {
262-
reject(new Error(`Promise timed out after ${timeout} ms`));
267+
if (signal.aborted) {
268+
reject(signal.reason);
269+
}
270+
signal.addEventListener("abort", () => {
263271
model.off("msg:custom", handler);
264-
}, timeout);
272+
reject(signal.reason);
273+
});
265274

266275
/**
267276
* @param {{ id: string, kind: "anywidget-command-response", response: T }} msg
268277
* @param {DataView[]} buffers
269278
*/
270279
function handler(msg, buffers) {
271280
if (!(msg.id === id)) return;
272-
clearTimeout(timer);
273281
resolve([msg.response, buffers]);
274282
model.off("msg:custom", handler);
275283
}
276284
model.on("msg:custom", handler);
277285
model.send(
278286
{ id, kind: "anywidget-command", name, msg },
279287
undefined,
280-
buffers,
288+
options.buffers ?? [],
281289
);
282290
});
283291
}

packages/types/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,10 @@ export type Experimental = {
4747
invoke: <T>(
4848
name: string,
4949
msg?: any,
50-
buffers?: DataView[],
50+
options?: {
51+
buffers?: DataView[];
52+
signal?: AbortSignal;
53+
},
5154
) => Promise<[T, DataView[]]>;
5255
};
5356

0 commit comments

Comments
 (0)