Skip to content

Commit efaa073

Browse files
atlowChemidanielleadams
authored andcommitted
lib: implement AbortSignal.any()
PR-URL: #47821 Backport-PR-URL: #48800Fixes: #47811 Refs: whatwg/dom#1152 Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> Reviewed-By: Moshe Atlow <moshe@atlow.co.il> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
1 parent d002f9b commit efaa073

10 files changed

Lines changed: 410 additions & 19 deletions

File tree

‎doc/api/globals.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,18 @@ added:
121121

122122
Returns a new `AbortSignal` which will be aborted in `delay` milliseconds.
123123

124+
#### Static method: `AbortSignal.any(signals)`
125+
126+
<!-- YAML
127+
added: REPLACEME
128+
-->
129+
130+
*`signals` {AbortSignal\[]} The `AbortSignal`s of which to compose a new `AbortSignal`.
131+
132+
Returns a new `AbortSignal` which will be aborted if any of the provided
133+
signals are aborted. Its [`abortSignal.reason`][] will be set to whichever
134+
one of the `signals` caused it to be aborted.
135+
124136
#### Event: `'abort'`
125137

126138
<!-- YAML
@@ -906,6 +918,7 @@ A browser-compatible implementation of [`WritableStreamDefaultWriter`][].
906918
[`WritableStream`]: webstreams.md#class-writablestream
907919
[`__dirname`]: modules.md#__dirname
908920
[`__filename`]: modules.md#__filename
921+
[`abortSignal.reason`]: #abortsignalreason
909922
[`buffer.atob()`]: buffer.md#bufferatobdata
910923
[`buffer.btoa()`]: buffer.md#bufferbtoadata
911924
[`clearImmediate`]: timers.md#clearimmediateimmediate

‎lib/internal/abort_controller.js‎

