@fluidframework/container-loader 2.93.0 → 2.101.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 +12 -0
- package/api-report/container-loader.legacy.alpha.api.md +13 -1
- package/dist/captureReferencedContents.d.ts +154 -0
- package/dist/captureReferencedContents.d.ts.map +1 -0
- package/dist/captureReferencedContents.js +349 -0
- package/dist/captureReferencedContents.js.map +1 -0
- package/dist/connectionManager.d.ts +2 -1
- package/dist/connectionManager.d.ts.map +1 -1
- package/dist/connectionManager.js +39 -8
- package/dist/connectionManager.js.map +1 -1
- package/dist/connectionStateHandler.d.ts.map +1 -1
- package/dist/connectionStateHandler.js +3 -1
- package/dist/connectionStateHandler.js.map +1 -1
- package/dist/container.d.ts.map +1 -1
- package/dist/container.js +13 -4
- package/dist/container.js.map +1 -1
- package/dist/containerStorageAdapter.d.ts +20 -2
- package/dist/containerStorageAdapter.d.ts.map +1 -1
- package/dist/containerStorageAdapter.js +2 -2
- package/dist/containerStorageAdapter.js.map +1 -1
- package/dist/createAndLoadContainerUtils.d.ts +95 -0
- package/dist/createAndLoadContainerUtils.d.ts.map +1 -1
- package/dist/createAndLoadContainerUtils.js +137 -11
- package/dist/createAndLoadContainerUtils.js.map +1 -1
- package/dist/frozenServices.d.ts +113 -30
- package/dist/frozenServices.d.ts.map +1 -1
- package/dist/frozenServices.js +236 -58
- package/dist/frozenServices.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/legacyAlpha.d.ts +2 -0
- package/dist/loader.d.ts +1 -1
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +1 -1
- package/dist/loader.js.map +1 -1
- package/dist/loaderLayerCompatState.d.ts +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/pendingLocalStateStore.d.ts.map +1 -1
- package/dist/pendingLocalStateStore.js +9 -3
- package/dist/pendingLocalStateStore.js.map +1 -1
- package/dist/retriableDocumentStorageService.d.ts +2 -1
- package/dist/retriableDocumentStorageService.d.ts.map +1 -1
- package/dist/retriableDocumentStorageService.js +3 -2
- package/dist/retriableDocumentStorageService.js.map +1 -1
- package/dist/serializedStateManager.d.ts +16 -1
- package/dist/serializedStateManager.d.ts.map +1 -1
- package/dist/serializedStateManager.js +11 -1
- package/dist/serializedStateManager.js.map +1 -1
- package/lib/captureReferencedContents.d.ts +154 -0
- package/lib/captureReferencedContents.d.ts.map +1 -0
- package/lib/captureReferencedContents.js +338 -0
- package/lib/captureReferencedContents.js.map +1 -0
- package/lib/connectionManager.d.ts +2 -1
- package/lib/connectionManager.d.ts.map +1 -1
- package/lib/connectionManager.js +40 -9
- package/lib/connectionManager.js.map +1 -1
- package/lib/connectionStateHandler.d.ts.map +1 -1
- package/lib/connectionStateHandler.js +3 -1
- package/lib/connectionStateHandler.js.map +1 -1
- package/lib/container.d.ts.map +1 -1
- package/lib/container.js +14 -5
- package/lib/container.js.map +1 -1
- package/lib/containerStorageAdapter.d.ts +20 -2
- package/lib/containerStorageAdapter.d.ts.map +1 -1
- package/lib/containerStorageAdapter.js +2 -2
- package/lib/containerStorageAdapter.js.map +1 -1
- package/lib/createAndLoadContainerUtils.d.ts +95 -0
- package/lib/createAndLoadContainerUtils.d.ts.map +1 -1
- package/lib/createAndLoadContainerUtils.js +128 -3
- package/lib/createAndLoadContainerUtils.js.map +1 -1
- package/lib/frozenServices.d.ts +113 -30
- package/lib/frozenServices.d.ts.map +1 -1
- package/lib/frozenServices.js +233 -57
- package/lib/frozenServices.js.map +1 -1
- package/lib/index.d.ts +2 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +2 -1
- package/lib/index.js.map +1 -1
- package/lib/legacyAlpha.d.ts +2 -0
- package/lib/loader.d.ts +1 -1
- package/lib/loader.d.ts.map +1 -1
- package/lib/loader.js +2 -2
- package/lib/loader.js.map +1 -1
- package/lib/loaderLayerCompatState.d.ts +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/pendingLocalStateStore.d.ts.map +1 -1
- package/lib/pendingLocalStateStore.js +9 -3
- package/lib/pendingLocalStateStore.js.map +1 -1
- package/lib/retriableDocumentStorageService.d.ts +2 -1
- package/lib/retriableDocumentStorageService.d.ts.map +1 -1
- package/lib/retriableDocumentStorageService.js +3 -2
- package/lib/retriableDocumentStorageService.js.map +1 -1
- package/lib/serializedStateManager.d.ts +16 -1
- package/lib/serializedStateManager.d.ts.map +1 -1
- package/lib/serializedStateManager.js +11 -1
- package/lib/serializedStateManager.js.map +1 -1
- package/package.json +13 -13
- package/src/captureReferencedContents.ts +446 -0
- package/src/connectionManager.ts +47 -8
- package/src/connectionStateHandler.ts +14 -9
- package/src/container.ts +18 -4
- package/src/containerStorageAdapter.ts +22 -2
- package/src/createAndLoadContainerUtils.ts +229 -2
- package/src/frozenServices.ts +285 -64
- package/src/index.ts +7 -0
- package/src/loader.ts +4 -2
- package/src/packageVersion.ts +1 -1
- package/src/pendingLocalStateStore.ts +8 -1
- package/src/retriableDocumentStorageService.ts +11 -4
- package/src/serializedStateManager.ts +28 -1
package/src/connectionManager.ts
CHANGED
|
@@ -63,7 +63,11 @@ import {
|
|
|
63
63
|
ReconnectMode,
|
|
64
64
|
} from "./contracts.js";
|
|
65
65
|
import { DeltaQueue } from "./deltaQueue.js";
|
|
66
|
-
import {
|
|
66
|
+
import {
|
|
67
|
+
FrozenDeltaStream,
|
|
68
|
+
isFrozenDeltaStreamConnection,
|
|
69
|
+
isWritableFrozenDeltaStreamConnection,
|
|
70
|
+
} from "./frozenServices.js";
|
|
67
71
|
import { SignalType } from "./protocol.js";
|
|
68
72
|
import { isDeltaStreamConnectionForbiddenError } from "./utils.js";
|
|
69
73
|
|
|
@@ -344,6 +348,7 @@ export class ConnectionManager implements IConnectionManager {
|
|
|
344
348
|
reconnectAllowed: boolean,
|
|
345
349
|
private readonly logger: ITelemetryLoggerExt,
|
|
346
350
|
private readonly props: IConnectionManagerFactoryArgs,
|
|
351
|
+
private maxInitialConnectionAttempts?: number,
|
|
347
352
|
) {
|
|
348
353
|
this.clientDetails = this.client.details;
|
|
349
354
|
this.defaultReconnectionMode = this.client.mode;
|
|
@@ -581,9 +586,9 @@ export class ConnectionManager implements IConnectionManager {
|
|
|
581
586
|
LogLevel.verbose,
|
|
582
587
|
);
|
|
583
588
|
if (isDeltaStreamConnectionForbiddenError(origError)) {
|
|
584
|
-
connection = new FrozenDeltaStream(
|
|
585
|
-
|
|
586
|
-
error: origError,
|
|
589
|
+
connection = new FrozenDeltaStream({
|
|
590
|
+
storageOnlyReason: origError.storageOnlyReason,
|
|
591
|
+
readonlyConnectionReason: { text: origError.message, error: origError },
|
|
587
592
|
});
|
|
588
593
|
requestedMode = "read";
|
|
589
594
|
break;
|
|
@@ -591,11 +596,10 @@ export class ConnectionManager implements IConnectionManager {
|
|
|
591
596
|
isFluidError(origError) &&
|
|
592
597
|
origError.errorType === DriverErrorTypes.outOfStorageError
|
|
593
598
|
) {
|
|
594
|
-
// If we get out of storage error from calling joinsession, then use the
|
|
599
|
+
// If we get out of storage error from calling joinsession, then use the FrozenDeltaStream object so
|
|
595
600
|
// that user can at least load the container.
|
|
596
|
-
connection = new FrozenDeltaStream(
|
|
597
|
-
text: origError.message,
|
|
598
|
-
error: origError,
|
|
601
|
+
connection = new FrozenDeltaStream({
|
|
602
|
+
readonlyConnectionReason: { text: origError.message, error: origError },
|
|
599
603
|
});
|
|
600
604
|
requestedMode = "read";
|
|
601
605
|
break;
|
|
@@ -622,6 +626,17 @@ export class ConnectionManager implements IConnectionManager {
|
|
|
622
626
|
|
|
623
627
|
lastError = origError;
|
|
624
628
|
|
|
629
|
+
// When maxInitialConnectionAttempts is set, do not retry beyond the allowed attempts.
|
|
630
|
+
// The consumer will own the retry policy.
|
|
631
|
+
if (
|
|
632
|
+
this.maxInitialConnectionAttempts !== undefined &&
|
|
633
|
+
connectRepeatCount >= this.maxInitialConnectionAttempts
|
|
634
|
+
) {
|
|
635
|
+
const error = normalizeError(origError, { props: fatalConnectErrorProp });
|
|
636
|
+
this.props.closeHandler(error);
|
|
637
|
+
throw error;
|
|
638
|
+
}
|
|
639
|
+
|
|
625
640
|
// We will not perform retries if the container disconnected and the ReconnectMode is set to Disabled or Never
|
|
626
641
|
// so break out of the re-connecting while-loop after first attempt
|
|
627
642
|
if (this.reconnectMode !== ReconnectMode.Enabled) {
|
|
@@ -688,6 +703,11 @@ export class ConnectionManager implements IConnectionManager {
|
|
|
688
703
|
return;
|
|
689
704
|
}
|
|
690
705
|
|
|
706
|
+
// Clear the max connection attempts limit now that a connection has been established.
|
|
707
|
+
// The limit is only intended to scope initial connection retries;
|
|
708
|
+
// once connected, normal reconnect behavior should apply.
|
|
709
|
+
this.maxInitialConnectionAttempts = undefined;
|
|
710
|
+
|
|
691
711
|
this.setupNewSuccessfulConnection(connection, requestedMode, reason);
|
|
692
712
|
}
|
|
693
713
|
|
|
@@ -1072,6 +1092,25 @@ export class ConnectionManager implements IConnectionManager {
|
|
|
1072
1092
|
|
|
1073
1093
|
public sendMessages(messages: IDocumentMessage[]): void {
|
|
1074
1094
|
assert(this.connected, 0x2b4 /* "not connected on sending ops!" */);
|
|
1095
|
+
// WritableFrozenDeltaStream short-circuit: writable-frozen containers
|
|
1096
|
+
// (`loadFrozenContainerFromPendingState({ readOnly: false })`) attach a
|
|
1097
|
+
// WritableFrozenDeltaStream as the live connection. Its `mode` is "read" (advertising
|
|
1098
|
+
// "write" would imply quorum membership we cannot honor), so a runtime submit
|
|
1099
|
+
// would otherwise fall into the read-mode reconnect branch below. That branch
|
|
1100
|
+
// schedules `reconnect("write")`, which under `ReconnectMode.Never`
|
|
1101
|
+
// (`allowReconnect: false`) calls `closeHandler` and closes the container — the
|
|
1102
|
+
// opposite of what writable-frozen wants. Drop the messages here: the runtime's
|
|
1103
|
+
// outbox keeps them in `pendingStateManager` so `getPendingLocalState()` can
|
|
1104
|
+
// capture them, which is the entire point of the writable-frozen flow.
|
|
1105
|
+
//
|
|
1106
|
+
// Match only the writable variant (a sibling class, not a subclass) so the read-only
|
|
1107
|
+
// `FrozenDeltaStream` retains its `submit` 403-nack tripwire — a stray submit on a
|
|
1108
|
+
// storage-only frozen connection signals an upstream invariant break and should
|
|
1109
|
+
// remain observable. The read-only variant shouldn't reach here in normal flow anyway
|
|
1110
|
+
// (its `storageOnly` policy keeps the runtime from submitting).
|
|
1111
|
+
if (isWritableFrozenDeltaStreamConnection(this.connection)) {
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1075
1114
|
// If connection is "read" or implicit "read" (got leave op for "write" connection),
|
|
1076
1115
|
// then op can't make it through - we will get a nack if op is sent.
|
|
1077
1116
|
// We can short-circuit this process.
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import type { IDeltaManager } from "@fluidframework/container-definitions/internal";
|
|
7
7
|
import type { ITelemetryBaseProperties } from "@fluidframework/core-interfaces";
|
|
8
|
+
import { LogLevel } from "@fluidframework/core-interfaces";
|
|
8
9
|
import { assert, Timer } from "@fluidframework/core-utils/internal";
|
|
9
10
|
import type { IClient, ISequencedClient } from "@fluidframework/driver-definitions";
|
|
10
11
|
import type { IAnyDriverError } from "@fluidframework/driver-definitions/internal";
|
|
@@ -687,15 +688,19 @@ export class ConnectionStateHandler implements IConnectionStateHandler {
|
|
|
687
688
|
this.prevClientLeftTimer.restart();
|
|
688
689
|
} else {
|
|
689
690
|
// Adding this event temporarily so that we can get help debugging if something goes wrong.
|
|
690
|
-
this.handler.logger.sendTelemetryEvent(
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
691
|
+
this.handler.logger.sendTelemetryEvent(
|
|
692
|
+
{
|
|
693
|
+
eventName: "noWaitOnDisconnected",
|
|
694
|
+
details: JSON.stringify({
|
|
695
|
+
clientId: this._clientId,
|
|
696
|
+
inQuorum: currentClientInQuorum,
|
|
697
|
+
waitingForLeaveOp: this.waitingForLeaveOp,
|
|
698
|
+
hadOutstandingOps: this.handler.shouldClientJoinWrite(),
|
|
699
|
+
}),
|
|
700
|
+
},
|
|
701
|
+
undefined, // error
|
|
702
|
+
LogLevel.info,
|
|
703
|
+
);
|
|
699
704
|
}
|
|
700
705
|
}
|
|
701
706
|
|
package/src/container.ts
CHANGED
|
@@ -84,7 +84,7 @@ import {
|
|
|
84
84
|
} from "@fluidframework/driver-utils/internal";
|
|
85
85
|
import {
|
|
86
86
|
type TelemetryEventCategory,
|
|
87
|
-
type
|
|
87
|
+
type TelemetryLoggerExt,
|
|
88
88
|
EventEmitterWithErrorHandling,
|
|
89
89
|
GenericError,
|
|
90
90
|
type IFluidErrorBase,
|
|
@@ -94,6 +94,7 @@ import {
|
|
|
94
94
|
connectedEventName,
|
|
95
95
|
createChildLogger,
|
|
96
96
|
createChildMonitoringContext,
|
|
97
|
+
extractTelemetryLoggerExt,
|
|
97
98
|
formatTick,
|
|
98
99
|
normalizeError,
|
|
99
100
|
raiseConnectedEvent,
|
|
@@ -429,7 +430,7 @@ export class Container
|
|
|
429
430
|
private readonly codeLoader: ICodeDetailsLoader;
|
|
430
431
|
private readonly options: ILoaderOptions;
|
|
431
432
|
private readonly scope: FluidObject;
|
|
432
|
-
private readonly subLogger:
|
|
433
|
+
private readonly subLogger: TelemetryLoggerExt;
|
|
433
434
|
private readonly detachedBlobStorage: MemoryDetachedBlobStorage | undefined;
|
|
434
435
|
private readonly protocolHandlerBuilder: InternalProtocolHandlerBuilder;
|
|
435
436
|
private readonly signalAudience = new Audience();
|
|
@@ -858,7 +859,7 @@ export class Container
|
|
|
858
859
|
logger: this.mc.logger,
|
|
859
860
|
// WARNING: logger on this context should not including getters like containerConnectionState above (on this.subLogger),
|
|
860
861
|
// as that will result in attempt to dereference this.connectionStateHandler from this call while it's still undefined.
|
|
861
|
-
mc: loggerToMonitoringContext(subLogger),
|
|
862
|
+
mc: loggerToMonitoringContext(extractTelemetryLoggerExt(subLogger)),
|
|
862
863
|
connectionStateChanged: (value, oldState, reason) => {
|
|
863
864
|
this.logConnectionStateChangeTelemetry(value, oldState, reason);
|
|
864
865
|
if (this.loaded) {
|
|
@@ -1069,6 +1070,7 @@ export class Container
|
|
|
1069
1070
|
|
|
1070
1071
|
this.connectionStateHandler.dispose();
|
|
1071
1072
|
this.serializedStateManager.dispose();
|
|
1073
|
+
this._runtime?.close?.();
|
|
1072
1074
|
} catch (newError) {
|
|
1073
1075
|
this.mc.logger.sendErrorEvent({ eventName: "ContainerCloseException" }, newError);
|
|
1074
1076
|
}
|
|
@@ -1103,6 +1105,8 @@ export class Container
|
|
|
1103
1105
|
eventName: "ContainerDispose",
|
|
1104
1106
|
// Only log error if container isn't closed
|
|
1105
1107
|
category: !this.closed && error !== undefined ? "error" : "generic",
|
|
1108
|
+
isDirty: this.isDirty,
|
|
1109
|
+
lastSequenceNumber: this._deltaManager.lastSequenceNumber,
|
|
1106
1110
|
},
|
|
1107
1111
|
error,
|
|
1108
1112
|
);
|
|
@@ -1609,7 +1613,11 @@ export class Container
|
|
|
1609
1613
|
this.connectToDeltaStream(connectionArgs);
|
|
1610
1614
|
}
|
|
1611
1615
|
|
|
1612
|
-
|
|
1616
|
+
// When DisableLoadConnectionRetries is enabled, use no internal retries.
|
|
1617
|
+
// The consumer will own the retry policy.
|
|
1618
|
+
const disableLoadRetries =
|
|
1619
|
+
this.mc.config.getBoolean("Fluid.Container.DisableLoadConnectionRetries") === true;
|
|
1620
|
+
this.storageAdapter.connectToService(this.service, disableLoadRetries ? 0 : undefined);
|
|
1613
1621
|
|
|
1614
1622
|
this.attachmentData = {
|
|
1615
1623
|
state: AttachState.Attached,
|
|
@@ -1994,6 +2002,8 @@ export class Container
|
|
|
1994
2002
|
|
|
1995
2003
|
private createDeltaManager(): DeltaManager<ConnectionManager> {
|
|
1996
2004
|
const serviceProvider = (): IDocumentService | undefined => this.service;
|
|
2005
|
+
const disableLoadConnectionRetries =
|
|
2006
|
+
this.mc.config.getBoolean("Fluid.Container.DisableLoadConnectionRetries") === true;
|
|
1997
2007
|
const deltaManager = new DeltaManager<ConnectionManager>(
|
|
1998
2008
|
serviceProvider,
|
|
1999
2009
|
createChildLogger({ logger: this.subLogger, namespace: "DeltaManager" }),
|
|
@@ -2006,6 +2016,7 @@ export class Container
|
|
|
2006
2016
|
this._canReconnect,
|
|
2007
2017
|
createChildLogger({ logger: this.subLogger, namespace: "ConnectionManager" }),
|
|
2008
2018
|
props,
|
|
2019
|
+
disableLoadConnectionRetries ? 1 : undefined /* maxInitialConnectionAttempts */,
|
|
2009
2020
|
),
|
|
2010
2021
|
);
|
|
2011
2022
|
|
|
@@ -2386,6 +2397,9 @@ export class Container
|
|
|
2386
2397
|
this.subLogger,
|
|
2387
2398
|
{ eventName: "CodeLoad" },
|
|
2388
2399
|
async () => this.codeLoader.load(codeDetails),
|
|
2400
|
+
undefined, // markers
|
|
2401
|
+
undefined, // sampleThreshold
|
|
2402
|
+
LogLevel.info,
|
|
2389
2403
|
);
|
|
2390
2404
|
|
|
2391
2405
|
this._loadedModule = {
|
|
@@ -36,13 +36,32 @@ import type {
|
|
|
36
36
|
import { convertSnapshotInfoToSnapshot } from "./utils.js";
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
|
-
* Stringified blobs from a summary/snapshot tree.
|
|
39
|
+
* Stringified blobs from a summary/snapshot tree, keyed by blob id.
|
|
40
|
+
* Values are **UTF-8-encoded** — this is the right encoding for JSON or
|
|
41
|
+
* other text the runtime authors and consumes through this map. For
|
|
42
|
+
* arbitrary binary payloads (e.g. attachment blob contents), use
|
|
43
|
+
* {@link IBase64BlobContents} instead; a UTF-8 round-trip silently
|
|
44
|
+
* corrupts non-UTF-8 byte sequences with replacement characters.
|
|
40
45
|
* @internal
|
|
41
46
|
*/
|
|
42
47
|
export interface ISerializableBlobContents {
|
|
43
48
|
[id: string]: string;
|
|
44
49
|
}
|
|
45
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Stringified blobs inlined in a summary/snapshot tree, keyed by blob id.
|
|
53
|
+
* Values are **base64-encoded** raw bytes. Used for attachment-blob
|
|
54
|
+
* payloads, which may carry arbitrary binary data (images, encrypted
|
|
55
|
+
* blobs, etc.). Mirrors the encoding used by the runtime's own
|
|
56
|
+
* pending-blob serializer in `BlobManager`. Structurally identical to
|
|
57
|
+
* {@link ISerializableBlobContents}; the two types exist to keep the
|
|
58
|
+
* encoding contract visible at every call site.
|
|
59
|
+
* @internal
|
|
60
|
+
*/
|
|
61
|
+
export interface IBase64BlobContents {
|
|
62
|
+
[id: string]: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
46
65
|
/**
|
|
47
66
|
* This class wraps the actual storage and make sure no wrong apis are called according to
|
|
48
67
|
* container attach state.
|
|
@@ -104,7 +123,7 @@ export class ContainerStorageAdapter
|
|
|
104
123
|
this.disposed = true;
|
|
105
124
|
}
|
|
106
125
|
|
|
107
|
-
public connectToService(service: IDocumentService): void {
|
|
126
|
+
public connectToService(service: IDocumentService, maxRetries?: number): void {
|
|
108
127
|
if (!(this._storageService instanceof BlobOnlyStorage)) {
|
|
109
128
|
return;
|
|
110
129
|
}
|
|
@@ -113,6 +132,7 @@ export class ContainerStorageAdapter
|
|
|
113
132
|
const retriableStorage = (this._storageService = new RetriableDocumentStorageService(
|
|
114
133
|
storageServiceP,
|
|
115
134
|
this.logger,
|
|
135
|
+
maxRetries,
|
|
116
136
|
));
|
|
117
137
|
|
|
118
138
|
// A storage service wrapper which intercept calls to uploadSummaryWithContext and ensure they include
|
|
@@ -19,11 +19,16 @@ import type {
|
|
|
19
19
|
import type { IClientDetails } from "@fluidframework/driver-definitions";
|
|
20
20
|
import type {
|
|
21
21
|
IDocumentServiceFactory,
|
|
22
|
+
ISequencedDocumentMessage,
|
|
23
|
+
ISnapshot,
|
|
24
|
+
ISnapshotTree,
|
|
22
25
|
IUrlResolver,
|
|
23
26
|
} from "@fluidframework/driver-definitions/internal";
|
|
24
|
-
import { DriverHeader } from "@fluidframework/driver-definitions/internal";
|
|
27
|
+
import { DriverHeader, FetchSource } from "@fluidframework/driver-definitions/internal";
|
|
28
|
+
import { getSnapshotTree } from "@fluidframework/driver-utils/internal";
|
|
25
29
|
import {
|
|
26
30
|
GenericError,
|
|
31
|
+
UsageError,
|
|
27
32
|
normalizeError,
|
|
28
33
|
createChildMonitoringContext,
|
|
29
34
|
mixinMonitoringContext,
|
|
@@ -33,16 +38,28 @@ import {
|
|
|
33
38
|
} from "@fluidframework/telemetry-utils/internal";
|
|
34
39
|
import { v4 as uuid } from "uuid";
|
|
35
40
|
|
|
41
|
+
import {
|
|
42
|
+
captureReferencedAttachmentBlobs,
|
|
43
|
+
extractBlobAttachReferences,
|
|
44
|
+
inlineAttachmentBlobsByReference,
|
|
45
|
+
parseGcSnapshotData,
|
|
46
|
+
readReferencedSnapshotBlobs,
|
|
47
|
+
snapshotHasLoadingGroups,
|
|
48
|
+
unreferencedAttachmentBlobLocalIds,
|
|
49
|
+
type IBlobAttachReference,
|
|
50
|
+
} from "./captureReferencedContents.js";
|
|
36
51
|
import { DebugLogger } from "./debugLogger.js";
|
|
37
52
|
import { createFrozenDocumentServiceFactory } from "./frozenServices.js";
|
|
38
53
|
import { Loader } from "./loader.js";
|
|
39
54
|
import { pkgVersion } from "./packageVersion.js";
|
|
40
55
|
import type { ProtocolHandlerBuilder } from "./protocol.js";
|
|
56
|
+
import type { IPendingContainerState } from "./serializedStateManager.js";
|
|
41
57
|
import type {
|
|
42
58
|
LoadSummarizerSummaryResult,
|
|
43
59
|
OnDemandSummaryResults,
|
|
44
60
|
SummarizeOnDemandResults,
|
|
45
61
|
} from "./summarizerResultTypes.js";
|
|
62
|
+
import { getDocumentAttributes } from "./utils.js";
|
|
46
63
|
|
|
47
64
|
interface OnDemandSummarizeResultsPromises {
|
|
48
65
|
readonly summarySubmitted: Promise<SummarizeOnDemandResults["summarySubmitted"]>;
|
|
@@ -229,6 +246,34 @@ export interface ILoadFrozenContainerFromPendingStateProps
|
|
|
229
246
|
* Pending local state to be applied to the container.
|
|
230
247
|
*/
|
|
231
248
|
readonly pendingLocalState: string;
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Controls whether the frozen container is surfaced as read-only.
|
|
252
|
+
*
|
|
253
|
+
* Defaults to `true`. When `true`, the container reports `readOnlyInfo.readonly === true`
|
|
254
|
+
* with `storageOnly === true`, matching the historical behavior of frozen loads.
|
|
255
|
+
*
|
|
256
|
+
* When `false`, the container loads as writable so the runtime will accept DDS submissions.
|
|
257
|
+
* The connection itself stays `Connected`: the connection manager recognizes the synthetic
|
|
258
|
+
* frozen delta stream and drops outbound messages at the network layer, so no read→write
|
|
259
|
+
* reconnect is attempted. Local DDS state continues to update via optimistic apply, and
|
|
260
|
+
* submitted ops accumulate in the runtime's pending-state manager. Use this when callers
|
|
261
|
+
* want to accrue and capture pending state without publishing it.
|
|
262
|
+
*
|
|
263
|
+
* @remarks
|
|
264
|
+
* The flag uses negative polarity (`readOnly`) rather than a positive opt-in (`writable`)
|
|
265
|
+
* to align with `IContainer.readOnlyInfo.readonly`, which is the established surface for
|
|
266
|
+
* read/write state on a loaded container. A future positive-polarity option can layer on
|
|
267
|
+
* top of this without breaking callers, but flipping the polarity now would split readers
|
|
268
|
+
* between two conventions for the same concept.
|
|
269
|
+
*
|
|
270
|
+
* Subsystem behavior is unchanged from the read-only frozen path regardless of `readOnly`:
|
|
271
|
+
* storage operations still throw (only `readBlob` is supported); summarizer / id-compressor
|
|
272
|
+
* never fire because no acks arrive; the quorum is whatever was captured in pending state
|
|
273
|
+
* and gains no members during the writable-frozen lifetime. The only behavioral delta is
|
|
274
|
+
* that the runtime accepts DDS submissions and accumulates them in `pendingStateManager`.
|
|
275
|
+
*/
|
|
276
|
+
readonly readOnly?: boolean;
|
|
232
277
|
}
|
|
233
278
|
|
|
234
279
|
/**
|
|
@@ -241,10 +286,192 @@ export async function loadFrozenContainerFromPendingState(
|
|
|
241
286
|
): Promise<IContainer> {
|
|
242
287
|
return loadExistingContainer({
|
|
243
288
|
...props,
|
|
244
|
-
documentServiceFactory: createFrozenDocumentServiceFactory(
|
|
289
|
+
documentServiceFactory: createFrozenDocumentServiceFactory(
|
|
290
|
+
props.documentServiceFactory,
|
|
291
|
+
props.readOnly,
|
|
292
|
+
),
|
|
245
293
|
});
|
|
246
294
|
}
|
|
247
295
|
|
|
296
|
+
/**
|
|
297
|
+
* Properties for {@link captureFullContainerState}.
|
|
298
|
+
* @legacy @alpha
|
|
299
|
+
*/
|
|
300
|
+
export interface ICaptureFullContainerStateProps {
|
|
301
|
+
/**
|
|
302
|
+
* The url resolver used to resolve the request into a Fluid resolved url.
|
|
303
|
+
*/
|
|
304
|
+
readonly urlResolver: IUrlResolver;
|
|
305
|
+
/**
|
|
306
|
+
* The document service factory used to construct the driver services
|
|
307
|
+
* against which the state is captured.
|
|
308
|
+
*/
|
|
309
|
+
readonly documentServiceFactory: IDocumentServiceFactory;
|
|
310
|
+
/**
|
|
311
|
+
* The request identifying the container whose state is to be captured.
|
|
312
|
+
*/
|
|
313
|
+
readonly request: IRequest;
|
|
314
|
+
/**
|
|
315
|
+
* Optional logger for driver-side telemetry.
|
|
316
|
+
*/
|
|
317
|
+
readonly logger?: ITelemetryBaseLogger | undefined;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Captures the current state of an attached container using only driver-level
|
|
322
|
+
* services, without instantiating a runtime or loading a full container. The
|
|
323
|
+
* returned string is a serialized pending container state in the same wire
|
|
324
|
+
* format produced by a live container's pending-state serialization, and can
|
|
325
|
+
* be handed to {@link loadExistingContainer} as `pendingLocalState`.
|
|
326
|
+
*
|
|
327
|
+
* The output is a self-contained view of the container's referenced graph:
|
|
328
|
+
* the latest snapshot, inlined contents of every blob reachable through
|
|
329
|
+
* referenced subtrees, inlined contents of every referenced attachment blob
|
|
330
|
+
* keyed by storage id, and all ops with sequence numbers after the base
|
|
331
|
+
* snapshot's sequence number (as read from its attributes blob).
|
|
332
|
+
*
|
|
333
|
+
* Reachability respects GC. Snapshot subtrees flagged `unreferenced: true`
|
|
334
|
+
* are skipped (their contents are not inlined). Attachment blobs that GC has
|
|
335
|
+
* marked unreferenced, tombstoned, or deleted are skipped. When the snapshot
|
|
336
|
+
* has no GC tree (GC disabled or pre-GC document), no filtering is applied.
|
|
337
|
+
*
|
|
338
|
+
* Blob reads on load hit the `ContainerStorageAdapter` cache populated from
|
|
339
|
+
* the captured `snapshotBlobs` map, so a frozen loader can serve the full
|
|
340
|
+
* referenced graph without a live storage service.
|
|
341
|
+
*
|
|
342
|
+
* `pendingRuntimeState` is `undefined` — no runtime is instantiated — so the
|
|
343
|
+
* output cannot carry DDS-level in-flight changes. It is intended for state
|
|
344
|
+
* relay, inspection, and durable-state snapshot use cases.
|
|
345
|
+
*
|
|
346
|
+
* Containers that declare loading groups are not yet supported: the function
|
|
347
|
+
* throws `UsageError` if any referenced subtree carries a `groupId`. Group
|
|
348
|
+
* snapshots would need a separate prefetch + serialization path; until there
|
|
349
|
+
* is a known consumer and end-to-end coverage, the capture refuses rather
|
|
350
|
+
* than silently producing pending state that omits group data.
|
|
351
|
+
*
|
|
352
|
+
* Note: if a new snapshot lands between the snapshot fetch and the ops fetch,
|
|
353
|
+
* the returned state may not reflect the very latest snapshot, but remains
|
|
354
|
+
* internally consistent: ops are anchored to the snapshot that was captured.
|
|
355
|
+
*
|
|
356
|
+
* No `mixinMonitoringContext` / `configProvider` is wired here, deliberately
|
|
357
|
+
* diverging from the sibling entry points in this file. The function reads
|
|
358
|
+
* no feature flags and instantiates no runtime, so there is nothing for a
|
|
359
|
+
* monitoring context to gate or attribute. If a future change introduces
|
|
360
|
+
* config-gated behavior or runtime-attributed telemetry, add the wiring
|
|
361
|
+
* back together with that change.
|
|
362
|
+
* @legacy @alpha
|
|
363
|
+
*/
|
|
364
|
+
export async function captureFullContainerState({
|
|
365
|
+
urlResolver,
|
|
366
|
+
documentServiceFactory,
|
|
367
|
+
request,
|
|
368
|
+
logger,
|
|
369
|
+
}: ICaptureFullContainerStateProps): Promise<string> {
|
|
370
|
+
const resolvedUrl = await urlResolver.resolve(request);
|
|
371
|
+
if (resolvedUrl === undefined) {
|
|
372
|
+
throw new UsageError("Failed to resolve request to a Fluid URL");
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const documentService = await documentServiceFactory.createDocumentService(
|
|
376
|
+
resolvedUrl,
|
|
377
|
+
logger,
|
|
378
|
+
);
|
|
379
|
+
try {
|
|
380
|
+
const storage = await documentService.connectToStorage();
|
|
381
|
+
|
|
382
|
+
const versions = await storage.getVersions(
|
|
383
|
+
// `null` signals "latest"
|
|
384
|
+
// eslint-disable-next-line unicorn/no-null
|
|
385
|
+
null,
|
|
386
|
+
1,
|
|
387
|
+
"captureFullContainerState",
|
|
388
|
+
FetchSource.noCache,
|
|
389
|
+
);
|
|
390
|
+
const version = versions[0];
|
|
391
|
+
const snapshot: ISnapshot | ISnapshotTree | undefined =
|
|
392
|
+
storage.getSnapshot === undefined
|
|
393
|
+
? ((await storage.getSnapshotTree(version, "captureFullContainerState")) ?? undefined)
|
|
394
|
+
: await storage.getSnapshot({
|
|
395
|
+
cacheSnapshot: false,
|
|
396
|
+
versionId: version?.id,
|
|
397
|
+
scenarioName: "captureFullContainerState",
|
|
398
|
+
});
|
|
399
|
+
if (snapshot === undefined) {
|
|
400
|
+
throw new GenericError("Failed to fetch snapshot for captureFullContainerState");
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const baseSnapshot = getSnapshotTree(snapshot);
|
|
404
|
+
if (snapshotHasLoadingGroups(baseSnapshot)) {
|
|
405
|
+
throw new UsageError(
|
|
406
|
+
"captureFullContainerState does not yet support containers with loading groups",
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
const attributes = await getDocumentAttributes(storage, baseSnapshot);
|
|
410
|
+
const gcData = await parseGcSnapshotData(baseSnapshot, storage);
|
|
411
|
+
// Structural snapshot blobs (JSON/text the runtime authored) are
|
|
412
|
+
// UTF-8-encoded; attachment blobs may carry arbitrary binary bytes
|
|
413
|
+
// and are base64-encoded. Keep them on separate fields of the
|
|
414
|
+
// pending state so the load side can apply the matching decoder
|
|
415
|
+
// without ambiguity. See IPendingContainerState.attachmentBlobContents.
|
|
416
|
+
const [snapshotBlobs, attachmentBlobContents] = await Promise.all([
|
|
417
|
+
readReferencedSnapshotBlobs(snapshot, storage), // utf8 encoded
|
|
418
|
+
captureReferencedAttachmentBlobs(baseSnapshot, storage, gcData), // base64 encoded
|
|
419
|
+
]);
|
|
420
|
+
|
|
421
|
+
const deltaStorage = await documentService.connectToDeltaStorage();
|
|
422
|
+
const opsStream = deltaStorage.fetchMessages(
|
|
423
|
+
attributes.sequenceNumber + 1,
|
|
424
|
+
undefined,
|
|
425
|
+
undefined,
|
|
426
|
+
false,
|
|
427
|
+
"captureFullContainerState",
|
|
428
|
+
);
|
|
429
|
+
const savedOps: ISequencedDocumentMessage[] = [];
|
|
430
|
+
const postSnapshotBlobReferences: IBlobAttachReference[] = [];
|
|
431
|
+
let opsResult = await opsStream.read();
|
|
432
|
+
while (!opsResult.done) {
|
|
433
|
+
for (const op of opsResult.value) {
|
|
434
|
+
savedOps.push(op);
|
|
435
|
+
// Blobs uploaded after the base snapshot are not in its
|
|
436
|
+
// `.blobs` redirect table, so `captureReferencedAttachmentBlobs`
|
|
437
|
+
// did not see them. The wire-format BlobAttach op carries
|
|
438
|
+
// `(localId, storageId)` in its metadata; collect those here so
|
|
439
|
+
// we can backfill the bytes before sealing the artifact.
|
|
440
|
+
const refs = extractBlobAttachReferences(op);
|
|
441
|
+
if (refs.length > 0) {
|
|
442
|
+
postSnapshotBlobReferences.push(...refs);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
opsResult = await opsStream.read();
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (postSnapshotBlobReferences.length > 0) {
|
|
449
|
+
const added = await inlineAttachmentBlobsByReference(
|
|
450
|
+
postSnapshotBlobReferences,
|
|
451
|
+
storage,
|
|
452
|
+
unreferencedAttachmentBlobLocalIds(gcData),
|
|
453
|
+
attachmentBlobContents,
|
|
454
|
+
);
|
|
455
|
+
Object.assign(attachmentBlobContents, added);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const pendingState: IPendingContainerState = {
|
|
459
|
+
attached: true,
|
|
460
|
+
baseSnapshot,
|
|
461
|
+
snapshotBlobs,
|
|
462
|
+
attachmentBlobContents:
|
|
463
|
+
Object.keys(attachmentBlobContents).length === 0 ? undefined : attachmentBlobContents,
|
|
464
|
+
loadedGroupIdSnapshots: undefined,
|
|
465
|
+
pendingRuntimeState: undefined,
|
|
466
|
+
savedOps,
|
|
467
|
+
url: resolvedUrl.url,
|
|
468
|
+
};
|
|
469
|
+
return JSON.stringify(pendingState);
|
|
470
|
+
} finally {
|
|
471
|
+
documentService.dispose();
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
248
475
|
/**
|
|
249
476
|
* Loads a summarizer container with the required headers, triggers an on-demand summary, and then closes it.
|
|
250
477
|
* Returns success/failure and an optional error for host-side handling.
|