Skip to content

Commit

Permalink
decouple AbortController (and friends) from Node.js internals
Browse files Browse the repository at this point in the history
  • Loading branch information
shirakaba committed Mar 20, 2024
1 parent 9369b12 commit 09b9a82
Show file tree
Hide file tree
Showing 13 changed files with 263 additions and 67 deletions.
34 changes: 30 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

[![npm status](https://img.shields.io/npm/v/dom-events-wintercg.svg)](https://npm.im/dom-events-wintercg)

A polyfill for the [DOM Events](https://dom.spec.whatwg.org/#introduction-to-dom-events) APIs:
A polyfill for [DOM Events](https://dom.spec.whatwg.org/#introduction-to-dom-events) and related APIs:

- [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController)
- [AbortSignal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal)
- [CustomEvent](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent)
- [DOMException](https://developer.mozilla.org/en-US/docs/Web/API/DOMException)
- [Event](https://developer.mozilla.org/en-US/docs/Web/API/Event)
- [EventTarget](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget)

Expand Down Expand Up @@ -53,7 +56,7 @@ import { polyfill } from 'dom-events-wintercg';

polyfill(globalThis);

// Event, EventTarget, and CustomEvent will now be available in global scope
// All implemented APIs will now be available in global scope

const eventTarget = new EventTarget();
const event = new Event('click', {});
Expand Down Expand Up @@ -141,13 +144,16 @@ Below, I'll describe for each bundler how to integrate this package into your bu

#### Webpack 5

This configuration ensures that `CustomEvent`, `Event`, and `EventTarget` are available from global scope:
This configuration ensures that all the implemented APIs are available from global scope:

```js
const webpackConfig = {
plugins: [
new webpack.ProvidePlugin({
AbortController: ['dom-events-wintercg', 'AbortController'],
AbortSignal: ['dom-events-wintercg', 'AbortSignal'],
CustomEvent: ['dom-events-wintercg', 'CustomEvent'],
DOMException: ['dom-events-wintercg', 'DOMException'],
Event: ['dom-events-wintercg', 'Event'],
EventTarget: ['dom-events-wintercg', 'EventTarget'],
}),
Expand All @@ -166,7 +172,10 @@ Additionally, you can polyfill _some of_ the Node.js [events](https://nodejs.org
+ },
plugins: [
new webpack.ProvidePlugin({
AbortController: ['dom-events-wintercg', 'AbortController'],
AbortSignal: ['dom-events-wintercg', 'AbortSignal'],
CustomEvent: ['dom-events-wintercg', 'CustomEvent'],
DOMException: ['dom-events-wintercg', 'DOMException'],
Event: ['dom-events-wintercg', 'Event'],
EventTarget: ['dom-events-wintercg', 'EventTarget'],
}),
Expand All @@ -176,7 +185,11 @@ Additionally, you can polyfill _some of_ the Node.js [events](https://nodejs.org

## Prerequisities

Your JS engine or runtime must support the following APIs (this is a non-exhaustive list):
This polyfill relies on a few language features.

### Required APIs

Your JS engine/runtime must support the following APIs (this is a non-exhaustive list):

- At least [ES6](https://www.w3schools.com/js/js_es6.asp). I'm not sure exactly what this repo makes use of, but technically the linter allows up to ES2022.
- [Private properties](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private_properties)
Expand All @@ -187,6 +200,19 @@ Your JS engine or runtime must support the following APIs (this is a non-exhaust
- [WeakRef](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakRef)
- Basic [ESM](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules) (`import` and `export`)

### Optional APIs

Some of the features of this polyfill are optional, and will fail gracefully if your JS engine/runtime lacks support for the underlying APIs.

### `AbortSignal.timeout()`

[AbortSignal.timeout()](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/timeout_static) support requires the following APIs:

- [setTimeout](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout)
- [clearTimeout](https://developer.mozilla.org/en-US/docs/Web/API/clearTimeout)

If missing, `AbortSignal.timeout()` will throw an Error with code `ERR_METHOD_NOT_IMPLEMENTED` when called.

## Differences from browser EventTarget

Beyond the differences explained in the Node.js [SDK docs](https://nodejs.org/api/events.html#nodejs-eventtarget-vs-dom-eventtarget), see this excellent article from NearForm about how they first [brought EventTarget to Node.js](https://www.nearform.com/insights/node-js-and-the-struggles-of-being-an-eventtarget/), which covers some of the compromises they had to make in the implementation. In particular, there is no concept of bubbling or capturing, and `event.preventDefault()` is a bit useless, as it never has a "default action" to prevent.
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"type": "module",
"main": "src/index.js",
"files": [
"LICENCE-abort-controller",
"LICENCE-nodejs",
"src"
],
Expand Down
86 changes: 37 additions & 49 deletions src/abort-controller.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
'use strict';

// Modeled very closely on the AbortController implementation
// in https://github.com/mysticatea/abort-controller (MIT license)

const {
import {
ObjectAssign,
ObjectDefineProperties,
ObjectSetPrototypeOf,
Expand All @@ -14,9 +12,9 @@ const {
Symbol,
SymbolToStringTag,
WeakRef,
} = primordials;
} from './primordials.js';

const {
import {
defineEventHandler,
EventTarget,
Event,
Expand All @@ -25,57 +23,52 @@ const {
kRemoveListener,
kResistStopPropagation,
kWeakHandler,
} = require('internal/event_target');
const {
} from './event-target.js';
import {
createDeferredPromise,
customInspectSymbol,
kEmptyObject,
kEnumerableProperty,
} = require('internal/util');
const { inspect } = require('internal/util/inspect');
const {
codes: { ERR_ILLEGAL_CONSTRUCTOR, ERR_INVALID_ARG_TYPE, ERR_INVALID_THIS },
} = require('internal/errors');

const {
inspect,
} from './util.js';
import { assert, codes } from './errors.js';
import {
validateAbortSignal,
validateAbortSignalArray,
validateObject,
validateUint32,
kValidateObjectAllowArray,
kValidateObjectAllowFunction,
} = require('internal/validators');

const { DOMException } = internalBinding('messaging');

const { clearTimeout, setTimeout } = require('timers');
const assert = require('internal/assert');
} from './validators.js';
import { DOMException } from './dom-exception.js';

const {
ERR_ILLEGAL_CONSTRUCTOR,
ERR_INVALID_ARG_TYPE,
ERR_INVALID_THIS,
ERR_METHOD_NOT_IMPLEMENTED,
} = codes;

import {
kDeserialize,
kTransfer,
kTransferList,
} = require('internal/worker/js_transferable');

let _MessageChannel;
let markTransferMode;

// Loading the MessageChannel and markTransferable have to be done lazily
// because otherwise we'll end up with a require cycle that ends up with
// an incomplete initialization of abort_controller.
markTransferMode,
} from './js-transferable.js';
import { MessageChannel as NoOpMessageChannel } from './message-channel.js';

function lazyMessageChannel() {
_MessageChannel ??= require('internal/worker/io').MessageChannel;
return new _MessageChannel();
const MessageChannel = globalThis.MessageChannel ?? NoOpMessageChannel;
return new MessageChannel();
}

function lazyMarkTransferMode(obj, cloneable, transferable) {
markTransferMode ??=
require('internal/worker/js_transferable').markTransferMode;
markTransferMode(obj, cloneable, transferable);
}

const clearTimeoutRegistry = new SafeFinalizationRegistry(clearTimeout);
const clearTimeoutRegistry = new SafeFinalizationRegistry(
globalThis.clearTimeout,
);
const gcPersistentSignals = new SafeSet();

const kAborted = Symbol('kAborted');
Expand Down Expand Up @@ -110,7 +103,7 @@ function validateThisAbortSignal(obj) {
// the created timer object. Separately, we add the signal to a
// FinalizerRegistry that will clear the timeout when the signal is gc'd.
function setWeakAbortSignalTimeout(weakRef, delay) {
const timeout = setTimeout(() => {
const timeout = globalThis.setTimeout(() => {
const signal = weakRef.deref();
if (signal !== undefined) {
gcPersistentSignals.delete(signal);
Expand All @@ -127,7 +120,7 @@ function setWeakAbortSignalTimeout(weakRef, delay) {
return timeout;
}

class AbortSignal extends EventTarget {
export class AbortSignal extends EventTarget {
constructor() {
throw new ERR_ILLEGAL_CONSTRUCTOR();
}
Expand Down Expand Up @@ -181,6 +174,10 @@ class AbortSignal extends EventTarget {
* @returns {AbortSignal}
*/
static timeout(delay) {
if (!globalThis.setTimeout || !globalThis.clearTimeout) {
throw new ERR_METHOD_NOT_IMPLEMENTED('timeout()');
}

validateUint32(delay, 'delay', false);
const signal = createAbortSignal();
signal[kTimeout] = true;
Expand Down Expand Up @@ -324,7 +321,7 @@ class AbortSignal extends EventTarget {
}
}

function ClonedAbortSignal() {
export function ClonedAbortSignal() {
return createAbortSignal({ transferable: true });
}
ClonedAbortSignal.prototype[kDeserialize] = () => {};
Expand Down Expand Up @@ -384,7 +381,7 @@ function abortSignal(signal, reason) {
});
}

class AbortController {
export class AbortController {
#signal;

/**
Expand Down Expand Up @@ -425,7 +422,7 @@ class AbortController {
* @param {AbortSignal} signal
* @returns {AbortSignal}
*/
function transferableAbortSignal(signal) {
export function transferableAbortSignal(signal) {
if (signal?.[kAborted] === undefined)
throw new ERR_INVALID_ARG_TYPE('signal', 'AbortSignal', signal);
lazyMarkTransferMode(signal, false, true);
Expand All @@ -435,7 +432,7 @@ function transferableAbortSignal(signal) {
/**
* Creates an AbortController with a transferable AbortSignal
*/
function transferableAbortController() {
export function transferableAbortController() {
return AbortController[kMakeTransferable]();
}

Expand All @@ -444,7 +441,7 @@ function transferableAbortController() {
* @param {any} resource
* @returns {Promise<void>}
*/
async function aborted(signal, resource) {
export async function aborted(signal, resource) {
if (signal === undefined) {
throw new ERR_INVALID_ARG_TYPE('signal', 'AbortSignal', signal);
}
Expand Down Expand Up @@ -478,12 +475,3 @@ ObjectDefineProperty(AbortController.prototype, SymbolToStringTag, {
configurable: true,
value: 'AbortController',
});

module.exports = {
AbortController,
AbortSignal,
ClonedAbortSignal,
aborted,
transferableAbortSignal,
transferableAbortController,
};
10 changes: 3 additions & 7 deletions src/dom-exception.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
'use strict';

const {
import {
ErrorCaptureStackTrace,
ErrorPrototype,
ObjectDefineProperties,
Expand All @@ -11,7 +9,7 @@ const {
SafeSet,
SymbolToStringTag,
TypeError,
} = primordials;
} from './primordials.js';

function throwInvalidThisError(Base, type) {
const err = new Base();
Expand Down Expand Up @@ -48,7 +46,7 @@ const disusedNamesSet = new SafeSet()
.add('NoDataAllowedError')
.add('ValidationError');

class DOMException {
export class DOMException {
constructor(message = '', options = 'Error') {
ErrorCaptureStackTrace(this);

Expand Down Expand Up @@ -153,5 +151,3 @@ for (const { 0: name, 1: codeName, 2: value } of [
ObjectDefineProperty(DOMException.prototype, codeName, desc);
nameToCodeMap.set(name, value);
}

exports.DOMException = DOMException;
4 changes: 4 additions & 0 deletions src/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ export function hideStackFrames(fn) {
return fn;
}

E('ERR_ILLEGAL_CONSTRUCTOR', 'Illegal constructor', TypeError);

E(
'ERR_INVALID_ARG_TYPE',
(name, expected, actual) => {
Expand Down Expand Up @@ -275,6 +277,8 @@ E(

E('ERR_INVALID_THIS', 'Value of "this" must be of type %s', TypeError);

E('ERR_METHOD_NOT_IMPLEMENTED', 'The %s method is not implemented', Error);

E(
'ERR_MISSING_ARGS',
(...args) => {
Expand Down
10 changes: 10 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
export {
AbortController,
aborted,
AbortSignal,
ClonedAbortSignal,
transferableAbortController,
transferableAbortSignal,
} from './abort-controller.js';
export { DOMException } from './dom-exception.js';
export {
CustomEvent,
defineEventHandler,
Expand All @@ -6,4 +15,5 @@ export {
kWeakHandler,
} from './event-target.js';
export { setMaxListeners } from './events.js';
export { MessageChannel } from './message-channel.js';
export { polyfill } from './polyfill.js';
Loading

0 comments on commit 09b9a82

Please sign in to comment.