Commit f344e7f552e for nodejs

commit f344e7f552e3656bd37517151c5dc87d22b33771
Author: Matteo Collina <hello@matteocollina.com>
Date:   Mon Nov 9 07:49:29 2026 +0100

    worker: preload streams into startup snapshot

    Worker stdio loads stream after restoring the main-thread context,
    initializing its constructors and prototype methods on every startup.
    Preload stream during snapshot construction so workers can restore that
    initialized state instead. Worker-specific stdio instances and message
    ports are still created at runtime.

    Verify snapshot membership, stdio methods, and isolation from
    parent-thread stream mutations.

    Assisted-by: pi
    Signed-off-by: Matteo Collina <hello@matteocollina.com>
    PR-URL: https://github.com/nodejs/node/pull/66311
    Reviewed-By: James M Snell <jasnell@gmail.com>
    Reviewed-By: Robert Nagy <ronagy@icloud.com>

diff --git a/lib/internal/bootstrap/switches/is_main_thread.js b/lib/internal/bootstrap/switches/is_main_thread.js
index e9e37275b17..3e18fe6e322 100644
--- a/lib/internal/bootstrap/switches/is_main_thread.js
+++ b/lib/internal/bootstrap/switches/is_main_thread.js
@@ -312,6 +312,9 @@ if (isBuildingSnapshot()) {
   require('internal/modules/typescript');
   require('internal/blob');
   require('internal/dns/utils');
+  // Workers restore this context before loading internal/worker/io, which
+  // needs the initialized stream constructors for its stdio subclasses.
+  require('stream');
 }

 // Needed to refresh the time origin.
