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,3 +1,4 @@
|
||||
import equal from "fast-deep-equal";
|
||||
import { create } from "zustand";
|
||||
import { subscribeWithSelector } from "zustand/middleware";
|
||||
import type { DaemonClient } from "@server/client/daemon-client";
|
||||
@@ -159,6 +160,41 @@ export function mergeWorkspaceSnapshotWithExisting(input: {
|
||||
};
|
||||
}
|
||||
|
||||
function preserveWorkspaceDescriptorIdentity(
|
||||
incoming: WorkspaceDescriptor,
|
||||
existing?: WorkspaceDescriptor | null,
|
||||
): WorkspaceDescriptor {
|
||||
if (existing && equal(existing, incoming)) {
|
||||
return existing;
|
||||
}
|
||||
return incoming;
|
||||
}
|
||||
|
||||
function preserveWorkspaceMapIdentity(
|
||||
existing: Map<string, WorkspaceDescriptor>,
|
||||
incoming: Map<string, WorkspaceDescriptor>,
|
||||
): Map<string, WorkspaceDescriptor> {
|
||||
if (existing === incoming) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
const next = new Map<string, WorkspaceDescriptor>();
|
||||
let changed = existing.size !== incoming.size;
|
||||
const existingEntries = existing.entries();
|
||||
|
||||
for (const [key, workspace] of incoming) {
|
||||
const existingWorkspace = existing.get(key);
|
||||
const nextWorkspace = preserveWorkspaceDescriptorIdentity(workspace, existingWorkspace);
|
||||
next.set(key, nextWorkspace);
|
||||
const existingEntry = existingEntries.next().value;
|
||||
if (!existingEntry || existingEntry[0] !== key || existingEntry[1] !== nextWorkspace) {
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
return changed ? next : existing;
|
||||
}
|
||||
|
||||
export type ExplorerEntryKind = "file" | "directory";
|
||||
export type ExplorerFileKind = "text" | "image" | "binary";
|
||||
export type ExplorerEncoding = "utf-8" | "base64" | "none";
|
||||
@@ -938,14 +974,18 @@ export const useSessionStore = create<SessionStore>()(
|
||||
}
|
||||
const nextWorkspaces =
|
||||
typeof workspaces === "function" ? workspaces(session.workspaces) : workspaces;
|
||||
if (session.workspaces === nextWorkspaces) {
|
||||
const preservedWorkspaces = preserveWorkspaceMapIdentity(
|
||||
session.workspaces,
|
||||
nextWorkspaces,
|
||||
);
|
||||
if (session.workspaces === preservedWorkspaces) {
|
||||
return prev;
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
sessions: {
|
||||
...prev.sessions,
|
||||
[serverId]: { ...session, workspaces: nextWorkspaces },
|
||||
[serverId]: { ...session, workspaces: preservedWorkspaces },
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -962,13 +1002,15 @@ export const useSessionStore = create<SessionStore>()(
|
||||
let changed = false;
|
||||
for (const workspace of nextEntries) {
|
||||
const existing = next.get(workspace.id);
|
||||
if (existing === workspace) {
|
||||
const mergedWorkspace = mergeWorkspaceSnapshotWithExisting({
|
||||
incoming: workspace,
|
||||
existing,
|
||||
});
|
||||
const nextWorkspace = preserveWorkspaceDescriptorIdentity(mergedWorkspace, existing);
|
||||
if (existing === nextWorkspace) {
|
||||
continue;
|
||||
}
|
||||
next.set(
|
||||
workspace.id,
|
||||
mergeWorkspaceSnapshotWithExisting({ incoming: workspace, existing }),
|
||||
);
|
||||
next.set(workspace.id, nextWorkspace);
|
||||
changed = true;
|
||||
}
|
||||
if (!changed) {
|
||||
|
||||
Reference in New Issue
Block a user