JsonProject.apply#

Writes the edits of a SchedulingChanges payload into the accepted state of a JsonProject and marks it unsolved. It doesn't schedule anything: follow it with solve without data or with level to get the new dates. To see what an edit would do without committing it, use preview.

Signature#

apply(changes_json: string): string

Synchronous and CPU-bound. Edits the handle in place. In the browser build, call it only after init() has resolved.

Request#

Argument Payload Required Description
changes_json SchedulingChanges yes The edits to commit. The order the operations run in is on the SchedulingChanges page.

Behavior#

  • Operations are partially accepted. Each one is tried on its own, so a rejected operation leaves no partial edit and the operations around it still apply. Rejections go into rejected of a success envelope, not into an error envelope.
  • Any rejection sets needsResync. After that, solve without data, apply and level return needs-resync until a successful solve with full data.
  • The project is unsolved afterwards. preview, loadProfile, evaluate, slots and suggestAssignments return no-baseline until the next solve, and every earlier baselineId is stale.
  • Tasks added or updated by successful operations are reported by the next acyclic solve without data, or level, even when the planner leaves them where the edit put them. They accumulate across several apply calls until then.
  • A clone() made before the call doesn't see the edit.

Response#

The success envelope carries an ApplyReport. The envelope shape is described in Wire format.

ApplyReport#

Which operations of the change set took effect, which were refused, and what the accepted ones changed beyond the records they named. All fields are always present.

Field Type Description
applied { path: string }[] Accepted operations, in the order they ran. path names the operation, e.g. tasks.update[0].
rejected { path: string, message: string }[] Refused operations, in the order they ran, each with the reason. Empty when the whole change set was accepted.
effects ChangeEffects Ids removed, generated or dropped by the accepted operations.
needsResync boolean true when rejected isn't empty.
revision number The project's state revision after this call. Every apply raises it, so a larger value from the same handle means a later state.

Every operation of the change set appears in exactly one of applied and rejected.

ChangeEffects#

Side effects of the accepted operations, for keeping the caller's own copy of the data in sync. Every field is an array of Ids, always present and empty when there was no such effect. A rejected operation adds nothing.

Field Type Description
removedTasks Id[] Each task named in tasks.remove, followed by its descendants, which are removed with it.
removedLinks Id[] Links named in links.remove, and links removed because their source or target task was removed.
generatedLinks Id[] Ids given to added links that had no id, in the form $generated_link_1, $generated_link_2, and so on.
droppedLinks Id[] Links still in the project that became inactive because of this change set. Links that were already inactive aren't listed.
removedAssignments Id[] Assignments named in assignments.remove, and assignments removed because their task or resource was removed.
generatedAssignments Id[] Ids given to added assignments that had no id.
droppedAssignments Id[] Assignments still in the project that became inactive because of this change set.

Errors#

Error When
kind: "project", code: "stale-baseline" baselineId in the change set doesn't name the current baseline. Nothing is applied.
kind: "project", code: "needs-resync" An earlier apply rejected an operation and no solve with data followed. Checked before baselineId, so it wins over stale-baseline.
kind: "parse" changes_json isn't valid JSON, has the wrong JSON type, misses a required field, or has an unknown field in the change set or its buckets.
kind: "validation" A value the decoder rejects, such as an invalid date or an unknown enum string (path: null). Nothing is applied.

Errors come back in the returned envelope; the call doesn't throw for bad input. Calling it on a freed handle throws at host level. See Error handling.

Note

A priority outside 0..1000 or a malformed hour range fails the whole call with a validation error that has no path. In apply, this means no operation from that change set is applied.

Note

A start date that falls on non-working time is kept unsnapped when the task has no predecessor. The end is computed forward from it on the working calendar.

Example#

Request (changes_json), sent after a solve that returned baselineId "1-b0605af8cba3d7c9":

{
  "baselineId": "1-b0605af8cba3d7c9",
  "tasks": {
    "update": [{ "id": "design", "duration": 3 }],
    "remove": ["gone"]
  },
  "links": {
    "add": [{ "source": "design", "target": "build", "type": "ss" }]
  }
}

Response:

{
  "ok": true,
  "result": {
    "applied": [{ "path": "tasks.update[0]" }, { "path": "links.add[0]" }],
    "effects": {
      "droppedAssignments": [],
      "droppedLinks": [],
      "generatedAssignments": [],
      "generatedLinks": ["$generated_link_1"],
      "removedAssignments": [],
      "removedLinks": [],
      "removedTasks": []
    },
    "needsResync": true,
    "rejected": [{ "message": "unknown task id", "path": "tasks.remove[0]" }],
    "revision": 2
  }
}

The project has no task gone, so the removal is rejected and needsResync is set. The update and the link addition still apply.

Host call in Node, resyncing with full data after a rejection:

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

const data = {
  tasks: [
    { id: "design", start_date: "2026-08-10 08:00", duration: 2 },
    { id: "build", start_date: "2026-08-10 08:00", duration: 3 },
  ],
  links: [{ id: "l1", source: "design", target: "build", type: "fs" }],
};

const project = new JsonProject("{}");
try {
  const solved = JSON.parse(project.solve(JSON.stringify(data), "{}"));
  if (!solved.ok) throw new Error(solved.error.message);

  const changes = {
    baselineId: solved.result.baselineId,
    tasks: { update: [{ id: "design", duration: 3 }], remove: ["gone"] },
  };
  const envelope = JSON.parse(project.apply(JSON.stringify(changes)));
  if (!envelope.ok) {
    const { kind, code, message } = envelope.error;
    throw new Error(`${kind} ${code ?? ""}: ${message}`);
  }

  const report = envelope.result;
  for (const op of report.rejected) {
    console.log(`rejected ${op.path}: ${op.message}`);
  }

  // After a rejection, only solve with full data is accepted.
  const next = report.needsResync
    ? project.solve(JSON.stringify(data), "{}")
    : project.solve(undefined, "{}");
  console.log(JSON.parse(next).result.updates);
} finally {
  project.free();
}