diff options
author | 2023-09-05 16:52:57 -0800 | |
---|---|---|
committer | 2023-09-05 17:52:57 -0700 | |
commit | 1bd5b245b8a55353e60a2decad507ef8014be044 (patch) | |
tree | 1a5cd5bcc7d7758bbfd154cf49470c1b0f3dc1bb | |
parent | acfd028e8f859a0e8139b7adab5d319e326c2373 (diff) | |
download | bun-1bd5b245b8a55353e60a2decad507ef8014be044.tar.gz bun-1bd5b245b8a55353e60a2decad507ef8014be044.tar.zst bun-1bd5b245b8a55353e60a2decad507ef8014be044.zip |
Align `process.nextTick` execution order with Node (#4409)
* Align `process.nextTick` execution order with Node
* some tests
* formatting
* fixups
* fix the test failures
* simplify the logic here
* push it up
---------
Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com>
Co-authored-by: dave caruso <me@paperdave.net>
-rw-r--r-- | src/bun.js/bindings/JSNextTickQueue.cpp | 97 | ||||
-rw-r--r-- | src/bun.js/bindings/JSNextTickQueue.h | 41 | ||||
-rw-r--r-- | src/bun.js/bindings/Process.cpp | 98 | ||||
-rw-r--r-- | src/bun.js/bindings/Process.lut.h | 2 | ||||
-rw-r--r-- | src/bun.js/bindings/ZigGlobalObject.cpp | 124 | ||||
-rw-r--r-- | src/bun.js/bindings/ZigGlobalObject.h | 5 | ||||
-rw-r--r-- | src/bun.js/bindings/bindings.cpp | 3 | ||||
-rw-r--r-- | src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h | 1 | ||||
-rw-r--r-- | src/bun.js/bindings/webcore/DOMIsoSubspaces.h | 2 | ||||
-rw-r--r-- | src/bun.js/event_loop.zig | 15 | ||||
-rw-r--r-- | src/bun_js.zig | 2 | ||||
-rw-r--r-- | src/js/builtins/ProcessObjectInternals.ts | 202 | ||||
-rw-r--r-- | src/js/out/WebCoreJSBuiltins.cpp | 8 | ||||
-rw-r--r-- | src/js/out/WebCoreJSBuiltins.h | 11 | ||||
-rw-r--r-- | test/js/bun/spawn/exit-code.test.ts | 5 | ||||
-rw-r--r-- | test/js/node/process/process-nexttick.test.js | 950 | ||||
-rw-r--r-- | test/js/node/readline/readline_promises.node.test.ts | 2 |
17 files changed, 1481 insertions, 87 deletions
diff --git a/src/bun.js/bindings/JSNextTickQueue.cpp b/src/bun.js/bindings/JSNextTickQueue.cpp new file mode 100644 index 000000000..8916ef6c8 --- /dev/null +++ b/src/bun.js/bindings/JSNextTickQueue.cpp @@ -0,0 +1,97 @@ +#include "root.h" + +#include "JavaScriptCore/JSCJSValueInlines.h" +#include "JavaScriptCore/JSInternalPromise.h" +#include "JavaScriptCore/LazyPropertyInlines.h" +#include <JavaScriptCore/Weak.h> +#include <JavaScriptCore/GetterSetter.h> + +#include "JSNextTickQueue.h" +#include <JavaScriptCore/JSGlobalObject.h> +#include <JavaScriptCore/Structure.h> +#include <JavaScriptCore/JSInternalFieldObjectImplInlines.h> +#include "ExtendedDOMClientIsoSubspaces.h" +#include "ExtendedDOMIsoSubspaces.h" +#include "BunClientData.h" + +namespace Bun { + +using namespace JSC; + +const JSC::ClassInfo JSNextTickQueue::s_info = { "JSNextTickQueue"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSNextTickQueue) }; + +template<typename, JSC::SubspaceAccess mode> +JSC::GCClient::IsoSubspace* JSNextTickQueue::subspaceFor(JSC::VM& vm) +{ + return WebCore::subspaceForImpl<JSNextTickQueue, WebCore::UseCustomHeapCellType::No>( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForJSNextTickQueue.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForJSNextTickQueue = std::forward<decltype(space)>(space); }, + [](auto& spaces) { return spaces.m_subspaceForJSNextTickQueue.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForJSNextTickQueue = std::forward<decltype(space)>(space); }); +} + +JSNextTickQueue* JSNextTickQueue::create(VM& vm, Structure* structure) +{ + JSNextTickQueue* mod = new (NotNull, allocateCell<JSNextTickQueue>(vm)) JSNextTickQueue(vm, structure); + return mod; +} +Structure* JSNextTickQueue::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype) +{ + return Structure::create(vm, globalObject, prototype, TypeInfo(CellType, StructureFlags), info()); +} + +JSNextTickQueue::JSNextTickQueue(VM& vm, Structure* structure) + : Base(vm, structure) +{ +} + +void JSNextTickQueue::finishCreation(VM& vm) +{ + Base::finishCreation(vm); +} + +template<typename Visitor> +void JSNextTickQueue::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + auto* thisObject = jsCast<JSNextTickQueue*>(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); +} + +DEFINE_VISIT_CHILDREN(JSNextTickQueue); + +JSNextTickQueue* JSNextTickQueue::create(JSC::JSGlobalObject* globalObject) +{ + auto& vm = globalObject->vm(); + auto* obj = create(vm, createStructure(vm, globalObject, jsNull())); + obj->finishCreation(vm); + return obj; +} + +bool JSNextTickQueue::isEmpty() +{ + return !internalField(0) || internalField(0).get().asNumber() == 0; +} + +void JSNextTickQueue::drain(JSC::VM& vm, JSC::JSGlobalObject* globalObject) +{ + bool mustResetContext = false; + if (isEmpty()) { + vm.drainMicrotasks(); + mustResetContext = true; + } + + if (!isEmpty()) { + if (mustResetContext) { + globalObject->m_asyncContextData.get()->putInternalField(vm, 0, jsUndefined()); + } + auto* drainFn = internalField(2).get().getObject(); + + auto throwScope = DECLARE_THROW_SCOPE(vm); + MarkedArgumentBuffer drainArgs; + JSC::call(globalObject, drainFn, drainArgs, "Failed to drain next tick queue"_s); + } +} + +}
\ No newline at end of file diff --git a/src/bun.js/bindings/JSNextTickQueue.h b/src/bun.js/bindings/JSNextTickQueue.h new file mode 100644 index 000000000..c3bd228cc --- /dev/null +++ b/src/bun.js/bindings/JSNextTickQueue.h @@ -0,0 +1,41 @@ +#include "root.h" +#include "headers-handwritten.h" + +#include "JavaScriptCore/JSCInlines.h" +#include "BunClientData.h" +#include <JavaScriptCore/JSInternalFieldObjectImpl.h> + +namespace Bun { +using namespace JSC; + +class JSNextTickQueue : public JSC::JSInternalFieldObjectImpl<3> { +public: + static constexpr unsigned numberOfInternalFields = 3; + using Base = JSC::JSInternalFieldObjectImpl<3>; + + template<typename, JSC::SubspaceAccess mode> static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm); + + JS_EXPORT_PRIVATE static JSNextTickQueue* create(VM&, Structure*); + static JSNextTickQueue* create(JSC::JSGlobalObject* globalObject); + static JSNextTickQueue* createWithInitialValues(VM&, Structure*); + static Structure* createStructure(VM&, JSGlobalObject*, JSValue); + + static std::array<JSValue, numberOfInternalFields> initialValues() + { + return { { + jsNumber(-1), + jsUndefined(), + jsUndefined(), + } }; + } + + DECLARE_EXPORT_INFO; + DECLARE_VISIT_CHILDREN; + + JSNextTickQueue(JSC::VM&, JSC::Structure*); + void finishCreation(JSC::VM&); + + bool isEmpty(); + void drain(JSC::VM& vm, JSC::JSGlobalObject* globalObject); +}; +}
\ No newline at end of file diff --git a/src/bun.js/bindings/Process.cpp b/src/bun.js/bindings/Process.cpp index 5c9c03dd2..252d00075 100644 --- a/src/bun.js/bindings/Process.cpp +++ b/src/bun.js/bindings/Process.cpp @@ -19,6 +19,7 @@ #include <termios.h> #include <errno.h> #include <sys/ioctl.h> +#include "JSNextTickQueue.h" #pragma mark - Node.js Process @@ -160,63 +161,6 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionInternalGetWindowSize, return JSC::JSValue::encode(jsBoolean(true)); } -JSC_DEFINE_HOST_FUNCTION(Process_functionNextTick, - (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) -{ - JSC::VM& vm = globalObject->vm(); - auto argCount = callFrame->argumentCount(); - if (argCount == 0) { - auto scope = DECLARE_THROW_SCOPE(globalObject->vm()); - JSC::throwTypeError(globalObject, scope, "nextTick requires 1 argument (a function)"_s); - return JSC::JSValue::encode(JSC::JSValue {}); - } - - JSC::JSValue job = callFrame->uncheckedArgument(0); - - if (!job.isObject() || !job.getObject()->isCallable()) { - auto scope = DECLARE_THROW_SCOPE(globalObject->vm()); - JSC::throwTypeError(globalObject, scope, "nextTick expects a function"_s); - return JSC::JSValue::encode(JSC::JSValue {}); - } - - Zig::GlobalObject* global = JSC::jsCast<Zig::GlobalObject*>(globalObject); - JSC::JSValue asyncContextValue = globalObject->m_asyncContextData.get()->getInternalField(0); - - switch (callFrame->argumentCount()) { - case 1: { - global->queueMicrotask(global->performMicrotaskFunction(), job, asyncContextValue, JSC::JSValue {}, JSC::JSValue {}); - break; - } - case 2: { - global->queueMicrotask(global->performMicrotaskFunction(), job, asyncContextValue, callFrame->uncheckedArgument(1), JSC::JSValue {}); - break; - } - case 3: { - global->queueMicrotask(global->performMicrotaskFunction(), job, asyncContextValue, callFrame->uncheckedArgument(1), callFrame->uncheckedArgument(2)); - break; - } - default: { - JSC::JSArray* args = JSC::constructEmptyArray(globalObject, nullptr, argCount - 1); - if (UNLIKELY(!args)) { - auto scope = DECLARE_THROW_SCOPE(vm); - throwVMError(globalObject, scope, createOutOfMemoryError(globalObject)); - return JSC::JSValue::encode(JSC::JSValue {}); - } - - for (unsigned i = 1; i < argCount; i++) { - args->putDirectIndex(globalObject, i - 1, callFrame->uncheckedArgument(i)); - } - - global->queueMicrotask( - global->performMicrotaskVariadicFunction(), job, args, asyncContextValue, JSC::JSValue {}); - - break; - } - } - - return JSC::JSValue::encode(jsUndefined()); -} - JSC_DECLARE_HOST_FUNCTION(Process_functionDlopen); JSC_DEFINE_HOST_FUNCTION(Process_functionDlopen, (JSC::JSGlobalObject * globalObject_, JSC::CallFrame* callFrame)) @@ -279,7 +223,7 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionDlopen, } } - JSC::EncodedJSValue (*napi_register_module_v1)(JSC::JSGlobalObject* globalObject, + JSC::EncodedJSValue (*napi_register_module_v1)(JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue exports); napi_register_module_v1 = reinterpret_cast<JSC::EncodedJSValue (*)(JSC::JSGlobalObject*, @@ -1533,6 +1477,42 @@ static JSValue constructMemoryUsage(VM& vm, JSObject* processObject) return memoryUsage; } +JSC_DEFINE_HOST_FUNCTION(jsFunctionReportUncaughtException, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + JSValue arg0 = callFrame->argument(0); + Bun__reportUnhandledError(globalObject, JSValue::encode(arg0)); + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsFunctionDrainMicrotaskQueue, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + globalObject->vm().drainMicrotasks(); + return JSValue::encode(jsUndefined()); +} + +static JSValue constructProcessNextTickFn(VM& vm, JSObject* processObject) +{ + JSGlobalObject* lexicalGlobalObject = processObject->globalObject(); + Zig::GlobalObject* globalObject = jsCast<Zig::GlobalObject*>(lexicalGlobalObject); + JSValue nextTickQueueObject; + if (!globalObject->m_nextTickQueue) { + Bun::JSNextTickQueue* queue = Bun::JSNextTickQueue::create(globalObject); + globalObject->m_nextTickQueue.set(vm, globalObject, queue); + nextTickQueueObject = queue; + } else { + nextTickQueueObject = jsCast<Bun::JSNextTickQueue*>(globalObject->m_nextTickQueue.get()); + } + + JSC::JSFunction* initializer = JSC::JSFunction::create(vm, processObjectInternalsInitializeNextTickQueueCodeGenerator(vm), lexicalGlobalObject); + JSC::MarkedArgumentBuffer args; + args.append(processObject); + args.append(nextTickQueueObject); + args.append(JSC::JSFunction::create(vm, globalObject, 1, String(), jsFunctionDrainMicrotaskQueue, ImplementationVisibility::Private)); + args.append(JSC::JSFunction::create(vm, globalObject, 1, String(), jsFunctionReportUncaughtException, ImplementationVisibility::Private)); + + return JSC::call(globalObject, initializer, JSC::getCallData(initializer), globalObject->globalThis(), args); +} + static JSValue constructFeatures(VM& vm, JSObject* processObject) { // { @@ -1742,7 +1722,7 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionKill, mainModule JSBuiltin ReadOnly|Builtin|Accessor|Function 0 memoryUsage constructMemoryUsage PropertyCallback moduleLoadList Process_stubEmptyArray PropertyCallback - nextTick Process_functionNextTick Function 1 + nextTick constructProcessNextTickFn PropertyCallback openStdin Process_functionOpenStdin Function 0 pid constructPid PropertyCallback platform constructPlatform PropertyCallback diff --git a/src/bun.js/bindings/Process.lut.h b/src/bun.js/bindings/Process.lut.h index 3f2d9255d..4086fb19e 100644 --- a/src/bun.js/bindings/Process.lut.h +++ b/src/bun.js/bindings/Process.lut.h @@ -179,7 +179,7 @@ static const struct HashTableValue processObjectTableValues[62] = { { "mainModule"_s, ((static_cast<unsigned>(PropertyAttribute::ReadOnly|PropertyAttribute::Builtin|PropertyAttribute::Accessor|PropertyAttribute::Function)) & ~PropertyAttribute::Function) | PropertyAttribute::Builtin, NoIntrinsic, { HashTableValue::BuiltinGeneratorType, processObjectMainModuleCodeGenerator, 0 } }, { "memoryUsage"_s, static_cast<unsigned>(PropertyAttribute::PropertyCallback), NoIntrinsic, { HashTableValue::LazyPropertyType, constructMemoryUsage } }, { "moduleLoadList"_s, static_cast<unsigned>(PropertyAttribute::PropertyCallback), NoIntrinsic, { HashTableValue::LazyPropertyType, Process_stubEmptyArray } }, - { "nextTick"_s, static_cast<unsigned>(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, Process_functionNextTick, 1 } }, + { "nextTick"_s, static_cast<unsigned>(PropertyAttribute::PropertyCallback), NoIntrinsic, { HashTableValue::LazyPropertyType, constructProcessNextTickFn } }, { "openStdin"_s, static_cast<unsigned>(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, Process_functionOpenStdin, 0 } }, { "pid"_s, static_cast<unsigned>(PropertyAttribute::PropertyCallback), NoIntrinsic, { HashTableValue::LazyPropertyType, constructPid } }, { "platform"_s, static_cast<unsigned>(PropertyAttribute::PropertyCallback), NoIntrinsic, { HashTableValue::LazyPropertyType, constructPlatform } }, diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index 7edbd42e6..286084b4d 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -130,6 +130,7 @@ #endif #include "BunObject.h" +#include "JSNextTickQueue.h" using namespace Bun; @@ -294,7 +295,7 @@ extern "C" void JSCInitialize(const char* envp[], size_t envc, void (*onCrash)(c } extern "C" void* Bun__getVM(); -extern "C" JSGlobalObject* Bun__getDefaultGlobal(); +extern "C" Zig::GlobalObject* Bun__getDefaultGlobal(); // Error.captureStackTrace may cause computeErrorInfo to be called twice // Rather than figure out the plumbing in JSC, we just skip the next call @@ -432,6 +433,44 @@ static String computeErrorInfo(JSC::VM& vm, Vector<StackFrame>& stackTrace, unsi return computeErrorInfoWithoutPrepareStackTrace(vm, stackTrace, line, column, sourceURL, errorInstance); } +static void resetOnEachMicrotaskTick(JSC::VM& vm, Zig::GlobalObject* globalObject); + +static void checkIfNextTickWasCalledDuringMicrotask(JSC::VM& vm) +{ + auto* globalObject = Bun__getDefaultGlobal(); + if (auto nextTickQueueValue = globalObject->m_nextTickQueue.get()) { + auto* queue = jsCast<Bun::JSNextTickQueue*>(nextTickQueueValue); + resetOnEachMicrotaskTick(vm, globalObject); + queue->drain(vm, globalObject); + } +} + +static void cleanupAsyncHooksData(JSC::VM& vm) +{ + auto* globalObject = Bun__getDefaultGlobal(); + globalObject->m_asyncContextData.get()->putInternalField(vm, 0, jsUndefined()); + globalObject->asyncHooksNeedsCleanup = false; + if (!globalObject->m_nextTickQueue) { + vm.setOnEachMicrotaskTick(&checkIfNextTickWasCalledDuringMicrotask); + checkIfNextTickWasCalledDuringMicrotask(vm); + } else { + vm.setOnEachMicrotaskTick(nullptr); + } +} + +static void resetOnEachMicrotaskTick(JSC::VM& vm, Zig::GlobalObject* globalObject) +{ + if (globalObject->asyncHooksNeedsCleanup) { + vm.setOnEachMicrotaskTick(&cleanupAsyncHooksData); + } else { + if (globalObject->m_nextTickQueue) { + vm.setOnEachMicrotaskTick(nullptr); + } else { + vm.setOnEachMicrotaskTick(&checkIfNextTickWasCalledDuringMicrotask); + } + } +} + extern "C" JSC__JSGlobalObject* Zig__GlobalObject__create(void* console_client, int32_t executionContextId, bool miniMode, void* worker_ptr) { auto heapSize = miniMode ? JSC::HeapType::Small : JSC::HeapType::Large; @@ -479,6 +518,16 @@ extern "C" JSC__JSGlobalObject* Zig__GlobalObject__create(void* console_client, JSC::gcProtect(globalObject); + vm.setOnEachMicrotaskTick([](JSC::VM& vm) -> void { + auto* globalObject = Bun__getDefaultGlobal(); + if (auto nextTickQueue = globalObject->m_nextTickQueue.get()) { + resetOnEachMicrotaskTick(vm, globalObject); + Bun::JSNextTickQueue* queue = jsCast<Bun::JSNextTickQueue*>(nextTickQueue); + queue->drain(vm, globalObject); + return; + } + }); + vm.ref(); return globalObject; } @@ -1115,6 +1164,17 @@ JSC_DEFINE_HOST_FUNCTION(functionSetTimeout, return JSC::JSValue::encode(JSC::JSValue {}); } +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + return Bun__Timer__setTimeout(globalObject, JSC::JSValue::encode(job), JSC::JSValue::encode(num), JSValue::encode(arguments)); } @@ -1166,6 +1226,17 @@ JSC_DEFINE_HOST_FUNCTION(functionSetInterval, return JSC::JSValue::encode(JSC::JSValue {}); } +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + return Bun__Timer__setInterval(globalObject, JSC::JSValue::encode(job), JSC::JSValue::encode(num), JSValue::encode(arguments)); } @@ -1182,6 +1253,17 @@ JSC_DEFINE_HOST_FUNCTION(functionClearInterval, JSC::JSValue num = callFrame->argument(0); +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + return Bun__Timer__clearInterval(globalObject, JSC::JSValue::encode(num)); } @@ -1198,6 +1280,17 @@ JSC_DEFINE_HOST_FUNCTION(functionClearTimeout, JSC::JSValue num = callFrame->argument(0); +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + return Bun__Timer__clearTimeout(globalObject, JSC::JSValue::encode(num)); } @@ -1393,12 +1486,6 @@ JSC_DEFINE_HOST_FUNCTION(functionCallback, (JSC::JSGlobalObject * globalObject, return JSC::JSValue::encode(JSC::call(globalObject, callback, callData, JSC::jsUndefined(), JSC::MarkedArgumentBuffer())); } -static void cleanupAsyncHooksData(JSC::VM& vm) -{ - vm.setOnEachMicrotaskTick(nullptr); - Bun__getDefaultGlobal()->m_asyncContextData.get()->putInternalField(vm, 0, jsUndefined()); -} - // $lazy("async_hooks").cleanupLater JSC_DEFINE_HOST_FUNCTION(asyncHooksCleanupLater, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { @@ -1406,7 +1493,9 @@ JSC_DEFINE_HOST_FUNCTION(asyncHooksCleanupLater, (JSC::JSGlobalObject * globalOb // - nobody else uses setOnEachMicrotaskTick // - this is called by js if we set async context in a way we may not clear it // - AsyncLocalStorage.prototype.run cleans up after itself and does not call this cb - globalObject->vm().setOnEachMicrotaskTick(&cleanupAsyncHooksData); + auto* global = jsCast<Zig::GlobalObject*>(globalObject); + global->asyncHooksNeedsCleanup = true; + resetOnEachMicrotaskTick(globalObject->vm(), global); return JSC::JSValue::encode(JSC::jsUndefined()); } @@ -3955,6 +4044,23 @@ extern "C" bool JSC__JSGlobalObject__startRemoteInspector(JSC__JSGlobalObject* g #endif } +void GlobalObject::drainMicrotasks() +{ + auto& vm = this->vm(); + if (auto nextTickQueue = this->m_nextTickQueue.get()) { + Bun::JSNextTickQueue* queue = jsCast<Bun::JSNextTickQueue*>(nextTickQueue); + queue->drain(vm, this); + return; + } + + vm.drainMicrotasks(); +} + +extern "C" void JSC__JSGlobalObject__drainMicrotasks(Zig::GlobalObject* globalObject) +{ + globalObject->drainMicrotasks(); +} + template<typename Visitor> void GlobalObject::visitChildrenImpl(JSCell* cell, Visitor& visitor) { @@ -4008,6 +4114,8 @@ void GlobalObject::visitChildrenImpl(JSCell* cell, Visitor& visitor) visitor.append(thisObject->m_JSWebSocketSetterValue); visitor.append(thisObject->m_JSWorkerSetterValue); + visitor.append(thisObject->m_nextTickQueue); + thisObject->m_JSArrayBufferSinkClassStructure.visit(visitor); thisObject->m_JSBufferListClassStructure.visit(visitor); thisObject->m_JSFFIFunctionStructure.visit(visitor); diff --git a/src/bun.js/bindings/ZigGlobalObject.h b/src/bun.js/bindings/ZigGlobalObject.h index 029f90132..e622016de 100644 --- a/src/bun.js/bindings/ZigGlobalObject.h +++ b/src/bun.js/bindings/ZigGlobalObject.h @@ -296,6 +296,8 @@ public: return m_processEnvObject.getInitializedOnMainThread(this); } + void drainMicrotasks(); + void handleRejectedPromises(); void initGeneratedLazyClasses(); @@ -363,6 +365,8 @@ public: return func; } + bool asyncHooksNeedsCleanup = false; + /** * WARNING: You must update visitChildrenImpl() if you add a new field. * @@ -381,6 +385,7 @@ public: mutable WriteBarrier<JSFunction> m_readableStreamToText; mutable WriteBarrier<JSFunction> m_readableStreamToFormData; + mutable WriteBarrier<Unknown> m_nextTickQueue; mutable WriteBarrier<Unknown> m_BunCommonJSModuleValue; mutable WriteBarrier<Unknown> m_JSBroadcastChannelSetterValue; mutable WriteBarrier<Unknown> m_JSBufferSetterValue; diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index ca072f2b1..6413e0470 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -2527,7 +2527,6 @@ JSC__JSInternalPromise* JSC__JSModuleLoader__loadAndEvaluateModule(JSC__JSGlobalObject* globalObject, const BunString* arg1) { - globalObject->vm().drainMicrotasks(); auto name = Bun::toWTFString(*arg1); name.impl()->ref(); @@ -2546,9 +2545,7 @@ JSC__JSModuleLoader__loadAndEvaluateModule(JSC__JSGlobalObject* globalObject, JSC::JSInternalPromise::rejectedPromise(globalObject, callFrame->argument(0))); }); - globalObject->vm().drainMicrotasks(); auto result = promise->then(globalObject, resolverFunction, rejecterFunction); - globalObject->vm().drainMicrotasks(); // if (promise->status(globalObject->vm()) == // JSC::JSPromise::Status::Fulfilled) { diff --git a/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h b/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h index 4c09df6a5..e21a62bf8 100644 --- a/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h +++ b/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h @@ -42,6 +42,7 @@ public: std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForProcessObject; std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForInternalModuleRegistry; std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForBunInspectorConnection; + std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForJSNextTickQueue; #include "ZigGeneratedClasses+DOMClientIsoSubspaces.h" /* --- bun --- */ diff --git a/src/bun.js/bindings/webcore/DOMIsoSubspaces.h b/src/bun.js/bindings/webcore/DOMIsoSubspaces.h index 2b834cf3c..806aa4454 100644 --- a/src/bun.js/bindings/webcore/DOMIsoSubspaces.h +++ b/src/bun.js/bindings/webcore/DOMIsoSubspaces.h @@ -42,7 +42,7 @@ public: std::unique_ptr<IsoSubspace> m_subspaceForProcessObject; std::unique_ptr<IsoSubspace> m_subspaceForInternalModuleRegistry; std::unique_ptr<IsoSubspace> m_subspaceForBunInspectorConnection; - + std::unique_ptr<IsoSubspace> m_subspaceForJSNextTickQueue; #include "ZigGeneratedClasses+DOMIsoSubspaces.h" /*-- BUN --*/ diff --git a/src/bun.js/event_loop.zig b/src/bun.js/event_loop.zig index 640a9276c..f1367c239 100644 --- a/src/bun.js/event_loop.zig +++ b/src/bun.js/event_loop.zig @@ -529,14 +529,14 @@ pub const EventLoop = struct { this.virtual_machine.event_loop_handle.?.tick(); } } - - pub fn drainMicrotasksWithVM(this: *EventLoop, vm: *JSC.VM) void { - vm.drainMicrotasks(); + extern fn JSC__JSGlobalObject__drainMicrotasks(*JSC.JSGlobalObject) void; + fn drainMicrotasksWithGlobal(this: *EventLoop, globalObject: *JSC.JSGlobalObject) void { + JSC__JSGlobalObject__drainMicrotasks(globalObject); this.drainDeferredTasks(); } pub fn drainMicrotasks(this: *EventLoop) void { - this.drainMicrotasksWithVM(this.global.vm()); + this.drainMicrotasksWithGlobal(this.global); } pub fn ensureAliveForOneTick(this: *EventLoop) void { @@ -666,7 +666,7 @@ pub const EventLoop = struct { } global_vm.releaseWeakRefs(); - this.drainMicrotasksWithVM(global_vm); + this.drainMicrotasksWithGlobal(global); } this.tasks.head = if (this.tasks.count == 0) 0 else this.tasks.head; @@ -824,13 +824,14 @@ pub const EventLoop = struct { this.processGCTimer(); - var global_vm = ctx.global.vm(); + var global = ctx.global; + var global_vm = global.vm(); while (true) { while (this.tickWithCount() > 0) : (this.global.handleRejectedPromises()) { this.tickConcurrent(); } else { global_vm.releaseWeakRefs(); - this.drainMicrotasksWithVM(global_vm); + this.drainMicrotasksWithGlobal(global); this.tickConcurrent(); if (this.tasks.count > 0) continue; } diff --git a/src/bun_js.zig b/src/bun_js.zig index 46942b849..0605c57a1 100644 --- a/src/bun_js.zig +++ b/src/bun_js.zig @@ -241,6 +241,8 @@ pub const Run = struct { pub fn start(this: *Run) void { var vm = this.vm; vm.hot_reload = this.ctx.debug.hot_reload; + vm.onUnhandledRejection = &onUnhandledRejectionBeforeClose; + if (this.ctx.debug.hot_reload != .none) { JSC.HotReloader.enableHotModuleReloading(vm); } diff --git a/src/js/builtins/ProcessObjectInternals.ts b/src/js/builtins/ProcessObjectInternals.ts index e6c04c90f..2bb8648df 100644 --- a/src/js/builtins/ProcessObjectInternals.ts +++ b/src/js/builtins/ProcessObjectInternals.ts @@ -1,4 +1,5 @@ /* + * Copyright Joyent, Inc. and other Node contributors. * Copyright 2023 Codeblog Corp. All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -199,3 +200,204 @@ export function getStdinStream(fd) { return stream; } + +export function initializeNextTickQueue(process, nextTickQueue, drainMicrotasksFn, reportUncaughtExceptionFn) { + var queue; + var process; + var nextTickQueue = nextTickQueue; + var drainMicrotasks = drainMicrotasksFn; + var reportUncaughtException = reportUncaughtExceptionFn; + + function validateFunction(cb) { + if (typeof cb !== "function") { + const err = new TypeError(`The "callback" argument must be of type "function". Received type ${typeof cb}`); + err.code = "ERR_INVALID_ARG_TYPE"; + throw err; + } + } + + var setup; + setup = () => { + queue = (function createQueue() { + // Currently optimal queue size, tested on V8 6.0 - 6.6. Must be power of two. + const kSize = 2048; + const kMask = kSize - 1; + + // The FixedQueue is implemented as a singly-linked list of fixed-size + // circular buffers. It looks something like this: + // + // head tail + // | | + // v v + // +-----------+ <-----\ +-----------+ <------\ +-----------+ + // | [null] | \----- | next | \------- | next | + // +-----------+ +-----------+ +-----------+ + // | item | <-- bottom | item | <-- bottom | [empty] | + // | item | | item | | [empty] | + // | item | | item | | [empty] | + // | item | | item | | [empty] | + // | item | | item | bottom --> | item | + // | item | | item | | item | + // | ... | | ... | | ... | + // | item | | item | | item | + // | item | | item | | item | + // | [empty] | <-- top | item | | item | + // | [empty] | | item | | item | + // | [empty] | | [empty] | <-- top top --> | [empty] | + // +-----------+ +-----------+ +-----------+ + // + // Or, if there is only one circular buffer, it looks something + // like either of these: + // + // head tail head tail + // | | | | + // v v v v + // +-----------+ +-----------+ + // | [null] | | [null] | + // +-----------+ +-----------+ + // | [empty] | | item | + // | [empty] | | item | + // | item | <-- bottom top --> | [empty] | + // | item | | [empty] | + // | [empty] | <-- top bottom --> | item | + // | [empty] | | item | + // +-----------+ +-----------+ + // + // Adding a value means moving `top` forward by one, removing means + // moving `bottom` forward by one. After reaching the end, the queue + // wraps around. + // + // When `top === bottom` the current queue is empty and when + // `top + 1 === bottom` it's full. This wastes a single space of storage + // but allows much quicker checks. + + class FixedCircularBuffer { + constructor() { + this.bottom = 0; + this.top = 0; + this.list = $newArrayWithSize(kSize); + this.next = null; + } + + isEmpty() { + return this.top === this.bottom; + } + + isFull() { + return ((this.top + 1) & kMask) === this.bottom; + } + + push(data) { + this.list[this.top] = data; + this.top = (this.top + 1) & kMask; + } + + shift() { + var { list, bottom } = this; + const nextItem = list[bottom]; + if (nextItem === undefined) return null; + list[bottom] = undefined; + this.bottom = (bottom + 1) & kMask; + return nextItem; + } + } + + class FixedQueue { + constructor() { + this.head = this.tail = new FixedCircularBuffer(); + } + + isEmpty() { + return this.head.isEmpty(); + } + + push(data) { + if (this.head.isFull()) { + // Head is full: Creates a new queue, sets the old queue's `.next` to it, + // and sets it as the new main queue. + this.head = this.head.next = new FixedCircularBuffer(); + } + this.head.push(data); + } + + shift() { + const tail = this.tail; + const next = tail.shift(); + if (tail.isEmpty() && tail.next !== null) { + // If there is another queue, it forms the new tail. + this.tail = tail.next; + tail.next = null; + } + return next; + } + } + + return new FixedQueue(); + })(); + + function processTicksAndRejections() { + var tock; + do { + while ((tock = queue.shift()) !== null) { + var callback = tock.callback; + var args = tock.args; + var frame = tock.frame; + var restore = $getInternalField($asyncContext, 0); + $putInternalField($asyncContext, 0, frame); + try { + if (args === undefined) { + callback(); + } else { + switch (args.length) { + case 1: + callback(args[0]); + break; + case 2: + callback(args[0], args[1]); + break; + case 3: + callback(args[0], args[1], args[2]); + break; + case 4: + callback(args[0], args[1], args[2], args[3]); + break; + default: + callback(...args); + break; + } + } + } catch (e) { + reportUncaughtException(e); + } finally { + $putInternalField($asyncContext, 0, restore); + } + } + + drainMicrotasks(); + } while (!queue.isEmpty()); + } + + $putInternalField(nextTickQueue, 0, 0); + $putInternalField(nextTickQueue, 1, queue); + $putInternalField(nextTickQueue, 2, processTicksAndRejections); + setup = undefined; + }; + + function nextTick(cb, args) { + validateFunction(cb); + if (setup) { + setup(); + process = globalThis.process; + } + if (process._exiting) return; + + queue.push({ + callback: cb, + args: $argumentCount() > 1 ? Array.prototype.slice.$call(arguments, 1) : undefined, + frame: $getInternalField($asyncContext, 0), + }); + $putInternalField(nextTickQueue, 0, 1); + } + + return nextTick; +} diff --git a/src/js/out/WebCoreJSBuiltins.cpp b/src/js/out/WebCoreJSBuiltins.cpp index 1003fd522..d767a3c39 100644 --- a/src/js/out/WebCoreJSBuiltins.cpp +++ b/src/js/out/WebCoreJSBuiltins.cpp @@ -658,6 +658,14 @@ const int s_processObjectInternalsGetStdinStreamCodeLength = 1386; static const JSC::Intrinsic s_processObjectInternalsGetStdinStreamCodeIntrinsic = JSC::NoIntrinsic; const char* const s_processObjectInternalsGetStdinStreamCode = "(function (fd){\"use strict\";var reader,readerRef;function ref(){reader\?\?=@Bun.stdin.stream().getReader(),readerRef\?\?=setInterval(()=>{},1<<30)}function unref(){if(readerRef)clearInterval(readerRef),readerRef=@undefined;if(reader)reader.cancel(),reader=@undefined}const stream=new((@getInternalField(@internalModuleRegistry,44))||(@createInternalModuleById(44))).ReadStream(fd),originalOn=stream.on;stream.on=function(event,listener){if(event===\"readable\")ref();return originalOn.call(this,event,listener)},stream.fd=fd;const originalPause=stream.pause;stream.pause=function(){return unref(),originalPause.call(this)};const originalResume=stream.resume;stream.resume=function(){return ref(),originalResume.call(this)};async function internalRead(stream2){try{var done,value;const read=reader\?.readMany();if(@isPromise(read))({done,value}=await read);else({done,value}=read);if(!done){stream2.push(value[0]);const length=value.length;for(let i=1;i<length;i++)stream2.push(value[i])}else stream2.emit(\"end\"),stream2.pause()}catch(err){stream2.destroy(err)}}return stream._read=function(size){internalRead(this)},stream.on(\"resume\",()=>{ref(),stream._undestroy()}),stream._readableState.reading=!1,stream.on(\"pause\",()=>{process.nextTick(()=>{if(!stream.readableFlowing)stream._readableState.reading=!1})}),stream.on(\"close\",()=>{process.nextTick(()=>{stream.destroy(),unref()})}),stream})\n"; +// initializeNextTickQueue +const JSC::ConstructAbility s_processObjectInternalsInitializeNextTickQueueCodeConstructAbility = JSC::ConstructAbility::CannotConstruct; +const JSC::ConstructorKind s_processObjectInternalsInitializeNextTickQueueCodeConstructorKind = JSC::ConstructorKind::None; +const JSC::ImplementationVisibility s_processObjectInternalsInitializeNextTickQueueCodeImplementationVisibility = JSC::ImplementationVisibility::Public; +const int s_processObjectInternalsInitializeNextTickQueueCodeLength = 2336; +static const JSC::Intrinsic s_processObjectInternalsInitializeNextTickQueueCodeIntrinsic = JSC::NoIntrinsic; +const char* const s_processObjectInternalsInitializeNextTickQueueCode = "(function (process,nextTickQueue,drainMicrotasksFn,reportUncaughtExceptionFn){\"use strict\";var queue,process,nextTickQueue=nextTickQueue,drainMicrotasks=drainMicrotasksFn,reportUncaughtException=reportUncaughtExceptionFn;function validateFunction(cb){if(typeof cb!==\"function\"){const err=@makeTypeError(`The \"callback\" argument must be of type \"function\". Received type ${typeof cb}`);throw err.code=\"ERR_INVALID_ARG_TYPE\",err}}var setup=()=>{queue=function createQueue(){class FixedCircularBuffer{constructor(){this.bottom=0,this.top=0,this.list=@newArrayWithSize(2048),this.next=null}isEmpty(){return this.top===this.bottom}isFull(){return(this.top+1&2047)===this.bottom}push(data){this.list[this.top]=data,this.top=this.top+1&2047}shift(){var{list,bottom}=this;const nextItem=list[bottom];if(nextItem===@undefined)return null;return list[bottom]=@undefined,this.bottom=bottom+1&2047,nextItem}}class FixedQueue{constructor(){this.head=this.tail=new FixedCircularBuffer}isEmpty(){return this.head.isEmpty()}push(data){if(this.head.isFull())this.head=this.head.next=new FixedCircularBuffer;this.head.push(data)}shift(){const tail=this.tail,next=tail.shift();if(tail.isEmpty()&&tail.next!==null)this.tail=tail.next,tail.next=null;return next}}return new FixedQueue}();function processTicksAndRejections(){var tock;do{while((tock=queue.shift())!==null){var{callback,args,frame}=tock,restore=@getInternalField(@asyncContext,0);@putInternalField(@asyncContext,0,frame);try{if(args===@undefined)callback();else switch(args.length){case 1:callback(args[0]);break;case 2:callback(args[0],args[1]);break;case 3:callback(args[0],args[1],args[2]);break;case 4:callback(args[0],args[1],args[2],args[3]);break;default:callback(...args);break}}catch(e){reportUncaughtException(e)}finally{@putInternalField(@asyncContext,0,restore)}}drainMicrotasks()}while(!queue.isEmpty())}@putInternalField(nextTickQueue,0,0),@putInternalField(nextTickQueue,1,queue),@putInternalField(nextTickQueue,2,processTicksAndRejections),setup=@undefined};function nextTick(cb,args){if(validateFunction(cb),setup)setup(),process=globalThis.process;if(process._exiting)return;queue.push({callback:cb,args:@argumentCount()>1\?@Array.prototype.slice.@call(arguments,1):@undefined,frame:@getInternalField(@asyncContext,0)}),@putInternalField(nextTickQueue,0,1)}return nextTick})\n"; + #define DEFINE_BUILTIN_GENERATOR(codeName, functionName, overriddenName, argumentCount) \ JSC::FunctionExecutable* codeName##Generator(JSC::VM& vm) \ {\ diff --git a/src/js/out/WebCoreJSBuiltins.h b/src/js/out/WebCoreJSBuiltins.h index cf28fa82a..4fc91dbd9 100644 --- a/src/js/out/WebCoreJSBuiltins.h +++ b/src/js/out/WebCoreJSBuiltins.h @@ -1179,20 +1179,31 @@ extern const JSC::ConstructAbility s_processObjectInternalsGetStdinStreamCodeCon extern const JSC::ConstructorKind s_processObjectInternalsGetStdinStreamCodeConstructorKind; extern const JSC::ImplementationVisibility s_processObjectInternalsGetStdinStreamCodeImplementationVisibility; +// initializeNextTickQueue +#define WEBCORE_BUILTIN_PROCESSOBJECTINTERNALS_INITIALIZENEXTTICKQUEUE 1 +extern const char* const s_processObjectInternalsInitializeNextTickQueueCode; +extern const int s_processObjectInternalsInitializeNextTickQueueCodeLength; +extern const JSC::ConstructAbility s_processObjectInternalsInitializeNextTickQueueCodeConstructAbility; +extern const JSC::ConstructorKind s_processObjectInternalsInitializeNextTickQueueCodeConstructorKind; +extern const JSC::ImplementationVisibility s_processObjectInternalsInitializeNextTickQueueCodeImplementationVisibility; + #define WEBCORE_FOREACH_PROCESSOBJECTINTERNALS_BUILTIN_DATA(macro) \ macro(binding, processObjectInternalsBinding, 1) \ macro(getStdioWriteStream, processObjectInternalsGetStdioWriteStream, 1) \ macro(getStdinStream, processObjectInternalsGetStdinStream, 1) \ + macro(initializeNextTickQueue, processObjectInternalsInitializeNextTickQueue, 4) \ #define WEBCORE_FOREACH_PROCESSOBJECTINTERNALS_BUILTIN_CODE(macro) \ macro(processObjectInternalsBindingCode, binding, ASCIILiteral(), s_processObjectInternalsBindingCodeLength) \ macro(processObjectInternalsGetStdioWriteStreamCode, getStdioWriteStream, ASCIILiteral(), s_processObjectInternalsGetStdioWriteStreamCodeLength) \ macro(processObjectInternalsGetStdinStreamCode, getStdinStream, ASCIILiteral(), s_processObjectInternalsGetStdinStreamCodeLength) \ + macro(processObjectInternalsInitializeNextTickQueueCode, initializeNextTickQueue, ASCIILiteral(), s_processObjectInternalsInitializeNextTickQueueCodeLength) \ #define WEBCORE_FOREACH_PROCESSOBJECTINTERNALS_BUILTIN_FUNCTION_NAME(macro) \ macro(binding) \ macro(getStdioWriteStream) \ macro(getStdinStream) \ + macro(initializeNextTickQueue) \ #define DECLARE_BUILTIN_GENERATOR(codeName, functionName, overriddenName, argumentCount) \ JSC::FunctionExecutable* codeName##Generator(JSC::VM&); diff --git a/test/js/bun/spawn/exit-code.test.ts b/test/js/bun/spawn/exit-code.test.ts index cda76a395..1b97179f6 100644 --- a/test/js/bun/spawn/exit-code.test.ts +++ b/test/js/bun/spawn/exit-code.test.ts @@ -17,6 +17,11 @@ it("unhandled promise rejection reports exit code 1", () => { expect(exitCode).toBe(1); }); +it("handled promise rejection reports exit code 0", () => { + const { exitCode } = spawnSync([bunExe(), import.meta.dir + "/exit-code-handled-throw.js"]); + expect(exitCode).toBe(1); +}); + it("process.exit(0) works", () => { const { exitCode } = spawnSync([bunExe(), import.meta.dir + "/exit-code-0.js"]); expect(exitCode).toBe(0); diff --git a/test/js/node/process/process-nexttick.test.js b/test/js/node/process/process-nexttick.test.js index becf3c236..6f1eee6ba 100644 --- a/test/js/node/process/process-nexttick.test.js +++ b/test/js/node/process/process-nexttick.test.js @@ -1,4 +1,8 @@ -import { it } from "bun:test"; +// Running this file in jest/vitest does not work as expected. Jest & Vitest +// mess with timers, producing unreliable results. You must manually test this +// in Node. +import { test, expect, it } from "bun:test"; +const isBun = !!process.versions.bun; it("process.nextTick", async () => { // You can verify this test is correct by copy pasting this into a browser's console and checking it doesn't throw an error. @@ -52,25 +56,35 @@ it("process.nextTick", async () => { }); { - var passed = false; + let passed = false; try { queueMicrotask(1234); } catch (exception) { - passed = exception instanceof TypeError; + if (isBun) { + passed = exception instanceof TypeError; + } else { + // Node.js throws a non-TypeError TypeError + passed = exception instanceof Error && exception.name === "TypeError"; + } } - if (!passed) throw new Error("queueMicrotask should throw a TypeError if the argument is not a function"); + if (!passed) throw new Error("1: queueMicrotask should throw a TypeError if the argument is not a function"); } { - var passed = false; + let passed = false; try { queueMicrotask(); } catch (exception) { - passed = exception instanceof TypeError; + if (isBun) { + passed = exception instanceof TypeError; + } else { + // Node.js throws a non-TypeError TypeError + passed = exception instanceof Error && exception.name === "TypeError"; + } } - if (!passed) throw new Error("queueMicrotask should throw a TypeError if the argument is empty"); + if (!passed) throw new Error("2: queueMicrotask should throw a TypeError if the argument is empty"); } }); @@ -97,3 +111,925 @@ it("process.nextTick 5 args", async () => { }, ...args); }); }); + +it("process.nextTick runs after queueMicrotask", async () => { + var resolve; + var promise = new Promise(_resolve => { + resolve = _resolve; + }); + + const order = []; + var nextTickI = 0; + var microtaskI = 0; + var remaining = 400; + var runs = []; + for (let i = 0; i < 100; i++) { + queueMicrotask(() => { + runs.push(queueMicrotask.name); + order.push("queueMicrotask " + microtaskI++); + if (--remaining === 0) resolve(order); + }); + process.nextTick(() => { + runs.push(process.nextTick.name); + order.push("process.nextTick " + nextTickI++); + if (--remaining === 0) resolve(order); + }); + } + + for (let i = 0; i < 100; i++) { + queueMicrotask(() => { + runs.push(queueMicrotask.name); + order.push("queueMicrotask " + microtaskI++); + if (--remaining === 0) resolve(order); + }); + } + + for (let i = 0; i < 100; i++) { + process.nextTick(() => { + runs.push(process.nextTick.name); + order.push("process.nextTick " + nextTickI++); + if (--remaining === 0) resolve(order); + }); + } + + await promise; + expect({ + order, + runs, + }).toEqual({ + "order": [ + "process.nextTick 0", + "process.nextTick 1", + "process.nextTick 2", + "process.nextTick 3", + "process.nextTick 4", + "process.nextTick 5", + "process.nextTick 6", + "process.nextTick 7", + "process.nextTick 8", + "process.nextTick 9", + "process.nextTick 10", + "process.nextTick 11", + "process.nextTick 12", + "process.nextTick 13", + "process.nextTick 14", + "process.nextTick 15", + "process.nextTick 16", + "process.nextTick 17", + "process.nextTick 18", + "process.nextTick 19", + "process.nextTick 20", + "process.nextTick 21", + "process.nextTick 22", + "process.nextTick 23", + "process.nextTick 24", + "process.nextTick 25", + "process.nextTick 26", + "process.nextTick 27", + "process.nextTick 28", + "process.nextTick 29", + "process.nextTick 30", + "process.nextTick 31", + "process.nextTick 32", + "process.nextTick 33", + "process.nextTick 34", + "process.nextTick 35", + "process.nextTick 36", + "process.nextTick 37", + "process.nextTick 38", + "process.nextTick 39", + "process.nextTick 40", + "process.nextTick 41", + "process.nextTick 42", + "process.nextTick 43", + "process.nextTick 44", + "process.nextTick 45", + "process.nextTick 46", + "process.nextTick 47", + "process.nextTick 48", + "process.nextTick 49", + "process.nextTick 50", + "process.nextTick 51", + "process.nextTick 52", + "process.nextTick 53", + "process.nextTick 54", + "process.nextTick 55", + "process.nextTick 56", + "process.nextTick 57", + "process.nextTick 58", + "process.nextTick 59", + "process.nextTick 60", + "process.nextTick 61", + "process.nextTick 62", + "process.nextTick 63", + "process.nextTick 64", + "process.nextTick 65", + "process.nextTick 66", + "process.nextTick 67", + "process.nextTick 68", + "process.nextTick 69", + "process.nextTick 70", + "process.nextTick 71", + "process.nextTick 72", + "process.nextTick 73", + "process.nextTick 74", + "process.nextTick 75", + "process.nextTick 76", + "process.nextTick 77", + "process.nextTick 78", + "process.nextTick 79", + "process.nextTick 80", + "process.nextTick 81", + "process.nextTick 82", + "process.nextTick 83", + "process.nextTick 84", + "process.nextTick 85", + "process.nextTick 86", + "process.nextTick 87", + "process.nextTick 88", + "process.nextTick 89", + "process.nextTick 90", + "process.nextTick 91", + "process.nextTick 92", + "process.nextTick 93", + "process.nextTick 94", + "process.nextTick 95", + "process.nextTick 96", + "process.nextTick 97", + "process.nextTick 98", + "process.nextTick 99", + "process.nextTick 100", + "process.nextTick 101", + "process.nextTick 102", + "process.nextTick 103", + "process.nextTick 104", + "process.nextTick 105", + "process.nextTick 106", + "process.nextTick 107", + "process.nextTick 108", + "process.nextTick 109", + "process.nextTick 110", + "process.nextTick 111", + "process.nextTick 112", + "process.nextTick 113", + "process.nextTick 114", + "process.nextTick 115", + "process.nextTick 116", + "process.nextTick 117", + "process.nextTick 118", + "process.nextTick 119", + "process.nextTick 120", + "process.nextTick 121", + "process.nextTick 122", + "process.nextTick 123", + "process.nextTick 124", + "process.nextTick 125", + "process.nextTick 126", + "process.nextTick 127", + "process.nextTick 128", + "process.nextTick 129", + "process.nextTick 130", + "process.nextTick 131", + "process.nextTick 132", + "process.nextTick 133", + "process.nextTick 134", + "process.nextTick 135", + "process.nextTick 136", + "process.nextTick 137", + "process.nextTick 138", + "process.nextTick 139", + "process.nextTick 140", + "process.nextTick 141", + "process.nextTick 142", + "process.nextTick 143", + "process.nextTick 144", + "process.nextTick 145", + "process.nextTick 146", + "process.nextTick 147", + "process.nextTick 148", + "process.nextTick 149", + "process.nextTick 150", + "process.nextTick 151", + "process.nextTick 152", + "process.nextTick 153", + "process.nextTick 154", + "process.nextTick 155", + "process.nextTick 156", + "process.nextTick 157", + "process.nextTick 158", + "process.nextTick 159", + "process.nextTick 160", + "process.nextTick 161", + "process.nextTick 162", + "process.nextTick 163", + "process.nextTick 164", + "process.nextTick 165", + "process.nextTick 166", + "process.nextTick 167", + "process.nextTick 168", + "process.nextTick 169", + "process.nextTick 170", + "process.nextTick 171", + "process.nextTick 172", + "process.nextTick 173", + "process.nextTick 174", + "process.nextTick 175", + "process.nextTick 176", + "process.nextTick 177", + "process.nextTick 178", + "process.nextTick 179", + "process.nextTick 180", + "process.nextTick 181", + "process.nextTick 182", + "process.nextTick 183", + "process.nextTick 184", + "process.nextTick 185", + "process.nextTick 186", + "process.nextTick 187", + "process.nextTick 188", + "process.nextTick 189", + "process.nextTick 190", + "process.nextTick 191", + "process.nextTick 192", + "process.nextTick 193", + "process.nextTick 194", + "process.nextTick 195", + "process.nextTick 196", + "process.nextTick 197", + "process.nextTick 198", + "process.nextTick 199", + "queueMicrotask 0", + "queueMicrotask 1", + "queueMicrotask 2", + "queueMicrotask 3", + "queueMicrotask 4", + "queueMicrotask 5", + "queueMicrotask 6", + "queueMicrotask 7", + "queueMicrotask 8", + "queueMicrotask 9", + "queueMicrotask 10", + "queueMicrotask 11", + "queueMicrotask 12", + "queueMicrotask 13", + "queueMicrotask 14", + "queueMicrotask 15", + "queueMicrotask 16", + "queueMicrotask 17", + "queueMicrotask 18", + "queueMicrotask 19", + "queueMicrotask 20", + "queueMicrotask 21", + "queueMicrotask 22", + "queueMicrotask 23", + "queueMicrotask 24", + "queueMicrotask 25", + "queueMicrotask 26", + "queueMicrotask 27", + "queueMicrotask 28", + "queueMicrotask 29", + "queueMicrotask 30", + "queueMicrotask 31", + "queueMicrotask 32", + "queueMicrotask 33", + "queueMicrotask 34", + "queueMicrotask 35", + "queueMicrotask 36", + "queueMicrotask 37", + "queueMicrotask 38", + "queueMicrotask 39", + "queueMicrotask 40", + "queueMicrotask 41", + "queueMicrotask 42", + "queueMicrotask 43", + "queueMicrotask 44", + "queueMicrotask 45", + "queueMicrotask 46", + "queueMicrotask 47", + "queueMicrotask 48", + "queueMicrotask 49", + "queueMicrotask 50", + "queueMicrotask 51", + "queueMicrotask 52", + "queueMicrotask 53", + "queueMicrotask 54", + "queueMicrotask 55", + "queueMicrotask 56", + "queueMicrotask 57", + "queueMicrotask 58", + "queueMicrotask 59", + "queueMicrotask 60", + "queueMicrotask 61", + "queueMicrotask 62", + "queueMicrotask 63", + "queueMicrotask 64", + "queueMicrotask 65", + "queueMicrotask 66", + "queueMicrotask 67", + "queueMicrotask 68", + "queueMicrotask 69", + "queueMicrotask 70", + "queueMicrotask 71", + "queueMicrotask 72", + "queueMicrotask 73", + "queueMicrotask 74", + "queueMicrotask 75", + "queueMicrotask 76", + "queueMicrotask 77", + "queueMicrotask 78", + "queueMicrotask 79", + "queueMicrotask 80", + "queueMicrotask 81", + "queueMicrotask 82", + "queueMicrotask 83", + "queueMicrotask 84", + "queueMicrotask 85", + "queueMicrotask 86", + "queueMicrotask 87", + "queueMicrotask 88", + "queueMicrotask 89", + "queueMicrotask 90", + "queueMicrotask 91", + "queueMicrotask 92", + "queueMicrotask 93", + "queueMicrotask 94", + "queueMicrotask 95", + "queueMicrotask 96", + "queueMicrotask 97", + "queueMicrotask 98", + "queueMicrotask 99", + "queueMicrotask 100", + "queueMicrotask 101", + "queueMicrotask 102", + "queueMicrotask 103", + "queueMicrotask 104", + "queueMicrotask 105", + "queueMicrotask 106", + "queueMicrotask 107", + "queueMicrotask 108", + "queueMicrotask 109", + "queueMicrotask 110", + "queueMicrotask 111", + "queueMicrotask 112", + "queueMicrotask 113", + "queueMicrotask 114", + "queueMicrotask 115", + "queueMicrotask 116", + "queueMicrotask 117", + "queueMicrotask 118", + "queueMicrotask 119", + "queueMicrotask 120", + "queueMicrotask 121", + "queueMicrotask 122", + "queueMicrotask 123", + "queueMicrotask 124", + "queueMicrotask 125", + "queueMicrotask 126", + "queueMicrotask 127", + "queueMicrotask 128", + "queueMicrotask 129", + "queueMicrotask 130", + "queueMicrotask 131", + "queueMicrotask 132", + "queueMicrotask 133", + "queueMicrotask 134", + "queueMicrotask 135", + "queueMicrotask 136", + "queueMicrotask 137", + "queueMicrotask 138", + "queueMicrotask 139", + "queueMicrotask 140", + "queueMicrotask 141", + "queueMicrotask 142", + "queueMicrotask 143", + "queueMicrotask 144", + "queueMicrotask 145", + "queueMicrotask 146", + "queueMicrotask 147", + "queueMicrotask 148", + "queueMicrotask 149", + "queueMicrotask 150", + "queueMicrotask 151", + "queueMicrotask 152", + "queueMicrotask 153", + "queueMicrotask 154", + "queueMicrotask 155", + "queueMicrotask 156", + "queueMicrotask 157", + "queueMicrotask 158", + "queueMicrotask 159", + "queueMicrotask 160", + "queueMicrotask 161", + "queueMicrotask 162", + "queueMicrotask 163", + "queueMicrotask 164", + "queueMicrotask 165", + "queueMicrotask 166", + "queueMicrotask 167", + "queueMicrotask 168", + "queueMicrotask 169", + "queueMicrotask 170", + "queueMicrotask 171", + "queueMicrotask 172", + "queueMicrotask 173", + "queueMicrotask 174", + "queueMicrotask 175", + "queueMicrotask 176", + "queueMicrotask 177", + "queueMicrotask 178", + "queueMicrotask 179", + "queueMicrotask 180", + "queueMicrotask 181", + "queueMicrotask 182", + "queueMicrotask 183", + "queueMicrotask 184", + "queueMicrotask 185", + "queueMicrotask 186", + "queueMicrotask 187", + "queueMicrotask 188", + "queueMicrotask 189", + "queueMicrotask 190", + "queueMicrotask 191", + "queueMicrotask 192", + "queueMicrotask 193", + "queueMicrotask 194", + "queueMicrotask 195", + "queueMicrotask 196", + "queueMicrotask 197", + "queueMicrotask 198", + "queueMicrotask 199", + ], + "runs": [ + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "nextTick", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + "queueMicrotask", + ], + }); +}); + +it("process.nextTick can be called 100,000 times", async () => { + var county = 0; + function ticky() { + county++; + } + for (let i = 0; i < 100_000; i++) { + process.nextTick(ticky); + } + + await 1; + expect(county).toBe(100_000); +}); + +it("process.nextTick works more than once", async () => { + var county = 0; + function ticky() { + county++; + } + for (let i = 0; i < 1000; i++) { + process.nextTick(ticky); + await 1; + } + expect(county).toBe(1); + await new Promise(resolve => setTimeout(resolve, 0)); + expect(county).toBe(1000); +}); + +// `enterWith` is problematic because it and `nextTick` both rely on +// JSC's `global.onEachMicrotaskTick`, and this test is designed to +// cover what happens when both are active +it("process.nextTick and AsyncLocalStorage.enterWith don't conflict", async () => { + const AsyncLocalStorage = require("async_hooks").AsyncLocalStorage; + const t = require("timers/promises"); + const storage = new AsyncLocalStorage(); + + let call1 = false; + let call2 = false; + + process.nextTick(() => (call1 = true)); + + const p = Promise.withResolvers(); + const p2 = p.promise.then(() => { + return storage.getStore(); // should not leak "hello" + }); + const promise = Promise.resolve().then(async () => { + storage.enterWith("hello"); + process.nextTick(() => (call2 = true)); + + let didCall = false; + let value = null; + function ticky() { + didCall = true; + value = storage.getStore(); + } + process.nextTick(ticky); + await t.setTimeout(1); + expect(didCall).toBe(true); + expect(value).toBe("hello"); + expect(storage.getStore()).toBe("hello"); + }); + + expect(storage.getStore()).toBe(undefined); + await promise; + p.resolve(); + expect(await p2).toBe(undefined); + + expect(call1).toBe(true); + expect(call2).toBe(true); +}); diff --git a/test/js/node/readline/readline_promises.node.test.ts b/test/js/node/readline/readline_promises.node.test.ts index a46fe841f..a6c2fcef2 100644 --- a/test/js/node/readline/readline_promises.node.test.ts +++ b/test/js/node/readline/readline_promises.node.test.ts @@ -40,7 +40,7 @@ describe("readline/promises.createInterface()", () => { rli.on("line", mustNotCall()); fi.emit("data", "\t"); - process.nextTick(() => { + queueMicrotask(() => { expect(fi.output).toMatch(/^Tab completion error/); rli.close(); done(); |