Skip to content
Merged
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
42 changes: 42 additions & 0 deletions benchmark/sqlite/sqlite-diagnostic-channel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
'use strict';
const common = require('../common.js');
const sqlite = require('node:sqlite');
const dc = require('node:diagnostics_channel');
const assert = require('node:assert');

const bench = common.createBenchmark(main, {
n: [1e5],
mode: ['none', 'subscribed', 'unsubscribed'],
});

function main(conf) {
const { n, mode } = conf;

const db = new sqlite.DatabaseSync(':memory:');
db.exec('CREATE TABLE t (x INTEGER)');
const insert = db.prepare('INSERT INTO t VALUES (?)');

let subscriber;
if (mode === 'subscribed') {
subscriber = () => {};
dc.subscribe('sqlite.db.query', subscriber);
} else if (mode === 'unsubscribed') {
subscriber = () => {};
dc.subscribe('sqlite.db.query', subscriber);
dc.unsubscribe('sqlite.db.query', subscriber);
}
// mode === 'none': no subscription ever made

let result;
bench.start();
for (let i = 0; i < n; i++) {
result = insert.run(i);
}
bench.end(n);

if (mode === 'subscribed') {
dc.unsubscribe('sqlite.db.query', subscriber);
}

assert.ok(result !== undefined);
}
9 changes: 9 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -1563,6 +1563,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 @@ -3889,6 +3897,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
39 changes: 39 additions & 0 deletions doc/api/diagnostics_channel.md
Original file line number Diff line number Diff line change
Expand Up @@ -1924,10 +1924,47 @@ added: v16.18.0

Emitted when a new thread is created.

#### SQLite

<!-- YAML
added: REPLACEME
-->

> Stability: 1 - Experimental

##### Event: `'sqlite.db.query'`

* `sql` {string} The expanded SQL with bound parameter values substituted.
If expansion fails, the source SQL with unsubstituted placeholders is used
instead.
* `database` {DatabaseSync} The [`DatabaseSync`][] instance that executed the
statement.
* `duration` {number} SQLite's internal estimate of the statement run time in
nanoseconds. This reflects C-layer execution time only and does not include
JavaScript binding overhead such as argument marshaling or result-row
construction.

Emitted after a SQL statement finishes executing against a [`DatabaseSync`][]
instance. This is a **profiling** event: it fires once per statement upon
completion and reports an estimated duration from SQLite's internal profiler.
It is not a distributed-tracing span. There is no corresponding start event,
no async context propagation, and no parent-span linkage. If you need
OpenTelemetry-compatible spans or async context propagation, wrap your SQLite
calls with a [`TracingChannel`][] at the JavaScript layer instead.

Publishing is zero-overhead when there are no subscribers.

No event is emitted for a statement that is abandoned mid-iteration and later
finalized, either explicitly through [`statement.close()`][] or when the
statement is garbage collected. Subscribers must not close the database or the
statement, since both are still in use while the event is being delivered; see
[`database.close()`][] and [`statement.close()`][].

