WPC Class (Worker Procedure Call)
The standard Web Workers are awesome but they require to write additional logic by using onmessage
handler on both sides (main thread and the Woker thread) and exchange data using postMessage
. Vovk.ts applies the same principle that is used at controllers and builds main-thread client-side library using the auto-generated .vovk.json. It uses built-in browser API aush as addEventListener
and postMessage
and does not utilise eval
function or Function
constructor.
WPC Class is created from an Isomorphic Service Class by applying @worker()
class decorator that defines onmessage
handler in the Web Worker scope.
Quick explanation: Isomorphic Service Class is a static class that provides code that is shared between front-end and back-end. It should implement static methods as pure functions (opens in a new tab) that don't have access to neither application state nor server-side capabilities such as access to the database.
import { worker } from 'vovk';
@worker()
export default class HelloWorker {
static heavyCalculation(iterations: number) {
let result: number;
// ... heavy calculations
return result;
}
}
In a non-worker scope @worker()
does nothing. You can import the class safely in other modules, including back-end code where it's going to be behave as a normal collection of pure functions.
To compile the worker interface, you need to pass them to initVovk
as workers
object option and export the type of this object as Workers
.
import { initVovk } from 'vovk';
import HelloController from '../../../hello/HelloController';
import HelloWorker from '../../../hello/HelloWorker';
import ByeWorker from '../../../bye/ByeWorker';
const controllers = { HelloController };
const workers = { HelloWorker, ByeWorker };
export type Controllers = typeof controllers;
export type Workers = typeof workers;
export const { GET, POST, PUT, DELETE } = initVovk({ controllers, workers });
Once this is done, vovk-client is going to export the worker library that provides interface to invoke heavy calculations but doesn't initialise the Web Worker itself. To initialise the Web Worker at the main-thread interface it needs to be instantiated normally and passed as an argument of employ
static method.
import { HelloWorker } from 'vovk-client';
HelloWorker.employ(new Worker(new URL('./path/to/HelloWorker.ts', import.meta.url)));
This bulky syntax is required to invoke the Webpack 5+ loader used by Next.js internally. After it's done the static methods of the mapped class type return Promise
to delegate heavy calculations to the parallel thread.
const result = await HelloWorker.heavyCalculation(1e9);
Note that Worker
class does not exist in Next.js SSR environment and in case if the code is exposed to non-client-side environment (for example outside of useEffect
) it's recommended to check if Worker
exists at the global scope.
import { HelloWorker } from 'vovk-client';
if(typeof Worker !== 'undefined') {
HelloWorker.employ(new Worker(new URL('./path/to/HelloWorker.ts', import.meta.url)));
}
employ
method returns the worker interface itself so as a nicer solution you can use ternary operator to make the Worker library to be nullish.
import { HelloWorker } from 'vovk-client';
const MyWorker = typeof Worker === 'undefined'
? null
: HelloWorker.employ(new Worker(new URL('./path/to/HelloWorker.ts', import.meta.url)));
await MyWorker?.heavyCalculation(1e9);
Worker termination
A worker can be terminated with built-in terminate
method.
HelloWorker.terminate();
Async generators
WPC classes support generators and async generators to implement continious event streaming.
import { worker } from 'vovk';
@worker()
export default class HelloWorker {
static *generator() {
for (let i = 0; i < 10; i++) {
yield i;
}
}
static async *asyncGenerator() {
for (let i = 0; i < 10; i++) {
await new Promise((resolve) => setTimeout(resolve, 100));
yield i;
}
}
}
Vovk.ts turns them both into async generators when they're imported from vovk-client.
import { HelloWorker } from 'vovk-client';
// ... plug in the Web Worker with "use" method ...
for await (const number of HelloWorker.generator()) {
console.log(number); // 0 ... 9
}
for await (const number of HelloWorker.asyncGenerator()) {
console.log(number); // 0 ... 9
}
Making HTTP requests inside a WPC Class
Since Web Workers are run in a browser (but just in another thread) it's capable to fetch server-side data as expected.
import { get } from 'vovk';
export class HelloController {
@get.auto()
static getIterations() {
return { iterations: 100_000_000 };
}
}
import { HelloController } from 'vovk-client';
@worker()
export default class HelloWorker {
static async heavyCalculation() {
const { iterations } = await HelloController.getIterations();
let result: number;
// ...
return result;
}
}
Using WPC Class inside another WPC Class
Workers can use other workers. The syntax remains the same and you don't need to check for Worker
variable to exist.
import { AnotherWorker } from 'vovk-client';
AnotherWorker.employ(new Worker(new URL('./path/to/AnotherWorker.ts', import.meta.url)));
export default class HelloWorker {
heavyCalculation() {
const anotherWorkerResult = await AnotherWorker.doSomethingHeavy();
// ...
}
}
Forking the Worker
To fork the worker and create as many parallel processes as needed you can use fork
method instead of employ
.
import { HelloWorker } from 'vovk-client';
function getFork() {
return HelloWorker.fork(new Worker(new URL('./path/to/HelloWorker.ts', import.meta.url)));
}
const HelloWorker1 = getFork();
const HelloWorker2 = getFork();
const HelloWorker3 = getFork();
const [result1, result2, result3] = await Promise.all([
HelloWorker1.heavyCalculation(),
HelloWorker2.heavyCalculation(),
HelloWorker3.heavyCalculation(),
]);
Live Worker Example
import { worker } from 'vovk';
@worker()
export default class HelloWorker {
/**
* Factorizes a large number into its prime factors
* @param number - the large number to factorize
*/
static factorize(number: bigint): bigint[] {
let factors: bigint[] = [];
if (number < 2n) {
return [number];
}
while (number % 2n === 0n) {
factors.push(2n);
number /= 2n;
}
for (let i = 3n; i * i <= number; i += 2n) {
while (number % i === 0n) {
factors.push(i);
number /= i;
}
}
if (number > 1n) {
factors.push(number); // Remaining number is a prime factor
}
return factors;
}
}