1'use strict'; 2 3// Modeled very closely on the AbortController implementation 4// in https://github.com/mysticatea/abort-controller (MIT license) 5 6const { 7 ObjectAssign, 8 ObjectDefineProperties, 9 ObjectSetPrototypeOf, 10 ObjectDefineProperty, 11 PromiseResolve, 12 SafeFinalizationRegistry, 13 SafeSet, 14 Symbol, 15 SymbolToStringTag, 16 WeakRef, 17} = primordials; 18 19const { 20 defineEventHandler, 21 EventTarget, 22 Event, 23 kTrustEvent, 24 kNewListener, 25 kRemoveListener, 26 kResistStopPropagation, 27 kWeakHandler, 28} = require('internal/event_target'); 29const { 30 createDeferredPromise, 31 customInspectSymbol, 32 kEmptyObject, 33 kEnumerableProperty, 34} = require('internal/util'); 35const { inspect } = require('internal/util/inspect'); 36const { 37 codes: { 38 ERR_ILLEGAL_CONSTRUCTOR, 39 ERR_INVALID_ARG_TYPE, 40 ERR_INVALID_THIS, 41 }, 42} = require('internal/errors'); 43 44const { 45 validateAbortSignal, 46 validateAbortSignalArray, 47 validateObject, 48 validateUint32, 49} = require('internal/validators'); 50 51const { 52 DOMException, 53} = internalBinding('messaging'); 54 55const { 56 clearTimeout, 57 setTimeout, 58} = require('timers'); 59const assert = require('internal/assert'); 60 61const { 62 messaging_deserialize_symbol: kDeserialize, 63 messaging_transfer_symbol: kTransfer, 64 messaging_transfer_list_symbol: kTransferList, 65} = internalBinding('symbols'); 66 67let _MessageChannel; 68let makeTransferable; 69 70// Loading the MessageChannel and makeTransferable have to be done lazily 71// because otherwise we'll end up with a require cycle that ends up with 72// an incomplete initialization of abort_controller. 73 74function lazyMessageChannel() { 75 _MessageChannel ??= require('internal/worker/io').MessageChannel; 76 return new _MessageChannel(); 77} 78 79function lazyMakeTransferable(obj) { 80 makeTransferable ??= 81 require('internal/worker/js_transferable').makeTransferable; 82 return makeTransferable(obj); 83} 84 85const clearTimeoutRegistry = new SafeFinalizationRegistry(clearTimeout); 86const gcPersistentSignals = new SafeSet(); 87 88const kAborted = Symbol('kAborted'); 89const kReason = Symbol('kReason'); 90const kCloneData = Symbol('kCloneData'); 91const kTimeout = Symbol('kTimeout'); 92const kMakeTransferable = Symbol('kMakeTransferable'); 93const kComposite = Symbol('kComposite'); 94const kSourceSignals = Symbol('kSourceSignals'); 95const kDependantSignals = Symbol('kDependantSignals'); 96 97function customInspect(self, obj, depth, options) { 98 if (depth < 0) 99 return self; 100 101 const opts = ObjectAssign({}, options, { 102 depth: options.depth === null ? null : options.depth - 1, 103 }); 104 105 return `${self.constructor.name} ${inspect(obj, opts)}`; 106} 107 108function validateThisAbortSignal(obj) { 109 if (obj?.[kAborted] === undefined) 110 throw new ERR_INVALID_THIS('AbortSignal'); 111} 112 113// Because the AbortSignal timeout cannot be canceled, we don't want the 114// presence of the timer alone to keep the AbortSignal from being garbage 115// collected if it otherwise no longer accessible. We also don't want the 116// timer to keep the Node.js process open on it's own. Therefore, we wrap 117// the AbortSignal in a WeakRef and have the setTimeout callback close 118// over the WeakRef rather than directly over the AbortSignal, and we unref 119// the created timer object. Separately, we add the signal to a 120// FinalizerRegistry that will clear the timeout when the signal is gc'd. 121function setWeakAbortSignalTimeout(weakRef, delay) { 122 const timeout = setTimeout(() => { 123 const signal = weakRef.deref(); 124 if (signal !== undefined) { 125 gcPersistentSignals.delete(signal); 126 abortSignal( 127 signal, 128 new DOMException( 129 'The operation was aborted due to timeout', 130 'TimeoutError')); 131 } 132 }, delay); 133 timeout.unref(); 134 return timeout; 135} 136 137class AbortSignal extends EventTarget { 138 constructor() { 139 throw new ERR_ILLEGAL_CONSTRUCTOR(); 140 } 141 142 /** 143 * @type {boolean} 144 */ 145 get aborted() { 146 validateThisAbortSignal(this); 147 return !!this[kAborted]; 148 } 149 150 /** 151 * @type {any} 152 */ 153 get reason() { 154 validateThisAbortSignal(this); 155 return this[kReason]; 156 } 157 158 throwIfAborted() { 159 validateThisAbortSignal(this); 160 if (this[kAborted]) { 161 throw this[kReason]; 162 } 163 } 164 165 [customInspectSymbol](depth, options) { 166 return customInspect(this, { 167 aborted: this.aborted, 168 }, depth, options); 169 } 170 171 /** 172 * @param {any} [reason] 173 * @returns {AbortSignal} 174 */ 175 static abort( 176 reason = new DOMException('This operation was aborted', 'AbortError')) { 177 return createAbortSignal({ aborted: true, reason }); 178 } 179 180 /** 181 * @param {number} delay 182 * @returns {AbortSignal} 183 */ 184 static timeout(delay) { 185 validateUint32(delay, 'delay', false); 186 const signal = createAbortSignal(); 187 signal[kTimeout] = true; 188 clearTimeoutRegistry.register( 189 signal, 190 setWeakAbortSignalTimeout(new WeakRef(signal), delay)); 191 return signal; 192 } 193 194 /** 195 * @param {AbortSignal[]} signals 196 * @returns {AbortSignal} 197 */ 198 static any(signals) { 199 validateAbortSignalArray(signals, 'signals'); 200 const resultSignal = createAbortSignal({ composite: true }); 201 if (!signals.length) { 202 return resultSignal; 203 } 204 const resultSignalWeakRef = new WeakRef(resultSignal); 205 resultSignal[kSourceSignals] = new SafeSet(); 206 for (let i = 0; i < signals.length; i++) { 207 const signal = signals[i]; 208 if (signal.aborted) { 209 abortSignal(resultSignal, signal.reason); 210 return resultSignal; 211 } 212 signal[kDependantSignals] ??= new SafeSet(); 213 if (!signal[kComposite]) { 214 resultSignal[kSourceSignals].add(new WeakRef(signal)); 215 signal[kDependantSignals].add(resultSignalWeakRef); 216 } else if (!signal[kSourceSignals]) { 217 continue; 218 } else { 219 for (const sourceSignal of signal[kSourceSignals]) { 220 const sourceSignalRef = sourceSignal.deref(); 221 if (!sourceSignalRef) { 222 continue; 223 } 224 assert(!sourceSignalRef.aborted); 225 assert(!sourceSignalRef[kComposite]); 226 227 if (resultSignal[kSourceSignals].has(sourceSignal)) { 228 continue; 229 } 230 resultSignal[kSourceSignals].add(sourceSignal); 231 sourceSignalRef[kDependantSignals].add(resultSignalWeakRef); 232 } 233 } 234 } 235 return resultSignal; 236 } 237 238 [kNewListener](size, type, listener, once, capture, passive, weak) { 239 super[kNewListener](size, type, listener, once, capture, passive, weak); 240 const isTimeoutOrNonEmptyCompositeSignal = this[kTimeout] || (this[kComposite] && this[kSourceSignals]?.size); 241 if (isTimeoutOrNonEmptyCompositeSignal && 242 type === 'abort' && 243 !this.aborted && 244 !weak && 245 size === 1) { 246 // If this is a timeout signal, or a non-empty composite signal, and we're adding a non-weak abort 247 // listener, then we don't want it to be gc'd while the listener 248 // is attached and the timer still hasn't fired. So, we retain a 249 // strong ref that is held for as long as the listener is registered. 250 gcPersistentSignals.add(this); 251 } 252 } 253 254 [kRemoveListener](size, type, listener, capture) { 255 super[kRemoveListener](size, type, listener, capture); 256 const isTimeoutOrNonEmptyCompositeSignal = this[kTimeout] || (this[kComposite] && this[kSourceSignals]?.size); 257 if (isTimeoutOrNonEmptyCompositeSignal && type === 'abort' && size === 0) { 258 gcPersistentSignals.delete(this); 259 } 260 } 261 262 [kTransfer]() { 263 validateThisAbortSignal(this); 264 const aborted = this.aborted; 265 if (aborted) { 266 const reason = this.reason; 267 return { 268 data: { aborted, reason }, 269 deserializeInfo: 'internal/abort_controller:ClonedAbortSignal', 270 }; 271 } 272 273 const { port1, port2 } = this[kCloneData]; 274 this[kCloneData] = undefined; 275 276 this.addEventListener('abort', () => { 277 port1.postMessage(this.reason); 278 port1.close(); 279 }, { once: true }); 280 281 return { 282 data: { port: port2 }, 283 deserializeInfo: 'internal/abort_controller:ClonedAbortSignal', 284 }; 285 } 286 287 [kTransferList]() { 288 if (!this.aborted) { 289 const { port1, port2 } = lazyMessageChannel(); 290 port1.unref(); 291 port2.unref(); 292 this[kCloneData] = { 293 port1, 294 port2, 295 }; 296 return [port2]; 297 } 298 return []; 299 } 300 301 [kDeserialize]({ aborted, reason, port }) { 302 if (aborted) { 303 this[kAborted] = aborted; 304 this[kReason] = reason; 305 return; 306 } 307 308 port.onmessage = ({ data }) => { 309 abortSignal(this, data); 310 port.close(); 311 port.onmessage = undefined; 312 }; 313 // The receiving port, by itself, should never keep the event loop open. 314 // The unref() has to be called *after* setting the onmessage handler. 315 port.unref(); 316 } 317} 318 319function ClonedAbortSignal() { 320 return createAbortSignal({ transferable: true }); 321} 322ClonedAbortSignal.prototype[kDeserialize] = () => { }; 323 324ObjectDefineProperties(AbortSignal.prototype, { 325 aborted: kEnumerableProperty, 326}); 327 328ObjectDefineProperty(AbortSignal.prototype, SymbolToStringTag, { 329 __proto__: null, 330 writable: false, 331 enumerable: false, 332 configurable: true, 333 value: 'AbortSignal', 334}); 335 336defineEventHandler(AbortSignal.prototype, 'abort'); 337 338/** 339 * @param {{ 340 * aborted? : boolean, 341 * reason? : any, 342 * transferable? : boolean, 343 * composite? : boolean, 344 * }} [init] 345 * @returns {AbortSignal} 346 */ 347function createAbortSignal(init = kEmptyObject) { 348 const { 349 aborted = false, 350 reason = undefined, 351 transferable = false, 352 composite = false, 353 } = init; 354 const signal = new EventTarget(); 355 ObjectSetPrototypeOf(signal, AbortSignal.prototype); 356 signal[kAborted] = aborted; 357 signal[kReason] = reason; 358 signal[kComposite] = composite; 359 return transferable ? lazyMakeTransferable(signal) : signal; 360} 361 362function abortSignal(signal, reason) { 363 if (signal[kAborted]) return; 364 signal[kAborted] = true; 365 signal[kReason] = reason; 366 const event = new Event('abort', { 367 [kTrustEvent]: true, 368 }); 369 signal.dispatchEvent(event); 370 signal[kDependantSignals]?.forEach((s) => { 371 const signalRef = s.deref(); 372 if (signalRef) abortSignal(signalRef, reason); 373 }); 374} 375 376// TODO(joyeecheung): use private fields and we'll get invalid access 377// validation from V8 instead of throwing ERR_INVALID_THIS ourselves. 378const kSignal = Symbol('signal'); 379 380function validateAbortController(obj) { 381 if (obj?.[kSignal] === undefined) 382 throw new ERR_INVALID_THIS('AbortController'); 383} 384 385class AbortController { 386 constructor() { 387 this[kSignal] = createAbortSignal(); 388 } 389 390 /** 391 * @type {AbortSignal} 392 */ 393 get signal() { 394 validateAbortController(this); 395 return this[kSignal]; 396 } 397 398 /** 399 * @param {any} [reason] 400 */ 401 abort(reason = new DOMException('This operation was aborted', 'AbortError')) { 402 validateAbortController(this); 403 abortSignal(this[kSignal], reason); 404 } 405 406 [customInspectSymbol](depth, options) { 407 return customInspect(this, { 408 signal: this.signal, 409 }, depth, options); 410 } 411 412 static [kMakeTransferable]() { 413 const controller = new AbortController(); 414 controller[kSignal] = transferableAbortSignal(controller[kSignal]); 415 return controller; 416 } 417} 418 419/** 420 * Enables the AbortSignal to be transferable using structuredClone/postMessage. 421 * @param {AbortSignal} signal 422 * @returns {AbortSignal} 423 */ 424function transferableAbortSignal(signal) { 425 if (signal?.[kAborted] === undefined) 426 throw new ERR_INVALID_ARG_TYPE('signal', 'AbortSignal', signal); 427 return lazyMakeTransferable(signal); 428} 429 430/** 431 * Creates an AbortController with a transferable AbortSignal 432 */ 433function transferableAbortController() { 434 return AbortController[kMakeTransferable](); 435} 436 437/** 438 * @param {AbortSignal} signal 439 * @param {any} resource 440 * @returns {Promise<void>} 441 */ 442async function aborted(signal, resource) { 443 if (signal === undefined) { 444 throw new ERR_INVALID_ARG_TYPE('signal', 'AbortSignal', signal); 445 } 446 validateAbortSignal(signal, 'signal'); 447 validateObject(resource, 'resource', { nullable: false, allowFunction: true, allowArray: true }); 448 if (signal.aborted) 449 return PromiseResolve(); 450 const abortPromise = createDeferredPromise(); 451 const opts = { __proto__: null, [kWeakHandler]: resource, once: true, [kResistStopPropagation]: true }; 452 signal.addEventListener('abort', abortPromise.resolve, opts); 453 return abortPromise.promise; 454} 455 456ObjectDefineProperties(AbortController.prototype, { 457 signal: kEnumerableProperty, 458 abort: kEnumerableProperty, 459}); 460 461ObjectDefineProperty(AbortController.prototype, SymbolToStringTag, { 462 __proto__: null, 463 writable: false, 464 enumerable: false, 465 configurable: true, 466 value: 'AbortController', 467}); 468 469module.exports = { 470 AbortController, 471 AbortSignal, 472 ClonedAbortSignal, 473 aborted, 474 transferableAbortSignal, 475 transferableAbortController, 476}; 477