[Canonical page](<https://docs.dhtmlx.com/planning-engine/wasm/how-to/run-in-a-worker/>)
[Full documentation index](<https://docs.dhtmlx.com/planning-engine/llms.txt>)


# Run in a worker

Keep the page or the server responsive while large [`solve`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-solve.md>), [`level`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-level.md>), [`evaluate`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-evaluate.md>) or [`suggestAssignments`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-suggest-assignments.md>) calls run.

Every raw export is synchronous and blocks the thread that calls it. Load the module inside a worker, keep the [`JsonProject`](<https://docs.dhtmlx.com/planning-engine/wasm/api/json-project.md>) handle there, and pass JSON strings between the threads. A handle belongs to the module instance that created it, so it can't be sent to another thread. Background: [Module and handles](<https://docs.dhtmlx.com/planning-engine/wasm/module-and-handles.md>).

## Prerequisites

- The browser build in `pkg/`, or the Node.js build in `pkg-node/` for a worker thread. See [Module and handles](<https://docs.dhtmlx.com/planning-engine/wasm/module-and-handles.md#build-targets>).
- For the browser: module workers (`new Worker(url, { type: "module" })`), since `pkg/plen_wasm.js` is an ES module.

## Steps

1. Create the worker script. It loads the module, creates one project and answers each message with the envelope text, unparsed:

   ```js
   // scheduler-worker.js
   import init, { JsonProject } from "./pkg/plen_wasm.js";

   const OPS = new Set([
     "solve", "preview", "analyze", "apply", "level",
     "loadProfile", "evaluate", "slots", "suggestAssignments",
   ]);

   // Set the handler right away and wait for the module inside it,
   // so messages sent during loading aren't missed.
   const ready = init().then(() => new JsonProject("{}"));

   self.onmessage = async ({ data: { id, op, args } }) => {
     const project = await ready;
     if (op === "close") {
       project.free();
       self.close();
       return;
     }
     try {
       if (!OPS.has(op)) throw new Error(`unknown op ${op}`);
       self.postMessage({ id, text: project[op](...args) });
     } catch (error) {
       self.postMessage({ id, thrown: String(error), trap: error instanceof WebAssembly.RuntimeError });
     }
   };
   ```

   The `OPS` list keeps [`free`](<https://docs.dhtmlx.com/planning-engine/wasm/api/json-project.md#free>) and [`clone`](<https://docs.dhtmlx.com/planning-engine/wasm/api/json-project.md#clone>) out of reach of the main thread. `init()` fetches `plen_wasm_bg.wasm` from the folder that holds `plen_wasm.js`. To load it from another URL, see [init](<https://docs.dhtmlx.com/planning-engine/wasm/api/init.md>).

2. Start the worker on the main thread and turn each request into a promise. A request id matches every reply to its call:

   ```js
   // scheduler-client.js
   export function startScheduler() {
     const worker = new Worker(new URL("./scheduler-worker.js", import.meta.url), { type: "module" });
     const pending = new Map();
     let nextId = 0;

     worker.onmessage = ({ data: { id, text, thrown, trap } }) => {
       const { resolve, reject } = pending.get(id);
       pending.delete(id);
       if (thrown === undefined) resolve(JSON.parse(text));
       else reject(Object.assign(new Error(thrown), { trap }));
     };

     const call = (op, ...args) =>
       new Promise((resolve, reject) => {
         const id = nextId++;
         pending.set(id, { resolve, reject });
         worker.postMessage({ id, op, args });
       });
     const close = () => worker.postMessage({ op: "close" });

     return { call, close };
   }
   ```

   Arguments are the same JSON strings the export takes. `call` resolves with the parsed [envelope](<https://docs.dhtmlx.com/planning-engine/wasm/wire-format.md#envelope>), so check `ok` as you would after a direct call. See [Error handling](<https://docs.dhtmlx.com/planning-engine/wasm/error-handling.md>). If the main thread only forwards results, resolve with `text` and skip the parse.

3. Load the data, then send the heavy calls through `call`. The main thread stays free while the worker runs them:

   ```js
   import { startScheduler } from "./scheduler-client.js";

   // Two 2-day tasks, both booked full time on dev1 from Monday, August 10, 2026.
   const data = {
     tasks: [
       { id: "A", start_date: "2026-08-10 08:00", duration: 2 },
       { id: "B", start_date: "2026-08-10 08:00", duration: 2 },
     ],
     resources: [{ id: "dev1" }],
     assignments: [
       { task: "A", resource: "dev1", value: 1 },
       { task: "B", resource: "dev1", value: 1 },
     ],
   };

   const scheduler = startScheduler();

   const loaded = await scheduler.call("solve", JSON.stringify(data), JSON.stringify({ includeUnchanged: true }));
   if (!loaded.ok) throw new Error(loaded.error.message);
   console.log(`loaded ${loaded.result.updates.length} tasks`);

   const leveled = await scheduler.call("level", "{}");
   if (!leveled.ok) throw new Error(leveled.error.message);
   for (const update of leveled.result.updates) {
     console.log(`${update.id}: ${update.start_date} .. ${update.end_date}, delay ${update.leveling_delay}`);
   }
   ```

   Pass `null` as the data argument for a [`solve`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-solve.md>) or [`analyze`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-analyze.md>) without data: `scheduler.call("solve", null, "{}")`.

4. Close the worker when the project is no longer needed. The `close` message frees the handle before the worker stops:

   ```js
   scheduler.close();
   ```

   `worker.terminate()` also releases everything, since the module instance goes away with the worker. It doesn't wait for a running call to finish.

## In a Node.js worker thread

Use `pkg-node/` and `node:worker_threads`. The Node build is ready as soon as `require` returns, so the worker needs no [`init()`](<https://docs.dhtmlx.com/planning-engine/wasm/api/init.md>):

```js
// scheduler-worker.js
const { parentPort } = require("node:worker_threads");
const { JsonProject } = require("./pkg-node/plen_wasm.js");

const OPS = new Set([
  "solve", "preview", "analyze", "apply", "level",
  "loadProfile", "evaluate", "slots", "suggestAssignments",
]);
const project = new JsonProject("{}");

parentPort.on("message", ({ id, op, args }) => {
  if (op === "close") {
    project.free();
    parentPort.close();
    return;
  }
  try {
    if (!OPS.has(op)) throw new Error(`unknown op ${op}`);
    parentPort.postMessage({ id, text: project[op](...args) });
  } catch (error) {
    parentPort.postMessage({ id, thrown: String(error), trap: error instanceof WebAssembly.RuntimeError });
  }
});
```

On the main thread, the client from step 2 changes in two lines:

```js
const path = require("node:path");
const { Worker } = require("node:worker_threads");

const worker = new Worker(path.join(__dirname, "scheduler-worker.js"));
worker.on("message", ({ id, text, thrown, trap }) => {
  // same body as worker.onmessage in step 2
});
```

## Several workers from one download

A module instance is single-threaded, so one worker runs one call at a time. For parallel work, start several workers. Each one holds its own instance and its own projects. To fetch and compile the `.wasm` file only once, compile it on the main thread and send the [compiled module](<https://docs.dhtmlx.com/planning-engine/wasm/api/init.md#initsync>) to each worker:

```js
// main thread
const wasmUrl = new URL("./pkg/plen_wasm_bg.wasm", import.meta.url);
const wasmModule = await WebAssembly.compileStreaming(fetch(wasmUrl));

const workers = [1, 2, 3].map(() => {
  const worker = new Worker(new URL("./pool-worker.js", import.meta.url), { type: "module" });
  worker.postMessage({ wasmModule });
  return worker;
});
```

```js
// pool-worker.js
import { initSync, JsonProject } from "./pkg/plen_wasm.js";

let project;

self.onmessage = ({ data }) => {
  if (data.wasmModule) {
    initSync({ module: data.wasmModule });
    project = new JsonProject("{}");
    return;
  }
  // ...handle { id, op, args } as in step 1
};
```

Messages to one worker arrive in order, so the module message is handled before any request.

[`evaluate`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-evaluate.md>) doesn't split its candidates across workers by itself. To score them in parallel, give each worker a share of the candidates and its own solved copy of the project.

## If the worker hits an engine failure

An [internal engine failure](<https://docs.dhtmlx.com/planning-engine/wasm/error-handling.md#engine-failures>) doesn't come back as an [error envelope](<https://docs.dhtmlx.com/planning-engine/wasm/wire-format.md#envelope>). The call throws instead, and the client above rejects with `trap: true`.

:::note
An internal engine failure surfaces as a `WebAssembly.RuntimeError`; after one, create the project again (or reload the module) before continuing.
:::

In a worker, the cleanest way to reload is to `terminate()` it, start a new one, and send the full data again with [`solve`](<https://docs.dhtmlx.com/planning-engine/wasm/api/project-solve.md>).

## Result

Open the page with the browser console. It prints:

```text
loaded 2 tasks
B: 2026-08-12 08:00 .. 2026-08-14 08:00, delay 2
```

The page stays interactive while the worker runs. The handle lives only in the worker, and the main thread sees nothing but JSON.
