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:
@@ -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", () => {
|
||||
|
||||
@@ -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