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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/internal/process/pre_execution.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,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.
Expand Down
100 changes: 100 additions & 0 deletions lib/internal/util/temporal.js
Original file line number Diff line number Diff line change
@@ -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;
}
Comment on lines +89 to +94
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is going to be quite bad for performance, we should probably add a comment about that. V8 seems to have utilities to check that, but looks like they're not exposed :/

if (!IsJSTemporalPlainDate(*value) && !IsJSTemporalPlainTime(*value) &&
!IsJSTemporalPlainDateTime(*value) &&
!IsJSTemporalZonedDateTime(*value) &&
!IsJSTemporalPlainYearMonth(*value) &&
!IsJSTemporalPlainMonthDay(*value) && !IsJSTemporalInstant(*value)) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm hoping that these will very much be a stop-gap until some API methods get implemented. Having said that, @panva did find recently that using this sort of speculative method check in the JS layer was surprisingly faster than invoking the C++ typecheck call in benchmarks on main (that case was for ArrayBuffers specifically).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's interesting! Still with try/catch? Another approach would be to use @@toStringTag (defined in e.g. https://tc39.es/proposal-temporal/#sec-temporal.instant.prototype-%symbol.tostringtag%). btw shouldn't we move this to internal/util/types?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's interesting! Still with try/catch?

Same approach, yes. Would have to look for the exact benchmark.

Another approach would be to use @@toStringTag (defined in e.g. https://tc39.es/proposal-temporal/#sec-temporal.instant.prototype-%symbol.tostringtag%).

It's a property, not a getter, on the Temporal prototypes.

btw shouldn't we move this to internal/util/types?

Pretty sure that's snapshotted. Additionally, those methods are automatically exposed to userland, and I don't think this should be considered a stable enough approach for that.

};
}

function capitalizeKey(key) {
return `${StringPrototypeToUpperCase(key[0])}${StringPrototypeSubstring(key, 1)}`;
}
1 change: 1 addition & 0 deletions test/parallel/test-bootstrap-modules.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ expected.beforePreExec = new Set([

expected.atRunTime = new Set([
'NativeModule internal/process/pre_execution',
'NativeModule internal/util/temporal',
]);

const { isMainThread } = require('worker_threads');
Expand Down
18 changes: 18 additions & 0 deletions test/parallel/test-temporal-utils-no-temporal.js
Original file line number Diff line number Diff line change
@@ -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);
25 changes: 25 additions & 0 deletions test/parallel/test-temporal-utils.js
Original file line number Diff line number Diff line change
@@ -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);
Loading