@fluidframework/container-runtime 2.111.0 → 2.113.0-411909

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 (48) 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.d.ts.map +1 -1
  13. package/dist/packageVersion.js +1 -1
  14. package/dist/packageVersion.js.map +1 -1
  15. package/dist/pendingStateManager.d.ts +7 -0
  16. package/dist/pendingStateManager.d.ts.map +1 -1
  17. package/dist/pendingStateManager.js +15 -0
  18. package/dist/pendingStateManager.js.map +1 -1
  19. package/dist/runtimeLayerCompatState.d.ts +1 -1
  20. package/dist/summary/summarizerNode/summarizerNode.d.ts.map +1 -1
  21. package/dist/summary/summarizerNode/summarizerNode.js +6 -5
  22. package/dist/summary/summarizerNode/summarizerNode.js.map +1 -1
  23. package/lib/blobManager/blobManager.d.ts +6 -3
  24. package/lib/blobManager/blobManager.d.ts.map +1 -1
  25. package/lib/blobManager/blobManager.js +19 -5
  26. package/lib/blobManager/blobManager.js.map +1 -1
  27. package/lib/containerRuntime.d.ts +31 -0
  28. package/lib/containerRuntime.d.ts.map +1 -1
  29. package/lib/containerRuntime.js +69 -10
  30. package/lib/containerRuntime.js.map +1 -1
  31. package/lib/packageVersion.d.ts +1 -1
  32. package/lib/packageVersion.d.ts.map +1 -1
  33. package/lib/packageVersion.js +1 -1
  34. package/lib/packageVersion.js.map +1 -1
  35. package/lib/pendingStateManager.d.ts +7 -0
  36. package/lib/pendingStateManager.d.ts.map +1 -1
  37. package/lib/pendingStateManager.js +15 -0
  38. package/lib/pendingStateManager.js.map +1 -1
  39. package/lib/runtimeLayerCompatState.d.ts +1 -1
  40. package/lib/summary/summarizerNode/summarizerNode.d.ts.map +1 -1
  41. package/lib/summary/summarizerNode/summarizerNode.js +6 -5
  42. package/lib/summary/summarizerNode/summarizerNode.js.map +1 -1
  43. package/package.json +18 -18
  44. package/src/blobManager/blobManager.ts +20 -4
  45. package/src/containerRuntime.ts +78 -11
  46. package/src/packageVersion.ts +1 -1
  47. package/src/pendingStateManager.ts +16 -0
  48. package/src/summary/summarizerNode/summarizerNode.ts +9 -7
@@ -754,6 +754,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
754
754
  this.rollbackStagedChange(runtimeOp, localOpMetadata);
755
755
  });
756
756
  this.updateDocumentDirtyState();
757
+ this.updateHasStagedChangesState();
757
758
  return batchInfos;
758
759
  }, "discard"),
