fachman

0.1.0 • Public • Published

Work in progress

This readme, just like the repo itself, is still very much a work in progress.

Most of the basics now work (creation of browser workers an node childprocesses, the proxy for accessing methods in worker, calling them and getting promise, support for various module formats, etc...). Some tests regarding minor things are still failing (worker.idle does not show change when worker has running tasks), but the module should now work. As a technical preview at the least.

Also more text should be added to readme and docs.

Fachman

Writing multithreaded code has never been easier.

Fachman means hard working expert in Czech language. And it sounds kinda cool.

Wat?

offers Promise based solution for calling code in, and retrieving data from separate worker thread. Works across many platforms and can be used in both the browsers with native and Nodejs. Thanks to Proxy the API mimics shape of your worker script and let's you call all the worker functions from main thread as if they were defined in main thread.

Spawns child processes in Node.js and creates Worker threads in browsers.

It started off as a promise returning request-response wrapper over worker.postMessage()/self.addEventListener('message') for calling worker functions from the main thread. Availability of the new Proxystandard then allowed a nicer way of calling those methods with automatically created disposable proxy objects. Need for load balancer lead to creation of Cluster class that distributes tasks among available threads. And finally a desire to reuse code and an elegance of Node's style of coding drove the implementation of Node equivalent of WebWorkers and bringing some of its traits (like EventEmitter styled eventing) back to the browser land.

Installation

npm install fachman

Usage

index.mjs Imports tree of Node flavored *.mjs files written with ES Modules. Ideal for use in Node 8.5 (with flag).

index.js UMD bundle for simple drop-in use in browsers and older Node versions.

TODO - importing scripts in browser using <script> or require() in the main thread and with importScripts() or require() inside worker.

TODO
TODO

Features

Promise based tasks

Each function call returns Promise because of the asynchronous nature of working with separate threads. Doesn't matter wheter your worker functions are synchronous (return actual result) or asynchronous (return Promise object which will then resolve the value), main thread will always receive Promise resolving the result.

That being said. async/await is now natively widely availabe so you can hop onto the unicorn and ride into the sunset.

var pw = new ProxyWorker('worker.js')
 
