@fluidframework/container-runtime 2.40.0-336023 → 2.40.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 (92) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/api-report/container-runtime.legacy.alpha.api.md +4 -0
  3. package/container-runtime.test-files.tar +0 -0
  4. package/dist/blobManager/blobManager.d.ts +31 -8
  5. package/dist/blobManager/blobManager.d.ts.map +1 -1
  6. package/dist/blobManager/blobManager.js +90 -17
  7. package/dist/blobManager/blobManager.js.map +1 -1
  8. package/dist/channelCollection.d.ts +8 -2
  9. package/dist/channelCollection.d.ts.map +1 -1
  10. package/dist/channelCollection.js +29 -6
  11. package/dist/channelCollection.js.map +1 -1
  12. package/dist/compatUtils.d.ts +19 -10
  13. package/dist/compatUtils.d.ts.map +1 -1
  14. package/dist/compatUtils.js +39 -32
  15. package/dist/compatUtils.js.map +1 -1
  16. package/dist/containerRuntime.d.ts +29 -13
  17. package/dist/containerRuntime.d.ts.map +1 -1
  18. package/dist/containerRuntime.js +139 -149
  19. package/dist/containerRuntime.js.map +1 -1
  20. package/dist/dataStoreContext.d.ts +12 -4
  21. package/dist/dataStoreContext.d.ts.map +1 -1
  22. package/dist/dataStoreContext.js +37 -18
  23. package/dist/dataStoreContext.js.map +1 -1
  24. package/dist/index.d.ts +1 -0
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js.map +1 -1
  27. package/dist/legacy.d.ts +1 -0
  28. package/dist/opLifecycle/index.d.ts +1 -1
  29. package/dist/opLifecycle/index.d.ts.map +1 -1
  30. package/dist/opLifecycle/index.js.map +1 -1
  31. package/dist/opLifecycle/outbox.d.ts +20 -7
  32. package/dist/opLifecycle/outbox.d.ts.map +1 -1
  33. package/dist/opLifecycle/outbox.js +16 -20
  34. package/dist/opLifecycle/outbox.js.map +1 -1
  35. package/dist/packageVersion.d.ts +1 -1
  36. package/dist/packageVersion.d.ts.map +1 -1
  37. package/dist/packageVersion.js +1 -1
  38. package/dist/packageVersion.js.map +1 -1
  39. package/dist/pendingStateManager.d.ts +22 -8
  40. package/dist/pendingStateManager.d.ts.map +1 -1
  41. package/dist/pendingStateManager.js +11 -16
  42. package/dist/pendingStateManager.js.map +1 -1
  43. package/lib/blobManager/blobManager.d.ts +31 -8
  44. package/lib/blobManager/blobManager.d.ts.map +1 -1
  45. package/lib/blobManager/blobManager.js +91 -18
  46. package/lib/blobManager/blobManager.js.map +1 -1
  47. package/lib/channelCollection.d.ts +8 -2
  48. package/lib/channelCollection.d.ts.map +1 -1
  49. package/lib/channelCollection.js +29 -6
  50. package/lib/channelCollection.js.map +1 -1
  51. package/lib/compatUtils.d.ts +19 -10
  52. package/lib/compatUtils.d.ts.map +1 -1
  53. package/lib/compatUtils.js +36 -29
  54. package/lib/compatUtils.js.map +1 -1
  55. package/lib/containerRuntime.d.ts +29 -13
  56. package/lib/containerRuntime.d.ts.map +1 -1
  57. package/lib/containerRuntime.js +60 -70
  58. package/lib/containerRuntime.js.map +1 -1
  59. package/lib/dataStoreContext.d.ts +12 -4
  60. package/lib/dataStoreContext.d.ts.map +1 -1
  61. package/lib/dataStoreContext.js +38 -19
  62. package/lib/dataStoreContext.js.map +1 -1
  63. package/lib/index.d.ts +1 -0
  64. package/lib/index.d.ts.map +1 -1
  65. package/lib/index.js.map +1 -1
  66. package/lib/legacy.d.ts +1 -0
  67. package/lib/opLifecycle/index.d.ts +1 -1
  68. package/lib/opLifecycle/index.d.ts.map +1 -1
  69. package/lib/opLifecycle/index.js.map +1 -1
  70. package/lib/opLifecycle/outbox.d.ts +20 -7
  71. package/lib/opLifecycle/outbox.d.ts.map +1 -1
  72. package/lib/opLifecycle/outbox.js +16 -20
  73. package/lib/opLifecycle/outbox.js.map +1 -1
  74. package/lib/packageVersion.d.ts +1 -1
  75. package/lib/packageVersion.d.ts.map +1 -1
  76. package/lib/packageVersion.js +1 -1
  77. package/lib/packageVersion.js.map +1 -1
  78. package/lib/pendingStateManager.d.ts +22 -8
  79. package/lib/pendingStateManager.d.ts.map +1 -1
  80. package/lib/pendingStateManager.js +11 -16
  81. package/lib/pendingStateManager.js.map +1 -1
  82. package/package.json +18 -18
  83. package/src/blobManager/blobManager.ts +141 -33
  84. package/src/channelCollection.ts +42 -6
  85. package/src/compatUtils.ts +53 -30
  86. package/src/containerRuntime.ts +102 -81
  87. package/src/dataStoreContext.ts +44 -25
  88. package/src/index.ts +1 -0
  89. package/src/opLifecycle/index.ts +1 -0
  90. package/src/opLifecycle/outbox.ts +42 -33
  91. package/src/packageVersion.ts +1 -1
  92. package/src/pendingStateManager.ts +37 -20