759
760
  commitChanges: (options) => {
@@ -761,10 +762,12 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
761
762
  exitStagingMode(() => {
762
763
  // Replay all staged batches in typical FIFO order.
763
764
  // We'll be out of staging mode so they'll be sent to the service finally.
764
- return this.pendingStateManager.replayPendingStates({
765
+ const batchInfos = this.pendingStateManager.replayPendingStates({
765
766
  committingStagedBatches: true,
766
767
  squash,
767
768
  });
769
+ this.updateHasStagedChangesState();
770
+ return batchInfos;
768
771
  }, "commit");
769
772
  },
770
773
  };
@@ -1226,6 +1229,8 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
1226
1229
  // We haven't emitted dirty/saved yet, but this is the baseline so we know to emit when it changes
1227
1230
  this.lastEmittedDirty = this.computeCurrentDirtyState();
1228
1231
  context.updateDirtyContainerState(this.lastEmittedDirty);
1232
+ // We haven't emitted hasStagedChangesChanged yet, but this is the baseline so we know to emit when it changes
1233
+ this.lastEmittedHasStagedChanges = this.computeCurrentHasStagedChanges();
1229
1234
  // Reference Sequence Number may have just changed, and it must be consistent across a batch,
1230
1235
  // so we should flush now to clear the way for the next ops.
1231
1236
  // NOTE: This will be redundant whenever CR.process was called for the op (since we flush there too) -
@@ -2009,11 +2014,13 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
2009
2014
  const result = this.duplicateBatchDetector?.processInboundBatch(batchStart);
2010
2015
  if (result?.duplicate === true) {
2011
2016
  const error = new internal_8.DataCorruptionError("Duplicate batch - The same batch was sequenced twice", { batchId: batchStart.batchId });
2017
+ const batchIdExplicit = batchStart.batchId !== undefined;
2018
+ const otherBatchIdExplicit = result.otherBatchInfo?.batchIdExplicit ?? false;
2012
2019
  this.mc.logger.sendTelemetryEvent({
2013
2020
  eventName: "DuplicateBatch",
2014
2021
  details: {
2015
2022
  batchId: batchStart.batchId,
2016
- batchIdExplicit: batchStart.batchId !== undefined,
2023
+ batchIdExplicit,
2017
2024
  clientId: batchStart.clientId,
2018
2025
  batchStartCsn: batchStart.batchStartCsn,
2019
2026
  size: inboundResult.length,
@@ -2024,7 +2031,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
2024
2031
  // loaded from a summary snapshot rather than seen at runtime.
2025
2032
  otherClientId: result.otherBatchInfo?.clientId,
2026
2033
  otherBatchStartCsn: result.otherBatchInfo?.batchStartCsn,
2027
- otherBatchIdExplicit: result.otherBatchInfo?.batchIdExplicit,
2034
+ otherBatchIdExplicit,
2028
2035
  otherFromSnapshot: result.otherBatchInfo === undefined,
2029
2036
  ...(0, internal_8.extractSafePropertiesFromMessage)(batchStart.keyMessage),
2030
2037
  // For grouped batches, `keyMessage` is one of the sub-messages produced by
@@ -2035,10 +2042,12 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
2035
2042
  messageClientSequenceNumber: batchStart.batchStartCsn,
2036
2043
  },
2037
2044
  }, error);
2038
- // 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
2039
- // 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
2040
- // or once we are able to identify batch duplication reason (forking vs service bug).
2041
- // throw error;
2045
+ // Only throw the error if either the current batch or the other batch has an explicit batchId since that indicates a meaningful duplication scenario
2046
+ // coming from our batch readings rather than a server outage scenario.
2047
+ const shouldThrowOnDuplicate = batchIdExplicit || otherBatchIdExplicit;
2048
+ if (shouldThrowOnDuplicate) {
2049
+ throw error;
2050
+ }
2042
2051
  }
2043
2052
  }
2044
2053
  // Reach out to PendingStateManager, either to zip localOpMetadata into the *local* message list,
@@ -2357,6 +2366,11 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
2357
2366
  this.closeFn(error2);
2358
2367
  throw error2;
2359
2368
  }
2369
+ // Flushing moves any staged batch from the Outbox into the PendingStateManager. Since
2370
+ // computeCurrentHasStagedChanges() now also considers a non-empty Outbox in Staging Mode, the
2371
+ // externally-visible value shouldn't change here, but re-checking is cheap and keeps this
2372
+ // method robust to future changes in how the two are tracked.
2373
+ this.updateHasStagedChangesState();
2360
2374
  }