[BoundedChannel Channels]: #boundedchannel-channels
[TracingChannel Channels]: #tracingchannel-channels
[`'uncaughtException'`]: process.md#event-uncaughtexception
[`BoundedChannel`]: #class-boundedchannel
[`DatabaseSync`]: sqlite.md#class-databasesync
[`TracingChannel`]: #class-tracingchannel
[`asyncEnd` event]: #asyncendevent
[`asyncStart` event]: #asyncstartevent
Expand All @@ -1938,6 +1975,7 @@ Emitted when a new thread is created.
[`channel.unsubscribe(onMessage)`]: #channelunsubscribeonmessage
[`channel.withStoreScope(data)`]: #channelwithstorescopedata
[`child_process.spawn()`]: child_process.md#child_processspawncommand-args-options
[`database.close()`]: sqlite.md#databaseclose
[`diagnostics_channel.channel(name)`]: #diagnostics_channelchannelname
[`diagnostics_channel.subscribe(name, onMessage)`]: #diagnostics_channelsubscribename-onmessage
[`diagnostics_channel.tracingChannel()`]: #diagnostics_channeltracingchannelnameorchannels
Expand All @@ -1947,6 +1985,7 @@ Emitted when a new thread is created.
[`net.Server.listen()`]: net.md#serverlisten
[`process.execve()`]: process.md#processexecvefile-args-env
[`start` event]: #startevent
[`statement.close()`]: sqlite.md#statementclose
[`worker_threads.locks`]: worker_threads.md#worker_threadslocks
[context loss]: async_context.md#troubleshooting-context-loss
[thenable object]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise#thenables
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 mostly 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

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
22 changes: 17 additions & 5 deletions doc/api/sqlite.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ import sqlite from 'node:sqlite';
const sqlite = require('node:sqlite');
```

This module is only available under the `node:` scheme.
This module is only available under the `node:` scheme. SQL trace events can
be observed via the [`diagnostics_channel`][] module. See
[`'sqlite.db.query'`][] for details.

The following example shows the basic usage of the `node:sqlite` module to open
an in-memory database, write data to the database, and then read the data back.
Expand Down Expand Up @@ -311,8 +313,8 @@ added: v22.5.0
Closes the database connection. An exception is thrown if the database is not
open. An [`ERR_INVALID_STATE`][] error is thrown if the method is called while
a statement is executing, such as inside a user-defined function, an aggregate
function, or an authorizer callback. This method is a wrapper around
[`sqlite3_close_v2()`][].
function, an authorizer callback, or a [`'sqlite.db.query'`][] subscriber. This
method is a wrapper around [`sqlite3_close_v2()`][].

### `database.loadExtension(path[, entryPoint])`

Expand Down Expand Up @@ -1122,7 +1124,12 @@ added: REPLACEME
-->

Finalizes the prepared statement. An exception is thrown if the statement is
already finalized. This method is a wrapper around [`sqlite3_finalize()`][].
already finalized. An [`ERR_INVALID_STATE`][] error is thrown if this statement
is currently executing, which happens when the method is called from a callback
that the statement itself triggered, such as a user-defined function, an
aggregate function, or a [`'sqlite.db.query'`][] subscriber. Other statements
on the same connection can be finalized from such a callback. This method is a
wrapper around [`sqlite3_finalize()`][].

### `statement.columns()`

Expand Down Expand Up @@ -1369,7 +1376,9 @@ added: REPLACEME
-->

Finalizes the prepared statement. If the prepared statement is already
finalized, then this is a no-op.
finalized, then this is a no-op. An [`ERR_INVALID_STATE`][] error is thrown if
this statement is currently executing, under the same conditions as
[`statement.close()`][].

### `statement.stat(counter)`

Expand Down Expand Up @@ -1890,6 +1899,7 @@ callback function to indicate what type of operation is being authorized.
[Run-Time Limits]: https://www.sqlite.org/c3ref/limit.html
[SQL injection]: https://en.wikipedia.org/wiki/SQL_injection
[Type conversion between JavaScript and SQLite]: #type-conversion-between-javascript-and-sqlite
[`'sqlite.db.query'`]: diagnostics_channel.md#event-sqlitedbquery
[`ATTACH DATABASE`]: https://www.sqlite.org/lang_attach.html
[`ERR_INVALID_STATE`]: errors.md#err_invalid_state
[`PRAGMA foreign_keys`]: https://www.sqlite.org/pragma.html#pragma_foreign_keys
Expand All @@ -1903,6 +1913,7 @@ callback function to indicate what type of operation is being authorized.
[`database.createTagStore()`]: #databasecreatetagstoremaxsize
[`database.serialize()`]: #databaseserializedbname
[`database.setAuthorizer()`]: #databasesetauthorizercallback
[`diagnostics_channel`]: diagnostics_channel.md
[`sqlite3_backup_finish()`]: https://www.sqlite.org/c3ref/backup_finish.html#sqlite3backupfinish
[`sqlite3_backup_init()`]: https://www.sqlite.org/c3ref/backup_finish.html#sqlite3backupinit
[`sqlite3_backup_step()`]: https://www.sqlite.org/c3ref/backup_finish.html#sqlite3backupstep
Expand Down Expand Up @@ -1934,6 +1945,7 @@ callback function to indicate what type of operation is being authorized.
[`sqlite3session_create()`]: https://www.sqlite.org/session/sqlite3session_create.html
[`sqlite3session_delete()`]: https://www.sqlite.org/session/sqlite3session_delete.html
[`sqlite3session_patchset()`]: https://www.sqlite.org/session/sqlite3session_patchset.html
[`statement.close()`]: #statementclose
[`statement.setAllowBareNamedParameters()`]: #statementsetallowbarenamedparametersenabled
[`statement.setAllowUnknownNamedParameters()`]: #statementsetallowunknownnamedparametersenabled
[`statement.stat()`]: #statementstatcounter
Expand Down
5 changes: 5 additions & 0 deletions doc/node.1
Original file line number Diff line number Diff line change
Expand Up @@ -836,6 +836,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 @@ -2022,6 +2025,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
8 changes: 8 additions & 0 deletions lib/diagnostics_channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,19 @@ function markActive(channel) {
ObjectSetPrototypeOf(channel, ActiveChannel.prototype);
channel._subscribers = [];
channel._stores = new SafeMap();

// Notify native modules that this channel just got its first subscriber.
if (channel._index !== undefined)
dc_binding.notifyChannelActive(channel._index);
}

function maybeMarkInactive(channel) {
// When there are no more active subscribers or bound, restore to fast prototype.
if (!channel._subscribers.length && !channel._stores.size) {
// Notify native modules that this channel just lost its last subscriber.
if (channel._index !== undefined)
dc_binding.notifyChannelInactive(channel._index);

// eslint-disable-next-line no-use-before-define
ObjectSetPrototypeOf(channel, Channel.prototype);
channel._subscribers = undefined;
Expand Down
4 changes: 4 additions & 0 deletions lib/eslint.config_partial.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,10 @@ export default [
name: 'WebAssembly',
message: 'Use `const { WebAssembly } = globalThis;` instead of the global.',
},
{
name: 'Worker',
message: "Use `const { Worker } = require('internal/webworker');` instead of the global.",
},
{
name: 'WritableStream',
message: "Use `const { WritableStream } = require('internal/webstreams/writablestream')` instead of the global.",
Expand Down
Loading
Loading