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(...@@ -117,12 +117,26 @@ function settledTurnStateForSessionStatus(
117 }117 }
118}118}
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.
120function updateThread(122function updateThread(
121 threads: ReadonlyArray<OrchestrationThread>,123 threads: ReadonlyArray<OrchestrationThread>,
122 threadId: ThreadId,124 threadId: ThreadId,
123 patch: ThreadPatch,125 patch: ThreadPatch,
124): OrchestrationThread[] {126): ReadonlyArray<OrchestrationThread> {
125 return threads.map((thread) => (thread.id === threadId ? { ...thread, ...patch } : thread));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;
126}140}
127141
128/** Patch that swaps a thread's links and re-derives the legacy single-PR field from them. */142/** Patch that swaps a thread's links and re-derives the legacy single-PR field from them. */
...@@ -767,7 +781,8 @@ export function projectEvent(...@@ -767,7 +781,8 @@ export function projectEvent(
767 event.type,781 event.type,
768 "payload",782 "payload",
769 );783 );
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];
771 if (!thread) {786 if (!thread) {
772 return nextBase;787 return nextBase;
773 }788 }
...@@ -815,7 +830,7 @@ export function projectEvent(...@@ -815,7 +830,7 @@ export function projectEvent(
815830
816 return {831 return {
817 ...nextBase,832 ...nextBase,
818 threads: updateThread(nextBase.threads, payload.threadId, {833 threads: patchThreadAt(nextBase.threads, threadIndex, {
819 messages: cappedMessages,834 messages: cappedMessages,
820 updatedAt: event.occurredAt,835 updatedAt: event.occurredAt,
821 }),836 }),
...@@ -1055,7 +1070,8 @@ export function projectEvent(...@@ -1055,7 +1070,8 @@ export function projectEvent(
1055 "payload",1070 "payload",
1056 ).pipe(1071 ).pipe(
1057 Effect.map((payload) => {1072 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];
1059 if (!thread) {1075 if (!thread) {
1060 return nextBase;1076 return nextBase;
1061 }1077 }
...@@ -1069,7 +1085,7 @@ export function projectEvent(...@@ -1069,7 +1085,7 @@ export function projectEvent(
10691085
1070 return {1086 return {
1071 ...nextBase,1087 ...nextBase,
1072 threads: updateThread(nextBase.threads, payload.threadId, {1088 threads: patchThreadAt(nextBase.threads, threadIndex, {
1073 activities,1089 activities,
1074 updatedAt: event.occurredAt,1090 updatedAt: event.occurredAt,
1075 }),1091 }),