JsonProject#

A project that lives in wasm memory between calls. It holds the accepted data, the baseline from the last solve and the cached result, so later calls send only edits and options. Create one with new JsonProject(options_json) or by calling clone() on an existing handle. For a one-shot run without a handle, use schedule.

Declaration#

class JsonProject {
  constructor(options_json: string);
  solve(data_json: string | null | undefined, options_json: string): string;
  preview(changes_json: string, options_json: string): string;
  analyze(data_json: string | null | undefined, options_json: string): string;
  apply(changes_json: string): string;
  level(options_json: string): string;
  loadProfile(options_json: string): string;
  evaluate(candidates_json: string, options_json: string): string;
  slots(pool_json: string, requirements_json: string): string;
  suggestAssignments(slots_json: string, options_json: string): string;
  clone(): JsonProject;
  free(): void;
  [Symbol.dispose](): void;
}

The class has the same declaration in the browser build (pkg/plen_wasm.js) and the Node build (pkg-node/plen_wasm.js). In the browser build, init() must resolve before the first new JsonProject.

Release every handle, clones included, with free() or a using declaration ([Symbol.dispose]).

Creating#

new JsonProject(options_json: string)

Creates an empty project. It has no data and no baseline, so the first call is usually solve with data.

Request#

Argument Payload Required Description
options_json ProjectOptions yes Project-wide options. "{}" for defaults

ProjectOptions#

How the project treats imported data. The options are fixed for the handle's lifetime, and a clone inherits them. Unknown fields are rejected.

Field Type Required Default Description
validation "strict" | "repair" no "strict" "strict" rejects invalid data with a validation error. "repair" fixes duplicate ids and missing or inconsistent dates and reports each fix as a repaired note. null means the default

What each mode accepts and fixes: Accept messy input.

Errors#

The constructor is the only export that throws on bad input. It throws a string, the JSON text of an error envelope, not an Error object.

kind code When
parse null options_json isn't valid JSON, isn't an object, or has an unknown field
validation null validation is a string other than "strict" or "repair"

In the browser build, calling the constructor before init() resolves fails at host level; see Host-level failures.

Example#

Create a handle in Node, with the thrown string parsed as an envelope:

const { JsonProject } = require("./pkg-node/plen_wasm.js");

let project;
try {
  project = new JsonProject(JSON.stringify({ validation: "repair" }));
} catch (thrown) {
  const envelope = JSON.parse(thrown);
  throw new Error(`${envelope.error.kind}: ${envelope.error.message}`);
}
// ... solve, apply, preview
project.free();

options_json that fails:

{ "validation": "lenient" }

What the constructor throws:

{
  "ok": false,
  "error": { "kind": "validation", "code": null, "message": "unknown validation mode", "path": null }
}

Operations#

Each operation takes JSON text and returns an envelope as JSON text. None of them throws for bad input.

Operation Description
solve Load data or reschedule edits, and accept the dates as the baseline
preview Schedule hypothetical changes against the baseline without changing the project
analyze Critical path and slack over supplied or current dates, without scheduling
apply Commit edits to the accepted state
level Resolve resource overallocation with delays and splits, and accept the result
loadProfile Per-bucket load and capacity for resources on the baseline
evaluate Score candidate change sets against the baseline
slots Build assignment slots from workers' skills and task requirements
suggestAssignments Propose assignments that fill, repair or rebalance slots

Handle members#

clone#

clone(): JsonProject

Returns a new handle with the same accepted state, baseline, cached result and options. The copy shares immutable state with the original until one of them is edited, and edits to one never affect the other. Works on a project in any state, solved or not.

Returns - a separate JsonProject. Free it on its own: freeing the original doesn't free the clone, and the other way round.

Errors - throws at host level when called on a freed handle.

const proposal = project.clone();
try {
  const leveled = JSON.parse(proposal.level("{}"));
  // show leveled.result as a proposal; project is unchanged
} finally {
  proposal.free();
}

free#

free(): void

Releases the project's wasm memory right away. After free(), every method call on the handle throws an Error from the generated bindings, and so does a second free().

A handle that becomes unreachable without free() is released eventually by a FinalizationRegistry, at a time the garbage collector chooses.

[Symbol.dispose]#

[Symbol.dispose](): void

The same function as free(). Lets hosts that support explicit resource management free the handle at the end of a scope:

{
  using project = new JsonProject("{}");
  const envelope = JSON.parse(project.solve(JSON.stringify(data), "{}"));
  // project is freed when the block exits
}

Example#

Load a project, try an edit on a clone, and free both handles, in Node:

const { JsonProject } = require("./pkg-node/plen_wasm.js");

const data = {
  config: { use_calendars: false },
  tasks: [
    { id: "a", start_date: "2026-08-10 08:00", duration: 2 },
    { id: "b", start_date: "2026-08-10 08:00", duration: 1 },
  ],
  links: [{ id: "ab", source: "a", target: "b" }],
};

const project = new JsonProject("{}");
try {
  JSON.parse(project.solve(JSON.stringify(data), "{}"));

  const copy = project.clone();
  try {
    copy.apply(JSON.stringify({ tasks: { update: [{ id: "a", duration: 4 }] } }));
    const tried = JSON.parse(copy.solve(undefined, "{}"));
    console.log(tried.result.updates); // a and b moved in the copy
  } finally {
    copy.free();
  }

  // The original still holds its own baseline.
  const current = JSON.parse(project.solve(undefined, "{}"));
  console.log(current.result.updates); // b: 2026-08-12 08:00 - 2026-08-13 08:00
} finally {
  project.free();
}

Response of copy.solve(undefined, "{}"):

{
  "ok": true,
  "result": {
    "baselineId": "<opaque token>",
    "conflicts": [],
    "converged": true,
    "cycles": [],
    "notes": [],
    "updates": [
      { "id": "a", "kind": "task", "start_date": "2026-08-10 08:00", "end_date": "2026-08-14 08:00", "duration": 4 },
      { "id": "b", "kind": "task", "start_date": "2026-08-14 08:00", "end_date": "2026-08-15 08:00" }
    ]
  }
}