Lines changed: 67 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ const {
4242

4343
const{
4444
validateAbortSignal,
45+
validateAbortSignalArray,
4546
validateObject,
4647
validateUint32,
4748
}=require('internal/validators');
@@ -54,6 +55,7 @@ const {
5455
clearTimeout,
5556
setTimeout,
5657
}=require('timers');
58+
constassert=require('internal/assert');
5759

5860
const{
5961
messaging_deserialize_symbol: kDeserialize,
@@ -80,13 +82,16 @@ function lazyMakeTransferable(obj) {
8082
}
8183

8284
constclearTimeoutRegistry=newSafeFinalizationRegistry(clearTimeout);
83-
consttimeOutSignals=newSafeSet();
85+
constgcPersistentSignals=newSafeSet();
8486

8587
constkAborted=Symbol('kAborted');
8688
constkReason=Symbol('kReason');
8789
constkCloneData=Symbol('kCloneData');
8890
constkTimeout=Symbol('kTimeout');
8991
constkMakeTransferable=Symbol('kMakeTransferable');
92+
constkComposite=Symbol('kComposite');
93+
constkSourceSignals=Symbol('kSourceSignals');
94+
constkDependantSignals=Symbol('kDependantSignals');
9095

9196
functioncustomInspect(self,obj,depth,options){
9297
if(depth<0)
@@ -116,7 +121,7 @@ function setWeakAbortSignalTimeout(weakRef, delay) {
116121
consttimeout=setTimeout(()=>{
117122
constsignal=weakRef.deref();
118123
if(signal!==undefined){
119-
timeOutSignals.delete(signal);
124+
gcPersistentSignals.delete(signal);
120125
abortSignal(
121126
signal,
122127
newDOMException(
@@ -185,25 +190,68 @@ class AbortSignal extends EventTarget {
185190
returnsignal;
186191
}
187192

193+
/**
194+
* @param {AbortSignal[]} signals
195+
* @returns {AbortSignal}
196+
*/
197+
staticany(signals){
198+
validateAbortSignalArray(signals,'signals');
199+
constresultSignal=createAbortSignal({composite: true});
200+
constresultSignalWeakRef=newWeakRef(resultSignal);
201+
resultSignal[kSourceSignals]=newSafeSet();
202+
for(leti=0;i<signals.length;i++){
203+
constsignal=signals[i];
204+
if(signal.aborted){
205+
abortSignal(resultSignal,signal.reason);
206+
returnresultSignal;
207+
}
208+
signal[kDependantSignals]??=newSafeSet();
209+
if(!signal[kComposite]){
210+
resultSignal[kSourceSignals].add(newWeakRef(signal));
211+
signal[kDependantSignals].add(resultSignalWeakRef);
212+
}elseif(!signal[kSourceSignals]){
213+
continue;
214+
}else{
215+
for(constsourceSignalofsignal[kSourceSignals]){
216+
constsourceSignalRef=sourceSignal.deref();
217+
if(!sourceSignalRef){
218+
continue;
219+
}
220+
assert(!sourceSignalRef.aborted);
221+
assert(!sourceSignalRef[kComposite]);
222+
223+
if(resultSignal[kSourceSignals].has(sourceSignal)){
224+
continue;
225+
}
226+
resultSignal[kSourceSignals].add(sourceSignal);
227+
sourceSignalRef[kDependantSignals].add(resultSignalWeakRef);
228+
}
229+
}
230+
}
231+
returnresultSignal;
232+
}
233+
188234
[kNewListener](size,type,listener,once,capture,passive,weak){
189235
super[kNewListener](size,type,listener,once,capture,passive,weak);
190-
if(this[kTimeout]&&
191-
type==='abort'&&
192-
!this.aborted&&
193-
!weak&&
194-
size===1){
195-
// If this is a timeout signal, and we're adding a non-weak abort
236+
constisTimeoutOrNonEmptyCompositeSignal=this[kTimeout]||(this[kComposite]&&this[kSourceSignals]?.size);
237+
if(isTimeoutOrNonEmptyCompositeSignal&&
238+
type==='abort'&&
239+
!this.aborted&&
240+
!weak&&
241+
size===1){
242+
// If this is a timeout signal, or a non-empty composite signal, and we're adding a non-weak abort
196243
// listener, then we don't want it to be gc'd while the listener
197244
// is attached and the timer still hasn't fired. So, we retain a
198245
// strong ref that is held for as long as the listener is registered.
199-
timeOutSignals.add(this);
246+
gcPersistentSignals.add(this);
200247
}
201248
}
202249

203250
[kRemoveListener](size,type,listener,capture){
204251
super[kRemoveListener](size,type,listener,capture);
205-
if(this[kTimeout]&&type==='abort'&&size===0){
206-
timeOutSignals.delete(this);
252+
constisTimeoutOrNonEmptyCompositeSignal=this[kTimeout]||(this[kComposite]&&this[kSourceSignals]?.size);
253+
if(isTimeoutOrNonEmptyCompositeSignal&&type==='abort'&&size===0){
254+
gcPersistentSignals.delete(this);
207255
}
208256
}
209257

@@ -287,7 +335,8 @@ defineEventHandler(AbortSignal.prototype, 'abort');
287335
* @param {{
288336
* aborted? : boolean,
289337
* reason? : any,
290-
* transferable? : boolean
338+
* transferable? : boolean,
339+
* composite? : boolean,
291340
* }} [init]
292341
* @returns {AbortSignal}
293342
*/
@@ -296,11 +345,13 @@ function createAbortSignal(init = kEmptyObject) {
296345
aborted =false,
297346
reason =undefined,
298347
transferable =false,
348+
composite =false,
299349
}=init;
300350
constsignal=newEventTarget();
301351
ObjectSetPrototypeOf(signal,AbortSignal.prototype);
302352
signal[kAborted]=aborted;
303353
signal[kReason]=reason;
354+
signal[kComposite]=composite;
304355
returntransferable ? lazyMakeTransferable(signal) : signal;
305356
}
306357

@@ -312,6 +363,10 @@ function abortSignal(signal, reason) {
312363
[kTrustEvent]: true,
313364
});
314365
signal.dispatchEvent(event);
366+
signal[kDependantSignals]?.forEach((s)=>{
367+
constsignalRef=s.deref();
368+
if(signalRef)abortSignal(signalRef,reason);
369+
});
315370
}
316371

317372
// TODO(joyeecheung): use private fields and we'll get invalid access

‎lib/internal/validators.js‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,26 @@ function validateBooleanArray(value, name) {
324324
}
325325
}
326326

327+
/**
328+
* @callback validateAbortSignalArray
329+
* @param {*} value
330+
* @param {string} name
331+
* @returns {asserts value is AbortSignal[]}
332+
*/
333+
334+
/** @type {validateAbortSignalArray} */
335+
functionvalidateAbortSignalArray(value,name){
336+
validateArray(value,name);
337+
for(leti=0;i<value.length;i++){
338+
constsignal=value[i];
339+
constindexedName=`${name}[${i}]`;
340+
if(signal==null){
341+
thrownewERR_INVALID_ARG_TYPE(indexedName,'AbortSignal',signal);
342+
}
343+
validateAbortSignal(signal,indexedName);
344+
}
345+
}
346+
327347
/**
328348
* @param {*} signal
329349
* @param {string} [name='signal']
@@ -528,6 +548,7 @@ module.exports = {
528548
validateArray,
529549
validateStringArray,
530550
validateBooleanArray,
551+
validateAbortSignalArray,
531552
validateBoolean,
532553
validateBuffer,
533554
validateDictionary,

‎test/common/wpt.js‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -614,7 +614,7 @@ class WPTRunner {
614614

615615
process.on('exit',()=>{
616616
for(constspecofthis.inProgress){
617-
this.fail(spec,{name: 'Unknown'},kIncomplete);
617+
this.fail(spec,{name: 'Incomplete'},kIncomplete);
618618
}
619619
inspect.defaultOptions.depth=Infinity;
620620
// Sorts the rules to have consistent output
@@ -738,11 +738,11 @@ class WPTRunner {
738738
* @param {object} harnessStatus - The status object returned by WPT harness.
739739
*/
740740
completionCallback(filename,harnessStatus){
741+
conststatus=this.getTestStatus(harnessStatus.status);
742+
741743
// Treat it like a test case failure
742-
if(harnessStatus.status===2){
743-
consttitle=this.getTestTitle(filename);
744-
console.log(`---- ${title} ----`);
745-
this.resultCallback(filename,{status: 2,name: 'Unknown'});
744+
if(status===kTimeout){
745+
this.fail(filename,{name: 'WPT testharness timeout'},kTimeout);
746746
}
747747
this.inProgress.delete(filename);
748748
// Always force termination of the worker. Some tests allocate resources

‎test/common/wpt/worker.js‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,17 @@ add_result_callback((result) => {
4141
});
4242
});
4343

44+
// Keep the event loop alive
45+
consttimeout=setTimeout(()=>{
46+
parentPort.postMessage({
47+
type: 'completion',
48+
status: {status: 2},
49+
});
50+
},2**31-1);// Max timeout is 2^31-1, when overflown the timeout is set to 1.
51+
4452
// eslint-disable-next-line no-undef
4553
add_completion_callback((_,status)=>{
54+
clearTimeout(timeout);
4655
parentPort.postMessage({
4756
type: 'completion',
4857
status,

‎test/fixtures/wpt/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Last update:
1212

1313
- common: https://github.com/web-platform-tests/wpt/tree/03c5072aff/common
1414
- console: https://github.com/web-platform-tests/wpt/tree/767ae35464/console
15-
- dom/abort: https://github.com/web-platform-tests/wpt/tree/8fadb38120/dom/abort
15+
- dom/abort: https://github.com/web-platform-tests/wpt/tree/d1f1ecbd52/dom/abort
1616
- dom/events: https://github.com/web-platform-tests/wpt/tree/ab8999891c/dom/events
1717
- encoding: https://github.com/web-platform-tests/wpt/tree/0c1b9d1622/encoding
1818
- fetch/data-urls/resources: https://github.com/web-platform-tests/wpt/tree/7c79d998ff/fetch/data-urls/resources
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// META: script=./resources/abort-signal-any-tests.js
2+
3+
abortSignalAnySignalOnlyTests(AbortSignal);
4+
abortSignalAnyTests(AbortSignal,AbortController);

0 commit comments

Comments
 (0)