@fluidframework/container-runtime 2.110.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 (40) hide show
  1. package/.dependency-cruiser.cjs +41 -0
  2. package/CHANGELOG.md +31 -0
  3. package/container-runtime.test-files.tar +0 -0
  4. package/dist/blobManager/blobManager.d.ts +6 -3
  5. package/dist/blobManager/blobManager.d.ts.map +1 -1
  6. package/dist/blobManager/blobManager.js +19 -5
  7. package/dist/blobManager/blobManager.js.map +1 -1
  8. package/dist/containerRuntime.d.ts +31 -0
  9. package/dist/containerRuntime.d.ts.map +1 -1
  10. package/dist/containerRuntime.js +80 -11
  11. package/dist/containerRuntime.js.map +1 -1
  12. package/dist/packageVersion.d.ts +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 +2 -2
  20. package/lib/blobManager/blobManager.d.ts +6 -3
  21. package/lib/blobManager/blobManager.d.ts.map +1 -1
  22. package/lib/blobManager/blobManager.js +19 -5
  23. package/lib/blobManager/blobManager.js.map +1 -1
  24. package/lib/containerRuntime.d.ts +31 -0
  25. package/lib/containerRuntime.d.ts.map +1 -1
  26. package/lib/containerRuntime.js +80 -11
  27. package/lib/containerRuntime.js.map +1 -1
  28. package/lib/packageVersion.d.ts +1 -1
  29. package/lib/packageVersion.js +1 -1
  30. package/lib/packageVersion.js.map +1 -1
  31. package/lib/pendingStateManager.d.ts +7 -0
  32. package/lib/pendingStateManager.d.ts.map +1 -1
  33. package/lib/pendingStateManager.js +15 -0
  34. package/lib/pendingStateManager.js.map +1 -1
  35. package/lib/runtimeLayerCompatState.d.ts +2 -2
  36. package/package.json +21 -19
  37. package/src/blobManager/blobManager.ts +20 -4
  38. package/src/containerRuntime.ts +89 -12
  39. package/src/packageVersion.ts +1 -1
  40. package/src/pendingStateManager.ts +16 -0
@@ -747,6 +747,7 @@ export class ContainerRuntime extends TypedEventEmitter {
747
747
  this.rollbackStagedChange(runtimeOp, localOpMetadata);
748
748
  });
749
749
  this.updateDocumentDirtyState();
750
+ this.updateHasStagedChangesState();
750
751
  return batchInfos;
751
752
  }, "discard"),
