Skip to content

Commit c83a797

Browse files
committed
Extract shared project ordering util and memoize selector factories
- Extract orderByPriority() in lib/utils.ts to deduplicate the project ordering logic that was independently implemented in both useHandleNewThread.ts and Sidebar.tsx. - Memoize selectProjectById and selectThreadById with a simple Map cache so the same argument always returns the same selector reference, allowing Zustand's fast-path identity check to skip re-running the selector on every store notification.
1 parent 314edce commit c83a797

File tree

4 files changed

+69
-32
lines changed

4 files changed

+69
-32
lines changed

apps/web/src/components/Sidebar.tsx

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,13 @@ import {
5454
import { isElectron } from "../env";
5555
import { APP_STAGE_LABEL, APP_VERSION } from "../branding";
5656
import { isTerminalFocused } from "../lib/terminalFocus";
57-
import { isLinuxPlatform, isMacPlatform, newCommandId, newProjectId } from "../lib/utils";
57+
import {
58+
isLinuxPlatform,
59+
isMacPlatform,
60+
newCommandId,
61+
newProjectId,
62+
orderByPriority,
63+
} from "../lib/utils";
5864
import { useStore } from "../store";
5965
import { useUiStateStore } from "../uiStateStore";
6066
import {
@@ -500,18 +506,10 @@ export default function Sidebar() {
500506
const platform = navigator.platform;
501507
const shouldBrowseForProjectImmediately = isElectron && !isLinuxDesktop;
502508
const shouldShowProjectPathEntry = addingProject && !shouldBrowseForProjectImmediately;
503-
const orderedProjects = useMemo(() => {
504-
if (projectOrder.length === 0) {
505-
return projects;
506-
}
507-
const projectsById = new Map(projects.map((project) => [project.id, project] as const));
508-
const ordered = projectOrder.flatMap((projectId) => {
509-
const project = projectsById.get(projectId);
510-
return project ? [project] : [];
511-
});
512-
const remaining = projects.filter((project) => !projectOrder.includes(project.id));
513-
return [...ordered, ...remaining];
514-
}, [projectOrder, projects]);
509+
const orderedProjects = useMemo(
510+
() => orderByPriority(projects, projectOrder, (p) => p.id),
511+
[projectOrder, projects],
512+
);
515513
const sidebarProjects = useMemo<SidebarProjectSnapshot[]>(
516514
() =>
517515
orderedProjects.map((project) => ({

apps/web/src/hooks/useHandleNewThread.ts

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
type DraftThreadState,
88
useComposerDraftStore,
99
} from "../composerDraftStore";
10-
import { newThreadId } from "../lib/utils";
10+
import { newThreadId, orderByPriority } from "../lib/utils";
1111
import { selectThreadById, useStore } from "../store";
1212
import { useUiStateStore } from "../uiStateStore";
1313

@@ -23,15 +23,10 @@ export function useHandleNewThread() {
2323
const activeDraftThread = useComposerDraftStore((store) =>
2424
routeThreadId ? (store.draftThreadsByThreadId[routeThreadId] ?? null) : null,
2525
);
26-
const orderedProjects = useMemo(() => {
27-
if (projectOrder.length === 0) {
28-
return projectIds;
29-
}
30-
const projectIdsSet = new Set(projectIds);
31-
const ordered = projectOrder.filter((projectId) => projectIdsSet.has(projectId));
32-
const remaining = projectIds.filter((projectId) => !projectOrder.includes(projectId));
33-
return [...ordered, ...remaining];
34-
}, [projectIds, projectOrder]);
26+
const orderedProjects = useMemo(
27+
() => orderByPriority(projectIds, projectOrder, (id) => id),
28+
[projectIds, projectOrder],
29+
);
3530

3631
const handleNewThread = useCallback(
3732
(

apps/web/src/lib/utils.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,23 @@ export const newProjectId = (): ProjectId => ProjectId.makeUnsafe(randomUUID());
3434
export const newThreadId = (): ThreadId => ThreadId.makeUnsafe(randomUUID());
3535

3636
export const newMessageId = (): MessageId => MessageId.makeUnsafe(randomUUID());
37+
38+
/**
39+
* Reorder `items` so that those whose key appears in `orderedKeys` come first
40+
* (in `orderedKeys` order), followed by the remaining items in their original order.
41+
*/
42+
export function orderByPriority<T>(
43+
items: readonly T[],
44+
orderedKeys: readonly string[],
45+
getKey: (item: T) => string,
46+
): T[] {
47+
if (orderedKeys.length === 0) return items.slice();
48+
const itemsByKey = new Map(items.map((item) => [getKey(item), item] as const));
49+
const ordered = orderedKeys.flatMap((key) => {
50+
const item = itemsByKey.get(key);
51+
return item ? [item] : [];
52+
});
53+
const orderedKeySet = new Set(orderedKeys);
54+
const remaining = items.filter((item) => !orderedKeySet.has(getKey(item)));
55+
return [...ordered, ...remaining];
56+
}

apps/web/src/store.ts

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -852,15 +852,39 @@ export function applyOrchestrationEvents(
852852
return events.reduce((nextState, event) => applyOrchestrationEvent(nextState, event), state);
853853
}
854854

855-
export const selectProjectById =
856-
(projectId: Project["id"] | null | undefined) =>
857-
(state: AppState): Project | undefined =>
858-
projectId ? state.projects.find((project) => project.id === projectId) : undefined;
859-
860-
export const selectThreadById =
861-
(threadId: ThreadId | null | undefined) =>
862-
(state: AppState): Thread | undefined =>
863-
threadId ? state.threads.find((thread) => thread.id === threadId) : undefined;
855+
const _projectSelectorCache = new Map<
856+
string | null | undefined,
857+
(state: AppState) => Project | undefined
858+
>();
859+
export function selectProjectById(
860+
projectId: Project["id"] | null | undefined,
861+
): (state: AppState) => Project | undefined {
862+
const key = projectId ?? null;
863+
let selector = _projectSelectorCache.get(key);
864+
if (!selector) {
865+
selector = (state: AppState) =>
866+
projectId ? state.projects.find((project) => project.id === projectId) : undefined;
867+
_projectSelectorCache.set(key, selector);
868+
}
869+
return selector;
870+
}
871+
872+
const _threadSelectorCache = new Map<
873+
string | null | undefined,
874+
(state: AppState) => Thread | undefined
875+
>();
876+
export function selectThreadById(
877+
threadId: ThreadId | null | undefined,
878+
): (state: AppState) => Thread | undefined {
879+
const key = threadId ?? null;
880+
let selector = _threadSelectorCache.get(key);
881+
if (!selector) {
882+
selector = (state: AppState) =>
883+
threadId ? state.threads.find((thread) => thread.id === threadId) : undefined;
884+
_threadSelectorCache.set(key, selector);
885+
}
886+
return selector;
887+
}
864888

865889
export function setError(state: AppState, threadId: ThreadId, error: string | null): AppState {
866890
const threads = updateThread(state.threads, threadId, (t) => {

0 commit comments

Comments
 (0)