@fluidframework/container-runtime 2.111.0 → 2.112.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/container-runtime.test-files.tar +0 -0
  3. package/dist/blobManager/blobManager.d.ts +6 -3
  4. package/dist/blobManager/blobManager.d.ts.map +1 -1
  5. package/dist/blobManager/blobManager.js +19 -5
  6. package/dist/blobManager/blobManager.js.map +1 -1
  7. package/dist/containerRuntime.d.ts +31 -0
  8. package/dist/containerRuntime.d.ts.map +1 -1
  9. package/dist/containerRuntime.js +69 -10
  10. package/dist/containerRuntime.js.map +1 -1
  11. package/dist/packageVersion.d.ts +1 -1
  12. package/dist/packageVersion.js +1 -1
  13. package/dist/packageVersion.js.map +1 -1
  14. package/dist/pendingStateManager.d.ts +7 -0
  15. package/dist/pendingStateManager.d.ts.map +1 -1
  16. package/dist/pendingStateManager.js +15 -0
  17. package/dist/pendingStateManager.js.map +1 -1
  18. package/dist/runtimeLayerCompatState.d.ts +1 -1
  19. package/lib/blobManager/blobManager.d.ts +6 -3
  20. package/lib/blobManager/blobManager.d.ts.map +1 -1
  21. package/lib/blobManager/blobManager.js +19 -5
  22. package/lib/blobManager/blobManager.js.map +1 -1
  23. package/lib/containerRuntime.d.ts +31 -0
  24. package/lib/containerRuntime.d.ts.map +1 -1
  25. package/lib/containerRuntime.js +69 -10
  26. package/lib/containerRuntime.js.map +1 -1
  27. package/lib/packageVersion.d.ts +1 -1
  28. package/lib/packageVersion.js +1 -1
  29. package/lib/packageVersion.js.map +1 -1
  30. package/lib/pendingStateManager.d.ts +7 -0
  31. package/lib/pendingStateManager.d.ts.map +1 -1
  32. package/lib/pendingStateManager.js +15 -0
  33. package/lib/pendingStateManager.js.map +1 -1
  34. package/lib/runtimeLayerCompatState.d.ts +1 -1
  35. package/package.json +18 -18
  36. package/src/blobManager/blobManager.ts +20 -4
  37. package/src/containerRuntime.ts +78 -11
  38. package/src/packageVersion.ts +1 -1
  39. package/src/pendingStateManager.ts +16 -0
@@ -1523,6 +1523,7 @@ export class ContainerRuntime
1523
1523
 
1524
1524
  private lastEmittedDirty: boolean;
1525
1525
  private emitDirtyDocumentEvent = true;
1526
+ private lastEmittedHasStagedChanges: boolean;
1526
1527
  private readonly useDeltaManagerOpsProxy: boolean;
1527
1528
  private readonly closeSummarizerDelayMs: number;
1528
1529
 
@@ -2196,6 +2197,9 @@ export class ContainerRuntime
2196
2197
  this.lastEmittedDirty = this.computeCurrentDirtyState();
2197
2198
  context.updateDirtyContainerState(this.lastEmittedDirty);
2198
2199
 
2200
+ // We haven't emitted hasStagedChangesChanged yet, but this is the baseline so we know to emit when it changes
2201
+ this.lastEmittedHasStagedChanges = this.computeCurrentHasStagedChanges();
2202
+
2199
2203
  // Reference Sequence Number may have just changed, and it must be consistent across a batch,
2200
2204
  // so we should flush now to clear the way for the next ops.
2201
2205
  // NOTE: This will be redundant whenever CR.process was called for the op (since we flush there too) -
@@ -3244,13 +3248,14 @@ export class ContainerRuntime
3244
3248
  "Duplicate batch - The same batch was sequenced twice",
3245
3249
  { batchId: batchStart.batchId },
3246
3250
  );
