feat: Collect blocking errors per batch - #2442
Merged
Merged
Conversation
`blockSyncFor` mixed four concerns: `lifecycle.block()`, `_blockedCauses.set`, the retry `setInterval`, and the `user-alert`/`offline` side effects. This is fine for a single cause but blocks the multi-error feature: we need to register N causes (with their alerts) before starting a single retry timer. Split `blockSyncFor` into `registerBlockingCause` (cause registration + side effects, no lifecycle, no interval) and `scheduleRetry` (`lifecycle.block` + a single `setInterval` whose delay is `minRetryDelay(causes)`). `blockSyncFor` becomes a thin wrapper over the two so existing call sites (RemoteWatcher, `syncBatch` catch) keep their behavior unchanged. `minRetryDelay` picks the shortest delay across causes so retry happens soonest; `retryAll` already iterates every cause on each tick, so one global interval is enough.
The `catch` block of `syncBatch` held a ~120-line switch on `err.code` that interleaved logging, blocking, skipping, conflict creation, and recovery for the failed change. Extracting it into `handleSyncError(err, change)` makes the disposition table readable on its own and prepares the ground for the multi-error loop, which needs to call it per failed change rather than once for the whole batch. Pure move: the switch, the log level selection, and the `blockSyncFor`/`skipChange`/`createConflict` calls are unchanged. The outer `catch` now just guards `willStop` and non-`SyncError` throws, then delegates. The loop still breaks at the first throw — multi-error collection comes next.
`syncBatch` stopped at the first `apply` throw: the outer `catch` handled a single `SyncError` and every later change in the batch waited for the failed change's retry delay. The GUI can display N alerts but only ever received one. The loop now catches per-`apply`. Each failed change is registered via `registerBlockingCause` (accumulating causes and alerts); independent changes after the failure are still applied. Dependents of a failed change are skipped for this cycle via `blockedIds`, a local `Set` cross-referenced with `DependencyGraph.directPrerequisites`. `localSeq` is frozen as soon as `blockedIds` is non-empty so the failed change re-enters the feed on the next cycle; already-applied independents come back as no-ops. A single `scheduleRetry` at the end of the batch starts one global retry timer with `minRetryDelay(causes)`. `handleSyncError` returns a `'blocked' | 'skipped' | 'recovered' | 'fatal'` tag and no longer touches `localSeq` — the loop owns it. `apply` returns `Promise<void>` and no longer calls `setLocalSeq` for the same reason.
taratatach
force-pushed
the
feat/handle-multiple-sync-errors
branch
from
July 3, 2026 10:32
1e56f2c to
bce0b0e
Compare
taratatach
marked this pull request as ready for review
July 3, 2026 10:34
taratatach
added a commit
that referenced
this pull request
Jul 3, 2026
The platform incompatibilities integration tests stubbed `blockSyncFor` to assert which changes got blocked, but the per-batch error collection (#2442) means blocking causes now flow through `scheduleRetry`, called once at the end of the batch with the array of accumulated causes. Stubbing `blockSyncFor` no longer reflects what the sync actually does. We now stub `scheduleRetry` and have `shouldHaveBlockedFor` assert it was called with a causes array containing a cause matching the expected path and `IncompatibleDoc` code. sinon's `calledWithMatch` uses `deepEqual` (full equality) for array expectations rather than partial matching, so we use `sinon.match.some(sinon.match({...}))` to assert the array contains at least one element partially matching the expected cause. Failures are asserted via `should(bool).be.true(msg)` with a self-contained message listing the actual blocked paths, since sinon's default formatter dumps the matcher internals (`{ test, message }`) and is unreadable.
5 tasks
taratatach
added a commit
that referenced
this pull request
Jul 4, 2026
When a remote move renamed `i` to an incompatible path `i*`, then again to a compatible path `z`, `move` set `dst.moveFrom = src` (`i*`), pointing at a path never materialized on the filesystem. The local watcher then tried to move a non-existent `i*` to `z` and failed. `move` now chains through `src.moveFrom` when the local side is not up-to-date, so `dst.moveFrom` records the actual local path to move from (the original `i`) instead of the intermediate incompatible path. When the local side is already up-to-date or `src` has no `moveFrom`, it uses `src` directly as before. The platform incompatibilities integration tests are adapted to the per-batch error collection (#2442): `blockSyncFor` is no longer the right stub since blocking causes now flow through `scheduleRetry`, called once per batch with the accumulated causes array. The new `shouldHaveBlockedFor` uses `sinon.match.some` to assert the array contains a matching cause, and reports actual blocked paths on failure. New integration cases cover the compatible → incompatible → compatible rename sequence.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
syncBatchstopped at the firstapplythrow: the outercatchhandled a single
SyncErrorand every later change in the batchwaited for the failed change's retry delay. The GUI can display N
alerts but only ever received one.
The loop now catches per-
apply. Each failed change is registeredvia
registerBlockingCause(accumulating causes and alerts);independent changes after the failure are still applied. Dependents
of a failed change are skipped for this cycle via
blockedIds, alocal
Setcross-referenced withDependencyGraph.directPrerequisites.localSeqis frozen as soon asblockedIdsis non-empty so thefailed change re-enters the feed on the next cycle; already-applied
independents come back as no-ops. A single
scheduleRetryat theend of the batch starts one global retry timer with
minRetryDelay(causes).handleSyncErrorreturns a'blocked' | 'skipped' | 'recovered' | 'fatal'tag and no longer toucheslocalSeq— the loop owns it.applyreturnsPromise<void>and no longer callssetLocalSeqfor the same reason.Fixes #2439 and #2441
Please make sure the following boxes are checked: