Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 1 addition & 0 deletions benchmark/misc/startup-core.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ function main({ n, script, mode }) {
const warmup = 3;
const state = { n, finished: -warmup };
if (mode === 'worker') {
// eslint-disable-next-line no-global-assign
Worker = require('worker_threads').Worker;
spawnWorker(script, bench, state);
} else {
Expand Down
9 changes: 9 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -1527,6 +1527,14 @@ changes:

Enable experimental WebAssembly System Interface (WASI) support.

### `--experimental-web-worker`

<!-- YAML
added: REPLACEME
-->

Enable experimental support for the Web Worker API.

### `--experimental-worker-inspection`

<!-- YAML
Expand Down Expand Up @@ -3868,6 +3876,7 @@ one is included in the list below.
* `--experimental-vfs`
* `--experimental-vm-modules`
* `--experimental-wasi-unstable-preview1`
* `--experimental-web-worker`
* `--force-context-aware`
* `--force-fips`
* `--force-node-api-uncaught-exceptions-policy`
Expand Down
103 changes: 103 additions & 0 deletions doc/api/globals.md
Original file line number Diff line number Diff line change
Expand Up @@ -1310,6 +1310,105 @@ changes:
A browser-compatible implementation of {WebSocket}. Disable this API
with the [`--no-experimental-websocket`][] CLI flag.

## Class: `Worker`

<!-- YAML
added: REPLACEME
-->

> Stability: 1 - Experimental. Enable this API with the
> [`--experimental-web-worker`][] CLI flag.

A browser-compatible implementation of Web Workers of the [HTML Standard][],
implemented on top of [`node:worker_threads`][]. Threads created with it
are given the {DedicatedWorkerGlobalScope} API (`self`,
`name`, `location`, `navigator`, `postMessage()`, `close()`, and
`importScripts()`), in addition to the usual Node.js globals, such as `process`.

```js
// worker.js
addEventListener('message', (event) => {
postMessage(`${event.data} from ${name}!`);
});
```

```js
// main.js
const worker = new Worker('./worker.js', { name: 'greeter' });

worker.addEventListener('message', (event) => {
console.log(event.data); // Prints: Hello from greeter!
worker.terminate();
});

worker.postMessage('Hello');
```

Because their lifetime and sharing model depend on origins and
browsing contexts, Node.js does not currently implement `SharedWorker`.

### Loading worker scripts

Worker scripts are read synchronously from the local file system or from
memory rather than fetched over the network, which changes which URLs are
accepted and how failures are reported:

* `new Worker()` and `importScripts()` accept only `file:`, `data:`, and
`blob:` URLs. Any other scheme makes `new Worker()` throw a
`NotSupportedError` and `importScripts()` throw a `NetworkError`.
* A script that cannot be read makes `importScripts()` throw a `NetworkError`;
for `new Worker()` it fires an `error` event at the `Worker` object.
* Redirects, the `nosniff` check, and HTTP MIME type validation do not apply.
MIME types are validated only for `data:` and `blob:` URLs. The
`credentials` option is validated for API compatibility but has no effect,
since no network request is made.
* On the main thread, relative script URLs are resolved against the current
working directory, because there is no document base URL. Within a worker
they are resolved against the worker's own URL (as is done in the spec).
* For `blob:` URLs, the script must be held in memory, so blobs backed by a file,
such as those returned by [`fs.openAsBlob()`][], cannot be used.

### Differences from the HTML Standard
Comment thread
avivkeller marked this conversation as resolved.

Besides script loading, mentioned above:

* Node.js has no origin model, so same-origin and cross-origin distinctions do
not exist and `location.origin` is `'null'` for every supported scheme.
* `close()` terminates the worker immediately instead of following the
specification's "closing flag" algorithm, so code remaining in the current
task after `close()` is not executed.
* The worker global is the normal Node.js global object with
`DedicatedWorkerGlobalScope` inserted into its prototype chain, rather than
a fresh global created from the interface. Node.js globals such as
`process`, `Buffer`, and `require()` remain available to worker scripts.
* `ErrorEvent`s dispatched at `Worker` instances include `message` and
`error`, but `filename`, `lineno`, and `colno` are always `''`, `0`, and
`0`. An uncaught exception terminates the worker thread, and an unhandled
`error` event is not propagated further: it neither reaches the parent's
global scope nor affects the exit code of the process.
* The following {WorkerGlobalScope} events are never dispatched, although
their handler properties exist: `languagechange`, `online`, and `offline`,
since these concepts do not exist in Node.js; `rejectionhandled` and
`unhandledrejection`, since Node.js exposes the equivalent does not
implement the `PromiseRejectionEvent` interface or the per-rejection
`preventDefault()` behavior required by the HTML Standard.

### Web Workers and `node:worker_threads`

Every Web Worker is backed by a [`node:worker_threads`][] {Worker}, so the
two APIs share their threading, structured clone, and transfer semantics.
Inside a worker, \[`worker_threads.parentPort`]\[] is the port behind
`self.postMessage()` and the worker's `message` events, `isMainThread` is
`false`, and `workerData` is `undefined`.

As a rule of thumb, use [`node:worker_threads`][] directly when a program
needs `workerData`, a custom `env` or `execArgv`, resource limits, stdio
redirection, the `'online'` and `'exit'` events, or `worker.threadId`;
`Worker` accepts only the `name`, `type`, and `credentials` options and,
per the specification, its `terminate()` returns `undefined`, rather than
a promise. Threads started through [`node:worker_threads`][] are ordinary
Node.js threads and do not get the worker global scope APIs.

## Class: `WritableStream`

<!-- YAML
Expand Down Expand Up @@ -1355,10 +1454,12 @@ A browser-compatible implementation of [`WritableStreamDefaultWriter`][].
[CommonJS module]: modules.md
[CommonJS modules]: modules.md
[ECMAScript module]: esm.md
[HTML Standard]: https://html.spec.whatwg.org/multipage/workers.html
[Navigator API]: https://html.spec.whatwg.org/multipage/system-state.html#the-navigator-object
[RFC 5646]: https://www.rfc-editor.org/rfc/rfc5646.txt
[Web Crypto API]: webcrypto.md
[`--experimental-eventsource`]: cli.md#--experimental-eventsource
[`--experimental-web-worker`]: cli.md#--experimental-web-worker
[`--localstorage-file`]: cli.md#--localstorage-filefile
[`--no-experimental-global-navigator`]: cli.md#--no-experimental-global-navigator
[`--no-experimental-websocket`]: cli.md#--no-experimental-websocket
Expand Down Expand Up @@ -1410,9 +1511,11 @@ A browser-compatible implementation of [`WritableStreamDefaultWriter`][].
[`console`]: console.md
[`exports`]: modules.md#exports
[`fetch()`]: https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch
[`fs.openAsBlob()`]: fs.md#fsopenasblobpath-options
[`globalThis`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/globalThis
[`localStorage`]: https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage
[`module`]: modules.md#module
[`node:worker_threads`]: worker_threads.md
[`perf_hooks.performance`]: perf_hooks.md#perf_hooksperformance
[`process.nextTick()`]: process.md#processnexttickcallback-args
[`process` object]: process.md#process
Expand Down
5 changes: 5 additions & 0 deletions doc/node.1
Original file line number Diff line number Diff line change
Expand Up @@ -820,6 +820,9 @@ Enable experimental ES Module support in the \fBnode:vm\fR module.
.It Fl -experimental-wasi-unstable-preview1
Enable experimental WebAssembly System Interface (WASI) support.
.
.It Fl -experimental-web-worker
Enable experimental support for the Web Worker API.
.
.It Fl -experimental-worker-inspection
Enable experimental support for the worker inspection with Chrome DevTools.
.
Expand Down Expand Up @@ -2009,6 +2012,8 @@ one is included in the list below.
.It
\fB--experimental-wasi-unstable-preview1\fR
.It
\fB--experimental-web-worker\fR
.It
\fB--force-context-aware\fR
.It
\fB--force-fips\fR
Expand Down
1 change: 1 addition & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ export default [
WritableStreamDefaultWriter: 'readonly',
WritableStreamDefaultController: 'readonly',
WebSocket: 'readonly',
Worker: 'readonly',
},
},
},
Expand Down
43 changes: 40 additions & 3 deletions lib/internal/blob.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const {
} = internalBinding('buffer');

const {
TextDecoder,
getUtf8Decoder,
TextEncoder,
} = require('internal/encoding');
const { URL } = require('internal/url');
Expand Down Expand Up @@ -88,7 +88,6 @@ let ReadableStream;
let TextDecoderStream;

const enc = new TextEncoder();
let dec;

// Yes, lazy loading is annoying but because of circular
// references between the url, internal/blob, and buffer
Expand Down Expand Up @@ -312,7 +311,7 @@ class Blob {
if (!isBlob(this))
return PromiseReject(new ERR_INVALID_THIS('Blob'));

dec ??= new TextDecoder();
const dec = getUtf8Decoder();

return PromisePrototypeThen(
arrayBuffer(this),
Expand Down Expand Up @@ -466,6 +465,43 @@ function arrayBuffer(blob) {
return promise;
}

/**
* Read a blob's data synchronously. This is only possible when every part
* of the blob is memory-resident, in which case the reader's pull callbacks
* are invoked synchronously; otherwise (e.g. for file-backed blobs)
* undefined is returned.
* @param {Blob} blob
* @returns {ArrayBuffer|undefined}
*/
function getBlobDataSync(blob) {
const reader = blob[kHandle].getReader();
const buffers = [];
let result;
let ended = false;
while (!ended) {
let sync = false;
reader.pull((status, buffer) => {
sync = true;
if (status === 0) {
// EOS; buffer should be undefined here.
result = concat(buffers);
ended = true;
return;
} else if (status < 0) {
ended = true;
return;
}
if (buffer !== undefined)
ArrayPrototypePush(buffers, buffer);
});
if (!sync) {
// The data is not available synchronously.
break;
}
}
return result;
}

function createBlobReaderStream(reader) {
return new lazyReadableStream({
type: 'bytes',
Expand Down Expand Up @@ -644,6 +680,7 @@ module.exports = {
createBlobFromFilePath,
createBlobReaderIterable,
createBlobReaderStream,
getBlobDataSync,
isBlob,
kHandle,
resolveObjectURL,
Expand Down
2 changes: 2 additions & 0 deletions lib/internal/bootstrap/web/exposed-window-or-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ exposeLazyInterfaces(globalThis, 'internal/worker/io', ['BroadcastChannel']);
exposeLazyInterfaces(globalThis, 'internal/worker/io', [
'MessageChannel', 'MessagePort',
]);
// https://html.spec.whatwg.org/multipage/workers.html#dedicated-workers-and-the-worker-interface
exposeLazyInterfaces(globalThis, 'internal/webworker', ['Worker']);
// https://www.w3.org/TR/FileAPI/#dfn-Blob
exposeLazyInterfaces(globalThis, 'internal/blob', ['Blob']);
// https://www.w3.org/TR/FileAPI/#dfn-file
Expand Down
9 changes: 9 additions & 0 deletions lib/internal/encoding.js
Original file line number Diff line number Diff line change
Expand Up @@ -614,8 +614,17 @@ ObjectDefineProperties(TextDecoder.prototype, {
},
});

// A lazily created TextDecoder for the common case of decoding UTF-8 with
// the default options, shared between internal modules.
let utf8Decoder;
function getUtf8Decoder() {
utf8Decoder ??= new TextDecoder();
return utf8Decoder;
}

module.exports = {
getEncodingFromLabel,
getUtf8Decoder,
TextDecoder,
TextEncoder,
};
12 changes: 11 additions & 1 deletion lib/internal/event_target.js
Original file line number Diff line number Diff line change
Expand Up @@ -722,7 +722,10 @@ class EventTarget {
return;

type = webidl.converters.DOMString(type);
const capture = options?.capture === true;
// Flatten the options argument like addEventListener does.
// Refs: https://dom.spec.whatwg.org/#concept-flatten-options
const capture = typeof options === 'boolean' ?
options : options?.capture === true;

if (this[kEvents] === undefined)
return;
Expand Down Expand Up @@ -1163,6 +1166,13 @@ function defineEventHandler(emitter, name, event = name) {

function set(value) {
validateThisInternalField(this, kHandlers, 'EventTarget');
// Event handler IDL attributes are [LegacyTreatNonObjectAsNull]: values
// that are neither callable nor objects deactivate the handler.
// Refs: https://html.spec.whatwg.org/multipage/webappapis.html#event-handler-idl-attributes
if (typeof value !== 'function' &&
(typeof value !== 'object' || value === null)) {
value = null;
}
if (this[kHandlers] === undefined)
this[kHandlers] = new SafeMap();
let wrappedHandler = this[kHandlers].get(event);
Expand Down
15 changes: 14 additions & 1 deletion lib/internal/main/worker_thread.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ port.on('message', (message) => {
hasStdin,
publicPort,
workerData,
webWorkerData,
mainThreadPort,
} = message;

Expand All @@ -116,6 +117,11 @@ port.on('message', (message) => {
require('internal/worker').assignEnvironmentData(environmentData);
setupMainThreadPort(mainThreadPort);

if (webWorkerData !== undefined) {
require('internal/webworker')
.installDedicatedWorkerGlobalScope(webWorkerData.url, webWorkerData);
}

// The counter is only passed to the workers created by the main thread,
// not to workers created by other workers.
let cachedCwd = '';
Expand Down Expand Up @@ -155,7 +161,14 @@ port.on('message', (message) => {
break;
}

case 'classic': if (getOptionValue('--input-type') !== 'module') {
case 'classic': if (webWorkerData?.source !== undefined) {
// The source of a classic web Worker script loaded from a blob: or
// data: URL. Unlike a worker eval, "run a classic script" evaluates
// the source in the worker's global scope.
require('internal/webworker')
.runClassicScriptSource(webWorkerData.source, webWorkerData.url);
break;
} else if (getOptionValue('--input-type') !== 'module') {
const name = '[worker eval]';
// This is necessary for CJS module compilation.
// TODO: pass this with something really internal.
Expand Down
6 changes: 1 addition & 5 deletions lib/internal/modules/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -446,8 +446,6 @@ function assertBufferSource(body, allowString, hookName) {
);
}

let DECODER = null;

/**
* Converts a buffer or buffer-like object to a string.
* @param {string | ArrayBuffer | ArrayBufferView} body - The buffer or buffer-like object to convert to a string.
Expand All @@ -456,9 +454,7 @@ let DECODER = null;
function stringify(body) {
if (typeof body === 'string') { return body; }
assertBufferSource(body, false, 'load');
const { TextDecoder } = require('internal/encoding');
DECODER = DECODER === null ? new TextDecoder() : DECODER;
return DECODER.decode(body);
return require('internal/encoding').getUtf8Decoder().decode(body);
}

/**
Expand Down
1 change: 1 addition & 0 deletions lib/internal/navigator.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ ObjectDefineProperties(Navigator.prototype, {

module.exports = {
getNavigatorPlatform,
kInitialize,
navigator: new Navigator(kInitialize),
Navigator,
};
Loading
Loading