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:
Mohamed Boudra
2026-04-19 20:19:45 +07:00
parent 8991d34f78
commit bd1fb311ea
27 changed files with 1797 additions and 623 deletions
@@ -12,6 +12,7 @@ import {
HostRuntimeController,
HostRuntimeStore,
type HostRuntimeControllerDeps,
type HostRuntimeSnapshot,
} from "./host-runtime";
class FakeDaemonClient {
@@ -264,6 +265,27 @@ function clearProbeBackoff(controller: HostRuntimeController): void {
).connectionLastProbedAt.clear();
}
type HostRuntimeSnapshotPatch = Partial<Omit<HostRuntimeSnapshot, "serverId" | "clientGeneration">>;
function updateControllerSnapshot(
controller: HostRuntimeController,
patch: HostRuntimeSnapshotPatch,
): void {
(
controller as unknown as {
updateSnapshot: (patch: HostRuntimeSnapshotPatch) => void;
}
).updateSnapshot(patch);
}
function makeProbeMap(
entries: Array<
[string, HostRuntimeSnapshot["probeByConnectionId"] extends Map<string, infer T> ? T : never]
>,
): HostRuntimeSnapshot["probeByConnectionId"] {
return new Map(entries);
}
describe("HostRuntimeController", () => {
it("keeps known hosts in connecting when a created client reports idle during connect", async () => {
const host = makeHost({
@@ -923,6 +945,134 @@ describe("HostRuntimeController", () => {
expect(controller.getSnapshot().clientGeneration).toBe(generationBeforeProbes);
expect(createdClients).toHaveLength(0);
});
it("does not notify or replace the snapshot for equal probe maps", () => {
const controller = new HostRuntimeController({ host: makeHost() });
const firstProbeMap = makeProbeMap([
["direct:lan:6767", { status: "available", latencyMs: 12 }],
]);
const equalProbeMap = makeProbeMap([
["direct:lan:6767", { status: "available", latencyMs: 12 }],
]);
updateControllerSnapshot(controller, { probeByConnectionId: firstProbeMap });
const snapshotAfterFirstProbe = controller.getSnapshot();
let notifyCount = 0;
const unsubscribe = controller.subscribe(() => {
notifyCount += 1;
});
updateControllerSnapshot(controller, { probeByConnectionId: equalProbeMap });
expect(notifyCount).toBe(0);
expect(controller.getSnapshot()).toBe(snapshotAfterFirstProbe);
expect(controller.getSnapshot().probeByConnectionId).toBe(firstProbeMap);
unsubscribe();
});
it("does not notify or replace the snapshot when connection status is already equal", () => {
const controller = new HostRuntimeController({ host: makeHost() });
updateControllerSnapshot(controller, { connectionStatus: "online" });
const snapshotAfterOnline = controller.getSnapshot();
let notifyCount = 0;
const unsubscribe = controller.subscribe(() => {
notifyCount += 1;
});
updateControllerSnapshot(controller, { connectionStatus: "online" });
expect(notifyCount).toBe(0);
expect(controller.getSnapshot()).toBe(snapshotAfterOnline);
unsubscribe();
});
it("does not notify or replace the snapshot when every patched field is equal", () => {
const controller = new HostRuntimeController({ host: makeHost() });
const firstProbeMap = makeProbeMap([
["direct:lan:6767", { status: "available", latencyMs: 12 }],
]);
const equalProbeMap = makeProbeMap([
["direct:lan:6767", { status: "available", latencyMs: 12 }],
]);
updateControllerSnapshot(controller, {
connectionStatus: "online",
probeByConnectionId: firstProbeMap,
});
const snapshotAfterSetup = controller.getSnapshot();
let notifyCount = 0;
const unsubscribe = controller.subscribe(() => {
notifyCount += 1;
});
updateControllerSnapshot(controller, {
connectionStatus: "online",
probeByConnectionId: equalProbeMap,
});
expect(notifyCount).toBe(0);
expect(controller.getSnapshot()).toBe(snapshotAfterSetup);
expect(controller.getSnapshot().probeByConnectionId).toBe(firstProbeMap);
unsubscribe();
});
it("notifies once for a changed field while preserving equal field identity", () => {
const controller = new HostRuntimeController({ host: makeHost() });
const firstProbeMap = makeProbeMap([
["direct:lan:6767", { status: "available", latencyMs: 12 }],
]);
const equalProbeMap = makeProbeMap([
["direct:lan:6767", { status: "available", latencyMs: 12 }],
]);
updateControllerSnapshot(controller, {
connectionStatus: "online",
probeByConnectionId: firstProbeMap,
});
const snapshotBeforeChange = controller.getSnapshot();
let notifyCount = 0;
const unsubscribe = controller.subscribe(() => {
notifyCount += 1;
});
updateControllerSnapshot(controller, {
connectionStatus: "offline",
probeByConnectionId: equalProbeMap,
});
expect(notifyCount).toBe(1);
expect(controller.getSnapshot()).not.toBe(snapshotBeforeChange);
expect(controller.getSnapshot().connectionStatus).toBe("offline");
expect(controller.getSnapshot().probeByConnectionId).toBe(firstProbeMap);
unsubscribe();
});
it("notifies once and replaces the probe map when probe contents change", () => {
const controller = new HostRuntimeController({ host: makeHost() });
const firstProbeMap = makeProbeMap([
["direct:lan:6767", { status: "available", latencyMs: 12 }],
]);
const changedProbeMap = makeProbeMap([
["direct:lan:6767", { status: "available", latencyMs: 12 }],
["relay:relay.paseo.sh:443", { status: "unavailable", latencyMs: null }],
]);
updateControllerSnapshot(controller, { probeByConnectionId: firstProbeMap });
const snapshotBeforeChange = controller.getSnapshot();
let notifyCount = 0;
const unsubscribe = controller.subscribe(() => {
notifyCount += 1;
});
updateControllerSnapshot(controller, { probeByConnectionId: changedProbeMap });
expect(notifyCount).toBe(1);
expect(controller.getSnapshot()).not.toBe(snapshotBeforeChange);
expect(controller.getSnapshot().probeByConnectionId).toBe(changedProbeMap);
expect(controller.getSnapshot().probeByConnectionId).not.toBe(firstProbeMap);
unsubscribe();
});
});
describe("HostRuntimeStore", () => {
+28 -7
View File
@@ -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),
};