752
753
  commitChanges: (options) => {
@@ -754,10 +755,12 @@ export class ContainerRuntime extends TypedEventEmitter {
754
755
  exitStagingMode(() => {
755
756
  // Replay all staged batches in typical FIFO order.
756
757
  // We'll be out of staging mode so they'll be sent to the service finally.
757
- return this.pendingStateManager.replayPendingStates({
758
+ const batchInfos = this.pendingStateManager.replayPendingStates({
758
759
  committingStagedBatches: true,
759
760
  squash,
760
761
  });
762
+ this.updateHasStagedChangesState();
763
+ return batchInfos;
761
764
  }, "commit");
762
765
  },
763
766
  };
@@ -1219,6 +1222,8 @@ export class ContainerRuntime extends TypedEventEmitter {
1219
1222
  // We haven't emitted dirty/saved yet, but this is the baseline so we know to emit when it changes
1220
1223
  this.lastEmittedDirty = this.computeCurrentDirtyState();
1221
1224
  context.updateDirtyContainerState(this.lastEmittedDirty);
1225
+ // We haven't emitted hasStagedChangesChanged yet, but this is the baseline so we know to emit when it changes
1226
+ this.lastEmittedHasStagedChanges = this.computeCurrentHasStagedChanges();
1222
1227
  // Reference Sequence Number may have just changed, and it must be consistent across a batch,
1223
1228
  // so we should flush now to clear the way for the next ops.
1224
1229
  // NOTE: This will be redundant whenever CR.process was called for the op (since we flush there too) -
@@ -1340,8 +1345,18 @@ export class ContainerRuntime extends TypedEventEmitter {
1340
1345
  if (this.isSummarizerClient) {
1341
1346
  // We want to dynamically import any thing inside summaryDelayLoadedModule module only when we are the summarizer client,
1342
1347
  // so that all non summarizer clients don't have to load the code inside this module.
1348
+ // Import the delay-loaded module by its leaf path rather than through the `./summary/index.js` barrel,
1349
+ // which is also statically imported above for summarization dependencies every client loads
1350
+ // (SummaryManager, SummaryCollection, SummarizerClientElection, etc.). A bundler that traces re-exports
1351
+ // per symbol (e.g. webpack honoring sideEffects + providedExports) already keeps the summarizer out of
1352
+ // the initial chunk even if this dynamic import targets the barrel, because the barrel's static importers
1353
+ // use only non-summarizer symbols. A bundler without that analysis would instead treat the
1354
+ // statically-imported barrel as making the whole summarizer subgraph available and fold it into the
1355
+ // initial chunk. Targeting summaryDelayLoadedModule/index.js directly makes the split deterministic
1356
+ // across bundlers.
1343
1357
  const module = await import(
1344
- /* webpackChunkName: "summarizerDelayLoadedModule" */ "./summary/index.js");
1358
+ // eslint-disable-next-line import-x/no-internal-modules -- Needed to import the delay-loaded module directly.
1359
+ /* webpackChunkName: "summarizerDelayLoadedModule" */ "./summary/summaryDelayLoadedModule/index.js");
1345
1360
  this._summarizer = new module.Summarizer(this /* ISummarizerRuntime */, () => this.summaryConfiguration, this /* ISummarizerInternalsProvider */, this.handleContext, summaryCollection, async (runtime) => module.RunWhileConnectedCoordinator.create(runtime,
1346
1361
  // Summarization runs in summarizer client and needs access to the real (non-proxy) active
1347
1362
  // information. The proxy delta manager would always return false for summarizer client.
@@ -1992,11 +2007,13 @@ export class ContainerRuntime extends TypedEventEmitter {
1992
2007
  const result = this.duplicateBatchDetector?.processInboundBatch(batchStart);
1993
2008
  if (result?.duplicate === true) {
1994
2009
  const error = new DataCorruptionError("Duplicate batch - The same batch was sequenced twice", { batchId: batchStart.batchId });
2010
+ const batchIdExplicit = batchStart.batchId !== undefined;
2011
+ const otherBatchIdExplicit = result.otherBatchInfo?.batchIdExplicit ?? false;
1995
2012
  this.mc.logger.sendTelemetryEvent({
1996
2013
  eventName: "DuplicateBatch",
1997
2014
  details: {
1998
2015
  batchId: batchStart.batchId,
1999
- batchIdExplicit: batchStart.batchId !== undefined,
2016
+ batchIdExplicit,
2000
2017
  clientId: batchStart.clientId,
2001
2018
  batchStartCsn: batchStart.batchStartCsn,
2002
2019
  size: inboundResult.length,
@@ -2007,7 +2024,7 @@ export class ContainerRuntime extends TypedEventEmitter {
2007
2024
  // loaded from a summary snapshot rather than seen at runtime.
2008
2025
  otherClientId: result.otherBatchInfo?.clientId,
2009
2026
  otherBatchStartCsn: result.otherBatchInfo?.batchStartCsn,
2010
- otherBatchIdExplicit: result.otherBatchInfo?.batchIdExplicit,
2027
+ otherBatchIdExplicit,
2011
2028
  otherFromSnapshot: result.otherBatchInfo === undefined,
2012
2029
  ...extractSafePropertiesFromMessage(batchStart.keyMessage),
2013
2030
  // For grouped batches, `keyMessage` is one of the sub-messages produced by
@@ -2018,10 +2035,12 @@ export class ContainerRuntime extends TypedEventEmitter {
2018
2035
  messageClientSequenceNumber: batchStart.batchStartCsn,
2019
2036
  },
2020
2037
  }, error);
2021
- // 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
2022
- // 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
2023
- // or once we are able to identify batch duplication reason (forking vs service bug).
2024
- // throw error;
2038
+ // Only throw the error if either the current batch or the other batch has an explicit batchId since that indicates a meaningful duplication scenario
2039
+ // coming from our batch readings rather than a server outage scenario.
2040
+ const shouldThrowOnDuplicate = batchIdExplicit || otherBatchIdExplicit;
2041
+ if (shouldThrowOnDuplicate) {
2042
+ throw error;
2043
+ }
2025
2044
  }
2026
2045
  }
2027
2046
  // Reach out to PendingStateManager, either to zip localOpMetadata into the *local* message list,
@@ -2340,6 +2359,11 @@ export class ContainerRuntime extends TypedEventEmitter {
2340
2359
  this.closeFn(error2);
2341
2360
  throw error2;
2342
2361
  }
2362
+ // Flushing moves any staged batch from the Outbox into the PendingStateManager. Since
2363
+ // computeCurrentHasStagedChanges() now also considers a non-empty Outbox in Staging Mode, the
2364
+ // externally-visible value shouldn't change here, but re-checking is cheap and keeps this
2365
+ // method robust to future changes in how the two are tracked.
2366
+ this.updateHasStagedChangesState();
2343
2367
  }
2344
2368
  /**
2345
2369
  * {@inheritDoc @fluidframework/runtime-definitions#IContainerRuntimeBase.orderSequentially}
@@ -2482,6 +2506,34 @@ export class ContainerRuntime extends TypedEventEmitter {
2482
2506
  this.pendingStateManager.hasPendingUserChanges() ||
2483
2507
  this.outbox.containsUserChanges());
2484
2508
  }
2509
+ /**
2510
+ * Returns true if there are any staged changes, i.e. changes submitted while in Staging Mode
2511
+ * (see {@link @fluidframework/runtime-definitions#IContainerRuntimeBaseInternal.enterStagingMode})
2512
+ * that have not yet been discarded or committed.
2513
+ *
2514
+ * @remarks This is distinct from {@link ContainerRuntime.isDirty}: a container may be dirty due to
2515
+ * ordinary unacknowledged local changes without having any staged changes.
2516
+ */
2517
+ get hasStagedChanges() {
2518
+ // Rather than recomputing this in the moment, just regurgitate the last emitted state.
2519
+ return this.lastEmittedHasStagedChanges;
2520
+ }
2521
+ /**
2522
+ * Returns true if there are currently any staged (not yet discarded or committed) changes pending.
2523
+ *
2524
+ * @remarks While in Staging Mode, newly submitted ops sit in the Outbox until the next flush before
2525
+ * they're moved to the PendingStateManager (where `pendingStateManager.hasStagedChanges()` looks).
2526
+ * So we also check the Outbox here, otherwise there would be a window between submit and flush where
2527
+ * staged changes exist but this would incorrectly report false.
2528
+ *
2529
+ * @remarks We don't care about the type of ops in the Outbox here (unlike dirty state), just whether
2530
+ * there's anything queued at all, for consistency with how `pendingStateManager.hasStagedChanges()`
2531
+ * doesn't discriminate by op type either.
2532
+ */
2533
+ computeCurrentHasStagedChanges() {
2534
+ return (this.pendingStateManager.hasStagedChanges() ||
2535
+ (this.inStagingMode && !this.outbox.isEmpty));
2536
+ }
2485
2537
  /**
2486
2538
  * Submits the signal to be sent to other clients.
2487
2539
  * @param type - Type of the signal.
@@ -3086,12 +3138,28 @@ export class ContainerRuntime extends TypedEventEmitter {
3086
3138
  */
3087
3139
  updateDocumentDirtyState() {
3088
3140
  const dirty = this.computeCurrentDirtyState();
3089
- if (this.lastEmittedDirty === dirty) {
3141
+ if (this.lastEmittedDirty !== dirty) {
3142
+ this.lastEmittedDirty = dirty;
3143
+ if (this.emitDirtyDocumentEvent) {
3144
+ this.emit(dirty ? "dirty" : "saved");
3145
+ }
3146
+ }
3147
+ }
3148
+ /**
3149
+ * Emit "hasStagedChangesChanged" if the current staged-changes state differs from what was last emitted.
3150
+ * This must be called explicitly at each place staged changes can be added or removed (submit, flush,
3151
+ * discardChanges, commitChanges) -- unlike {@link ContainerRuntime.updateDocumentDirtyState}, it is not
3152
+ * safe to call this unconditionally alongside dirty tracking, since most dirty-state transitions (e.g.
3153
+ * acking ops, reconnecting) can't affect staged changes.
3154
+ */
3155
+ updateHasStagedChangesState() {
3156
+ const hasStagedChanges = this.computeCurrentHasStagedChanges();
3157
+ if (this.lastEmittedHasStagedChanges === hasStagedChanges) {
3090
3158
  return;
3091
3159
  }
3092
- this.lastEmittedDirty = dirty;
3160
+ this.lastEmittedHasStagedChanges = hasStagedChanges;
3093
3161
  if (this.emitDirtyDocumentEvent) {
3094
- this.emit(dirty ? "dirty" : "saved");
3162
+ this.emit("hasStagedChangesChanged", hasStagedChanges);
3095
3163
  }
3096
3164
  }
3097
3165
  // Keep in sync with IFluidRootParentContextPrivate.submitMessage.
@@ -3226,6 +3294,7 @@ export class ContainerRuntime extends TypedEventEmitter {
3226
3294
  throw dpe;
3227
3295
  }
3228
3296
  this.updateDocumentDirtyState();
3297
+ this.updateHasStagedChangesState();
3229
3298
  }
3230
3299
  scheduleFlush() {
3231
3300
  // During staging mode, suppress automatic flush scheduling until the main batch