@fluidframework/container-loader 0.52.1 → 0.54.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/dist/connectionManager.d.ts +153 -0
  2. package/dist/connectionManager.d.ts.map +1 -0
  3. package/dist/connectionManager.js +664 -0
  4. package/dist/connectionManager.js.map +1 -0
  5. package/dist/connectionStateHandler.d.ts +1 -0
  6. package/dist/connectionStateHandler.d.ts.map +1 -1
  7. package/dist/connectionStateHandler.js +6 -0
  8. package/dist/connectionStateHandler.js.map +1 -1
  9. package/dist/container.d.ts +2 -22
  10. package/dist/container.d.ts.map +1 -1
  11. package/dist/container.js +121 -151
  12. package/dist/container.js.map +1 -1
  13. package/dist/containerContext.d.ts +1 -0
  14. package/dist/containerContext.d.ts.map +1 -1
  15. package/dist/containerContext.js +4 -0
  16. package/dist/containerContext.js.map +1 -1
  17. package/dist/contracts.d.ts +112 -0
  18. package/dist/contracts.d.ts.map +1 -0
  19. package/dist/contracts.js +14 -0
  20. package/dist/contracts.js.map +1 -0
  21. package/dist/deltaManager.d.ts +26 -142
  22. package/dist/deltaManager.d.ts.map +1 -1
  23. package/dist/deltaManager.js +143 -770
  24. package/dist/deltaManager.js.map +1 -1
  25. package/dist/loader.d.ts +14 -4
  26. package/dist/loader.d.ts.map +1 -1
  27. package/dist/loader.js +10 -4
  28. package/dist/loader.js.map +1 -1
  29. package/dist/packageVersion.d.ts +1 -1
  30. package/dist/packageVersion.js +1 -1
  31. package/dist/packageVersion.js.map +1 -1
  32. package/dist/protocolTreeDocumentStorageService.d.ts +2 -2
  33. package/dist/protocolTreeDocumentStorageService.d.ts.map +1 -1
  34. package/lib/connectionManager.d.ts +153 -0
  35. package/lib/connectionManager.d.ts.map +1 -0
  36. package/lib/connectionManager.js +660 -0
  37. package/lib/connectionManager.js.map +1 -0
  38. package/lib/connectionStateHandler.d.ts +1 -0
  39. package/lib/connectionStateHandler.d.ts.map +1 -1
  40. package/lib/connectionStateHandler.js +6 -0
  41. package/lib/connectionStateHandler.js.map +1 -1
  42. package/lib/container.d.ts +2 -22
  43. package/lib/container.d.ts.map +1 -1
  44. package/lib/container.js +122 -152
  45. package/lib/container.js.map +1 -1
  46. package/lib/containerContext.d.ts +1 -0
  47. package/lib/containerContext.d.ts.map +1 -1
  48. package/lib/containerContext.js +4 -0
  49. package/lib/containerContext.js.map +1 -1
  50. package/lib/contracts.d.ts +112 -0
  51. package/lib/contracts.d.ts.map +1 -0
  52. package/lib/contracts.js +11 -0
  53. package/lib/contracts.js.map +1 -0
  54. package/lib/deltaManager.d.ts +26 -142
  55. package/lib/deltaManager.d.ts.map +1 -1
  56. package/lib/deltaManager.js +147 -774
  57. package/lib/deltaManager.js.map +1 -1
  58. package/lib/loader.d.ts +14 -4
  59. package/lib/loader.d.ts.map +1 -1
  60. package/lib/loader.js +11 -5
  61. package/lib/loader.js.map +1 -1
  62. package/lib/packageVersion.d.ts +1 -1
  63. package/lib/packageVersion.js +1 -1
  64. package/lib/packageVersion.js.map +1 -1
  65. package/lib/protocolTreeDocumentStorageService.d.ts +2 -2
  66. package/lib/protocolTreeDocumentStorageService.d.ts.map +1 -1
  67. package/package.json +9 -9
  68. package/src/connectionManager.ts +892 -0
  69. package/src/connectionStateHandler.ts +8 -0
  70. package/src/container.ts +165 -187
  71. package/src/containerContext.ts +4 -0
  72. package/src/contracts.ts +156 -0
  73. package/src/deltaManager.ts +181 -978
  74. package/src/loader.ts +59 -27
  75. package/src/packageVersion.ts +1 -1
