@@ -42,6 +42,7 @@ const { |
42 | 42 |
|
43 | 43 | const { |
44 | 44 | validateAbortSignal, |
| 45 | + validateAbortSignalArray, |
45 | 46 | validateObject, |
46 | 47 | validateUint32, |
47 | 48 | } = require('internal/validators'); |
@@ -54,6 +55,7 @@ const { |
54 | 55 | clearTimeout, |
55 | 56 | setTimeout, |
56 | 57 | } = require('timers'); |
| 58 | +const assert = require('internal/assert'); |
57 | 59 |
|
58 | 60 | const { |
59 | 61 | messaging_deserialize_symbol: kDeserialize, |
@@ -80,13 +82,16 @@ function lazyMakeTransferable(obj) { |
80 | 82 | } |
81 | 83 |
|
82 | 84 | const clearTimeoutRegistry = new SafeFinalizationRegistry(clearTimeout); |
83 | | -const timeOutSignals = new SafeSet(); |
| 85 | +const gcPersistentSignals = new SafeSet(); |
84 | 86 |
|
85 | 87 | const kAborted = Symbol('kAborted'); |
86 | 88 | const kReason = Symbol('kReason'); |
87 | 89 | const kCloneData = Symbol('kCloneData'); |
88 | 90 | const kTimeout = Symbol('kTimeout'); |
89 | 91 | const kMakeTransferable = Symbol('kMakeTransferable'); |
| 92 | +const kComposite = Symbol('kComposite'); |
| 93 | +const kSourceSignals = Symbol('kSourceSignals'); |
| 94 | +const kDependantSignals = Symbol('kDependantSignals'); |
90 | 95 |
|
91 | 96 | function customInspect(self, obj, depth, options) { |
92 | 97 | if (depth < 0) |
@@ -116,7 +121,7 @@ function setWeakAbortSignalTimeout(weakRef, delay) { |
116 | 121 | const timeout = setTimeout(() => { |
117 | 122 | const signal = weakRef.deref(); |
118 | 123 | if (signal !== undefined) { |
119 | | - timeOutSignals.delete(signal); |
| 124 | + gcPersistentSignals.delete(signal); |
120 | 125 | abortSignal( |
121 | 126 | signal, |
122 | 127 | new DOMException( |
@@ -185,25 +190,68 @@ class AbortSignal extends EventTarget { |
185 | 190 | return signal; |
186 | 191 | } |
187 | 192 |
|
| 193 | + /** |
| 194 | + * @param {AbortSignal[]} signals |
| 195 | + * @returns {AbortSignal} |
| 196 | + */ |
| 197 | + static any(signals) { |
| 198 | + validateAbortSignalArray(signals, 'signals'); |
| 199 | + const resultSignal = createAbortSignal({ composite: true }); |
| 200 | + const resultSignalWeakRef = new WeakRef(resultSignal); |
| 201 | + resultSignal[kSourceSignals] = new SafeSet(); |
| 202 | + for (let i = 0; i < signals.length; i++) { |
| 203 | + const signal = signals[i]; |
| 204 | + if (signal.aborted) { |
| 205 | + abortSignal(resultSignal, signal.reason); |
| 206 | + return resultSignal; |
| 207 | + } |
| 208 | + signal[kDependantSignals] ??= new SafeSet(); |
| 209 | + if (!signal[kComposite]) { |
| 210 | + resultSignal[kSourceSignals].add(new WeakRef(signal)); |
| 211 | + signal[kDependantSignals].add(resultSignalWeakRef); |
| 212 | + } else if (!signal[kSourceSignals]) { |
| 213 | + continue; |
| 214 | + } else { |
| 215 | + for (const sourceSignal of signal[kSourceSignals]) { |
| 216 | + const sourceSignalRef = 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 | + return resultSignal; |
| 232 | + } |
| 233 | + |
188 | 234 | [kNewListener](size, type, listener, once, capture, passive, weak) { |
189 | 235 | super[kNewListener](size, type, listener, once, capture, passive, weak); |
190 | | - if (this[kTimeout] && |
| 236 | + const isTimeoutOrNonEmptyCompositeSignal = this[kTimeout] || (this[kComposite] && this[kSourceSignals]?.size); |
| 237 | + if (isTimeoutOrNonEmptyCompositeSignal && |
191 | 238 | type === 'abort' && |
192 | 239 | !this.aborted && |
193 | 240 | !weak && |
194 | 241 | size === 1) { |
195 | | - // If this is a timeout signal, and we're adding a non-weak abort |
| 242 | + // If this is a timeout signal, or a non-empty composite signal, and we're adding a non-weak abort |
196 | 243 | // listener, then we don't want it to be gc'd while the listener |
197 | 244 | // is attached and the timer still hasn't fired. So, we retain a |
198 | 245 | // strong ref that is held for as long as the listener is registered. |
199 | | - timeOutSignals.add(this); |
| 246 | + gcPersistentSignals.add(this); |
200 | 247 | } |
201 | 248 | } |
202 | 249 |
|
203 | 250 | [kRemoveListener](size, type, listener, capture) { |
204 | 251 | super[kRemoveListener](size, type, listener, capture); |
205 | | - if (this[kTimeout] && type === 'abort' && size === 0) { |
206 | | - timeOutSignals.delete(this); |
| 252 | + const isTimeoutOrNonEmptyCompositeSignal = this[kTimeout] || (this[kComposite] && this[kSourceSignals]?.size); |
| 253 | + if (isTimeoutOrNonEmptyCompositeSignal && type === 'abort' && size === 0) { |
| 254 | + gcPersistentSignals.delete(this); |
207 | 255 | } |
208 | 256 | } |
209 | 257 |
|
@@ -287,7 +335,8 @@ defineEventHandler(AbortSignal.prototype, 'abort'); |
287 | 335 | * @param {{ |
288 | 336 | * aborted? : boolean, |
289 | 337 | * reason? : any, |
290 | | - * transferable? : boolean |
| 338 | + * transferable? : boolean, |
| 339 | + * composite? : boolean, |
291 | 340 | * }} [init] |
292 | 341 | * @returns {AbortSignal} |
293 | 342 | */ |
@@ -296,11 +345,13 @@ function createAbortSignal(init = kEmptyObject) { |
296 | 345 | aborted = false, |
297 | 346 | reason = undefined, |
298 | 347 | transferable = false, |
| 348 | + composite = false, |
299 | 349 | } = init; |
300 | 350 | const signal = new EventTarget(); |
301 | 351 | ObjectSetPrototypeOf(signal, AbortSignal.prototype); |
302 | 352 | signal[kAborted] = aborted; |
303 | 353 | signal[kReason] = reason; |
| 354 | + signal[kComposite] = composite; |
304 | 355 | return transferable ? lazyMakeTransferable(signal) : signal; |
305 | 356 | } |
306 | 357 |
|
@@ -312,6 +363,10 @@ function abortSignal(signal, reason) { |
312 | 363 | [kTrustEvent]: true, |
313 | 364 | }); |
314 | 365 | signal.dispatchEvent(event); |
| 366 | + signal[kDependantSignals]?.forEach((s) => { |
| 367 | + const signalRef = s.deref(); |
| 368 | + if (signalRef) abortSignal(signalRef, reason); |
| 369 | + }); |
315 | 370 | } |
316 | 371 |
|
317 | 372 | class AbortController { |
|