// classic promise.then()
pw.proxy.cpuIntenseFunction(...)
    .then(result => console.log(result)
 
// async/await variation
var result = await pw.proxy.cpuIntenseFunction(...)

Note: Don't forget to wrap await calls in async function() {...}

EventEmitter style .on() and .emit() on both ends

Fachman sets up EventEmitter interface on the outside (instance of ProxyWorker or Cluster) and inside of the worker (self object) and links them together. Emitting events on one of these sides replicates it on the end as well.

// master.js
var pw = new ProxyWorker('worker.js')
// notify worker about changed size of the window
window.addEventListener('resize', e => pw.emit('window-resized', innerWidth, innerHeight))
// listen for greetings from worker
pw.on('greeting', () => console.log('worker says hi'))
// worker.js
var windowWidth
var windowHeight
// list on any change in size of the window and adjust local variables
self.on('window-resized', (innerWidth, innerHeight) => {
    windowWidth = innerWidth
    windowHeight = innerHeight
})
// say hi to the master thread
self.emit('greeting')
 

Better yet. Emitting event on cluster trickles it down to all workers within the cluster.

// master.js
var cluster = new Cluster('worker.js')
// send 'window-resized' to all workers within cluster
window.addEventListener('resize', e => cluster.emit('window-resized', innerWidth, innerHeight))

Packed with tons of sugar.

Proxies are being dynamically generated as you type so that worker.proxy.deeply.nested.method(...) does not throw error, but resolves into call of deeply.nested.method(...) in your worker script and propagates result back to your main thread call.

// master.js
var pw = new ProxyWorker('worker.js')
pw.proxy.deeply.nested.method([1,10,100])
    .then(result => console.log(result === 11))
// worker.js
var deeply = {
    nested: {
        method: arg => arg[0] + arg[1]
    }
}

Note: You can avoid using proxies and instead call worker.invokeTask(...)

var promise = worker.invokeTask({
    path: 'deeply.nested.method',
    args: [1,10,100]
})

Batteries included.

Autotransfer of memory between Workers.

WebWorkers in the browsers allow one-time unidirectional sharing of chunks of memory from master to worker or from worker to master. That prevents costly cloning of the memory. But the developers have to explicitly define which objects to transfer through second parameter transferables in webworker.postMessage({img: imgBuffer}, [imgBuffer]). Since we're building on top of this API and hiding it away from you, fachman automatically tries to find transferables in your data and tells browser to transfer them over to worker.

We believe this fits majority of usecases, but you can always opt out of this by setting autoTransferArgs option to false

More about transferables on MDN

In the following example, you can see main thread fetching an image and then transfering in back and forth between worker which does some CPU intensive operations with the

var response = await fetch('myImage.jpg')
// Fetches an image as a #1 buffer 
var arrayBuffer = await response.arrayBuffer()
// Transfers original #1 buffer to worker and receives a new #2 buffer that was created inside worker and transfered from worker to the main thread.
arrayBuffer = await worker.proxy.grayscale(arrayBuffer)
// Transfers #2 buffer to worker and receives #3 buffer.
arrayBuffer = await worker.proxy.sharpen(arrayBuffer, 1.8)
// Transfers #3 buffer to worker and receives #4 buffer.
if (resize)
    arrayBuffer = await worker.proxy.resizeImage(arrayBuffer, 300, 200)
// Transfers #2 buffer to worker and receives #5 buffer.
if (rotate)
    arrayBuffer = await worker.proxy.rotateImage(arrayBuffer, 'right')
await fetch({
    method: 'POST',
    body: arrayBuffer
})
console.log('image has been successfully downloaded, modified and reuploaded')

To take this example even further, the ArrayBufer could be transformed into SharedArrayBuffer and operations that do not change the length of the buffer (grayscale and sharpen in this example) could edit the data directly inside the very same buffer (the same place in memory) they receive.

...
var arrayBuffer = await response.arrayBuffer()
// Copy #1 ArrayBuffer's data into new #2 SharedArrayBuffer that can reside in both threads.
var sharedArrayBuffer = sharedArrayBufferFromArrayBuffer(arrayBuffer)
// Pass refference to #2 SAB buffer to the worker and let it modify the memory of the SAB
await worker.proxy.grayscale(sharedArrayBuffer)
// Pass refference to #2 SAB buffer to the worker and let it modify the memory of the SAB
await worker.proxy.sharpen(sharedArrayBuffer, 1.8)
// sharedArrayBuffer is still the same object pointing to the same space in memory that has been changed twice.
...

worker scope

self.on()
self.emit()

TO BE ELABORATED

WebWorker shim for Node.js

Node does not support WebWorker API and up until recently it was difficult to write multi threaded code (now you can at least use the new cluster module, but that's not perfect either).

TO BE ELABORATED

// ES modules style
import {Worker} from 'fachman'
// CJS style
var Worker = require('fachman').Worker

EventEmitter shim

Like any good Node or Node-like API, fachman's classes inherit from EventEmitter class. But EventEmitter is not available in browser and bundling the original would add another 8kb (minimized).

Familiar API

Fachman mimics APIs, events and general shape of both Worker class from WebWorkers spec available in browser, as well as Node's cluster module.

TO BE ELABORATED

API

ProxyWorker class

TO BE ELABORATED

Constructor

new ProxyWorker(workerPath, [workerOptions]) creates worker object,

Properties

proxy Proxy

ready Promise

worker Worker

online

running

idle

Methods

invokeTask

terminate

close

destroy

Events

online Worker successfuly started and is now fully operational.

exit(code) Thread was closed. Argument code describes how. 0 means it was closed gracefully by user. Any other code means error.

idle No task is currently running on this thread.

running Task is being executed on this thread.

task-start(task) Internal event signalling that task has been sent to worker to execute it.

task-end(task) Worker has finished processing task and has returned result or error.

Cluster class

TO BE ELABORATED

Constructor

new Cluster([workerPath], workerOptions)

new Cluster(workerPath, [threads])

Properties

proxy Proxy

ready Promise

running

Methods

Events

workerOptions object

workerPath optional if workerPath is specified other way.

Path to worker file to be created.

threads optional Number of threads to be created by Cluster.

Defaults to number of CPU cores (or threads in case of Intel CPU's with hyperthreading).

canEnqueueTasks optional

startupDelay optional

workerStartupDelay optional

autoTransferArgs optional

Caveats

Dependencies

While Fachman has no direct 3rd party dependency, it uses EventEmitter class from Node events module (aside from other core Node modules in Node portion of the code to shim/polyfill WebWorkers). To keep the filesize as little as possible it is not bundled with Fachmann. Instead, tiny shim of EventEmitter is. But events module will always be preffered and used if it's available.

UMD bundles, where U stands for Universal, can be used in any environments and module loaders. In node, dependencies will be require()'d, but in browsers they will be looked up in window or self object. It is generally a good idea to not have these global objects polluted with properties of names of Node modules like the afore mentioned events while containing something entirely else.

e.g. don't do var events = ['something', 'my', 'app', 'does'] because it will end up as a window.events and might cause problems. However having custom (UMD) bundle of events module, that creates window.events.EventEmitter is nice (but you don't have to).

Other Node core modules used (but not required in browser) events, child_process, net, os.

Using the same event names

Fachman internally uses a few events on the ProxyWorker and Cluster instances to pass information down to worker's self object and to overall control the lifecycle of a worker. For example exit, online and few others that can be seen in APIs.

These EventEmitters are available to you for your app's needs, but please avoid using the same event names that fachman uses.

Accessing global properties in worker

Variables and functions defined outside of any block are automatically assigned to window or self objects. That makes it easy for fachman to locate and call these functions.

ES class declaractions don't create properties on the global object and are thus inaccessible without explicitly exposing them.

// master.js
var pw = new ProxyWorker('worker.js')
 
pw.proxy.hello(42, 'fish') // works and calls hello() inside worker
pw.proxy.Encoder.myEncoder(...) // works and calls Encoder.myEncoder() inside worker
pw.proxy.Decoder.myDecoder(...) // error
// worker.js
function hello(number, string) {
    return `this is the result. Number: ${number}, String: ${string}`
}
 
var Encoder = {
    myEncoder(...) {...}
}
 
class Decoder {
    static myDecoder(...) {...}
}
 
// typeof hello === 'function'
// typeof self.hello === 'function'
 
// typeof Encoder === 'object'
// typeof self.Encoder === 'object'
// typeof self.Encoder.myEncoder === 'function'
 
// typeof Decoder === 'function'
// typeof self.Decoder === 'undefined'

Apis for exposing such objects is planned. Shhh... in the meantime, dirty trick always get the job done self.Decoder = Decoder. But you didn't hear it from me.

Future plans

Streams

Support for Node's Streams and upcoming browser's WHATWG Streams spec is planned.

If everything works out correctly, support will include ReadableStream, WritableStream, .pipeTo(), .pipeThrough() as well as Node's equvalent Readable, Writable, .pipe()

TODOs

  • Disabling autotransfer (to unify behavior with node)
  • support for streams. Both Node and the new browers ones.
  • Durability - restart worker if it crashes/stops and restart ongoing tasks or offload them to another worker.
  • More tests.

Versions

Current Tags

  • Version
    Downloads (Last 7 Days)
    • Tag
  • 0.1.0
    0
    • latest

Version History

  • Version
    Downloads (Last 7 Days)
    • Published
  • 0.1.0
    0
  • 0.0.1
    0

Package Sidebar

Install

npm i fachman

Weekly Downloads

0

Version

0.1.0

License

MIT

Last publish

Collaborators

  • mikekovarik