@@ -0,0 +1,664 @@
1
+ "use strict";
2
+ /*!
3
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
4
+ * Licensed under the MIT License.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.ConnectionManager = void 0;
8
+ const common_utils_1 = require("@fluidframework/common-utils");
9
+ const telemetry_utils_1 = require("@fluidframework/telemetry-utils");
10
+ const protocol_definitions_1 = require("@fluidframework/protocol-definitions");
11
+ const driver_utils_1 = require("@fluidframework/driver-utils");
12
+ const container_utils_1 = require("@fluidframework/container-utils");
13
+ const deltaQueue_1 = require("./deltaQueue");
14
+ const contracts_1 = require("./contracts");
15
+ const MaxReconnectDelayInMs = 8000;
16
+ const InitialReconnectDelayInMs = 1000;
17
+ const DefaultChunkSize = 16 * 1024;
18
+ const fatalConnectErrorProp = { fatalConnectError: true };
19
+ function getNackReconnectInfo(nackContent) {
20
+ const message = `Nack (${nackContent.type}): ${nackContent.message}`;
21
+ const canRetry = nackContent.code !== 403;
22
+ const retryAfterMs = nackContent.retryAfter !== undefined ? nackContent.retryAfter * 1000 : undefined;
23
+ return driver_utils_1.createGenericNetworkError(`nack [${nackContent.code}]`, message, canRetry, retryAfterMs, { statusCode: nackContent.code });
24
+ }
25
+ const createReconnectError = (fluidErrorCode, err) => telemetry_utils_1.wrapError(err, (errorMessage) => new driver_utils_1.GenericNetworkError(fluidErrorCode, errorMessage, true /* canRetry */));
26
+ /**
27
+ * Implementation of IDocumentDeltaConnection that does not support submitting
28
+ * or receiving ops. Used in storage-only mode.
29
+ */
30
+ class NoDeltaStream extends common_utils_1.TypedEventEmitter {
31
+ constructor() {
32
+ super(...arguments);
33
+ this.clientId = "storage-only client";
34
+ this.claims = {
35
+ scopes: [protocol_definitions_1.ScopeType.DocRead],
36
+ };
37
+ this.mode = "read";
38
+ this.existing = true;
39
+ this.maxMessageSize = 0;
40
+ this.version = "";
41
+ this.initialMessages = [];
42
+ this.initialSignals = [];
43
+ this.initialClients = [];
44
+ this.serviceConfiguration = {
45
+ maxMessageSize: 0,
46
+ blockSize: 0,
47
+ summary: undefined,
48
+ };
49
+ this.checkpointSequenceNumber = undefined;
50
+ this._disposed = false;
51
+ }
52
+ submit(messages) {
53
+ this.emit("nack", this.clientId, messages.map((operation) => {
54
+ return {
55
+ operation,
56
+ content: { message: "Cannot submit with storage-only connection", code: 403 },
57
+ };
58
+ }));
59
+ }
60
+ submitSignal(message) {
61
+ this.emit("nack", this.clientId, {
62
+ operation: message,
63
+ content: { message: "Cannot submit signal with storage-only connection", code: 403 },
64
+ });
65
+ }
66
+ get disposed() { return this._disposed; }
67
+ dispose() { this._disposed = true; }
68
+ }
69
+ /**
70
+ * Implementation of IConnectionManager, used by Container class
71
+ * Implements constant connectivity to relay service, by reconnecting in case of loast connection or error.
72
+ * Exposes various controls to influecen this process, including manual reconnects, forced read-only mode, etc.
73
+ */
74
+ class ConnectionManager {
75
+ constructor(serviceProvider, client, reconnectAllowed, logger, props) {
76
+ this.serviceProvider = serviceProvider;
77
+ this.client = client;
78
+ this.logger = logger;
79
+ this.props = props;
80
+ this.pendingConnection = false;
81
+ /** tracks host requiring read-only mode. */
82
+ this._forceReadonly = false;
83
+ /** True if there is pending (async) reconnection from "read" to "write" */
84
+ this.pendingReconnect = false;
85
+ /** downgrade "write" connection to "read" */
86
+ this.downgradedConnection = false;
87
+ this.clientSequenceNumber = 0;
88
+ this.clientSequenceNumberObserved = 0;
89
+ /** Counts the number of noops sent by the client which may not be acked. */
90
+ this.trailingNoopCount = 0;
91
+ this.connectFirstConnection = true;
92
+ this._connectionVerboseProps = {};
93
+ this._connectionProps = {};
94
+ this.closed = false;
95
+ this.opHandler = (documentId, messagesArg) => {
96
+ const messages = Array.isArray(messagesArg) ? messagesArg : [messagesArg];
97
+ this.props.incomingOpHandler(messages, "opHandler");
98
+ };
99
+ // Always connect in write mode after getting nacked.
100
+ this.nackHandler = (documentId, messages) => {
101
+ const message = messages[0];
102
+ // TODO: we should remove this check when service updates?
103
+ // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
104
+ if (this._readonlyPermissions) {
105
+ this.props.closeHandler(driver_utils_1.createWriteError("writeOnReadOnlyDocument"));
106
+ }
107
+ // check message.content for Back-compat with old service.
108
+ const reconnectInfo = message.content !== undefined
109
+ ? getNackReconnectInfo(message.content) :
110
+ driver_utils_1.createGenericNetworkError("nackReasonUnknown", undefined, true);
111
+ this.reconnectOnError("write", reconnectInfo);
112
+ };
113
+ // Connection mode is always read on disconnect/error unless the system mode was write.
114
+ this.disconnectHandlerInternal = (disconnectReason) => {
115
+ // Note: we might get multiple disconnect calls on same socket, as early disconnect notification
116
+ // ("server_disconnect", ODSP-specific) is mapped to "disconnect"
117
+ this.reconnectOnError(this.defaultReconnectionMode, createReconnectError("dmDocumentDeltaConnectionDisconnected", disconnectReason));
118
+ };
119
+ this.errorHandler = (error) => {
120
+ this.reconnectOnError(this.defaultReconnectionMode, createReconnectError("dmDocumentDeltaConnectionError", error));
121
+ };
122
+ this.clientDetails = this.client.details;
123
+ this.defaultReconnectionMode = this.client.mode;
124
+ this._reconnectMode = reconnectAllowed ? contracts_1.ReconnectMode.Enabled : contracts_1.ReconnectMode.Never;
125
+ // Outbound message queue. The outbound queue is represented as a queue of an array of ops. Ops contained
126
+ // within an array *must* fit within the maxMessageSize and are guaranteed to be ordered sequentially.
127
+ this._outbound = new deltaQueue_1.DeltaQueue((messages) => {
128
+ if (this.connection === undefined) {
129
+ throw new Error("Attempted to submit an outbound message without connection");
130
+ }
131
+ this.connection.submit(messages);
132
+ });
133
+ this._outbound.on("error", (error) => {
134
+ this.props.closeHandler(telemetry_utils_1.normalizeError(error));
135
+ });
136
+ }
137
+ get connectionVerboseProps() { return this._connectionVerboseProps; }
138
+ /**
139
+ * The current connection mode, initially read.
140
+ */
141
+ get connectionMode() {
142
+ var _a;
143
+ common_utils_1.assert(!this.downgradedConnection || ((_a = this.connection) === null || _a === void 0 ? void 0 : _a.mode) === "write", 0x277 /* "Did we forget to reset downgradedConnection on new connection?" */);
144
+ if (this.connection === undefined) {
145
+ return "read";
146
+ }
147
+ return this.connection.mode;
148
+ }
149
+ get connected() { return this.connection !== undefined; }
150
+ get clientId() { var _a; return (_a = this.connection) === null || _a === void 0 ? void 0 : _a.clientId; }
151
+ /**
152
+ * Automatic reconnecting enabled or disabled.
153
+ * If set to Never, then reconnecting will never be allowed.
154
+ */
155
+ get reconnectMode() {
156
+ return this._reconnectMode;
157
+ }
158
+ get maxMessageSize() {
159
+ var _a, _b, _c;
160
+ return (_c = (_b = (_a = this.connection) === null || _a === void 0 ? void 0 : _a.serviceConfiguration) === null || _b === void 0 ? void 0 : _b.maxMessageSize) !== null && _c !== void 0 ? _c : DefaultChunkSize;
161
+ }
162
+ get version() {
163
+ if (this.connection === undefined) {
164
+ throw new Error("Cannot check version without a connection");
165
+ }
166
+ return this.connection.version;
167
+ }
168
+ get serviceConfiguration() {
169
+ var _a;
170
+ return (_a = this.connection) === null || _a === void 0 ? void 0 : _a.serviceConfiguration;
171
+ }
172
+ get scopes() {
173
+ var _a;
174
+ return (_a = this.connection) === null || _a === void 0 ? void 0 : _a.claims.scopes;
175
+ }
176
+ get outbound() {
177
+ return this._outbound;
178
+ }
179
+ /**
180
+ * Returns set of props that can be logged in telemetry that provide some insights / statistics
181
+ * about current or last connection (if there is no connection at the moment)
182
+ */
183
+ get connectionProps() {
184
+ if (this.connection !== undefined) {
185
+ return this._connectionProps;
186
+ }
187
+ else {
188
+ return Object.assign(Object.assign({}, this._connectionProps), {
189
+ // Report how many ops this client sent in last disconnected session
190
+ sentOps: this.clientSequenceNumber });
191
+ }
192
+ }
193
+ shouldJoinWrite() {
194
+ // We don't have to wait for ack for topmost NoOps. So subtract those.
195
+ return this.clientSequenceNumberObserved < (this.clientSequenceNumber - this.trailingNoopCount);
196
+ }
197
+ /**
198
+ * Tells if container is in read-only mode.
199
+ * Data stores should listen for "readonly" notifications and disallow user
200
+ * making changes to data stores.
201
+ * Readonly state can be because of no storage write permission,
202
+ * or due to host forcing readonly mode for container.
203
+ * It is undefined if we have not yet established websocket connection
204
+ * and do not know if user has write access to a file.
205
+ */
206
+ get readonly() {
207
+ if (this._forceReadonly) {
208
+ return true;
209
+ }
210
+ return this._readonlyPermissions;
211
+ }
212
+ get readOnlyInfo() {
213
+ const storageOnly = this.connection !== undefined && this.connection instanceof NoDeltaStream;
214
+ if (storageOnly || this._forceReadonly || this._readonlyPermissions === true) {
215
+ return {
216
+ readonly: true,
217
+ forced: this._forceReadonly,
218
+ permissions: this._readonlyPermissions,
219
+ storageOnly,
220
+ };
221
+ }
222
+ return { readonly: this._readonlyPermissions };
223
+ }
224
+ static detailsFromConnection(connection) {
225
+ return {
226
+ claims: connection.claims,
227
+ clientId: connection.clientId,
228
+ existing: connection.existing,
229
+ checkpointSequenceNumber: connection.checkpointSequenceNumber,
230
+ get initialClients() { return connection.initialClients; },
231
+ mode: connection.mode,
232
+ serviceConfiguration: connection.serviceConfiguration,
233
+ version: connection.version,
234
+ };
235
+ }
236
+ dispose(error) {
237
+ if (this.closed) {
238
+ return;
239
+ }
240
+ this.closed = true;
241
+ this.pendingConnection = false;
242
+ // Ensure that things like triggerConnect() will short circuit
243
+ this._reconnectMode = contracts_1.ReconnectMode.Never;
244
+ this._outbound.clear();
245
+ const disconnectReason = error !== undefined
246
+ ? `Closing DeltaManager (${error.message})`
247
+ : "Closing DeltaManager";
248
+ // This raises "disconnect" event if we have active connection.
249
+ this.disconnectFromDeltaStream(disconnectReason);
250
+ // Notify everyone we are in read-only state.
251
+ // Useful for data stores in case we hit some critical error,
252
+ // to switch to a mode where user edits are not accepted
253
+ this.set_readonlyPermissions(true);
254
+ }
255
+ /**
256
+ * Enables or disables automatic reconnecting.
257
+ * Will throw an error if reconnectMode set to Never.
258
+ */
259
+ setAutoReconnect(mode) {
260
+ common_utils_1.assert(mode !== contracts_1.ReconnectMode.Never && this._reconnectMode !== contracts_1.ReconnectMode.Never, 0x278 /* "API is not supported for non-connecting or closed container" */);
261
+ this._reconnectMode = mode;
262
+ if (mode !== contracts_1.ReconnectMode.Enabled) {
263
+ // immediately disconnect - do not rely on service eventually dropping connection.
264
+ this.disconnectFromDeltaStream("setAutoReconnect");
265
+ }
266
+ }
267
+ /**
268
+ * Sends signal to runtime (and data stores) to be read-only.
269
+ * Hosts may have read only views, indicating to data stores that no edits are allowed.
270
+ * This is independent from this._readonlyPermissions (permissions) and this.connectionMode
271
+ * (server can return "write" mode even when asked for "read")
272
+ * Leveraging same "readonly" event as runtime & data stores should behave the same in such case
273
+ * as in read-only permissions.
274
+ * But this.active can be used by some DDSes to figure out if ops can be sent
275
+ * (for example, read-only view still participates in code proposals / upgrades decisions)
276
+ *
277
+ * Forcing Readonly does not prevent DDS from generating ops. It is up to user code to honour
278
+ * the readonly flag. If ops are generated, they will accumulate locally and not be sent. If
279
+ * there are pending in the outbound queue, it will stop sending until force readonly is
280
+ * cleared.
281
+ *
282
+ * @param readonly - set or clear force readonly.
283
+ */
284
+ forceReadonly(readonly) {
285
+ if (readonly !== this._forceReadonly) {
286
+ this.logger.sendTelemetryEvent({
287
+ eventName: "ForceReadOnly",
288
+ value: readonly,
289
+ });
290
+ }
291
+ const oldValue = this.readonly;
292
+ this._forceReadonly = readonly;
293
+ if (oldValue !== this.readonly) {
294
+ common_utils_1.assert(this._reconnectMode !== contracts_1.ReconnectMode.Never, 0x279 /* "API is not supported for non-connecting or closed container" */);
295
+ let reconnect = false;
296
+ if (this.readonly === true) {
297
+ // If we switch to readonly while connected, we should disconnect first
298
+ // See comment in the "readonly" event handler to deltaManager set up by
299
+ // the ContainerRuntime constructor
300
+ if (this.shouldJoinWrite()) {
301
+ // If we have pending changes, then we will never send them - it smells like
302
+ // host logic error.
303
+ this.logger.sendErrorEvent({ eventName: "ForceReadonlyPendingChanged" });
304
+ }
305
+ reconnect = this.disconnectFromDeltaStream("Force readonly");
306
+ }
307
+ this.props.readonlyChangeHandler(this.readonly);
308
+ if (reconnect) {
309
+ // reconnect if we disconnected from before.
310
+ this.triggerConnect("read");
311
+ }
312
+ }
313
+ }
314
+ set_readonlyPermissions(readonly) {
315
+ const oldValue = this.readonly;
316
+ this._readonlyPermissions = readonly;
317
+ if (oldValue !== this.readonly) {
318
+ this.props.readonlyChangeHandler(this.readonly);
319
+ }
320
+ }
321
+ connect(connectionMode) {
322
+ this.connectCore(connectionMode).catch((error) => {
323
+ const normalizedError = telemetry_utils_1.normalizeError(error, { props: fatalConnectErrorProp });
324
+ this.props.closeHandler(normalizedError);
325
+ });
326
+ }
327
+ async connectCore(connectionMode) {
328
+ var _a;
329
+ common_utils_1.assert(!this.closed, 0x26a /* "not closed" */);
330
+ if (this.connection !== undefined || this.pendingConnection) {
331
+ return;
332
+ }
333
+ let requestedMode = connectionMode !== null && connectionMode !== void 0 ? connectionMode : this.defaultReconnectionMode;
334
+ // if we have any non-acked ops from last connection, reconnect as "write".
335
+ // without that we would connect in view-only mode, which will result in immediate
336
+ // firing of "connected" event from Container and switch of current clientId (as tracked
337
+ // by all DDSes). This will make it impossible to figure out if ops actually made it through,
338
+ // so DDSes will immediately resubmit all pending ops, and some of them will be duplicates, corrupting document
339
+ if (this.shouldJoinWrite()) {
340
+ requestedMode = "write";
341
+ }
342
+ const docService = this.serviceProvider();
343
+ common_utils_1.assert(docService !== undefined, 0x2a7 /* "Container is not attached" */);
344
+ let connection;
345
+ if (((_a = docService.policies) === null || _a === void 0 ? void 0 : _a.storageOnly) === true) {
346
+ connection = new NoDeltaStream();
347
+ // to keep setupNewSuccessfulConnection happy
348
+ this.pendingConnection = true;
349
+ this.setupNewSuccessfulConnection(connection, "read");
350
+ common_utils_1.assert(!this.pendingConnection, 0x2b3 /* "logic error" */);
351
+ return;
352
+ }
353
+ // this.pendingConnection resets to false as soon as we know the outcome of the connection attempt
354
+ this.pendingConnection = true;
355
+ let delayMs = InitialReconnectDelayInMs;
356
+ let connectRepeatCount = 0;
357
+ const connectStartTime = common_utils_1.performance.now();
358
+ let lastError;
359
+ // This loop will keep trying to connect until successful, with a delay between each iteration.
360
+ while (connection === undefined) {
361
+ if (this.closed) {
362
+ throw new Error("Attempting to connect a closed DeltaManager");
363
+ }
364
+ connectRepeatCount++;
365
+ try {
366
+ this.client.mode = requestedMode;
367
+ connection = await docService.connectToDeltaStream(this.client);
368
+ if (connection.disposed) {
369
+ // Nobody observed this connection, so drop it on the floor and retry.
370
+ this.logger.sendTelemetryEvent({ eventName: "ReceivedClosedConnection" });
371
+ connection = undefined;
372
+ }
373
+ }
374
+ catch (origError) {
375
+ if (typeof origError === "object" && origError !== null &&
376
+ (origError === null || origError === void 0 ? void 0 : origError.errorType) === driver_utils_1.DeltaStreamConnectionForbiddenError.errorType) {
377
+ connection = new NoDeltaStream();
378
+ requestedMode = "read";
379
+ break;
380
+ }
381
+ // Socket.io error when we connect to wrong socket, or hit some multiplexing bug
382
+ if (!driver_utils_1.canRetryOnError(origError)) {
383
+ const error = telemetry_utils_1.normalizeError(origError, { props: fatalConnectErrorProp });
384
+ this.props.closeHandler(error);
385
+ throw error;
386
+ }
387
+ // Log error once - we get too many errors in logs when we are offline,
388
+ // and unfortunately there is no reliable way to detect that.
389
+ if (connectRepeatCount === 1) {
390
+ driver_utils_1.logNetworkFailure(this.logger, {
391
+ delay: delayMs,
392
+ eventName: "DeltaConnectionFailureToConnect",
393
+ duration: telemetry_utils_1.TelemetryLogger.formatTick(common_utils_1.performance.now() - connectStartTime),
394
+ }, origError);
395
+ }
396
+ lastError = origError;
397
+ const retryDelayFromError = driver_utils_1.getRetryDelayFromError(origError);
398
+ delayMs = retryDelayFromError !== null && retryDelayFromError !== void 0 ? retryDelayFromError : Math.min(delayMs * 2, MaxReconnectDelayInMs);
399
+ if (retryDelayFromError !== undefined) {
400
+ this.props.reconnectionDelayHandler(retryDelayFromError, origError);
401
+ }
402
+ await driver_utils_1.waitForConnectedState(delayMs);
403
+ }
404
+ }
405
+ // If we retried more than once, log an event about how long it took
406
+ if (connectRepeatCount > 1) {
407
+ this.logger.sendTelemetryEvent({
408
+ eventName: "MultipleDeltaConnectionFailures",
409
+ attempts: connectRepeatCount,
410
+ duration: telemetry_utils_1.TelemetryLogger.formatTick(common_utils_1.performance.now() - connectStartTime),
411
+ }, lastError);
412
+ }
413
+ this.setupNewSuccessfulConnection(connection, requestedMode);
414
+ }
415
+ /**
416
+ * Start the connection. Any error should result in container being close.
417
+ * And report the error if it excape for any reason.
418
+ * @param args - The connection arguments
419
+ */
420
+ triggerConnect(connectionMode) {
421
+ common_utils_1.assert(this.connection === undefined, 0x239 /* "called only in disconnected state" */);
422
+ if (this.reconnectMode !== contracts_1.ReconnectMode.Enabled) {
423
+ return;
424
+ }
425
+ this.connect(connectionMode);
426
+ }
427
+ /**
428
+ * Disconnect the current connection.
429
+ * @param reason - Text description of disconnect reason to emit with disconnect event
430
+ */
431
+ disconnectFromDeltaStream(reason) {
432
+ this.pendingReconnect = false;
433
+ this.downgradedConnection = false;
434
+ if (this.connection === undefined) {
435
+ return false;
436
+ }
437
+ common_utils_1.assert(!this.pendingConnection, 0x27b /* "reentrancy may result in incorrect behavior" */);
438
+ const connection = this.connection;
439
+ // Avoid any re-entrancy - clear object reference
440
+ this.connection = undefined;
441
+ // Remove listeners first so we don't try to retrigger this flow accidentally through reconnectOnError
442
+ connection.off("op", this.opHandler);
443
+ connection.off("signal", this.props.signalHandler);
444
+ connection.off("nack", this.nackHandler);
445
+ connection.off("disconnect", this.disconnectHandlerInternal);
446
+ connection.off("error", this.errorHandler);
447
+ connection.off("pong", this.props.pongHandler);
448
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
449
+ this._outbound.pause();
450
+ this._outbound.clear();
451
+ this.props.disconnectHandler(reason);
452
+ connection.dispose();
453
+ this._connectionVerboseProps = {};
454
+ return true;
455
+ }
456
+ /**
457
+ * Once we've successfully gotten a connection, we need to set up state, attach event listeners, and process
458
+ * initial messages.
459
+ * @param connection - The newly established connection
460
+ */
461
+ setupNewSuccessfulConnection(connection, requestedMode) {
462
+ // Old connection should have been cleaned up before establishing a new one
463
+ common_utils_1.assert(this.connection === undefined, 0x0e6 /* "old connection exists on new connection setup" */);
464
+ common_utils_1.assert(!connection.disposed, 0x28a /* "can't be disposed - Callers need to ensure that!" */);
465
+ if (this.pendingConnection) {
466
+ this.pendingConnection = false;
467
+ }
468
+ else {
469
+ common_utils_1.assert(this.closed, 0x27f /* "reentrancy may result in incorrect behavior" */);
470
+ }
471
+ this.connection = connection;
472
+ // Does information in scopes & mode matches?
473
+ // If we asked for "write" and got "read", then file is read-only
474
+ // But if we ask read, server can still give us write.
475
+ const readonly = !connection.claims.scopes.includes(protocol_definitions_1.ScopeType.DocWrite);
476
+ // This connection mode validation logic is moving to the driver layer in 0.44. These two asserts can be
477
+ // removed after those packages have released and become ubiquitous.
478
+ common_utils_1.assert(requestedMode === "read" || readonly === (this.connectionMode === "read"), 0x0e7 /* "claims/connectionMode mismatch" */);
479
+ common_utils_1.assert(!readonly || this.connectionMode === "read", 0x0e8 /* "readonly perf with write connection" */);
480
+ this.set_readonlyPermissions(readonly);
481
+ if (this.closed) {
482
+ // Raise proper events, Log telemetry event and close connection.
483
+ this.disconnectFromDeltaStream("ConnectionManager already closed");
484
+ return;
485
+ }
486
+ this._outbound.resume();
487
+ connection.on("op", this.opHandler);
488
+ connection.on("signal", this.props.signalHandler);
489
+ connection.on("nack", this.nackHandler);
490
+ connection.on("disconnect", this.disconnectHandlerInternal);
491
+ connection.on("error", this.errorHandler);
492
+ connection.on("pong", this.props.pongHandler);
493
+ // Initial messages are always sorted. However, due to early op handler installed by drivers and appending those
494
+ // ops to initialMessages, resulting set is no longer sorted, which would result in client hitting storage to
495
+ // fill in gap. We will recover by cancelling this request once we process remaining ops, but it's a waste that
496
+ // we could avoid
497
+ const initialMessages = connection.initialMessages.sort((a, b) => a.sequenceNumber - b.sequenceNumber);
498
+ // Some storages may provide checkpointSequenceNumber to identify how far client is behind.
499
+ let checkpointSequenceNumber = connection.checkpointSequenceNumber;
500
+ this._connectionVerboseProps = {
501
+ clientId: connection.clientId,
502
+ mode: connection.mode,
503
+ };
504
+ // reset connection props
505
+ this._connectionProps = {};
506
+ if (connection.relayServiceAgent !== undefined) {
507
+ this._connectionVerboseProps.relayServiceAgent = connection.relayServiceAgent;
508
+ this._connectionProps.relayServiceAgent = connection.relayServiceAgent;
509
+ }
510
+ this._connectionProps.socketDocumentId = connection.claims.documentId;
511
+ this._connectionProps.connectionMode = connection.mode;
512
+ let last = -1;
513
+ if (initialMessages.length !== 0) {
514
+ this._connectionVerboseProps.connectionInitialOpsFrom = initialMessages[0].sequenceNumber;
515
+ last = initialMessages[initialMessages.length - 1].sequenceNumber;
516
+ this._connectionVerboseProps.connectionInitialOpsTo = last + 1;
517
+ // Update knowledge of how far we are behind, before raising "connect" event
518
+ // This is duplication of what incomingOpHandler() does, but we have to raise event before we get there,
519
+ // so duplicating update logic here as well.
520
+ if (checkpointSequenceNumber === undefined || checkpointSequenceNumber < last) {
521
+ checkpointSequenceNumber = last;
522
+ }
523
+ }
524
+ this.props.incomingOpHandler(initialMessages, this.connectFirstConnection ? "InitialOps" : "ReconnectOps");
525
+ if (connection.initialSignals !== undefined) {
526
+ for (const signal of connection.initialSignals) {
527
+ this.props.signalHandler(signal);
528
+ }
529
+ }
530
+ const details = ConnectionManager.detailsFromConnection(connection);
531
+ details.checkpointSequenceNumber = checkpointSequenceNumber;
532
+ this.props.connectHandler(details);
533
+ this.connectFirstConnection = false;
534
+ }
535
+ /**
536
+ * Disconnect the current connection and reconnect.
537
+ * @param connection - The connection that wants to reconnect - no-op if it's different from this.connection
538
+ * @param requestedMode - Read or write
539
+ * @param error - Error reconnect information including whether or not to reconnect
540
+ * @returns A promise that resolves when the connection is reestablished or we stop trying
541
+ */
542
+ reconnectOnError(requestedMode, error) {
543
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
544
+ this.reconnectOnErrorCore(requestedMode, error.message, error);
545
+ }
546
+ /**
547
+ * Disconnect the current connection and reconnect.
548
+ * @param connection - The connection that wants to reconnect - no-op if it's different from this.connection
549
+ * @param requestedMode - Read or write
550
+ * @param error - Error reconnect information including whether or not to reconnect
551
+ * @returns A promise that resolves when the connection is reestablished or we stop trying
552
+ */
553
+ async reconnectOnErrorCore(requestedMode, disconnectMessage, error) {
554
+ // We quite often get protocol errors before / after observing nack/disconnect
555
+ // we do not want to run through same sequence twice.
556
+ // If we're already disconnected/disconnecting it's not appropriate to call this again.
557
+ common_utils_1.assert(this.connection !== undefined, 0x0eb /* "Missing connection for reconnect" */);
558
+ this.disconnectFromDeltaStream(disconnectMessage);
559
+ const canRetry = error !== undefined ? driver_utils_1.canRetryOnError(error) : true;
560
+ // If reconnection is not an option, close the DeltaManager
561
+ if (!canRetry) {
562
+ this.props.closeHandler(telemetry_utils_1.normalizeError(error, { props: fatalConnectErrorProp }));
563
+ }
564
+ else if (this.reconnectMode === contracts_1.ReconnectMode.Never) {
565
+ // Do not raise container error if we are closing just because we lost connection.
566
+ // Those errors (like IdleDisconnect) would show up in telemetry dashboards and
567
+ // are very misleading, as first initial reaction - some logic is broken.
568
+ this.props.closeHandler();
569
+ }
570
+ // If closed then we can't reconnect
571
+ if (this.closed || this.reconnectMode !== contracts_1.ReconnectMode.Enabled) {
572
+ return;
573
+ }
574
+ const delayMs = error !== undefined ? driver_utils_1.getRetryDelayFromError(error) : undefined;
575
+ if (delayMs !== undefined) {
576
+ this.props.reconnectionDelayHandler(delayMs, error);
577
+ await driver_utils_1.waitForConnectedState(delayMs);
578
+ }
579
+ this.triggerConnect(requestedMode);
580
+ }
581
+ prepareMessageToSend(message) {
582
+ var _a, _b;
583
+ if (this.readonly === true) {
584
+ common_utils_1.assert(this.readOnlyInfo.readonly === true, 0x1f0 /* "Unexpected mismatch in readonly" */);
585
+ const error = new container_utils_1.GenericError("deltaManagerReadonlySubmit", undefined /* error */, {
586
+ readonly: this.readOnlyInfo.readonly,
587
+ forcedReadonly: this.readOnlyInfo.forced,
588
+ readonlyPermissions: this.readOnlyInfo.permissions,
589
+ storageOnly: this.readOnlyInfo.storageOnly,
590
+ });
591
+ this.props.closeHandler(error);
592
+ return undefined;
593
+ }
594
+ // reset clientSequenceNumber if we are using new clientId.
595
+ // we keep info about old connection as long as possible to be able to account for all non-acked ops
596
+ // that we pick up on next connection.
597
+ common_utils_1.assert(!!this.connection, 0x0e4 /* "Lost old connection!" */);
598
+ if (this.lastSubmittedClientId !== ((_a = this.connection) === null || _a === void 0 ? void 0 : _a.clientId)) {
599
+ this.lastSubmittedClientId = (_b = this.connection) === null || _b === void 0 ? void 0 : _b.clientId;
600
+ this.clientSequenceNumber = 0;
601
+ this.clientSequenceNumberObserved = 0;
602
+ }
603
+ if (message.type === protocol_definitions_1.MessageType.NoOp) {
604
+ this.trailingNoopCount++;
605
+ }
606
+ else {
607
+ this.trailingNoopCount = 0;
608
+ }
609
+ return Object.assign(Object.assign({}, message), { clientSequenceNumber: ++this.clientSequenceNumber });
610
+ }
611
+ submitSignal(content) {
612
+ if (this.connection !== undefined) {
613
+ this.connection.submitSignal(content);
614
+ }
615
+ else {
616
+ this.logger.sendErrorEvent({ eventName: "submitSignalDisconnected" });
617
+ }
618
+ }
619
+ sendMessages(messages) {
620
+ common_utils_1.assert(this.connected, 0x2b4 /* "not connected on sending ops!" */);
621
+ // If connection is "read" or implicit "read" (got leave op for "write" connection),
622
+ // then op can't make it through - we will get a nack if op is sent.
623
+ // We can short-circuit this process.
624
+ // Note that we also want nacks to be rare and be treated as catastrophic failures.
625
+ // Be careful with reentrancy though - disconnected event should not be be raised in the
626
+ // middle of the current workflow, but rather on clean stack!
627
+ if (this.connectionMode === "read" || this.downgradedConnection) {
628
+ if (!this.pendingReconnect) {
629
+ this.pendingReconnect = true;
630
+ Promise.resolve().then(async () => {
631
+ if (this.pendingReconnect) { // still valid?
632
+ await this.reconnectOnErrorCore("write", // connectionMode
633
+ "Switch to write");
634
+ }
635
+ })
636
+ .catch(() => { });
637
+ }
638
+ return;
639
+ }
640
+ common_utils_1.assert(!this.pendingReconnect, 0x2b5 /* "logic error" */);
641
+ this._outbound.push(messages);
642
+ }
643
+ beforeProcessingIncomingOp(message) {
644
+ // if we have connection, and message is local, then we better treat is as local!
645
+ common_utils_1.assert(this.clientId !== message.clientId || this.lastSubmittedClientId === message.clientId, 0x0ee /* "Not accounting local messages correctly" */);
646
+ if (this.lastSubmittedClientId !== undefined && this.lastSubmittedClientId === message.clientId) {
647
+ const clientSequenceNumber = message.clientSequenceNumber;
648
+ common_utils_1.assert(this.clientSequenceNumberObserved < clientSequenceNumber, 0x0ef /* "client seq# not growing" */);
649
+ common_utils_1.assert(clientSequenceNumber <= this.clientSequenceNumber, 0x0f0 /* "Incoming local client seq# > generated by this client" */);
650
+ this.clientSequenceNumberObserved = clientSequenceNumber;
651
+ }
652
+ if (message.type === protocol_definitions_1.MessageType.ClientLeave) {
653
+ const systemLeaveMessage = message;
654
+ const clientId = JSON.parse(systemLeaveMessage.data);
655
+ if (clientId === this.clientId) {
656
+ // We have been kicked out from quorum
657
+ this.logger.sendPerformanceEvent({ eventName: "ReadConnectionTransition" });
658
+ this.downgradedConnection = true;
659
+ }
660
+ }
661
+ }
662
+ }
663
+ exports.ConnectionManager = ConnectionManager;
664
+ //# sourceMappingURL=connectionManager.js.map