Skip to main content
Deno 2 is finally here 🎉️
Learn more

NOT READY FOR PRODUCTION - EXPECT ANY API CHANGE

CHANGE HISTORY

Intro

At the highest level of its design, Concurrent.js is a module loader like require and import. But instead of loading a module into the main thread, it loads the module into workers. Every instance of an imported class and every invocation of an imported function would be run on an individual worker if available (or share a worker with other operations if it’s not). Concurrent.js provides a simple “write less, do more” isomorphic API that works on Node.js, Deno (under development), and browsers.

This library would be eventually used to build the Blend RTE that would support concurrency without the hassle of dealing with the Concurrent.js API.

Important notes

  • This is a helper library not a new implementation of Web Workers.
  • This is an early version of the library and must not be used in a real project.

Features

  • Supporting Node.js
  • Supporting browser
  • Supporting Deno
  • Executing JavaScript (ECMAScript & CommonJS)
  • Executing TypeScript
  • Executing WebAssembly
  • Accessing exported functions
  • Accessing exported classes
    • Instantiation
    • Instance members
      • Methods
      • Getters and setters
      • Fields
    • Static members
      • Methods
      • Getters and setters
      • Fields
  • Reactive concurrency
  • Inter-worker communication
    • Event sourcing
    • Data sharing
    • Dependency injection
  • Sandboxing
  • Language interoperability (Server-side)
    • C
    • Rust
    • Python

Technical facts

  • Concurrent.js has no runtime dependency.
  • Built with Node.js and uses the ECMAScript module system.
  • Built upon web workers (a.k.a. worker threads).
  • Alters nothing, no native nor programmer-defined.
  • Written in TypeScript with the strictest ESNext config.
  • Packaged as platform-specific bundles that target ES2020.
  • Except for a module path that has to be a string other things are strongly typed and any mistake would be detected by using TypeScript.

Install

npm i @bitair/concurrent.js@latest

Usage

import concurrent from '@bitair/concurrent.js'
const { factorial } = await concurrent.load('extra-bigint')
const result = await factorial(50n) // Noticed the await operator?
console.log(result)
await concurrent.terminate()

Save and run the hello world script to see it in action:

bash hello_world.sh

Client-side example

.
├── src
    ├── services
        ├── index.js
    ├── app.js
    ├── worker_script.js
    .
├── static
    ├── index.html
.
worker_script.js
// This is a single-line module that it's code will be generated by the bundler
import '@bitair/concurrent.js/worker_script'
services/index.js
import { factorial } from 'extra-bigint'
export { factorial }
app.js
import concurrent from '@bitair/concurrent.js'
const { factorial } = await concurrent.load(new URL('./services/index.js', import.meta.url))
console.log(await factorial(50_000n))
index.html
<!DOCTYPE html>
<html>
  <body>
    <script type="module" src="scripts/main.js"></script>
  </body>
</html>

Bundling

A client-side app should consist of the following bundles:

  1. App code
  2. Concurrent modules (can be multiple bundles)
  3. worker_script.js
build.sh
#!/bin/bash
npx esbuild src/app.js --bundle --format=esm --platform=browser --target=esnext --outfile=static/scripts/main.js
npx esbuild src/worker_script.js --bundle --format=esm --platform=browser --target=esnext --outfile=static/scripts/worker_script.js
npx esbuild src/services/index.js --bundle --format=esm --platform=browser --target=esnext --outfile=static/scripts/services/index.js
bash ./build.sh && npx http-server static

Base URL

Concurrent.js uses the import.meta.url property as the base URL to resolve the scripts. A custom URL can be provided as well. It’s usefule in case of targeting an old ECMAScript version for bundling:

npx esbuild src/app.js --target=es6 --define:process.env.BASE_URL=\"http://127.0.0.1:8080/scripts/\" --bundle --format=esm --platform=browser --outfile=static/scripts/main.js

API

concurrent.load<T>(src: string | URL, settings: ExecutionSettings) : T

Loads a module into a worker when a class of the module is instantiated or a function of the module is invoked.

  • src: string

    The path or URL of the loading module. The value of this parameter must be either an absolute path/URL or a package name. The package must be already installed and accessible to the module.

  • settings: ExecutionSettings

    • settings.parallel [default=false]

      Setting it would cause Concurrent.js to allocate a dedicated thread to every instance of an imported class and also to every invocation of an imported function. That would guarantee a parallel execution.

    • settings.timeout [default=Infinity] [Not implemented]

      Setting it would prevent a parallel operation to occupy a thread forever. By reaching the timeout Concurrent.js would terminate and re-instantiate the thread and also would throw a timeout exception. The setting can’t be used when the parallel flag is off. This is because a non-parallel operation would share a worker with other operations and that’s not a reasonable practice to terminate all other operations when one of them hangs. There would be a solution for this by implementing reactive concurrency though.

concurrent.config(settings: ConcurrencySettings): void

Configs the global settings of Concurrent.js

  • settings: ConcurrencySettings

    • settings.disabled: boolean [default=false]

      Setting it would disable Concurrent.js without the requirement to change any other code.

    • settings.maxThreads: number [default=1]

      The max number of available threads. For achieving parallelism its value must be less than or equal to the total available processors. In Node.js v19.4.0 or later the number can be extracted from os.availableParallelism.

    • settings.threadAllocationTimeout: number | typeof Infinity [default=Infinity]

      Number of seconds that an operation would be waiting for a thread allocation. By reaching the timeout an exception would be thrown.

    • settings.threadIdleTimeout: number | typeof Infinity [default=Infinity]

      Number of minutes that Concurrent.js would be waiting before terminating an idle thread.

    • settings.minThreads: number [default=0]

      The minimum number of threads that should be terminated even if they are idle.

concurrent.terminate(force?: boolean): Promise<void>

Terminates Concurrent.js

  • force?: boolean [Not implemented]

    Forces Concurrent.js to exit immidiatly without waiting for workers to finish their tasks.

License

MIT License