@@ -87,6 +87,13 @@ type ThreadDetailSubscriptionEntry = {
8787const environmentConnections = new Map < EnvironmentId , EnvironmentConnection > ( ) ;
8888const environmentConnectionListeners = new Set < ( ) => void > ( ) ;
8989const threadDetailSubscriptions = new Map < string , ThreadDetailSubscriptionEntry > ( ) ;
90+ const lastAppliedProjectionVersionByEnvironment = new Map <
91+ EnvironmentId ,
92+ {
93+ readonly sequence : number ;
94+ readonly updatedAt : string | null ;
95+ }
96+ > ( ) ;
9097
9198let activeService : EnvironmentServiceState | null = null ;
9299let needsProviderInvalidation = false ;
@@ -102,6 +109,98 @@ const THREAD_DETAIL_SUBSCRIPTION_IDLE_EVICTION_MS = 15 * 60 * 1000;
102109const MAX_CACHED_THREAD_DETAIL_SUBSCRIPTIONS = 32 ;
103110const NOOP = ( ) => undefined ;
104111
112+ function compareAppliedProjectionVersion (
113+ left : { readonly sequence : number ; readonly updatedAt : string | null } ,
114+ right : { readonly sequence : number ; readonly updatedAt : string | null } ,
115+ ) : number {
116+ if ( left . sequence !== right . sequence ) {
117+ return left . sequence - right . sequence ;
118+ }
119+
120+ const leftUpdatedAt = left . updatedAt ?? "" ;
121+ const rightUpdatedAt = right . updatedAt ?? "" ;
122+ if ( leftUpdatedAt === rightUpdatedAt ) {
123+ return 0 ;
124+ }
125+
126+ return leftUpdatedAt < rightUpdatedAt ? - 1 : 1 ;
127+ }
128+
129+ function toAppliedProjectionVersion (
130+ snapshot : Pick < OrchestrationShellSnapshot , "snapshotSequence" | "updatedAt" > ,
131+ ) : {
132+ readonly sequence : number ;
133+ readonly updatedAt : string ;
134+ } {
135+ return {
136+ sequence : snapshot . snapshotSequence ,
137+ updatedAt : snapshot . updatedAt ,
138+ } ;
139+ }
140+
141+ export function shouldApplyProjectionSnapshot ( input : {
142+ readonly current : {
143+ readonly sequence : number ;
144+ readonly updatedAt : string | null ;
145+ } | null ;
146+ readonly next : Pick < OrchestrationShellSnapshot , "snapshotSequence" | "updatedAt" > ;
147+ } ) : boolean {
148+ if ( input . current === null ) {
149+ return true ;
150+ }
151+
152+ return compareAppliedProjectionVersion ( input . current , toAppliedProjectionVersion ( input . next ) ) < 0 ;
153+ }
154+
155+ export function shouldApplyProjectionEvent ( input : {
156+ readonly current : {
157+ readonly sequence : number ;
158+ readonly updatedAt : string | null ;
159+ } | null ;
160+ readonly sequence : number ;
161+ } ) : boolean {
162+ if ( input . current === null ) {
163+ return true ;
164+ }
165+
166+ return input . sequence > input . current . sequence ;
167+ }
168+
169+ function readLastAppliedProjectionVersion ( environmentId : EnvironmentId ) : {
170+ readonly sequence : number ;
171+ readonly updatedAt : string | null ;
172+ } | null {
173+ return lastAppliedProjectionVersionByEnvironment . get ( environmentId ) ?? null ;
174+ }
175+
176+ function markAppliedProjectionSnapshot (
177+ environmentId : EnvironmentId ,
178+ snapshot : Pick < OrchestrationShellSnapshot , "snapshotSequence" | "updatedAt" > ,
179+ ) : void {
180+ const nextVersion = toAppliedProjectionVersion ( snapshot ) ;
181+ const currentVersion = readLastAppliedProjectionVersion ( environmentId ) ;
182+ if (
183+ currentVersion !== null &&
184+ compareAppliedProjectionVersion ( currentVersion , nextVersion ) >= 0
185+ ) {
186+ return ;
187+ }
188+
189+ lastAppliedProjectionVersionByEnvironment . set ( environmentId , nextVersion ) ;
190+ }
191+
192+ function markAppliedProjectionEvent ( environmentId : EnvironmentId , sequence : number ) : void {
193+ const currentVersion = readLastAppliedProjectionVersion ( environmentId ) ;
194+ if ( currentVersion !== null && sequence <= currentVersion . sequence ) {
195+ return ;
196+ }
197+
198+ lastAppliedProjectionVersionByEnvironment . set ( environmentId , {
199+ sequence,
200+ updatedAt : currentVersion ?. updatedAt ?? null ,
201+ } ) ;
202+ }
203+
105204function getThreadDetailSubscriptionKey ( environmentId : EnvironmentId , threadId : ThreadId ) : string {
106205 return scopedThreadKey ( scopeThreadRef ( environmentId , threadId ) ) ;
107206}
@@ -600,6 +699,15 @@ export function applyEnvironmentThreadDetailEvent(
600699}
601700
602701function applyShellEvent ( event : OrchestrationShellStreamEvent , environmentId : EnvironmentId ) {
702+ if (
703+ ! shouldApplyProjectionEvent ( {
704+ current : readLastAppliedProjectionVersion ( environmentId ) ,
705+ sequence : event . sequence ,
706+ } )
707+ ) {
708+ return ;
709+ }
710+
603711 const threadId =
604712 event . kind === "thread-upserted"
605713 ? event . thread . id
@@ -610,6 +718,7 @@ function applyShellEvent(event: OrchestrationShellStreamEvent, environmentId: En
610718 const previousThread = threadRef ? selectThreadByRef ( useStore . getState ( ) , threadRef ) : undefined ;
611719
612720 useStore . getState ( ) . applyShellEvent ( event , environmentId ) ;
721+ markAppliedProjectionEvent ( environmentId , event . sequence ) ;
613722
614723 switch ( event . kind ) {
615724 case "project-upserted" :
@@ -643,7 +752,17 @@ function createEnvironmentConnectionHandlers() {
643752 return {
644753 applyShellEvent,
645754 syncShellSnapshot : ( snapshot : OrchestrationShellSnapshot , environmentId : EnvironmentId ) => {
755+ if (
756+ ! shouldApplyProjectionSnapshot ( {
757+ current : readLastAppliedProjectionVersion ( environmentId ) ,
758+ next : snapshot ,
759+ } )
760+ ) {
761+ return ;
762+ }
763+
646764 useStore . getState ( ) . syncServerShellSnapshot ( snapshot , environmentId ) ;
765+ markAppliedProjectionSnapshot ( environmentId , snapshot ) ;
647766 reconcileThreadDetailSubscriptionsForEnvironment (
648767 environmentId ,
649768 snapshot . threads . map ( ( thread ) => thread . id ) ,
@@ -758,6 +877,7 @@ async function removeConnection(environmentId: EnvironmentId): Promise<boolean>
758877 }
759878
760879 disposeThreadDetailSubscriptionsForEnvironment ( environmentId ) ;
880+ lastAppliedProjectionVersionByEnvironment . delete ( environmentId ) ;
761881 environmentConnections . delete ( environmentId ) ;
762882 emitEnvironmentConnectionRegistryChange ( ) ;
763883 await connection . dispose ( ) ;
@@ -1086,6 +1206,7 @@ export function startEnvironmentConnectionService(queryClient: QueryClient): ()
10861206
10871207export async function resetEnvironmentServiceForTests ( ) : Promise < void > {
10881208 stopActiveService ( ) ;
1209+ lastAppliedProjectionVersionByEnvironment . clear ( ) ;
10891210 for ( const key of Array . from ( threadDetailSubscriptions . keys ( ) ) ) {
10901211 disposeThreadDetailSubscriptionByKey ( key ) ;
10911212 }
0 commit comments