@@ -43,8 +43,13 @@ export default class FakeLoopbackNetworkModel {
4343 this . replicationTick = 0 ;
4444 this . authoritativeFrame = 0 ;
4545 this . localFrame = 0 ;
46+ this . predictedFrame = 0 ;
47+ this . predictionLeadFrames = 0 ;
4648 this . lastReconcileAtMs = null ;
4749 this . reconciliationCount = 0 ;
50+ this . predictionCount = 0 ;
51+ this . lastPredictedSequence = 0 ;
52+ this . predictionRecords = [ ] ;
4853
4954 this . autoPacketTimerSeconds = 0 ;
5055 this . autoPacketIntervalSeconds = 0.65 ;
@@ -148,9 +153,34 @@ export default class FakeLoopbackNetworkModel {
148153 oneWayDelayMs
149154 } ) ;
150155
156+ this . applyLocalPrediction ( sequence , label ) ;
157+
151158 return true ;
152159 }
153160
161+ applyLocalPrediction ( sequence , label ) {
162+ const nextPredicted = this . predictedFrame + 1 ;
163+ this . predictedFrame = nextPredicted ;
164+ this . localFrame = Math . max ( this . localFrame , nextPredicted ) ;
165+ this . lastPredictedSequence = Math . max ( this . lastPredictedSequence , Number ( sequence ) || 0 ) ;
166+ this . predictionCount += 1 ;
167+ this . predictionRecords . push ( {
168+ sequence : Number ( sequence ) || 0 ,
169+ frame : nextPredicted ,
170+ label : String ( label || "" ) ,
171+ timestampMs : Math . round ( this . elapsedSeconds * 1000 )
172+ } ) ;
173+ if ( this . predictionRecords . length > 128 ) {
174+ const trimCount = this . predictionRecords . length - 128 ;
175+ this . predictionRecords . splice ( 0 , trimCount ) ;
176+ }
177+
178+ this . pushTrace ( "PREDICTION_APPLIED" , {
179+ sequence : Number ( sequence ) || 0 ,
180+ predictedFrame : nextPredicted
181+ } ) ;
182+ }
183+
154184 updatePhaseProgress ( dtSeconds ) {
155185 if ( this . phase === "disconnected" ) {
156186 return ;
@@ -210,35 +240,60 @@ export default class FakeLoopbackNetworkModel {
210240 }
211241
212242 updateDivergenceState ( ) {
213- this . authoritativeFrame = Math . max ( 0 , Number ( this . replicationTick ) || 0 ) ;
243+ this . authoritativeFrame = Math . max ( 0 , Number ( this . ackedSequence ) || 0 ) ;
214244 const ackedFloor = Math . max ( 0 , Number ( this . ackedSequence ) || 0 ) ;
215245 if ( this . localFrame < ackedFloor ) {
216246 this . localFrame = ackedFloor ;
217247 }
218- this . localFrame = clamp ( this . localFrame , 0 , this . authoritativeFrame ) ;
248+ this . predictedFrame = Math . max ( this . predictedFrame , this . localFrame ) ;
249+ this . predictionLeadFrames = Math . max ( 0 , this . localFrame - this . authoritativeFrame ) ;
219250 }
220251
221252 applyReconciliation ( ) {
222253 if ( this . phase !== "connected" ) {
223254 return ;
224255 }
225256
226- const divergence = Math . max ( 0 , this . authoritativeFrame - this . localFrame ) ;
257+ const delta = this . localFrame - this . authoritativeFrame ;
227258 const stableWindow = 2 ;
228- if ( divergence <= stableWindow ) {
259+ const maxLead = 6 ;
260+
261+ if ( delta >= 0 && delta <= maxLead ) {
262+ this . predictionLeadFrames = delta ;
229263 return ;
230264 }
231265
232- const correction = Math . min ( 2 , divergence - stableWindow ) ;
233- this . localFrame = clamp ( this . localFrame + correction , 0 , this . authoritativeFrame ) ;
266+ if ( delta < 0 ) {
267+ const correctionForward = Math . min ( 2 , Math . abs ( delta ) ) ;
268+ this . localFrame += correctionForward ;
269+ this . predictedFrame = Math . max ( this . predictedFrame , this . localFrame ) ;
270+ this . reconciliationCount += 1 ;
271+ this . lastReconcileAtMs = Math . round ( this . elapsedSeconds * 1000 ) ;
272+ this . pushTrace ( "RECONCILE_APPLIED" , {
273+ correctionFrames : correctionForward ,
274+ correctionDirection : "forward" ,
275+ authoritativeFrame : this . authoritativeFrame ,
276+ localFrame : this . localFrame ,
277+ divergenceAfter : this . localFrame - this . authoritativeFrame
278+ } ) ;
279+ return ;
280+ }
281+
282+ const excess = Math . max ( 0 , delta - maxLead ) ;
283+ const correctionBack = Math . min ( 2 , excess || Math . max ( 0 , delta - stableWindow ) ) ;
284+ if ( correctionBack <= 0 ) {
285+ return ;
286+ }
287+ this . localFrame = Math . max ( this . authoritativeFrame , this . localFrame - correctionBack ) ;
234288 this . reconciliationCount += 1 ;
235289 this . lastReconcileAtMs = Math . round ( this . elapsedSeconds * 1000 ) ;
236290
237291 this . pushTrace ( "RECONCILE_APPLIED" , {
238- correctionFrames : correction ,
292+ correctionFrames : correctionBack ,
293+ correctionDirection : "backward" ,
239294 authoritativeFrame : this . authoritativeFrame ,
240295 localFrame : this . localFrame ,
241- divergenceAfter : Math . max ( 0 , this . authoritativeFrame - this . localFrame )
296+ divergenceAfter : this . localFrame - this . authoritativeFrame
242297 } ) ;
243298 }
244299
@@ -261,9 +316,10 @@ export default class FakeLoopbackNetworkModel {
261316 getSnapshot ( ) {
262317 const traceTail = this . traceEvents . slice ( - 18 ) ;
263318
264- const frameDelta = Math . max ( 0 , this . authoritativeFrame - this . localFrame ) ;
265- const divergenceStatus = frameDelta > 8 ? "diverged" : ( frameDelta > 2 ? "drifting" : "stable" ) ;
266- const divergenceScore = frameDelta > 4 ? "warning" : "ok" ;
319+ const frameDelta = this . localFrame - this . authoritativeFrame ;
320+ const absoluteDelta = Math . abs ( frameDelta ) ;
321+ const divergenceStatus = absoluteDelta > 8 ? "diverged" : ( absoluteDelta > 2 ? "drifting" : "stable" ) ;
322+ const divergenceScore = absoluteDelta > 4 ? "warning" : "ok" ;
267323
268324 return {
269325 sessionId : this . sessionId ,
@@ -288,6 +344,10 @@ export default class FakeLoopbackNetworkModel {
288344 backlog : this . replicationBacklog ,
289345 authoritativeFrame : this . authoritativeFrame ,
290346 localFrame : this . localFrame ,
347+ predictedFrame : this . predictedFrame ,
348+ predictionLeadFrames : this . predictionLeadFrames ,
349+ predictionCount : this . predictionCount ,
350+ lastPredictedSequence : this . lastPredictedSequence ,
291351 reconciliationCount : this . reconciliationCount ,
292352 lastReconcileAtMs : this . lastReconcileAtMs
293353 } ,
0 commit comments