Two offline-queue replay edges (#133):
(a) Unbounded 5xx retry. On status >= 500 the entry is kept and re-attempted on every reconnect and every app foreground, with no attempt counter, backoff, or TTL — plus a full /me/state fetch each time. A persistently-500ing slug fires forever. (Note: coalescing bounds entry count, learn staleness and 4xx-drop are handled — this is the remaining unbounded-retry hazard.)
mobile/src/services/remoteProgressRepository.ts — flushQueue 5xx continue; ProgressContext.tsx foreground/connectivity triggers.
(b) Sign-out mid-flush cross-account send (privacy). flushQueue checks epoch only at the top of each loop iteration and before the final reconcile. If forget() (sign-out / expired-token SIGNED_OUT) runs while a replay() is in flight, that request still completes and getAccessToken() returns the now-current session — so a queued mutation from account A can be POSTed under account B’s token before the next epoch check aborts. Narrow (one-mutation window) but real on a shared device.
Suggested direction — (a) per-entry attempt/first-seen timestamp; drop or back off after N tries / age. (b) re-check epoch immediately after each await this.replay(...) before treating it as applied, and/or thread an epoch-keyed abort signal into the in-flight request.
Two offline-queue replay edges (#133):
(a) Unbounded 5xx retry. On
status >= 500the entry is kept and re-attempted on every reconnect and every app foreground, with no attempt counter, backoff, or TTL — plus a full/me/statefetch each time. A persistently-500ing slug fires forever. (Note: coalescing bounds entry count,learnstaleness and 4xx-drop are handled — this is the remaining unbounded-retry hazard.)mobile/src/services/remoteProgressRepository.ts—flushQueue5xxcontinue;ProgressContext.tsxforeground/connectivity triggers.(b) Sign-out mid-flush cross-account send (privacy).
flushQueuechecksepochonly at the top of each loop iteration and before the final reconcile. Ifforget()(sign-out / expired-token SIGNED_OUT) runs while areplay()is in flight, that request still completes andgetAccessToken()returns the now-current session — so a queued mutation from account A can be POSTed under account B’s token before the next epoch check aborts. Narrow (one-mutation window) but real on a shared device.Suggested direction — (a) per-entry attempt/first-seen timestamp; drop or back off after N tries / age. (b) re-check
epochimmediately after eachawait this.replay(...)before treating it as applied, and/or thread an epoch-keyed abort signal into the in-flight request.