refactor(app): reshape session store reads around narrow hook surface
Eliminate sidebar re-render cascades by fixing both sides of the subscription contract: - Session-store writes preserve identity: setWorkspaces, mergeWorkspaces, removeWorkspace, and patchWorkspaceScripts return the previous Map/entry reference when content is unchanged. - HostRuntimeController.updateSnapshot only notifies when a patched field actually changed; idle probe ticks no longer replace the snapshot. - New canonical hook surface in stores/session-store-hooks.ts (useWorkspace, useWorkspaceFields, useWorkspaceStructure, useWorkspaceKeys, useResolveWorkspaceIdByCwd, useWorkspaceStatusesForBadges, useWorkspaceExecutionAuthority, useRecommendedProjectPaths, useHasWorkspaces). These are the only place to subscribe to workspaces. - 15 greedy subscribers migrated to the hook surface. The sidebar is now driven by useWorkspaceStructure for identity/ordering; each row hydrates its own descriptor via useWorkspaceFields, so a workspace update re-renders only its row. Archive-redirect callbacks switched to event-time useSessionStore.getState(). - Deleted dead code: use-session-directory.ts, workspace-fetch-debug.ts, and the unused SidebarProjectEntry aggregates (activeCount, totalWorkspaces, aggregated statusBucket). No user-visible behavior change. Targeted tests green (79 tests across host-runtime, session-store, session-store-hooks, sidebar list, sidebar row model, sidebar shortcuts, workspace source of truth).
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { useSyncExternalStore, useCallback, useMemo } from "react";
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import equal from "fast-deep-equal/es6";
|
||||
import {
|
||||
DaemonClient,
|
||||
type ConnectionState,
|
||||
@@ -67,6 +68,16 @@ export type HostRuntimeSnapshot = {
|
||||
clientGeneration: number;
|
||||
};
|
||||
|
||||
type HostRuntimeSnapshotPatch = Partial<Omit<HostRuntimeSnapshot, "serverId" | "clientGeneration">>;
|
||||
|
||||
function setSnapshotPatchField<Key extends keyof HostRuntimeSnapshotPatch>(
|
||||
patch: HostRuntimeSnapshotPatch,
|
||||
key: Key,
|
||||
value: HostRuntimeSnapshot[Key],
|
||||
): void {
|
||||
patch[key] = value;
|
||||
}
|
||||
|
||||
export function isHostRuntimeConnected(snapshot: HostRuntimeSnapshot | null): boolean {
|
||||
return snapshot?.connectionStatus === "online";
|
||||
}
|
||||
@@ -831,15 +842,25 @@ export class HostRuntimeController {
|
||||
});
|
||||
}
|
||||
|
||||
private updateSnapshot(
|
||||
patch: Partial<Omit<HostRuntimeSnapshot, "serverId" | "clientGeneration">>,
|
||||
): void {
|
||||
const next: HostRuntimeSnapshot = {
|
||||
private updateSnapshot(patch: HostRuntimeSnapshotPatch): void {
|
||||
const preservedPatch: HostRuntimeSnapshotPatch = { ...patch };
|
||||
let hasChanged = this.host.serverId !== this.snapshot.serverId;
|
||||
for (const key of Object.keys(patch) as Array<keyof HostRuntimeSnapshotPatch>) {
|
||||
const incomingValue = patch[key];
|
||||
if (equal(this.snapshot[key], incomingValue)) {
|
||||
setSnapshotPatchField(preservedPatch, key, this.snapshot[key]);
|
||||
continue;
|
||||
}
|
||||
hasChanged = true;
|
||||
}
|
||||
if (!hasChanged) {
|
||||
return;
|
||||
}
|
||||
this.snapshot = {
|
||||
...this.snapshot,
|
||||
...patch,
|
||||
...preservedPatch,
|
||||
serverId: this.host.serverId,
|
||||
};
|
||||
this.snapshot = next;
|
||||
for (const listener of this.listeners) {
|
||||
listener();
|
||||
}
|
||||
@@ -1021,7 +1042,7 @@ export class HostRuntimeController {
|
||||
state,
|
||||
lastError: client.lastError,
|
||||
});
|
||||
const patch: Partial<Omit<HostRuntimeSnapshot, "serverId" | "clientGeneration">> = {
|
||||
const patch: HostRuntimeSnapshotPatch = {
|
||||
...toSnapshotConnectionPatch(this.connectionMachineState),
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user