Recover from rejected changes#
Get an editor working again after apply rejects part of a change set, without restarting the app or losing the user's edits.
When apply rejects an operation, the report has needsResync: true. From then on, solve(null), apply and level throw ProjectError with code "needs-resync". Calls that need a baseline (preview, loadProfile, evaluate, suggestAssignments) throw ProjectError with code "no-baseline". To clear the flag, send the full data again with solve(data). Editing a project explains why.
Prerequisites#
- A long-lived
Projectthat you solved once withsolve(data). - 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 report:
const = ;If
applythrows, nothing was applied.ProjectErrormeans"needs-resync"from an earlier rejection or"stale-baseline".InputErrormeans a malformed change set. A rejection inside the batch doesn't throw. -
Check
report.needsResync. If it'sfalse, continue as usual withsolve(null): -
Show the user what was refused. Each entry of
report.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 it is if it's already correct. Don't try to repair the project with more
applycalls, because they throw"needs-resync". -
Resync with the full data. Set
includeUnchangedso the result has the dates of every task, not only the ones that moved:const = ; -
Write every entry of
result.updatesback into your UI. The engine's state can differ from what the UI shows, so refresh all task dates, not only the edited ones. -
Use the new
result.baselineIdfor the nextpreviewor change set. Tokens from before the resync are stale.
The full example below stretches one task. In the same batch, it removes a task the engine doesn't have:
// The editor's own model: the data you resync from.
const = ;
const = new;
;
// The user stretches "design" to 4 days. The UI also sends a removal
// of "review", a task the engine never had.
model.. = 4;
const = ;
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(data) throws InputError with the field path, for example tasks[1].id. The project keeps its previous state, and solve(null), apply and level still throw "needs-resync". Fix the data and call solve(data) again. See Error handling for the full list of validation errors.
Drop the edit instead of resyncing#
To avoid a full reload, apply the change set to a clone() first. A rejection on the clone doesn't touch the original:
const = ;
const = ;
Use this when the user should get either the whole change set or none of it.
Note:
clone()copies the whole project at once, so its cost grows with the project's size.
Result#
The example prints the rejected removal to stderr and the refreshed dates of every task to stdout:
rejected tasks.remove[0]: unknown task id
design 2026-01-05 00:00..2026-01-09 00:00
After the resync, apply, solve(null), level and preview work again, and report.needsResync is false for the next valid change set.