harden(desktop): RecoveryStateStore listener safety + frozen state
- Snapshot listener set before notifying so subscribers can subscribe/ unsubscribe during notification without breaking the pass - Per-callback try/catch so one throwing subscriber does not silence others - Object.freeze on each state object so the live reference returned by get() cannot be accidentally mutated externally (compile-time Readonly<> is hint only) - 3 new tests pinning these invariants
This commit is contained in:
@@ -30,7 +30,7 @@ const INITIAL_STATE: RecoveryState = {
|
||||
};
|
||||
|
||||
export class RecoveryStateStore {
|
||||
private state: RecoveryState = { ...INITIAL_STATE };
|
||||
private state: RecoveryState = Object.freeze({ ...INITIAL_STATE }) as RecoveryState;
|
||||
private listeners = new Set<(s: RecoveryState) => void>();
|
||||
private inRecoveryMode = false;
|
||||
|
||||
@@ -39,8 +39,17 @@ export class RecoveryStateStore {
|
||||
}
|
||||
|
||||
update(partial: Partial<RecoveryState>): void {
|
||||
this.state = { ...this.state, ...partial };
|
||||
for (const cb of this.listeners) cb(this.state);
|
||||
this.state = Object.freeze({ ...this.state, ...partial }) as RecoveryState;
|
||||
// Snapshot before iterating: a listener can subscribe/unsubscribe others
|
||||
// (or itself) during notification without affecting the current notify pass.
|
||||
const snapshot = Array.from(this.listeners);
|
||||
for (const cb of snapshot) {
|
||||
try {
|
||||
cb(this.state);
|
||||
} catch (err) {
|
||||
console.error('[recovery] listener threw:', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subscribe(cb: (s: RecoveryState) => void): () => void {
|
||||
|
||||
Reference in New Issue
Block a user