Document failed refresh persistence edge case
This commit is contained in:
@@ -54,6 +54,8 @@ If a replacement bundle has already been built before bailing, dispose it. This
|
|||||||
|
|
||||||
**Severity: Medium-Low**
|
**Severity: Medium-Low**
|
||||||
|
|
||||||
|
**Status: Accepted and documented.** The missing-lineage-link failure mode and its recovery limits are now explicit in the main plan under “Edge case: a failed write leaves a missing lineage link.” Conditional persistence continues to prefer the authoritative stored generation.
|
||||||
|
|
||||||
On `main`, a failed session write left storage stale, but the next dispatch rewrote the complete session unconditionally. Storage therefore healed on the next refresh. On this branch, the `jti`-chained merge (`session-merge.ts:171-200`) cannot distinguish another tab advancing the chain from this tab losing its own previous chain-link write.
|
On `main`, a failed session write left storage stale, but the next dispatch rewrote the complete session unconditionally. Storage therefore healed on the next refresh. On this branch, the `jti`-chained merge (`session-merge.ts:171-200`) cannot distinguish another tab advancing the chain from this tab losing its own previous chain-link write.
|
||||||
|
|
||||||
Concrete sequence:
|
Concrete sequence:
|
||||||
|
|||||||
@@ -238,6 +238,31 @@ Tab A action: broadcast update notification
|
|||||||
|
|
||||||
Broadcasting after a failed write would tell Tab B to reread localStorage while it still contains `(7, A)`, spreading the stale generation instead of the new one.
|
Broadcasting after a failed write would tell Tab B to reread localStorage while it still contains `(7, A)`, spreading the stale generation instead of the new one.
|
||||||
|
|
||||||
|
### Edge case: a failed write leaves a missing lineage link
|
||||||
|
|
||||||
|
If the process survives a failed write, its live `PasswordSession` may advance while authoritative storage remains behind:
|
||||||
|
|
||||||
|
```text
|
||||||
|
t0 memory/storage: generation A
|
||||||
|
t1 server refresh: A -> B
|
||||||
|
t1 reducer/session: generation B
|
||||||
|
t1 storage write: fails; localStorage remains A
|
||||||
|
|
||||||
|
t2 server refresh: B -> C
|
||||||
|
t2 conditional write:
|
||||||
|
base generation: B
|
||||||
|
localStorage: A
|
||||||
|
action: reject C because the persisted base is not B
|
||||||
|
```
|
||||||
|
|
||||||
|
The committed reconciliation then rebuilds the live session from persisted generation `A`, discarding the valid in-memory generation `C`. If `A` is still inside the PDS reuse window, subsequent refreshes can walk back through the server lineage and recover, potentially requiring extra round trips. If `A`'s fixed grace period has expired, the account may be logged out.
|
||||||
|
|
||||||
|
This is a deliberate safety tradeoff. The conditional merge cannot prove that the missing `A -> B` write belonged to this process; the in-memory `B` could instead be stale credentials from another tab or an older login lineage. Accepting `B -> C` without a persisted `B` would weaken the central invariant and could overwrite a different authoritative session. The implementation therefore prefers the persisted generation even though an isolated failed write can turn a healthy in-memory session into a later logout.
|
||||||
|
|
||||||
|
A retry of the original `A -> B` write before another refresh closes the gap. Native retries the same queued root snapshot once. Web reports the storage failure without updating its persisted cache or broadcasting; automatic refresh paths log the failure, while explicit refresh operations report it to their caller.
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
After successfully writing localStorage, notify other tabs:
|
After successfully writing localStorage, notify other tabs:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
|
|||||||
Reference in New Issue
Block a user