3247
-
3251
+ const batchIdExplicit = batchStart.batchId !== undefined;
3252
+ const otherBatchIdExplicit = result.otherBatchInfo?.batchIdExplicit ?? false;
3248
3253
  this.mc.logger.sendTelemetryEvent(
3249
3254
  {
3250
3255
  eventName: "DuplicateBatch",
3251
3256
  details: {
3252
3257
  batchId: batchStart.batchId,
3253
- batchIdExplicit: batchStart.batchId !== undefined,
3258
+ batchIdExplicit,
3254
3259
  clientId: batchStart.clientId,
3255
3260
  batchStartCsn: batchStart.batchStartCsn,
3256
3261
  size: inboundResult.length,
@@ -3261,7 +3266,7 @@ export class ContainerRuntime
3261
3266
  // loaded from a summary snapshot rather than seen at runtime.
3262
3267
  otherClientId: result.otherBatchInfo?.clientId,
3263
3268
  otherBatchStartCsn: result.otherBatchInfo?.batchStartCsn,
3264
- otherBatchIdExplicit: result.otherBatchInfo?.batchIdExplicit,
3269
+ otherBatchIdExplicit,
3265
3270
  otherFromSnapshot: result.otherBatchInfo === undefined,
3266
3271
  ...extractSafePropertiesFromMessage(batchStart.keyMessage),
3267
3272
  // For grouped batches, `keyMessage` is one of the sub-messages produced by
@@ -3274,10 +3279,12 @@ export class ContainerRuntime
3274
3279
  },
3275
3280
  error,
3276
3281
  );
3277
- // Due to a live incident where we had a bug in the service that caused duplicate batches to be sent to clients, we want to log when we detect a duplicate batch, but we don't want to throw an error
3278
- // as it could hit the same service bug. We need to monitor below event to catch legitimate container forking scenarios and reenable throwing the data corruption error once the service bug is fixed and we stop seeing duplicate batches in the wild
3279
- // or once we are able to identify batch duplication reason (forking vs service bug).
3280
- // throw error;
3282
+ // Only throw the error if either the current batch or the other batch has an explicit batchId since that indicates a meaningful duplication scenario
3283
+ // coming from our batch readings rather than a server outage scenario.
3284
+ const shouldThrowOnDuplicate = batchIdExplicit || otherBatchIdExplicit;
3285
+ if (shouldThrowOnDuplicate) {
3286
+ throw error;
3287
+ }
3281
3288
  }
3282
3289
  }
3283
3290
 
@@ -3717,6 +3724,12 @@ export class ContainerRuntime
3717
3724
  this.closeFn(error2);
3718
3725
  throw error2;
3719
3726
  }
3727
+
3728
+ // Flushing moves any staged batch from the Outbox into the PendingStateManager. Since
3729
+ // computeCurrentHasStagedChanges() now also considers a non-empty Outbox in Staging Mode, the
3730
+ // externally-visible value shouldn't change here, but re-checking is cheap and keeps this
3731
+ // method robust to future changes in how the two are tracked.
3732
+ this.updateHasStagedChangesState();
3720
3733
  }
3721
3734
 