2361
2375
  /**
2362
2376
  * {@inheritDoc @fluidframework/runtime-definitions#IContainerRuntimeBase.orderSequentially}
@@ -2499,6 +2513,34 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
2499
2513
  this.pendingStateManager.hasPendingUserChanges() ||
2500
2514
  this.outbox.containsUserChanges());
2501
2515
  }
2516
+ /**
2517
+ * Returns true if there are any staged changes, i.e. changes submitted while in Staging Mode
2518
+ * (see {@link @fluidframework/runtime-definitions#IContainerRuntimeBaseInternal.enterStagingMode})
2519
+ * that have not yet been discarded or committed.
2520
+ *
2521
+ * @remarks This is distinct from {@link ContainerRuntime.isDirty}: a container may be dirty due to
2522
+ * ordinary unacknowledged local changes without having any staged changes.
2523
+ */
2524
+ get hasStagedChanges() {
2525
+ // Rather than recomputing this in the moment, just regurgitate the last emitted state.
2526
+ return this.lastEmittedHasStagedChanges;
2527
+ }
2528
+ /**
2529
+ * Returns true if there are currently any staged (not yet discarded or committed) changes pending.
2530
+ *
2531
+ * @remarks While in Staging Mode, newly submitted ops sit in the Outbox until the next flush before
2532
+ * they're moved to the PendingStateManager (where `pendingStateManager.hasStagedChanges()` looks).
2533
+ * So we also check the Outbox here, otherwise there would be a window between submit and flush where
2534
+ * staged changes exist but this would incorrectly report false.
2535
+ *
2536
+ * @remarks We don't care about the type of ops in the Outbox here (unlike dirty state), just whether
2537
+ * there's anything queued at all, for consistency with how `pendingStateManager.hasStagedChanges()`
2538
+ * doesn't discriminate by op type either.
2539
+ */
2540
+ computeCurrentHasStagedChanges() {
2541
+ return (this.pendingStateManager.hasStagedChanges() ||
2542
+ (this.inStagingMode && !this.outbox.isEmpty));
2543
+ }
2502
2544
  /**
2503
2545
  * Submits the signal to be sent to other clients.
2504
2546
  * @param type - Type of the signal.
@@ -3103,12 +3145,28 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
3103
3145
  */
3104
3146
  updateDocumentDirtyState() {
3105
3147
  const dirty = this.computeCurrentDirtyState();
3106
- if (this.lastEmittedDirty === dirty) {
3148
+ if (this.lastEmittedDirty !== dirty) {
3149
+ this.lastEmittedDirty = dirty;
3150
+ if (this.emitDirtyDocumentEvent) {
3151
+ this.emit(dirty ? "dirty" : "saved");
3152
+ }
3153
+ }
3154
+ }
3155
+ /**
3156
+ * Emit "hasStagedChangesChanged" if the current staged-changes state differs from what was last emitted.
3157
+ * This must be called explicitly at each place staged changes can be added or removed (submit, flush,
3158
+ * discardChanges, commitChanges) -- unlike {@link ContainerRuntime.updateDocumentDirtyState}, it is not
3159
+ * safe to call this unconditionally alongside dirty tracking, since most dirty-state transitions (e.g.
3160
+ * acking ops, reconnecting) can't affect staged changes.
3161
+ */
3162
+ updateHasStagedChangesState() {
3163
+ const hasStagedChanges = this.computeCurrentHasStagedChanges();
3164
+ if (this.lastEmittedHasStagedChanges === hasStagedChanges) {
3107
3165
  return;
3108
3166
  }
3109
- this.lastEmittedDirty = dirty;
3167
+ this.lastEmittedHasStagedChanges = hasStagedChanges;
3110
3168
  if (this.emitDirtyDocumentEvent) {
3111
- this.emit(dirty ? "dirty" : "saved");
3169
+ this.emit("hasStagedChangesChanged", hasStagedChanges);
3112
3170
  }
3113
3171
  }
3114
3172
  // Keep in sync with IFluidRootParentContextPrivate.submitMessage.
@@ -3243,6 +3301,7 @@ class ContainerRuntime extends client_utils_1.TypedEventEmitter {
3243
3301
  throw dpe;
3244
3302
  }
3245
3303
  this.updateDocumentDirtyState();
3304
+ this.updateHasStagedChangesState();
3246
3305
  }
3247
3306
  scheduleFlush() {
3248
3307
  // During staging mode, suppress automatic flush scheduling until the main batch