diff --git a/lib/internal/streams/duplex.js b/lib/internal/streams/duplex.js
index dce8bd6e0fd..6d31f62d099 100644
--- a/lib/internal/streams/duplex.js
+++ b/lib/internal/streams/duplex.js
@@ -46,15 +46,14 @@ const {
 const destroyImpl = require('internal/streams/destroy');
 const { kOnConstructed } = require('internal/streams/utils');

-ObjectSetPrototypeOf(Duplex.prototype, Readable.prototype);
-ObjectSetPrototypeOf(Duplex, Readable);
-
 {
   const keys = ObjectKeys(Writable.prototype);
   // Allow the keys array to be GC'ed.
   for (let i = 0; i < keys.length; i++) {
     const method = keys[i];
-    Duplex.prototype[method] ||= Writable.prototype[method];
+    if (!Readable.prototype[method]) {
+      Duplex.prototype[method] = Writable.prototype[method];
+    }
   }
 }

@@ -201,3 +200,8 @@ Duplex.from = function(body) {
   duplexify ??= require('internal/streams/duplexify');
   return duplexify(body, 'body');
 };
+
+// Define own methods before inheriting from constructors or prototypes that
+// user code may have frozen before this module was loaded.
+ObjectSetPrototypeOf(Duplex.prototype, Readable.prototype);
+ObjectSetPrototypeOf(Duplex, Readable);
diff --git a/lib/internal/streams/operators.js b/lib/internal/streams/operators.js
index a5cbe1af428..87b82b65b56 100644
--- a/lib/internal/streams/operators.js
+++ b/lib/internal/streams/operators.js
@@ -6,10 +6,13 @@ const {
   MathFloor,
   Number,
   NumberIsNaN,
+  ObjectDefineProperty,
+  ObjectKeys,
   Promise,
   PromisePrototypeThen,
   PromiseReject,
   PromiseResolve,
+  ReflectApply,
   Symbol,
 } = primordials;

@@ -18,6 +21,7 @@ const { AbortController, AbortSignal } = require('internal/abort_controller');
 const {
   AbortError,
   codes: {
+    ERR_ILLEGAL_CONSTRUCTOR,
     ERR_MISSING_ARGS,
     ERR_OUT_OF_RANGE,
   },
@@ -31,6 +35,7 @@ const {
 const { kWeakHandler, kResistStopPropagation } = require('internal/event_target');
 const destroyImpl = require('internal/streams/destroy');
 const { finished } = require('internal/streams/end-of-stream');
+const Stream = require('stream');

 const kEmpty = Symbol('kEmpty');
 const kEof = Symbol('kEof');
@@ -373,7 +378,7 @@ function take(number, options = undefined) {
   }.call(this);
 }

-module.exports.streamReturningOperators = {
+const streamReturningOperators = {
   drop,
   filter,
   flatMap,
@@ -381,7 +386,7 @@ module.exports.streamReturningOperators = {
   take,
 };

-module.exports.promiseReturningOperators = {
+const promiseReturningOperators = {
   every,
   forEach,
   reduce,
@@ -389,3 +394,33 @@ module.exports.promiseReturningOperators = {
   some,
   find,
 };
+
+const streamKeys = ObjectKeys(streamReturningOperators);
+for (let i = 0; i < streamKeys.length; i++) {
+  const key = streamKeys[i];
+  const op = streamReturningOperators[key];
+  function fn(...args) {
+    if (new.target) {
+      throw new ERR_ILLEGAL_CONSTRUCTOR();
+    }
+    return Stream.Readable.from(ReflectApply(op, this, args));
+  }
+  ObjectDefineProperty(fn, 'name', { __proto__: null, value: op.name });
+  ObjectDefineProperty(fn, 'length', { __proto__: null, value: op.length });
+  module.exports[key] = fn;
+}
+
+const promiseKeys = ObjectKeys(promiseReturningOperators);
+for (let i = 0; i < promiseKeys.length; i++) {
+  const key = promiseKeys[i];
+  const op = promiseReturningOperators[key];
+  function fn(...args) {
+    if (new.target) {
+      throw new ERR_ILLEGAL_CONSTRUCTOR();
+    }
+    return ReflectApply(op, this, args);
+  }
+  ObjectDefineProperty(fn, 'name', { __proto__: null, value: op.name });
+  ObjectDefineProperty(fn, 'length', { __proto__: null, value: op.length });
+  module.exports[key] = fn;
+}
diff --git a/lib/internal/streams/passthrough.js b/lib/internal/streams/passthrough.js
index d37f9caf011..58736129511 100644
--- a/lib/internal/streams/passthrough.js
+++ b/lib/internal/streams/passthrough.js
@@ -32,8 +32,6 @@ const {
 module.exports = PassThrough;

 const Transform = require('internal/streams/transform');
-ObjectSetPrototypeOf(PassThrough.prototype, Transform.prototype);
-ObjectSetPrototypeOf(PassThrough, Transform);

 function PassThrough(options) {
   if (!(this instanceof PassThrough))
@@ -45,3 +43,7 @@ function PassThrough(options) {
 PassThrough.prototype._transform = function(chunk, encoding, cb) {
   cb(null, chunk);
 };
+
+// Install the override before inheriting a potentially frozen prototype.
+ObjectSetPrototypeOf(PassThrough.prototype, Transform.prototype);
+ObjectSetPrototypeOf(PassThrough, Transform);
diff --git a/lib/internal/streams/pipeline.js b/lib/internal/streams/pipeline.js
index 546ff579d8e..6937d435434 100644
--- a/lib/internal/streams/pipeline.js
+++ b/lib/internal/streams/pipeline.js
@@ -5,13 +5,17 @@

 const {
   ArrayIsArray,
+  ObjectDefineProperty,
   Promise,
   SymbolAsyncIterator,
   SymbolDispose,
 } = primordials;

 const { eos } = require('internal/streams/end-of-stream');
-const { once } = require('internal/util');
+const {
+  once,
+  promisify: { custom: customPromisify },
+} = require('internal/util');
 const destroyImpl = require('internal/streams/destroy');
 const Duplex = require('internal/streams/duplex');
 const {
@@ -469,4 +473,12 @@ function pipe(src, dst, finish, finishOnlyHandleError, { end }) {
   return eos(dst, { readable: false, writable: true }, finish);
 }

+ObjectDefineProperty(pipeline, customPromisify, {
+  __proto__: null,
+  enumerable: true,
+  get() {
+    return require('stream/promises').pipeline;
+  },
+});
+
 module.exports = { pipelineImpl, pipeline };
diff --git a/lib/internal/streams/transform.js b/lib/internal/streams/transform.js
index 2f4d498bc78..2d16f2568ed 100644
--- a/lib/internal/streams/transform.js
+++ b/lib/internal/streams/transform.js
@@ -74,8 +74,6 @@ const {
 } = require('internal/errors').codes;
 const Duplex = require('internal/streams/duplex');
 const { getHighWaterMark } = require('internal/streams/state');
-ObjectSetPrototypeOf(Transform.prototype, Duplex.prototype);
-ObjectSetPrototypeOf(Transform, Duplex);

 const kCallback = Symbol('kCallback');

@@ -202,3 +200,7 @@ Transform.prototype._read = function() {
     callback();
   }
 };
+
+// Install overrides before inheriting potentially frozen prototype methods.
+ObjectSetPrototypeOf(Transform.prototype, Duplex.prototype);
+ObjectSetPrototypeOf(Transform, Duplex);
diff --git a/lib/stream.js b/lib/stream.js
index 078fa092f48..5538df766a7 100644
--- a/lib/stream.js
+++ b/lib/stream.js
@@ -23,32 +23,20 @@

 const {
   ObjectDefineProperty,
-  ObjectKeys,
-  ReflectApply,
+  ObjectSetPrototypeOf,
 } = primordials;

 const {
+  defineLazyProperties,
   promisify: { custom: customPromisify },
 } = require('internal/util');

-const {
-  streamReturningOperators,
-  promiseReturningOperators,
-} = require('internal/streams/operators');
-
-const {
-  codes: {
-    ERR_ILLEGAL_CONSTRUCTOR,
-  },
-} = require('internal/errors');
-const compose = require('internal/streams/compose');
 const { setDefaultHighWaterMark, getDefaultHighWaterMark } = require('internal/streams/state');
-const { pipeline } = require('internal/streams/pipeline');
 const { destroyer } = require('internal/streams/destroy');
 const { eos } = require('internal/streams/end-of-stream');
 const internalBuffer = require('internal/buffer');

-const promises = require('stream/promises');
+let promises;
 const utils = require('internal/streams/utils');
 const { isArrayBufferView, isUint8Array } = require('internal/util/types');

@@ -61,57 +49,29 @@ Stream.isReadable = utils.isReadable;
 Stream.isWritable = utils.isWritable;

 Stream.Readable = require('internal/streams/readable');
-const streamKeys = ObjectKeys(streamReturningOperators);
-for (let i = 0; i < streamKeys.length; i++) {
-  const key = streamKeys[i];
-  const op = streamReturningOperators[key];
-  function fn(...args) {
-    if (new.target) {
-      throw new ERR_ILLEGAL_CONSTRUCTOR();
-    }
-    return Stream.Readable.from(ReflectApply(op, this, args));
-  }
-  ObjectDefineProperty(fn, 'name', { __proto__: null, value: op.name });
-  ObjectDefineProperty(fn, 'length', { __proto__: null, value: op.length });
-  ObjectDefineProperty(Stream.Readable.prototype, key, {
-    __proto__: null,
-    value: fn,
-    enumerable: false,
-    configurable: true,
-    writable: true,
-  });
-}
-const promiseKeys = ObjectKeys(promiseReturningOperators);
-for (let i = 0; i < promiseKeys.length; i++) {
-  const key = promiseKeys[i];
-  const op = promiseReturningOperators[key];
-  function fn(...args) {
-    if (new.target) {
-      throw new ERR_ILLEGAL_CONSTRUCTOR();
-    }
-    return ReflectApply(op, this, args);
-  }
-  ObjectDefineProperty(fn, 'name', { __proto__: null, value: op.name });
-  ObjectDefineProperty(fn, 'length', { __proto__: null, value: op.length });
-  ObjectDefineProperty(Stream.Readable.prototype, key, {
-    __proto__: null,
-    value: fn,
-    enumerable: false,
-    configurable: true,
-    writable: true,
-  });
-}
+defineLazyProperties(Stream.Readable.prototype, 'internal/streams/operators', [
+  'drop', 'filter', 'flatMap', 'map', 'take',
+  'every', 'forEach', 'reduce', 'toArray', 'some', 'find',
+], false);
 Stream.Writable = require('internal/streams/writable');
-Stream.Duplex = require('internal/streams/duplex');
-Stream.Transform = require('internal/streams/transform');
-Stream.PassThrough = require('internal/streams/passthrough');
-Stream.duplexPair = require('internal/streams/duplexpair');
-Stream.pipeline = pipeline;
+// These modules export a single function rather than named properties.
+// Set the prototype after creation to retain fast properties.
+const lazy = ObjectSetPrototypeOf({
+  Duplex: 'internal/streams/duplex',
+  Transform: 'internal/streams/transform',
+  PassThrough: 'internal/streams/passthrough',
+  duplexPair: 'internal/streams/duplexpair',
+  compose: 'internal/streams/compose',
+}, null);
+defineLazyProperties(Stream, lazy, [
+  'Duplex', 'Transform', 'PassThrough', 'duplexPair',
+]);
+defineLazyProperties(Stream, 'internal/streams/pipeline', ['pipeline']);
 const { addAbortSignal } = require('internal/streams/add-abort-signal');
 Stream.addAbortSignal = addAbortSignal;
 Stream.finished = eos;
 Stream.destroy = destroyer;
-Stream.compose = compose;
+defineLazyProperties(Stream, lazy, ['compose']);
 Stream.setDefaultHighWaterMark = setDefaultHighWaterMark;
 Stream.getDefaultHighWaterMark = getDefaultHighWaterMark;

@@ -120,15 +80,7 @@ ObjectDefineProperty(Stream, 'promises', {
   configurable: true,
   enumerable: true,
   get() {
-    return promises;
-  },
-});
-
-ObjectDefineProperty(pipeline, customPromisify, {
-  __proto__: null,
-  enumerable: true,
-  get() {
-    return promises.pipeline;
+    return promises ??= require('stream/promises');
   },
 });

@@ -136,6 +88,7 @@ ObjectDefineProperty(eos, customPromisify, {
   __proto__: null,
   enumerable: true,
   get() {
+    promises ??= require('stream/promises');
     return promises.finished;
   },
 });
diff --git a/lib/stream/promises.js b/lib/stream/promises.js
index 43c378a20f2..903ea560707 100644
--- a/lib/stream/promises.js
+++ b/lib/stream/promises.js
@@ -12,12 +12,13 @@ const {
   isWebStream,
 } = require('internal/streams/utils');

-const { pipelineImpl: pl } = require('internal/streams/pipeline');
+let pl;
 const { finished } = require('internal/streams/end-of-stream');

 require('stream');

 function pipeline(...streams) {
+  pl ??= require('internal/streams/pipeline').pipelineImpl;
   return new Promise((resolve, reject) => {
     let signal;
     let end;
diff --git a/src/node_util.cc b/src/node_util.cc
index 06b743d0ac9..e19ba77e264 100644
--- a/src/node_util.cc
+++ b/src/node_util.cc
@@ -352,6 +352,7 @@ static void IsInsideNodeModules(const FunctionCallbackInfo<Value>& args) {
   args.GetReturnValue().Set(result);
 }

+template <bool return_module>
 static void DefineLazyPropertiesGetter(
     Local<v8::Name> name, const v8::PropertyCallbackInfo<Value>& info) {
   Isolate* isolate = info.GetIsolate();
@@ -381,6 +382,10 @@ static void DefineLazyPropertiesGetter(
     // V8 will have scheduled an error to be thrown.
     return;
   }
+  if constexpr (return_module) {
+    info.GetReturnValue().Set(require_result);
+    return;
+  }
   Local<Value> ret;
   if (!require_result.As<v8::Object>()->Get(context, name).ToLocal(&ret)) {
     // V8 will have scheduled an error to be thrown.
@@ -390,13 +395,15 @@ static void DefineLazyPropertiesGetter(
 }

 static void DefineLazyProperties(const FunctionCallbackInfo<Value>& args) {
-  // target: object, id: string, keys: string[][, enumerable = true]
+  // target: object, source: string | object, keys: string[],
+  // enumerable: boolean = true
   CHECK_GE(args.Length(), 3);
   // target: Object where to define the lazy properties.
   CHECK(args[0]->IsObject());
-  // id: Internal module to lazy-load where the API to expose are implemented.
-  CHECK(args[1]->IsString());
-  // keys: Keys to map from `require(id)` and `target`.
+  // source: Internal module ID for named exports, or a map from property names
+  // to module IDs for modules whose entire exports object is the lazy value.
+  CHECK(args[1]->IsString() || args[1]->IsObject());
+  // keys: Property names to expose on `target`.
   CHECK(args[2]->IsArray());
   // enumerable: Whether the property should be enumerable.
   CHECK(args.Length() == 3 || args[3]->IsBoolean());
@@ -404,7 +411,7 @@ static void DefineLazyProperties(const FunctionCallbackInfo<Value>& args) {
   auto context = args.GetIsolate()->GetCurrentContext();

   auto target = args[0].As<Object>();
-  Local<Value> id = args[1];
+  Local<Value> source = args[1];
   v8::PropertyAttribute attribute =
       args.Length() == 3 || args[3]->IsTrue() ? v8::None : v8::DontEnum;

@@ -417,12 +424,19 @@ static void DefineLazyProperties(const FunctionCallbackInfo<Value>& args) {
       return;
     }
     CHECK(key->IsString());
+    Local<Value> id = source;
+    auto getter = DefineLazyPropertiesGetter<false>;
+    if (source->IsObject()) {
+      if (!source.As<Object>()->Get(context, key).ToLocal(&id)) {
+        // V8 will have scheduled an error to be thrown.
+        return;
+      }
+      CHECK(id->IsString());
+      getter = DefineLazyPropertiesGetter<true>;
+    }
     if (target
-            ->SetLazyDataProperty(context,
-                                  key.As<String>(),
-                                  DefineLazyPropertiesGetter,
-                                  id,
-                                  attribute)
+            ->SetLazyDataProperty(
+                context, key.As<String>(), getter, id, attribute)
             .IsNothing()) {
       // V8 will have scheduled an error to be thrown.
       return;
@@ -499,7 +513,8 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
   registry->Register(ParseEnv);
   registry->Register(IsInsideNodeModules);
   registry->Register(DefineLazyProperties);
-  registry->Register(DefineLazyPropertiesGetter);
+  registry->Register(DefineLazyPropertiesGetter<false>);
+  registry->Register(DefineLazyPropertiesGetter<true>);
   registry->Register(ConstructSharedArrayBuffer);
   registry->Register(MarkPromiseAsHandled);
 }
diff --git a/test/parallel/test-bootstrap-modules.js b/test/parallel/test-bootstrap-modules.js
index e6a40d45e26..1c5db787a8b 100644
--- a/test/parallel/test-bootstrap-modules.js
+++ b/test/parallel/test-bootstrap-modules.js
@@ -146,6 +146,16 @@ if (contextFromSnapshot) {
     'NativeModule internal/mime',
     'NativeModule internal/modules/typescript',
     'NativeModule internal/net',
+    'NativeModule internal/streams/add-abort-signal',
+    'NativeModule internal/streams/destroy',
+    'NativeModule internal/streams/end-of-stream',
+    'NativeModule internal/streams/from',
+    'NativeModule internal/streams/legacy',
+    'NativeModule internal/streams/readable',
+    'NativeModule internal/streams/state',
+    'NativeModule internal/streams/writable',
+    'NativeModule stream',
+    'NativeModule string_decoder',
   ].forEach(expected.beforePreExec.add.bind(expected.beforePreExec));
 } else if (isMainThread) {
   expected.beforePreExec.delete(getFormatNativeModule);
@@ -154,29 +164,20 @@ if (contextFromSnapshot) {
 if (!isMainThread) {
   [
     'NativeModule diagnostics_channel',
-    'NativeModule internal/abort_controller',
     'NativeModule internal/process/worker_thread_only',
     'NativeModule internal/streams/add-abort-signal',
-    'NativeModule internal/streams/compose',
     'NativeModule internal/streams/destroy',
-    'NativeModule internal/streams/duplex',
-    'NativeModule internal/streams/duplexpair',
     'NativeModule internal/streams/end-of-stream',
     'NativeModule internal/streams/from',
     'NativeModule internal/streams/legacy',
-    'NativeModule internal/streams/operators',
-    'NativeModule internal/streams/passthrough',
-    'NativeModule internal/streams/pipeline',
     'NativeModule internal/streams/readable',
     'NativeModule internal/streams/state',
-    'NativeModule internal/streams/transform',
     'NativeModule internal/streams/utils',
     'NativeModule internal/streams/writable',
     'NativeModule internal/worker',
     'NativeModule internal/worker/io',
     'NativeModule internal/worker/messaging',
     'NativeModule stream',
-    'NativeModule stream/promises',
     'NativeModule string_decoder',
     'NativeModule worker_threads',
   ].forEach(expected.atRunTime.add.bind(expected.atRunTime));
diff --git a/test/parallel/test-worker-snapshot-streams.js b/test/parallel/test-worker-snapshot-streams.js
new file mode 100644
index 00000000000..bda73a129c8
--- /dev/null
+++ b/test/parallel/test-worker-snapshot-streams.js
@@ -0,0 +1,41 @@
+// Flags: --expose-internals
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const { Worker, isMainThread, workerData } = require('worker_threads');
+const { internalBinding } = require('internal/test/binding');
+
+const contextFromSnapshot =
+  process.config.variables.node_use_node_snapshot &&
+  !process.execArgv.includes('--no-node-snapshot') &&
+  (isMainThread || !process.execArgv.includes('--no-worker-snapshot'));
+const { compiledInSnapshot } = internalBinding('builtins').getCacheUsage();
+for (const id of ['stream', 'internal/streams/readable', 'internal/streams/writable']) {
+  assert.strictEqual(compiledInSnapshot.includes(id), Boolean(contextFromSnapshot), id);
+}
+
+const { Readable, Writable, getDefaultHighWaterMark, setDefaultHighWaterMark } = require('stream');
+
+if (isMainThread) {
+  const highWaterMark = getDefaultHighWaterMark(false);
+  // Runtime changes in the parent must not become the worker's initial state.
+  setDefaultHighWaterMark(false, highWaterMark + 1);
+  Readable.prototype.parentOnly = true;
+  const worker = new Worker(__filename, { workerData: { highWaterMark } });
+  worker.on('error', common.mustNotCall());
+  worker.on('exit', common.mustCall((code) => assert.strictEqual(code, 0)));
+} else {
+  assert.strictEqual(Readable.prototype.parentOnly, undefined);
+  if (workerData?.highWaterMark !== undefined) {
+    assert.strictEqual(getDefaultHighWaterMark(false), workerData.highWaterMark);
+  }
+  assert(process.stdin instanceof Readable);
+  assert(process.stdout instanceof Writable);
+  assert(process.stderr instanceof Writable);
+  assert.strictEqual(typeof process.stdin.map, 'function');
+  assert.strictEqual(typeof process.stdin.toArray, 'function');
+  Readable.from([1, 2, 3]).map((value) => value * 2).toArray().then((values) => {
+    assert.deepStrictEqual(values, [2, 4, 6]);
+  }).then(common.mustCall());
+}