3722
3735
  /**
@@ -3899,6 +3912,7 @@ export class ContainerRuntime
3899
3912
  },
3900
3913
  );
3901
3914
  this.updateDocumentDirtyState();
3915
+ this.updateHasStagedChangesState();
3902
3916
  return batchInfos;
3903
3917
  }, "discard"),
3904
3918
  commitChanges: (options) => {
@@ -3906,10 +3920,12 @@ export class ContainerRuntime
3906
3920
  exitStagingMode(() => {
3907
3921
  // Replay all staged batches in typical FIFO order.
3908
3922
  // We'll be out of staging mode so they'll be sent to the service finally.
3909
- return this.pendingStateManager.replayPendingStates({
3923
+ const batchInfos = this.pendingStateManager.replayPendingStates({
3910
3924
  committingStagedBatches: true,
3911
3925
  squash,
3912
3926
  });
3927
+ this.updateHasStagedChangesState();
3928
+ return batchInfos;
3913
3929
  }, "commit");
3914
3930
  },
3915
3931
  };
@@ -4036,6 +4052,38 @@ export class ContainerRuntime
4036
4052
  );
4037
4053
  }
4038
4054
 
4055
+ /**
4056
+ * Returns true if there are any staged changes, i.e. changes submitted while in Staging Mode
4057
+ * (see {@link @fluidframework/runtime-definitions#IContainerRuntimeBaseInternal.enterStagingMode})
4058
+ * that have not yet been discarded or committed.
4059
+ *
4060
+ * @remarks This is distinct from {@link ContainerRuntime.isDirty}: a container may be dirty due to
4061
+ * ordinary unacknowledged local changes without having any staged changes.
4062
+ */
4063
+ public get hasStagedChanges(): boolean {
4064
+ // Rather than recomputing this in the moment, just regurgitate the last emitted state.
4065
+ return this.lastEmittedHasStagedChanges;
4066
+ }
4067
+
4068
+ /**
4069
+ * Returns true if there are currently any staged (not yet discarded or committed) changes pending.
4070
+ *
4071
+ * @remarks While in Staging Mode, newly submitted ops sit in the Outbox until the next flush before
4072
+ * they're moved to the PendingStateManager (where `pendingStateManager.hasStagedChanges()` looks).
4073
+ * So we also check the Outbox here, otherwise there would be a window between submit and flush where
4074
+ * staged changes exist but this would incorrectly report false.
4075
+ *
4076
+ * @remarks We don't care about the type of ops in the Outbox here (unlike dirty state), just whether
4077
+ * there's anything queued at all, for consistency with how `pendingStateManager.hasStagedChanges()`
4078
+ * doesn't discriminate by op type either.
4079
+ */
4080
+ private computeCurrentHasStagedChanges(): boolean {
4081
+ return (
4082
+ this.pendingStateManager.hasStagedChanges() ||
4083
+ (this.inStagingMode && !this.outbox.isEmpty)
4084
+ );
4085
+ }
4086
+
4039
4087
  /**
4040
4088
  * Submits the signal to be sent to other clients.
4041
4089
  * @param type - Type of the signal.
@@ -4871,13 +4919,31 @@ export class ContainerRuntime
4871
4919
  private updateDocumentDirtyState(): void {
4872
4920
  const dirty: boolean = this.computeCurrentDirtyState();
4873
4921
 
4874
- if (this.lastEmittedDirty === dirty) {
4922
+ if (this.lastEmittedDirty !== dirty) {
4923
+ this.lastEmittedDirty = dirty;
4924
+ if (this.emitDirtyDocumentEvent) {
4925
+ this.emit(dirty ? "dirty" : "saved");
4926
+ }
4927
+ }
4928
+ }
4929
+
4930
+ /**
4931
+ * Emit "hasStagedChangesChanged" if the current staged-changes state differs from what was last emitted.
4932
+ * This must be called explicitly at each place staged changes can be added or removed (submit, flush,
4933
+ * discardChanges, commitChanges) -- unlike {@link ContainerRuntime.updateDocumentDirtyState}, it is not
4934
+ * safe to call this unconditionally alongside dirty tracking, since most dirty-state transitions (e.g.
4935
+ * acking ops, reconnecting) can't affect staged changes.
4936
+ */
4937
+ private updateHasStagedChangesState(): void {
4938
+ const hasStagedChanges: boolean = this.computeCurrentHasStagedChanges();
4939
+
4940
+ if (this.lastEmittedHasStagedChanges === hasStagedChanges) {
4875
4941
  return;
4876
4942
  }
4877
4943
 
4878
- this.lastEmittedDirty = dirty;
4944
+ this.lastEmittedHasStagedChanges = hasStagedChanges;
4879
4945
  if (this.emitDirtyDocumentEvent) {
4880
- this.emit(dirty ? "dirty" : "saved");
4946
+ this.emit("hasStagedChangesChanged", hasStagedChanges);
4881
4947
  }
4882
4948
  }
4883
4949
 
@@ -5056,6 +5122,7 @@ export class ContainerRuntime
5056
5122
  }
5057
5123
 
5058
5124
  this.updateDocumentDirtyState();
5125
+ this.updateHasStagedChangesState();
5059
5126
  }
5060
5127
 
5061
5128
  private scheduleFlush(): void {
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/container-runtime";
9
- export const pkgVersion = "2.111.0";
9
+ export const pkgVersion = "2.112.0";
@@ -333,6 +333,22 @@ export class PendingStateManager implements IDisposable {
333
333
  return this.initialMessages.length > 0;
334
334
  }
335
335
 
336
+ /**
337
+ * Checks the pending messages to see if any of them are staged (submitted while in Staging Mode).
338
+ * Unlike {@link PendingStateManager.hasPendingUserChanges}, this does not filter by "dirtyable" messages:
339
+ * anything submitted while in Staging Mode counts as a staged change that must be committed or discarded,
340
+ * regardless of whether it would otherwise count towards dirty tracking.
341
+ */
342
+ public hasStagedChanges(): boolean {
343
+ for (let i = 0; i < this.pendingMessages.length; i++) {
344
+ const element = this.pendingMessages.get(i);
345
+ if (element?.batchInfo.staged === true) {
346
+ return true;
347
+ }
348
+ }
349
+ return false;
350
+ }
351
+
336
352
  /**
337
353
  * The minimumPendingMessageSequenceNumber is the minimum of the first pending message and the first initial message.
338
354
  *