Skip to content
Closed
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
8 changes: 4 additions & 4 deletions src/jsc/bindings/node/NodeTimers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -240,9 +240,9 @@ static JSC::EncodedJSValue timersPromisesExport(JSGlobalObject* lexicalGlobalObj
auto& vm = JSC::getVM(lexicalGlobalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
auto* globalObject = defaultGlobalObject(lexicalGlobalObject);
JSValue timersPromises = globalObject->internalModuleRegistry()->requireId(lexicalGlobalObject, vm, InternalModuleRegistry::Field::NodeTimersPromises);
JSValue timersPromises = globalObject->internalModuleRegistry()->requireId(globalObject, vm, InternalModuleRegistry::Field::NodeTimersPromises);
RETURN_IF_EXCEPTION(scope, {});
RELEASE_AND_RETURN(scope, JSValue::encode(timersPromises.get(lexicalGlobalObject, Identifier::fromString(vm, name))));
RELEASE_AND_RETURN(scope, JSValue::encode(timersPromises.get(globalObject, Identifier::fromString(vm, name))));
}

JSC_DEFINE_CUSTOM_GETTER(setTimeoutPromisifyCustomGetter, (JSGlobalObject * globalObject, JSC::EncodedJSValue, PropertyName))
Expand All @@ -263,11 +263,11 @@ JSC_DEFINE_CUSTOM_GETTER(setImmediatePromisifyCustomGetter, (JSGlobalObject * gl
static JSValue createTimerFunction(VM& vm, JSObject* globalObject, ASCIILiteral name, NativeFunction function, JSC::CustomGetterSetter::CustomGetter promisifyCustomGetter)
{
auto* timerFunction = JSFunction::create(vm, globalObject->globalObject(), 1, name, function, ImplementationVisibility::Public);
// Same shape as Node's lib/timers.js: an enumerable, non-configurable accessor.
// Node's shape: enumerable, non-configurable, getter only. ReadOnly makes a write throw like Node's getter-only property.
timerFunction->putDirectCustomAccessor(vm,
Identifier::fromUid(vm.symbolRegistry().symbolForKey("nodejs.util.promisify.custom"_s)),
CustomGetterSetter::create(vm, promisifyCustomGetter, nullptr),
PropertyAttribute::CustomAccessor | PropertyAttribute::DontDelete | 0);
PropertyAttribute::CustomAccessor | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly | 0);
return timerFunction;
}

Expand Down
54 changes: 44 additions & 10 deletions test/js/node/timers/node-timers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,25 +46,59 @@ it("node.js util.promisify(setInterval) works", async () => {
});

it("timers expose util.promisify.custom as a lazy accessor without loading node:util first", async () => {
// Matches Node's lib/timers.js: an enumerable, non-configurable getter that resolves to timers/promises.
// Matches Node's lib/timers.js: an enumerable, non-configurable, getter-only accessor that resolves to
// the timers/promises export of the same realm. A strict-mode write and Object.assign throw, a sloppy-mode
// write is a no-op. The same probe runs in the main thread and in a worker, before either loads a module.
const probe = `const probe = () => {
const sym = Symbol.for("nodejs.util.promisify.custom");
const timers = [setTimeout, setInterval, setImmediate];
const shape = fn => {
const d = Object.getOwnPropertyDescriptor(fn, sym);
return d && { get: typeof d.get, set: typeof d.set, enumerable: d.enumerable, configurable: d.configurable };
};
const before = timers.map(shape);
// new Function so that the directive is compiled as written, instead of going through the transpiler.
const write = (fn, body) => { try { new Function("fn", "sym", body)(fn, sym); return "no throw"; } catch (e) { return e.constructor.name; } };
const writes = timers.map(fn => ({
strict: write(fn, '"use strict"; fn[sym] = 1;'),
sloppy: write(fn, 'fn[sym] = 1;'),
assign: write(fn, 'Object.assign(fn, { [sym]: 1 });'),
}));
// Read after the writes, so this also proves they changed nothing.
const tp = require("node:timers/promises");
const same = [setTimeout[sym] === tp.setTimeout, setInterval[sym] === tp.setInterval, setImmediate[sym] === tp.setImmediate];
return { before, writes, same };
};`;
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`const sym = Symbol.for("nodejs.util.promisify.custom");
const shape = fn => { const d = Object.getOwnPropertyDescriptor(fn, sym); return d && { get: typeof d.get, set: typeof d.set, enumerable: d.enumerable, configurable: d.configurable }; };
const before = [setTimeout, setInterval, setImmediate].map(shape);
const tp = require("node:timers/promises");
const same = [setTimeout[sym] === tp.setTimeout, setInterval[sym] === tp.setInterval, setImmediate[sym] === tp.setImmediate];
console.log(JSON.stringify({ before, same }));`,
`${probe}
const main = probe();
const { Worker } = require("node:worker_threads");
const worker = new Worker(${JSON.stringify(probe)} + 'require("node:worker_threads").parentPort.postMessage(probe());', {
eval: true,
});
let inWorker;
worker.once("message", result => { inWorker = result; });
worker.once("exit", workerExitCode => {
console.log(JSON.stringify({ main, inWorker, workerExitCode }));
});`,
],
env: bunEnv,
stdout: "pipe",
stderr: "inherit",
stderr: "pipe",
});
const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]);
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
const accessor = { get: "function", set: "undefined", enumerable: true, configurable: false };
expect(JSON.parse(stdout)).toEqual({ before: [accessor, accessor, accessor], same: [true, true, true] });
const rejected = { strict: "TypeError", sloppy: "no throw", assign: "TypeError" };
const expected = {
before: [accessor, accessor, accessor],
writes: [rejected, rejected, rejected],
same: [true, true, true],
};
expect(JSON.parse(stdout)).toEqual({ main: expected, inWorker: expected, workerExitCode: 0 });
expect(exitCode).toBe(0);
});

Expand Down
Loading