├── .eslintrc ├── .github └── workflows │ └── nodejs.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── binding.gyp ├── package.json ├── src ├── binding.cc └── index.ts ├── test └── index.ts └── tsconfig.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "semistandard", 4 | "plugin:promise/recommended", 5 | "plugin:@typescript-eslint/recommended" 6 | ], 7 | "rules": { 8 | "@typescript-eslint/no-explicit-any": 0, 9 | "@typescript-eslint/no-empty-function": 0, 10 | "no-return-assign": 0, 11 | "space-before-function-paren": ["error", "never"] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: CI 4 | 5 | jobs: 6 | test-posix: 7 | name: Unix tests 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | os: [ubuntu-latest, macos-latest, windows-latest] 12 | node-version: [15.x, 16.x, 17.x, 18.x] 13 | exclude: 14 | - os: windows-latest 15 | node-version: 15.x 16 | runs-on: ${{matrix.os}} 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v2 21 | with: 22 | check-latest: true 23 | node-version: ${{ matrix.node-version }} 24 | - name: Install Dependencies 25 | run: npm install 26 | - name: Test 27 | run: npm test 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .lock-wscript 3 | .idea/ 4 | .vscode/ 5 | *.iml 6 | .nvmrc 7 | .nyc_output 8 | *.swp 9 | lerna-debug.log 10 | lib-cov 11 | npm-debug.log 12 | .idea/ 13 | coverage/ 14 | dist/ 15 | node_modules/ 16 | .lock-wscript 17 | .cache/ 18 | expansions.yaml 19 | tmp/expansions.yaml 20 | .evergreen/mongodb 21 | tmp/ 22 | .esm-wrapper.mjs 23 | /lib/ 24 | package-lock.json 25 | build/ 26 | crash.log 27 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at . All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Anna Henningsen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # synchronous-worker – Run Node.js APIs synchronously 2 | 3 | ## Usage Example 4 | 5 | ```js 6 | const w = new SynchronousWorker(); 7 | const fetch = w.createRequire(__filename)('node-fetch'); 8 | const response = w.runLoopUntilPromiseResolved(fetch('http://example.org')); 9 | const text = w.runLoopUntilPromiseResolved(response.text()); 10 | console.log(text); 11 | ``` 12 | 13 | ## API 14 | 15 | ### `new SynchronousWorker([options])` 16 | 17 | Create a new Node.js instance on the same thread. Valid options are: 18 | 19 | - `sharedEventLoop`: Use the same event loop as the outer Node.js instance. 20 | If this is passed, the `.runLoop()` and `.runLoopUntilPromiseResolved()` 21 | methods become unavailable. Defaults to `false`. 22 | - `sharedMicrotaskQueue`: Use the same microtask queue as the outer Node.js 23 | instance. This is used for resolving promises created in the inner context, 24 | including those implicitly generated by `async/await`. If this is passed, 25 | the `.runLoopUntilPromiseResolved()` method becomes unavailable. 26 | Defaults to `false`. 27 | 28 | While this package will accept 29 | `{ sharedEventLoop: false, sharedMicrotaskQueue: true }` as options, passing 30 | them does not typically make sense. 31 | 32 | ### `synchronousWorker.runLoop([mode])` 33 | 34 | Spin the event loop of the inner Node.js instance. `mode` can be either 35 | `default`, `once` or `nowait`. See the [libuv documentation for `uv_run()`] 36 | for details on these modes. 37 | 38 | ### `synchronousWorker.runLoopUntilPromiseResolved(promise)` 39 | 40 | Spin the event loop of the innsert Node.js instance until a specific `Promise` 41 | is resolved. 42 | 43 | ### `synchronousWorker.runInWorkerScope(fn)` 44 | 45 | Wrap `fn` and run it as if it were run on the event loop of the inner Node.js 46 | instance. In particular, this ensures that Promises created by the function 47 | itself are resolved correctly. You should generally use this to run any code 48 | inside the innert Node.js instance that performs asynchronous activity and that 49 | is not already running in an asynchronous context (you can compare this to 50 | the code that runs synchronously from the main file of a Node.js application). 51 | 52 | ### `synchronousWorker.loopAlive` 53 | 54 | This is a read-only boolean property indicating whether there are currently any 55 | items on the event loop of the inner Node.js instance. 56 | 57 | ### `synchronousWorker.stop()` 58 | 59 | Interrupt any execution of code inside the inner Node.js instance, i.e. 60 | return directly from a `.runLoop()`, `.runLoopUntilPromiseResolved()` or 61 | `.runInWorkerScope()` call. This will render the Node.js instance unusable 62 | and is generally comparable to running `process.exit()`. 63 | 64 | This method returns a `Promise` that will be resolved when all resources 65 | associated with this Node.js instance are released. This `Promise` resolves on 66 | the event loop of the *outer* Node.js instance. 67 | 68 | ### `synchronousWorker.createRequire(filename)` 69 | 70 | Create a `require()` function that can be used for loading code inside the 71 | inner Node.js instance. See [`module.createRequire()`][] for details. 72 | 73 | ### `synchronousWorker.globalThis` 74 | 75 | Returns a reference to the global object of the inner Node.js instance. 76 | 77 | ### `synchronousWorker.process` 78 | 79 | Returns a reference to the `process` object of the inner Node.js instance. 80 | 81 | # FAQ 82 | 83 | ## What does this module do? 84 | 85 | Create a new Node.js instance, using the same thread and the same JS heap. 86 | You can create Node.js API objects, like network sockets, inside the new 87 | Node.js instance, and spin the underlying event loop manually. 88 | 89 | ## Why would I use this package? 90 | 91 | The most common use case is probably running asynchronous code synchronously, 92 | in situations where doing so cannot be avoided (even though one should try 93 | really hard to avoid it). Another popular npm package that does this is 94 | [`deasync`][], but `deasync` 95 | 96 | - solves this problem by starting the event loop while it is already running 97 | (which is explicitly *not* supported by libuv and may lead to crashes) 98 | - doesn’t allow specifying *which* resources or callbacks should be waited for, 99 | and instead allows everything inside the current thread to progress. 100 | 101 | ## How can I avoid using this package? 102 | 103 | If you do not need to directly interact with the objects inside the inner 104 | Node.js instance, a lot of the time [`Worker threads`][] together with 105 | [`Atomics.wait()`][] will give you what you need. For example, the 106 | `node-fetch` snippet from above could also be written as: 107 | 108 | ```js 109 | const { 110 | Worker, MessageChannel, receiveMessageOnPort 111 | } = require('worker_threads'); 112 | 113 | const { port1, port2 } = new MessageChannel(); 114 | const notifyHandle = new Int32Array(new SharedArrayBuffer(4)); 115 | 116 | const w = new Worker(` 117 | const { 118 | parentPort, workerData: { notifyHandle, port2 } 119 | } = require('worker_threads'); 120 | 121 | (async () => { 122 | const fetch = require('node-fetch'); 123 | const response = await fetch('http://example.org'); 124 | const text = await response.text(); 125 | port2.postMessage({ text }); 126 | Atomics.store(notifyHandle, 0, 1); 127 | Atomics.notify(notifyHandle, 0); 128 | })();`, { 129 | eval: true, workerData: { notifyHandle, port2 }, transferList: [ port2 ] 130 | }); 131 | 132 | Atomics.wait(notifyHandle, 0, 0); 133 | const { text } = receiveMessageOnPort(port1).message; 134 | console.log(text); 135 | ``` 136 | 137 | That’s arguably a bit more complicated, but doesn’t require any native code 138 | and only uses APIs that are also available on lower Node.js versions. 139 | 140 | ## Which Node.js versions are supported? 141 | 142 | In order to work, synchronous-worker needs a recent Node.js version, because 143 | older versions are missing a few bugfixes or features. The following PRs are 144 | relevant for this (all of them are included in Node.js 15.5.0): 145 | 146 | - [#36581](https://github.com/nodejs/node/pull/36581) 147 | - [#36482](https://github.com/nodejs/node/pull/36482) 148 | - [#36441](https://github.com/nodejs/node/pull/36441) 149 | - [#36414](https://github.com/nodejs/node/pull/36414) 150 | - [#36413](https://github.com/nodejs/node/pull/36413) 151 | 152 | ## My async functions/Promises/… don’t work 153 | 154 | If you run a `SynchronousWorker` with its own microtask queue (i.e. in default 155 | mode), code like this will not work as expected: 156 | 157 | ```js 158 | const w = new SynchronousWorker(); 159 | let promise; 160 | w.runInWorkerScope(() => { 161 | promise = (async() => { 162 | return await w.createRequire(__filename)('node-fetch')(...); 163 | })(); 164 | }); 165 | w.runLoopUntilPromiseResolved(promise); 166 | ``` 167 | 168 | The reason for this is that `async` functions (and Promise `.then()` handlers) 169 | add their microtasks to the microtask queue for the Context in which the 170 | async function (or `.then()` callback) was defined, and not the Context in which 171 | the original `Promise` was created. Put in other words, it is possible for a 172 | `Promise` chain to be run on different microtask queues. 173 | 174 | While I find this behavior counterintuitive, it is what the V8 engine does, 175 | and is not under the control of Node.js or this package. 176 | 177 | What this means is that you will need to make sure that the functions are 178 | compiled in the Context in which they are supposed to be run; the two main 179 | ways to achieve that are to: 180 | 181 | - Put them in a separate file that is loaded through `w.createRequire()` 182 | - Use `w.createRequire(__filename)('vm').runInThisContext()` to manually compile 183 | the code for the function in the Context of the target Node.js instance. 184 | 185 | For example: 186 | 187 | ```js 188 | const w = new SynchronousWorker(); 189 | const req = w.createRequire(__filename); 190 | let promise; 191 | w.runInWorkerScope(() => { 192 | promise = req('vm').runInThisContext(`(async(req) => { 193 | return await req('node-fetch')(...); 194 | })`)(req)); 195 | }); 196 | w.runLoopUntilPromiseResolved(promise); 197 | ``` 198 | 199 | ## I found a bug/crash while using this package. What do I do now? 200 | 201 | You can [file a bug report][] on Github. Please include a reproduction, the 202 | version of this package that you’re using, and the Node.js version that you’re 203 | using, and ideally also make sure that it’s a first-time report. 204 | 205 | [`deasync`]: https://www.npmjs.com/package/deasync 206 | [`Worker threads`]: https://nodejs.org/api/worker_threads.html 207 | [`Atomics.wait()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/wait 208 | [libuv documentation for `uv_run()`]: http://docs.libuv.org/en/v1.x/loop.html#c.uv_run 209 | [`module.createRequire()`]: https://nodejs.org/api/module.html#module_module_createrequire_filename 210 | [file a bug report]: https://github.com/addaleax/synchronous-worker/issues 211 | -------------------------------------------------------------------------------- /binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | 'targets': [{ 3 | 'target_name': 'synchronous_worker', 4 | 'sources': [ 'src/binding.cc' ] 5 | }] 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "synchronous-worker", 3 | "version": "1.0.5", 4 | "description": "Run Node.js APIs synchronously", 5 | "main": "lib/index.js", 6 | "exports": { 7 | "require": "./lib/index.js", 8 | "import": "./.esm-wrapper.mjs" 9 | }, 10 | "engines": { 11 | "node": ">= 15.5.0" 12 | }, 13 | "scripts": { 14 | "lint": "eslint {src,test}/**/*.ts", 15 | "test": "npm run lint && npm run build && nyc npm run test-only", 16 | "test-nolint": "npm run build && npm run test-only", 17 | "test-only": "mocha --colors -r ts-node/register test/*.ts", 18 | "build": "node-gyp rebuild && npm run compile-ts && gen-esm-wrapper . ./.esm-wrapper.mjs", 19 | "prepack": "npm run build", 20 | "compile-ts": "tsc -p tsconfig.json" 21 | }, 22 | "keywords": [ 23 | "node.js", 24 | "worker", 25 | "threads", 26 | "synchronous" 27 | ], 28 | "author": "Anna Henningsen ", 29 | "homepage": "https://github.com/addaleax/synchronous-worker", 30 | "repository": { 31 | "type": "git", 32 | "url": "https://github.com/addaleax/synchronous-worker.git" 33 | }, 34 | "bugs": { 35 | "url": "https://github.com/addaleax/synchronous-worker/issues" 36 | }, 37 | "license": "MIT", 38 | "devDependencies": { 39 | "@types/mocha": "^8.0.3", 40 | "@types/node": "^14.11.1", 41 | "@typescript-eslint/eslint-plugin": "^4.2.0", 42 | "@typescript-eslint/parser": "^4.2.0", 43 | "eslint": "^7.9.0", 44 | "eslint-config-semistandard": "^15.0.1", 45 | "eslint-config-standard": "^14.1.1", 46 | "eslint-plugin-import": "^2.22.0", 47 | "eslint-plugin-node": "^11.1.0", 48 | "eslint-plugin-promise": "^4.2.1", 49 | "eslint-plugin-standard": "^4.0.1", 50 | "gen-esm-wrapper": "^1.1.0", 51 | "mocha": "^8.1.3", 52 | "node-fetch": "^2.6.1", 53 | "nyc": "^15.1.0", 54 | "ts-node": "^10.8.1", 55 | "typescript": "^4.0.3" 56 | }, 57 | "dependencies": { 58 | "bindings": "^1.5.0" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/binding.cc: -------------------------------------------------------------------------------- 1 | #include "node_version.h" 2 | 3 | // Node.js published binary compatibility 4 | #undef _GLIBCXX_USE_CXX11_ABI 5 | #if defined(__arm__) || NODE_VERSION_AT_LEAST(18, 0, 0) 6 | #define _GLIBCXX_USE_CXX11_ABI 1 7 | #else 8 | #define _GLIBCXX_USE_CXX11_ABI 0 9 | #endif 10 | 11 | #include "node.h" 12 | #include "uv.h" 13 | 14 | using namespace node; 15 | using namespace v8; 16 | 17 | namespace synchronous_worker { 18 | 19 | template inline void USE(T&&) {} 20 | 21 | class Worker { 22 | public: 23 | Worker(Isolate* isolate, Local wrap); 24 | 25 | static void New(const FunctionCallbackInfo& args); 26 | static void Start(const FunctionCallbackInfo& args); 27 | static void Load(const FunctionCallbackInfo& args); 28 | static void RunLoop(const FunctionCallbackInfo& args); 29 | static void IsLoopAlive(const FunctionCallbackInfo& args); 30 | static void SignalStop(const FunctionCallbackInfo& args); 31 | static void Stop(const FunctionCallbackInfo& args); 32 | static void RunInCallbackScope(const FunctionCallbackInfo& args); 33 | 34 | struct WorkerScope : public EscapableHandleScope, 35 | public Context::Scope, 36 | public Isolate::SafeForTerminationScope { 37 | public: 38 | explicit WorkerScope(Worker* w); 39 | ~WorkerScope(); 40 | 41 | private: 42 | Worker* w_; 43 | bool orig_can_be_terminated_; 44 | }; 45 | 46 | Local context() const; 47 | 48 | private: 49 | static Worker* Unwrap(const FunctionCallbackInfo& arg); 50 | static void CleanupHook(void* arg); 51 | void OnExit(int code); 52 | 53 | void Start(bool own_loop, bool own_microtaskqueue); 54 | MaybeLocal Load(Local callback); 55 | MaybeLocal RunInCallbackScope(Local callback); 56 | void RunLoop(uv_run_mode mode); 57 | bool IsLoopAlive(); 58 | void SignalStop(); 59 | void Stop(bool may_throw); 60 | 61 | Isolate* isolate_; 62 | Global wrap_; 63 | 64 | uv_loop_t loop_; 65 | std::unique_ptr microtask_queue_; 66 | Global outer_context_; 67 | Global context_; 68 | IsolateData* isolate_data_ = nullptr; 69 | Environment* env_ = nullptr; 70 | bool signaled_stop_ = false; 71 | bool can_be_terminated_ = false; 72 | bool loop_is_running_ = false; 73 | }; 74 | 75 | Worker::WorkerScope::WorkerScope(Worker* w) 76 | : EscapableHandleScope(w->isolate_), 77 | Scope(w->context()), 78 | SafeForTerminationScope(w->isolate_), 79 | w_(w), 80 | orig_can_be_terminated_(w->can_be_terminated_) { 81 | w_->can_be_terminated_ = true; 82 | } 83 | 84 | Worker::WorkerScope::~WorkerScope() { 85 | w_->can_be_terminated_ = orig_can_be_terminated_; 86 | } 87 | 88 | Local Worker::context() const { 89 | return context_.Get(isolate_); 90 | } 91 | 92 | Worker::Worker(Isolate* isolate, Local wrap) 93 | : isolate_(isolate), wrap_(isolate, wrap) { 94 | AddEnvironmentCleanupHook(isolate, CleanupHook, this); 95 | loop_.data = nullptr; 96 | wrap->SetAlignedPointerInInternalField(0, this); 97 | 98 | Local outer_context = isolate_->GetCurrentContext(); 99 | outer_context_.Reset(isolate_, outer_context); 100 | } 101 | 102 | Worker* Worker::Unwrap(const FunctionCallbackInfo& args) { 103 | Local value = args.This(); 104 | if (!value->IsObject() || value.As()->InternalFieldCount() < 1) { 105 | Isolate* isolate = args.GetIsolate(); 106 | isolate->ThrowException( 107 | Exception::Error( 108 | String::NewFromUtf8Literal(isolate, "Invalid 'this' value"))); 109 | return nullptr; 110 | } 111 | return static_cast( 112 | value.As()->GetAlignedPointerFromInternalField(0)); 113 | } 114 | 115 | void Worker::New(const FunctionCallbackInfo& args) { 116 | new Worker(args.GetIsolate(), args.This()); 117 | } 118 | 119 | void Worker::Start(const FunctionCallbackInfo& args) { 120 | Worker* self = Unwrap(args); 121 | if (self == nullptr) return; 122 | self->Start( 123 | args[0]->BooleanValue(args.GetIsolate()), 124 | args[1]->BooleanValue(args.GetIsolate())); 125 | } 126 | 127 | void Worker::Stop(const FunctionCallbackInfo& args) { 128 | Worker* self = Unwrap(args); 129 | if (self == nullptr) return; 130 | self->Stop(true); 131 | } 132 | 133 | void Worker::SignalStop(const FunctionCallbackInfo& args) { 134 | Worker* self = Unwrap(args); 135 | if (self == nullptr) return; 136 | self->SignalStop(); 137 | args.GetIsolate()->CancelTerminateExecution(); 138 | } 139 | 140 | void Worker::Load(const FunctionCallbackInfo& args) { 141 | Worker* self = Unwrap(args); 142 | if (self == nullptr) return; 143 | if (!args[0]->IsFunction()) { 144 | self->isolate_->ThrowException( 145 | Exception::TypeError( 146 | String::NewFromUtf8Literal(self->isolate_, 147 | "The load() argument must be a function"))); 148 | return; 149 | } 150 | Local result; 151 | if (self->Load(args[0].As()).ToLocal(&result)) { 152 | args.GetReturnValue().Set(result); 153 | } 154 | } 155 | 156 | void Worker::RunLoop(const FunctionCallbackInfo& args) { 157 | Worker* self = Unwrap(args); 158 | if (self == nullptr) return; 159 | int64_t mode; 160 | if (!args[0]->IntegerValue(args.GetIsolate()->GetCurrentContext()).To(&mode)) 161 | return; 162 | self->RunLoop(static_cast(mode)); 163 | } 164 | 165 | void Worker::IsLoopAlive(const FunctionCallbackInfo& args) { 166 | Worker* self = Unwrap(args); 167 | if (self == nullptr) return; 168 | args.GetReturnValue().Set(self->IsLoopAlive()); 169 | } 170 | 171 | void Worker::RunInCallbackScope(const FunctionCallbackInfo& args) { 172 | Worker* self = Unwrap(args); 173 | if (self == nullptr) return; 174 | if (!args[0]->IsFunction()) { 175 | self->isolate_->ThrowException( 176 | Exception::TypeError( 177 | String::NewFromUtf8Literal(self->isolate_, 178 | "The runInCallbackScope() argument must be a function"))); 179 | return; 180 | } 181 | Local result; 182 | if (self->RunInCallbackScope(args[0].As()).ToLocal(&result)) { 183 | args.GetReturnValue().Set(result); 184 | } 185 | } 186 | 187 | MaybeLocal Worker::RunInCallbackScope(Local fn) { 188 | if (context_.IsEmpty() || signaled_stop_) { 189 | isolate_->ThrowException(Exception::Error( 190 | String::NewFromUtf8Literal(isolate_, "Worker has been stopped"))); 191 | return MaybeLocal(); 192 | } 193 | WorkerScope worker_scope(this); 194 | CallbackScope callback_scope(isolate_, wrap_.Get(isolate_), { 1, 0 }); 195 | MaybeLocal ret = fn->Call(context(), Null(isolate_), 0, nullptr); 196 | if (signaled_stop_) { 197 | isolate_->CancelTerminateExecution(); 198 | } 199 | return worker_scope.EscapeMaybe(ret); 200 | } 201 | 202 | void Worker::Start(bool own_loop, bool own_microtaskqueue) { 203 | signaled_stop_ = false; 204 | Local outer_context = outer_context_.Get(isolate_); 205 | Environment* outer_env = GetCurrentEnvironment(outer_context); 206 | assert(outer_env != nullptr); 207 | uv_loop_t* outer_loop = GetCurrentEventLoop(isolate_); 208 | assert(outer_loop != nullptr); 209 | 210 | if (own_loop) { 211 | int ret = uv_loop_init(&loop_); 212 | if (ret != 0) { 213 | isolate_->ThrowException(UVException(isolate_, ret, "uv_loop_init")); 214 | return; 215 | } 216 | loop_.data = this; 217 | } 218 | 219 | MicrotaskQueue* microtask_queue = 220 | own_microtaskqueue ? 221 | (microtask_queue_ = v8::MicrotaskQueue::New( 222 | isolate_, v8::MicrotasksPolicy::kExplicit)).get() : 223 | outer_context_.Get(isolate_)->GetMicrotaskQueue(); 224 | uv_loop_t* loop = own_loop ? &loop_ : GetCurrentEventLoop(isolate_); 225 | 226 | Local context = Context::New( 227 | isolate_, 228 | nullptr /* extensions */, 229 | MaybeLocal() /* global_template */, 230 | MaybeLocal() /* global_value */, 231 | DeserializeInternalFieldsCallback() /* internal_fields_deserializer */, 232 | microtask_queue); 233 | context->SetSecurityToken(outer_context->GetSecurityToken()); 234 | if (context.IsEmpty() || 235 | #if NODE_MAJOR_VERSION < 17 236 | !InitializeContext(context) 237 | #else 238 | !InitializeContext(context).FromMaybe(false) 239 | #endif 240 | ) { 241 | return; 242 | } 243 | 244 | context_.Reset(isolate_, context); 245 | Context::Scope context_scope(context); 246 | isolate_data_ = CreateIsolateData( 247 | isolate_, 248 | loop, 249 | GetMultiIsolatePlatform(outer_env), 250 | GetArrayBufferAllocator(GetEnvironmentIsolateData(outer_env))); 251 | assert(isolate_data_ != nullptr); 252 | ThreadId thread_id = AllocateEnvironmentThreadId(); 253 | auto inspector_parent_handle = GetInspectorParentHandle( 254 | outer_env, thread_id, "file:///synchronous-worker.js"); 255 | env_ = CreateEnvironment( 256 | isolate_data_, 257 | context, 258 | {}, 259 | {}, 260 | static_cast( 261 | EnvironmentFlags::kTrackUnmanagedFds | 262 | EnvironmentFlags::kNoRegisterESMLoader), 263 | thread_id, 264 | std::move(inspector_parent_handle)); 265 | assert(env_ != nullptr); 266 | SetProcessExitHandler(env_, [this](Environment* env, int code) { 267 | OnExit(code); 268 | }); 269 | } 270 | 271 | void Worker::OnExit(int code) { 272 | HandleScope handle_scope(isolate_); 273 | Local self = wrap_.Get(isolate_); 274 | Local outer_context = outer_context_.Get(isolate_); 275 | Context::Scope context_scope(outer_context); 276 | Isolate::SafeForTerminationScope termination_scope(isolate_); 277 | Local onexit_v; 278 | if (!self->Get(outer_context, String::NewFromUtf8Literal(isolate_, "onexit")) 279 | .ToLocal(&onexit_v) || !onexit_v->IsFunction()) { 280 | return; 281 | } 282 | Local args[] = { Integer::New(isolate_, code) }; 283 | USE(onexit_v.As()->Call(outer_context, self, 1, args)); 284 | SignalStop(); 285 | } 286 | 287 | void Worker::SignalStop() { 288 | signaled_stop_ = true; 289 | if (env_ != nullptr && can_be_terminated_) { 290 | node::Stop(env_); 291 | } 292 | } 293 | 294 | void Worker::Stop(bool may_throw) { 295 | if (loop_.data == nullptr) { 296 | // If running in shared-event-loop mode, spin the outer event loop 297 | // until all currently pending items have been addressed, so that 298 | // FreeEnvironment() does not run the outer loop's handles. 299 | TryCatch try_catch(isolate_); 300 | try_catch.SetVerbose(true); 301 | SealHandleScope seal_handle_scope(isolate_); 302 | uv_run(GetCurrentEventLoop(isolate_), UV_RUN_NOWAIT); 303 | } 304 | if (env_ != nullptr) { 305 | if (!signaled_stop_) { 306 | SignalStop(); 307 | isolate_->CancelTerminateExecution(); 308 | } 309 | FreeEnvironment(env_); 310 | env_ = nullptr; 311 | } 312 | if (isolate_data_ != nullptr) { 313 | FreeIsolateData(isolate_data_); 314 | isolate_data_ = nullptr; 315 | } 316 | context_.Reset(); 317 | outer_context_.Reset(); 318 | if (loop_.data != nullptr) { 319 | loop_.data = nullptr; 320 | int ret = uv_loop_close(&loop_); 321 | if (ret != 0 && may_throw) { 322 | isolate_->ThrowException(UVException(isolate_, ret, "uv_loop_close")); 323 | } 324 | } 325 | microtask_queue_.reset(); 326 | 327 | RemoveEnvironmentCleanupHook(isolate_, CleanupHook, this); 328 | if (!wrap_.IsEmpty()) { 329 | HandleScope handle_scope(isolate_); 330 | wrap_.Get(isolate_)->SetAlignedPointerInInternalField(0, nullptr); 331 | } 332 | wrap_.Reset(); 333 | delete this; 334 | } 335 | 336 | MaybeLocal Worker::Load(Local callback) { 337 | if (env_ == nullptr || signaled_stop_) { 338 | isolate_->ThrowException( 339 | Exception::Error( 340 | String::NewFromUtf8Literal(isolate_, "Worker not initialized"))); 341 | return MaybeLocal(); 342 | } 343 | 344 | WorkerScope worker_scope(this); 345 | return worker_scope.EscapeMaybe( 346 | LoadEnvironment(env_, [&](const StartExecutionCallbackInfo& info) { 347 | Local argv[] = { 348 | info.process_object, 349 | info.native_require, 350 | context()->Global() 351 | }; 352 | return callback->Call(context(), Null(isolate_), 3, argv); 353 | })); 354 | }; 355 | 356 | void Worker::CleanupHook(void* arg) { 357 | static_cast(arg)->Stop(false); 358 | } 359 | 360 | void Worker::RunLoop(uv_run_mode mode) { 361 | if (loop_.data == nullptr || context_.IsEmpty() || signaled_stop_) { 362 | isolate_->ThrowException(Exception::Error( 363 | String::NewFromUtf8Literal(isolate_, "Worker has been stopped"))); 364 | return; 365 | } 366 | if (loop_is_running_) { 367 | isolate_->ThrowException(Exception::Error( 368 | String::NewFromUtf8Literal(isolate_, "Cannot nest calls to runLoop"))); 369 | return; 370 | } 371 | WorkerScope worker_scope(this); 372 | TryCatch try_catch(isolate_); 373 | try_catch.SetVerbose(true); 374 | SealHandleScope seal_handle_scope(isolate_); 375 | loop_is_running_ = true; 376 | uv_run(&loop_, mode); 377 | loop_is_running_ = false; 378 | if (signaled_stop_) { 379 | isolate_->CancelTerminateExecution(); 380 | } 381 | } 382 | 383 | bool Worker::IsLoopAlive() { 384 | if (loop_.data == nullptr || signaled_stop_) return false; 385 | return uv_loop_alive(&loop_); 386 | } 387 | 388 | NODE_MODULE_INIT() { 389 | Isolate* isolate = context->GetIsolate(); 390 | Local templ = FunctionTemplate::New(isolate, Worker::New); 391 | templ->SetClassName(String::NewFromUtf8Literal(isolate, "SynchronousWorker")); 392 | templ->InstanceTemplate()->SetInternalFieldCount(1); 393 | Local proto = templ->PrototypeTemplate(); 394 | 395 | Local s = Signature::New(isolate, templ); 396 | proto->Set(String::NewFromUtf8Literal(isolate, "start"), 397 | FunctionTemplate::New(isolate, Worker::Start, {}, s)); 398 | proto->Set(String::NewFromUtf8Literal(isolate, "load"), 399 | FunctionTemplate::New(isolate, Worker::Load, {}, s)); 400 | proto->Set(String::NewFromUtf8Literal(isolate, "stop"), 401 | FunctionTemplate::New(isolate, Worker::Stop, {}, s)); 402 | proto->Set(String::NewFromUtf8Literal(isolate, "signalStop"), 403 | FunctionTemplate::New(isolate, Worker::SignalStop, {}, s)); 404 | proto->Set(String::NewFromUtf8Literal(isolate, "runLoop"), 405 | FunctionTemplate::New(isolate, Worker::RunLoop, {}, s)); 406 | proto->Set(String::NewFromUtf8Literal(isolate, "isLoopAlive"), 407 | FunctionTemplate::New(isolate, Worker::IsLoopAlive, {}, s)); 408 | proto->Set(String::NewFromUtf8Literal(isolate, "runInCallbackScope"), 409 | FunctionTemplate::New(isolate, Worker::RunInCallbackScope, {}, s)); 410 | 411 | Local worker_fn; 412 | if (!templ->GetFunction(context).ToLocal(&worker_fn)) 413 | return; 414 | USE(exports->Set(context, 415 | String::NewFromUtf8Literal(isolate, "SynchronousWorkerImpl"), 416 | worker_fn)); 417 | 418 | NODE_DEFINE_CONSTANT(exports, UV_RUN_DEFAULT); 419 | NODE_DEFINE_CONSTANT(exports, UV_RUN_ONCE); 420 | NODE_DEFINE_CONSTANT(exports, UV_RUN_NOWAIT); 421 | } 422 | 423 | } 424 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import bindings from 'bindings'; 2 | import { EventEmitter } from 'events'; 3 | import type Module from 'module'; 4 | const { 5 | SynchronousWorkerImpl, 6 | UV_RUN_DEFAULT, 7 | UV_RUN_ONCE, 8 | UV_RUN_NOWAIT 9 | } = bindings('synchronous_worker'); 10 | const kHandle = Symbol('kHandle'); 11 | const kProcess = Symbol('kProcess'); 12 | const kModule = Symbol('kModule'); 13 | const kGlobalThis = Symbol('kGlobalThis'); 14 | const kHasOwnEventLoop = Symbol('kHasOwnEventLoop'); 15 | const kHasOwnMicrotaskQueue = Symbol('kHasOwnMicrotaskQueue'); 16 | const kPromiseInspector = Symbol('kPromiseInspector'); 17 | const kStoppedPromise = Symbol('kStoppedPromise'); 18 | 19 | interface Options { 20 | sharedEventLoop: boolean; 21 | sharedMicrotaskQueue: boolean; 22 | } 23 | 24 | type InspectedPromise = { 25 | state: 'pending'; 26 | value: null; 27 | } | { 28 | state: 'fulfilled'; 29 | value: T; 30 | } | { 31 | state: 'rejected'; 32 | value: Error; 33 | }; 34 | 35 | class SynchronousWorker extends EventEmitter { 36 | [kHandle]: any; 37 | [kProcess]: NodeJS.Process; 38 | [kGlobalThis]: any; 39 | [kModule]: typeof Module; 40 | [kHasOwnEventLoop]: boolean; 41 | [kHasOwnMicrotaskQueue]: boolean; 42 | [kPromiseInspector]: (promise: Promise) => InspectedPromise; 43 | [kStoppedPromise]: Promise; 44 | 45 | constructor(options?: Partial) { 46 | super(); 47 | this[kHasOwnEventLoop] = !(options?.sharedEventLoop); 48 | this[kHasOwnMicrotaskQueue] = !(options?.sharedMicrotaskQueue); 49 | 50 | this[kHandle] = new SynchronousWorkerImpl(); 51 | this[kHandle].onexit = (code) => { 52 | this.stop(); 53 | this.emit('exit', code); 54 | }; 55 | try { 56 | this[kHandle].start(this[kHasOwnEventLoop], this[kHasOwnMicrotaskQueue]); 57 | this[kHandle].load((process, nativeRequire, globalThis) => { 58 | const origExit = process.reallyExit; 59 | process.reallyExit = (...args) => { 60 | const ret = origExit.call(process, ...args); 61 | // Make a dummy call to make sure the termination exception is 62 | // propagated. For some reason, this isn't necessarily the case 63 | // otherwise. 64 | process.memoryUsage(); 65 | return ret; 66 | }; 67 | this[kProcess] = process; 68 | this[kModule] = nativeRequire('module'); 69 | this[kGlobalThis] = globalThis; 70 | process.on('uncaughtException', (err) => { 71 | if (process.listenerCount('uncaughtException') === 1) { 72 | this.emit('error', err); 73 | process.exit(1); 74 | } 75 | }); 76 | }); 77 | } catch (err) { 78 | this[kHandle].stop(); 79 | throw err; 80 | } 81 | } 82 | 83 | runLoop(mode: 'default' | 'once' | 'nowait' = 'default'): void { 84 | if (!this[kHasOwnEventLoop]) { 85 | throw new Error('Can only use .runLoop() when using a separate event loop'); 86 | } 87 | let uvMode = UV_RUN_DEFAULT; 88 | if (mode === 'once') uvMode = UV_RUN_ONCE; 89 | if (mode === 'nowait') uvMode = UV_RUN_NOWAIT; 90 | this[kHandle].runLoop(uvMode); 91 | } 92 | 93 | runLoopUntilPromiseResolved(promise: Promise): T { 94 | if (!this[kHasOwnEventLoop] || !this[kHasOwnMicrotaskQueue]) { 95 | throw new Error( 96 | 'Can only use .runLoopUntilPromiseResolved() when using a separate event loop and microtask queue'); 97 | } 98 | this[kPromiseInspector] ??= this.createRequire(__filename)('vm').runInThisContext( 99 | `(promise => { 100 | const obj = { state: 'pending', value: null }; 101 | promise.then((v) => { obj.state = 'fulfilled'; obj.value = v; }, 102 | (v) => { obj.state = 'rejected'; obj.value = v; }); 103 | return obj; 104 | })`); 105 | const inspected = this[kPromiseInspector](promise); 106 | this.runInWorkerScope(() => {}); // Flush the µtask queue 107 | while (inspected.state === 'pending') { 108 | this.runLoop('once'); 109 | } 110 | if (inspected.state === 'rejected') { 111 | throw inspected.value; 112 | } 113 | return inspected.value; 114 | } 115 | 116 | get loopAlive(): boolean { 117 | if (!this[kHasOwnEventLoop]) { 118 | throw new Error('Can only use .loopAlive when using a separate event loop'); 119 | } 120 | return this[kHandle].isLoopAlive(); 121 | } 122 | 123 | async stop(): Promise { 124 | return this[kStoppedPromise] ??= new Promise(resolve => { 125 | this[kHandle].signalStop(); 126 | setImmediate(() => { 127 | this[kHandle].stop(); 128 | resolve(); 129 | }); 130 | }); 131 | } 132 | 133 | get process(): NodeJS.Process { 134 | return this[kProcess]; 135 | } 136 | 137 | get globalThis(): any { 138 | return this[kGlobalThis]; 139 | } 140 | 141 | createRequire(...args: Parameters): NodeJS.Require { 142 | return this[kModule].createRequire(...args); 143 | } 144 | 145 | runInWorkerScope(method: () => any): any { 146 | return this[kHandle].runInCallbackScope(method); 147 | } 148 | } 149 | 150 | export = SynchronousWorker; 151 | -------------------------------------------------------------------------------- /test/index.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import SynchronousWorker from '../'; 3 | 4 | describe('SynchronousWorker allows running Node.js code', () => { 5 | it('inside its own event loop', () => { 6 | const w = new SynchronousWorker(); 7 | w.runInWorkerScope(() => { 8 | const req = w.createRequire(__filename); 9 | const fetch = req('node-fetch'); 10 | const httpServer = req('http').createServer((req, res) => { 11 | if (req.url === '/stop') { 12 | w.stop(); 13 | } 14 | res.writeHead(200); 15 | res.end('Ok\n'); 16 | }); 17 | httpServer.listen(0, req('vm').runInThisContext(`({fetch, httpServer }) => (async () => { 18 | const res = await fetch('http://localhost:' + httpServer.address().port + '/'); 19 | globalThis.responseText = await res.text(); 20 | await fetch('http://localhost:' + httpServer.address().port + '/stop'); 21 | })`)({ fetch, httpServer })); 22 | }); 23 | 24 | assert.strictEqual(w.loopAlive, true); 25 | w.runLoop('default'); 26 | assert.strictEqual(w.loopAlive, false); 27 | 28 | assert.strictEqual(w.globalThis.responseText, 'Ok\n'); 29 | }); 30 | 31 | it('with its own µtask queue', () => { 32 | const w = new SynchronousWorker({ sharedEventLoop: true }); 33 | let ran = false; 34 | w.runInWorkerScope(() => { 35 | w.globalThis.queueMicrotask(() => ran = true); 36 | }); 37 | assert.strictEqual(ran, true); 38 | }); 39 | 40 | it('with its own µtask queue but shared event loop', (done) => { 41 | const w = new SynchronousWorker({ sharedEventLoop: true }); 42 | let ran = false; 43 | w.runInWorkerScope(() => { 44 | w.globalThis.setImmediate(() => { 45 | w.globalThis.queueMicrotask(() => ran = true); 46 | }); 47 | }); 48 | assert.strictEqual(ran, false); 49 | setImmediate(() => { 50 | assert.strictEqual(ran, true); 51 | done(); 52 | }); 53 | }); 54 | 55 | it('with its own loop but shared µtask queue', () => { 56 | const w = new SynchronousWorker({ sharedMicrotaskQueue: true }); 57 | let ran = false; 58 | w.runInWorkerScope(() => { 59 | w.globalThis.setImmediate(() => { 60 | w.globalThis.queueMicrotask(() => ran = true); 61 | }); 62 | }); 63 | 64 | assert.strictEqual(ran, false); 65 | w.runLoop('default'); 66 | assert.strictEqual(ran, true); 67 | }); 68 | 69 | it('with its own loop but shared µtask queue (no callback scope)', (done) => { 70 | const w = new SynchronousWorker({ sharedMicrotaskQueue: true }); 71 | let ran = false; 72 | w.globalThis.queueMicrotask(() => ran = true); 73 | assert.strictEqual(ran, false); 74 | queueMicrotask(() => { 75 | assert.strictEqual(ran, true); 76 | done(); 77 | }); 78 | }); 79 | 80 | it('allows waiting for a specific promise to be resolved', () => { 81 | const w = new SynchronousWorker(); 82 | const req = w.createRequire(__filename); 83 | let srv; 84 | let serverUpPromise; 85 | let fetchPromise; 86 | w.runInWorkerScope(() => { 87 | srv = req('http').createServer((req, res) => res.end('contents')).listen(0); 88 | serverUpPromise = req('events').once(srv, 'listening'); 89 | }); 90 | w.runLoopUntilPromiseResolved(serverUpPromise); 91 | w.runInWorkerScope(() => { 92 | fetchPromise = req('node-fetch')('http://localhost:' + srv.address().port); 93 | }); 94 | const fetched = w.runLoopUntilPromiseResolved(fetchPromise) as any; 95 | assert.strictEqual(fetched.ok, true); 96 | assert.strictEqual(fetched.status, 200); 97 | }); 98 | 99 | context('process.exit', () => { 100 | it('interrupts runInWorkerScope', () => { 101 | const w = new SynchronousWorker(); 102 | let ranBefore = false; 103 | let ranAfter = false; 104 | let observedCode = -1; 105 | w.on('exit', (code) => observedCode = code); 106 | w.runInWorkerScope(() => { 107 | ranBefore = true; 108 | w.process.exit(1); 109 | ranAfter = true; 110 | }); 111 | assert.strictEqual(ranBefore, true); 112 | assert.strictEqual(ranAfter, false); 113 | assert.strictEqual(observedCode, 1); 114 | }); 115 | 116 | it('interrupts runLoop', () => { 117 | const w = new SynchronousWorker(); 118 | let ranBefore = false; 119 | let ranAfter = false; 120 | let observedCode = -1; 121 | w.on('exit', (code) => observedCode = code); 122 | w.runInWorkerScope(() => { 123 | w.globalThis.setImmediate(() => { 124 | ranBefore = true; 125 | w.process.exit(1); 126 | ranAfter = true; 127 | }); 128 | }); 129 | w.runLoop('default'); 130 | assert.strictEqual(ranBefore, true); 131 | assert.strictEqual(ranAfter, false); 132 | assert.strictEqual(observedCode, 1); 133 | }); 134 | 135 | it('does not kill the process outside of any scopes', () => { 136 | const w = new SynchronousWorker(); 137 | let observedCode = -1; 138 | 139 | w.on('exit', (code) => observedCode = code); 140 | w.process.exit(1); 141 | 142 | assert.strictEqual(observedCode, 1); 143 | 144 | assert.throws(() => { 145 | w.runLoop('default'); 146 | }, /Worker has been stopped/); 147 | }); 148 | }); 149 | 150 | it('allows adding uncaught exception listeners', () => { 151 | const w = new SynchronousWorker(); 152 | let uncaughtException; 153 | let erroredOrExited = false; 154 | w.on('exit', () => erroredOrExited = true); 155 | w.on('errored', () => erroredOrExited = true); 156 | w.process.on('uncaughtException', err => uncaughtException = err); 157 | w.globalThis.setImmediate(() => { throw new Error('foobar'); }); 158 | w.runLoop('default'); 159 | assert.strictEqual(erroredOrExited, false); 160 | assert.strictEqual(uncaughtException.message, 'foobar'); 161 | }); 162 | 163 | it('handles entirely uncaught exceptions inside the loop well', () => { 164 | const w = new SynchronousWorker(); 165 | let observedCode; 166 | let observedError; 167 | w.on('exit', code => observedCode = code); 168 | w.on('error', error => observedError = error); 169 | w.globalThis.setImmediate(() => { throw new Error('foobar'); }); 170 | w.runLoop('default'); 171 | assert.strictEqual(observedCode, 1); 172 | assert.strictEqual(observedError.message, 'foobar'); 173 | }); 174 | 175 | it('forbids nesting .runLoop() calls', () => { 176 | const w = new SynchronousWorker(); 177 | let uncaughtException; 178 | w.process.on('uncaughtException', err => uncaughtException = err); 179 | w.globalThis.setImmediate(() => w.runLoop('default')); 180 | w.runLoop('default'); 181 | assert.strictEqual(uncaughtException.message, 'Cannot nest calls to runLoop'); 182 | }); 183 | 184 | it('properly handles timers that are about to expire when FreeEnvironment() is called on a shared event loop', async() => { 185 | const w = new SynchronousWorker({ 186 | sharedEventLoop: true, 187 | sharedMicrotaskQueue: true 188 | }); 189 | 190 | setImmediate(() => { 191 | setTimeout(() => {}, 20); 192 | const now = Date.now(); 193 | while (Date.now() - now < 30); 194 | }); 195 | await w.stop(); 196 | }); 197 | }); 198 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "downlevelIteration": true, 5 | "sourceMap": true, 6 | "strictNullChecks": false, 7 | "declaration": true, 8 | "removeComments": true, 9 | "target": "es2019", 10 | "lib": ["es2019"], 11 | "outDir": "./lib", 12 | "moduleResolution": "node", 13 | "module": "commonjs" 14 | }, 15 | "include": [ 16 | "./src/**/*" 17 | ], 18 | "exclude": [ 19 | "./src/**/*.spec.*" 20 | ] 21 | } 22 | --------------------------------------------------------------------------------