From 117f8f6658fb350d0ceae65a2778393640effd42 Mon Sep 17 00:00:00 2001 From: Renegade334 Date: Wed, 6 May 2026 03:21:00 +0100 Subject: [PATCH 1/3] lib: add internal Temporal utils Signed-off-by: Renegade334 --- lib/internal/process/pre_execution.js | 2 + lib/internal/util/temporal.js | 100 ++++++++++++++++++ test/parallel/test-bootstrap-modules.js | 1 + .../test-temporal-utils-no-temporal.js | 18 ++++ test/parallel/test-temporal-utils.js | 25 +++++ 5 files changed, 146 insertions(+) create mode 100644 lib/internal/util/temporal.js create mode 100644 test/parallel/test-temporal-utils-no-temporal.js create mode 100644 test/parallel/test-temporal-utils.js diff --git a/lib/internal/process/pre_execution.js b/lib/internal/process/pre_execution.js index 0f5aaf857aeb..27fb261174e1 100644 --- a/lib/internal/process/pre_execution.js +++ b/lib/internal/process/pre_execution.js @@ -142,6 +142,8 @@ function prepareExecution(options) { require('internal/dns/utils').initializeDns(); } + require('internal/util/temporal').initialize(); + if (isMainThread) { assert(internalBinding('worker').isMainThread); // Worker threads will get the manifest in the message handler. diff --git a/lib/internal/util/temporal.js b/lib/internal/util/temporal.js new file mode 100644 index 000000000000..e6b704dd3a6a --- /dev/null +++ b/lib/internal/util/temporal.js @@ -0,0 +1,100 @@ +'use strict'; + +// This module is initialized in the pre-execution phase, before user code is +// run. Since the module namespace is not populated at snapshot time, this +// module should be not be imported with a destructuring pattern in a module +// header or used within top-level module code, as the properties may not yet +// be initialized. Either import this module lazily, or import the namespace +// object without destructuring and access its properties at the point of use. + +// TODO(Renegade334): Remove typecheck methods once available via V8 API. +// Replace with primordials if/when temporal_rs becomes a mandatory V8 +// build dependency and the harmony_temporal flag is removed. + +const { + ArrayPrototypeForEach, + FunctionPrototypeCall, + ObjectEntries, + ObjectGetOwnPropertyDescriptors, + SafeMap, + StringPrototypeSubstring, + StringPrototypeToUpperCase, + globalThis, + uncurryThis, +} = primordials; + +// The keys of this Map are the Temporal constructor names. +// The values are the names of the most lightweight brand-aware getter within +// each prototype, which can be called speculatively as a brand check. +const constructors = new SafeMap() + .set('Duration', 'nanoseconds') + .set('Instant', 'epochNanoseconds') + .set('PlainDate', 'calendarId') + .set('PlainDateTime', 'calendarId') + .set('PlainMonthDay', 'calendarId') + .set('PlainTime', 'nanosecond') + .set('PlainYearMonth', 'calendarId') + .set('ZonedDateTime', 'calendarId'); + +exports.initialize = () => { + const { Temporal } = globalThis; + + if (Temporal === undefined) { + exports.hasTemporal = false; + + for (const name of constructors.keys()) { + exports[`isTemporal${name}`] = () => false; + } + + return; + } + + exports.hasTemporal = true; + + for (const { 0: name, 1: brandedGetter } of constructors) { + const constructor = Temporal[name]; + exports[`Temporal${name}`] = constructor; + + ArrayPrototypeForEach( + ObjectEntries(ObjectGetOwnPropertyDescriptors(constructor)), + ({ 0: key, 1: desc }) => { + if (typeof key === 'string' && typeof desc.value === 'function') { + exports[`Temporal${name}${capitalizeKey(key)}`] = desc.value; + } + }, + ); + + const descriptors = ObjectGetOwnPropertyDescriptors(constructor.prototype); + ArrayPrototypeForEach( + ObjectEntries(descriptors), + ({ 0: key, 1: desc }) => { + if (typeof key !== 'string') return; + + if ('get' in desc) { + exports[`Temporal${name}PrototypeGet${capitalizeKey(key)}`] = uncurryThis(desc.get); + } else { + exports[`Temporal${name}Prototype${capitalizeKey(key)}`] = typeof desc.value === 'function' ? + uncurryThis(desc.value) : + desc.value; + } + }, + ); + + exports[`isTemporal${name}`] = makeTypeCheckMethod(descriptors[brandedGetter].get); + } +}; + +function makeTypeCheckMethod(getter) { + return (value) => { + try { + FunctionPrototypeCall(getter, value); + return true; + } catch { + return false; + } + }; +} + +function capitalizeKey(key) { + return `${StringPrototypeToUpperCase(key[0])}${StringPrototypeSubstring(key, 1)}`; +} diff --git a/test/parallel/test-bootstrap-modules.js b/test/parallel/test-bootstrap-modules.js index e6a40d45e26f..c59afb65e203 100644 --- a/test/parallel/test-bootstrap-modules.js +++ b/test/parallel/test-bootstrap-modules.js @@ -114,6 +114,7 @@ expected.beforePreExec = new Set([ expected.atRunTime = new Set([ 'NativeModule internal/process/pre_execution', + 'NativeModule internal/util/temporal', ]); const { isMainThread } = require('worker_threads'); diff --git a/test/parallel/test-temporal-utils-no-temporal.js b/test/parallel/test-temporal-utils-no-temporal.js new file mode 100644 index 000000000000..d1ca2c4c0ea4 --- /dev/null +++ b/test/parallel/test-temporal-utils-no-temporal.js @@ -0,0 +1,18 @@ +// Flags: --expose-internals --no-harmony-temporal --no-warnings + +'use strict'; + +const common = require('../common'); +const assert = require('assert'); + +common.allowGlobals(globalThis.Temporal = { + Instant: class Instant {}, +}); + +const instant = new Temporal.Instant(); + +// Module contents should be independent of global state at require time +const temporalUtils = require('internal/util/temporal'); + +assert.strictEqual(temporalUtils.hasTemporal, false); +assert.strictEqual(temporalUtils.isTemporalInstant(instant), false); diff --git a/test/parallel/test-temporal-utils.js b/test/parallel/test-temporal-utils.js new file mode 100644 index 000000000000..d7354a651311 --- /dev/null +++ b/test/parallel/test-temporal-utils.js @@ -0,0 +1,25 @@ +// Flags: --expose-internals --no-warnings + +'use strict'; + +const common = require('../common'); +const assert = require('assert'); + +if (!common.hasTemporal) { + common.skip('No Temporal support'); +} + +const instant = Temporal.Now.instant(); + +delete globalThis.Temporal; +assert.strictEqual(typeof Temporal, 'undefined'); + +// Module contents should be independent of global state at require time +const temporalUtils = require('internal/util/temporal'); + +assert.strictEqual(temporalUtils.hasTemporal, true); +assert.strictEqual(instant.constructor, temporalUtils.TemporalInstant); +assert.strictEqual(instant.epochNanoseconds, + temporalUtils.TemporalInstantPrototypeGetEpochNanoseconds(instant)); +assert.strictEqual(temporalUtils.isTemporalInstant(instant), true); +assert.strictEqual(temporalUtils.isTemporalZonedDateTime(instant), false); From 979e1cbd4a3b5f5cee1be5834885a3ce9e5775e5 Mon Sep 17 00:00:00 2001 From: Renegade334 Date: Wed, 20 May 2026 13:01:10 +0200 Subject: [PATCH 2/3] fixup! lib: add internal Temporal utils --- lib/internal/util/temporal.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/lib/internal/util/temporal.js b/lib/internal/util/temporal.js index e6b704dd3a6a..559de6226288 100644 --- a/lib/internal/util/temporal.js +++ b/lib/internal/util/temporal.js @@ -7,6 +7,22 @@ // be initialized. Either import this module lazily, or import the namespace // object without destructuring and access its properties at the point of use. +// This module exposes the following exports: +// * `hasTemporal`, a boolean property indicating whether the Temporal builtin +// is present during initialization. +// * Typecheck methods for Temporal objects: `isTemporalDuration()`, +// `isTemporalInstant()` etc. If `hasTemporal` is false, then these methods +// will always return `false`. Note that these are much less performant than +// the native typecheck methods in internal/util/types, and should not be +// used in performance-critical paths. +// If `hasTemporal` is true, then this module also exports the following, in +// primordial style: +// * Temporal constructors: `TemporalDuration`, `TemporalInstant` etc. +// * Static methods on temporal constructors: `TemporalDurationCompare`, +// `TemporalDurationFrom` etc. +// * Temporal prototype properties, methods and getters: +// `TemporalDurationPrototypeAbs`, `TemporalDurationPrototypeGetBlank` etc. + // TODO(Renegade334): Remove typecheck methods once available via V8 API. // Replace with primordials if/when temporal_rs becomes a mandatory V8 // build dependency and the harmony_temporal flag is removed. From 7aa114995971c5e2edddfa48a9898069480da60e Mon Sep 17 00:00:00 2001 From: Pieter Develtere Date: Tue, 27 Oct 2026 13:58:06 +0100 Subject: [PATCH 3/3] assert,util: compare Temporal objects by value Temporal objects keep their state in internal slots and have no own properties, so deep equality considered any two Temporal objects of the same type equal, e.g. two different Temporal.Instant values. Compare builtin Temporal objects with their `equals` method, and Temporal.Duration (which has no `equals`) field by field, the same way Date objects are compared by their time value. Signed-off-by: Pieter Develtere --- doc/api/assert.md | 18 ++++++++ doc/api/util.md | 3 ++ lib/internal/util/comparisons.js | 73 ++++++++++++++++++++++++++++++- test/parallel/test-assert-deep.js | 55 ++++++++++++++++++++++- 4 files changed, 147 insertions(+), 2 deletions(-) diff --git a/doc/api/assert.md b/doc/api/assert.md index c33f4ba82336..d5f1293f6ef8 100644 --- a/doc/api/assert.md +++ b/doc/api/assert.md @@ -342,6 +342,9 @@ An alias of [`assert.ok()`][].