Recover from rejected changes#
Get an editor working again after apply rejects part of a change set, without reloading the module or losing the user's edits.
A rejection isn't an error envelope. apply returns ok: true with the refused operations in rejected, and sets needsResync. From then on, solve without data, apply and level return a project error with code needs-resync. Calls that need a baseline (preview, loadProfile, evaluate, suggestAssignments) return no-baseline. To clear this state, send the full data again with solve(dataJson, …). See Editing a project for why.
Prerequisites#
- A long-lived
JsonProject, solved once withsolve(dataJson, …). - Your own model that can produce the full
ProjectDatathe user expects to see. You resync from this data.
Steps#
-
Apply the change set and keep the
ApplyReportfrom the envelope'sresult:const = ;An error envelope here means nothing was applied:
needs-resyncfrom an earlier rejection,stale-baseline, or aparse/validationerror in the change set. A rejection inside the batch isn't an error envelope.A
priorityoutside 0..1000 or a malformed hour range fails the whole call with avalidationerror that has nopath. Inapply, this means no operation from that change set is applied. -
Check
needsResync. If it'sfalse, continue as usual withsolvewithout data: -
Show the user what was refused. Each entry of
rejectedhas the operation'spath(such astasks.remove[0]) and amessage:The other operations in the batch were applied. A rejected operation leaves no partial edit behind.
-
Make your model hold the state you want. Undo the rejected edits in it, or keep it as is if it's already correct. Don't try to repair the project with more
applycalls: they returnneeds-resync. -
Resync with the full data. Set
includeUnchangedin the solve options so the result carries the dates of every task, not only the ones that moved:const = ; const = ; -
Write every entry of
result.updatesback into your UI. The engine's state may differ from what the UI shows, so refresh all task dates, not just the edited ones. -
Take the new
baselineIdfromresultfor the nextpreviewor change set. Tokens from before the resync are stale.
The full example below stretches one task and, in the same batch, removes a task the engine doesn't have:
const = ;
// The editor's own model: the data you resync from.
const = ;
const = new;
The report from that apply looks like this:
The real report also carries effects, omitted here.
When the resync data is invalid#
The resync goes through the same validation as the first load. If your model produces invalid data, such as a duplicate task id, solve(dataJson, …) returns a validation error envelope with the field path:
The project keeps its previous state and still returns needs-resync. Fix the data and call solve(dataJson, …) again. See Error handling for the envelope and the error kinds.
Drop the edit instead of resyncing#
To avoid a full reload, apply the change set to a clone() first. A clone is cheap, and a rejection there doesn't touch the original:
const = ;
const = ;
Use this when the user should get either the whole change set or none of it. Free whichever handle you drop: each one holds its own WebAssembly memory. See Module and handles.
Result#
Run the example with node. It prints the rejected removal on stderr and the refreshed date of every task on stdout:
rejected tasks.remove[0]: unknown task id
design 2026-01-05 00:00..2026-01-09 00:00
After the resync, apply, solve without data, level and preview work again, and needsResync is false for the next valid change set.