mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-03 08:06:59 +00:00
The sync adapters from ed2525e start a headless task for every sync, but
the task only reported "disabled". This adds the engine it runs
(`src/device-sync/engine`, `src/device-sync/jmap`): for one Android
account and authority it downloads the server's changes, uploads what
the device changed, and hands a report back to SyncManager.
- Downloads follow `/changes` from the state kept in the provider's
SyncState. Rows and the state they reach are written in one batch, so
a killed run resumes where it stopped. Without a state, after
`cannotCalculateChanges` or after the user discarded deletions, a full
reconcile compares ids instead.
- Uploads go in three phases across accounts (creates, updates,
deletions), so moving an item to another account never destroys it
before the copy exists. A create carries a uid written to the device
first, and the server is asked for that uid before creating, so a lost
response or a killed run never makes a duplicate. Updates are patches
guarded by `ifInState`; nothing is replaced whole.
- More than 50 deletions that are also more than a fifth of the synced
items hold every deletion back and report `tooManyDeletions`, so
Android asks the user; "undo" restores the items from the server.
- An item the server refuses is marked and not sent again until it
changes or its back-off (1 hour, doubling to a day) ends. Such
refusals and planner errors stay with their item; the rest of the run
goes on.
- Runs of one account and authority take a lock, stop at checkpoints
when cancelled or close to the deadline, and never throw: every
failure becomes an outcome, and a rejected sign-in posts one "Sign in
again" notification.
- The JMAP side uses a client of its own per app account, never the
UI's, since a run can share the JS runtime with the app.
Turning sync off or signing out runs the same upload first
(`teardownAuthority`) and deletes the rows only once nothing is waiting,
or when the user agreed to lose it.
The planners that map items to rows land in the next commits; until
then a run fails before it touches a row.
Checked: 94 engine tests against the fake provider and JMAP server
(sync flows, failures, crashes and retries, teardown), and `npm run
typecheck`, `npm test` and `npm run i18n:check` on this commit.
35 lines
1.5 KiB
TypeScript
35 lines
1.5 KiB
TypeScript
/**
|
|
* Poison markers (docs/device-sync.md, "Uploads", SetErrors): an item the
|
|
* server refused (`invalidProperties`, `invalidPatch`, `tooLarge`,
|
|
* `overQuota`, …) is not retried until its back-off ends (1 h, doubling to
|
|
* a day) or it changes. The engine writes the marker (SYNC4 / SYNC_DATA5)
|
|
* and checks it before planning an upload; the fingerprint covers what the
|
|
* item would upload from: its mapped rows, its flags and its shadow.
|
|
*/
|
|
import { jsonHash } from '../common/json';
|
|
import type { PoisonMarker } from '../planner';
|
|
|
|
export function poisonFingerprint(content: unknown): string {
|
|
return jsonHash(content);
|
|
}
|
|
|
|
/** True while the marker holds: same fingerprint and the back-off not over. */
|
|
export function isBackedOff(marker: PoisonMarker | null, fingerprint: string, now: number): boolean {
|
|
return !!marker && marker.fp === fingerprint && marker.until > now;
|
|
}
|
|
|
|
/** The next marker after a refusal: attempts count up while the item stays the same. */
|
|
export function nextMarker(
|
|
previous: PoisonMarker | null,
|
|
fingerprint: string,
|
|
error: { type: string; description?: string },
|
|
now: number,
|
|
backoff: { firstMs: number; maxMs: number },
|
|
): PoisonMarker {
|
|
const n = previous && previous.fp === fingerprint ? previous.n + 1 : 1;
|
|
const delay = Math.min(backoff.firstMs * 2 ** (n - 1), backoff.maxMs);
|
|
const marker: PoisonMarker = { fp: fingerprint, type: error.type, n, until: now + delay };
|
|
if (error.description) marker.description = error.description.slice(0, 500);
|
|
return marker;
|
|
}
|