refactor(sync): replace the session-status setState monkeypatch with an owned API
The global session-status store patched its own setState to derive active membership for callers replacing statusById — a boundary that silently trusted any caller passing both fields to keep them consistent. replaceGlobalSessionStatusById is now the one sanctioned way to swap the map from outside the event reducers; the runtime-switch reset and the tests that replaced the map directly go through it, and the patch is gone. Follow-up to #3126 review.
This commit is contained in:
@@ -5,12 +5,13 @@ import {
|
||||
applyGlobalSessionStatusEvents,
|
||||
applyGlobalSessionStatusSnapshot,
|
||||
useGlobalSessionStatusStore,
|
||||
replaceGlobalSessionStatusById,
|
||||
} from "./global-session-status"
|
||||
import { resetSessionOrdering, useSessionOrderingStore } from "./session-ordering"
|
||||
import { resetSessionActivityTiming, useSessionActivityTimingStore } from "./session-activity-timing"
|
||||
|
||||
beforeEach(() => {
|
||||
useGlobalSessionStatusStore.setState({ statusById: new Map() })
|
||||
replaceGlobalSessionStatusById(new Map())
|
||||
resetSessionOrdering()
|
||||
resetSessionActivityTiming()
|
||||
})
|
||||
@@ -162,7 +163,7 @@ describe("global session status index", () => {
|
||||
properties: { sessionID: "session-a", status: { type: "busy" } },
|
||||
} as Event)
|
||||
|
||||
useGlobalSessionStatusStore.setState({ statusById: new Map() })
|
||||
replaceGlobalSessionStatusById(new Map())
|
||||
|
||||
expect(activeSessionIds()?.size).toBe(0)
|
||||
})
|
||||
|
||||
@@ -40,61 +40,28 @@ const initialState: GlobalSessionStatusState = {
|
||||
export const useGlobalSessionStatusStore = create<GlobalSessionStatusState>(() => initialState);
|
||||
useGlobalSessionStatusStore.subscribe(() => countSyncPerformance('globalStatusPublications'));
|
||||
|
||||
// Runtime switching currently replaces statusById directly. Keep that boundary
|
||||
// synchronized without making normal status mutations derive membership again.
|
||||
const storeSetState = useGlobalSessionStatusStore.setState;
|
||||
type GlobalSessionStatusStateUpdate = GlobalSessionStatusState
|
||||
| Partial<GlobalSessionStatusState>
|
||||
| ((state: GlobalSessionStatusState) => GlobalSessionStatusState | Partial<GlobalSessionStatusState>);
|
||||
|
||||
function setSynchronizedState(
|
||||
partial: GlobalSessionStatusStateUpdate,
|
||||
replace?: false,
|
||||
): void;
|
||||
function setSynchronizedState(
|
||||
partial: GlobalSessionStatusState | ((state: GlobalSessionStatusState) => GlobalSessionStatusState),
|
||||
replace: true,
|
||||
): void;
|
||||
function setSynchronizedState(partial: GlobalSessionStatusStateUpdate, replace?: boolean): void {
|
||||
if (partial instanceof Function) {
|
||||
if (replace === true) {
|
||||
// SAFETY: Zustand's `replace: true` overload only accepts a complete state or a complete-state updater.
|
||||
storeSetState(partial as GlobalSessionStatusState | ((state: GlobalSessionStatusState) => GlobalSessionStatusState), true);
|
||||
} else {
|
||||
storeSetState(partial, replace);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (partial.statusById === undefined || partial.activeSessionIds) {
|
||||
if (replace === true) {
|
||||
// SAFETY: Zustand's `replace: true` overload only accepts a complete state or a complete-state updater.
|
||||
storeSetState(partial as GlobalSessionStatusState, true);
|
||||
} else {
|
||||
storeSetState(partial, replace);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const nextStatusById = partial.statusById;
|
||||
/**
|
||||
* Replaces the status map wholesale and derives active membership from it.
|
||||
* This is the ONE sanctioned way to swap statusById from outside the event
|
||||
* reducers (runtime switch, tests) — previously a setState monkeypatch
|
||||
* derived membership for arbitrary callers, which silently trusted any
|
||||
* caller passing both fields to keep them consistent.
|
||||
*/
|
||||
export const replaceGlobalSessionStatusById = (statusById: Map<string, GlobalSessionStatusEntry>): void => {
|
||||
const current = useGlobalSessionStatusStore.getState();
|
||||
const nextActiveSessionIds = new Set<string>();
|
||||
for (const [sessionId, entry] of nextStatusById) {
|
||||
for (const [sessionId, entry] of statusById) {
|
||||
if (entry.status.type === 'busy' || entry.status.type === 'retry') {
|
||||
nextActiveSessionIds.add(sessionId);
|
||||
}
|
||||
}
|
||||
const sameMembership = nextActiveSessionIds.size === current.activeSessionIds.size
|
||||
&& [...nextActiveSessionIds].every((sessionId) => current.activeSessionIds.has(sessionId));
|
||||
const nextState = {
|
||||
...current,
|
||||
...partial,
|
||||
useGlobalSessionStatusStore.setState({
|
||||
statusById,
|
||||
activeSessionIds: sameMembership ? current.activeSessionIds : nextActiveSessionIds,
|
||||
};
|
||||
if (replace === true) storeSetState(nextState, true);
|
||||
else storeSetState(nextState, replace);
|
||||
}
|
||||
|
||||
useGlobalSessionStatusStore.setState = setSynchronizedState;
|
||||
});
|
||||
};
|
||||
|
||||
const normalizeStatusType = (type: string | undefined): ActiveStatusType | 'idle' => {
|
||||
if (type === 'busy') return 'busy';
|
||||
|
||||
Reference in New Issue
Block a user