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 project sets needsResync. From then on, solve(null, …), apply and level throw ProjectException with code ProjectError.NEEDS_RESYNC. Reads that need a baseline (preview, loadProfile, evaluate, suggestAssignments) throw ProjectError.NO_BASELINE. To clear the flag, reload the full data with solve(data, …). See Editing a project for why.
Prerequisites#
- A long-lived
Projectsolved 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
ApplyReport:var report ;If
applythrows aProjectException, nothing was applied:NEEDS_RESYNCcomes from an earlier rejection,STALE_BASELINEfrom abaselineIdthat isn't current. A rejection inside the batch doesn't throw. -
Check
needsResync. If it'sfalse, continue as usual withsolve(null, …):if -
Show the user what was refused. Each
RejectedOperationhas the operation'spath(such astasks.remove[0]) and amessage:forThe 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 throwNEEDS_RESYNC. -
Resync with the full data. Set
includeUnchangedso the result carries the dates of every task, not only the ones that moved:var resync ; var result ;solvedeep-copiesmodel, so later edits to your model don't leak into the project. -
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:
;
;
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 InputException with the field path in path, for example tasks[1].id. The project keeps its previous state and still throws NEEDS_RESYNC. Fix the data and call solve(data, …) again. See Error handling for the full list of validation errors.
When you didn't check the report#
If some code path skipped needsResync, the next apply or solve(null, …) throws. ProjectException is unchecked, so catch it where you send edits and resync on NEEDS_RESYNC:
try
apply checks NEEDS_RESYNC before anything else, so the change set in that call isn't applied. Put its edits in your model before the resync.
Drop the edit instead of resyncing#
To avoid a full reload, apply the change set to a copy first. copy() is cheap, and a rejection on the copy doesn't touch the original:
var draft ;
var report ;
if else
Use this when the user should either get the whole change set or none of it.
Result#
The example 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(null, …), level and preview work again, and report.needsResync is false for the next valid change set.