authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-09-25 19:11:58-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-25 19:11:58-07:00
logc216ba4dade683bf3a7c96bf63b33a85afc1a16c
treefb5024d59378d3dcdd5865d8274e0caa8c48b186
parent2a9832b8019039f812706639c11ccc24ce4c4e05
signature Signed by PGP key B5690EEEBB952194

perf(server): stop remapping every thread on each thread event (#13720)

Co-authored-by: Claude <noreply@anthropic.com>

1 files changed, 22 insertions(+), 6 deletions(-)

apps/server/src/orchestration/projector.ts+22-6
......@@ -117,12 +117,26 @@ function settledTurnStateForSessionStatus(
117117 }
118118}
119119
120// Runs for every thread event (including streaming deltas) against every
121// thread the server has ever seen, so copy the array rather than map it.
120122function updateThread(
121123 threads: ReadonlyArray<OrchestrationThread>,
122124 threadId: ThreadId,
123125 patch: ThreadPatch,
124): OrchestrationThread[] {
125 return threads.map((thread) => (thread.id === threadId ? { ...thread, ...patch } : thread));
126): ReadonlyArray<OrchestrationThread> {
127 const index = threads.findIndex((thread) => thread.id === threadId);
128 return index === -1 ? threads : patchThreadAt(threads, index, patch);
129}
130
131/** For callers that already located the thread and must not scan again. */
132function patchThreadAt(
133 threads: ReadonlyArray<OrchestrationThread>,
134 index: number,
135 patch: ThreadPatch,
136): ReadonlyArray<OrchestrationThread> {
137 const next = threads.slice();
138 next[index] = { ...threads[index]!, ...patch };
139 return next;
126140}
127141
128142/** Patch that swaps a thread's links and re-derives the legacy single-PR field from them. */
......@@ -767,7 +781,8 @@ export function projectEvent(
767781 event.type,
768782 "payload",
769783 );
770 const thread = nextBase.threads.find((entry) => entry.id === payload.threadId);
784 const threadIndex = nextBase.threads.findIndex((entry) => entry.id === payload.threadId);
785 const thread = nextBase.threads[threadIndex];
771786 if (!thread) {
772787 return nextBase;
773788 }
......@@ -815,7 +830,7 @@ export function projectEvent(
815830
816831 return {
817832 ...nextBase,
818 threads: updateThread(nextBase.threads, payload.threadId, {
833 threads: patchThreadAt(nextBase.threads, threadIndex, {
819834 messages: cappedMessages,
820835 updatedAt: event.occurredAt,
821836 }),
......@@ -1055,7 +1070,8 @@ export function projectEvent(
10551070 "payload",
10561071 ).pipe(
10571072 Effect.map((payload) => {
1058 const thread = nextBase.threads.find((entry) => entry.id === payload.threadId);
1073 const threadIndex = nextBase.threads.findIndex((entry) => entry.id === payload.threadId);
1074 const thread = nextBase.threads[threadIndex];
10591075 if (!thread) {
10601076 return nextBase;
10611077 }
......@@ -1069,7 +1085,7 @@ export function projectEvent(
10691085
10701086 return {
10711087 ...nextBase,
1072 threads: updateThread(nextBase.threads, payload.threadId, {
1088 threads: patchThreadAt(nextBase.threads, threadIndex, {
10731089 activities,
10741090 updatedAt: event.occurredAt,
10751091 }),