Web Worker with WPC

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.

/src/modules/hello/HelloWorker.ts
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.

/src/app/api/[[...vovk]]/route.ts
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.

/src/modules/hello/HelloWorker.ts
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.

/src/modules/hello/HelloController.ts
import { get } from 'vovk';
 
export class HelloController {
    @get.auto()
    static getIterations() {
        return { iterations: 100_000_000 };
    }
}
/src/modules/hello/HelloWorker.ts
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

/src/modules/worker/HelloWorker.ts
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;
  }
}

Source code (opens in a new tab)