@@ -1282,7 +1282,7 @@ export class ContainerRuntime
1282
1282
1283
1283
/**
1284
1284
* Invokes the given callback and expects that no ops are submitted
1285
- * until execution finishes. If an op is submitted, an error will be raised .
1285
+ * until execution finishes. If an op is submitted, it will be marked as reentrant .
1286
1286
*
1287
1287
* @param callback - the callback to be invoked
1288
1288
*/
@@ -1306,7 +1306,7 @@ export class ContainerRuntime
1306
1306
return this . _disposed ;
1307
1307
}
1308
1308
1309
- private dirtyContainer : boolean ;
1309
+ private lastEmittedDirty : boolean ;
1310
1310
private emitDirtyDocumentEvent = true ;
1311
1311
private readonly useDeltaManagerOpsProxy : boolean ;
1312
1312
private readonly closeSummarizerDelayMs : number ;
@@ -1532,8 +1532,8 @@ export class ContainerRuntime
1532
1532
this . mc . logger . sendTelemetryEvent ( {
1533
1533
eventName : "Attached" ,
1534
1534
details : {
1535
- dirtyContainer : this . dirtyContainer ,
1536
- hasPendingMessages : this . hasPendingMessages ( ) ,
1535
+ lastEmittedDirty : this . lastEmittedDirty ,
1536
+ currentDirtyState : this . computeCurrentDirtyState ( ) ,
1537
1537
} ,
1538
1538
} ) ;
1539
1539
} ) ;
@@ -1897,9 +1897,9 @@ export class ContainerRuntime
1897
1897
this . closeSummarizerDelayMs =
1898
1898
closeSummarizerDelayOverride ?? defaultCloseSummarizerDelayMs ;
1899
1899
1900
- this . dirtyContainer =
1901
- this . attachState !== AttachState . Attached || this . hasPendingMessages ( ) ;
1902
- context . updateDirtyContainerState ( this . dirtyContainer ) ;
1900
+ // We haven't emitted dirty/saved yet, but this is the baseline so we know to emit when it changes
1901
+ this . lastEmittedDirty = this . computeCurrentDirtyState ( ) ;
1902
+ context . updateDirtyContainerState ( this . lastEmittedDirty ) ;
1903
1903
1904
1904
if ( ! this . skipSafetyFlushDuringProcessStack ) {
1905
1905
// Reference Sequence Number may have just changed, and it must be consistent across a batch,
@@ -2528,17 +2528,12 @@ export class ContainerRuntime
2528
2528
return ;
2529
2529
}
2530
2530
2531
- // We need to temporary clear the dirty flags and disable
2532
- // dirty state change events to detect whether replaying ops
2533
- // has any effect.
2534
-
2535
- // Save the old state, reset to false, disable event emit
2536
- const oldState = this . dirtyContainer ;
2537
- this . dirtyContainer = false ;
2538
-
2531
+ // Replaying is an internal operation and we don't want to generate noise while doing it.
2532
+ // So temporarily disable dirty state change events, and save the old state.
2533
+ // When we're done, we'll emit the event if the state changed.
2534
+ const oldState = this . lastEmittedDirty ;
2539
2535
assert ( this . emitDirtyDocumentEvent , 0x127 /* "dirty document event not set on replay" */ ) ;
2540
2536
this . emitDirtyDocumentEvent = false ;
2541
- let newState : boolean ;
2542
2537
2543
2538
try {
2544
2539
// Any ID Allocation ops that failed to submit after the pending state was queued need to have
@@ -2550,14 +2545,13 @@ export class ContainerRuntime
2550
2545
// replay the ops
2551
2546
this . pendingStateManager . replayPendingStates ( ) ;
2552
2547
} finally {
2553
- // Save the new start and restore the old state, re-enable event emit
2554
- newState = this . dirtyContainer ;
2555
- this . dirtyContainer = oldState ;
2548
+ // Restore the old state, re-enable event emit
2549
+ this . lastEmittedDirty = oldState ;
2556
2550
this . emitDirtyDocumentEvent = true ;
2557
2551
}
2558
2552
2559
- // Officially transition from the old state to the new state.
2560
- this . updateDocumentDirtyState ( newState ) ;
2553
+ // This will emit an event if the state changed relative to before replay
2554
+ this . updateDocumentDirtyState ( ) ;
2561
2555
}
2562
2556
2563
2557
/**
@@ -2928,6 +2922,9 @@ export class ContainerRuntime
2928
2922
runtimeBatch : boolean ,
2929
2923
groupedBatch : boolean ,
2930
2924
) : void {
2925
+ // This message could have been the last pending local (dirtyable) message, in which case we need to update dirty state to "saved"
2926
+ this . updateDocumentDirtyState ( ) ;
2927
+
2931
2928
if ( locationInBatch . batchStart ) {
2932
2929
const firstMessage = messagesWithMetadata [ 0 ] ?. message ;
2933
2930
assert ( firstMessage !== undefined , 0xa31 /* Batch must have at least one message */ ) ;
@@ -3043,12 +3040,6 @@ export class ContainerRuntime
3043
3040
3044
3041
this . _processedClientSequenceNumber = message . clientSequenceNumber ;
3045
3042
3046
- // If there are no more pending messages after processing a local message,
3047
- // the document is no longer dirty.
3048
- if ( ! this . hasPendingMessages ( ) ) {
3049
- this . updateDocumentDirtyState ( false ) ;
3050
- }
3051
-
3052
3043
// The DeltaManager used to do this, but doesn't anymore as of Loader v2.4
3053
3044
// Anyone listening to our "op" event would expect the contents to be parsed per this same logic
3054
3045
if (
@@ -3079,12 +3070,6 @@ export class ContainerRuntime
3079
3070
local : boolean ,
3080
3071
savedOp ?: boolean ,
3081
3072
) : void {
3082
- // If there are no more pending messages after processing a local message,
3083
- // the document is no longer dirty.
3084
- if ( ! this . hasPendingMessages ( ) ) {
3085
- this . updateDocumentDirtyState ( false ) ;
3086
- }
3087
-
3088
3073
// Get the contents without the localOpMetadata because not all message types know about localOpMetadata.
3089
3074
const contents = messagesContent . map ( ( c ) => c . contents ) ;
3090
3075
@@ -3239,7 +3224,6 @@ export class ContainerRuntime
3239
3224
*/
3240
3225
public orderSequentially < T > ( callback : ( ) => T ) : T {
3241
3226
let checkpoint : IBatchCheckpoint | undefined ;
3242
- const checkpointDirtyState = this . dirtyContainer ;
3243
3227
// eslint-disable-next-line import/no-deprecated
3244
3228
let stageControls : StageControlsExperimental | undefined ;
3245
3229
if ( this . mc . config . getBoolean ( "Fluid.ContainerRuntime.EnableRollback" ) ) {
@@ -3261,10 +3245,7 @@ export class ContainerRuntime
3261
3245
checkpoint . rollback ( ( message : LocalBatchMessage ) =>
3262
3246
this . rollback ( message . runtimeOp , message . localOpMetadata ) ,
3263
3247
) ;
3264
- // reset the dirty state after rollback to what it was before to keep it consistent
3265
- if ( this . dirtyContainer !== checkpointDirtyState ) {
3266
- this . updateDocumentDirtyState ( checkpointDirtyState ) ;
3267
- }
3248
+ this . updateDocumentDirtyState ( ) ;
3268
3249
stageControls ?. discardChanges ( ) ;
3269
3250
stageControls = undefined ;
3270
3251
} catch ( error_ ) {
@@ -3360,9 +3341,7 @@ export class ContainerRuntime
3360
3341
) ;
3361
3342
this . rollback ( runtimeOp , localOpMetadata ) ;
3362
3343
} ) ;
3363
- if ( this . attachState === AttachState . Attached ) {
3364
- this . updateDocumentDirtyState ( this . pendingMessagesCount !== 0 ) ;
3365
- }
3344
+ this . updateDocumentDirtyState ( ) ;
3366
3345
} ) ,
3367
3346
commitChanges : ( optionsParam ) => {
3368
3347
const options = { ...defaultStagingCommitOptions , ...optionsParam } ;
@@ -3474,40 +3453,20 @@ export class ContainerRuntime
3474
3453
* either were not sent out to delta stream or were not yet acknowledged.
3475
3454
*/
3476
3455
public get isDirty ( ) : boolean {
3477
- return this . dirtyContainer ;
3456
+ // Rather than recomputing the dirty state in this moment,
3457
+ // just regurgitate the last emitted dirty state.
3458
+ return this . lastEmittedDirty ;
3478
3459
}
3479
3460
3480
- private isContainerMessageDirtyable ( {
3481
- type,
3482
- contents,
3483
- } : LocalContainerRuntimeMessage ) : boolean {
3484
- // Certain container runtime messages should not mark the container dirty such as the old built-in
3485
- // AgentScheduler and Garbage collector messages.
3486
- switch ( type ) {
3487
- case ContainerMessageType . Attach : {
3488
- const attachMessage = contents as InboundAttachMessage ;
3489
- if ( attachMessage . id === agentSchedulerId ) {
3490
- return false ;
3491
- }
3492
- break ;
3493
- }
3494
- case ContainerMessageType . FluidDataStoreOp : {
3495
- const envelope = contents ;
3496
- if ( envelope . address === agentSchedulerId ) {
3497
- return false ;
3498
- }
3499
- break ;
3500
- }
3501
- case ContainerMessageType . IdAllocation :
3502
- case ContainerMessageType . DocumentSchemaChange :
3503
- case ContainerMessageType . GC : {
3504
- return false ;
3505
- }
3506
- default : {
3507
- break ;
3508
- }
3509
- }
3510
- return true ;
3461
+ /**
3462
+ * Returns true if the container is dirty: not attached, or no pending user messages (could be some "non-dirtyable" ones though)
3463
+ */
3464
+ private computeCurrentDirtyState ( ) : boolean {
3465
+ return (
3466
+ this . attachState !== AttachState . Attached ||
3467
+ this . pendingStateManager . hasPendingUserChanges ( ) ||
3468
+ this . outbox . containsUserChanges ( )
3469
+ ) ;
3511
3470
}
3512
3471
3513
3472
/**
@@ -3545,9 +3504,7 @@ export class ContainerRuntime
3545
3504
this . emit ( "attached" ) ;
3546
3505
}
3547
3506
3548
- if ( attachState === AttachState . Attached && ! this . hasPendingMessages ( ) ) {
3549
- this . updateDocumentDirtyState ( false ) ;
3550
- }
3507
+ this . updateDocumentDirtyState ( ) ;
3551
3508
this . channelCollection . setAttachState ( attachState ) ;
3552
3509
}
3553
3510
@@ -4333,22 +4290,22 @@ export class ContainerRuntime
4333
4290
return this . pendingMessagesCount !== 0 ;
4334
4291
}
4335
4292
4336
- private updateDocumentDirtyState ( dirty : boolean ) : void {
4337
- if ( this . attachState === AttachState . Attached ) {
4338
- // Other way is not true = see this.isContainerMessageDirtyable()
4339
- assert (
4340
- ! dirty || this . hasPendingMessages ( ) ,
4341
- 0x3d3 /* if doc is dirty, there has to be pending ops */ ,
4342
- ) ;
4343
- } else {
4344
- assert ( dirty , 0x3d2 /* Non-attached container is dirty */ ) ;
4345
- }
4293
+ /**
4294
+ * Emit "dirty" or "saved" event based on the current dirty state of the document.
4295
+ * This must be called every time the states underlying the dirty state change.
4296
+ *
4297
+ * @privateRemarks - It's helpful to think of this as an event handler registered
4298
+ * for hypothetical "changed" events for PendingStateManager, Outbox, and Container Attach machinery.
4299
+ * But those events don't exist so we manually call this wherever we know those changes happen.
4300
+ */
4301
+ private updateDocumentDirtyState ( ) : void {
4302
+ const dirty : boolean = this . computeCurrentDirtyState ( ) ;
4346
4303
4347
- if ( this . dirtyContainer === dirty ) {
4304
+ if ( this . lastEmittedDirty === dirty ) {
4348
4305
return ;
4349
4306
}
4350
4307
4351
- this . dirtyContainer = dirty ;
4308
+ this . lastEmittedDirty = dirty ;
4352
4309
if ( this . emitDirtyDocumentEvent ) {
4353
4310
this . emit ( dirty ? "dirty" : "saved" ) ;
4354
4311
}
@@ -4501,9 +4458,7 @@ export class ContainerRuntime
4501
4458
throw dpe ;
4502
4459
}
4503
4460
4504
- if ( this . isContainerMessageDirtyable ( containerRuntimeMessage ) ) {
4505
- this . updateDocumentDirtyState ( true ) ;
4506
- }
4461
+ this . updateDocumentDirtyState ( ) ;
4507
4462
}
4508
4463
4509
4464
private scheduleFlush ( ) : void {
@@ -4938,3 +4893,36 @@ export function createNewSignalEnvelope(
4938
4893
4939
4894
return newEnvelope ;
4940
4895
}
4896
+
4897
+ export function isContainerMessageDirtyable ( {
4898
+ type,
4899
+ contents,
4900
+ } : LocalContainerRuntimeMessage ) : boolean {
4901
+ // Certain container runtime messages should not mark the container dirty such as the old built-in
4902
+ // AgentScheduler and Garbage collector messages.
4903
+ switch ( type ) {
4904
+ case ContainerMessageType . Attach : {
4905
+ const attachMessage = contents as InboundAttachMessage ;
4906
+ if ( attachMessage . id === agentSchedulerId ) {
4907
+ return false ;
4908
+ }
4909
+ break ;
4910
+ }
4911
+ case ContainerMessageType . FluidDataStoreOp : {
4912
+ const envelope = contents ;
4913
+ if ( envelope . address === agentSchedulerId ) {
4914
+ return false ;
4915
+ }
4916
+ break ;
4917
+ }
4918
+ case ContainerMessageType . IdAllocation :
4919
+ case ContainerMessageType . DocumentSchemaChange :
4920
+ case ContainerMessageType . GC : {
4921
+ return false ;
4922
+ }
4923
+ default : {
4924
+ break ;
4925
+ }
4926
+ }
4927
+ return true ;
4928
+ }
0 commit comments