@fluidframework/container-runtime 2.33.2 → 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 +26 -10
  9. package/dist/channelCollection.d.ts.map +1 -1
  10. package/dist/channelCollection.js +42 -11
  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 +15 -16
  21. package/dist/dataStoreContext.d.ts.map +1 -1
  22. package/dist/dataStoreContext.js +37 -19
  23. package/dist/dataStoreContext.js.map +1 -1
  24. package/dist/index.d.ts +1 -3
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +1 -9
  27. package/dist/index.js.map +1 -1
  28. package/dist/legacy.d.ts +1 -0
  29. package/dist/opLifecycle/index.d.ts +1 -1
  30. package/dist/opLifecycle/index.d.ts.map +1 -1
  31. package/dist/opLifecycle/index.js.map +1 -1
  32. package/dist/opLifecycle/outbox.d.ts +20 -7
  33. package/dist/opLifecycle/outbox.d.ts.map +1 -1
  34. package/dist/opLifecycle/outbox.js +16 -20
  35. package/dist/opLifecycle/outbox.js.map +1 -1
  36. package/dist/packageVersion.d.ts +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 +26 -10
  48. package/lib/channelCollection.d.ts.map +1 -1
  49. package/lib/channelCollection.js +43 -12
  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 +15 -16
  60. package/lib/dataStoreContext.d.ts.map +1 -1
  61. package/lib/dataStoreContext.js +38 -20
  62. package/lib/dataStoreContext.js.map +1 -1
  63. package/lib/index.d.ts +1 -3
  64. package/lib/index.d.ts.map +1 -1
  65. package/lib/index.js +0 -3
  66. package/lib/index.js.map +1 -1
  67. package/lib/legacy.d.ts +1 -0
  68. package/lib/opLifecycle/index.d.ts +1 -1
  69. package/lib/opLifecycle/index.d.ts.map +1 -1
  70. package/lib/opLifecycle/index.js.map +1 -1
  71. package/lib/opLifecycle/outbox.d.ts +20 -7
  72. package/lib/opLifecycle/outbox.d.ts.map +1 -1
  73. package/lib/opLifecycle/outbox.js +16 -20
  74. package/lib/opLifecycle/outbox.js.map +1 -1
  75. package/lib/packageVersion.d.ts +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 +77 -19
  85. package/src/compatUtils.ts +53 -30
  86. package/src/containerRuntime.ts +102 -81
  87. package/src/dataStoreContext.ts +48 -38
  88. package/src/index.ts +1 -13
  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) {
@@ -120,11 +120,22 @@ interface FluidDataStoreMessage {
120
120
  type: string;
121
121
  }
122
122
 
123
+ /**
124
+ * This version of the interface is private to this the package. it should never be exported under any tag.
125
+ * It is used to manage interactions within the container-runtime package. If something is needed
126
+ * cross package, it is likely it is also being used cross layer (ContainerRuntime * DataStoreRuntime).
127
+ * If that is the case, the change likely needs to be staged directly on IFluidParentContext. Changes
128
+ * being staged on IFluidParentContext can be added here as well, likely with optionality removed,
129
+ * to ease interactions within this package.
130
+ */
131
+ export interface IFluidParentContextPrivate extends Omit<IFluidParentContext, "isReadOnly"> {
132
+ readonly isReadOnly: () => boolean;
133
+ }
134
+
123
135
  /**
124
136
  * Creates a shallow wrapper of {@link IFluidParentContext}. The wrapper can then have its methods overwritten as needed
125
137
  */
126
- export function wrapContext(context: IFluidParentContext): IFluidParentContext {
127
- const isReadOnly = context.isReadOnly;
138
+ export function wrapContext(context: IFluidParentContextPrivate): IFluidParentContextPrivate {
128
139
  return {
129
140
  get IFluidDataStoreRegistry() {
130
141
  return context.IFluidDataStoreRegistry;
@@ -150,7 +161,7 @@ export function wrapContext(context: IFluidParentContext): IFluidParentContext {
150
161
  get attachState() {
151
162
  return context.attachState;
152
163
  },
153
- isReadOnly: isReadOnly === undefined ? undefined : () => isReadOnly(),
164
+ isReadOnly: () => context.isReadOnly(),
154
165
  containerRuntime: context.containerRuntime,
155
166
  scope: context.scope,
156
167
  gcThrowOnTombstoneUsage: context.gcThrowOnTombstoneUsage,
@@ -201,8 +212,8 @@ export function wrapContext(context: IFluidParentContext): IFluidParentContext {
201
212
  */
202
213
  function wrapContextForInnerChannel(
203
214
  id: string,
204
- parentContext: IFluidParentContext,
205
- ): IFluidParentContext {
215
+ parentContext: IFluidParentContextPrivate,
216
+ ): IFluidParentContextPrivate {
206
217
  const context = wrapContext(parentContext);
207
218
 
208
219
  context.submitMessage = (type: string, content: unknown, localOpMetadata: unknown) => {
@@ -274,7 +285,7 @@ export class ChannelCollection implements IFluidDataStoreChannel, IDisposable {
274
285
 
275
286
  constructor(
276
287
  protected readonly baseSnapshot: ISnapshotTree | ISnapshot | undefined,
277
- public readonly parentContext: IFluidParentContext,
288
+ public readonly parentContext: IFluidParentContextPrivate,
278
289
  baseLogger: ITelemetryBaseLogger,
279
290
  private readonly gcNodeUpdated: (props: IGCNodeUpdatedProps) => void,
280
291
  private readonly isDataStoreDeleted: (nodePath: string) => boolean,
@@ -372,7 +383,7 @@ export class ChannelCollection implements IFluidDataStoreChannel, IDisposable {
372
383
  */
373
384
  private shouldSendAttachLog = true;
374
385
 
375
- protected wrapContextForInnerChannel(id: string): IFluidParentContext {
386
+ protected wrapContextForInnerChannel(id: string): IFluidParentContextPrivate {
376
387
  return wrapContextForInnerChannel(id, this.parentContext);
377
388
  }
378
389
 
@@ -692,7 +703,12 @@ export class ChannelCollection implements IFluidDataStoreChannel, IDisposable {
692
703
  }
693
704
  public readonly dispose = (): void => this.disposeOnce.value;
694
705
 
695
- 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 {
696
712
  switch (type) {
697
713
  case ContainerMessageType.Attach:
698
714
  case ContainerMessageType.Alias: {
@@ -700,7 +716,7 @@ export class ChannelCollection implements IFluidDataStoreChannel, IDisposable {
700
716
  return;
701
717
  }
702
718
  case ContainerMessageType.FluidDataStoreOp: {
703
- return this.reSubmitChannelOp(type, content, localOpMetadata);
719
+ return this.reSubmitChannelOp(type, content, localOpMetadata, squash);
704
720
  }
705
721
  default: {
706
722
  assert(false, 0x907 /* unknown op type */);
@@ -708,7 +724,12 @@ export class ChannelCollection implements IFluidDataStoreChannel, IDisposable {
708
724
  }
709
725
  }
710
726
 
711
- 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 {
712
733
  const envelope = content as IEnvelope;
713
734
  const context = this.contexts.get(envelope.address);
714
735
  // If the data store has been deleted, log an error and throw an error. If there are local changes for a
@@ -723,7 +744,7 @@ export class ChannelCollection implements IFluidDataStoreChannel, IDisposable {
723
744
  }
724
745
  assert(!!context, 0x160 /* "There should be a store context for the op" */);
725
746
  const innerContents = envelope.contents as FluidDataStoreMessage;
726
- context.reSubmit(innerContents.type, innerContents.content, localOpMetadata);
747
+ context.reSubmit(innerContents.type, innerContents.content, localOpMetadata, squash);
727
748
  }
728
749
 
729
750
  public rollback(type: string, content: unknown, localOpMetadata: unknown): void {
@@ -1124,7 +1145,7 @@ export class ChannelCollection implements IFluidDataStoreChannel, IDisposable {
1124
1145
  public notifyReadOnlyState(readonly: boolean): void {
1125
1146
  for (const [fluidDataStoreId, context] of this.contexts) {
1126
1147
  try {
1127
- context.notifyReadOnlyState(readonly);
1148
+ context.notifyReadOnlyState();
1128
1149
  } catch (error) {
1129
1150
  this.mc.logger.sendErrorEvent(
1130
1151
  {
@@ -1133,7 +1154,7 @@ export class ChannelCollection implements IFluidDataStoreChannel, IDisposable {
1133
1154
  fluidDataStoreId,
1134
1155
  }),
1135
1156
  details: {
1136
- runtimeReadonly: this.parentContext.isReadOnly?.(),
1157
+ runtimeReadonly: this.parentContext.isReadOnly(),
1137
1158
  readonly,
1138
1159
  },
1139
1160
  },
@@ -1143,6 +1164,32 @@ export class ChannelCollection implements IFluidDataStoreChannel, IDisposable {
1143
1164
  }
1144
1165
  }
1145
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
+
1146
1193
  public setAttachState(attachState: AttachState.Attaching | AttachState.Attached): void {
1147
1194
  for (const [, context] of this.contexts) {
1148
1195
  // Fire only for bounded stores.
@@ -1622,9 +1669,7 @@ export function detectOutboundReferences(
1622
1669
  /**
1623
1670
  * @internal
1624
1671
  */
1625
- export class ChannelCollectionFactory<T extends ChannelCollection = ChannelCollection>
1626
- implements IFluidDataStoreFactory
1627
- {
1672
+ export class ChannelCollectionFactory implements IFluidDataStoreFactory {
1628
1673
  public readonly type = "ChannelCollectionChannel";
1629
1674
 
1630
1675
  public IFluidDataStoreRegistry: IFluidDataStoreRegistry;
@@ -1635,12 +1680,11 @@ export class ChannelCollectionFactory<T extends ChannelCollection = ChannelColle
1635
1680
  private readonly provideEntryPoint: (
1636
1681
  runtime: IFluidDataStoreChannel,
1637
1682
  ) => Promise<FluidObject>,
1638
- private readonly ctor: (...args: ConstructorParameters<typeof ChannelCollection>) => T,
1639
1683
  ) {
1640
1684
  this.IFluidDataStoreRegistry = new FluidDataStoreRegistry(registryEntries);
1641
1685
  }
1642
1686
 
1643
- public get IFluidDataStoreFactory(): ChannelCollectionFactory<T> {
1687
+ public get IFluidDataStoreFactory(): ChannelCollectionFactory {
1644
1688
  return this;
1645
1689
  }
1646
1690
 
@@ -1648,7 +1692,21 @@ export class ChannelCollectionFactory<T extends ChannelCollection = ChannelColle
1648
1692
  context: IFluidDataStoreContext,
1649
1693
  _existing: boolean,
1650
1694
  ): Promise<IFluidDataStoreChannel> {
1651
- const runtime = this.ctor(
1695
+ // when work is done to complete nested datastores
1696
+ // this class should move to a new package
1697
+ // and IFluidDataStoreContext will need to support
1698
+ // all the cross layer needs of the channel context,
1699
+ // and the below assert should be removed.
1700
+ //
1701
+ // for now this will continue to work if both
1702
+ // this factory and the container runtime are
1703
+ // from the same package.
1704
+ assert(
1705
+ context instanceof FluidDataStoreContext,
1706
+ 0xb8f /* we don't support the layer boundary here today */,
1707
+ );
1708
+
1709
+ const runtime = new ChannelCollection(
1652
1710
  context.baseSnapshot,
1653
1711
  context, // parentContext
1654
1712
  context.baseLogger,