JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 32
  • Score
    100M100P100Q61729F
  • License MIT

Concurrent Computing for JavaScript

Package Exports

  • @bitair/concurrent.js
  • @bitair/concurrent.js/worker_script

Readme

NOT READY FOR PRODUCTION

Intro

Concurrent.js helps with concurrent computation in JavaScript runtime environments. It can run CPU-intensive operations without blocking the main thread (i.e. the event loop or a browser tab/window). Concurrent.js is designed to load a JavaScript module with its dependencies into workers and make the module's exports accessible as usual. It provides a "write less, do more" isomorphic API that works on Node.js, Deno, and web browsers.

Features

  • Loading CommonJS modules into workers
  • Loading ECMAScript modules
  • Accessing exported functions
  • Accessing exported classes
    • Instantiation
    • Instance members
      • Methods
      • Getters and setters
      • Fields
    • Static members
      • Methods
      • Getters and setters
      • Fields
  • Loading WebAssembly
  • Supporting Node.js
  • Supporting web browsers
  • Supporting Deno
  • Reactive multithreading
  • Sandboxing

Install

npm i -S @bitair/concurrent.js

Hello World!

Let's assume that we have a math module that potentially can block the main thread:

// math.js
export class Arithmetic {
  isPrime(n) {
    return isPrime(n)
  }

  // ...other methods
}

function isPrime(n) {
    const j = Math.sqrt(n)
    for (let i = 2; i <= j; i++) {
      if (n % i === 0) return false
    }
    return n > 1
}
// ...other exported classes and functions

In order to run it without blocking the main thread:

  1. Use the load method instead of require or import functions:

    const { Arithmetic } = await concurrent.load(MATH_MODULE_SRC) // MATH_MODULE_SRC must be an absolute path or URL
  2. Instantiate any of the exported classes in the usual way and invoke their methods (must be done asynchronously, e.g. using the await operator). Instances must be explicitly deleted using the dispose method:

    const arithmetic = new Arithmetic()
    const result = await arithmetic.isPrime(123456789)
    // ...rest of the code
    await concurrent.dispose(arithmetic)
  3. Import and invoke exported functions (must be done asynchronously (e.g. using the await operator) :

    const { isPrime } = await concurrent.load(MATH_MODULE_SRC)
    const result = await isPrime(123456789)
    // ...rest of the code
  4. Terminate Concurrent.js before exiting the app:

    await concurrent.terminate()

Client-side usage

A client-side app should consist of three scripts/bundles:

  1. One that contains the app's code

  2. Another one that contains the concurrent modules' code

  3. A worker_script.js bundle that must be generated from the following code:

    import '@bitair/concurrent.js/worker_script'

Please see the sample projects for a complete code. In order to run a sample project:

cd SAMPLE_DIR
npm i
npm run build
npm start

Testing usage

Using Concurrent.js in a test environment would slow down the testing process since instantiating the workers would take a couple of seconds. So it's recommended to disable it when testing your app. The rest of the code doesn't need to be changed. When Concurrent.js is disabled the load method would revert to the dynamic import function and other methods would do nothing.

//test/index.js
import concurrent from '@bitair/concurrent.js'

concurrent.config({
  disabled: true
})

// ...rest of the code

You may also move this code to the app's main module and for example activate Concurrent.js only in production environment:

//index.js
import concurrent from '@bitair/concurrent.js'

concurrent.config({
  disabled: process.env.NAME !== 'PROD'
})

// ...rest of the code

Parallelism

In order to achieve parallelism the load method accepts a flag named isolated that tells Concurrent.js to allocate an individual worker to every instance of an object and also to every invocation of a function (here function indicates an exported function not a method of an instance). Concurrent.js has also a config method that can be used to set the number of max spawning threads. In parallelism, the number must be less than or equal to the total available CPU cores. In Node.js v19.4.0 or later the number can be extracted from os.availableParallelism:

import concurrent from '@bitair/concurrent.js'

concurrent.config({
  maxThreads: 16 // Use os.availableParallelism() if your Node.js version is above v19.4.0.
})

const { Arithmetic } = await concurrent.load(new URL('./math.js', import.meta.url), {
  isolated: true
})

const ops = []
for (let i = 0; i <= 20; i++) {
  const arithmetic = new Arithmetic()
  ops.push(
    Promise.resolve(arithmetic.isPrime(i)).then(async result => {
      await dispose(arithmetic) // The 17th operation will be waiting for a worker to be released, so removing this line would freeze the app.
      return result
    })
  )
}

const results = await Promise.all(ops)
// ...rest of the code

API

IN PROGRESS

Notes

  • All concurrent methods and functions must be called asynchronously.
  • The data that would be sent to or received from concurrent instances/functions must be supported by the structured clone algorithm .
  • Workers can't run TypeScript. So in case of using tools like ts-node the load method would perform a normal import using the dynamic import function.
  • All unhandled exceptions would be bubbled up to the main thread.
  • Every function call and object instantiation would be run on an individual worker if available or share a worker if the isolated flag is not enabled.
  • When using Concurrent.js on the client side the app and its concurrent modules should be bundled separately in order to not load the entire app inside a worker.
  • Don't use Concurrent.js for IO-intensive tasks as the Node.js official document suggests: "Workers (threads) are useful for performing CPU-intensive JavaScript operations. They do not help much with I/O-intensive work. The Node.js built-in asynchronous I/O operations are more efficient than Workers can be."

Terminology

  • Multitasking means running multiple processes side by side and ability to switch between them. It requires one thread.
  • Concurrency means running multiple processes on multiple threads. It requires at least two threads. Concurrency is a performant form of multitasking.
  • Parallelism means running multiple processes on multiple CPUs. It requires at least two CPUs. Parallelism is a performant form of concurrency.

Although this library is able to perform parallel computing when the host machine has multiple CPUs the name of the library isn't Parallel.js because it's impossible to back the name in the case of a host machine that has only one CPU.

License

MIT License