@fluidframework/container-loader 1.4.0-115997 → 2.0.0-dev-rc.1.0.0.224419
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/.eslintrc.js +18 -21
- package/.mocharc.js +12 -0
- package/CHANGELOG.md +364 -0
- package/README.md +152 -56
- package/api-extractor-lint.json +4 -0
- package/api-extractor.json +2 -2
- package/api-report/container-loader.api.md +143 -0
- package/dist/{audience.js → audience.cjs} +15 -13
- package/dist/audience.cjs.map +1 -0
- package/dist/audience.d.ts +3 -6
- package/dist/audience.d.ts.map +1 -1
- package/dist/catchUpMonitor.cjs +43 -0
- package/dist/catchUpMonitor.cjs.map +1 -0
- package/dist/catchUpMonitor.d.ts +29 -0
- package/dist/catchUpMonitor.d.ts.map +1 -0
- package/dist/{connectionManager.js → connectionManager.cjs} +397 -240
- package/dist/connectionManager.cjs.map +1 -0
- package/dist/connectionManager.d.ts +23 -33
- package/dist/connectionManager.d.ts.map +1 -1
- package/dist/{connectionState.js → connectionState.cjs} +5 -7
- package/dist/connectionState.cjs.map +1 -0
- package/dist/connectionState.d.ts +3 -5
- package/dist/connectionState.d.ts.map +1 -1
- package/dist/connectionStateHandler.cjs +474 -0
- package/dist/connectionStateHandler.cjs.map +1 -0
- package/dist/connectionStateHandler.d.ts +127 -29
- package/dist/connectionStateHandler.d.ts.map +1 -1
- package/dist/container-loader-alpha.d.ts +274 -0
- package/dist/container-loader-beta.d.ts +75 -0
- package/dist/container-loader-public.d.ts +75 -0
- package/dist/container-loader-untrimmed.d.ts +331 -0
- package/dist/container.cjs +1585 -0
- package/dist/container.cjs.map +1 -0
- package/dist/container.d.ts +227 -83
- package/dist/container.d.ts.map +1 -1
- package/dist/containerContext.cjs +74 -0
- package/dist/containerContext.cjs.map +1 -0
- package/dist/containerContext.d.ts +33 -59
- package/dist/containerContext.d.ts.map +1 -1
- package/dist/containerStorageAdapter.cjs +234 -0
- package/dist/containerStorageAdapter.cjs.map +1 -0
- package/dist/containerStorageAdapter.d.ts +48 -23
- package/dist/containerStorageAdapter.d.ts.map +1 -1
- package/dist/{contracts.js → contracts.cjs} +5 -5
- package/dist/contracts.cjs.map +1 -0
- package/dist/contracts.d.ts +45 -17
- package/dist/contracts.d.ts.map +1 -1
- package/dist/debugLogger.cjs +101 -0
- package/dist/debugLogger.cjs.map +1 -0
- package/dist/debugLogger.d.ts +30 -0
- package/dist/debugLogger.d.ts.map +1 -0
- package/dist/{deltaManager.js → deltaManager.cjs} +379 -186
- package/dist/deltaManager.cjs.map +1 -0
- package/dist/deltaManager.d.ts +54 -18
- package/dist/deltaManager.d.ts.map +1 -1
- package/dist/{deltaQueue.js → deltaQueue.cjs} +29 -28
- package/dist/deltaQueue.cjs.map +1 -0
- package/dist/deltaQueue.d.ts +3 -4
- package/dist/deltaQueue.d.ts.map +1 -1
- package/dist/disposal.cjs +25 -0
- package/dist/disposal.cjs.map +1 -0
- package/dist/disposal.d.ts +13 -0
- package/dist/disposal.d.ts.map +1 -0
- package/dist/error.cjs +32 -0
- package/dist/error.cjs.map +1 -0
- package/dist/error.d.ts +23 -0
- package/dist/error.d.ts.map +1 -0
- package/dist/index.cjs +19 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/loader.cjs +148 -0
- package/dist/loader.cjs.map +1 -0
- package/dist/loader.d.ts +38 -19
- package/dist/loader.d.ts.map +1 -1
- package/dist/location-redirection-utilities/index.cjs +11 -0
- package/dist/location-redirection-utilities/index.cjs.map +1 -0
- package/dist/location-redirection-utilities/index.d.ts +6 -0
- package/dist/location-redirection-utilities/index.d.ts.map +1 -0
- package/dist/location-redirection-utilities/resolveWithLocationRedirection.cjs +53 -0
- package/dist/location-redirection-utilities/resolveWithLocationRedirection.cjs.map +1 -0
- package/dist/location-redirection-utilities/resolveWithLocationRedirection.d.ts +24 -0
- package/dist/location-redirection-utilities/resolveWithLocationRedirection.d.ts.map +1 -0
- package/dist/{collabWindowTracker.js → noopHeuristic.cjs} +37 -39
- package/dist/noopHeuristic.cjs.map +1 -0
- package/dist/noopHeuristic.d.ts +23 -0
- package/dist/noopHeuristic.d.ts.map +1 -0
- package/dist/{packageVersion.js → packageVersion.cjs} +2 -2
- package/dist/packageVersion.cjs.map +1 -0
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.d.ts.map +1 -1
- package/dist/protocol.cjs +99 -0
- package/dist/protocol.cjs.map +1 -0
- package/dist/protocol.d.ts +38 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/{protocolTreeDocumentStorageService.js → protocolTreeDocumentStorageService.cjs} +8 -5
- package/dist/protocolTreeDocumentStorageService.cjs.map +1 -0
- package/dist/protocolTreeDocumentStorageService.d.ts +8 -4
- package/dist/protocolTreeDocumentStorageService.d.ts.map +1 -1
- package/dist/quorum.cjs +16 -0
- package/dist/quorum.cjs.map +1 -0
- package/dist/quorum.d.ts +1 -14
- package/dist/quorum.d.ts.map +1 -1
- package/dist/{retriableDocumentStorageService.js → retriableDocumentStorageService.cjs} +36 -21
- package/dist/retriableDocumentStorageService.cjs.map +1 -0
- package/dist/retriableDocumentStorageService.d.ts +7 -5
- package/dist/retriableDocumentStorageService.d.ts.map +1 -1
- package/dist/tsdoc-metadata.json +11 -0
- package/dist/{utils.js → utils.cjs} +52 -14
- package/dist/utils.cjs.map +1 -0
- package/dist/utils.d.ts +34 -1
- package/dist/utils.d.ts.map +1 -1
- package/lib/{audience.d.ts → audience.d.mts} +3 -10
- package/lib/audience.d.mts.map +1 -0
- package/lib/{audience.js → audience.mjs} +15 -17
- package/lib/audience.mjs.map +1 -0
- package/lib/catchUpMonitor.d.mts +29 -0
- package/lib/catchUpMonitor.d.mts.map +1 -0
- package/lib/catchUpMonitor.mjs +39 -0
- package/lib/catchUpMonitor.mjs.map +1 -0
- package/lib/{connectionManager.d.ts → connectionManager.d.mts} +23 -33
- package/lib/connectionManager.d.mts.map +1 -0
- package/lib/{connectionManager.js → connectionManager.mjs} +378 -218
- package/lib/connectionManager.mjs.map +1 -0
- package/lib/{connectionState.d.ts → connectionState.d.mts} +3 -5
- package/lib/connectionState.d.mts.map +1 -0
- package/lib/{connectionState.js → connectionState.mjs} +4 -6
- package/lib/connectionState.mjs.map +1 -0
- package/lib/connectionStateHandler.d.mts +179 -0
- package/lib/connectionStateHandler.d.mts.map +1 -0
- package/lib/connectionStateHandler.mjs +469 -0
- package/lib/connectionStateHandler.mjs.map +1 -0
- package/lib/container-loader-alpha.d.mts +274 -0
- package/lib/container-loader-beta.d.mts +75 -0
- package/lib/container-loader-public.d.mts +75 -0
- package/lib/container-loader-untrimmed.d.mts +331 -0
- package/lib/container.d.mts +382 -0
- package/lib/container.d.mts.map +1 -0
- package/lib/container.mjs +1579 -0
- package/lib/container.mjs.map +1 -0
- package/lib/containerContext.d.mts +58 -0
- package/lib/containerContext.d.mts.map +1 -0
- package/lib/containerContext.mjs +70 -0
- package/lib/containerContext.mjs.map +1 -0
- package/lib/containerStorageAdapter.d.mts +73 -0
- package/lib/containerStorageAdapter.d.mts.map +1 -0
- package/lib/containerStorageAdapter.mjs +228 -0
- package/lib/containerStorageAdapter.mjs.map +1 -0
- package/lib/{contracts.d.ts → contracts.d.mts} +45 -17
- package/lib/contracts.d.mts.map +1 -0
- package/lib/{contracts.js → contracts.mjs} +4 -4
- package/lib/contracts.mjs.map +1 -0
- package/lib/debugLogger.d.mts +30 -0
- package/lib/debugLogger.d.mts.map +1 -0
- package/lib/debugLogger.mjs +93 -0
- package/lib/debugLogger.mjs.map +1 -0
- package/lib/{deltaManager.d.ts → deltaManager.d.mts} +54 -18
- package/lib/deltaManager.d.mts.map +1 -0
- package/lib/{deltaManager.js → deltaManager.mjs} +361 -165
- package/lib/deltaManager.mjs.map +1 -0
- package/lib/{deltaQueue.d.ts → deltaQueue.d.mts} +3 -4
- package/lib/deltaQueue.d.mts.map +1 -0
- package/lib/{deltaQueue.js → deltaQueue.mjs} +25 -24
- package/lib/deltaQueue.mjs.map +1 -0
- package/lib/disposal.d.mts +13 -0
- package/lib/disposal.d.mts.map +1 -0
- package/lib/disposal.mjs +21 -0
- package/lib/disposal.mjs.map +1 -0
- package/lib/error.d.mts +23 -0
- package/lib/error.d.mts.map +1 -0
- package/lib/error.mjs +28 -0
- package/lib/error.mjs.map +1 -0
- package/lib/index.d.mts +11 -0
- package/lib/index.d.mts.map +1 -0
- package/lib/index.mjs +10 -0
- package/lib/index.mjs.map +1 -0
- package/lib/{loader.d.ts → loader.d.mts} +39 -20
- package/lib/loader.d.mts.map +1 -0
- package/lib/loader.mjs +143 -0
- package/lib/loader.mjs.map +1 -0
- package/lib/location-redirection-utilities/index.d.mts +6 -0
- package/lib/location-redirection-utilities/index.d.mts.map +1 -0
- package/lib/location-redirection-utilities/index.mjs +6 -0
- package/lib/location-redirection-utilities/index.mjs.map +1 -0
- package/lib/location-redirection-utilities/resolveWithLocationRedirection.d.mts +24 -0
- package/lib/location-redirection-utilities/resolveWithLocationRedirection.d.mts.map +1 -0
- package/lib/location-redirection-utilities/resolveWithLocationRedirection.mjs +48 -0
- package/lib/location-redirection-utilities/resolveWithLocationRedirection.mjs.map +1 -0
- package/lib/noopHeuristic.d.mts +23 -0
- package/lib/noopHeuristic.d.mts.map +1 -0
- package/lib/{collabWindowTracker.js → noopHeuristic.mjs} +33 -35
- package/lib/noopHeuristic.mjs.map +1 -0
- package/lib/{packageVersion.d.ts → packageVersion.d.mts} +1 -1
- package/lib/{packageVersion.d.ts.map → packageVersion.d.mts.map} +1 -1
- package/lib/{packageVersion.js → packageVersion.mjs} +2 -2
- package/lib/packageVersion.mjs.map +1 -0
- package/lib/protocol.d.mts +38 -0
- package/lib/protocol.d.mts.map +1 -0
- package/lib/protocol.mjs +94 -0
- package/lib/protocol.mjs.map +1 -0
- package/lib/{protocolTreeDocumentStorageService.d.ts → protocolTreeDocumentStorageService.d.mts} +8 -4
- package/lib/protocolTreeDocumentStorageService.d.mts.map +1 -0
- package/lib/{protocolTreeDocumentStorageService.js → protocolTreeDocumentStorageService.mjs} +8 -5
- package/lib/protocolTreeDocumentStorageService.mjs.map +1 -0
- package/lib/quorum.d.mts +4 -0
- package/lib/quorum.d.mts.map +1 -0
- package/lib/quorum.mjs +12 -0
- package/lib/quorum.mjs.map +1 -0
- package/lib/{retriableDocumentStorageService.d.ts → retriableDocumentStorageService.d.mts} +7 -5
- package/lib/retriableDocumentStorageService.d.mts.map +1 -0
- package/lib/{retriableDocumentStorageService.js → retriableDocumentStorageService.mjs} +35 -20
- package/lib/retriableDocumentStorageService.mjs.map +1 -0
- package/lib/utils.d.mts +67 -0
- package/lib/utils.d.mts.map +1 -0
- package/lib/{utils.js → utils.mjs} +47 -11
- package/lib/utils.mjs.map +1 -0
- package/package.json +163 -70
- package/prettier.config.cjs +8 -0
- package/src/audience.ts +59 -49
- package/src/catchUpMonitor.ts +61 -0
- package/src/connectionManager.ts +1154 -910
- package/src/connectionState.ts +22 -25
- package/src/connectionStateHandler.ts +689 -319
- package/src/container.ts +2476 -1792
- package/src/containerContext.ts +98 -330
- package/src/containerStorageAdapter.ts +301 -105
- package/src/contracts.ts +184 -146
- package/src/debugLogger.ts +123 -0
- package/src/deltaManager.ts +1165 -900
- package/src/deltaQueue.ts +156 -152
- package/src/disposal.ts +25 -0
- package/src/error.ts +44 -0
- package/src/index.ts +14 -15
- package/src/loader.ts +356 -427
- package/src/location-redirection-utilities/index.ts +9 -0
- package/src/location-redirection-utilities/resolveWithLocationRedirection.ts +61 -0
- package/src/noopHeuristic.ts +107 -0
- package/src/packageVersion.ts +1 -1
- package/src/protocol.ts +150 -0
- package/src/protocolTreeDocumentStorageService.ts +35 -35
- package/src/quorum.ts +11 -50
- package/src/retriableDocumentStorageService.ts +135 -95
- package/src/utils.ts +159 -86
- package/tsc-multi.test.json +4 -0
- package/tsconfig.json +10 -12
- package/dist/audience.js.map +0 -1
- package/dist/collabWindowTracker.d.ts +0 -19
- package/dist/collabWindowTracker.d.ts.map +0 -1
- package/dist/collabWindowTracker.js.map +0 -1
- package/dist/connectionManager.js.map +0 -1
- package/dist/connectionState.js.map +0 -1
- package/dist/connectionStateHandler.js +0 -280
- package/dist/connectionStateHandler.js.map +0 -1
- package/dist/container.js +0 -1284
- package/dist/container.js.map +0 -1
- package/dist/containerContext.js +0 -217
- package/dist/containerContext.js.map +0 -1
- package/dist/containerStorageAdapter.js +0 -104
- package/dist/containerStorageAdapter.js.map +0 -1
- package/dist/contracts.js.map +0 -1
- package/dist/deltaManager.js.map +0 -1
- package/dist/deltaManagerProxy.d.ts +0 -54
- package/dist/deltaManagerProxy.d.ts.map +0 -1
- package/dist/deltaManagerProxy.js +0 -115
- package/dist/deltaManagerProxy.js.map +0 -1
- package/dist/deltaQueue.js.map +0 -1
- package/dist/index.js +0 -16
- package/dist/index.js.map +0 -1
- package/dist/loader.js +0 -241
- package/dist/loader.js.map +0 -1
- package/dist/packageVersion.js.map +0 -1
- package/dist/protocolTreeDocumentStorageService.js.map +0 -1
- package/dist/quorum.js +0 -44
- package/dist/quorum.js.map +0 -1
- package/dist/retriableDocumentStorageService.js.map +0 -1
- package/dist/utils.js.map +0 -1
- package/lib/audience.d.ts.map +0 -1
- package/lib/audience.js.map +0 -1
- package/lib/collabWindowTracker.d.ts +0 -19
- package/lib/collabWindowTracker.d.ts.map +0 -1
- package/lib/collabWindowTracker.js.map +0 -1
- package/lib/connectionManager.d.ts.map +0 -1
- package/lib/connectionManager.js.map +0 -1
- package/lib/connectionState.d.ts.map +0 -1
- package/lib/connectionState.js.map +0 -1
- package/lib/connectionStateHandler.d.ts +0 -81
- package/lib/connectionStateHandler.d.ts.map +0 -1
- package/lib/connectionStateHandler.js +0 -276
- package/lib/connectionStateHandler.js.map +0 -1
- package/lib/container.d.ts +0 -238
- package/lib/container.d.ts.map +0 -1
- package/lib/container.js +0 -1276
- package/lib/container.js.map +0 -1
- package/lib/containerContext.d.ts +0 -84
- package/lib/containerContext.d.ts.map +0 -1
- package/lib/containerContext.js +0 -213
- package/lib/containerContext.js.map +0 -1
- package/lib/containerStorageAdapter.d.ts +0 -48
- package/lib/containerStorageAdapter.d.ts.map +0 -1
- package/lib/containerStorageAdapter.js +0 -99
- package/lib/containerStorageAdapter.js.map +0 -1
- package/lib/contracts.d.ts.map +0 -1
- package/lib/contracts.js.map +0 -1
- package/lib/deltaManager.d.ts.map +0 -1
- package/lib/deltaManager.js.map +0 -1
- package/lib/deltaManagerProxy.d.ts +0 -54
- package/lib/deltaManagerProxy.d.ts.map +0 -1
- package/lib/deltaManagerProxy.js +0 -110
- package/lib/deltaManagerProxy.js.map +0 -1
- package/lib/deltaQueue.d.ts.map +0 -1
- package/lib/deltaQueue.js.map +0 -1
- package/lib/index.d.ts +0 -8
- package/lib/index.d.ts.map +0 -1
- package/lib/index.js +0 -8
- package/lib/index.js.map +0 -1
- package/lib/loader.d.ts.map +0 -1
- package/lib/loader.js +0 -236
- package/lib/loader.js.map +0 -1
- package/lib/packageVersion.js.map +0 -1
- package/lib/protocolTreeDocumentStorageService.d.ts.map +0 -1
- package/lib/protocolTreeDocumentStorageService.js.map +0 -1
- package/lib/quorum.d.ts +0 -21
- package/lib/quorum.d.ts.map +0 -1
- package/lib/quorum.js +0 -38
- package/lib/quorum.js.map +0 -1
- package/lib/retriableDocumentStorageService.d.ts.map +0 -1
- package/lib/retriableDocumentStorageService.js.map +0 -1
- package/lib/utils.d.ts +0 -34
- package/lib/utils.d.ts.map +0 -1
- package/lib/utils.js.map +0 -1
- package/src/collabWindowTracker.ts +0 -102
- package/src/deltaManagerProxy.ts +0 -158
- package/tsconfig.esnext.json +0 -7
|
@@ -0,0 +1,1579 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
|
|
3
|
+
* Licensed under the MIT License.
|
|
4
|
+
*/
|
|
5
|
+
import { v4 as uuid } from "uuid";
|
|
6
|
+
import { assert, unreachableCase } from "@fluidframework/core-utils";
|
|
7
|
+
import { TypedEventEmitter, performance } from "@fluid-internal/client-utils";
|
|
8
|
+
import { LogLevel, } from "@fluidframework/core-interfaces";
|
|
9
|
+
import { AttachState, isFluidCodeDetails, } from "@fluidframework/container-definitions";
|
|
10
|
+
import { readAndParse, OnlineStatus, isOnline, runWithRetry, isCombinedAppAndProtocolSummary, MessageType2, } from "@fluidframework/driver-utils";
|
|
11
|
+
import { MessageType, SummaryType, } from "@fluidframework/protocol-definitions";
|
|
12
|
+
import { createChildLogger, EventEmitterWithErrorHandling, PerformanceEvent, raiseConnectedEvent, connectedEventName, normalizeError, createChildMonitoringContext, wrapError, formatTick, GenericError, UsageError, } from "@fluidframework/telemetry-utils";
|
|
13
|
+
import { Audience } from "./audience.mjs";
|
|
14
|
+
import { ContainerContext } from "./containerContext.mjs";
|
|
15
|
+
import { ReconnectMode, getPackageName, } from "./contracts.mjs";
|
|
16
|
+
import { DeltaManager } from "./deltaManager.mjs";
|
|
17
|
+
import { RelativeLoader } from "./loader.mjs";
|
|
18
|
+
import { pkgVersion } from "./packageVersion.mjs";
|
|
19
|
+
import { ContainerStorageAdapter, getBlobContentsFromTree, getBlobContentsFromTreeWithBlobContents, } from "./containerStorageAdapter.mjs";
|
|
20
|
+
import { createConnectionStateHandler } from "./connectionStateHandler.mjs";
|
|
21
|
+
import { combineAppAndProtocolSummary, getProtocolSnapshotTree, getSnapshotTreeFromSerializedContainer, } from "./utils.mjs";
|
|
22
|
+
import { initQuorumValuesFromCodeDetails } from "./quorum.mjs";
|
|
23
|
+
import { NoopHeuristic } from "./noopHeuristic.mjs";
|
|
24
|
+
import { ConnectionManager } from "./connectionManager.mjs";
|
|
25
|
+
import { ConnectionState } from "./connectionState.mjs";
|
|
26
|
+
import { ProtocolHandler, protocolHandlerShouldProcessSignal, } from "./protocol.mjs";
|
|
27
|
+
const detachedContainerRefSeqNumber = 0;
|
|
28
|
+
const dirtyContainerEvent = "dirty";
|
|
29
|
+
const savedContainerEvent = "saved";
|
|
30
|
+
const packageNotFactoryError = "Code package does not implement IRuntimeFactory";
|
|
31
|
+
const hasBlobsSummaryTree = ".hasAttachmentBlobs";
|
|
32
|
+
/**
|
|
33
|
+
* Waits until container connects to delta storage and gets up-to-date.
|
|
34
|
+
*
|
|
35
|
+
* Useful when resolving URIs and hitting 404, due to container being loaded from (stale) snapshot and not being
|
|
36
|
+
* up to date. Host may chose to wait in such case and retry resolving URI.
|
|
37
|
+
*
|
|
38
|
+
* Warning: Will wait infinitely for connection to establish if there is no connection.
|
|
39
|
+
* May result in deadlock if Container.disconnect() is called and never followed by a call to Container.connect().
|
|
40
|
+
*
|
|
41
|
+
* @returns `true`: container is up to date, it processed all the ops that were know at the time of first connection.
|
|
42
|
+
*
|
|
43
|
+
* `false`: storage does not provide indication of how far the client is. Container processed all the ops known to it,
|
|
44
|
+
* but it maybe still behind.
|
|
45
|
+
*
|
|
46
|
+
* @throws an error beginning with `"Container closed"` if the container is closed before it catches up.
|
|
47
|
+
* @alpha
|
|
48
|
+
*/
|
|
49
|
+
export async function waitContainerToCatchUp(container) {
|
|
50
|
+
// Make sure we stop waiting if container is closed.
|
|
51
|
+
if (container.closed) {
|
|
52
|
+
throw new UsageError("waitContainerToCatchUp: Container closed");
|
|
53
|
+
}
|
|
54
|
+
return new Promise((resolve, reject) => {
|
|
55
|
+
const deltaManager = container.deltaManager;
|
|
56
|
+
const closedCallback = (err) => {
|
|
57
|
+
container.off("closed", closedCallback);
|
|
58
|
+
const baseMessage = "Container closed while waiting to catch up";
|
|
59
|
+
reject(err !== undefined
|
|
60
|
+
? wrapError(err, (innerMessage) => new GenericError(`${baseMessage}: ${innerMessage}`))
|
|
61
|
+
: new GenericError(baseMessage));
|
|
62
|
+
};
|
|
63
|
+
container.on("closed", closedCallback);
|
|
64
|
+
// Depending on config, transition to "connected" state may include the guarantee
|
|
65
|
+
// that all known ops have been processed. If so, we may introduce additional wait here.
|
|
66
|
+
// Waiting for "connected" state in either case gets us at least to our own Join op
|
|
67
|
+
// which is a reasonable approximation of "caught up"
|
|
68
|
+
const waitForOps = () => {
|
|
69
|
+
assert(container.connectionState === ConnectionState.CatchingUp ||
|
|
70
|
+
container.connectionState === ConnectionState.Connected, 0x0cd /* "Container disconnected while waiting for ops!" */);
|
|
71
|
+
const hasCheckpointSequenceNumber = deltaManager.hasCheckpointSequenceNumber;
|
|
72
|
+
const connectionOpSeqNumber = deltaManager.lastKnownSeqNumber;
|
|
73
|
+
assert(deltaManager.lastSequenceNumber <= connectionOpSeqNumber, 0x266 /* "lastKnownSeqNumber should never be below last processed sequence number" */);
|
|
74
|
+
if (deltaManager.lastSequenceNumber === connectionOpSeqNumber) {
|
|
75
|
+
container.off("closed", closedCallback);
|
|
76
|
+
resolve(hasCheckpointSequenceNumber);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const callbackOps = (message) => {
|
|
80
|
+
if (connectionOpSeqNumber <= message.sequenceNumber) {
|
|
81
|
+
container.off("closed", closedCallback);
|
|
82
|
+
resolve(hasCheckpointSequenceNumber);
|
|
83
|
+
deltaManager.off("op", callbackOps);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
deltaManager.on("op", callbackOps);
|
|
87
|
+
};
|
|
88
|
+
// We can leverage DeltaManager's "connect" event here and test for ConnectionState.Disconnected
|
|
89
|
+
// But that works only if service provides us checkPointSequenceNumber
|
|
90
|
+
// Our internal testing is based on R11S that does not, but almost all tests connect as "write" and
|
|
91
|
+
// use this function to catch up, so leveraging our own join op as a fence/barrier
|
|
92
|
+
if (container.connectionState === ConnectionState.Connected) {
|
|
93
|
+
waitForOps();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const callback = () => {
|
|
97
|
+
container.off(connectedEventName, callback);
|
|
98
|
+
waitForOps();
|
|
99
|
+
};
|
|
100
|
+
container.on(connectedEventName, callback);
|
|
101
|
+
if (container.connectionState === ConnectionState.Disconnected) {
|
|
102
|
+
container.connect();
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
const getCodeProposal =
|
|
107
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
108
|
+
(quorum) => quorum.get("code") ?? quorum.get("code2");
|
|
109
|
+
/**
|
|
110
|
+
* Helper function to report to telemetry cases where operation takes longer than expected (200ms)
|
|
111
|
+
* @param logger - logger to use
|
|
112
|
+
* @param eventName - event name
|
|
113
|
+
* @param action - functor to call and measure
|
|
114
|
+
*/
|
|
115
|
+
export async function ReportIfTooLong(logger, eventName, action) {
|
|
116
|
+
const event = PerformanceEvent.start(logger, { eventName });
|
|
117
|
+
const props = await action();
|
|
118
|
+
if (event.duration > 200) {
|
|
119
|
+
event.end(props);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const summarizerClientType = "summarizer";
|
|
123
|
+
export class Container extends EventEmitterWithErrorHandling {
|
|
124
|
+
/**
|
|
125
|
+
* Load an existing container.
|
|
126
|
+
* @internal
|
|
127
|
+
*/
|
|
128
|
+
static async load(loadProps, createProps) {
|
|
129
|
+
const { version, pendingLocalState, loadMode, resolvedUrl, loadToSequenceNumber } = loadProps;
|
|
130
|
+
const container = new Container(createProps, loadProps);
|
|
131
|
+
const disableRecordHeapSize = container.mc.config.getBoolean("Fluid.Loader.DisableRecordHeapSize");
|
|
132
|
+
return PerformanceEvent.timedExecAsync(container.mc.logger, { eventName: "Load" }, async (event) => new Promise((resolve, reject) => {
|
|
133
|
+
const defaultMode = { opsBeforeReturn: "cached" };
|
|
134
|
+
// if we have pendingLocalState, anything we cached is not useful and we shouldn't wait for connection
|
|
135
|
+
// to return container, so ignore this value and use undefined for opsBeforeReturn
|
|
136
|
+
const mode = pendingLocalState
|
|
137
|
+
? { ...(loadMode ?? defaultMode), opsBeforeReturn: undefined }
|
|
138
|
+
: loadMode ?? defaultMode;
|
|
139
|
+
const onClosed = (err) => {
|
|
140
|
+
// pre-0.58 error message: containerClosedWithoutErrorDuringLoad
|
|
141
|
+
reject(err ?? new GenericError("Container closed without error during load"));
|
|
142
|
+
};
|
|
143
|
+
container.on("closed", onClosed);
|
|
144
|
+
container
|
|
145
|
+
.load(version, mode, resolvedUrl, pendingLocalState, loadToSequenceNumber)
|
|
146
|
+
.finally(() => {
|
|
147
|
+
container.removeListener("closed", onClosed);
|
|
148
|
+
})
|
|
149
|
+
.then((props) => {
|
|
150
|
+
event.end({ ...props, ...loadMode });
|
|
151
|
+
resolve(container);
|
|
152
|
+
}, (error) => {
|
|
153
|
+
const err = normalizeError(error);
|
|
154
|
+
// Depending where error happens, we can be attempting to connect to web socket
|
|
155
|
+
// and continuously retrying (consider offline mode)
|
|
156
|
+
// Host has no container to close, so it's prudent to do it here
|
|
157
|
+
// Note: We could only dispose the container instead of just close but that would
|
|
158
|
+
// the telemetry where users sometimes search for ContainerClose event to look
|
|
159
|
+
// for load failures. So not removing this at this time.
|
|
160
|
+
container.close(err);
|
|
161
|
+
container.dispose(err);
|
|
162
|
+
onClosed(err);
|
|
163
|
+
});
|
|
164
|
+
}), { start: true, end: true, cancel: "generic" }, disableRecordHeapSize !== true /* recordHeapSize */);
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Create a new container in a detached state.
|
|
168
|
+
*/
|
|
169
|
+
static async createDetached(createProps, codeDetails) {
|
|
170
|
+
const container = new Container(createProps);
|
|
171
|
+
return PerformanceEvent.timedExecAsync(container.mc.logger, { eventName: "CreateDetached" }, async (_event) => {
|
|
172
|
+
await container.createDetached(codeDetails);
|
|
173
|
+
return container;
|
|
174
|
+
}, { start: true, end: true, cancel: "generic" });
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Create a new container in a detached state that is initialized with a
|
|
178
|
+
* snapshot from a previous detached container.
|
|
179
|
+
*/
|
|
180
|
+
static async rehydrateDetachedFromSnapshot(createProps, snapshot) {
|
|
181
|
+
const container = new Container(createProps);
|
|
182
|
+
return PerformanceEvent.timedExecAsync(container.mc.logger, { eventName: "RehydrateDetachedFromSnapshot" }, async (_event) => {
|
|
183
|
+
const deserializedSummary = JSON.parse(snapshot);
|
|
184
|
+
if (!isCombinedAppAndProtocolSummary(deserializedSummary, hasBlobsSummaryTree)) {
|
|
185
|
+
throw new UsageError("Cannot rehydrate detached container. Incorrect format");
|
|
186
|
+
}
|
|
187
|
+
await container.rehydrateDetachedFromSnapshot(deserializedSummary);
|
|
188
|
+
return container;
|
|
189
|
+
}, { start: true, end: true, cancel: "generic" });
|
|
190
|
+
}
|
|
191
|
+
setLoaded() {
|
|
192
|
+
// It's conceivable the container could be closed when this is called
|
|
193
|
+
// Only transition states if currently loading
|
|
194
|
+
if (this._lifecycleState === "loading") {
|
|
195
|
+
// Propagate current connection state through the system.
|
|
196
|
+
this.propagateConnectionState(true /* initial transition */);
|
|
197
|
+
this._lifecycleState = "loaded";
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
get closed() {
|
|
201
|
+
return (this._lifecycleState === "closing" || this._lifecycleState === "closed" || this.disposed);
|
|
202
|
+
}
|
|
203
|
+
get disposed() {
|
|
204
|
+
return this._lifecycleState === "disposing" || this._lifecycleState === "disposed";
|
|
205
|
+
}
|
|
206
|
+
get runtime() {
|
|
207
|
+
if (this._runtime === undefined) {
|
|
208
|
+
throw new Error("Attempted to access runtime before it was defined");
|
|
209
|
+
}
|
|
210
|
+
return this._runtime;
|
|
211
|
+
}
|
|
212
|
+
get protocolHandler() {
|
|
213
|
+
if (this._protocolHandler === undefined) {
|
|
214
|
+
throw new Error("Attempted to access protocolHandler before it was defined");
|
|
215
|
+
}
|
|
216
|
+
return this._protocolHandler;
|
|
217
|
+
}
|
|
218
|
+
get connectionMode() {
|
|
219
|
+
return this._deltaManager.connectionManager.connectionMode;
|
|
220
|
+
}
|
|
221
|
+
get resolvedUrl() {
|
|
222
|
+
/**
|
|
223
|
+
* All attached containers will have a document service,
|
|
224
|
+
* this is required, as attached containers are attached to
|
|
225
|
+
* a service. Detached containers will neither have a document
|
|
226
|
+
* service or a resolved url as they only exist locally.
|
|
227
|
+
* in order to create a document service a resolved url must
|
|
228
|
+
* first be obtained, this is how the container is identified.
|
|
229
|
+
* Because of this, the document service's resolved url
|
|
230
|
+
* is always the same as the containers, as we had to
|
|
231
|
+
* obtain the resolved url, and then create the service from it.
|
|
232
|
+
*/
|
|
233
|
+
return this.service?.resolvedUrl;
|
|
234
|
+
}
|
|
235
|
+
get readOnlyInfo() {
|
|
236
|
+
return this._deltaManager.readOnlyInfo;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Sends signal to runtime (and data stores) to be read-only.
|
|
240
|
+
* Hosts may have read only views, indicating to data stores that no edits are allowed.
|
|
241
|
+
* This is independent from this._readonlyPermissions (permissions) and this.connectionMode
|
|
242
|
+
* (server can return "write" mode even when asked for "read")
|
|
243
|
+
* Leveraging same "readonly" event as runtime & data stores should behave the same in such case
|
|
244
|
+
* as in read-only permissions.
|
|
245
|
+
* But this.active can be used by some DDSes to figure out if ops can be sent
|
|
246
|
+
* (for example, read-only view still participates in code proposals / upgrades decisions)
|
|
247
|
+
*
|
|
248
|
+
* Forcing Readonly does not prevent DDS from generating ops. It is up to user code to honour
|
|
249
|
+
* the readonly flag. If ops are generated, they will accumulate locally and not be sent. If
|
|
250
|
+
* there are pending in the outbound queue, it will stop sending until force readonly is
|
|
251
|
+
* cleared.
|
|
252
|
+
*
|
|
253
|
+
* @param readonly - set or clear force readonly.
|
|
254
|
+
*/
|
|
255
|
+
forceReadonly(readonly) {
|
|
256
|
+
this._deltaManager.connectionManager.forceReadonly(readonly);
|
|
257
|
+
}
|
|
258
|
+
get deltaManager() {
|
|
259
|
+
return this._deltaManager;
|
|
260
|
+
}
|
|
261
|
+
get connectionState() {
|
|
262
|
+
return this.connectionStateHandler.connectionState;
|
|
263
|
+
}
|
|
264
|
+
get connected() {
|
|
265
|
+
return this.connectionStateHandler.connectionState === ConnectionState.Connected;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* The server provided id of the client.
|
|
269
|
+
* Set once this.connected is true, otherwise undefined
|
|
270
|
+
*/
|
|
271
|
+
get clientId() {
|
|
272
|
+
return this._clientId;
|
|
273
|
+
}
|
|
274
|
+
get offlineLoadEnabled() {
|
|
275
|
+
const enabled = this.mc.config.getBoolean("Fluid.Container.enableOfflineLoad") ??
|
|
276
|
+
this.options?.enableOfflineLoad === true;
|
|
277
|
+
// summarizer will not have any pending state we want to save
|
|
278
|
+
return enabled && this.deltaManager.clientDetails.capabilities.interactive;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Get the code details that are currently specified for the container.
|
|
282
|
+
* @returns The current code details if any are specified, undefined if none are specified.
|
|
283
|
+
*/
|
|
284
|
+
getSpecifiedCodeDetails() {
|
|
285
|
+
return this.getCodeDetailsFromQuorum();
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Get the code details that were used to load the container.
|
|
289
|
+
* @returns The code details that were used to load the container if it is loaded, undefined if it is not yet
|
|
290
|
+
* loaded.
|
|
291
|
+
*/
|
|
292
|
+
getLoadedCodeDetails() {
|
|
293
|
+
return this._loadedCodeDetails;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Retrieves the audience associated with the document
|
|
297
|
+
*/
|
|
298
|
+
get audience() {
|
|
299
|
+
return this.protocolHandler.audience;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Returns true if container is dirty.
|
|
303
|
+
* Which means data loss if container is closed at that same moment
|
|
304
|
+
* Most likely that happens when there is no network connection to Relay Service
|
|
305
|
+
*/
|
|
306
|
+
get isDirty() {
|
|
307
|
+
return this._dirtyContainer;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* {@inheritDoc @fluidframework/container-definitions#IContainer.entryPoint}
|
|
311
|
+
*/
|
|
312
|
+
async getEntryPoint() {
|
|
313
|
+
if (this._disposed) {
|
|
314
|
+
throw new UsageError("The context is already disposed");
|
|
315
|
+
}
|
|
316
|
+
if (this._runtime !== undefined) {
|
|
317
|
+
return this._runtime.getEntryPoint?.();
|
|
318
|
+
}
|
|
319
|
+
return new Promise((resolve, reject) => {
|
|
320
|
+
const runtimeInstantiatedHandler = () => {
|
|
321
|
+
assert(this._runtime !== undefined, 0x5a3 /* runtimeInstantiated fired but runtime is still undefined */);
|
|
322
|
+
resolve(this._runtime.getEntryPoint?.());
|
|
323
|
+
this._lifecycleEvents.off("disposed", disposedHandler);
|
|
324
|
+
};
|
|
325
|
+
const disposedHandler = () => {
|
|
326
|
+
reject(new Error("ContainerContext was disposed"));
|
|
327
|
+
this._lifecycleEvents.off("runtimeInstantiated", runtimeInstantiatedHandler);
|
|
328
|
+
};
|
|
329
|
+
this._lifecycleEvents.once("runtimeInstantiated", runtimeInstantiatedHandler);
|
|
330
|
+
this._lifecycleEvents.once("disposed", disposedHandler);
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* @internal
|
|
335
|
+
*/
|
|
336
|
+
constructor(createProps, loadProps) {
|
|
337
|
+
super((name, error) => {
|
|
338
|
+
this.mc.logger.sendErrorEvent({
|
|
339
|
+
eventName: "ContainerEventHandlerException",
|
|
340
|
+
name: typeof name === "string" ? name : undefined,
|
|
341
|
+
}, error);
|
|
342
|
+
});
|
|
343
|
+
/**
|
|
344
|
+
* Lifecycle state of the container, used mainly to prevent re-entrancy and telemetry
|
|
345
|
+
*
|
|
346
|
+
* States are allowed to progress to further states:
|
|
347
|
+
* "loading" - "loaded" - "closing" - "disposing" - "closed" - "disposed"
|
|
348
|
+
*
|
|
349
|
+
* For example, moving from "closed" to "disposing" is not allowed since it is an earlier state.
|
|
350
|
+
*
|
|
351
|
+
* loading: Container has been created, but is not yet in normal/loaded state
|
|
352
|
+
* loaded: Container is in normal/loaded state
|
|
353
|
+
* closing: Container has started closing process (for re-entrancy prevention)
|
|
354
|
+
* disposing: Container has started disposing process (for re-entrancy prevention)
|
|
355
|
+
* closed: Container has closed
|
|
356
|
+
* disposed: Container has been disposed
|
|
357
|
+
*/
|
|
358
|
+
this._lifecycleState = "loading";
|
|
359
|
+
this._attachState = AttachState.Detached;
|
|
360
|
+
/** During initialization we pause the inbound queues. We track this state to ensure we only call resume once */
|
|
361
|
+
this.inboundQueuePausedFromInit = true;
|
|
362
|
+
this.firstConnection = true;
|
|
363
|
+
this.connectionTransitionTimes = [];
|
|
364
|
+
this.attachStarted = false;
|
|
365
|
+
this._dirtyContainer = false;
|
|
366
|
+
this.savedOps = [];
|
|
367
|
+
this.clientsWhoShouldHaveLeft = new Set();
|
|
368
|
+
this.setAutoReconnectTime = performance.now();
|
|
369
|
+
this._lifecycleEvents = new TypedEventEmitter();
|
|
370
|
+
this._disposed = false;
|
|
371
|
+
this.getAbsoluteUrl = async (relativeUrl) => {
|
|
372
|
+
if (this.resolvedUrl === undefined) {
|
|
373
|
+
return undefined;
|
|
374
|
+
}
|
|
375
|
+
return this.urlResolver.getAbsoluteUrl(this.resolvedUrl, relativeUrl, getPackageName(this._loadedCodeDetails));
|
|
376
|
+
};
|
|
377
|
+
this.updateDirtyContainerState = (dirty) => {
|
|
378
|
+
if (this._dirtyContainer === dirty) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
this._dirtyContainer = dirty;
|
|
382
|
+
this.emit(dirty ? dirtyContainerEvent : savedContainerEvent);
|
|
383
|
+
};
|
|
384
|
+
const { canReconnect, clientDetailsOverride, urlResolver, documentServiceFactory, codeLoader, options, scope, subLogger, detachedBlobStorage, protocolHandlerBuilder, } = createProps;
|
|
385
|
+
this.connectionTransitionTimes[ConnectionState.Disconnected] = performance.now();
|
|
386
|
+
const pendingLocalState = loadProps?.pendingLocalState;
|
|
387
|
+
this._clientId = pendingLocalState?.clientId;
|
|
388
|
+
this._canReconnect = canReconnect ?? true;
|
|
389
|
+
this.clientDetailsOverride = clientDetailsOverride;
|
|
390
|
+
this.urlResolver = urlResolver;
|
|
391
|
+
this.serviceFactory = documentServiceFactory;
|
|
392
|
+
this.codeLoader = codeLoader;
|
|
393
|
+
// Warning: this is only a shallow clone. Mutation of any individual loader option will mutate it for
|
|
394
|
+
// all clients that were loaded from the same loader (including summarizer clients).
|
|
395
|
+
// Tracking alternative ways to handle this in AB#4129.
|
|
396
|
+
this.options = { ...options };
|
|
397
|
+
this.scope = scope;
|
|
398
|
+
this.detachedBlobStorage = detachedBlobStorage;
|
|
399
|
+
this.protocolHandlerBuilder =
|
|
400
|
+
protocolHandlerBuilder ??
|
|
401
|
+
((attributes, quorumSnapshot, sendProposal) => new ProtocolHandler(attributes, quorumSnapshot, sendProposal, new Audience(), (clientId) => this.clientsWhoShouldHaveLeft.has(clientId)));
|
|
402
|
+
// Note that we capture the createProps here so we can replicate the creation call when we want to clone.
|
|
403
|
+
this.clone = async (_loadProps, createParamOverrides) => {
|
|
404
|
+
return Container.load(_loadProps, {
|
|
405
|
+
...createProps,
|
|
406
|
+
...createParamOverrides,
|
|
407
|
+
});
|
|
408
|
+
};
|
|
409
|
+
this._containerId = uuid();
|
|
410
|
+
this.client = Container.setupClient(this._containerId, this.options, this.clientDetailsOverride);
|
|
411
|
+
// Create logger for data stores to use
|
|
412
|
+
const type = this.client.details.type;
|
|
413
|
+
const interactive = this.client.details.capabilities.interactive;
|
|
414
|
+
const clientType = `${interactive ? "interactive" : "noninteractive"}${type !== undefined && type !== "" ? `/${type}` : ""}`;
|
|
415
|
+
// Need to use the property getter for docId because for detached flow we don't have the docId initially.
|
|
416
|
+
// We assign the id later so property getter is used.
|
|
417
|
+
this.subLogger = createChildLogger({
|
|
418
|
+
logger: subLogger,
|
|
419
|
+
properties: {
|
|
420
|
+
all: {
|
|
421
|
+
clientType,
|
|
422
|
+
containerId: this._containerId,
|
|
423
|
+
docId: () => this.resolvedUrl?.id,
|
|
424
|
+
containerAttachState: () => this._attachState,
|
|
425
|
+
containerLifecycleState: () => this._lifecycleState,
|
|
426
|
+
containerConnectionState: () => ConnectionState[this.connectionState],
|
|
427
|
+
serializedContainer: pendingLocalState !== undefined,
|
|
428
|
+
},
|
|
429
|
+
// we need to be judicious with our logging here to avoid generating too much data
|
|
430
|
+
// all data logged here should be broadly applicable, and not specific to a
|
|
431
|
+
// specific error or class of errors
|
|
432
|
+
error: {
|
|
433
|
+
// load information to associate errors with the specific load point
|
|
434
|
+
dmInitialSeqNumber: () => this._deltaManager?.initialSequenceNumber,
|
|
435
|
+
dmLastProcessedSeqNumber: () => this._deltaManager?.lastSequenceNumber,
|
|
436
|
+
dmLastKnownSeqNumber: () => this._deltaManager?.lastKnownSeqNumber,
|
|
437
|
+
containerLoadedFromVersionId: () => this._loadedFromVersion?.id,
|
|
438
|
+
containerLoadedFromVersionDate: () => this._loadedFromVersion?.date,
|
|
439
|
+
// message information to associate errors with the specific execution state
|
|
440
|
+
// dmLastMsqSeqNumber: if present, same as dmLastProcessedSeqNumber
|
|
441
|
+
dmLastMsqSeqNumber: () => this.deltaManager?.lastMessage?.sequenceNumber,
|
|
442
|
+
dmLastMsqSeqTimestamp: () => this.deltaManager?.lastMessage?.timestamp,
|
|
443
|
+
dmLastMsqSeqClientId: () => this.deltaManager?.lastMessage?.clientId === null
|
|
444
|
+
? "null"
|
|
445
|
+
: this.deltaManager?.lastMessage?.clientId,
|
|
446
|
+
dmLastMsgClientSeq: () => this.deltaManager?.lastMessage?.clientSequenceNumber,
|
|
447
|
+
connectionStateDuration: () => performance.now() - this.connectionTransitionTimes[this.connectionState],
|
|
448
|
+
},
|
|
449
|
+
},
|
|
450
|
+
});
|
|
451
|
+
// Prefix all events in this file with container-loader
|
|
452
|
+
this.mc = createChildMonitoringContext({ logger: this.subLogger, namespace: "Container" });
|
|
453
|
+
this._deltaManager = this.createDeltaManager();
|
|
454
|
+
this.connectionStateHandler = createConnectionStateHandler({
|
|
455
|
+
logger: this.mc.logger,
|
|
456
|
+
connectionStateChanged: (value, oldState, reason) => {
|
|
457
|
+
if (value === ConnectionState.Connected) {
|
|
458
|
+
this._clientId = this.connectionStateHandler.pendingClientId;
|
|
459
|
+
}
|
|
460
|
+
this.logConnectionStateChangeTelemetry(value, oldState, reason);
|
|
461
|
+
if (this._lifecycleState === "loaded") {
|
|
462
|
+
this.propagateConnectionState(false /* initial transition */, value === ConnectionState.Disconnected
|
|
463
|
+
? reason
|
|
464
|
+
: undefined /* disconnectedReason */);
|
|
465
|
+
}
|
|
466
|
+
},
|
|
467
|
+
shouldClientJoinWrite: () => this._deltaManager.connectionManager.shouldJoinWrite(),
|
|
468
|
+
maxClientLeaveWaitTime: options.maxClientLeaveWaitTime,
|
|
469
|
+
logConnectionIssue: (eventName, category, details) => {
|
|
470
|
+
const mode = this.connectionMode;
|
|
471
|
+
// We get here when socket does not receive any ops on "write" connection, including
|
|
472
|
+
// its own join op.
|
|
473
|
+
// Report issues only if we already loaded container - op processing is paused while container is loading,
|
|
474
|
+
// so we always time-out processing of join op in cases where fetching snapshot takes a minute.
|
|
475
|
+
// It's not a problem with op processing itself - such issues should be tracked as part of boot perf monitoring instead.
|
|
476
|
+
this._deltaManager.logConnectionIssue({
|
|
477
|
+
eventName,
|
|
478
|
+
mode,
|
|
479
|
+
category: this._lifecycleState === "loading" ? "generic" : category,
|
|
480
|
+
duration: performance.now() -
|
|
481
|
+
this.connectionTransitionTimes[ConnectionState.CatchingUp],
|
|
482
|
+
...(details === undefined ? {} : { details: JSON.stringify(details) }),
|
|
483
|
+
});
|
|
484
|
+
// If this is "write" connection, it took too long to receive join op. But in most cases that's due
|
|
485
|
+
// to very slow op fetches and we will eventually get there.
|
|
486
|
+
// For "read" connections, we get here due to self join signal not arriving on time. We will need to
|
|
487
|
+
// better understand when and why it may happen.
|
|
488
|
+
// For now, attempt to recover by reconnecting. In future, maybe we can query relay service for
|
|
489
|
+
// current state of audience.
|
|
490
|
+
// Other possible recovery path - move to connected state (i.e. ConnectionStateHandler.joinOpTimer
|
|
491
|
+
// to call this.applyForConnectedState("addMemberEvent") for "read" connections)
|
|
492
|
+
if (mode === "read") {
|
|
493
|
+
const reason = { text: "NoJoinSignal" };
|
|
494
|
+
this.disconnectInternal(reason);
|
|
495
|
+
this.connectInternal({ reason, fetchOpsFromStorage: false });
|
|
496
|
+
}
|
|
497
|
+
},
|
|
498
|
+
clientShouldHaveLeft: (clientId) => {
|
|
499
|
+
this.clientsWhoShouldHaveLeft.add(clientId);
|
|
500
|
+
},
|
|
501
|
+
}, this.deltaManager, pendingLocalState?.clientId);
|
|
502
|
+
this.on(savedContainerEvent, () => {
|
|
503
|
+
this.connectionStateHandler.containerSaved();
|
|
504
|
+
});
|
|
505
|
+
// We expose our storage publicly, so it's possible others may call uploadSummaryWithContext() with a
|
|
506
|
+
// non-combined summary tree (in particular, ContainerRuntime.submitSummary). We'll intercept those calls
|
|
507
|
+
// using this callback and fix them up.
|
|
508
|
+
const addProtocolSummaryIfMissing = (summaryTree) => isCombinedAppAndProtocolSummary(summaryTree) === true
|
|
509
|
+
? summaryTree
|
|
510
|
+
: combineAppAndProtocolSummary(summaryTree, this.captureProtocolSummary());
|
|
511
|
+
// Whether the combined summary tree has been forced on by either the loader option or the monitoring context.
|
|
512
|
+
// Even if not forced on via this flag, combined summaries may still be enabled by service policy.
|
|
513
|
+
const forceEnableSummarizeProtocolTree = this.mc.config.getBoolean("Fluid.Container.summarizeProtocolTree2") ??
|
|
514
|
+
options.summarizeProtocolTree;
|
|
515
|
+
this.storageAdapter = new ContainerStorageAdapter(detachedBlobStorage, this.mc.logger, pendingLocalState?.snapshotBlobs, addProtocolSummaryIfMissing, forceEnableSummarizeProtocolTree);
|
|
516
|
+
const isDomAvailable = typeof document === "object" &&
|
|
517
|
+
document !== null &&
|
|
518
|
+
typeof document.addEventListener === "function" &&
|
|
519
|
+
document.addEventListener !== null;
|
|
520
|
+
// keep track of last time page was visible for telemetry (on interactive clients only)
|
|
521
|
+
if (isDomAvailable && interactive) {
|
|
522
|
+
this.lastVisible = document.hidden ? performance.now() : undefined;
|
|
523
|
+
this.visibilityEventHandler = () => {
|
|
524
|
+
if (document.hidden) {
|
|
525
|
+
this.lastVisible = performance.now();
|
|
526
|
+
}
|
|
527
|
+
else {
|
|
528
|
+
// settimeout so this will hopefully fire after disconnect event if being hidden caused it
|
|
529
|
+
setTimeout(() => {
|
|
530
|
+
this.lastVisible = undefined;
|
|
531
|
+
}, 0);
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
document.addEventListener("visibilitychange", this.visibilityEventHandler);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Retrieves the quorum associated with the document
|
|
539
|
+
*/
|
|
540
|
+
getQuorum() {
|
|
541
|
+
return this.protocolHandler.quorum;
|
|
542
|
+
}
|
|
543
|
+
dispose(error) {
|
|
544
|
+
this._deltaManager.dispose(error);
|
|
545
|
+
this.verifyClosed();
|
|
546
|
+
}
|
|
547
|
+
close(error) {
|
|
548
|
+
// 1. Ensure that close sequence is exactly the same no matter if it's initiated by host or by DeltaManager
|
|
549
|
+
// 2. We need to ensure that we deliver disconnect event to runtime properly. See connectionStateChanged
|
|
550
|
+
// handler. We only deliver events if container fully loaded. Transitioning from "loading" ->
|
|
551
|
+
// "closing" will lose that info (can also solve by tracking extra state).
|
|
552
|
+
this._deltaManager.close(error);
|
|
553
|
+
this.verifyClosed();
|
|
554
|
+
}
|
|
555
|
+
verifyClosed() {
|
|
556
|
+
assert(this.connectionState === ConnectionState.Disconnected, 0x0cf /* "disconnect event was not raised!" */);
|
|
557
|
+
assert(this._lifecycleState === "closed" || this._lifecycleState === "disposed", 0x314 /* Container properly closed */);
|
|
558
|
+
}
|
|
559
|
+
closeCore(error) {
|
|
560
|
+
assert(!this.closed, 0x315 /* re-entrancy */);
|
|
561
|
+
try {
|
|
562
|
+
// Ensure that we raise all key events even if one of these throws
|
|
563
|
+
try {
|
|
564
|
+
// Raise event first, to ensure we capture _lifecycleState before transition.
|
|
565
|
+
// This gives us a chance to know what errors happened on open vs. on fully loaded container.
|
|
566
|
+
// Log generic events instead of error events if container is in loading state, as most errors are not really FF errors
|
|
567
|
+
// which can pollute telemetry for real bugs
|
|
568
|
+
this.mc.logger.sendTelemetryEvent({
|
|
569
|
+
eventName: "ContainerClose",
|
|
570
|
+
category: this._lifecycleState !== "loading" && error !== undefined
|
|
571
|
+
? "error"
|
|
572
|
+
: "generic",
|
|
573
|
+
}, error);
|
|
574
|
+
this._lifecycleState = "closing";
|
|
575
|
+
this._protocolHandler?.close();
|
|
576
|
+
this.connectionStateHandler.dispose();
|
|
577
|
+
}
|
|
578
|
+
catch (exception) {
|
|
579
|
+
this.mc.logger.sendErrorEvent({ eventName: "ContainerCloseException" }, exception);
|
|
580
|
+
}
|
|
581
|
+
this.emit("closed", error);
|
|
582
|
+
if (this.visibilityEventHandler !== undefined) {
|
|
583
|
+
document.removeEventListener("visibilitychange", this.visibilityEventHandler);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
finally {
|
|
587
|
+
this._lifecycleState = "closed";
|
|
588
|
+
// There is no user for summarizer, so we need to ensure dispose is called
|
|
589
|
+
if (this.client.details.type === summarizerClientType) {
|
|
590
|
+
this.dispose(error);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
disposeCore(error) {
|
|
595
|
+
assert(!this._disposed, 0x54c /* Container already disposed */);
|
|
596
|
+
this._disposed = true;
|
|
597
|
+
try {
|
|
598
|
+
// Ensure that we raise all key events even if one of these throws
|
|
599
|
+
try {
|
|
600
|
+
// Raise event first, to ensure we capture _lifecycleState before transition.
|
|
601
|
+
// This gives us a chance to know what errors happened on open vs. on fully loaded container.
|
|
602
|
+
this.mc.logger.sendTelemetryEvent({
|
|
603
|
+
eventName: "ContainerDispose",
|
|
604
|
+
// Only log error if container isn't closed
|
|
605
|
+
category: !this.closed && error !== undefined ? "error" : "generic",
|
|
606
|
+
}, error);
|
|
607
|
+
// ! Progressing from "closed" to "disposing" is not allowed
|
|
608
|
+
if (this._lifecycleState !== "closed") {
|
|
609
|
+
this._lifecycleState = "disposing";
|
|
610
|
+
}
|
|
611
|
+
this._protocolHandler?.close();
|
|
612
|
+
this.connectionStateHandler.dispose();
|
|
613
|
+
const maybeError = error !== undefined ? new Error(error.message) : undefined;
|
|
614
|
+
this._runtime?.dispose(maybeError);
|
|
615
|
+
this.storageAdapter.dispose();
|
|
616
|
+
// Notify storage about critical errors. They may be due to disconnect between client & server knowledge
|
|
617
|
+
// about file, like file being overwritten in storage, but client having stale local cache.
|
|
618
|
+
// Driver need to ensure all caches are cleared on critical errors
|
|
619
|
+
this.service?.dispose(error);
|
|
620
|
+
}
|
|
621
|
+
catch (exception) {
|
|
622
|
+
this.mc.logger.sendErrorEvent({ eventName: "ContainerDisposeException" }, exception);
|
|
623
|
+
}
|
|
624
|
+
this.emit("disposed", error);
|
|
625
|
+
this.removeAllListeners();
|
|
626
|
+
if (this.visibilityEventHandler !== undefined) {
|
|
627
|
+
document.removeEventListener("visibilitychange", this.visibilityEventHandler);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
finally {
|
|
631
|
+
this._lifecycleState = "disposed";
|
|
632
|
+
this._lifecycleEvents.emit("disposed");
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
async closeAndGetPendingLocalState(stopBlobAttachingSignal) {
|
|
636
|
+
// runtime matches pending ops to successful ones by clientId and client seq num, so we need to close the
|
|
637
|
+
// container at the same time we get pending state, otherwise this container could reconnect and resubmit with
|
|
638
|
+
// a new clientId and a future container using stale pending state without the new clientId would resubmit them
|
|
639
|
+
const pendingState = await this.getPendingLocalStateCore({
|
|
640
|
+
notifyImminentClosure: true,
|
|
641
|
+
stopBlobAttachingSignal,
|
|
642
|
+
});
|
|
643
|
+
this.close();
|
|
644
|
+
return pendingState;
|
|
645
|
+
}
|
|
646
|
+
async getPendingLocalState() {
|
|
647
|
+
return this.getPendingLocalStateCore({ notifyImminentClosure: false });
|
|
648
|
+
}
|
|
649
|
+
async getPendingLocalStateCore(props) {
|
|
650
|
+
return PerformanceEvent.timedExecAsync(this.mc.logger, {
|
|
651
|
+
eventName: "getPendingLocalState",
|
|
652
|
+
notifyImminentClosure: props.notifyImminentClosure,
|
|
653
|
+
savedOpsSize: this.savedOps.length,
|
|
654
|
+
clientId: this.clientId,
|
|
655
|
+
}, async () => {
|
|
656
|
+
if (!this.offlineLoadEnabled) {
|
|
657
|
+
throw new UsageError("Can't get pending local state unless offline load is enabled");
|
|
658
|
+
}
|
|
659
|
+
if (this.closed || this._disposed) {
|
|
660
|
+
throw new UsageError("Pending state cannot be retried if the container is closed or disposed");
|
|
661
|
+
}
|
|
662
|
+
assert(this.attachState === AttachState.Attached, 0x0d1 /* "Container should be attached before close" */);
|
|
663
|
+
assert(this.resolvedUrl !== undefined && this.resolvedUrl.type === "fluid", 0x0d2 /* "resolved url should be valid Fluid url" */);
|
|
664
|
+
assert(!!this.baseSnapshot, 0x5d4 /* no base snapshot */);
|
|
665
|
+
assert(!!this.baseSnapshotBlobs, 0x5d5 /* no snapshot blobs */);
|
|
666
|
+
const pendingRuntimeState = await this.runtime.getPendingLocalState(props);
|
|
667
|
+
const pendingState = {
|
|
668
|
+
pendingRuntimeState,
|
|
669
|
+
baseSnapshot: this.baseSnapshot,
|
|
670
|
+
snapshotBlobs: this.baseSnapshotBlobs,
|
|
671
|
+
savedOps: this.savedOps,
|
|
672
|
+
url: this.resolvedUrl.url,
|
|
673
|
+
// no need to save this if there is no pending runtime state
|
|
674
|
+
clientId: pendingRuntimeState !== undefined ? this.clientId : undefined,
|
|
675
|
+
};
|
|
676
|
+
return JSON.stringify(pendingState);
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
get attachState() {
|
|
680
|
+
return this._attachState;
|
|
681
|
+
}
|
|
682
|
+
serialize() {
|
|
683
|
+
assert(this.attachState === AttachState.Detached, 0x0d3 /* "Should only be called in detached container" */);
|
|
684
|
+
const appSummary = this.runtime.createSummary();
|
|
685
|
+
const protocolSummary = this.captureProtocolSummary();
|
|
686
|
+
const combinedSummary = combineAppAndProtocolSummary(appSummary, protocolSummary);
|
|
687
|
+
if (this.detachedBlobStorage && this.detachedBlobStorage.size > 0) {
|
|
688
|
+
combinedSummary.tree[hasBlobsSummaryTree] = {
|
|
689
|
+
type: SummaryType.Blob,
|
|
690
|
+
content: "true",
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
return JSON.stringify(combinedSummary);
|
|
694
|
+
}
|
|
695
|
+
async attach(request, attachProps) {
|
|
696
|
+
await PerformanceEvent.timedExecAsync(this.mc.logger, { eventName: "Attach" }, async () => {
|
|
697
|
+
if (this._lifecycleState !== "loaded") {
|
|
698
|
+
// pre-0.58 error message: containerNotValidForAttach
|
|
699
|
+
throw new UsageError(`The Container is not in a valid state for attach [${this._lifecycleState}]`);
|
|
700
|
+
}
|
|
701
|
+
// If container is already attached or attach is in progress, throw an error.
|
|
702
|
+
assert(this._attachState === AttachState.Detached && !this.attachStarted, 0x205 /* "attach() called more than once" */);
|
|
703
|
+
this.attachStarted = true;
|
|
704
|
+
// If attachment blobs were uploaded in detached state we will go through a different attach flow
|
|
705
|
+
const hasAttachmentBlobs = this.detachedBlobStorage !== undefined && this.detachedBlobStorage.size > 0;
|
|
706
|
+
try {
|
|
707
|
+
assert(this.deltaManager.inbound.length === 0, 0x0d6 /* "Inbound queue should be empty when attaching" */);
|
|
708
|
+
let summary;
|
|
709
|
+
if (!hasAttachmentBlobs) {
|
|
710
|
+
// Get the document state post attach - possibly can just call attach but we need to change the
|
|
711
|
+
// semantics around what the attach means as far as async code goes.
|
|
712
|
+
const appSummary = this.runtime.createSummary();
|
|
713
|
+
const protocolSummary = this.captureProtocolSummary();
|
|
714
|
+
summary = combineAppAndProtocolSummary(appSummary, protocolSummary);
|
|
715
|
+
// Set the state as attaching as we are starting the process of attaching container.
|
|
716
|
+
// This should be fired after taking the summary because it is the place where we are
|
|
717
|
+
// starting to attach the container to storage.
|
|
718
|
+
// Also, this should only be fired in detached container.
|
|
719
|
+
this._attachState = AttachState.Attaching;
|
|
720
|
+
this.runtime.setAttachState(AttachState.Attaching);
|
|
721
|
+
this.emit("attaching");
|
|
722
|
+
if (this.offlineLoadEnabled) {
|
|
723
|
+
const snapshot = getSnapshotTreeFromSerializedContainer(summary);
|
|
724
|
+
this.baseSnapshot = snapshot;
|
|
725
|
+
this.baseSnapshotBlobs =
|
|
726
|
+
getBlobContentsFromTreeWithBlobContents(snapshot);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
// Actually go and create the resolved document
|
|
730
|
+
if (this.service === undefined) {
|
|
731
|
+
const createNewResolvedUrl = await this.urlResolver.resolve(request);
|
|
732
|
+
assert(this.client.details.type !== summarizerClientType &&
|
|
733
|
+
createNewResolvedUrl !== undefined, 0x2c4 /* "client should not be summarizer before container is created" */);
|
|
734
|
+
this.service = await runWithRetry(async () => this.serviceFactory.createContainer(summary, createNewResolvedUrl, this.subLogger, false), "containerAttach", this.mc.logger, {
|
|
735
|
+
cancel: this._deltaManager.closeAbortController.signal,
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
this.storageAdapter.connectToService(this.service);
|
|
739
|
+
if (hasAttachmentBlobs) {
|
|
740
|
+
// upload blobs to storage
|
|
741
|
+
assert(!!this.detachedBlobStorage, 0x24e /* "assertion for type narrowing" */);
|
|
742
|
+
// build a table mapping IDs assigned locally to IDs assigned by storage and pass it to runtime to
|
|
743
|
+
// support blob handles that only know about the local IDs
|
|
744
|
+
const redirectTable = new Map();
|
|
745
|
+
// if new blobs are added while uploading, upload them too
|
|
746
|
+
while (redirectTable.size < this.detachedBlobStorage.size) {
|
|
747
|
+
const newIds = this.detachedBlobStorage
|
|
748
|
+
.getBlobIds()
|
|
749
|
+
.filter((id) => !redirectTable.has(id));
|
|
750
|
+
for (const id of newIds) {
|
|
751
|
+
const blob = await this.detachedBlobStorage.readBlob(id);
|
|
752
|
+
const response = await this.storageAdapter.createBlob(blob);
|
|
753
|
+
redirectTable.set(id, response.id);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
// take summary and upload
|
|
757
|
+
const appSummary = this.runtime.createSummary(redirectTable);
|
|
758
|
+
const protocolSummary = this.captureProtocolSummary();
|
|
759
|
+
summary = combineAppAndProtocolSummary(appSummary, protocolSummary);
|
|
760
|
+
this._attachState = AttachState.Attaching;
|
|
761
|
+
this.runtime.setAttachState(AttachState.Attaching);
|
|
762
|
+
this.emit("attaching");
|
|
763
|
+
if (this.offlineLoadEnabled) {
|
|
764
|
+
const snapshot = getSnapshotTreeFromSerializedContainer(summary);
|
|
765
|
+
this.baseSnapshot = snapshot;
|
|
766
|
+
this.baseSnapshotBlobs =
|
|
767
|
+
getBlobContentsFromTreeWithBlobContents(snapshot);
|
|
768
|
+
}
|
|
769
|
+
await this.storageAdapter.uploadSummaryWithContext(summary, {
|
|
770
|
+
referenceSequenceNumber: 0,
|
|
771
|
+
ackHandle: undefined,
|
|
772
|
+
proposalHandle: undefined,
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
this._attachState = AttachState.Attached;
|
|
776
|
+
this.runtime.setAttachState(AttachState.Attached);
|
|
777
|
+
this.emit("attached");
|
|
778
|
+
if (!this.closed) {
|
|
779
|
+
this.handleDeltaConnectionArg({
|
|
780
|
+
fetchOpsFromStorage: false,
|
|
781
|
+
reason: { text: "createDetached" },
|
|
782
|
+
}, attachProps?.deltaConnection);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
catch (error) {
|
|
786
|
+
// add resolved URL on error object so that host has the ability to find this document and delete it
|
|
787
|
+
const newError = normalizeError(error);
|
|
788
|
+
newError.addTelemetryProperties({ resolvedUrl: this.resolvedUrl?.url });
|
|
789
|
+
this.close(newError);
|
|
790
|
+
throw newError;
|
|
791
|
+
}
|
|
792
|
+
}, { start: true, end: true, cancel: "generic" });
|
|
793
|
+
}
|
|
794
|
+
setAutoReconnectInternal(mode, reason) {
|
|
795
|
+
const currentMode = this._deltaManager.connectionManager.reconnectMode;
|
|
796
|
+
if (currentMode === mode) {
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
const now = performance.now();
|
|
800
|
+
const duration = now - this.setAutoReconnectTime;
|
|
801
|
+
this.setAutoReconnectTime = now;
|
|
802
|
+
this.mc.logger.sendTelemetryEvent({
|
|
803
|
+
eventName: mode === ReconnectMode.Enabled ? "AutoReconnectEnabled" : "AutoReconnectDisabled",
|
|
804
|
+
connectionMode: this.connectionMode,
|
|
805
|
+
connectionState: ConnectionState[this.connectionState],
|
|
806
|
+
duration,
|
|
807
|
+
});
|
|
808
|
+
this._deltaManager.connectionManager.setAutoReconnect(mode, reason);
|
|
809
|
+
}
|
|
810
|
+
connect() {
|
|
811
|
+
if (this.closed) {
|
|
812
|
+
throw new UsageError(`The Container is closed and cannot be connected`);
|
|
813
|
+
}
|
|
814
|
+
else if (this._attachState !== AttachState.Attached) {
|
|
815
|
+
throw new UsageError(`The Container is not attached and cannot be connected`);
|
|
816
|
+
}
|
|
817
|
+
else if (!this.connected) {
|
|
818
|
+
// Note: no need to fetch ops as we do it preemptively as part of DeltaManager.attachOpHandler().
|
|
819
|
+
// If there is gap, we will learn about it once connected, but the gap should be small (if any),
|
|
820
|
+
// assuming that connect() is called quickly after initial container boot.
|
|
821
|
+
this.connectInternal({
|
|
822
|
+
reason: { text: "DocumentConnect" },
|
|
823
|
+
fetchOpsFromStorage: false,
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
connectInternal(args) {
|
|
828
|
+
assert(!this.closed, 0x2c5 /* "Attempting to connect() a closed Container" */);
|
|
829
|
+
assert(this._attachState === AttachState.Attached, 0x2c6 /* "Attempting to connect() a container that is not attached" */);
|
|
830
|
+
// Resume processing ops and connect to delta stream
|
|
831
|
+
this.resumeInternal(args);
|
|
832
|
+
// Set Auto Reconnect Mode
|
|
833
|
+
const mode = ReconnectMode.Enabled;
|
|
834
|
+
this.setAutoReconnectInternal(mode, args.reason);
|
|
835
|
+
}
|
|
836
|
+
disconnect() {
|
|
837
|
+
if (this.closed) {
|
|
838
|
+
throw new UsageError(`The Container is closed and cannot be disconnected`);
|
|
839
|
+
}
|
|
840
|
+
else {
|
|
841
|
+
this.disconnectInternal({ text: "DocumentDisconnect" });
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
disconnectInternal(reason) {
|
|
845
|
+
assert(!this.closed, 0x2c7 /* "Attempting to disconnect() a closed Container" */);
|
|
846
|
+
// Set Auto Reconnect Mode
|
|
847
|
+
const mode = ReconnectMode.Disabled;
|
|
848
|
+
this.setAutoReconnectInternal(mode, reason);
|
|
849
|
+
}
|
|
850
|
+
resumeInternal(args) {
|
|
851
|
+
assert(!this.closed, 0x0d9 /* "Attempting to connect() a closed DeltaManager" */);
|
|
852
|
+
// Resume processing ops
|
|
853
|
+
if (this.inboundQueuePausedFromInit) {
|
|
854
|
+
this.inboundQueuePausedFromInit = false;
|
|
855
|
+
this._deltaManager.inbound.resume();
|
|
856
|
+
this._deltaManager.inboundSignal.resume();
|
|
857
|
+
}
|
|
858
|
+
// Ensure connection to web socket
|
|
859
|
+
this.connectToDeltaStream(args);
|
|
860
|
+
}
|
|
861
|
+
async proposeCodeDetails(codeDetails) {
|
|
862
|
+
if (!isFluidCodeDetails(codeDetails)) {
|
|
863
|
+
throw new Error("Provided codeDetails are not IFluidCodeDetails");
|
|
864
|
+
}
|
|
865
|
+
if (this.codeLoader.IFluidCodeDetailsComparer) {
|
|
866
|
+
const comparison = await this.codeLoader.IFluidCodeDetailsComparer.compare(codeDetails, this.getCodeDetailsFromQuorum());
|
|
867
|
+
if (comparison !== undefined && comparison <= 0) {
|
|
868
|
+
throw new Error("Proposed code details should be greater than the current");
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
return this.protocolHandler.quorum
|
|
872
|
+
.propose("code", codeDetails)
|
|
873
|
+
.then(() => true)
|
|
874
|
+
.catch(() => false);
|
|
875
|
+
}
|
|
876
|
+
async processCodeProposal() {
|
|
877
|
+
const codeDetails = this.getCodeDetailsFromQuorum();
|
|
878
|
+
await Promise.all([
|
|
879
|
+
this.deltaManager.inbound.pause(),
|
|
880
|
+
this.deltaManager.inboundSignal.pause(),
|
|
881
|
+
]);
|
|
882
|
+
if ((await this.satisfies(codeDetails)) === true) {
|
|
883
|
+
this.deltaManager.inbound.resume();
|
|
884
|
+
this.deltaManager.inboundSignal.resume();
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
// pre-0.58 error message: existingContextDoesNotSatisfyIncomingProposal
|
|
888
|
+
const error = new GenericError("Existing context does not satisfy incoming proposal");
|
|
889
|
+
this.close(error);
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* Determines if the currently loaded module satisfies the incoming constraint code details
|
|
893
|
+
*/
|
|
894
|
+
async satisfies(constraintCodeDetails) {
|
|
895
|
+
// If we have no module, it can't satisfy anything.
|
|
896
|
+
if (this._loadedModule === undefined) {
|
|
897
|
+
return false;
|
|
898
|
+
}
|
|
899
|
+
const comparers = [];
|
|
900
|
+
const maybeCompareCodeLoader = this.codeLoader;
|
|
901
|
+
if (maybeCompareCodeLoader.IFluidCodeDetailsComparer !== undefined) {
|
|
902
|
+
comparers.push(maybeCompareCodeLoader.IFluidCodeDetailsComparer);
|
|
903
|
+
}
|
|
904
|
+
const maybeCompareExport = this._loadedModule?.module.fluidExport;
|
|
905
|
+
if (maybeCompareExport?.IFluidCodeDetailsComparer !== undefined) {
|
|
906
|
+
comparers.push(maybeCompareExport.IFluidCodeDetailsComparer);
|
|
907
|
+
}
|
|
908
|
+
// If there are no comparers, then it's impossible to know if the currently loaded package satisfies
|
|
909
|
+
// the incoming constraint, so we return false. Assuming it does not satisfy is safer, to force a reload
|
|
910
|
+
// rather than potentially running with incompatible code.
|
|
911
|
+
if (comparers.length === 0) {
|
|
912
|
+
return false;
|
|
913
|
+
}
|
|
914
|
+
for (const comparer of comparers) {
|
|
915
|
+
const satisfies = await comparer.satisfies(this._loadedModule?.details, constraintCodeDetails);
|
|
916
|
+
if (satisfies === false) {
|
|
917
|
+
return false;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
return true;
|
|
921
|
+
}
|
|
922
|
+
async getVersion(version) {
|
|
923
|
+
const versions = await this.storageAdapter.getVersions(version, 1);
|
|
924
|
+
return versions[0];
|
|
925
|
+
}
|
|
926
|
+
connectToDeltaStream(args) {
|
|
927
|
+
// All agents need "write" access, including summarizer.
|
|
928
|
+
if (!this._canReconnect || !this.client.details.capabilities.interactive) {
|
|
929
|
+
args.mode = "write";
|
|
930
|
+
}
|
|
931
|
+
this._deltaManager.connect(args);
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Load container.
|
|
935
|
+
*
|
|
936
|
+
* @param specifiedVersion - Version SHA to load snapshot. If not specified, will fetch the latest snapshot.
|
|
937
|
+
*/
|
|
938
|
+
async load(specifiedVersion, loadMode, resolvedUrl, pendingLocalState, loadToSequenceNumber) {
|
|
939
|
+
const timings = { phase1: performance.now() };
|
|
940
|
+
this.service = await this.serviceFactory.createDocumentService(resolvedUrl, this.subLogger, this.client.details.type === summarizerClientType);
|
|
941
|
+
// Except in cases where it has stashed ops or requested by feature gate, the container will connect in "read" mode
|
|
942
|
+
const mode = this.mc.config.getBoolean("Fluid.Container.ForceWriteConnection") === true ||
|
|
943
|
+
(pendingLocalState?.savedOps.length ?? 0) > 0
|
|
944
|
+
? "write"
|
|
945
|
+
: "read";
|
|
946
|
+
const connectionArgs = {
|
|
947
|
+
reason: { text: "DocumentOpen" },
|
|
948
|
+
mode,
|
|
949
|
+
fetchOpsFromStorage: false,
|
|
950
|
+
};
|
|
951
|
+
// Start websocket connection as soon as possible. Note that there is no op handler attached yet, but the
|
|
952
|
+
// DeltaManager is resilient to this and will wait to start processing ops until after it is attached.
|
|
953
|
+
if (loadMode.deltaConnection === undefined && !pendingLocalState) {
|
|
954
|
+
this.connectToDeltaStream(connectionArgs);
|
|
955
|
+
}
|
|
956
|
+
this.storageAdapter.connectToService(this.service);
|
|
957
|
+
this._attachState = AttachState.Attached;
|
|
958
|
+
timings.phase2 = performance.now();
|
|
959
|
+
// Fetch specified snapshot.
|
|
960
|
+
const { snapshot, versionId } = pendingLocalState === undefined
|
|
961
|
+
? await this.fetchSnapshotTree(specifiedVersion)
|
|
962
|
+
: { snapshot: pendingLocalState.baseSnapshot, versionId: undefined };
|
|
963
|
+
if (pendingLocalState) {
|
|
964
|
+
this.baseSnapshot = pendingLocalState.baseSnapshot;
|
|
965
|
+
this.baseSnapshotBlobs = pendingLocalState.snapshotBlobs;
|
|
966
|
+
}
|
|
967
|
+
else {
|
|
968
|
+
assert(snapshot !== undefined, 0x237 /* "Snapshot should exist" */);
|
|
969
|
+
if (this.offlineLoadEnabled) {
|
|
970
|
+
this.baseSnapshot = snapshot;
|
|
971
|
+
// Save contents of snapshot now, otherwise closeAndGetPendingLocalState() must be async
|
|
972
|
+
this.baseSnapshotBlobs = await getBlobContentsFromTree(snapshot, this.storageAdapter);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
const attributes = await this.getDocumentAttributes(this.storageAdapter, snapshot);
|
|
976
|
+
// If we saved ops, we will replay them and don't need DeltaManager to fetch them
|
|
977
|
+
const sequenceNumber = pendingLocalState?.savedOps[pendingLocalState.savedOps.length - 1]?.sequenceNumber;
|
|
978
|
+
const dmAttributes = sequenceNumber !== undefined ? { ...attributes, sequenceNumber } : attributes;
|
|
979
|
+
let opsBeforeReturnP;
|
|
980
|
+
if (loadMode.pauseAfterLoad === true) {
|
|
981
|
+
// If we are trying to pause at a specific sequence number, ensure the latest snapshot is not newer than the desired sequence number.
|
|
982
|
+
if (loadMode.opsBeforeReturn === "sequenceNumber") {
|
|
983
|
+
assert(loadToSequenceNumber !== undefined, 0x727 /* sequenceNumber should be defined */);
|
|
984
|
+
// Note: It is possible that we think the latest snapshot is newer than the specified sequence number
|
|
985
|
+
// due to saved ops that may be replayed after the snapshot.
|
|
986
|
+
// https://dev.azure.com/fluidframework/internal/_workitems/edit/5055
|
|
987
|
+
if (dmAttributes.sequenceNumber > loadToSequenceNumber) {
|
|
988
|
+
throw new Error("Cannot satisfy request to pause the container at the specified sequence number. Most recent snapshot is newer than the specified sequence number.");
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
// Force readonly mode - this will ensure we don't receive an error for the lack of join op
|
|
992
|
+
this.forceReadonly(true);
|
|
993
|
+
// We need to setup a listener to stop op processing once we reach the desired sequence number (if specified).
|
|
994
|
+
const opHandler = () => {
|
|
995
|
+
if (loadToSequenceNumber === undefined) {
|
|
996
|
+
// If there is no specified sequence number, pause after the inbound queue is empty.
|
|
997
|
+
if (this.deltaManager.inbound.length !== 0) {
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
else {
|
|
1002
|
+
// If there is a specified sequence number, keep processing until we reach it.
|
|
1003
|
+
if (this.deltaManager.lastSequenceNumber < loadToSequenceNumber) {
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
// Pause op processing once we have processed the desired number of ops.
|
|
1008
|
+
void this.deltaManager.inbound.pause();
|
|
1009
|
+
void this.deltaManager.outbound.pause();
|
|
1010
|
+
this.off("op", opHandler);
|
|
1011
|
+
};
|
|
1012
|
+
if ((loadToSequenceNumber === undefined && this.deltaManager.inbound.length === 0) ||
|
|
1013
|
+
this.deltaManager.lastSequenceNumber === loadToSequenceNumber) {
|
|
1014
|
+
// If we have already reached the desired sequence number, call opHandler() to pause immediately.
|
|
1015
|
+
opHandler();
|
|
1016
|
+
}
|
|
1017
|
+
else {
|
|
1018
|
+
// If we have not yet reached the desired sequence number, setup a listener to pause once we reach it.
|
|
1019
|
+
this.on("op", opHandler);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
// Attach op handlers to finish initialization and be able to start processing ops
|
|
1023
|
+
// Kick off any ops fetching if required.
|
|
1024
|
+
switch (loadMode.opsBeforeReturn) {
|
|
1025
|
+
case undefined:
|
|
1026
|
+
// Start prefetch, but not set opsBeforeReturnP - boot is not blocked by it!
|
|
1027
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
1028
|
+
this.attachDeltaManagerOpHandler(dmAttributes, loadMode.deltaConnection !== "none" ? "all" : "none");
|
|
1029
|
+
break;
|
|
1030
|
+
case "sequenceNumber":
|
|
1031
|
+
case "cached":
|
|
1032
|
+
case "all":
|
|
1033
|
+
opsBeforeReturnP = this.attachDeltaManagerOpHandler(dmAttributes, loadMode.opsBeforeReturn);
|
|
1034
|
+
break;
|
|
1035
|
+
default:
|
|
1036
|
+
unreachableCase(loadMode.opsBeforeReturn);
|
|
1037
|
+
}
|
|
1038
|
+
// ...load in the existing quorum
|
|
1039
|
+
// Initialize the protocol handler
|
|
1040
|
+
await this.initializeProtocolStateFromSnapshot(attributes, this.storageAdapter, snapshot);
|
|
1041
|
+
timings.phase3 = performance.now();
|
|
1042
|
+
const codeDetails = this.getCodeDetailsFromQuorum();
|
|
1043
|
+
await this.instantiateRuntime(codeDetails, snapshot,
|
|
1044
|
+
// give runtime a dummy value so it knows we're loading from a stash blob
|
|
1045
|
+
pendingLocalState ? pendingLocalState?.pendingRuntimeState ?? {} : undefined);
|
|
1046
|
+
// replay saved ops
|
|
1047
|
+
if (pendingLocalState) {
|
|
1048
|
+
for (const message of pendingLocalState.savedOps) {
|
|
1049
|
+
this.processRemoteMessage({
|
|
1050
|
+
...message,
|
|
1051
|
+
metadata: { ...message.metadata, savedOp: true },
|
|
1052
|
+
});
|
|
1053
|
+
// allow runtime to apply stashed ops at this op's sequence number
|
|
1054
|
+
await this.runtime.notifyOpReplay?.(message);
|
|
1055
|
+
}
|
|
1056
|
+
pendingLocalState.savedOps = [];
|
|
1057
|
+
}
|
|
1058
|
+
// We might have hit some failure that did not manifest itself in exception in this flow,
|
|
1059
|
+
// do not start op processing in such case - static version of Container.load() will handle it correctly.
|
|
1060
|
+
if (!this.closed) {
|
|
1061
|
+
if (opsBeforeReturnP !== undefined) {
|
|
1062
|
+
this._deltaManager.inbound.resume();
|
|
1063
|
+
await PerformanceEvent.timedExecAsync(this.mc.logger, { eventName: "WaitOps" }, async () => opsBeforeReturnP);
|
|
1064
|
+
await PerformanceEvent.timedExecAsync(this.mc.logger, { eventName: "WaitOpProcessing" }, async () => this._deltaManager.inbound.waitTillProcessingDone());
|
|
1065
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
1066
|
+
this._deltaManager.inbound.pause();
|
|
1067
|
+
}
|
|
1068
|
+
this.handleDeltaConnectionArg(connectionArgs, loadMode.deltaConnection, pendingLocalState !== undefined);
|
|
1069
|
+
}
|
|
1070
|
+
// If we have not yet reached `loadToSequenceNumber`, we will wait for ops to arrive until we reach it
|
|
1071
|
+
if (loadToSequenceNumber !== undefined &&
|
|
1072
|
+
this.deltaManager.lastSequenceNumber < loadToSequenceNumber) {
|
|
1073
|
+
await new Promise((resolve, reject) => {
|
|
1074
|
+
const opHandler = (message) => {
|
|
1075
|
+
if (message.sequenceNumber >= loadToSequenceNumber) {
|
|
1076
|
+
resolve();
|
|
1077
|
+
this.off("op", opHandler);
|
|
1078
|
+
}
|
|
1079
|
+
};
|
|
1080
|
+
this.on("op", opHandler);
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
// Safety net: static version of Container.load() should have learned about it through "closed" handler.
|
|
1084
|
+
// But if that did not happen for some reason, fail load for sure.
|
|
1085
|
+
// Otherwise we can get into situations where container is closed and does not try to connect to ordering
|
|
1086
|
+
// service, but caller does not know that (callers do expect container to be not closed on successful path
|
|
1087
|
+
// and listen only on "closed" event)
|
|
1088
|
+
if (this.closed) {
|
|
1089
|
+
throw new Error("Container was closed while load()");
|
|
1090
|
+
}
|
|
1091
|
+
// Internal context is fully loaded at this point
|
|
1092
|
+
this.setLoaded();
|
|
1093
|
+
timings.end = performance.now();
|
|
1094
|
+
this.subLogger.sendTelemetryEvent({
|
|
1095
|
+
eventName: "LoadStagesTimings",
|
|
1096
|
+
details: JSON.stringify(timings),
|
|
1097
|
+
}, undefined, LogLevel.verbose);
|
|
1098
|
+
return {
|
|
1099
|
+
sequenceNumber: attributes.sequenceNumber,
|
|
1100
|
+
version: versionId,
|
|
1101
|
+
dmLastProcessedSeqNumber: this._deltaManager.lastSequenceNumber,
|
|
1102
|
+
dmLastKnownSeqNumber: this._deltaManager.lastKnownSeqNumber,
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
async createDetached(codeDetails) {
|
|
1106
|
+
const attributes = {
|
|
1107
|
+
sequenceNumber: detachedContainerRefSeqNumber,
|
|
1108
|
+
minimumSequenceNumber: 0,
|
|
1109
|
+
};
|
|
1110
|
+
await this.attachDeltaManagerOpHandler(attributes);
|
|
1111
|
+
// Need to just seed the source data in the code quorum. Quorum itself is empty
|
|
1112
|
+
const qValues = initQuorumValuesFromCodeDetails(codeDetails);
|
|
1113
|
+
this.initializeProtocolState(attributes, {
|
|
1114
|
+
members: [],
|
|
1115
|
+
proposals: [],
|
|
1116
|
+
values: qValues,
|
|
1117
|
+
});
|
|
1118
|
+
await this.instantiateRuntime(codeDetails, undefined);
|
|
1119
|
+
this.setLoaded();
|
|
1120
|
+
}
|
|
1121
|
+
async rehydrateDetachedFromSnapshot(detachedContainerSnapshot) {
|
|
1122
|
+
if (detachedContainerSnapshot.tree[hasBlobsSummaryTree] !== undefined) {
|
|
1123
|
+
assert(!!this.detachedBlobStorage && this.detachedBlobStorage.size > 0, 0x250 /* "serialized container with attachment blobs must be rehydrated with detached blob storage" */);
|
|
1124
|
+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
1125
|
+
delete detachedContainerSnapshot.tree[hasBlobsSummaryTree];
|
|
1126
|
+
}
|
|
1127
|
+
const snapshotTree = getSnapshotTreeFromSerializedContainer(detachedContainerSnapshot);
|
|
1128
|
+
this.storageAdapter.loadSnapshotForRehydratingContainer(snapshotTree);
|
|
1129
|
+
const attributes = await this.getDocumentAttributes(this.storageAdapter, snapshotTree);
|
|
1130
|
+
await this.attachDeltaManagerOpHandler(attributes);
|
|
1131
|
+
// Initialize the protocol handler
|
|
1132
|
+
const baseTree = getProtocolSnapshotTree(snapshotTree);
|
|
1133
|
+
const qValues = await readAndParse(this.storageAdapter, baseTree.blobs.quorumValues);
|
|
1134
|
+
this.initializeProtocolState(attributes, {
|
|
1135
|
+
members: [],
|
|
1136
|
+
proposals: [],
|
|
1137
|
+
values: qValues,
|
|
1138
|
+
});
|
|
1139
|
+
const codeDetails = this.getCodeDetailsFromQuorum();
|
|
1140
|
+
await this.instantiateRuntime(codeDetails, snapshotTree);
|
|
1141
|
+
this.setLoaded();
|
|
1142
|
+
}
|
|
1143
|
+
async getDocumentAttributes(storage, tree) {
|
|
1144
|
+
if (tree === undefined) {
|
|
1145
|
+
return {
|
|
1146
|
+
minimumSequenceNumber: 0,
|
|
1147
|
+
sequenceNumber: 0,
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
// Backward compatibility: old docs would have ".attributes" instead of "attributes"
|
|
1151
|
+
const attributesHash = ".protocol" in tree.trees
|
|
1152
|
+
? tree.trees[".protocol"].blobs.attributes
|
|
1153
|
+
: tree.blobs[".attributes"];
|
|
1154
|
+
const attributes = await readAndParse(storage, attributesHash);
|
|
1155
|
+
return attributes;
|
|
1156
|
+
}
|
|
1157
|
+
async initializeProtocolStateFromSnapshot(attributes, storage, snapshot) {
|
|
1158
|
+
const quorumSnapshot = {
|
|
1159
|
+
members: [],
|
|
1160
|
+
proposals: [],
|
|
1161
|
+
values: [],
|
|
1162
|
+
};
|
|
1163
|
+
if (snapshot !== undefined) {
|
|
1164
|
+
const baseTree = getProtocolSnapshotTree(snapshot);
|
|
1165
|
+
[quorumSnapshot.members, quorumSnapshot.proposals, quorumSnapshot.values] =
|
|
1166
|
+
await Promise.all([
|
|
1167
|
+
readAndParse(storage, baseTree.blobs.quorumMembers),
|
|
1168
|
+
readAndParse(storage, baseTree.blobs.quorumProposals),
|
|
1169
|
+
readAndParse(storage, baseTree.blobs.quorumValues),
|
|
1170
|
+
]);
|
|
1171
|
+
}
|
|
1172
|
+
this.initializeProtocolState(attributes, quorumSnapshot);
|
|
1173
|
+
}
|
|
1174
|
+
initializeProtocolState(attributes, quorumSnapshot) {
|
|
1175
|
+
const protocol = this.protocolHandlerBuilder(attributes, quorumSnapshot, (key, value) => this.submitMessage(MessageType.Propose, JSON.stringify({ key, value })));
|
|
1176
|
+
const protocolLogger = createChildLogger({
|
|
1177
|
+
logger: this.subLogger,
|
|
1178
|
+
namespace: "ProtocolHandler",
|
|
1179
|
+
});
|
|
1180
|
+
protocol.quorum.on("error", (error) => {
|
|
1181
|
+
protocolLogger.sendErrorEvent(error);
|
|
1182
|
+
});
|
|
1183
|
+
// Track membership changes and update connection state accordingly
|
|
1184
|
+
this.connectionStateHandler.initProtocol(protocol);
|
|
1185
|
+
protocol.quorum.on("addProposal", (proposal) => {
|
|
1186
|
+
if (proposal.key === "code" || proposal.key === "code2") {
|
|
1187
|
+
this.emit("codeDetailsProposed", proposal.value, proposal);
|
|
1188
|
+
}
|
|
1189
|
+
});
|
|
1190
|
+
protocol.quorum.on("approveProposal", (sequenceNumber, key, value) => {
|
|
1191
|
+
if (key === "code" || key === "code2") {
|
|
1192
|
+
if (!isFluidCodeDetails(value)) {
|
|
1193
|
+
this.mc.logger.sendErrorEvent({
|
|
1194
|
+
eventName: "CodeProposalNotIFluidCodeDetails",
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
this.processCodeProposal().catch((error) => {
|
|
1198
|
+
const normalizedError = normalizeError(error);
|
|
1199
|
+
this.close(normalizedError);
|
|
1200
|
+
throw error;
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
});
|
|
1204
|
+
// we need to make sure this member get set in a synchronous context,
|
|
1205
|
+
// or other things can happen after the object that will be set is created, but not yet set
|
|
1206
|
+
// this was breaking this._initialClients handling
|
|
1207
|
+
//
|
|
1208
|
+
this._protocolHandler = protocol;
|
|
1209
|
+
}
|
|
1210
|
+
captureProtocolSummary() {
|
|
1211
|
+
const quorumSnapshot = this.protocolHandler.snapshot();
|
|
1212
|
+
const summary = {
|
|
1213
|
+
tree: {
|
|
1214
|
+
attributes: {
|
|
1215
|
+
content: JSON.stringify(this.protocolHandler.attributes),
|
|
1216
|
+
type: SummaryType.Blob,
|
|
1217
|
+
},
|
|
1218
|
+
quorumMembers: {
|
|
1219
|
+
content: JSON.stringify(quorumSnapshot.members),
|
|
1220
|
+
type: SummaryType.Blob,
|
|
1221
|
+
},
|
|
1222
|
+
quorumProposals: {
|
|
1223
|
+
content: JSON.stringify(quorumSnapshot.proposals),
|
|
1224
|
+
type: SummaryType.Blob,
|
|
1225
|
+
},
|
|
1226
|
+
quorumValues: {
|
|
1227
|
+
content: JSON.stringify(quorumSnapshot.values),
|
|
1228
|
+
type: SummaryType.Blob,
|
|
1229
|
+
},
|
|
1230
|
+
},
|
|
1231
|
+
type: SummaryType.Tree,
|
|
1232
|
+
};
|
|
1233
|
+
return summary;
|
|
1234
|
+
}
|
|
1235
|
+
getCodeDetailsFromQuorum() {
|
|
1236
|
+
const quorum = this.protocolHandler.quorum;
|
|
1237
|
+
const pkg = getCodeProposal(quorum);
|
|
1238
|
+
return pkg;
|
|
1239
|
+
}
|
|
1240
|
+
static setupClient(containerId, options, clientDetailsOverride) {
|
|
1241
|
+
const loaderOptionsClient = structuredClone(options?.client);
|
|
1242
|
+
const client = loaderOptionsClient !== undefined
|
|
1243
|
+
? loaderOptionsClient
|
|
1244
|
+
: {
|
|
1245
|
+
details: {
|
|
1246
|
+
capabilities: { interactive: true },
|
|
1247
|
+
},
|
|
1248
|
+
mode: "read",
|
|
1249
|
+
permission: [],
|
|
1250
|
+
scopes: [],
|
|
1251
|
+
user: { id: "" },
|
|
1252
|
+
};
|
|
1253
|
+
if (clientDetailsOverride !== undefined) {
|
|
1254
|
+
client.details = {
|
|
1255
|
+
...client.details,
|
|
1256
|
+
...clientDetailsOverride,
|
|
1257
|
+
capabilities: {
|
|
1258
|
+
...client.details.capabilities,
|
|
1259
|
+
...clientDetailsOverride?.capabilities,
|
|
1260
|
+
},
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
client.details.environment = [
|
|
1264
|
+
client.details.environment,
|
|
1265
|
+
` loaderVersion:${pkgVersion}`,
|
|
1266
|
+
` containerId:${containerId}`,
|
|
1267
|
+
].join(";");
|
|
1268
|
+
return client;
|
|
1269
|
+
}
|
|
1270
|
+
/**
|
|
1271
|
+
* Returns true if connection is active, i.e. it's "write" connection and
|
|
1272
|
+
* container runtime was notified about this connection (i.e. we are up-to-date and could send ops).
|
|
1273
|
+
* This happens after client received its own joinOp and thus is in the quorum.
|
|
1274
|
+
* If it's not true, runtime is not in position to send ops.
|
|
1275
|
+
*/
|
|
1276
|
+
activeConnection() {
|
|
1277
|
+
return (this.connectionState === ConnectionState.Connected && this.connectionMode === "write");
|
|
1278
|
+
}
|
|
1279
|
+
createDeltaManager() {
|
|
1280
|
+
const serviceProvider = () => this.service;
|
|
1281
|
+
const deltaManager = new DeltaManager(serviceProvider, createChildLogger({ logger: this.subLogger, namespace: "DeltaManager" }), () => this.activeConnection(), (props) => new ConnectionManager(serviceProvider, () => this.isDirty, this.client, this._canReconnect, createChildLogger({ logger: this.subLogger, namespace: "ConnectionManager" }), props));
|
|
1282
|
+
// Disable inbound queues as Container is not ready to accept any ops until we are fully loaded!
|
|
1283
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
1284
|
+
deltaManager.inbound.pause();
|
|
1285
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
1286
|
+
deltaManager.inboundSignal.pause();
|
|
1287
|
+
deltaManager.on("connect", (details, _opsBehind) => {
|
|
1288
|
+
assert(this.connectionMode === details.mode, 0x4b7 /* mismatch */);
|
|
1289
|
+
this.connectionStateHandler.receivedConnectEvent(details);
|
|
1290
|
+
});
|
|
1291
|
+
deltaManager.on("establishingConnection", (reason) => {
|
|
1292
|
+
this.connectionStateHandler.establishingConnection(reason);
|
|
1293
|
+
});
|
|
1294
|
+
deltaManager.on("cancelEstablishingConnection", (reason) => {
|
|
1295
|
+
this.connectionStateHandler.cancelEstablishingConnection(reason);
|
|
1296
|
+
});
|
|
1297
|
+
deltaManager.on("disconnect", (reason) => {
|
|
1298
|
+
this.noopHeuristic?.notifyDisconnect();
|
|
1299
|
+
if (!this.closed) {
|
|
1300
|
+
this.connectionStateHandler.receivedDisconnectEvent(reason);
|
|
1301
|
+
}
|
|
1302
|
+
});
|
|
1303
|
+
deltaManager.on("throttled", (warning) => {
|
|
1304
|
+
const warn = warning;
|
|
1305
|
+
// Some "warning" events come from outside the container and are logged
|
|
1306
|
+
// elsewhere (e.g. summarizing container). We shouldn't log these here.
|
|
1307
|
+
if (warn.logged !== true) {
|
|
1308
|
+
this.mc.logger.sendTelemetryEvent({ eventName: "ContainerWarning" }, warn);
|
|
1309
|
+
}
|
|
1310
|
+
this.emit("warning", warn);
|
|
1311
|
+
});
|
|
1312
|
+
deltaManager.on("readonly", (readonly) => {
|
|
1313
|
+
this.setContextConnectedState(this.connectionState === ConnectionState.Connected, readonly);
|
|
1314
|
+
this.emit("readonly", readonly);
|
|
1315
|
+
});
|
|
1316
|
+
deltaManager.on("closed", (error) => {
|
|
1317
|
+
this.closeCore(error);
|
|
1318
|
+
});
|
|
1319
|
+
deltaManager.on("disposed", (error) => {
|
|
1320
|
+
this.disposeCore(error);
|
|
1321
|
+
});
|
|
1322
|
+
return deltaManager;
|
|
1323
|
+
}
|
|
1324
|
+
async attachDeltaManagerOpHandler(attributes, prefetchType) {
|
|
1325
|
+
return this._deltaManager.attachOpHandler(attributes.minimumSequenceNumber, attributes.sequenceNumber, {
|
|
1326
|
+
process: (message) => this.processRemoteMessage(message),
|
|
1327
|
+
processSignal: (message) => {
|
|
1328
|
+
this.processSignal(message);
|
|
1329
|
+
},
|
|
1330
|
+
}, prefetchType);
|
|
1331
|
+
}
|
|
1332
|
+
logConnectionStateChangeTelemetry(value, oldState, reason) {
|
|
1333
|
+
// Log actual event
|
|
1334
|
+
const time = performance.now();
|
|
1335
|
+
this.connectionTransitionTimes[value] = time;
|
|
1336
|
+
const duration = time - this.connectionTransitionTimes[oldState];
|
|
1337
|
+
let durationFromDisconnected;
|
|
1338
|
+
let connectionInitiationReason;
|
|
1339
|
+
let autoReconnect;
|
|
1340
|
+
let checkpointSequenceNumber;
|
|
1341
|
+
let opsBehind;
|
|
1342
|
+
if (value === ConnectionState.Disconnected) {
|
|
1343
|
+
autoReconnect = this._deltaManager.connectionManager.reconnectMode;
|
|
1344
|
+
}
|
|
1345
|
+
else {
|
|
1346
|
+
if (value === ConnectionState.Connected) {
|
|
1347
|
+
durationFromDisconnected =
|
|
1348
|
+
time - this.connectionTransitionTimes[ConnectionState.Disconnected];
|
|
1349
|
+
durationFromDisconnected = formatTick(durationFromDisconnected);
|
|
1350
|
+
}
|
|
1351
|
+
else if (value === ConnectionState.CatchingUp) {
|
|
1352
|
+
// This info is of most interesting while Catching Up.
|
|
1353
|
+
checkpointSequenceNumber = this.deltaManager.lastKnownSeqNumber;
|
|
1354
|
+
// Need to check that we have already loaded and fetched the snapshot.
|
|
1355
|
+
if (this.deltaManager.hasCheckpointSequenceNumber &&
|
|
1356
|
+
this._lifecycleState === "loaded") {
|
|
1357
|
+
opsBehind = checkpointSequenceNumber - this.deltaManager.lastSequenceNumber;
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
connectionInitiationReason = this.firstConnection ? "InitialConnect" : "AutoReconnect";
|
|
1361
|
+
}
|
|
1362
|
+
this.mc.logger.sendPerformanceEvent({
|
|
1363
|
+
eventName: `ConnectionStateChange_${ConnectionState[value]}`,
|
|
1364
|
+
from: ConnectionState[oldState],
|
|
1365
|
+
duration,
|
|
1366
|
+
durationFromDisconnected,
|
|
1367
|
+
reason: reason?.text,
|
|
1368
|
+
connectionInitiationReason,
|
|
1369
|
+
pendingClientId: this.connectionStateHandler.pendingClientId,
|
|
1370
|
+
clientId: this.clientId,
|
|
1371
|
+
autoReconnect,
|
|
1372
|
+
opsBehind,
|
|
1373
|
+
online: OnlineStatus[isOnline()],
|
|
1374
|
+
lastVisible: this.lastVisible !== undefined
|
|
1375
|
+
? performance.now() - this.lastVisible
|
|
1376
|
+
: undefined,
|
|
1377
|
+
checkpointSequenceNumber,
|
|
1378
|
+
quorumSize: this._protocolHandler?.quorum.getMembers().size,
|
|
1379
|
+
isDirty: this.isDirty,
|
|
1380
|
+
...this._deltaManager.connectionProps,
|
|
1381
|
+
}, reason?.error);
|
|
1382
|
+
if (value === ConnectionState.Connected) {
|
|
1383
|
+
this.firstConnection = false;
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
propagateConnectionState(initialTransition, disconnectedReason) {
|
|
1387
|
+
// When container loaded, we want to propagate initial connection state.
|
|
1388
|
+
// After that, we communicate only transitions to Connected & Disconnected states, skipping all other states.
|
|
1389
|
+
// This can be changed in the future, for example we likely should add "CatchingUp" event on Container.
|
|
1390
|
+
if (!initialTransition &&
|
|
1391
|
+
this.connectionState !== ConnectionState.Connected &&
|
|
1392
|
+
this.connectionState !== ConnectionState.Disconnected) {
|
|
1393
|
+
return;
|
|
1394
|
+
}
|
|
1395
|
+
const state = this.connectionState === ConnectionState.Connected;
|
|
1396
|
+
// Both protocol and context should not be undefined if we got so far.
|
|
1397
|
+
this.setContextConnectedState(state, this.readOnlyInfo.readonly ?? false);
|
|
1398
|
+
this.protocolHandler.setConnectionState(state, this.clientId);
|
|
1399
|
+
raiseConnectedEvent(this.mc.logger, this, state, this.clientId, disconnectedReason?.text);
|
|
1400
|
+
}
|
|
1401
|
+
// back-compat: ADO #1385: Remove in the future, summary op should come through submitSummaryMessage()
|
|
1402
|
+
submitContainerMessage(type, contents, batch, metadata) {
|
|
1403
|
+
switch (type) {
|
|
1404
|
+
case MessageType.Operation:
|
|
1405
|
+
return this.submitMessage(type, JSON.stringify(contents), batch, metadata);
|
|
1406
|
+
case MessageType.Summarize:
|
|
1407
|
+
return this.submitSummaryMessage(contents);
|
|
1408
|
+
default: {
|
|
1409
|
+
const newError = new GenericError("invalidContainerSubmitOpType", undefined /* error */, { messageType: type });
|
|
1410
|
+
this.close(newError);
|
|
1411
|
+
return -1;
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
/** @returns clientSequenceNumber of last message in a batch */
|
|
1416
|
+
submitBatch(batch, referenceSequenceNumber) {
|
|
1417
|
+
let clientSequenceNumber = -1;
|
|
1418
|
+
for (const message of batch) {
|
|
1419
|
+
clientSequenceNumber = this.submitMessage(MessageType.Operation, message.contents, true, // batch
|
|
1420
|
+
message.metadata, message.compression, referenceSequenceNumber);
|
|
1421
|
+
}
|
|
1422
|
+
this._deltaManager.flush();
|
|
1423
|
+
return clientSequenceNumber;
|
|
1424
|
+
}
|
|
1425
|
+
submitSummaryMessage(summary, referenceSequenceNumber) {
|
|
1426
|
+
// github #6451: this is only needed for staging so the server
|
|
1427
|
+
// know when the protocol tree is included
|
|
1428
|
+
// this can be removed once all clients send
|
|
1429
|
+
// protocol tree by default
|
|
1430
|
+
if (summary.details === undefined) {
|
|
1431
|
+
summary.details = {};
|
|
1432
|
+
}
|
|
1433
|
+
summary.details.includesProtocolTree = this.storageAdapter.summarizeProtocolTree;
|
|
1434
|
+
return this.submitMessage(MessageType.Summarize, JSON.stringify(summary), false /* batch */, undefined /* metadata */, undefined /* compression */, referenceSequenceNumber);
|
|
1435
|
+
}
|
|
1436
|
+
submitMessage(type, contents, batch, metadata, compression, referenceSequenceNumber) {
|
|
1437
|
+
if (this.connectionState !== ConnectionState.Connected) {
|
|
1438
|
+
this.mc.logger.sendErrorEvent({ eventName: "SubmitMessageWithNoConnection", type });
|
|
1439
|
+
return -1;
|
|
1440
|
+
}
|
|
1441
|
+
this.noopHeuristic?.notifyMessageSent();
|
|
1442
|
+
return this._deltaManager.submit(type, contents, batch, metadata, compression, referenceSequenceNumber);
|
|
1443
|
+
}
|
|
1444
|
+
processRemoteMessage(message) {
|
|
1445
|
+
if (this.offlineLoadEnabled) {
|
|
1446
|
+
this.savedOps.push(message);
|
|
1447
|
+
}
|
|
1448
|
+
const local = this.clientId === message.clientId;
|
|
1449
|
+
// Allow the protocol handler to process the message
|
|
1450
|
+
const result = this.protocolHandler.processMessage(message, local);
|
|
1451
|
+
// Forward messages to the loaded runtime for processing
|
|
1452
|
+
this.runtime.process(message, local);
|
|
1453
|
+
// Inactive (not in quorum or not writers) clients don't take part in the minimum sequence number calculation.
|
|
1454
|
+
if (this.activeConnection()) {
|
|
1455
|
+
if (this.noopHeuristic === undefined) {
|
|
1456
|
+
const serviceConfiguration = this.deltaManager.serviceConfiguration;
|
|
1457
|
+
// Note that config from first connection will be used for this container's lifetime.
|
|
1458
|
+
// That means that if relay service changes settings, such changes will impact only newly booted
|
|
1459
|
+
// clients.
|
|
1460
|
+
// All existing will continue to use settings they got earlier.
|
|
1461
|
+
assert(serviceConfiguration !== undefined, 0x2e4 /* "there should be service config for active connection" */);
|
|
1462
|
+
this.noopHeuristic = new NoopHeuristic(serviceConfiguration.noopTimeFrequency, serviceConfiguration.noopCountFrequency);
|
|
1463
|
+
this.noopHeuristic.on("wantsNoop", () => {
|
|
1464
|
+
// On disconnect we notify the heuristic which should prevent it from wanting a noop.
|
|
1465
|
+
// Hitting this assert would imply we lost activeConnection between notifying the heuristic of a processed message and
|
|
1466
|
+
// running the microtask that the heuristic queued in response.
|
|
1467
|
+
assert(this.activeConnection(), 0x241 /* "Trying to send noop without active connection" */);
|
|
1468
|
+
this.submitMessage(MessageType.NoOp);
|
|
1469
|
+
});
|
|
1470
|
+
}
|
|
1471
|
+
this.noopHeuristic.notifyMessageProcessed(message);
|
|
1472
|
+
// The contract with the protocolHandler is that returning "immediateNoOp" is equivalent to "please immediately accept the proposal I just processed".
|
|
1473
|
+
if (result.immediateNoOp === true) {
|
|
1474
|
+
// ADO:1385: Remove cast and use MessageType once definition changes propagate
|
|
1475
|
+
this.submitMessage(MessageType2.Accept);
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
this.emit("op", message);
|
|
1479
|
+
}
|
|
1480
|
+
submitSignal(content, targetClientId) {
|
|
1481
|
+
this._deltaManager.submitSignal(JSON.stringify(content), targetClientId);
|
|
1482
|
+
}
|
|
1483
|
+
processSignal(message) {
|
|
1484
|
+
// No clientId indicates a system signal message.
|
|
1485
|
+
if (protocolHandlerShouldProcessSignal(message)) {
|
|
1486
|
+
this.protocolHandler.processSignal(message);
|
|
1487
|
+
}
|
|
1488
|
+
else {
|
|
1489
|
+
const local = this.clientId === message.clientId;
|
|
1490
|
+
this.runtime.processSignal(message, local);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
/**
|
|
1494
|
+
* Get the most recent snapshot, or a specific version.
|
|
1495
|
+
* @param specifiedVersion - The specific version of the snapshot to retrieve
|
|
1496
|
+
* @returns The snapshot requested, or the latest snapshot if no version was specified, plus version ID
|
|
1497
|
+
*/
|
|
1498
|
+
async fetchSnapshotTree(specifiedVersion) {
|
|
1499
|
+
const version = await this.getVersion(specifiedVersion ?? null);
|
|
1500
|
+
if (version === undefined && specifiedVersion !== undefined) {
|
|
1501
|
+
// We should have a defined version to load from if specified version requested
|
|
1502
|
+
this.mc.logger.sendErrorEvent({
|
|
1503
|
+
eventName: "NoVersionFoundWhenSpecified",
|
|
1504
|
+
id: specifiedVersion,
|
|
1505
|
+
});
|
|
1506
|
+
}
|
|
1507
|
+
this._loadedFromVersion = version;
|
|
1508
|
+
const snapshot = (await this.storageAdapter.getSnapshotTree(version)) ?? undefined;
|
|
1509
|
+
if (snapshot === undefined && version !== undefined) {
|
|
1510
|
+
this.mc.logger.sendErrorEvent({ eventName: "getSnapshotTreeFailed", id: version.id });
|
|
1511
|
+
}
|
|
1512
|
+
return { snapshot, versionId: version?.id };
|
|
1513
|
+
}
|
|
1514
|
+
async instantiateRuntime(codeDetails, snapshot, pendingLocalState) {
|
|
1515
|
+
assert(this._runtime?.disposed !== false, 0x0dd /* "Existing runtime not disposed" */);
|
|
1516
|
+
// The relative loader will proxy requests to '/' to the loader itself assuming no non-cache flags
|
|
1517
|
+
// are set. Global requests will still go directly to the loader
|
|
1518
|
+
const maybeLoader = this.scope;
|
|
1519
|
+
const loader = new RelativeLoader(this, maybeLoader.ILoader);
|
|
1520
|
+
const loadCodeResult = await PerformanceEvent.timedExecAsync(this.subLogger, { eventName: "CodeLoad" }, async () => this.codeLoader.load(codeDetails));
|
|
1521
|
+
this._loadedModule = {
|
|
1522
|
+
module: loadCodeResult.module,
|
|
1523
|
+
// An older interface ICodeLoader could return an IFluidModule which didn't have details.
|
|
1524
|
+
// If we're using one of those older ICodeLoaders, then we fix up the module with the specified details here.
|
|
1525
|
+
// TODO: Determine if this is still a realistic scenario or if this fixup could be removed.
|
|
1526
|
+
details: loadCodeResult.details ?? codeDetails,
|
|
1527
|
+
};
|
|
1528
|
+
const fluidExport = this._loadedModule.module.fluidExport;
|
|
1529
|
+
const runtimeFactory = fluidExport?.IRuntimeFactory;
|
|
1530
|
+
if (runtimeFactory === undefined) {
|
|
1531
|
+
throw new Error(packageNotFactoryError);
|
|
1532
|
+
}
|
|
1533
|
+
const getSpecifiedCodeDetails = () => (this.protocolHandler.quorum.get("code") ??
|
|
1534
|
+
this.protocolHandler.quorum.get("code2"));
|
|
1535
|
+
const existing = snapshot !== undefined;
|
|
1536
|
+
const context = new ContainerContext(this.options, this.scope, snapshot, this._loadedFromVersion, this._deltaManager, this.storageAdapter, this.protocolHandler.quorum, this.protocolHandler.audience, loader, (type, contents, batch, metadata) => this.submitContainerMessage(type, contents, batch, metadata), (summaryOp, referenceSequenceNumber) => this.submitSummaryMessage(summaryOp, referenceSequenceNumber), (batch, referenceSequenceNumber) => this.submitBatch(batch, referenceSequenceNumber), (content, targetClientId) => this.submitSignal(content, targetClientId), (error) => this.dispose(error), (error) => this.close(error), this.updateDirtyContainerState, this.getAbsoluteUrl, () => this.resolvedUrl?.id, () => this.clientId, () => this.attachState, () => this.connected, getSpecifiedCodeDetails, this._deltaManager.clientDetails, existing, this.subLogger, pendingLocalState);
|
|
1537
|
+
this._runtime = await PerformanceEvent.timedExecAsync(this.subLogger, { eventName: "InstantiateRuntime" }, async () => runtimeFactory.instantiateRuntime(context, existing));
|
|
1538
|
+
this._lifecycleEvents.emit("runtimeInstantiated");
|
|
1539
|
+
this._loadedCodeDetails = codeDetails;
|
|
1540
|
+
}
|
|
1541
|
+
/**
|
|
1542
|
+
* Set the connected state of the ContainerContext
|
|
1543
|
+
* This controls the "connected" state of the ContainerRuntime as well
|
|
1544
|
+
* @param state - Is the container currently connected?
|
|
1545
|
+
* @param readonly - Is the container in readonly mode?
|
|
1546
|
+
*/
|
|
1547
|
+
setContextConnectedState(state, readonly) {
|
|
1548
|
+
if (this._runtime?.disposed === false) {
|
|
1549
|
+
/**
|
|
1550
|
+
* We want to lie to the ContainerRuntime when we are in readonly mode to prevent issues with pending
|
|
1551
|
+
* ops getting through to the DeltaManager.
|
|
1552
|
+
* The ContainerRuntime's "connected" state simply means it is ok to send ops
|
|
1553
|
+
* See https://dev.azure.com/fluidframework/internal/_workitems/edit/1246
|
|
1554
|
+
*/
|
|
1555
|
+
this.runtime.setConnectionState(state && !readonly, this.clientId);
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
handleDeltaConnectionArg(connectionArgs, deltaConnectionArg, canConnect = true) {
|
|
1559
|
+
switch (deltaConnectionArg) {
|
|
1560
|
+
case undefined:
|
|
1561
|
+
if (canConnect) {
|
|
1562
|
+
// connect to delta stream now since we did not before
|
|
1563
|
+
this.connectToDeltaStream(connectionArgs);
|
|
1564
|
+
}
|
|
1565
|
+
// intentional fallthrough
|
|
1566
|
+
case "delayed":
|
|
1567
|
+
assert(this.inboundQueuePausedFromInit, 0x346 /* inboundQueuePausedFromInit should be true */);
|
|
1568
|
+
this.inboundQueuePausedFromInit = false;
|
|
1569
|
+
this._deltaManager.inbound.resume();
|
|
1570
|
+
this._deltaManager.inboundSignal.resume();
|
|
1571
|
+
break;
|
|
1572
|
+
case "none":
|
|
1573
|
+
break;
|
|
1574
|
+
default:
|
|
1575
|
+
unreachableCase(deltaConnectionArg);
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
//# sourceMappingURL=container.mjs.map
|