@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.
- package/CHANGELOG.md +14 -0
- package/api-report/container-runtime.legacy.alpha.api.md +4 -0
- package/container-runtime.test-files.tar +0 -0
- package/dist/blobManager/blobManager.d.ts +31 -8
- package/dist/blobManager/blobManager.d.ts.map +1 -1
- package/dist/blobManager/blobManager.js +90 -17
- package/dist/blobManager/blobManager.js.map +1 -1
- package/dist/channelCollection.d.ts +8 -2
- package/dist/channelCollection.d.ts.map +1 -1
- package/dist/channelCollection.js +29 -6
- package/dist/channelCollection.js.map +1 -1
- package/dist/compatUtils.d.ts +19 -10
- package/dist/compatUtils.d.ts.map +1 -1
- package/dist/compatUtils.js +39 -32
- package/dist/compatUtils.js.map +1 -1
- package/dist/containerRuntime.d.ts +29 -13
- package/dist/containerRuntime.d.ts.map +1 -1
- package/dist/containerRuntime.js +139 -149
- package/dist/containerRuntime.js.map +1 -1
- package/dist/dataStoreContext.d.ts +12 -4
- package/dist/dataStoreContext.d.ts.map +1 -1
- package/dist/dataStoreContext.js +37 -18
- package/dist/dataStoreContext.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/legacy.d.ts +1 -0
- package/dist/opLifecycle/index.d.ts +1 -1
- package/dist/opLifecycle/index.d.ts.map +1 -1
- package/dist/opLifecycle/index.js.map +1 -1
- package/dist/opLifecycle/outbox.d.ts +20 -7
- package/dist/opLifecycle/outbox.d.ts.map +1 -1
- package/dist/opLifecycle/outbox.js +16 -20
- package/dist/opLifecycle/outbox.js.map +1 -1
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.d.ts.map +1 -1
- package/dist/packageVersion.js +1 -1
- package/dist/packageVersion.js.map +1 -1
- package/dist/pendingStateManager.d.ts +22 -8
- package/dist/pendingStateManager.d.ts.map +1 -1
- package/dist/pendingStateManager.js +11 -16
- package/dist/pendingStateManager.js.map +1 -1
- package/lib/blobManager/blobManager.d.ts +31 -8
- package/lib/blobManager/blobManager.d.ts.map +1 -1
- package/lib/blobManager/blobManager.js +91 -18
- package/lib/blobManager/blobManager.js.map +1 -1
- package/lib/channelCollection.d.ts +8 -2
- package/lib/channelCollection.d.ts.map +1 -1
- package/lib/channelCollection.js +29 -6
- package/lib/channelCollection.js.map +1 -1
- package/lib/compatUtils.d.ts +19 -10
- package/lib/compatUtils.d.ts.map +1 -1
- package/lib/compatUtils.js +36 -29
- package/lib/compatUtils.js.map +1 -1
- package/lib/containerRuntime.d.ts +29 -13
- package/lib/containerRuntime.d.ts.map +1 -1
- package/lib/containerRuntime.js +60 -70
- package/lib/containerRuntime.js.map +1 -1
- package/lib/dataStoreContext.d.ts +12 -4
- package/lib/dataStoreContext.d.ts.map +1 -1
- package/lib/dataStoreContext.js +38 -19
- package/lib/dataStoreContext.js.map +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js.map +1 -1
- package/lib/legacy.d.ts +1 -0
- package/lib/opLifecycle/index.d.ts +1 -1
- package/lib/opLifecycle/index.d.ts.map +1 -1
- package/lib/opLifecycle/index.js.map +1 -1
- package/lib/opLifecycle/outbox.d.ts +20 -7
- package/lib/opLifecycle/outbox.d.ts.map +1 -1
- package/lib/opLifecycle/outbox.js +16 -20
- package/lib/opLifecycle/outbox.js.map +1 -1
- package/lib/packageVersion.d.ts +1 -1
- package/lib/packageVersion.d.ts.map +1 -1
- package/lib/packageVersion.js +1 -1
- package/lib/packageVersion.js.map +1 -1
- package/lib/pendingStateManager.d.ts +22 -8
- package/lib/pendingStateManager.d.ts.map +1 -1
- package/lib/pendingStateManager.js +11 -16
- package/lib/pendingStateManager.js.map +1 -1
- package/package.json +18 -18
- package/src/blobManager/blobManager.ts +141 -33
- package/src/channelCollection.ts +42 -6
- package/src/compatUtils.ts +53 -30
- package/src/containerRuntime.ts +102 -81
- package/src/dataStoreContext.ts +44 -25
- package/src/index.ts +1 -0
- package/src/opLifecycle/index.ts +1 -0
- package/src/opLifecycle/outbox.ts +42 -33
- package/src/packageVersion.ts +1 -1
- 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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
137
|
-
|
|
173
|
+
export interface IBlobManagerEvents {
|
|
174
|
+
noPendingBlobs: () => void;
|
|
138
175
|
}
|
|
139
176
|
|
|
140
|
-
interface IBlobManagerInternalEvents
|
|
141
|
-
(
|
|
142
|
-
|
|
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 =
|
|
161
|
-
public get events():
|
|
198
|
+
private readonly publicEvents = createEmitter<IBlobManagerEvents>();
|
|
199
|
+
public get events(): Listenable<IBlobManagerEvents> {
|
|
162
200
|
return this.publicEvents;
|
|
163
201
|
}
|
|
164
|
-
private readonly internalEvents =
|
|
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
|
-
|
|
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
|
|
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
|
|
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<
|
|
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<
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
894
|
-
|
|
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
|
|
1012
|
+
const onHandleAttached = (attachedEntry: PendingBlob): void => {
|
|
905
1013
|
if (attachedEntry === entry) {
|
|
906
|
-
this.internalEvents.off("handleAttached",
|
|
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",
|
|
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(
|
|
1029
|
+
await Promise.allSettled(attachHandlesP);
|
|
922
1030
|
}
|
|
923
1031
|
|
|
924
1032
|
for (const [localId, entry] of this.pendingBlobs) {
|
package/src/channelCollection.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
1706
|
+
0xb8f /* we don't support the layer boundary here today */,
|
|
1671
1707
|
);
|
|
1672
1708
|
|
|
1673
1709
|
const runtime = new ChannelCollection(
|
package/src/compatUtils.ts
CHANGED
|
@@ -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
|
|
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 `
|
|
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
|
|
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
|
|
88
|
-
* to the appropriate default value for that property to supporting that
|
|
89
|
-
* the format changes introduced by the property, then the default value for that
|
|
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
|
|
93
|
-
* clients do not understand the document format when batching is enabled. If 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
|
|
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
|
-
// `
|
|
125
|
-
// `
|
|
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
|
-
// `
|
|
128
|
-
// Only enable as a default when `
|
|
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
|
|
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
|
|
174
|
+
* Returns the default RuntimeOptionsAffectingDocSchema configuration for a given minVersionForCollab.
|
|
154
175
|
*/
|
|
155
|
-
export function
|
|
156
|
-
|
|
176
|
+
export function getMinVersionForCollabDefaults(
|
|
177
|
+
minVersionForCollab: MinimumVersionForCollab,
|
|
157
178
|
): RuntimeOptionsAffectingDocSchema {
|
|
158
179
|
return getConfigsForCompatMode(
|
|
159
|
-
|
|
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
|
|
188
|
+
* Returns a default configuration given minVersionForCollab and configuration version map.
|
|
168
189
|
*/
|
|
169
190
|
export function getConfigsForCompatMode<T extends Record<SemanticVersion, unknown>>(
|
|
170
|
-
|
|
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
|
|
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
|
|
202
|
+
// value that is compatible with the version specified as the minVersionForCollab.
|
|
182
203
|
for (const version of versions) {
|
|
183
|
-
if (gte(
|
|
204
|
+
if (gte(minVersionForCollab, version)) {
|
|
184
205
|
defaultConfigs[key] = config[version as MinimumMinorSemanticVersion];
|
|
185
206
|
} else {
|
|
186
|
-
// If the
|
|
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
|
|
197
|
-
* A valid
|
|
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
|
|
220
|
+
export function isValidMinVersionForCollab(
|
|
221
|
+
minVersionForCollab: MinimumVersionForCollab,
|
|
222
|
+
): boolean {
|
|
200
223
|
return (
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
lte(
|
|
224
|
+
valid(minVersionForCollab) !== null &&
|
|
225
|
+
gte(minVersionForCollab, lowestMinVersionForCollab) &&
|
|
226
|
+
lte(minVersionForCollab, pkgVersion)
|
|
204
227
|
);
|
|
205
228
|
}
|