-
Notifications
You must be signed in to change notification settings - Fork 330
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Proposal: fetch with multiple AbortSignals #905
Comments
Given that |
I'm unclear on exactly the connection to the OP's problem, but the OP's suggestion at the bottom of their post of
is definitely something we considered. I believe we just put it off to get v1 out the door. An |
@annevk I think you are right, I will try it on another way... |
fwiw function anySignal(signals) {
const controller = new AbortController();
function onAbort() {
controller.abort();
// Cleanup
for (const signal of signals) {
signal.removeEventListener('abort', onAbort);
}
}
for (const signal of signals) {
if (signal.aborted) {
onAbort();
break;
}
signal.addEventListener('abort', onAbort);
}
return controller.signal;
} |
Useful function!
|
My bad, I should have used |
FYI: I added my experiments with abortable fetch on https://github.com/jovdb/fetch-hof I have used the CancelToken proposal in my experiment. The |
@benjamingr this issue might be of interest to you. I'm folding it into whatwg/dom#920 and hope that's acceptable to everyone. |
Great comment from @jakearchibald. Some proposed changes -
function anySignal(signals: AbortSignal[]) {
const controller = new AbortController();
const unsubscribe: (() => void)[] = [];
function onAbort(signal: AbortSignal) {
controller.abort(signal.reason);
unsubscribe.forEach((f) => f());
}
for (const signal of signals) {
if (signal.aborted) {
onAbort(signal);
break;
}
const handler = onAbort.bind(undefined, signal);
signal.addEventListener('abort', handler);
unsubscribe.push(() => signal.removeEventListener('abort', handler));
}
return controller.signal;
} |
fwiw, it can be simplified further: function anySignal(signals: AbortSignal[]): AbortSignal {
const controller = new AbortController();
for (const signal of signals) {
if (signal.aborted) return signal;
signal.addEventListener("abort", () => controller.abort(signal.reason), {
signal: controller.signal,
});
}
return controller.signal;
} …now that signals can be used to remove event listeners. |
Nice! Yeah I just learned about it yesterday. It’s not supported by node.js yet but is supported by Jsdom so should be good for testing browser code.
Re. implementation, there is a memory leak if returning early, it’s important to abort the controller too.
…________________________________
From: Jake Archibald ***@***.***>
Sent: 11 February 2023 12:55 AM
To: whatwg/fetch ***@***.***>
Cc: Alon Gamliel ***@***.***>; Comment ***@***.***>
Subject: Re: [whatwg/fetch] Proposal: fetch with multiple AbortSignals (#905)
fwiw, it can be simplified further:
function anySignal(signals: AbortSignal[]) {
const controller = new AbortController();
for (const signal of signals) {
if (signal.aborted) return signal;
signal.addEventListener("abort", () => controller.abort(signal.reason), {
signal: controller.signal,
});
}
return controller.signal;
}
…now that signals can be used to remove event listeners.
—
Reply to this email directly, view it on GitHub<#905 (comment)>, or unsubscribe<https://github.com/notifications/unsubscribe-auth/AB7TOENBK3FI6QQ6EF5TGETWWYUERANCNFSM4HMG6GOA>.
You are receiving this because you commented.Message ID: ***@***.***>
|
I'm pretty sure it is. |
@gamliela I don't think there's a memory leak there. Maybe this explains it https://jakearchibald.com/2020/events-and-gc/ And Node does support signals. |
@jakearchibald My comment about node.js was based on MDN docs - they must be outdated then. I think the AbortController stays in memory until it gets aborted. In case of early return this may happen much after the return (or never happen), hence the memory leak. I spent some time on this and created the example below. Run function anySignal(signals) {
const controller = new AbortController();
controller.data = Array(20000000).fill(1);
for (const signal of signals) {
if (signal.aborted) {
//controller.abort(); // <-------- this
return signal;
}
signal.addEventListener("abort", () => controller.abort(signal.reason), {
signal: controller.signal,
});
}
return controller.signal;
}
function test1() {
const signal1 = AbortSignal.timeout(10000);
const signal2 = AbortSignal.abort();
const signal = anySignal([signal1, signal2]);
signal1.addEventListener('abort', () => console.log('done!'));
} |
That isn't true. It can be GC'd like everything else in JS. |
I was talking in the context of the code, not about AbortController's in geneal. Of course they can be GC'd. Have a look at the code. |
We have a utility for this in node called util.aborted that doesn’t leak the callback |
@jakearchibald there is a memory leak if a not-first signal has already been aborted. The fix is to add function anySignal(signals: Iterable<AbortSignal>): AbortSignal {
const controller = new AbortController();
for (const signal of signals) {
if (signal.aborted) {
controller.abort(signal.reason);
return signal;
}
signal.addEventListener("abort", () => controller.abort(signal.reason), {
signal: controller.signal,
});
}
return controller.signal;
} |
@jakearchibald I'm afraid there is a bug in your implementation. When you do: So the final code should look like this: function anySignal(signals: AbortSignal[]): AbortSignal {
const controller = new AbortController();
for (const signal of signals) {
if (signal.aborted) {
controller.abort();
return signal;
}
signal.addEventListener("abort", () => controller.abort(signal.reason), {
signal: controller.signal,
});
}
return controller.signal;
} By the way, there is no need to pass Here's my version of the function with a single return statement, optional rest arguments and comments: /**
* Returns an abort signal that is getting aborted when
* at least one of the specified abort signals is aborted.
*
* Requires at least node.js 18.
*/
export function anySignal(
...args: (AbortSignal[] | [AbortSignal[]])
): AbortSignal {
// Allowing signals to be passed either as array
// of signals or as multiple arguments.
const signals = <AbortSignal[]> (
(args.length === 1 && Array.isArray(args[0]))
? args[0]
: args
);
const controller = new AbortController();
for (const signal of signals) {
if (signal.aborted) {
// Exiting early if one of the signals
// is already aborted.
controller.abort(signal.reason);
break;
}
// Listening for signals and removing the listeners
// when at least one symbol is aborted.
signal.addEventListener('abort',
() => controller.abort(signal.reason),
{ signal: controller.signal }
);
}
return controller.signal;
} @jakearchibald could you please update your example to fix this? I'm afraid people from Google search could accidentally copy it with the memory leak in it. |
It looks like this is now possible with the new Example: fetch(url, {
signal: AbortSignal.any([
userCancelSignal,
AbortSignal.timeout(10000),
]),
}); In the meanwhile I also published a helper to combine them cross-browser: |
Try this module https://github.com/jacobheun/any-signal |
I have some 'Higher Order functions' that can create me a
fetch
function with some extra behavior on it.Some of them can abort the request.
Here a simplified example with a timeout that can abort:
In a real application, the second
withTimeout
could also be awithCancel
function.The first
withTimeout
will create anAbortController
instance and set the signal property on the requestInit argument offetch
,The second
withTimeout
will also create anAbortController
and set the signal to the requestInit argument. This gives a problem because there already is a signal.I could create for this sample one
AbortController
outside thewithTimeout
's and pass it to bothwithTimeout
's as argument, but in a real application the enhancing of afetch
function can be done on different layers in the application. This causes other problems:abortController
instance towithTimeout
.abortController
instance is used for thisfetch
and have acces to it.and this makes the code more dirty in my opinion.
What would be great is a way to pass multiple abort signals to a
fetch
function, some idea's:fetch
also accept an array of abortSignalsAbortSignal
's into oneAbortSignal
Here the example on CodePen
The text was updated successfully, but these errors were encountered: