| ... | @@ -117,12 +117,26 @@ function settledTurnStateForSessionStatus( | ... | @@ -117,12 +117,26 @@ function settledTurnStateForSessionStatus( |
| 117 | } | 117 | } |
| 118 | } | 118 | } |
| 119 | | 119 | |
| | 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. |
| 120 | function updateThread( | 122 | function 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. */ |
| | 132 | function 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 | } |
| 127 | | 141 | |
| 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( |
| 815 | | 830 | |
| 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( |
| 1069 | | 1085 | |
| 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 | }), |