@fluidframework/container-loader 2.0.0-dev.5.2.0.169897 → 2.0.0-dev.6.4.0.191258
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +162 -0
- package/README.md +10 -6
- package/dist/audience.d.ts +1 -0
- package/dist/audience.d.ts.map +1 -1
- package/dist/audience.js +5 -3
- package/dist/audience.js.map +1 -1
- package/dist/catchUpMonitor.d.ts +1 -1
- package/dist/catchUpMonitor.d.ts.map +1 -1
- package/dist/catchUpMonitor.js +2 -2
- package/dist/catchUpMonitor.js.map +1 -1
- package/dist/connectionManager.d.ts +6 -6
- package/dist/connectionManager.d.ts.map +1 -1
- package/dist/connectionManager.js +97 -93
- package/dist/connectionManager.js.map +1 -1
- package/dist/connectionStateHandler.d.ts +19 -15
- package/dist/connectionStateHandler.d.ts.map +1 -1
- package/dist/connectionStateHandler.js +59 -59
- package/dist/connectionStateHandler.js.map +1 -1
- package/dist/container.d.ts +48 -38
- package/dist/container.d.ts.map +1 -1
- package/dist/container.js +447 -325
- package/dist/container.js.map +1 -1
- package/dist/containerContext.d.ts +22 -70
- package/dist/containerContext.d.ts.map +1 -1
- package/dist/containerContext.js +24 -221
- package/dist/containerContext.js.map +1 -1
- package/dist/containerStorageAdapter.d.ts +1 -1
- package/dist/containerStorageAdapter.d.ts.map +1 -1
- package/dist/containerStorageAdapter.js +47 -16
- package/dist/containerStorageAdapter.js.map +1 -1
- package/dist/contracts.d.ts +21 -10
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +3 -3
- package/dist/contracts.js.map +1 -1
- package/dist/debugLogger.d.ts +30 -0
- package/dist/debugLogger.d.ts.map +1 -0
- package/dist/debugLogger.js +95 -0
- package/dist/debugLogger.js.map +1 -0
- package/dist/deltaManager.d.ts +21 -9
- package/dist/deltaManager.d.ts.map +1 -1
- package/dist/deltaManager.js +114 -66
- package/dist/deltaManager.js.map +1 -1
- package/dist/deltaQueue.d.ts +1 -1
- package/dist/deltaQueue.d.ts.map +1 -1
- package/dist/deltaQueue.js +10 -10
- package/dist/deltaQueue.js.map +1 -1
- package/dist/disposal.d.ts +13 -0
- package/dist/disposal.d.ts.map +1 -0
- package/dist/disposal.js +25 -0
- package/dist/disposal.js.map +1 -0
- package/dist/error.d.ts +23 -0
- package/dist/error.d.ts.map +1 -0
- package/dist/error.js +32 -0
- package/dist/error.js.map +1 -0
- package/dist/loader.d.ts +23 -5
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +82 -51
- package/dist/loader.js.map +1 -1
- package/dist/noopHeuristic.d.ts +23 -0
- package/dist/noopHeuristic.d.ts.map +1 -0
- package/dist/noopHeuristic.js +90 -0
- package/dist/noopHeuristic.js.map +1 -0
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.js +1 -1
- package/dist/packageVersion.js.map +1 -1
- package/dist/protocol.d.ts +9 -12
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +26 -7
- package/dist/protocol.js.map +1 -1
- package/dist/protocolTreeDocumentStorageService.d.ts +1 -1
- package/dist/protocolTreeDocumentStorageService.d.ts.map +1 -1
- package/dist/protocolTreeDocumentStorageService.js.map +1 -1
- package/dist/quorum.d.ts +1 -14
- package/dist/quorum.d.ts.map +1 -1
- package/dist/quorum.js +1 -29
- package/dist/quorum.js.map +1 -1
- package/dist/retriableDocumentStorageService.d.ts +1 -1
- package/dist/retriableDocumentStorageService.d.ts.map +1 -1
- package/dist/retriableDocumentStorageService.js +4 -4
- package/dist/retriableDocumentStorageService.js.map +1 -1
- package/dist/utils.d.ts +8 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +30 -11
- package/dist/utils.js.map +1 -1
- package/lib/audience.d.ts +1 -0
- package/lib/audience.d.ts.map +1 -1
- package/lib/audience.js +4 -2
- package/lib/audience.js.map +1 -1
- package/lib/catchUpMonitor.d.ts +1 -1
- package/lib/catchUpMonitor.d.ts.map +1 -1
- package/lib/catchUpMonitor.js +1 -1
- package/lib/catchUpMonitor.js.map +1 -1
- package/lib/connectionManager.d.ts +6 -6
- package/lib/connectionManager.d.ts.map +1 -1
- package/lib/connectionManager.js +74 -67
- package/lib/connectionManager.js.map +1 -1
- package/lib/connectionStateHandler.d.ts +19 -15
- package/lib/connectionStateHandler.d.ts.map +1 -1
- package/lib/connectionStateHandler.js +36 -36
- package/lib/connectionStateHandler.js.map +1 -1
- package/lib/container.d.ts +48 -38
- package/lib/container.d.ts.map +1 -1
- package/lib/container.js +414 -292
- package/lib/container.js.map +1 -1
- package/lib/containerContext.d.ts +22 -70
- package/lib/containerContext.d.ts.map +1 -1
- package/lib/containerContext.js +24 -221
- package/lib/containerContext.js.map +1 -1
- package/lib/containerStorageAdapter.d.ts +1 -1
- package/lib/containerStorageAdapter.d.ts.map +1 -1
- package/lib/containerStorageAdapter.js +43 -12
- package/lib/containerStorageAdapter.js.map +1 -1
- package/lib/contracts.d.ts +21 -10
- package/lib/contracts.d.ts.map +1 -1
- package/lib/contracts.js +3 -3
- package/lib/contracts.js.map +1 -1
- package/lib/debugLogger.d.ts +30 -0
- package/lib/debugLogger.d.ts.map +1 -0
- package/lib/debugLogger.js +91 -0
- package/lib/debugLogger.js.map +1 -0
- package/lib/deltaManager.d.ts +21 -9
- package/lib/deltaManager.d.ts.map +1 -1
- package/lib/deltaManager.js +88 -37
- package/lib/deltaManager.js.map +1 -1
- package/lib/deltaQueue.d.ts +1 -1
- package/lib/deltaQueue.d.ts.map +1 -1
- package/lib/deltaQueue.js +3 -3
- package/lib/deltaQueue.js.map +1 -1
- package/lib/disposal.d.ts +13 -0
- package/lib/disposal.d.ts.map +1 -0
- package/lib/disposal.js +21 -0
- package/lib/disposal.js.map +1 -0
- package/lib/error.d.ts +23 -0
- package/lib/error.d.ts.map +1 -0
- package/lib/error.js +28 -0
- package/lib/error.js.map +1 -0
- package/lib/loader.d.ts +23 -5
- package/lib/loader.d.ts.map +1 -1
- package/lib/loader.js +82 -51
- package/lib/loader.js.map +1 -1
- package/lib/noopHeuristic.d.ts +23 -0
- package/lib/noopHeuristic.d.ts.map +1 -0
- package/lib/{collabWindowTracker.js → noopHeuristic.js} +31 -42
- package/lib/noopHeuristic.js.map +1 -0
- package/lib/packageVersion.d.ts +1 -1
- package/lib/packageVersion.js +1 -1
- package/lib/packageVersion.js.map +1 -1
- package/lib/protocol.d.ts +9 -12
- package/lib/protocol.d.ts.map +1 -1
- package/lib/protocol.js +24 -6
- package/lib/protocol.js.map +1 -1
- package/lib/protocolTreeDocumentStorageService.d.ts +1 -1
- package/lib/protocolTreeDocumentStorageService.d.ts.map +1 -1
- package/lib/protocolTreeDocumentStorageService.js.map +1 -1
- package/lib/quorum.d.ts +1 -14
- package/lib/quorum.d.ts.map +1 -1
- package/lib/quorum.js +0 -26
- package/lib/quorum.js.map +1 -1
- package/lib/retriableDocumentStorageService.d.ts +1 -1
- package/lib/retriableDocumentStorageService.d.ts.map +1 -1
- package/lib/retriableDocumentStorageService.js +2 -2
- package/lib/retriableDocumentStorageService.js.map +1 -1
- package/lib/utils.d.ts +8 -1
- package/lib/utils.d.ts.map +1 -1
- package/lib/utils.js +25 -7
- package/lib/utils.js.map +1 -1
- package/package.json +26 -28
- package/src/audience.ts +7 -1
- package/src/catchUpMonitor.ts +2 -2
- package/src/connectionManager.ts +76 -52
- package/src/connectionStateHandler.ts +46 -48
- package/src/container.ts +561 -326
- package/src/containerContext.ts +31 -349
- package/src/containerStorageAdapter.ts +49 -6
- package/src/contracts.ts +27 -13
- package/src/debugLogger.ts +113 -0
- package/src/deltaManager.ts +93 -36
- package/src/deltaQueue.ts +2 -1
- package/src/disposal.ts +25 -0
- package/src/error.ts +44 -0
- package/src/loader.ts +84 -36
- package/src/{collabWindowTracker.ts → noopHeuristic.ts} +38 -47
- package/src/packageVersion.ts +1 -1
- package/src/protocol.ts +26 -16
- package/src/protocolTreeDocumentStorageService.ts +1 -1
- package/src/quorum.ts +1 -40
- package/src/retriableDocumentStorageService.ts +3 -4
- package/src/utils.ts +33 -8
- package/dist/collabWindowTracker.d.ts +0 -19
- package/dist/collabWindowTracker.d.ts.map +0 -1
- package/dist/collabWindowTracker.js +0 -101
- package/dist/collabWindowTracker.js.map +0 -1
- package/dist/deltaManagerProxy.d.ts +0 -42
- package/dist/deltaManagerProxy.d.ts.map +0 -1
- package/dist/deltaManagerProxy.js +0 -79
- package/dist/deltaManagerProxy.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/deltaManagerProxy.d.ts +0 -42
- package/lib/deltaManagerProxy.d.ts.map +0 -1
- package/lib/deltaManagerProxy.js +0 -74
- package/lib/deltaManagerProxy.js.map +0 -1
- package/src/deltaManagerProxy.ts +0 -109
package/lib/container.js
CHANGED
|
@@ -5,30 +5,31 @@
|
|
|
5
5
|
// eslint-disable-next-line import/no-internal-modules
|
|
6
6
|
import merge from "lodash/merge";
|
|
7
7
|
import { v4 as uuid } from "uuid";
|
|
8
|
-
import { assert,
|
|
8
|
+
import { assert, unreachableCase } from "@fluidframework/core-utils";
|
|
9
|
+
import { TypedEventEmitter, performance } from "@fluid-internal/client-utils";
|
|
10
|
+
import { LogLevel, } from "@fluidframework/core-interfaces";
|
|
9
11
|
import { AttachState, isFluidCodeDetails, } from "@fluidframework/container-definitions";
|
|
10
|
-
import {
|
|
11
|
-
import { readAndParse, OnlineStatus, isOnline, combineAppAndProtocolSummary, runWithRetry, isCombinedAppAndProtocolSummary, } from "@fluidframework/driver-utils";
|
|
12
|
+
import { readAndParse, OnlineStatus, isOnline, runWithRetry, isCombinedAppAndProtocolSummary, MessageType2, } from "@fluidframework/driver-utils";
|
|
12
13
|
import { MessageType, SummaryType, } from "@fluidframework/protocol-definitions";
|
|
13
|
-
import {
|
|
14
|
+
import { createChildLogger, EventEmitterWithErrorHandling, PerformanceEvent, raiseConnectedEvent, connectedEventName, normalizeError, createChildMonitoringContext, wrapError, formatTick, GenericError, UsageError, } from "@fluidframework/telemetry-utils";
|
|
14
15
|
import { Audience } from "./audience";
|
|
15
16
|
import { ContainerContext } from "./containerContext";
|
|
16
|
-
import { ReconnectMode, getPackageName } from "./contracts";
|
|
17
|
+
import { ReconnectMode, getPackageName, } from "./contracts";
|
|
17
18
|
import { DeltaManager } from "./deltaManager";
|
|
18
|
-
import { DeltaManagerProxy } from "./deltaManagerProxy";
|
|
19
19
|
import { RelativeLoader } from "./loader";
|
|
20
20
|
import { pkgVersion } from "./packageVersion";
|
|
21
21
|
import { ContainerStorageAdapter, getBlobContentsFromTree, getBlobContentsFromTreeWithBlobContents, } from "./containerStorageAdapter";
|
|
22
22
|
import { createConnectionStateHandler } from "./connectionStateHandler";
|
|
23
|
-
import { getProtocolSnapshotTree, getSnapshotTreeFromSerializedContainer } from "./utils";
|
|
24
|
-
import { initQuorumValuesFromCodeDetails
|
|
25
|
-
import {
|
|
23
|
+
import { combineAppAndProtocolSummary, getProtocolSnapshotTree, getSnapshotTreeFromSerializedContainer, } from "./utils";
|
|
24
|
+
import { initQuorumValuesFromCodeDetails } from "./quorum";
|
|
25
|
+
import { NoopHeuristic } from "./noopHeuristic";
|
|
26
26
|
import { ConnectionManager } from "./connectionManager";
|
|
27
27
|
import { ConnectionState } from "./connectionState";
|
|
28
|
-
import { OnlyValidTermValue, ProtocolHandler, } from "./protocol";
|
|
28
|
+
import { OnlyValidTermValue, ProtocolHandler, protocolHandlerShouldProcessSignal, } from "./protocol";
|
|
29
29
|
const detachedContainerRefSeqNumber = 0;
|
|
30
30
|
const dirtyContainerEvent = "dirty";
|
|
31
31
|
const savedContainerEvent = "saved";
|
|
32
|
+
const packageNotFactoryError = "Code package does not implement IRuntimeFactory";
|
|
32
33
|
/**
|
|
33
34
|
* Waits until container connects to delta storage and gets up-to-date.
|
|
34
35
|
*
|
|
@@ -104,7 +105,7 @@ export async function waitContainerToCatchUp(container) {
|
|
|
104
105
|
}
|
|
105
106
|
const getCodeProposal =
|
|
106
107
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
107
|
-
(quorum) =>
|
|
108
|
+
(quorum) => quorum.get("code") ?? quorum.get("code2");
|
|
108
109
|
/**
|
|
109
110
|
* Helper function to report to telemetry cases where operation takes longer than expected (200ms)
|
|
110
111
|
* @param logger - logger to use
|
|
@@ -124,7 +125,6 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
124
125
|
* @internal
|
|
125
126
|
*/
|
|
126
127
|
constructor(createProps, loadProps) {
|
|
127
|
-
var _a;
|
|
128
128
|
super((name, error) => {
|
|
129
129
|
this.mc.logger.sendErrorEvent({
|
|
130
130
|
eventName: "ContainerEventHandlerException",
|
|
@@ -152,16 +152,31 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
152
152
|
this.inboundQueuePausedFromInit = true;
|
|
153
153
|
this.firstConnection = true;
|
|
154
154
|
this.connectionTransitionTimes = [];
|
|
155
|
-
this.messageCountAfterDisconnection = 0;
|
|
156
155
|
this.attachStarted = false;
|
|
157
156
|
this._dirtyContainer = false;
|
|
158
157
|
this.savedOps = [];
|
|
158
|
+
this.clientsWhoShouldHaveLeft = new Set();
|
|
159
159
|
this.setAutoReconnectTime = performance.now();
|
|
160
|
+
this._lifecycleEvents = new TypedEventEmitter();
|
|
160
161
|
this._disposed = false;
|
|
162
|
+
this.getAbsoluteUrl = async (relativeUrl) => {
|
|
163
|
+
if (this.resolvedUrl === undefined) {
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
return this.urlResolver.getAbsoluteUrl(this.resolvedUrl, relativeUrl, getPackageName(this._loadedCodeDetails));
|
|
167
|
+
};
|
|
168
|
+
this.updateDirtyContainerState = (dirty) => {
|
|
169
|
+
if (this._dirtyContainer === dirty) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
this._dirtyContainer = dirty;
|
|
173
|
+
this.emit(dirty ? dirtyContainerEvent : savedContainerEvent);
|
|
174
|
+
};
|
|
161
175
|
const { canReconnect, clientDetailsOverride, urlResolver, documentServiceFactory, codeLoader, options, scope, subLogger, detachedBlobStorage, protocolHandlerBuilder, } = createProps;
|
|
162
176
|
this.connectionTransitionTimes[ConnectionState.Disconnected] = performance.now();
|
|
163
|
-
const pendingLocalState = loadProps
|
|
164
|
-
this.
|
|
177
|
+
const pendingLocalState = loadProps?.pendingLocalState;
|
|
178
|
+
this._clientId = pendingLocalState?.clientId;
|
|
179
|
+
this._canReconnect = canReconnect ?? true;
|
|
165
180
|
this.clientDetailsOverride = clientDetailsOverride;
|
|
166
181
|
this.urlResolver = urlResolver;
|
|
167
182
|
this.serviceFactory = documentServiceFactory;
|
|
@@ -169,14 +184,18 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
169
184
|
// Warning: this is only a shallow clone. Mutation of any individual loader option will mutate it for
|
|
170
185
|
// all clients that were loaded from the same loader (including summarizer clients).
|
|
171
186
|
// Tracking alternative ways to handle this in AB#4129.
|
|
172
|
-
this.options =
|
|
187
|
+
this.options = { ...options };
|
|
173
188
|
this.scope = scope;
|
|
174
189
|
this.detachedBlobStorage = detachedBlobStorage;
|
|
175
190
|
this.protocolHandlerBuilder =
|
|
176
|
-
protocolHandlerBuilder
|
|
191
|
+
protocolHandlerBuilder ??
|
|
192
|
+
((attributes, quorumSnapshot, sendProposal) => new ProtocolHandler(attributes, quorumSnapshot, sendProposal, new Audience(), (clientId) => this.clientsWhoShouldHaveLeft.has(clientId)));
|
|
177
193
|
// Note that we capture the createProps here so we can replicate the creation call when we want to clone.
|
|
178
194
|
this.clone = async (_loadProps, createParamOverrides) => {
|
|
179
|
-
return Container.load(_loadProps,
|
|
195
|
+
return Container.load(_loadProps, {
|
|
196
|
+
...createProps,
|
|
197
|
+
...createParamOverrides,
|
|
198
|
+
});
|
|
180
199
|
};
|
|
181
200
|
// Create logger for data stores to use
|
|
182
201
|
const type = this.client.details.type;
|
|
@@ -184,45 +203,50 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
184
203
|
const clientType = `${interactive ? "interactive" : "noninteractive"}${type !== undefined && type !== "" ? `/${type}` : ""}`;
|
|
185
204
|
// Need to use the property getter for docId because for detached flow we don't have the docId initially.
|
|
186
205
|
// We assign the id later so property getter is used.
|
|
187
|
-
this.subLogger =
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
//
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
206
|
+
this.subLogger = createChildLogger({
|
|
207
|
+
logger: subLogger,
|
|
208
|
+
properties: {
|
|
209
|
+
all: {
|
|
210
|
+
clientType,
|
|
211
|
+
containerId: uuid(),
|
|
212
|
+
docId: () => this.resolvedUrl?.id,
|
|
213
|
+
containerAttachState: () => this._attachState,
|
|
214
|
+
containerLifecycleState: () => this._lifecycleState,
|
|
215
|
+
containerConnectionState: () => ConnectionState[this.connectionState],
|
|
216
|
+
serializedContainer: pendingLocalState !== undefined,
|
|
217
|
+
},
|
|
218
|
+
// we need to be judicious with our logging here to avoid generating too much data
|
|
219
|
+
// all data logged here should be broadly applicable, and not specific to a
|
|
220
|
+
// specific error or class of errors
|
|
221
|
+
error: {
|
|
222
|
+
// load information to associate errors with the specific load point
|
|
223
|
+
dmInitialSeqNumber: () => this._deltaManager?.initialSequenceNumber,
|
|
224
|
+
dmLastProcessedSeqNumber: () => this._deltaManager?.lastSequenceNumber,
|
|
225
|
+
dmLastKnownSeqNumber: () => this._deltaManager?.lastKnownSeqNumber,
|
|
226
|
+
containerLoadedFromVersionId: () => this._loadedFromVersion?.id,
|
|
227
|
+
containerLoadedFromVersionDate: () => this._loadedFromVersion?.date,
|
|
228
|
+
// message information to associate errors with the specific execution state
|
|
229
|
+
// dmLastMsqSeqNumber: if present, same as dmLastProcessedSeqNumber
|
|
230
|
+
dmLastMsqSeqNumber: () => this.deltaManager?.lastMessage?.sequenceNumber,
|
|
231
|
+
dmLastMsqSeqTimestamp: () => this.deltaManager?.lastMessage?.timestamp,
|
|
232
|
+
dmLastMsqSeqClientId: () => this.deltaManager?.lastMessage?.clientId === null
|
|
233
|
+
? "null"
|
|
234
|
+
: this.deltaManager?.lastMessage?.clientId,
|
|
235
|
+
dmLastMsgClientSeq: () => this.deltaManager?.lastMessage?.clientSequenceNumber,
|
|
236
|
+
connectionStateDuration: () => performance.now() - this.connectionTransitionTimes[this.connectionState],
|
|
237
|
+
},
|
|
214
238
|
},
|
|
215
239
|
});
|
|
216
240
|
// Prefix all events in this file with container-loader
|
|
217
|
-
this.mc =
|
|
241
|
+
this.mc = createChildMonitoringContext({ logger: this.subLogger, namespace: "Container" });
|
|
218
242
|
this._deltaManager = this.createDeltaManager();
|
|
219
243
|
this.connectionStateHandler = createConnectionStateHandler({
|
|
220
244
|
logger: this.mc.logger,
|
|
221
|
-
connectionStateChanged: (value, oldState, reason
|
|
245
|
+
connectionStateChanged: (value, oldState, reason) => {
|
|
222
246
|
if (value === ConnectionState.Connected) {
|
|
223
247
|
this._clientId = this.connectionStateHandler.pendingClientId;
|
|
224
248
|
}
|
|
225
|
-
this.logConnectionStateChangeTelemetry(value, oldState, reason
|
|
249
|
+
this.logConnectionStateChangeTelemetry(value, oldState, reason);
|
|
226
250
|
if (this._lifecycleState === "loaded") {
|
|
227
251
|
this.propagateConnectionState(false /* initial transition */, value === ConnectionState.Disconnected
|
|
228
252
|
? reason
|
|
@@ -238,9 +262,14 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
238
262
|
// Report issues only if we already loaded container - op processing is paused while container is loading,
|
|
239
263
|
// so we always time-out processing of join op in cases where fetching snapshot takes a minute.
|
|
240
264
|
// It's not a problem with op processing itself - such issues should be tracked as part of boot perf monitoring instead.
|
|
241
|
-
this._deltaManager.logConnectionIssue(
|
|
242
|
-
|
|
243
|
-
|
|
265
|
+
this._deltaManager.logConnectionIssue({
|
|
266
|
+
eventName,
|
|
267
|
+
mode,
|
|
268
|
+
category: this._lifecycleState === "loading" ? "generic" : category,
|
|
269
|
+
duration: performance.now() -
|
|
270
|
+
this.connectionTransitionTimes[ConnectionState.CatchingUp],
|
|
271
|
+
...(details === undefined ? {} : { details: JSON.stringify(details) }),
|
|
272
|
+
});
|
|
244
273
|
// If this is "write" connection, it took too long to receive join op. But in most cases that's due
|
|
245
274
|
// to very slow op fetches and we will eventually get there.
|
|
246
275
|
// For "read" connections, we get here due to self join signal not arriving on time. We will need to
|
|
@@ -250,11 +279,15 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
250
279
|
// Other possible recovery path - move to connected state (i.e. ConnectionStateHandler.joinOpTimer
|
|
251
280
|
// to call this.applyForConnectedState("addMemberEvent") for "read" connections)
|
|
252
281
|
if (mode === "read") {
|
|
253
|
-
|
|
254
|
-
this.
|
|
282
|
+
const reason = { text: "NoJoinSignal" };
|
|
283
|
+
this.disconnectInternal(reason);
|
|
284
|
+
this.connectInternal({ reason, fetchOpsFromStorage: false });
|
|
255
285
|
}
|
|
256
286
|
},
|
|
257
|
-
|
|
287
|
+
clientShouldHaveLeft: (clientId) => {
|
|
288
|
+
this.clientsWhoShouldHaveLeft.add(clientId);
|
|
289
|
+
},
|
|
290
|
+
}, this.deltaManager, pendingLocalState?.clientId);
|
|
258
291
|
this.on(savedContainerEvent, () => {
|
|
259
292
|
this.connectionStateHandler.containerSaved();
|
|
260
293
|
});
|
|
@@ -266,14 +299,15 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
266
299
|
: combineAppAndProtocolSummary(summaryTree, this.captureProtocolSummary());
|
|
267
300
|
// Whether the combined summary tree has been forced on by either the loader option or the monitoring context.
|
|
268
301
|
// Even if not forced on via this flag, combined summaries may still be enabled by service policy.
|
|
269
|
-
const forceEnableSummarizeProtocolTree =
|
|
270
|
-
|
|
302
|
+
const forceEnableSummarizeProtocolTree = this.mc.config.getBoolean("Fluid.Container.summarizeProtocolTree2") ??
|
|
303
|
+
options.summarizeProtocolTree;
|
|
304
|
+
this.storageAdapter = new ContainerStorageAdapter(detachedBlobStorage, this.mc.logger, pendingLocalState?.snapshotBlobs, addProtocolSummaryIfMissing, forceEnableSummarizeProtocolTree);
|
|
271
305
|
const isDomAvailable = typeof document === "object" &&
|
|
272
306
|
document !== null &&
|
|
273
307
|
typeof document.addEventListener === "function" &&
|
|
274
308
|
document.addEventListener !== null;
|
|
275
|
-
// keep track of last time page was visible for telemetry
|
|
276
|
-
if (isDomAvailable) {
|
|
309
|
+
// keep track of last time page was visible for telemetry (on interactive clients only)
|
|
310
|
+
if (isDomAvailable && interactive) {
|
|
277
311
|
this.lastVisible = document.hidden ? performance.now() : undefined;
|
|
278
312
|
this.visibilityEventHandler = () => {
|
|
279
313
|
if (document.hidden) {
|
|
@@ -294,7 +328,7 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
294
328
|
* @internal
|
|
295
329
|
*/
|
|
296
330
|
static async load(loadProps, createProps) {
|
|
297
|
-
const { version, pendingLocalState, loadMode, resolvedUrl } = loadProps;
|
|
331
|
+
const { version, pendingLocalState, loadMode, resolvedUrl, loadToSequenceNumber } = loadProps;
|
|
298
332
|
const container = new Container(createProps, loadProps);
|
|
299
333
|
const disableRecordHeapSize = container.mc.config.getBoolean("Fluid.Loader.DisableRecordHeapSize");
|
|
300
334
|
return PerformanceEvent.timedExecAsync(container.mc.logger, { eventName: "Load" }, async (event) => new Promise((resolve, reject) => {
|
|
@@ -302,26 +336,31 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
302
336
|
// if we have pendingLocalState, anything we cached is not useful and we shouldn't wait for connection
|
|
303
337
|
// to return container, so ignore this value and use undefined for opsBeforeReturn
|
|
304
338
|
const mode = pendingLocalState
|
|
305
|
-
?
|
|
339
|
+
? { ...(loadMode ?? defaultMode), opsBeforeReturn: undefined }
|
|
340
|
+
: loadMode ?? defaultMode;
|
|
306
341
|
const onClosed = (err) => {
|
|
307
342
|
// pre-0.58 error message: containerClosedWithoutErrorDuringLoad
|
|
308
|
-
reject(err
|
|
343
|
+
reject(err ?? new GenericError("Container closed without error during load"));
|
|
309
344
|
};
|
|
310
345
|
container.on("closed", onClosed);
|
|
311
346
|
container
|
|
312
|
-
.load(version, mode, resolvedUrl, pendingLocalState)
|
|
347
|
+
.load(version, mode, resolvedUrl, pendingLocalState, loadToSequenceNumber)
|
|
313
348
|
.finally(() => {
|
|
314
349
|
container.removeListener("closed", onClosed);
|
|
315
350
|
})
|
|
316
351
|
.then((props) => {
|
|
317
|
-
event.end(
|
|
352
|
+
event.end({ ...props, ...loadMode });
|
|
318
353
|
resolve(container);
|
|
319
354
|
}, (error) => {
|
|
320
355
|
const err = normalizeError(error);
|
|
321
356
|
// Depending where error happens, we can be attempting to connect to web socket
|
|
322
357
|
// and continuously retrying (consider offline mode)
|
|
323
358
|
// Host has no container to close, so it's prudent to do it here
|
|
359
|
+
// Note: We could only dispose the container instead of just close but that would
|
|
360
|
+
// the telemetry where users sometimes search for ContainerClose event to look
|
|
361
|
+
// for load failures. So not removing this at this time.
|
|
324
362
|
container.close(err);
|
|
363
|
+
container.dispose(err);
|
|
325
364
|
onClosed(err);
|
|
326
365
|
});
|
|
327
366
|
}), { start: true, end: true, cancel: "generic" }, disableRecordHeapSize !== true /* recordHeapSize */);
|
|
@@ -358,19 +397,16 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
358
397
|
}
|
|
359
398
|
}
|
|
360
399
|
get closed() {
|
|
361
|
-
return (this._lifecycleState === "closing" ||
|
|
362
|
-
this._lifecycleState === "closed" ||
|
|
363
|
-
this._lifecycleState === "disposing" ||
|
|
364
|
-
this._lifecycleState === "disposed");
|
|
400
|
+
return (this._lifecycleState === "closing" || this._lifecycleState === "closed" || this.disposed);
|
|
365
401
|
}
|
|
366
|
-
get
|
|
367
|
-
return this.
|
|
402
|
+
get disposed() {
|
|
403
|
+
return this._lifecycleState === "disposing" || this._lifecycleState === "disposed";
|
|
368
404
|
}
|
|
369
|
-
get
|
|
370
|
-
if (this.
|
|
371
|
-
throw new
|
|
405
|
+
get runtime() {
|
|
406
|
+
if (this._runtime === undefined) {
|
|
407
|
+
throw new Error("Attempted to access runtime before it was defined");
|
|
372
408
|
}
|
|
373
|
-
return this.
|
|
409
|
+
return this._runtime;
|
|
374
410
|
}
|
|
375
411
|
get protocolHandler() {
|
|
376
412
|
if (this._protocolHandler === undefined) {
|
|
@@ -385,7 +421,6 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
385
421
|
return this;
|
|
386
422
|
}
|
|
387
423
|
get resolvedUrl() {
|
|
388
|
-
var _a;
|
|
389
424
|
/**
|
|
390
425
|
* All attached containers will have a document service,
|
|
391
426
|
* this is required, as attached containers are attached to
|
|
@@ -397,17 +432,11 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
397
432
|
* is always the same as the containers, as we had to
|
|
398
433
|
* obtain the resolved url, and then create the service from it.
|
|
399
434
|
*/
|
|
400
|
-
return
|
|
401
|
-
}
|
|
402
|
-
get loadedFromVersion() {
|
|
403
|
-
return this._loadedFromVersion;
|
|
435
|
+
return this.service?.resolvedUrl;
|
|
404
436
|
}
|
|
405
437
|
get readOnlyInfo() {
|
|
406
438
|
return this._deltaManager.readOnlyInfo;
|
|
407
439
|
}
|
|
408
|
-
get closeSignal() {
|
|
409
|
-
return this._deltaManager.closeAbortController.signal;
|
|
410
|
-
}
|
|
411
440
|
/**
|
|
412
441
|
* Tracks host requiring read-only mode.
|
|
413
442
|
*/
|
|
@@ -423,13 +452,6 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
423
452
|
get connected() {
|
|
424
453
|
return this.connectionStateHandler.connectionState === ConnectionState.Connected;
|
|
425
454
|
}
|
|
426
|
-
/**
|
|
427
|
-
* Service configuration details. If running in offline mode will be undefined otherwise will contain service
|
|
428
|
-
* configuration details returned as part of the initial connection.
|
|
429
|
-
*/
|
|
430
|
-
get serviceConfiguration() {
|
|
431
|
-
return this._deltaManager.serviceConfiguration;
|
|
432
|
-
}
|
|
433
455
|
/**
|
|
434
456
|
* The server provided id of the client.
|
|
435
457
|
* Set once this.connected is true, otherwise undefined
|
|
@@ -437,21 +459,11 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
437
459
|
get clientId() {
|
|
438
460
|
return this._clientId;
|
|
439
461
|
}
|
|
440
|
-
/**
|
|
441
|
-
* The server provided claims of the client.
|
|
442
|
-
* Set once this.connected is true, otherwise undefined
|
|
443
|
-
*/
|
|
444
|
-
get scopes() {
|
|
445
|
-
return this._deltaManager.connectionManager.scopes;
|
|
446
|
-
}
|
|
447
|
-
get clientDetails() {
|
|
448
|
-
return this._deltaManager.clientDetails;
|
|
449
|
-
}
|
|
450
462
|
get offlineLoadEnabled() {
|
|
451
|
-
|
|
452
|
-
|
|
463
|
+
const enabled = this.mc.config.getBoolean("Fluid.Container.enableOfflineLoad") ??
|
|
464
|
+
this.options?.enableOfflineLoad === true;
|
|
453
465
|
// summarizer will not have any pending state we want to save
|
|
454
|
-
return enabled && this.clientDetails.capabilities.interactive;
|
|
466
|
+
return enabled && this.deltaManager.clientDetails.capabilities.interactive;
|
|
455
467
|
}
|
|
456
468
|
/**
|
|
457
469
|
* Get the code details that are currently specified for the container.
|
|
@@ -466,8 +478,7 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
466
478
|
* loaded.
|
|
467
479
|
*/
|
|
468
480
|
getLoadedCodeDetails() {
|
|
469
|
-
|
|
470
|
-
return (_a = this._context) === null || _a === void 0 ? void 0 : _a.codeDetails;
|
|
481
|
+
return this._loadedCodeDetails;
|
|
471
482
|
}
|
|
472
483
|
/**
|
|
473
484
|
* Retrieves the audience associated with the document
|
|
@@ -487,33 +498,25 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
487
498
|
* {@inheritDoc @fluidframework/container-definitions#IContainer.entryPoint}
|
|
488
499
|
*/
|
|
489
500
|
async getEntryPoint() {
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
// allow it since they mean a kind of read-only state for the Container.
|
|
493
|
-
// Note that all 4 are lifecycle states but only 'closed' and 'disposed' are emitted as events.
|
|
494
|
-
if (this._lifecycleState === "disposing" || this._lifecycleState === "disposed") {
|
|
495
|
-
throw new UsageError("The container is disposing or disposed");
|
|
501
|
+
if (this._disposed) {
|
|
502
|
+
throw new UsageError("The context is already disposed");
|
|
496
503
|
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
const contextChangedHandler = () => {
|
|
500
|
-
resolve();
|
|
501
|
-
this.off("disposed", disposedHandler);
|
|
502
|
-
};
|
|
503
|
-
const disposedHandler = (error) => {
|
|
504
|
-
reject(error !== null && error !== void 0 ? error : "The Container is disposed");
|
|
505
|
-
this.off("contextChanged", contextChangedHandler);
|
|
506
|
-
};
|
|
507
|
-
this.once("contextChanged", contextChangedHandler);
|
|
508
|
-
this.once("disposed", disposedHandler);
|
|
509
|
-
});
|
|
510
|
-
// The Promise above should only resolve (vs reject) if the 'contextChanged' event was emitted and that
|
|
511
|
-
// should have set this._context; making sure.
|
|
512
|
-
assert(this._context !== undefined, 0x5a2 /* Context still not defined after contextChanged event */);
|
|
504
|
+
if (this._runtime !== undefined) {
|
|
505
|
+
return this._runtime.getEntryPoint?.();
|
|
513
506
|
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
507
|
+
return new Promise((resolve, reject) => {
|
|
508
|
+
const runtimeInstantiatedHandler = () => {
|
|
509
|
+
assert(this._runtime !== undefined, 0x5a3 /* runtimeInstantiated fired but runtime is still undefined */);
|
|
510
|
+
resolve(this._runtime.getEntryPoint?.());
|
|
511
|
+
this._lifecycleEvents.off("disposed", disposedHandler);
|
|
512
|
+
};
|
|
513
|
+
const disposedHandler = () => {
|
|
514
|
+
reject(new Error("ContainerContext was disposed"));
|
|
515
|
+
this._lifecycleEvents.off("runtimeInstantiated", runtimeInstantiatedHandler);
|
|
516
|
+
};
|
|
517
|
+
this._lifecycleEvents.once("runtimeInstantiated", runtimeInstantiatedHandler);
|
|
518
|
+
this._lifecycleEvents.once("disposed", disposedHandler);
|
|
519
|
+
});
|
|
517
520
|
}
|
|
518
521
|
/**
|
|
519
522
|
* Retrieves the quorum associated with the document
|
|
@@ -538,7 +541,6 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
538
541
|
assert(this._lifecycleState === "closed" || this._lifecycleState === "disposed", 0x314 /* Container properly closed */);
|
|
539
542
|
}
|
|
540
543
|
closeCore(error) {
|
|
541
|
-
var _a;
|
|
542
544
|
assert(!this.closed, 0x315 /* re-entrancy */);
|
|
543
545
|
try {
|
|
544
546
|
// Ensure that we raise all key events even if one of these throws
|
|
@@ -554,7 +556,7 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
554
556
|
: "generic",
|
|
555
557
|
}, error);
|
|
556
558
|
this._lifecycleState = "closing";
|
|
557
|
-
|
|
559
|
+
this._protocolHandler?.close();
|
|
558
560
|
this.connectionStateHandler.dispose();
|
|
559
561
|
}
|
|
560
562
|
catch (exception) {
|
|
@@ -567,10 +569,13 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
567
569
|
}
|
|
568
570
|
finally {
|
|
569
571
|
this._lifecycleState = "closed";
|
|
572
|
+
// There is no user for summarizer, so we need to ensure dispose is called
|
|
573
|
+
if (this.client.details.type === summarizerClientType) {
|
|
574
|
+
this.dispose(error);
|
|
575
|
+
}
|
|
570
576
|
}
|
|
571
577
|
}
|
|
572
578
|
disposeCore(error) {
|
|
573
|
-
var _a, _b, _c;
|
|
574
579
|
assert(!this._disposed, 0x54c /* Container already disposed */);
|
|
575
580
|
this._disposed = true;
|
|
576
581
|
try {
|
|
@@ -580,20 +585,22 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
580
585
|
// This gives us a chance to know what errors happened on open vs. on fully loaded container.
|
|
581
586
|
this.mc.logger.sendTelemetryEvent({
|
|
582
587
|
eventName: "ContainerDispose",
|
|
583
|
-
|
|
588
|
+
// Only log error if container isn't closed
|
|
589
|
+
category: !this.closed && error !== undefined ? "error" : "generic",
|
|
584
590
|
}, error);
|
|
585
591
|
// ! Progressing from "closed" to "disposing" is not allowed
|
|
586
592
|
if (this._lifecycleState !== "closed") {
|
|
587
593
|
this._lifecycleState = "disposing";
|
|
588
594
|
}
|
|
589
|
-
|
|
595
|
+
this._protocolHandler?.close();
|
|
590
596
|
this.connectionStateHandler.dispose();
|
|
591
|
-
|
|
597
|
+
const maybeError = error !== undefined ? new Error(error.message) : undefined;
|
|
598
|
+
this._runtime?.dispose(maybeError);
|
|
592
599
|
this.storageAdapter.dispose();
|
|
593
600
|
// Notify storage about critical errors. They may be due to disconnect between client & server knowledge
|
|
594
601
|
// about file, like file being overwritten in storage, but client having stale local cache.
|
|
595
602
|
// Driver need to ensure all caches are cleared on critical errors
|
|
596
|
-
|
|
603
|
+
this.service?.dispose(error);
|
|
597
604
|
}
|
|
598
605
|
catch (exception) {
|
|
599
606
|
this.mc.logger.sendErrorEvent({ eventName: "ContainerDisposeException" }, exception);
|
|
@@ -606,42 +613,58 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
606
613
|
}
|
|
607
614
|
finally {
|
|
608
615
|
this._lifecycleState = "disposed";
|
|
616
|
+
this._lifecycleEvents.emit("disposed");
|
|
609
617
|
}
|
|
610
618
|
}
|
|
611
|
-
closeAndGetPendingLocalState() {
|
|
619
|
+
async closeAndGetPendingLocalState() {
|
|
612
620
|
// runtime matches pending ops to successful ones by clientId and client seq num, so we need to close the
|
|
613
621
|
// container at the same time we get pending state, otherwise this container could reconnect and resubmit with
|
|
614
622
|
// a new clientId and a future container using stale pending state without the new clientId would resubmit them
|
|
615
|
-
|
|
623
|
+
this.disconnectInternal({ text: "closeAndGetPendingLocalState" }); // TODO https://dev.azure.com/fluidframework/internal/_workitems/edit/5127
|
|
624
|
+
const pendingState = await this.getPendingLocalStateCore({ notifyImminentClosure: true });
|
|
616
625
|
this.close();
|
|
617
626
|
return pendingState;
|
|
618
627
|
}
|
|
619
|
-
getPendingLocalState() {
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
const pendingState = {
|
|
628
|
-
pendingRuntimeState: this.context.getPendingLocalState(),
|
|
629
|
-
baseSnapshot: this.baseSnapshot,
|
|
630
|
-
snapshotBlobs: this.baseSnapshotBlobs,
|
|
631
|
-
savedOps: this.savedOps,
|
|
632
|
-
url: this.resolvedUrl.url,
|
|
633
|
-
term: OnlyValidTermValue,
|
|
628
|
+
async getPendingLocalState() {
|
|
629
|
+
return this.getPendingLocalStateCore({ notifyImminentClosure: false });
|
|
630
|
+
}
|
|
631
|
+
async getPendingLocalStateCore(props) {
|
|
632
|
+
return PerformanceEvent.timedExecAsync(this.mc.logger, {
|
|
633
|
+
eventName: "getPendingLocalState",
|
|
634
|
+
notifyImminentClosure: props.notifyImminentClosure,
|
|
635
|
+
savedOpsSize: this.savedOps.length,
|
|
634
636
|
clientId: this.clientId,
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
|
|
637
|
+
}, async () => {
|
|
638
|
+
if (!this.offlineLoadEnabled) {
|
|
639
|
+
throw new UsageError("Can't get pending local state unless offline load is enabled");
|
|
640
|
+
}
|
|
641
|
+
if (this.closed || this._disposed) {
|
|
642
|
+
throw new UsageError("Pending state cannot be retried if the container is closed or disposed");
|
|
643
|
+
}
|
|
644
|
+
assert(this.attachState === AttachState.Attached, 0x0d1 /* "Container should be attached before close" */);
|
|
645
|
+
assert(this.resolvedUrl !== undefined && this.resolvedUrl.type === "fluid", 0x0d2 /* "resolved url should be valid Fluid url" */);
|
|
646
|
+
assert(!!this.baseSnapshot, 0x5d4 /* no base snapshot */);
|
|
647
|
+
assert(!!this.baseSnapshotBlobs, 0x5d5 /* no snapshot blobs */);
|
|
648
|
+
const pendingRuntimeState = await this.runtime.getPendingLocalState(props);
|
|
649
|
+
const pendingState = {
|
|
650
|
+
pendingRuntimeState,
|
|
651
|
+
baseSnapshot: this.baseSnapshot,
|
|
652
|
+
snapshotBlobs: this.baseSnapshotBlobs,
|
|
653
|
+
savedOps: this.savedOps,
|
|
654
|
+
url: this.resolvedUrl.url,
|
|
655
|
+
term: OnlyValidTermValue,
|
|
656
|
+
// no need to save this if there is no pending runtime state
|
|
657
|
+
clientId: pendingRuntimeState !== undefined ? this.clientId : undefined,
|
|
658
|
+
};
|
|
659
|
+
return JSON.stringify(pendingState);
|
|
660
|
+
});
|
|
638
661
|
}
|
|
639
662
|
get attachState() {
|
|
640
663
|
return this._attachState;
|
|
641
664
|
}
|
|
642
665
|
serialize() {
|
|
643
666
|
assert(this.attachState === AttachState.Detached, 0x0d3 /* "Should only be called in detached container" */);
|
|
644
|
-
const appSummary = this.
|
|
667
|
+
const appSummary = this.runtime.createSummary();
|
|
645
668
|
const protocolSummary = this.captureProtocolSummary();
|
|
646
669
|
const combinedSummary = combineAppAndProtocolSummary(appSummary, protocolSummary);
|
|
647
670
|
if (this.detachedBlobStorage && this.detachedBlobStorage.size > 0) {
|
|
@@ -652,9 +675,8 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
652
675
|
}
|
|
653
676
|
return JSON.stringify(combinedSummary);
|
|
654
677
|
}
|
|
655
|
-
async attach(request) {
|
|
678
|
+
async attach(request, attachProps) {
|
|
656
679
|
await PerformanceEvent.timedExecAsync(this.mc.logger, { eventName: "Attach" }, async () => {
|
|
657
|
-
var _a;
|
|
658
680
|
if (this._lifecycleState !== "loaded") {
|
|
659
681
|
// pre-0.58 error message: containerNotValidForAttach
|
|
660
682
|
throw new UsageError(`The Container is not in a valid state for attach [${this._lifecycleState}]`);
|
|
@@ -670,7 +692,7 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
670
692
|
if (!hasAttachmentBlobs) {
|
|
671
693
|
// Get the document state post attach - possibly can just call attach but we need to change the
|
|
672
694
|
// semantics around what the attach means as far as async code goes.
|
|
673
|
-
const appSummary = this.
|
|
695
|
+
const appSummary = this.runtime.createSummary();
|
|
674
696
|
const protocolSummary = this.captureProtocolSummary();
|
|
675
697
|
summary = combineAppAndProtocolSummary(appSummary, protocolSummary);
|
|
676
698
|
// Set the state as attaching as we are starting the process of attaching container.
|
|
@@ -678,6 +700,7 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
678
700
|
// starting to attach the container to storage.
|
|
679
701
|
// Also, this should only be fired in detached container.
|
|
680
702
|
this._attachState = AttachState.Attaching;
|
|
703
|
+
this.runtime.setAttachState(AttachState.Attaching);
|
|
681
704
|
this.emit("attaching");
|
|
682
705
|
if (this.offlineLoadEnabled) {
|
|
683
706
|
const snapshot = getSnapshotTreeFromSerializedContainer(summary);
|
|
@@ -692,7 +715,7 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
692
715
|
assert(this.client.details.type !== summarizerClientType &&
|
|
693
716
|
createNewResolvedUrl !== undefined, 0x2c4 /* "client should not be summarizer before container is created" */);
|
|
694
717
|
this.service = await runWithRetry(async () => this.serviceFactory.createContainer(summary, createNewResolvedUrl, this.subLogger, false), "containerAttach", this.mc.logger, {
|
|
695
|
-
cancel: this.
|
|
718
|
+
cancel: this._deltaManager.closeAbortController.signal,
|
|
696
719
|
});
|
|
697
720
|
}
|
|
698
721
|
await this.storageAdapter.connectToService(this.service);
|
|
@@ -714,10 +737,11 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
714
737
|
}
|
|
715
738
|
}
|
|
716
739
|
// take summary and upload
|
|
717
|
-
const appSummary = this.
|
|
740
|
+
const appSummary = this.runtime.createSummary(redirectTable);
|
|
718
741
|
const protocolSummary = this.captureProtocolSummary();
|
|
719
742
|
summary = combineAppAndProtocolSummary(appSummary, protocolSummary);
|
|
720
743
|
this._attachState = AttachState.Attaching;
|
|
744
|
+
this.runtime.setAttachState(AttachState.Attaching);
|
|
721
745
|
this.emit("attaching");
|
|
722
746
|
if (this.offlineLoadEnabled) {
|
|
723
747
|
const snapshot = getSnapshotTreeFromSerializedContainer(summary);
|
|
@@ -732,27 +756,28 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
732
756
|
});
|
|
733
757
|
}
|
|
734
758
|
this._attachState = AttachState.Attached;
|
|
759
|
+
this.runtime.setAttachState(AttachState.Attached);
|
|
735
760
|
this.emit("attached");
|
|
736
761
|
if (!this.closed) {
|
|
737
|
-
this.
|
|
762
|
+
this.handleDeltaConnectionArg({
|
|
738
763
|
fetchOpsFromStorage: false,
|
|
739
|
-
reason: "createDetached",
|
|
740
|
-
});
|
|
764
|
+
reason: { text: "createDetached" },
|
|
765
|
+
}, attachProps?.deltaConnection);
|
|
741
766
|
}
|
|
742
767
|
}
|
|
743
768
|
catch (error) {
|
|
744
769
|
// add resolved URL on error object so that host has the ability to find this document and delete it
|
|
745
770
|
const newError = normalizeError(error);
|
|
746
|
-
newError.addTelemetryProperties({ resolvedUrl:
|
|
771
|
+
newError.addTelemetryProperties({ resolvedUrl: this.resolvedUrl?.url });
|
|
747
772
|
this.close(newError);
|
|
748
773
|
throw newError;
|
|
749
774
|
}
|
|
750
775
|
}, { start: true, end: true, cancel: "generic" });
|
|
751
776
|
}
|
|
752
777
|
async request(path) {
|
|
753
|
-
return PerformanceEvent.timedExecAsync(this.mc.logger, { eventName: "Request" }, async () => this.
|
|
778
|
+
return PerformanceEvent.timedExecAsync(this.mc.logger, { eventName: "Request" }, async () => this.runtime.request(path), { end: true, cancel: "error" });
|
|
754
779
|
}
|
|
755
|
-
setAutoReconnectInternal(mode) {
|
|
780
|
+
setAutoReconnectInternal(mode, reason) {
|
|
756
781
|
const currentMode = this._deltaManager.connectionManager.reconnectMode;
|
|
757
782
|
if (currentMode === mode) {
|
|
758
783
|
return;
|
|
@@ -766,7 +791,7 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
766
791
|
connectionState: ConnectionState[this.connectionState],
|
|
767
792
|
duration,
|
|
768
793
|
});
|
|
769
|
-
this._deltaManager.connectionManager.setAutoReconnect(mode);
|
|
794
|
+
this._deltaManager.connectionManager.setAutoReconnect(mode, reason);
|
|
770
795
|
}
|
|
771
796
|
connect() {
|
|
772
797
|
if (this.closed) {
|
|
@@ -779,7 +804,10 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
779
804
|
// Note: no need to fetch ops as we do it preemptively as part of DeltaManager.attachOpHandler().
|
|
780
805
|
// If there is gap, we will learn about it once connected, but the gap should be small (if any),
|
|
781
806
|
// assuming that connect() is called quickly after initial container boot.
|
|
782
|
-
this.connectInternal({
|
|
807
|
+
this.connectInternal({
|
|
808
|
+
reason: { text: "DocumentConnect" },
|
|
809
|
+
fetchOpsFromStorage: false,
|
|
810
|
+
});
|
|
783
811
|
}
|
|
784
812
|
}
|
|
785
813
|
connectInternal(args) {
|
|
@@ -789,21 +817,21 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
789
817
|
this.resumeInternal(args);
|
|
790
818
|
// Set Auto Reconnect Mode
|
|
791
819
|
const mode = ReconnectMode.Enabled;
|
|
792
|
-
this.setAutoReconnectInternal(mode);
|
|
820
|
+
this.setAutoReconnectInternal(mode, args.reason);
|
|
793
821
|
}
|
|
794
822
|
disconnect() {
|
|
795
823
|
if (this.closed) {
|
|
796
824
|
throw new UsageError(`The Container is closed and cannot be disconnected`);
|
|
797
825
|
}
|
|
798
826
|
else {
|
|
799
|
-
this.disconnectInternal();
|
|
827
|
+
this.disconnectInternal({ text: "DocumentDisconnect" });
|
|
800
828
|
}
|
|
801
829
|
}
|
|
802
|
-
disconnectInternal() {
|
|
830
|
+
disconnectInternal(reason) {
|
|
803
831
|
assert(!this.closed, 0x2c7 /* "Attempting to disconnect() a closed Container" */);
|
|
804
832
|
// Set Auto Reconnect Mode
|
|
805
833
|
const mode = ReconnectMode.Disabled;
|
|
806
|
-
this.setAutoReconnectInternal(mode);
|
|
834
|
+
this.setAutoReconnectInternal(mode, reason);
|
|
807
835
|
}
|
|
808
836
|
resumeInternal(args) {
|
|
809
837
|
assert(!this.closed, 0x0d9 /* "Attempting to connect() a closed DeltaManager" */);
|
|
@@ -816,13 +844,6 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
816
844
|
// Ensure connection to web socket
|
|
817
845
|
this.connectToDeltaStream(args);
|
|
818
846
|
}
|
|
819
|
-
async getAbsoluteUrl(relativeUrl) {
|
|
820
|
-
var _a;
|
|
821
|
-
if (this.resolvedUrl === undefined) {
|
|
822
|
-
return undefined;
|
|
823
|
-
}
|
|
824
|
-
return this.urlResolver.getAbsoluteUrl(this.resolvedUrl, relativeUrl, getPackageName((_a = this._context) === null || _a === void 0 ? void 0 : _a.codeDetails));
|
|
825
|
-
}
|
|
826
847
|
async proposeCodeDetails(codeDetails) {
|
|
827
848
|
if (!isFluidCodeDetails(codeDetails)) {
|
|
828
849
|
throw new Error("Provided codeDetails are not IFluidCodeDetails");
|
|
@@ -844,7 +865,7 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
844
865
|
this.deltaManager.inbound.pause(),
|
|
845
866
|
this.deltaManager.inboundSignal.pause(),
|
|
846
867
|
]);
|
|
847
|
-
if ((await this.
|
|
868
|
+
if ((await this.satisfies(codeDetails)) === true) {
|
|
848
869
|
this.deltaManager.inbound.resume();
|
|
849
870
|
this.deltaManager.inboundSignal.resume();
|
|
850
871
|
return;
|
|
@@ -853,6 +874,37 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
853
874
|
const error = new GenericError("Existing context does not satisfy incoming proposal");
|
|
854
875
|
this.close(error);
|
|
855
876
|
}
|
|
877
|
+
/**
|
|
878
|
+
* Determines if the currently loaded module satisfies the incoming constraint code details
|
|
879
|
+
*/
|
|
880
|
+
async satisfies(constraintCodeDetails) {
|
|
881
|
+
// If we have no module, it can't satisfy anything.
|
|
882
|
+
if (this._loadedModule === undefined) {
|
|
883
|
+
return false;
|
|
884
|
+
}
|
|
885
|
+
const comparers = [];
|
|
886
|
+
const maybeCompareCodeLoader = this.codeLoader;
|
|
887
|
+
if (maybeCompareCodeLoader.IFluidCodeDetailsComparer !== undefined) {
|
|
888
|
+
comparers.push(maybeCompareCodeLoader.IFluidCodeDetailsComparer);
|
|
889
|
+
}
|
|
890
|
+
const maybeCompareExport = this._loadedModule?.module.fluidExport;
|
|
891
|
+
if (maybeCompareExport?.IFluidCodeDetailsComparer !== undefined) {
|
|
892
|
+
comparers.push(maybeCompareExport.IFluidCodeDetailsComparer);
|
|
893
|
+
}
|
|
894
|
+
// If there are no comparers, then it's impossible to know if the currently loaded package satisfies
|
|
895
|
+
// the incoming constraint, so we return false. Assuming it does not satisfy is safer, to force a reload
|
|
896
|
+
// rather than potentially running with incompatible code.
|
|
897
|
+
if (comparers.length === 0) {
|
|
898
|
+
return false;
|
|
899
|
+
}
|
|
900
|
+
for (const comparer of comparers) {
|
|
901
|
+
const satisfies = await comparer.satisfies(this._loadedModule?.details, constraintCodeDetails);
|
|
902
|
+
if (satisfies === false) {
|
|
903
|
+
return false;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
return true;
|
|
907
|
+
}
|
|
856
908
|
async getVersion(version) {
|
|
857
909
|
const versions = await this.storageAdapter.getVersions(version, 1);
|
|
858
910
|
return versions[0];
|
|
@@ -869,8 +921,8 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
869
921
|
*
|
|
870
922
|
* @param specifiedVersion - Version SHA to load snapshot. If not specified, will fetch the latest snapshot.
|
|
871
923
|
*/
|
|
872
|
-
async load(specifiedVersion, loadMode, resolvedUrl, pendingLocalState) {
|
|
873
|
-
|
|
924
|
+
async load(specifiedVersion, loadMode, resolvedUrl, pendingLocalState, loadToSequenceNumber) {
|
|
925
|
+
const timings = { phase1: performance.now() };
|
|
874
926
|
this.service = await this.serviceFactory.createDocumentService(resolvedUrl, this.subLogger, this.client.details.type === summarizerClientType);
|
|
875
927
|
// Ideally we always connect as "read" by default.
|
|
876
928
|
// Currently that works with SPO & r11s, because we get "write" connection when connecting to non-existing file.
|
|
@@ -882,7 +934,7 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
882
934
|
// A) creation flow breaks (as one of the clients "sees" file as existing, and hits #2 above)
|
|
883
935
|
// B) Once file is created, transition from view-only connection to write does not work - some bugs to be fixed.
|
|
884
936
|
const connectionArgs = {
|
|
885
|
-
reason: "DocumentOpen",
|
|
937
|
+
reason: { text: "DocumentOpen" },
|
|
886
938
|
mode: "write",
|
|
887
939
|
fetchOpsFromStorage: false,
|
|
888
940
|
};
|
|
@@ -901,6 +953,7 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
901
953
|
});
|
|
902
954
|
}
|
|
903
955
|
this._attachState = AttachState.Attached;
|
|
956
|
+
timings.phase2 = performance.now();
|
|
904
957
|
// Fetch specified snapshot.
|
|
905
958
|
const { snapshot, versionId } = pendingLocalState === undefined
|
|
906
959
|
? await this.fetchSnapshotTree(specifiedVersion)
|
|
@@ -914,14 +967,56 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
914
967
|
if (this.offlineLoadEnabled) {
|
|
915
968
|
this.baseSnapshot = snapshot;
|
|
916
969
|
// Save contents of snapshot now, otherwise closeAndGetPendingLocalState() must be async
|
|
917
|
-
this.baseSnapshotBlobs = await getBlobContentsFromTree(snapshot, this.
|
|
970
|
+
this.baseSnapshotBlobs = await getBlobContentsFromTree(snapshot, this.storageAdapter);
|
|
918
971
|
}
|
|
919
972
|
}
|
|
920
973
|
const attributes = await this.getDocumentAttributes(this.storageAdapter, snapshot);
|
|
921
974
|
// If we saved ops, we will replay them and don't need DeltaManager to fetch them
|
|
922
|
-
const sequenceNumber =
|
|
923
|
-
const dmAttributes = sequenceNumber !== undefined ?
|
|
975
|
+
const sequenceNumber = pendingLocalState?.savedOps[pendingLocalState.savedOps.length - 1]?.sequenceNumber;
|
|
976
|
+
const dmAttributes = sequenceNumber !== undefined ? { ...attributes, sequenceNumber } : attributes;
|
|
924
977
|
let opsBeforeReturnP;
|
|
978
|
+
if (loadMode.pauseAfterLoad === true) {
|
|
979
|
+
// If we are trying to pause at a specific sequence number, ensure the latest snapshot is not newer than the desired sequence number.
|
|
980
|
+
if (loadMode.opsBeforeReturn === "sequenceNumber") {
|
|
981
|
+
assert(loadToSequenceNumber !== undefined, 0x727 /* sequenceNumber should be defined */);
|
|
982
|
+
// Note: It is possible that we think the latest snapshot is newer than the specified sequence number
|
|
983
|
+
// due to saved ops that may be replayed after the snapshot.
|
|
984
|
+
// https://dev.azure.com/fluidframework/internal/_workitems/edit/5055
|
|
985
|
+
if (dmAttributes.sequenceNumber > loadToSequenceNumber) {
|
|
986
|
+
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.");
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
// Force readonly mode - this will ensure we don't receive an error for the lack of join op
|
|
990
|
+
this.forceReadonly(true);
|
|
991
|
+
// We need to setup a listener to stop op processing once we reach the desired sequence number (if specified).
|
|
992
|
+
const opHandler = () => {
|
|
993
|
+
if (loadToSequenceNumber === undefined) {
|
|
994
|
+
// If there is no specified sequence number, pause after the inbound queue is empty.
|
|
995
|
+
if (this.deltaManager.inbound.length !== 0) {
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
else {
|
|
1000
|
+
// If there is a specified sequence number, keep processing until we reach it.
|
|
1001
|
+
if (this.deltaManager.lastSequenceNumber < loadToSequenceNumber) {
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
// Pause op processing once we have processed the desired number of ops.
|
|
1006
|
+
void this.deltaManager.inbound.pause();
|
|
1007
|
+
void this.deltaManager.outbound.pause();
|
|
1008
|
+
this.off("op", opHandler);
|
|
1009
|
+
};
|
|
1010
|
+
if ((loadToSequenceNumber === undefined && this.deltaManager.inbound.length === 0) ||
|
|
1011
|
+
this.deltaManager.lastSequenceNumber === loadToSequenceNumber) {
|
|
1012
|
+
// If we have already reached the desired sequence number, call opHandler() to pause immediately.
|
|
1013
|
+
opHandler();
|
|
1014
|
+
}
|
|
1015
|
+
else {
|
|
1016
|
+
// If we have not yet reached the desired sequence number, setup a listener to pause once we reach it.
|
|
1017
|
+
this.on("op", opHandler);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
925
1020
|
// Attach op handlers to finish initialization and be able to start processing ops
|
|
926
1021
|
// Kick off any ops fetching if required.
|
|
927
1022
|
switch (loadMode.opsBeforeReturn) {
|
|
@@ -930,11 +1025,10 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
930
1025
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
931
1026
|
this.attachDeltaManagerOpHandler(dmAttributes, loadMode.deltaConnection !== "none" ? "all" : "none");
|
|
932
1027
|
break;
|
|
1028
|
+
case "sequenceNumber":
|
|
933
1029
|
case "cached":
|
|
934
|
-
opsBeforeReturnP = this.attachDeltaManagerOpHandler(dmAttributes, "cached");
|
|
935
|
-
break;
|
|
936
1030
|
case "all":
|
|
937
|
-
opsBeforeReturnP = this.attachDeltaManagerOpHandler(dmAttributes,
|
|
1031
|
+
opsBeforeReturnP = this.attachDeltaManagerOpHandler(dmAttributes, loadMode.opsBeforeReturn);
|
|
938
1032
|
break;
|
|
939
1033
|
default:
|
|
940
1034
|
unreachableCase(loadMode.opsBeforeReturn);
|
|
@@ -942,20 +1036,19 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
942
1036
|
// ...load in the existing quorum
|
|
943
1037
|
// Initialize the protocol handler
|
|
944
1038
|
await this.initializeProtocolStateFromSnapshot(attributes, this.storageAdapter, snapshot);
|
|
1039
|
+
timings.phase3 = performance.now();
|
|
945
1040
|
const codeDetails = this.getCodeDetailsFromQuorum();
|
|
946
|
-
await this.
|
|
947
|
-
|
|
1041
|
+
await this.instantiateRuntime(codeDetails, snapshot,
|
|
1042
|
+
// give runtime a dummy value so it knows we're loading from a stash blob
|
|
1043
|
+
pendingLocalState ? pendingLocalState?.pendingRuntimeState ?? {} : undefined);
|
|
948
1044
|
// replay saved ops
|
|
949
1045
|
if (pendingLocalState) {
|
|
950
1046
|
for (const message of pendingLocalState.savedOps) {
|
|
951
1047
|
this.processRemoteMessage(message);
|
|
952
1048
|
// allow runtime to apply stashed ops at this op's sequence number
|
|
953
|
-
await this.
|
|
1049
|
+
await this.runtime.notifyOpReplay?.(message);
|
|
954
1050
|
}
|
|
955
1051
|
pendingLocalState.savedOps = [];
|
|
956
|
-
// now set clientId to stashed clientId so live ops are correctly processed as local
|
|
957
|
-
assert(this.clientId === undefined, 0x5d6 /* Unexpected clientId when setting stashed clientId */);
|
|
958
|
-
this._clientId = pendingLocalState === null || pendingLocalState === void 0 ? void 0 : pendingLocalState.clientId;
|
|
959
1052
|
}
|
|
960
1053
|
// We might have hit some failure that did not manifest itself in exception in this flow,
|
|
961
1054
|
// do not start op processing in such case - static version of Container.load() will handle it correctly.
|
|
@@ -967,24 +1060,20 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
967
1060
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
968
1061
|
this._deltaManager.inbound.pause();
|
|
969
1062
|
}
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
1063
|
+
this.handleDeltaConnectionArg(connectionArgs, loadMode.deltaConnection, pendingLocalState !== undefined);
|
|
1064
|
+
}
|
|
1065
|
+
// If we have not yet reached `loadToSequenceNumber`, we will wait for ops to arrive until we reach it
|
|
1066
|
+
if (loadToSequenceNumber !== undefined &&
|
|
1067
|
+
this.deltaManager.lastSequenceNumber < loadToSequenceNumber) {
|
|
1068
|
+
await new Promise((resolve, reject) => {
|
|
1069
|
+
const opHandler = (message) => {
|
|
1070
|
+
if (message.sequenceNumber >= loadToSequenceNumber) {
|
|
1071
|
+
resolve();
|
|
1072
|
+
this.off("op", opHandler);
|
|
975
1073
|
}
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
this.inboundQueuePausedFromInit = false;
|
|
980
|
-
this._deltaManager.inbound.resume();
|
|
981
|
-
this._deltaManager.inboundSignal.resume();
|
|
982
|
-
break;
|
|
983
|
-
case "none":
|
|
984
|
-
break;
|
|
985
|
-
default:
|
|
986
|
-
unreachableCase(loadMode.deltaConnection);
|
|
987
|
-
}
|
|
1074
|
+
};
|
|
1075
|
+
this.on("op", opHandler);
|
|
1076
|
+
});
|
|
988
1077
|
}
|
|
989
1078
|
// Safety net: static version of Container.load() should have learned about it through "closed" handler.
|
|
990
1079
|
// But if that did not happen for some reason, fail load for sure.
|
|
@@ -996,6 +1085,11 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
996
1085
|
}
|
|
997
1086
|
// Internal context is fully loaded at this point
|
|
998
1087
|
this.setLoaded();
|
|
1088
|
+
timings.end = performance.now();
|
|
1089
|
+
this.subLogger.sendTelemetryEvent({
|
|
1090
|
+
eventName: "LoadStagesTimings",
|
|
1091
|
+
details: JSON.stringify(timings),
|
|
1092
|
+
}, undefined, LogLevel.verbose);
|
|
999
1093
|
return {
|
|
1000
1094
|
sequenceNumber: attributes.sequenceNumber,
|
|
1001
1095
|
version: versionId,
|
|
@@ -1003,7 +1097,7 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
1003
1097
|
dmLastKnownSeqNumber: this._deltaManager.lastKnownSeqNumber,
|
|
1004
1098
|
};
|
|
1005
1099
|
}
|
|
1006
|
-
async createDetached(
|
|
1100
|
+
async createDetached(codeDetails) {
|
|
1007
1101
|
const attributes = {
|
|
1008
1102
|
sequenceNumber: detachedContainerRefSeqNumber,
|
|
1009
1103
|
term: OnlyValidTermValue,
|
|
@@ -1011,14 +1105,13 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
1011
1105
|
};
|
|
1012
1106
|
await this.attachDeltaManagerOpHandler(attributes);
|
|
1013
1107
|
// Need to just seed the source data in the code quorum. Quorum itself is empty
|
|
1014
|
-
const qValues = initQuorumValuesFromCodeDetails(
|
|
1108
|
+
const qValues = initQuorumValuesFromCodeDetails(codeDetails);
|
|
1015
1109
|
this.initializeProtocolState(attributes, {
|
|
1016
1110
|
members: [],
|
|
1017
1111
|
proposals: [],
|
|
1018
1112
|
values: qValues,
|
|
1019
1113
|
});
|
|
1020
|
-
|
|
1021
|
-
await this.instantiateContextDetached(false);
|
|
1114
|
+
await this.instantiateRuntime(codeDetails, undefined);
|
|
1022
1115
|
this.setLoaded();
|
|
1023
1116
|
}
|
|
1024
1117
|
async rehydrateDetachedFromSnapshot(detachedContainerSnapshot) {
|
|
@@ -1033,14 +1126,13 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
1033
1126
|
// Initialize the protocol handler
|
|
1034
1127
|
const baseTree = getProtocolSnapshotTree(snapshotTree);
|
|
1035
1128
|
const qValues = await readAndParse(this.storageAdapter, baseTree.blobs.quorumValues);
|
|
1036
|
-
const codeDetails = getCodeDetailsFromQuorumValues(qValues);
|
|
1037
1129
|
this.initializeProtocolState(attributes, {
|
|
1038
1130
|
members: [],
|
|
1039
1131
|
proposals: [],
|
|
1040
|
-
values:
|
|
1132
|
+
values: qValues,
|
|
1041
1133
|
});
|
|
1042
|
-
|
|
1043
|
-
snapshotTree);
|
|
1134
|
+
const codeDetails = this.getCodeDetailsFromQuorum();
|
|
1135
|
+
await this.instantiateRuntime(codeDetails, snapshotTree);
|
|
1044
1136
|
this.setLoaded();
|
|
1045
1137
|
}
|
|
1046
1138
|
async getDocumentAttributes(storage, tree) {
|
|
@@ -1077,7 +1169,10 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
1077
1169
|
}
|
|
1078
1170
|
initializeProtocolState(attributes, quorumSnapshot) {
|
|
1079
1171
|
const protocol = this.protocolHandlerBuilder(attributes, quorumSnapshot, (key, value) => this.submitMessage(MessageType.Propose, JSON.stringify({ key, value })));
|
|
1080
|
-
const protocolLogger =
|
|
1172
|
+
const protocolLogger = createChildLogger({
|
|
1173
|
+
logger: this.subLogger,
|
|
1174
|
+
namespace: "ProtocolHandler",
|
|
1175
|
+
});
|
|
1081
1176
|
protocol.quorum.on("error", (error) => {
|
|
1082
1177
|
protocolLogger.sendErrorEvent(error);
|
|
1083
1178
|
});
|
|
@@ -1139,8 +1234,7 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
1139
1234
|
return pkg;
|
|
1140
1235
|
}
|
|
1141
1236
|
get client() {
|
|
1142
|
-
|
|
1143
|
-
const client = ((_a = this.options) === null || _a === void 0 ? void 0 : _a.client) !== undefined
|
|
1237
|
+
const client = this.options?.client !== undefined
|
|
1144
1238
|
? this.options.client
|
|
1145
1239
|
: {
|
|
1146
1240
|
details: {
|
|
@@ -1171,7 +1265,7 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
1171
1265
|
}
|
|
1172
1266
|
createDeltaManager() {
|
|
1173
1267
|
const serviceProvider = () => this.service;
|
|
1174
|
-
const deltaManager = new DeltaManager(serviceProvider,
|
|
1268
|
+
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));
|
|
1175
1269
|
// Disable inbound queues as Container is not ready to accept any ops until we are fully loaded!
|
|
1176
1270
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
1177
1271
|
deltaManager.inbound.pause();
|
|
@@ -1187,11 +1281,10 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
1187
1281
|
deltaManager.on("cancelEstablishingConnection", (reason) => {
|
|
1188
1282
|
this.connectionStateHandler.cancelEstablishingConnection(reason);
|
|
1189
1283
|
});
|
|
1190
|
-
deltaManager.on("disconnect", (reason
|
|
1191
|
-
|
|
1192
|
-
(_a = this.collabWindowTracker) === null || _a === void 0 ? void 0 : _a.stopSequenceNumberUpdate();
|
|
1284
|
+
deltaManager.on("disconnect", (reason) => {
|
|
1285
|
+
this.noopHeuristic?.notifyDisconnect();
|
|
1193
1286
|
if (!this.closed) {
|
|
1194
|
-
this.connectionStateHandler.receivedDisconnectEvent(reason
|
|
1287
|
+
this.connectionStateHandler.receivedDisconnectEvent(reason);
|
|
1195
1288
|
}
|
|
1196
1289
|
});
|
|
1197
1290
|
deltaManager.on("throttled", (warning) => {
|
|
@@ -1223,8 +1316,7 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
1223
1316
|
},
|
|
1224
1317
|
}, prefetchType);
|
|
1225
1318
|
}
|
|
1226
|
-
logConnectionStateChangeTelemetry(value, oldState, reason
|
|
1227
|
-
var _a;
|
|
1319
|
+
logConnectionStateChangeTelemetry(value, oldState, reason) {
|
|
1228
1320
|
// Log actual event
|
|
1229
1321
|
const time = performance.now();
|
|
1230
1322
|
this.connectionTransitionTimes[value] = time;
|
|
@@ -1241,30 +1333,44 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
1241
1333
|
if (value === ConnectionState.Connected) {
|
|
1242
1334
|
durationFromDisconnected =
|
|
1243
1335
|
time - this.connectionTransitionTimes[ConnectionState.Disconnected];
|
|
1244
|
-
durationFromDisconnected =
|
|
1336
|
+
durationFromDisconnected = formatTick(durationFromDisconnected);
|
|
1245
1337
|
}
|
|
1246
1338
|
else if (value === ConnectionState.CatchingUp) {
|
|
1247
1339
|
// This info is of most interesting while Catching Up.
|
|
1248
1340
|
checkpointSequenceNumber = this.deltaManager.lastKnownSeqNumber;
|
|
1249
|
-
|
|
1341
|
+
// Need to check that we have already loaded and fetched the snapshot.
|
|
1342
|
+
if (this.deltaManager.hasCheckpointSequenceNumber &&
|
|
1343
|
+
this._lifecycleState === "loaded") {
|
|
1250
1344
|
opsBehind = checkpointSequenceNumber - this.deltaManager.lastSequenceNumber;
|
|
1251
1345
|
}
|
|
1252
1346
|
}
|
|
1253
1347
|
connectionInitiationReason = this.firstConnection ? "InitialConnect" : "AutoReconnect";
|
|
1254
1348
|
}
|
|
1255
|
-
this.mc.logger.sendPerformanceEvent(
|
|
1349
|
+
this.mc.logger.sendPerformanceEvent({
|
|
1350
|
+
eventName: `ConnectionStateChange_${ConnectionState[value]}`,
|
|
1351
|
+
from: ConnectionState[oldState],
|
|
1352
|
+
duration,
|
|
1256
1353
|
durationFromDisconnected,
|
|
1257
|
-
reason,
|
|
1258
|
-
connectionInitiationReason,
|
|
1259
|
-
|
|
1354
|
+
reason: reason?.text,
|
|
1355
|
+
connectionInitiationReason,
|
|
1356
|
+
pendingClientId: this.connectionStateHandler.pendingClientId,
|
|
1357
|
+
clientId: this.clientId,
|
|
1358
|
+
autoReconnect,
|
|
1359
|
+
opsBehind,
|
|
1360
|
+
online: OnlineStatus[isOnline()],
|
|
1361
|
+
lastVisible: this.lastVisible !== undefined
|
|
1260
1362
|
? performance.now() - this.lastVisible
|
|
1261
|
-
: undefined,
|
|
1363
|
+
: undefined,
|
|
1364
|
+
checkpointSequenceNumber,
|
|
1365
|
+
quorumSize: this._protocolHandler?.quorum.getMembers().size,
|
|
1366
|
+
isDirty: this.isDirty,
|
|
1367
|
+
...this._deltaManager.connectionProps,
|
|
1368
|
+
}, reason?.error);
|
|
1262
1369
|
if (value === ConnectionState.Connected) {
|
|
1263
1370
|
this.firstConnection = false;
|
|
1264
1371
|
}
|
|
1265
1372
|
}
|
|
1266
1373
|
propagateConnectionState(initialTransition, disconnectedReason) {
|
|
1267
|
-
var _a;
|
|
1268
1374
|
// When container loaded, we want to propagate initial connection state.
|
|
1269
1375
|
// After that, we communicate only transitions to Connected & Disconnected states, skipping all other states.
|
|
1270
1376
|
// This can be changed in the future, for example we likely should add "CatchingUp" event on Container.
|
|
@@ -1274,22 +1380,10 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
1274
1380
|
return;
|
|
1275
1381
|
}
|
|
1276
1382
|
const state = this.connectionState === ConnectionState.Connected;
|
|
1277
|
-
const logOpsOnReconnect = this.connectionState === ConnectionState.Connected &&
|
|
1278
|
-
!this.firstConnection &&
|
|
1279
|
-
this.connectionMode === "write";
|
|
1280
|
-
if (logOpsOnReconnect) {
|
|
1281
|
-
this.messageCountAfterDisconnection = 0;
|
|
1282
|
-
}
|
|
1283
1383
|
// Both protocol and context should not be undefined if we got so far.
|
|
1284
|
-
this.setContextConnectedState(state,
|
|
1384
|
+
this.setContextConnectedState(state, this.readOnlyInfo.readonly ?? false);
|
|
1285
1385
|
this.protocolHandler.setConnectionState(state, this.clientId);
|
|
1286
|
-
raiseConnectedEvent(this.mc.logger, this, state, this.clientId, disconnectedReason);
|
|
1287
|
-
if (logOpsOnReconnect) {
|
|
1288
|
-
this.mc.logger.sendTelemetryEvent({
|
|
1289
|
-
eventName: "OpsSentOnReconnect",
|
|
1290
|
-
count: this.messageCountAfterDisconnection,
|
|
1291
|
-
});
|
|
1292
|
-
}
|
|
1386
|
+
raiseConnectedEvent(this.mc.logger, this, state, this.clientId, disconnectedReason?.text);
|
|
1293
1387
|
}
|
|
1294
1388
|
// back-compat: ADO #1385: Remove in the future, summary op should come through submitSummaryMessage()
|
|
1295
1389
|
submitContainerMessage(type, contents, batch, metadata) {
|
|
@@ -1327,13 +1421,11 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
1327
1421
|
return this.submitMessage(MessageType.Summarize, JSON.stringify(summary), false /* batch */, undefined /* metadata */, undefined /* compression */, referenceSequenceNumber);
|
|
1328
1422
|
}
|
|
1329
1423
|
submitMessage(type, contents, batch, metadata, compression, referenceSequenceNumber) {
|
|
1330
|
-
var _a;
|
|
1331
1424
|
if (this.connectionState !== ConnectionState.Connected) {
|
|
1332
1425
|
this.mc.logger.sendErrorEvent({ eventName: "SubmitMessageWithNoConnection", type });
|
|
1333
1426
|
return -1;
|
|
1334
1427
|
}
|
|
1335
|
-
this.
|
|
1336
|
-
(_a = this.collabWindowTracker) === null || _a === void 0 ? void 0 : _a.stopSequenceNumberUpdate();
|
|
1428
|
+
this.noopHeuristic?.notifyMessageSent();
|
|
1337
1429
|
return this._deltaManager.submit(type, contents, batch, metadata, compression, referenceSequenceNumber);
|
|
1338
1430
|
}
|
|
1339
1431
|
processRemoteMessage(message) {
|
|
@@ -1344,21 +1436,31 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
1344
1436
|
// Allow the protocol handler to process the message
|
|
1345
1437
|
const result = this.protocolHandler.processMessage(message, local);
|
|
1346
1438
|
// Forward messages to the loaded runtime for processing
|
|
1347
|
-
this.
|
|
1439
|
+
this.runtime.process(message, local);
|
|
1348
1440
|
// Inactive (not in quorum or not writers) clients don't take part in the minimum sequence number calculation.
|
|
1349
1441
|
if (this.activeConnection()) {
|
|
1350
|
-
if (this.
|
|
1442
|
+
if (this.noopHeuristic === undefined) {
|
|
1443
|
+
const serviceConfiguration = this.deltaManager.serviceConfiguration;
|
|
1351
1444
|
// Note that config from first connection will be used for this container's lifetime.
|
|
1352
1445
|
// That means that if relay service changes settings, such changes will impact only newly booted
|
|
1353
1446
|
// clients.
|
|
1354
1447
|
// All existing will continue to use settings they got earlier.
|
|
1355
|
-
assert(
|
|
1356
|
-
this.
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1448
|
+
assert(serviceConfiguration !== undefined, 0x2e4 /* "there should be service config for active connection" */);
|
|
1449
|
+
this.noopHeuristic = new NoopHeuristic(serviceConfiguration.noopTimeFrequency, serviceConfiguration.noopCountFrequency);
|
|
1450
|
+
this.noopHeuristic.on("wantsNoop", () => {
|
|
1451
|
+
// On disconnect we notify the heuristic which should prevent it from wanting a noop.
|
|
1452
|
+
// Hitting this assert would imply we lost activeConnection between notifying the heuristic of a processed message and
|
|
1453
|
+
// running the microtask that the heuristic queued in response.
|
|
1454
|
+
assert(this.activeConnection(), 0x241 /* "Trying to send noop without active connection" */);
|
|
1455
|
+
this.submitMessage(MessageType.NoOp);
|
|
1456
|
+
});
|
|
1457
|
+
}
|
|
1458
|
+
this.noopHeuristic.notifyMessageProcessed(message);
|
|
1459
|
+
// The contract with the protocolHandler is that returning "immediateNoOp" is equivalent to "please immediately accept the proposal I just processed".
|
|
1460
|
+
if (result.immediateNoOp === true) {
|
|
1461
|
+
// ADO:1385: Remove cast and use MessageType once definition changes propagate
|
|
1462
|
+
this.submitMessage(MessageType2.Accept);
|
|
1360
1463
|
}
|
|
1361
|
-
this.collabWindowTracker.scheduleSequenceNumberUpdate(message, result.immediateNoOp === true);
|
|
1362
1464
|
}
|
|
1363
1465
|
this.emit("op", message);
|
|
1364
1466
|
}
|
|
@@ -1367,12 +1469,12 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
1367
1469
|
}
|
|
1368
1470
|
processSignal(message) {
|
|
1369
1471
|
// No clientId indicates a system signal message.
|
|
1370
|
-
if (message
|
|
1472
|
+
if (protocolHandlerShouldProcessSignal(message)) {
|
|
1371
1473
|
this.protocolHandler.processSignal(message);
|
|
1372
1474
|
}
|
|
1373
1475
|
else {
|
|
1374
1476
|
const local = this.clientId === message.clientId;
|
|
1375
|
-
this.
|
|
1477
|
+
this.runtime.processSignal(message, local);
|
|
1376
1478
|
}
|
|
1377
1479
|
}
|
|
1378
1480
|
/**
|
|
@@ -1381,8 +1483,7 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
1381
1483
|
* @returns The snapshot requested, or the latest snapshot if no version was specified, plus version ID
|
|
1382
1484
|
*/
|
|
1383
1485
|
async fetchSnapshotTree(specifiedVersion) {
|
|
1384
|
-
|
|
1385
|
-
const version = await this.getVersion(specifiedVersion !== null && specifiedVersion !== void 0 ? specifiedVersion : null);
|
|
1486
|
+
const version = await this.getVersion(specifiedVersion ?? null);
|
|
1386
1487
|
if (version === undefined && specifiedVersion !== undefined) {
|
|
1387
1488
|
// We should have a defined version to load from if specified version requested
|
|
1388
1489
|
this.mc.logger.sendErrorEvent({
|
|
@@ -1391,35 +1492,38 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
1391
1492
|
});
|
|
1392
1493
|
}
|
|
1393
1494
|
this._loadedFromVersion = version;
|
|
1394
|
-
const snapshot = (
|
|
1495
|
+
const snapshot = (await this.storageAdapter.getSnapshotTree(version)) ?? undefined;
|
|
1395
1496
|
if (snapshot === undefined && version !== undefined) {
|
|
1396
1497
|
this.mc.logger.sendErrorEvent({ eventName: "getSnapshotTreeFailed", id: version.id });
|
|
1397
1498
|
}
|
|
1398
|
-
return { snapshot, versionId: version
|
|
1399
|
-
}
|
|
1400
|
-
async instantiateContextDetached(existing, snapshot) {
|
|
1401
|
-
const codeDetails = this.getCodeDetailsFromQuorum();
|
|
1402
|
-
if (codeDetails === undefined) {
|
|
1403
|
-
throw new Error("pkg should be provided in create flow!!");
|
|
1404
|
-
}
|
|
1405
|
-
await this.instantiateContext(existing, codeDetails, snapshot);
|
|
1499
|
+
return { snapshot, versionId: version?.id };
|
|
1406
1500
|
}
|
|
1407
|
-
async
|
|
1408
|
-
|
|
1409
|
-
assert(((_a = this._context) === null || _a === void 0 ? void 0 : _a.disposed) !== false, 0x0dd /* "Existing context not disposed" */);
|
|
1501
|
+
async instantiateRuntime(codeDetails, snapshot, pendingLocalState) {
|
|
1502
|
+
assert(this._runtime?.disposed !== false, 0x0dd /* "Existing runtime not disposed" */);
|
|
1410
1503
|
// The relative loader will proxy requests to '/' to the loader itself assuming no non-cache flags
|
|
1411
1504
|
// are set. Global requests will still go directly to the loader
|
|
1412
1505
|
const maybeLoader = this.scope;
|
|
1413
1506
|
const loader = new RelativeLoader(this, maybeLoader.ILoader);
|
|
1414
|
-
|
|
1415
|
-
this.
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1507
|
+
const loadCodeResult = await PerformanceEvent.timedExecAsync(this.subLogger, { eventName: "CodeLoad" }, async () => this.codeLoader.load(codeDetails));
|
|
1508
|
+
this._loadedModule = {
|
|
1509
|
+
module: loadCodeResult.module,
|
|
1510
|
+
// An older interface ICodeLoader could return an IFluidModule which didn't have details.
|
|
1511
|
+
// If we're using one of those older ICodeLoaders, then we fix up the module with the specified details here.
|
|
1512
|
+
// TODO: Determine if this is still a realistic scenario or if this fixup could be removed.
|
|
1513
|
+
details: loadCodeResult.details ?? codeDetails,
|
|
1514
|
+
};
|
|
1515
|
+
const fluidExport = this._loadedModule.module.fluidExport;
|
|
1516
|
+
const runtimeFactory = fluidExport?.IRuntimeFactory;
|
|
1517
|
+
if (runtimeFactory === undefined) {
|
|
1518
|
+
throw new Error(packageNotFactoryError);
|
|
1420
1519
|
}
|
|
1421
|
-
this.
|
|
1422
|
-
|
|
1520
|
+
const getSpecifiedCodeDetails = () => (this.protocolHandler.quorum.get("code") ??
|
|
1521
|
+
this.protocolHandler.quorum.get("code2"));
|
|
1522
|
+
const existing = snapshot !== undefined;
|
|
1523
|
+
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), (message) => this.submitSignal(message), (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);
|
|
1524
|
+
this._runtime = await PerformanceEvent.timedExecAsync(this.subLogger, { eventName: "InstantiateRuntime" }, async () => runtimeFactory.instantiateRuntime(context, existing));
|
|
1525
|
+
this._lifecycleEvents.emit("runtimeInstantiated");
|
|
1526
|
+
this._loadedCodeDetails = codeDetails;
|
|
1423
1527
|
}
|
|
1424
1528
|
/**
|
|
1425
1529
|
* Set the connected state of the ContainerContext
|
|
@@ -1428,17 +1532,35 @@ export class Container extends EventEmitterWithErrorHandling {
|
|
|
1428
1532
|
* @param readonly - Is the container in readonly mode?
|
|
1429
1533
|
*/
|
|
1430
1534
|
setContextConnectedState(state, readonly) {
|
|
1431
|
-
|
|
1432
|
-
if (((_a = this._context) === null || _a === void 0 ? void 0 : _a.disposed) === false) {
|
|
1535
|
+
if (this._runtime?.disposed === false) {
|
|
1433
1536
|
/**
|
|
1434
1537
|
* We want to lie to the ContainerRuntime when we are in readonly mode to prevent issues with pending
|
|
1435
1538
|
* ops getting through to the DeltaManager.
|
|
1436
1539
|
* The ContainerRuntime's "connected" state simply means it is ok to send ops
|
|
1437
1540
|
* See https://dev.azure.com/fluidframework/internal/_workitems/edit/1246
|
|
1438
1541
|
*/
|
|
1439
|
-
this.
|
|
1542
|
+
this.runtime.setConnectionState(state && !readonly, this.clientId);
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
handleDeltaConnectionArg(connectionArgs, deltaConnectionArg, canConnect = true) {
|
|
1546
|
+
switch (deltaConnectionArg) {
|
|
1547
|
+
case undefined:
|
|
1548
|
+
if (canConnect) {
|
|
1549
|
+
// connect to delta stream now since we did not before
|
|
1550
|
+
this.connectToDeltaStream(connectionArgs);
|
|
1551
|
+
}
|
|
1552
|
+
// intentional fallthrough
|
|
1553
|
+
case "delayed":
|
|
1554
|
+
assert(this.inboundQueuePausedFromInit, 0x346 /* inboundQueuePausedFromInit should be true */);
|
|
1555
|
+
this.inboundQueuePausedFromInit = false;
|
|
1556
|
+
this._deltaManager.inbound.resume();
|
|
1557
|
+
this._deltaManager.inboundSignal.resume();
|
|
1558
|
+
break;
|
|
1559
|
+
case "none":
|
|
1560
|
+
break;
|
|
1561
|
+
default:
|
|
1562
|
+
unreachableCase(deltaConnectionArg);
|
|
1440
1563
|
}
|
|
1441
1564
|
}
|
|
1442
1565
|
}
|
|
1443
|
-
Container.version = "^0.1.0";
|
|
1444
1566
|
//# sourceMappingURL=container.js.map
|