@@ -3,22 +3,22 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
 
6
- import {
7
- TypedEventEmitter,
8
- bufferToString,
9
- stringToBuffer,
10
- } from "@fluid-internal/client-utils";
6
+ import { bufferToString, createEmitter, stringToBuffer } from "@fluid-internal/client-utils";
11
7
  import { AttachState } from "@fluidframework/container-definitions";
12
8
  import {
13
9
  IContainerRuntime,
14
10
  IContainerRuntimeEvents,
15
11
  } from "@fluidframework/container-runtime-definitions/internal";
16
12
  import type {
17
- IEvent,
13
+ IEmitter,
18
14
  IEventProvider,
19
15
  IFluidHandleContext,
20
16
  IFluidHandleInternal,
21
17
  IFluidHandleInternalPayloadPending,
18
+ ILocalFluidHandle,
19
+ ILocalFluidHandleEvents,
20
+ Listenable,
21
+ PayloadState,
22
22
  } from "@fluidframework/core-interfaces/internal";
23
23
  import { assert, Deferred } from "@fluidframework/core-utils/internal";
24
24
  import {
@@ -65,7 +65,9 @@ import {
65
65
  */
66
66
  export class BlobHandle
67
67
  extends FluidHandleBase<ArrayBufferLike>
68
- implements IFluidHandleInternalPayloadPending<ArrayBufferLike>
68
+ implements
69
+ ILocalFluidHandle<ArrayBufferLike>,
70
+ IFluidHandleInternalPayloadPending<ArrayBufferLike>
69
71
  {
70
72
  private attached: boolean = false;
71
73
 
@@ -73,9 +75,30 @@ export class BlobHandle
73
75
  return this.routeContext.isAttached && this.attached;
74
76
  }
75
77
 
78
+ private _events:
79
+ | (Listenable<ILocalFluidHandleEvents> & IEmitter<ILocalFluidHandleEvents>)
80
+ | undefined;
81
+ public get events(): Listenable<ILocalFluidHandleEvents> {
82
+ return (this._events ??= createEmitter<ILocalFluidHandleEvents>());
83
+ }
84
+
85
+ private _state: PayloadState = "pending";
86
+ public get payloadState(): PayloadState {
87
+ return this._state;
88
+ }
89
+
90
+ /**
91
+ * The error property starts undefined, signalling that there has been no error yet.
92
+ * If an error occurs, the property will contain the error.
93
+ */
94
+ private _payloadShareError: unknown;
95
+ public get payloadShareError(): unknown {
96
+ return this._payloadShareError;
97
+ }
98
+
76
99
  public readonly absolutePath: string;
77
100
 
78
- constructor(
101
+ public constructor(
79
102
  public readonly path: string,
80
103
  public readonly routeContext: IFluidHandleContext,
81
104
  public get: () => Promise<ArrayBufferLike>,
@@ -86,6 +109,16 @@ export class BlobHandle
86
109
  this.absolutePath = generateHandleContextPath(path, this.routeContext);
87
110
  }
88
111
 
112
+ public readonly notifyShared = (): void => {
113
+ this._state = "shared";
114
+ this._events?.emit("payloadShared");
115
+ };
116
+
117
+ public readonly notifyFailed = (error: unknown): void => {
118
+ this._payloadShareError = error;
119
+ this._events?.emit("payloadShareFailed", error);
120
+ };
121
+
89
122
  public attachGraph(): void {
90
123
  if (!this.attached) {
91
124
  this.attached = true;
@@ -93,6 +126,10 @@ export class BlobHandle
93
126
  }
94
127
  }
95
128
 
129
+ // eslint-disable-next-line jsdoc/require-description
130
+ /**
131
+ * @deprecated No replacement provided. Arbitrary handles may not serve as a bind source.
132
+ */
96
133
  public bind(handle: IFluidHandleInternal): void {
97
134
  throw new Error("Cannot bind to blob handle");
98
135
  }
@@ -104,7 +141,7 @@ export type IBlobManagerRuntime = Pick<
104
141
  IContainerRuntime,
105
142
  "attachState" | "connected" | "baseLogger" | "clientDetails" | "disposed"
106
143
  > &
107
- TypedEventEmitter<IContainerRuntimeEvents>;
144
+ IEventProvider<IContainerRuntimeEvents>;
108
145
 
109
146
  type ICreateBlobResponseWithTTL = ICreateBlobResponse &
110
147
  Partial<Record<"minTTLInSeconds", number>>;
@@ -133,13 +170,14 @@ export interface IPendingBlobs {
133
170
  };
134
171
  }
135
172
 
136
- export interface IBlobManagerEvents extends IEvent {
137
- (event: "noPendingBlobs", listener: () => void);
173
+ export interface IBlobManagerEvents {
174
+ noPendingBlobs: () => void;
138
175
  }
139
176
 
140
- interface IBlobManagerInternalEvents extends IEvent {
141
- (event: "handleAttached", listener: (pending: PendingBlob) => void);
142
- (event: "processedBlobAttach", listener: (localId: string, storageId: string) => void);
177
+ interface IBlobManagerInternalEvents {
178
+ uploadFailed: (localId: string, error: unknown) => void;
179
+ handleAttached: (pending: PendingBlob) => void;
180
+ processedBlobAttach: (localId: string, storageId: string) => void;
143
181
  }
144
182
 
145
183
  const stashedPendingBlobOverrides: Pick<
@@ -157,11 +195,11 @@ export const blobManagerBasePath = "_blobs" as const;
157
195
  export class BlobManager {
158
196
  private readonly mc: MonitoringContext;
159
197
 
160
- private readonly publicEvents = new TypedEventEmitter<IBlobManagerEvents>();
161
- public get events(): IEventProvider<IBlobManagerEvents> {
198
+ private readonly publicEvents = createEmitter<IBlobManagerEvents>();
199
+ public get events(): Listenable<IBlobManagerEvents> {
162
200
  return this.publicEvents;
163
201
  }
164
- private readonly internalEvents = new TypedEventEmitter<IBlobManagerInternalEvents>();
202
+ private readonly internalEvents = createEmitter<IBlobManagerInternalEvents>();
165
203
 
166
204
  /**
167
205
  * Map of local IDs to storage IDs. Contains identity entries (storageId → storageId) for storage IDs. All requested IDs should
@@ -201,6 +239,8 @@ export class BlobManager {
201
239
  new Map();
202
240
  public readonly stashedBlobsUploadP: Promise<(void | ICreateBlobResponse)[]>;
203
241
 
242
+ private readonly createBlobPayloadPending: boolean;
243
+
204
244
  public constructor(props: {
205
245
  readonly routeContext: IFluidHandleContext;
206
246
 
@@ -238,6 +278,7 @@ export class BlobManager {
238
278
  runtime,
239
279
  stashedBlobs,
240
280
  localBlobIdGenerator,
281
+ createBlobPayloadPending,
241
282
  } = props;
242
283
  this.routeContext = routeContext;
243
284
  this.storage = storage;
@@ -245,6 +286,7 @@ export class BlobManager {
245
286
  this.isBlobDeleted = isBlobDeleted;
246
287
  this.runtime = runtime;
247
288
  this.localBlobIdGenerator = localBlobIdGenerator ?? uuid;
289
+ this.createBlobPayloadPending = createBlobPayloadPending;
248
290
 
249
291
  this.mc = createChildMonitoringContext({
250
292
  logger: this.runtime.baseLogger,
@@ -362,6 +404,12 @@ export class BlobManager {
362
404
  return this.redirectTable.get(blobId) !== undefined;
363
405
  }
364
406
 
407
+ /**
408
+ * Retrieve the blob with the given local blob id.
409
+ * @param blobId - The local blob id. Likely coming from a handle.
410
+ * @param payloadPending - Whether we suspect the payload may be pending and not available yet.
411
+ * @returns A promise which resolves to the blob contents
412
+ */
365
413
  public async getBlob(blobId: string, payloadPending: boolean): Promise<ArrayBufferLike> {
366
414
  // Verify that the blob is not deleted, i.e., it has not been garbage collected. If it is, this will throw
367
415
  // an error, failing the call.
@@ -386,7 +434,9 @@ export class BlobManager {
386
434
  } else {
387
435
  const attachedStorageId = this.redirectTable.get(blobId);
388
436
  if (!payloadPending) {
389
- assert(!!attachedStorageId, 0x11f /* "requesting unknown blobs" */);
437
+ // Only blob handles explicitly marked with pending payload are permitted to exist without
438
+ // yet knowing their storage id. Otherwise they must already be associated with a storage id.
439
+ assert(attachedStorageId !== undefined, 0x11f /* "requesting unknown blobs" */);
390
440
  }
391
441
  // If we didn't find it in the redirectTable, assume the attach op is coming eventually and wait.
392
442
  // We do this even if the local client doesn't have the blob payloadPending flag enabled, in case a
@@ -428,11 +478,11 @@ export class BlobManager {
428
478
  0x384 /* requesting handle for unknown blob */,
429
479
  );
430
480
  const pending = this.pendingBlobs.get(localId);
431
- // Create a callback function for once the blob has been attached
481
+ // Create a callback function for once the handle has been attached
432
482
  const callback = pending
433
483
  ? () => {
434
484
  pending.attached = true;
435
- // Notify listeners (e.g. serialization process) that blob has been attached
485
+ // Notify listeners (e.g. serialization process) that handle has been attached
436
486
  this.internalEvents.emit("handleAttached", pending);
437
487
  this.deletePendingBlobMaybe(localId);
438
488
  }
@@ -448,7 +498,7 @@ export class BlobManager {
448
498
 
449
499
  private async createBlobDetached(
450
500
  blob: ArrayBufferLike,
451
- ): Promise<IFluidHandleInternal<ArrayBufferLike>> {
501
+ ): Promise<IFluidHandleInternalPayloadPending<ArrayBufferLike>> {
452
502
  // Blobs created while the container is detached are stored in IDetachedBlobStorage.
453
503
  // The 'IDocumentStorageService.createBlob()' call below will respond with a localId.
454
504
  const response = await this.storage.createBlob(blob);
@@ -459,7 +509,7 @@ export class BlobManager {
459
509
  public async createBlob(
460
510
  blob: ArrayBufferLike,
461
511
  signal?: AbortSignal,
462
- ): Promise<IFluidHandleInternal<ArrayBufferLike>> {
512
+ ): Promise<IFluidHandleInternalPayloadPending<ArrayBufferLike>> {
463
513
  if (this.runtime.attachState === AttachState.Detached) {
464
514
  return this.createBlobDetached(blob);
465
515
  }
@@ -473,6 +523,15 @@ export class BlobManager {
473
523
  0x385 /* For clarity and paranoid defense against adding future attachment states */,
474
524
  );
475
525
 
526
+ return this.createBlobPayloadPending
527
+ ? this.createBlobWithPayloadPending(blob)
528
+ : this.createBlobLegacy(blob, signal);
529
+ }
530
+
531
+ private async createBlobLegacy(
532
+ blob: ArrayBufferLike,
533
+ signal?: AbortSignal,
534
+ ): Promise<IFluidHandleInternalPayloadPending<ArrayBufferLike>> {
476
535
  if (signal?.aborted) {
477
536
  throw this.createAbortError();
478
537
  }
@@ -503,6 +562,48 @@ export class BlobManager {
503
562
  });
504
563
  }
505
564
 
565
+ private createBlobWithPayloadPending(
566
+ blob: ArrayBufferLike,
567
+ ): IFluidHandleInternalPayloadPending<ArrayBufferLike> {
568
+ const localId = this.localBlobIdGenerator();
569
+
570
+ const blobHandle = new BlobHandle(
571
+ getGCNodePathFromBlobId(localId),
572
+ this.routeContext,
573
+ async () => blob,
574
+ true, // payloadPending
575
+ () => {
576
+ const pendingEntry: PendingBlob = {
577
+ blob,
578
+ handleP: new Deferred(),
579
+ uploadP: this.uploadBlob(localId, blob),
580
+ attached: true,
581
+ acked: false,
582
+ opsent: false,
583
+ };
584
+ this.pendingBlobs.set(localId, pendingEntry);
585
+ },
586
+ );
587
+
588
+ const onProcessedBlobAttach = (_localId: string, _storageId: string): void => {
589
+ if (_localId === localId) {
590
+ this.internalEvents.off("processedBlobAttach", onProcessedBlobAttach);
591
+ blobHandle.notifyShared();
592
+ }
593
+ };
594
+ this.internalEvents.on("processedBlobAttach", onProcessedBlobAttach);
595
+
596
+ const onUploadFailed = (_localId: string, error: unknown): void => {
597
+ if (_localId === localId) {
598
+ this.internalEvents.off("uploadFailed", onUploadFailed);
599
+ blobHandle.notifyFailed(error);
600
+ }
601
+ };
602
+ this.internalEvents.on("uploadFailed", onUploadFailed);
603
+
604
+ return blobHandle;
605
+ }
606
+
506
607
  private async uploadBlob(
507
608
  localId: string,
508
609
  blob: ArrayBufferLike,
@@ -546,6 +647,7 @@ export class BlobManager {
546
647
  // the promise but not throw any error outside.
547
648
  this.pendingBlobs.get(localId)?.handleP.reject(error);
548
649
  this.deletePendingBlob(localId);
650
+ this.internalEvents.emit("uploadFailed", localId, error);
549
651
  },
550
652
  );
551
653
  }
@@ -619,7 +721,9 @@ export class BlobManager {
619
721
  // an existing blob, we don't have to wait for the op to be ack'd since this step has already
620
722
  // happened before and so, the server won't delete it.
621
723
  this.setRedirection(localId, response.id);
622
- entry.handleP.resolve(this.getBlobHandle(localId));
724
+ const blobHandle = this.getBlobHandle(localId);
725
+ blobHandle.notifyShared();
726
+ entry.handleP.resolve(blobHandle);
623
727
  this.deletePendingBlobMaybe(localId);
624
728
  } else {
625
729
  // If there is already an op for this storage ID, append the local ID to the list. Once any op for
@@ -682,8 +786,8 @@ export class BlobManager {
682
786
  // set identity (id -> id) entry
683
787
  this.setRedirection(blobId, blobId);
684
788
 
789
+ assert(localId !== undefined, 0x50e /* local ID not present in blob attach message */);
685
790
  if (local) {
686
- assert(localId !== undefined, 0x50e /* local ID not present in blob attach message */);
687
791
  const waitingBlobs = this.opsInFlight.get(blobId);
688
792
  if (waitingBlobs !== undefined) {
689
793
  // For each op corresponding to this storage ID that we are waiting for, resolve the pending blob.
@@ -697,7 +801,9 @@ export class BlobManager {
697
801
  );
698
802
  this.setRedirection(pendingLocalId, blobId);
699
803
  entry.acked = true;
700
- entry.handleP.resolve(this.getBlobHandle(pendingLocalId));
804
+ const blobHandle = this.getBlobHandle(pendingLocalId);
805
+ blobHandle.notifyShared();
806
+ entry.handleP.resolve(blobHandle);
701
807
  this.deletePendingBlobMaybe(pendingLocalId);
702
808
  }
703
809
  this.opsInFlight.delete(blobId);
@@ -705,7 +811,9 @@ export class BlobManager {
705
811
  const localEntry = this.pendingBlobs.get(localId);
706
812
  if (localEntry) {
707
813
  localEntry.acked = true;
708
- localEntry.handleP.resolve(this.getBlobHandle(localId));
814
+ const blobHandle = this.getBlobHandle(localId);
815
+ blobHandle.notifyShared();
816
+ localEntry.handleP.resolve(blobHandle);
709
817
  this.deletePendingBlobMaybe(localId);
710
818
  }
711
819
  }
@@ -875,7 +983,7 @@ export class BlobManager {
875
983
  const localBlobs = new Set<PendingBlob>();
876
984
  // This while is used to stash blobs created while attaching and getting blobs
877
985
  while (localBlobs.size < this.pendingBlobs.size) {
878
- const attachBlobsP: Promise<void>[] = [];
986
+ const attachHandlesP: Promise<void>[] = [];
879
987
  for (const [localId, entry] of this.pendingBlobs) {
880
988
  if (!localBlobs.has(entry)) {
881
989
  localBlobs.add(entry);
@@ -890,8 +998,8 @@ export class BlobManager {
890
998
  // original createBlob call) and let them attach the blob. This is a lie we told since the upload
891
999
  // hasn't finished yet, but it's fine since we will retry on rehydration.
892
1000
  entry.handleP.resolve(this.getBlobHandle(localId));
893
- // Array of promises that will resolve when blobs get attached.
894
- attachBlobsP.push(
1001
+ // Array of promises that will resolve when handles get attached.
1002
+ attachHandlesP.push(
895
1003
  new Promise<void>((resolve, reject) => {
896
1004
  stopBlobAttachingSignal?.addEventListener(
897
1005
  "abort",
@@ -901,16 +1009,16 @@ export class BlobManager {
901
1009
  },
902
1010
  { once: true },
903
1011
  );
904
- const onBlobAttached = (attachedEntry: PendingBlob): void => {
1012
+ const onHandleAttached = (attachedEntry: PendingBlob): void => {
905
1013
  if (attachedEntry === entry) {
906
- this.internalEvents.off("handleAttached", onBlobAttached);
1014
+ this.internalEvents.off("handleAttached", onHandleAttached);
907
1015
  resolve();
908
1016
  }
909
1017
  };
910
1018
  if (entry.attached) {
911
1019
  resolve();
912
1020
  } else {
913
- this.internalEvents.on("handleAttached", onBlobAttached);
1021
+ this.internalEvents.on("handleAttached", onHandleAttached);
914
1022
  }
915
1023
  }),
916
1024
  );
@@ -918,7 +1026,7 @@ export class BlobManager {
918
1026
  }
919
1027
  // Wait for all blobs to be attached. This is important, otherwise serialized container
920
1028
  // could send the blobAttach op without any op that references the blob, making it useless.
921
- await Promise.allSettled(attachBlobsP);
1029
+ await Promise.allSettled(attachHandlesP);
922
1030
  }
923
1031
 
924
1032
  for (const [localId, entry] of this.pendingBlobs) {
@@ -703,7 +703,12 @@ export class ChannelCollection implements IFluidDataStoreChannel, IDisposable {
703
703
  }
704
704
  public readonly dispose = (): void => this.disposeOnce.value;
705
705
 
706
- public reSubmit(type: string, content: unknown, localOpMetadata: unknown): void {
706
+ public reSubmit(
707
+ type: string,
708
+ content: unknown,
709
+ localOpMetadata: unknown,
710
+ squash: boolean,
711
+ ): void {
707
712
  switch (type) {
708
713
  case ContainerMessageType.Attach:
709
714
  case ContainerMessageType.Alias: {
@@ -711,7 +716,7 @@ export class ChannelCollection implements IFluidDataStoreChannel, IDisposable {
711
716
  return;
712
717
  }
713
718
  case ContainerMessageType.FluidDataStoreOp: {
714
- return this.reSubmitChannelOp(type, content, localOpMetadata);
719
+ return this.reSubmitChannelOp(type, content, localOpMetadata, squash);
715
720
  }
716
721
  default: {
717
722
  assert(false, 0x907 /* unknown op type */);
@@ -719,7 +724,12 @@ export class ChannelCollection implements IFluidDataStoreChannel, IDisposable {
719
724
  }
720
725
  }
721
726
 
722
- protected reSubmitChannelOp(type: string, content: unknown, localOpMetadata: unknown): void {
727
+ protected reSubmitChannelOp(
728
+ type: string,
729
+ content: unknown,
730
+ localOpMetadata: unknown,
731
+ squash: boolean,
732
+ ): void {
723
733
  const envelope = content as IEnvelope;
724
734
  const context = this.contexts.get(envelope.address);
725
735
  // If the data store has been deleted, log an error and throw an error. If there are local changes for a
@@ -734,7 +744,7 @@ export class ChannelCollection implements IFluidDataStoreChannel, IDisposable {
734
744
  }
735
745
  assert(!!context, 0x160 /* "There should be a store context for the op" */);
736
746
  const innerContents = envelope.contents as FluidDataStoreMessage;
737
- context.reSubmit(innerContents.type, innerContents.content, localOpMetadata);
747
+ context.reSubmit(innerContents.type, innerContents.content, localOpMetadata, squash);
738
748
  }
739
749
 
740
750
  public rollback(type: string, content: unknown, localOpMetadata: unknown): void {
@@ -1135,7 +1145,7 @@ export class ChannelCollection implements IFluidDataStoreChannel, IDisposable {
1135
1145
  public notifyReadOnlyState(readonly: boolean): void {
1136
1146
  for (const [fluidDataStoreId, context] of this.contexts) {
1137
1147
  try {
1138
- context.notifyReadOnlyState(readonly);
1148
+ context.notifyReadOnlyState();
1139
1149
  } catch (error) {
1140
1150
  this.mc.logger.sendErrorEvent(
1141
1151
  {
@@ -1154,6 +1164,32 @@ export class ChannelCollection implements IFluidDataStoreChannel, IDisposable {
1154
1164
  }
1155
1165
  }
1156
1166
 
1167
+ /**
1168
+ * Notifies all data store contexts about the current staging mode state.
1169
+ *
1170
+ * @param staging - A boolean indicating whether the container is in staging mode.
1171
+ */
1172
+ public notifyStagingMode(staging: boolean): void {
1173
+ for (const [fluidDataStoreId, context] of this.contexts) {
1174
+ try {
1175
+ context.notifyStagingMode(staging);
1176
+ } catch (error) {
1177
+ this.mc.logger.sendErrorEvent(
1178
+ {
1179
+ eventName: "notifyStagingModeError",
1180
+ ...tagCodeArtifacts({
1181
+ fluidDataStoreId,
1182
+ }),
1183
+ details: {
1184
+ staging,
1185
+ },
1186
+ },
1187
+ error,
1188
+ );
1189
+ }
1190
+ }
1191
+ }
1192
+
1157
1193
  public setAttachState(attachState: AttachState.Attaching | AttachState.Attached): void {
1158
1194
  for (const [, context] of this.contexts) {
1159
1195
  // Fire only for bounded stores.
@@ -1667,7 +1703,7 @@ export class ChannelCollectionFactory implements IFluidDataStoreFactory {
1667
1703
  // from the same package.
1668
1704
  assert(
1669
1705
  context instanceof FluidDataStoreContext,
1670
- "we don't support the layer boundary here today",
1706
+ 0xb8f /* we don't support the layer boundary here today */,
1671
1707
  );
1672
1708
 
1673
1709
  const runtime = new ChannelCollection(
@@ -16,7 +16,7 @@ import { pkgVersion } from "./packageVersion.js";
16
16
  /**
17
17
  * Our policy is to support N/N-1 compatibility by default, where N is the most
18
18
  * recent public major release of the runtime.
19
- * Therefore, if the customer does not provide a compatibility mode, we will
19
+ * Therefore, if the customer does not provide a minVersionForCollab, we will
20
20
  * default to use N-1.
21
21
  *
22
22
  * However, this is not consistent with today's behavior. Some options (i.e.
@@ -26,12 +26,21 @@ import { pkgVersion } from "./packageVersion.js";
26
26
  * Importantly though, N/N-2 compatibility is still guaranteed with the proper
27
27
  * configurations set.
28
28
  *
29
- * Further to distinguish unspecified `compatibilityVersion` from a specified
29
+ * Further to distinguish unspecified `minVersionForCollab` from a specified
30
30
  * version and allow `enableExplicitSchemaControl` to default to `true` for
31
31
  * any 2.0.0+ version, we will use a special value of `2.0.0-defaults`, which
32
32
  * is semantically less than 2.0.0.
33
33
  */
34
- export const defaultCompatibilityVersion = "2.0.0-defaults" as const;
34
+ export const defaultMinVersionForCollab =
35
+ "2.0.0-defaults" as const satisfies MinimumVersionForCollab;
36
+
37
+ /**
38
+ * We don't want allow a version before the major public release of the LTS version.
39
+ * Today we use "1.0.0", because our policy supports N/N-1 & N/N-2, which includes
40
+ * all minor versions of N. Though LTS starts at 1.4.0, we should stay consistent
41
+ * with our policy and allow all 1.x versions to be compatible with 2.x.
42
+ */
43
+ const lowestMinVersionForCollab = "1.0.0" as const satisfies MinimumVersionForCollab;
35
44
 
36
45
  /**
37
46
  * String in a valid semver format specifying bottom of a minor version
@@ -43,6 +52,18 @@ export type MinimumMinorSemanticVersion = `${bigint}.${bigint}.0` | `${bigint}.0
43
52
 
44
53
  /**
45
54
  * String in a valid semver format of a specific version at least specifying minor.
55
+ *
56
+ * @legacy
57
+ * @alpha
58
+ */
59
+ export type MinimumVersionForCollab =
60
+ | `${1 | 2}.${bigint}.${bigint}`
61
+ | `${1 | 2}.${bigint}.${bigint}-${string}`;
62
+
63
+ /**
64
+ * String in a valid semver format of a specific version at least specifying minor.
65
+ * Unlike {@link MinimumVersionForCollab}, this type allows any bigint for the major version.
66
+ * Used as a more generic type that allows major versions other than 1 or 2.
46
67
  */
47
68
  export type SemanticVersion =
48
69
  | `${bigint}.${bigint}.${bigint}`
@@ -84,13 +105,13 @@ export type RuntimeOptionsAffectingDocSchema = Omit<
84
105
  /**
85
106
  * Mapping of RuntimeOptionsAffectingDocSchema to their compatibility related configs.
86
107
  *
87
- * Each key in this map corresponds to a property in RuntimeOptionsAffectingDocSchema. The value is an object that maps SemanticVersions
88
- * to the appropriate default value for that property to supporting that SemanticVersion. If clients running SemanticVersion X are able to understand
89
- * the format changes introduced by the property, then the default value for that SemanticVersion will enable the feature associated with the property.
108
+ * Each key in this map corresponds to a property in RuntimeOptionsAffectingDocSchema. The value is an object that maps MinimumVersionForCollab
109
+ * to the appropriate default value for that property to supporting that MinimumVersionForCollab. If clients running MinimumVersionForCollab X are able to understand
110
+ * the format changes introduced by the property, then the default value for that MinimumVersionForCollab will enable the feature associated with the property.
90
111
  * Otherwise, the feature will be disabled.
91
112
  *
92
- * For example if the compatibilityVersion is a 1.x version (i.e. "1.5.0"), then the default value for `enableGroupedBatching` will be false since 1.x
93
- * clients do not understand the document format when batching is enabled. If the compatibilityVersion is a 2.x client (i.e. "2.0.0" or later), then the
113
+ * For example if the minVersionForCollab is a 1.x version (i.e. "1.5.0"), then the default value for `enableGroupedBatching` will be false since 1.x
114
+ * clients do not understand the document format when batching is enabled. If the minVersionForCollab is a 2.x client (i.e. "2.0.0" or later), then the
94
115
  * default value for `enableGroupedBatching` will be true because clients running 2.0 or later will be able to understand the format changes associated
95
116
  * with the batching feature.
96
117
  */
@@ -118,14 +139,14 @@ const runtimeOptionsAffectingDocSchemaConfigMap = {
118
139
  explicitSchemaControl: {
119
140
  "1.0.0": false,
120
141
  // This option's intention is to prevent 1.x clients from joining sessions
121
- // when enabled. This is set to true when the compatibility version is set
142
+ // when enabled. This is set to true when the minVersionForCollab is set
122
143
  // to >=2.0.0 (explicitly). This is different than other 2.0 defaults
123
144
  // because it was not enabled by default prior to the implementation of
124
- // `compatibilityVersion`.
125
- // `defaultCompatibilityVersion` is set to "2.0.0-defaults" which "2.0.0"
145
+ // `minVersionForCollab`.
146
+ // `defaultMinVersionForCollab` is set to "2.0.0-defaults" which "2.0.0"
126
147
  // does not satisfy to avoiding enabling this option by default as of
127
- // `compatibilityVersion` introduction, which could be unexpected.
128
- // Only enable as a default when `compatibilityVersion` is specified at
148
+ // `minVersionForCollab` introduction, which could be unexpected.
149
+ // Only enable as a default when `minVersionForCollab` is specified at
129
150
  // 2.0.0+.
130
151
  "2.0.0": true,
131
152
  } as const,
@@ -138,7 +159,7 @@ const runtimeOptionsAffectingDocSchemaConfigMap = {
138
159
  } as const,
139
160
  gcOptions: {
140
161
  "1.0.0": {},
141
- // Although sweep is supported in 2.x, it is disabled by default until compatibilityVersion>=3.0.0 to be extra safe.
162
+ // Although sweep is supported in 2.x, it is disabled by default until minVersionForCollab>=3.0.0 to be extra safe.
142
163
  "3.0.0": { enableGCSweep: true },
143
164
  } as const,
144
165
  createBlobPayloadPending: {
@@ -150,13 +171,13 @@ const runtimeOptionsAffectingDocSchemaConfigMap = {
150
171
  } as const satisfies ConfigMap<RuntimeOptionsAffectingDocSchema>;
151
172
 
152
173
  /**
153
- * Returns the default RuntimeOptionsAffectingDocSchema configuration for a given compatibility version.
174
+ * Returns the default RuntimeOptionsAffectingDocSchema configuration for a given minVersionForCollab.
154
175
  */
155
- export function getCompatibilityVersionDefaults(
156
- compatibilityVersion: SemanticVersion,
176
+ export function getMinVersionForCollabDefaults(
177
+ minVersionForCollab: MinimumVersionForCollab,
157
178
  ): RuntimeOptionsAffectingDocSchema {
158
179
  return getConfigsForCompatMode(
159
- compatibilityVersion,
180
+ minVersionForCollab,
160
181
  runtimeOptionsAffectingDocSchemaConfigMap,
161
182
  // This is a bad cast away from Partial that getConfigsForCompatMode provides.
162
183
  // ConfigMap should be restructured to provide RuntimeOptionsAffectingDocSchema guarantee.
@@ -164,10 +185,10 @@ export function getCompatibilityVersionDefaults(
164
185
  }
165
186
 
166
187
  /**
167
- * Returns a default configuration given compatibility version and configuration version map.
188
+ * Returns a default configuration given minVersionForCollab and configuration version map.
168
189
  */
169
190
  export function getConfigsForCompatMode<T extends Record<SemanticVersion, unknown>>(
170
- compatibilityVersion: SemanticVersion,
191
+ minVersionForCollab: SemanticVersion,
171
192
  configMap: ConfigMap<T>,
172
193
  ): Partial<T> {
173
194
  const defaultConfigs: Partial<T> = {};
@@ -176,14 +197,14 @@ export function getConfigsForCompatMode<T extends Record<SemanticVersion, unknow
176
197
  const config = configMap[key as keyof T];
177
198
  // Sort the versions in ascending order so we can short circuit the loop.
178
199
  const versions = Object.keys(config).sort(compare);
179
- // For each config, we iterate over the keys and check if compatibilityVersion is greater than or equal to the version.
200
+ // For each config, we iterate over the keys and check if minVersionForCollab is greater than or equal to the version.
180
201
  // If so, we set it as the default value for the option. At the end of the loop we should have the most recent default
181
- // value that is compatible with the version specified as the compatibilityVersion.
202
+ // value that is compatible with the version specified as the minVersionForCollab.
182
203
  for (const version of versions) {
183
- if (gte(compatibilityVersion, version)) {
204
+ if (gte(minVersionForCollab, version)) {
184
205
  defaultConfigs[key] = config[version as MinimumMinorSemanticVersion];
185
206
  } else {
186
- // If the compatibility mode is less than the version, we break out of the loop since we don't need to check
207
+ // If the minVersionForCollab is less than the version, we break out of the loop since we don't need to check
187
208
  // any later versions.
188
209
  break;
189
210
  }
@@ -193,13 +214,15 @@ export function getConfigsForCompatMode<T extends Record<SemanticVersion, unknow
193
214
  }
194
215
 
195
216
  /**
196
- * Checks if the compatibility version is valid.
197
- * A valid compatibility version is a string that is a valid semver version and is less than or equal to the current package version.
217
+ * Checks if the minVersionForCollab is valid.
218
+ * A valid minVersionForCollab is a MinimumVersionForCollab that is at least `lowestMinVersionForCollab` and less than or equal to the current package version.
198
219
  */
199
- export function isValidCompatVersion(compatibilityVersion: SemanticVersion): boolean {
220
+ export function isValidMinVersionForCollab(
221
+ minVersionForCollab: MinimumVersionForCollab,
222
+ ): boolean {
200
223
  return (
201
- compatibilityVersion !== undefined &&
202
- valid(compatibilityVersion) !== null &&
203
- lte(compatibilityVersion, pkgVersion)
224
+ valid(minVersionForCollab) !== null &&
225
+ gte(minVersionForCollab, lowestMinVersionForCollab) &&
226
+ lte(minVersionForCollab, pkgVersion)
204
227
  );
205
228
  }