diff --git a/package.json b/package.json index 1e1fefc..8be513d 100644 --- a/package.json +++ b/package.json @@ -12,9 +12,9 @@ }, "scripts": { "lint": "eslint {src,test}/**/*.ts", - "test": "npm run lint && npm run build && nyc npm run test-only", + "test": "npm run lint && npm run build && npm run test-only", "test-nolint": "npm run build && npm run test-only", - "test-only": "mocha --colors -r ts-node/register test/*.ts", + "test-only": "mocha --trace-uncaught --colors -r ts-node/register test/*.ts", "build": "node-gyp rebuild && npm run compile-ts && gen-esm-wrapper . ./.esm-wrapper.mjs", "prepack": "npm run build", "compile-ts": "tsc -p tsconfig.json" diff --git a/src/binding.cc b/src/binding.cc index e0ab488..ea63715 100644 --- a/src/binding.cc +++ b/src/binding.cc @@ -385,6 +385,79 @@ bool Worker::IsLoopAlive() { return uv_loop_alive(&loop_); } +class StandaloneImmediate { + public: + StandaloneImmediate(Local context, Local function); + ~StandaloneImmediate(); + + private: + void OnTimeout(); + static void Cleanup(void* arg, void (*cb)(void*), void* cbarg); + + struct CloseCbData { void (*cb)(void*); void* cbarg; }; + std::vector close_cbs_; + Isolate* isolate_; + Global context_; + Global function_; + std::unique_ptr async_resource_; + uv_check_t check_; + AsyncCleanupHookHandle async_cleanup_handle_; +}; + +StandaloneImmediate::StandaloneImmediate(Local context, Local function) { + Isolate* isolate = context->GetIsolate(); + uv_loop_t* loop = GetCurrentEventLoop(isolate); + uv_check_init(loop, &check_); + uv_check_start(&check_, [](uv_check_t* check) { + static_cast(check->data)->OnTimeout(); + }); + check_.data = this; + async_cleanup_handle_ = AddEnvironmentCleanupHook(isolate, Cleanup, this); + + Local resource = Object::New(isolate); + async_resource_ = std::make_unique(isolate, resource, "synchronous-worker:Immediate"); + function_.Reset(isolate, function); + context_.Reset(isolate, isolate->GetCurrentContext()); + isolate_ = isolate; +} + +StandaloneImmediate::~StandaloneImmediate() { + if (async_cleanup_handle_) + RemoveEnvironmentCleanupHook(std::move(async_cleanup_handle_)); +} + +void StandaloneImmediate::OnTimeout() { + uv_check_stop(&check_); + HandleScope handle_scope(isolate_); + Context::Scope context_scope(context_.Get(isolate_)); + async_resource_->MakeCallback(function_.Get(isolate_), 0, nullptr); + Cleanup(this, [](void* arg) {}, nullptr); +} + +void StandaloneImmediate::Cleanup(void* arg, void (*cb)(void*), void* cbarg) { + StandaloneImmediate* self = static_cast(arg); + self->close_cbs_.emplace_back(CloseCbData { cb, cbarg }); + if (self->close_cbs_.size() > 1) { + return; + } + + uv_check_stop(&self->check_); + uv_close(reinterpret_cast(&self->check_), [](uv_handle_t* handle) { + StandaloneImmediate* self = static_cast(handle->data); + HandleScope handle_scope(self->isolate_); + Context::Scope context_scope(self->context_.Get(self->isolate_)); + + for (const auto& close_cb : self->close_cbs_) { + close_cb.cb(close_cb.cbarg); + } + delete self; + }); +} + +void StandaloneSetImmediate(const FunctionCallbackInfo& args) { + new StandaloneImmediate(args.GetIsolate()->GetCurrentContext(), args[0].As()); +} + NODE_MODULE_INIT() { Isolate* isolate = context->GetIsolate(); Local templ = FunctionTemplate::New(isolate, Worker::New); @@ -414,6 +487,12 @@ NODE_MODULE_INIT() { USE(exports->Set(context, String::NewFromUtf8Literal(isolate, "SynchronousWorkerImpl"), worker_fn)); + Local set_immediate_fn; + if (!Function::New(context, StandaloneSetImmediate).ToLocal(&set_immediate_fn)) + return; + USE(exports->Set(context, + String::NewFromUtf8Literal(isolate, "standaloneSetImmediate"), + set_immediate_fn)); NODE_DEFINE_CONSTANT(exports, UV_RUN_DEFAULT); NODE_DEFINE_CONSTANT(exports, UV_RUN_ONCE); diff --git a/src/index.ts b/src/index.ts index 6e97f5d..bd1d314 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,7 +5,8 @@ const { SynchronousWorkerImpl, UV_RUN_DEFAULT, UV_RUN_ONCE, - UV_RUN_NOWAIT + UV_RUN_NOWAIT, + standaloneSetImmediate } = bindings('synchronous_worker'); const kHandle = Symbol('kHandle'); const kProcess = Symbol('kProcess'); @@ -123,7 +124,7 @@ class SynchronousWorker extends EventEmitter { async stop(): Promise { return this[kStoppedPromise] ??= new Promise(resolve => { this[kHandle].signalStop(); - setImmediate(() => { + standaloneSetImmediate(() => { this[kHandle].stop(); resolve(); }); diff --git a/test/index.ts b/test/index.ts index 2b486c5..10f8735 100644 --- a/test/index.ts +++ b/test/index.ts @@ -194,4 +194,18 @@ describe('SynchronousWorker allows running Node.js code', () => { }); await w.stop(); }); + + it('properly handles immediates when FreeEnvironment() is called on a shared event loop', async() => { + const w = new SynchronousWorker({ + sharedEventLoop: true, + sharedMicrotaskQueue: true + }); + + setImmediate(() => { + setImmediate(() => { + setImmediate(() => {}); + }); + }); + await w.stop(); + }); });