Project#apply#

Writes edits into the accepted state of a Project and marks it unsolved. It doesn't schedule anything: follow it with solve(null) or level to get the new dates. To try edits without committing them, use preview.

Signature#

apply(changes: SchedulingChanges): ApplyReport

Synchronous. Runs on the calling thread.

Parameters#

Name Type Description
changes SchedulingChanges The edits to commit. Never mutated.

Behavior#

  • Operations run in order and are accepted one by one. A rejected operation goes into rejected in the report; apply doesn't throw for it.
  • Any rejection sets needsResync. From then on, apply throws ProjectError "needs-resync" until a full solve(data).
  • The state is unsolved afterwards, so preview, loadProfile, evaluate, slots and suggestAssignments need a solve(null) or level first.
  • Tasks added or updated by accepted operations accumulate across calls. The next acyclic solve(null) or level reports each of them in updates, even when its dates don't move.
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.

Returns#

An ApplyReport, a fresh object that shares nothing with changes or the project.

ApplyReport#

The outcome of one apply call. Every operation of the change set appears in exactly one of applied and rejected.

Field Type Description
applied { path: string }[] Accepted operations, in the order they ran. path names the operation, e.g. "tasks.update[0]".
rejected RejectedOperation[] Refused operations, in the same order, with a path and a message. A rejected operation leaves no partial edit. 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 project means a later state.

ChangeEffects#

Side effects of the accepted operations beyond the records they named. Each list holds ids in the order the operations ran and is 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 assigned to added links that had no id, in the form "$generated_link_1", "$generated_link_2", and so on. Use them to update or remove those links later.
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. A cascaded assignment without an id isn't listed.
generatedAssignments Id[] Ids assigned to added assignments that had no id, in the form "$generated_assignment_1", "$generated_assignment_2", and so on.
droppedAssignments Id[] Assignments still in the project that became inactive because of this change set. Assignments that were already inactive aren't listed.

Errors#

Error When
ProjectError "stale-baseline" changes.baselineId is set and doesn't name the current baseline.
ProjectError "needs-resync" An earlier apply had rejections and no solve(data) followed. Checked before changes.baselineId, so it wins over "stale-baseline".
InputError The change set is malformed: an unknown key on the change set or one of its buckets, a wrong type, a non-finite number or an invalid Date. path names the field.
TypeError changes has a circular reference or a BigInt.
Error("Project has been disposed") The project was disposed.

Example#

Commit an edit, resync with the full data if part of it was rejected, and reschedule.

import { Project } from "plen-js";

const data = {
  tasks: [{ id: 1, start_date: "2026-08-10 08:00", duration: 2 }],
};

const project = new Project();
const solved = project.solve(data);

const report = project.apply({
  baselineId: solved.baselineId,
  tasks: { update: [{ id: 1, duration: 3 }] },
});

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

const result = report.needsResync ? project.solve(data) : project.solve(null);
console.log(`${result.updates.length} task(s) moved`);