|
1 | 1 | import { useCallback, useMemo } from "react"; |
2 | 2 | import { useStore } from "jotai"; |
3 | | -import { PERSONAL_PROJECT_ID, type ThreadListEntry } from "@bb/domain"; |
| 3 | +import { |
| 4 | + PERSONAL_PROJECT_ID, |
| 5 | + type Host, |
| 6 | + type ThreadListEntry, |
| 7 | +} from "@bb/domain"; |
4 | 8 | import { useIsCompactViewport } from "@bb/shared-ui/hooks/use-compact-viewport"; |
5 | 9 | import type { |
6 | 10 | PluginSidebarProject, |
@@ -31,6 +35,29 @@ import { |
31 | 35 | const EMPTY_THREADS: readonly PluginSidebarThread[] = []; |
32 | 36 | const EMPTY_PROJECTS: readonly PluginSidebarProject[] = []; |
33 | 37 | const EMPTY_ENTRIES: ReadonlyMap<string, ThreadListEntry> = new Map(); |
| 38 | +const EMPTY_HOST_NAMES: ReadonlyMap<string, string> = new Map(); |
| 39 | + |
| 40 | +/** |
| 41 | + * Host-name map per hosts payload. Module-level (not `useMemo`) so every |
| 42 | + * `useSidebarThreads` caller derives the same map object from the same React |
| 43 | + * Query result; a per-hook map would give two plugin lists two keys and make |
| 44 | + * them evict each other's entries from {@link pluginSidebarThreadByEntry}. |
| 45 | + */ |
| 46 | +const hostNamesByHosts = new WeakMap< |
| 47 | + readonly Host[], |
| 48 | + ReadonlyMap<string, string> |
| 49 | +>(); |
| 50 | + |
| 51 | +function hostNamesFor( |
| 52 | + hosts: readonly Host[] | undefined, |
| 53 | +): ReadonlyMap<string, string> { |
| 54 | + if (hosts === undefined) return EMPTY_HOST_NAMES; |
| 55 | + const cached = hostNamesByHosts.get(hosts); |
| 56 | + if (cached !== undefined) return cached; |
| 57 | + const names = new Map(hosts.map((host) => [host.id, host.name] as const)); |
| 58 | + hostNamesByHosts.set(hosts, names); |
| 59 | + return names; |
| 60 | +} |
34 | 61 |
|
35 | 62 | /** |
36 | 63 | * Per-entry DTO memo. React Query structurally shares the sidebar payload, so |
@@ -74,10 +101,7 @@ export function useSidebarThreads(): PluginSidebarThreadsState { |
74 | 101 | // The sidebar already subscribes to host updates; this reads the same |
75 | 102 | // cached list so a row can print a machine name instead of a host id. |
76 | 103 | const { data: hosts } = useHosts(); |
77 | | - const hostNamesById = useMemo( |
78 | | - () => new Map((hosts ?? []).map((host) => [host.id, host.name] as const)), |
79 | | - [hosts], |
80 | | - ); |
| 104 | + const hostNamesById = hostNamesFor(hosts); |
81 | 105 |
|
82 | 106 | return useMemo<PluginSidebarThreadsState>(() => { |
83 | 107 | if (data === undefined) { |
|
0 commit comments