Use Promises!
The Promise object represents the eventual completion (or failure) of an asynchronous operation and its resulting value.
Step 1: Extend the native Worker class
// main.js
class MyWorker extends Worker {
constructor (src) {
super(src)
}
}
// worker.js
const COMMAND_ONE = 0
const COMMAND_TWO = 1
addEventListener('message', ({data}) => {
let result
switch (data.type) {
case COMMAND_ONE:
// Some awesome off-main computations
// result = ...
break
case COMMAND_TWO:
// etc.
break
default:
result = 'Received an empty message')
}
postMessage(result)
})
Step 2: Override postMessage()
By overriding the native postMessage() method, not only can we send messages to the worker thread, we can send the result back in a Promise. In the Worker script, no changes need to be made (yet).
// main.js
class MyWorker extends Worker {
constructor (src) {
super(src)
}
postMessage (message, transfer=[]) {
return new Promise((resolve) => {
const onmessage = ({data}) => {
this.removeEventListener('message', onmessage)
resolve(data)
}
this.addEventListener('message', onmessage)
super.postMessage(obj, transfer)
})
}
}
Step 3: Use MessagePorts
This is not optional. Since webworkers are asynchronous with the main thread, we cannot simply assume that any of MyWorker's responses will be received in the same order of the calls to postMessage().
For correct asynchronous handling we need to guarantee that the message we received in our local onmessage arrow function is indeed a response to the respective postMessage() call (rather then any message the Worker thread happened to send back at that time).
// main.js
class MyWorker extends Worker {
constructor (src) {
super(src)
}
postMessage (obj, transfer=[]) {
return new Promise((resolve) => {
const {port1, port2} = new MessageChannel()
transfer.push(port2)
this._lastCall = Date.now()
const onmessage = ({data}) => {
port1.removeEventListener('message', onmessage)
resolve(data)
}
port1.addEventListener('message', onmessage)
super.postMessage(obj, transfer)
port1.start()
})
}
}
// worker.js
const COMMAND_ONE = 0
const COMMAND_TWO = 1
addEventListener('message', ({data, ports}) => {
const port = ports[0]
let result
switch (data.type) {
case COMMAND_ONE:
// Some awesome off-main computations
// result = ...
break
case COMMAND_TWO:
// etc.
break
default:
result = 'Received an empty message')
}
// Send result back via a unique Port
port.postMessage(result)
})
Step 4: Use async/await
Rather then just calling postMessage() (which is still valid and could be appropriate e.g. if no response is expected and the timing the Worker script has no impact on other code) we can now use async functions and await the result before we move on.
Be careful to only use await if you are sure you will receive a response from the Worker thread. Not doing so will lock the async function!
// main.js
class MyWorker {
static COMMAND_ONE = 0
static COMMAND_TWO = 1
// etc.
// ...
}
async function foo (w) {
let value1 = await w.postMessage({type: MyWorker.COMMAND_ONE, values: ''})
value1 += value1 + await w.postMessage({type: MyWorker.COMMAND_TWO, values: ''})
let raceFor = []
raceFor.push(w.postMessage({type: MyWorker.COMMAND_THREE, values: ''}))
raceFor.push(w.postMessage({type: MyWorker.COMMAND_FOUR, values: ''}))
raceFor.push(w.postMessage({type: MyWorker.COMMAND_FIVE, values: ''}))
raceFor.push(w.postMessage({type: MyWorker.COMMAND_SIX, values: ''}))
let result
try {
result = Promise.race(raceFor)
}
catch (e) {
result = Promise.resolve('No winners')
}
return result
}
const w1 = new MyWorker('worker.js')
const res = foo(w1)
Promise - Javascript | MDN
MessageChannel - Javascript | MDN
async - Javascript | MDN