@fluidframework/container-loader 0.52.0 → 0.54.0-47413

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 (77) 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.d.ts.map +1 -1
  31. package/dist/packageVersion.js +1 -1
  32. package/dist/packageVersion.js.map +1 -1
  33. package/dist/protocolTreeDocumentStorageService.d.ts +2 -2
  34. package/dist/protocolTreeDocumentStorageService.d.ts.map +1 -1
  35. package/lib/connectionManager.d.ts +153 -0
  36. package/lib/connectionManager.d.ts.map +1 -0
  37. package/lib/connectionManager.js +660 -0
  38. package/lib/connectionManager.js.map +1 -0
  39. package/lib/connectionStateHandler.d.ts +1 -0
  40. package/lib/connectionStateHandler.d.ts.map +1 -1
  41. package/lib/connectionStateHandler.js +6 -0
  42. package/lib/connectionStateHandler.js.map +1 -1
  43. package/lib/container.d.ts +2 -22
  44. package/lib/container.d.ts.map +1 -1
  45. package/lib/container.js +122 -152
  46. package/lib/container.js.map +1 -1
  47. package/lib/containerContext.d.ts +1 -0
  48. package/lib/containerContext.d.ts.map +1 -1
  49. package/lib/containerContext.js +4 -0
  50. package/lib/containerContext.js.map +1 -1
  51. package/lib/contracts.d.ts +112 -0
  52. package/lib/contracts.d.ts.map +1 -0
  53. package/lib/contracts.js +11 -0
  54. package/lib/contracts.js.map +1 -0
  55. package/lib/deltaManager.d.ts +26 -142
  56. package/lib/deltaManager.d.ts.map +1 -1
  57. package/lib/deltaManager.js +147 -774
  58. package/lib/deltaManager.js.map +1 -1
  59. package/lib/loader.d.ts +14 -4
  60. package/lib/loader.d.ts.map +1 -1
  61. package/lib/loader.js +11 -5
  62. package/lib/loader.js.map +1 -1
  63. package/lib/packageVersion.d.ts +1 -1
  64. package/lib/packageVersion.d.ts.map +1 -1
  65. package/lib/packageVersion.js +1 -1
  66. package/lib/packageVersion.js.map +1 -1
  67. package/lib/protocolTreeDocumentStorageService.d.ts +2 -2
  68. package/lib/protocolTreeDocumentStorageService.d.ts.map +1 -1
  69. package/package.json +9 -9
  70. package/src/connectionManager.ts +892 -0
  71. package/src/connectionStateHandler.ts +8 -0
  72. package/src/container.ts +165 -187
  73. package/src/containerContext.ts +4 -0
  74. package/src/contracts.ts +156 -0
  75. package/src/deltaManager.ts +181 -978
  76. package/src/loader.ts +59 -27
  77. package/src/packageVersion.ts +1 -1
@@ -7,7 +7,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
7
7
  return (mod && mod.__esModule) ? mod : { "default": mod };
8
8
  };
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
- exports.DeltaManager = exports.ReconnectMode = void 0;
10
+ exports.DeltaManager = void 0;
11
11
  const abort_controller_1 = __importDefault(require("abort-controller"));
12
12
  const uuid_1 = require("uuid");
13
13
  const common_utils_1 = require("@fluidframework/common-utils");
@@ -18,78 +18,16 @@ const protocol_definitions_1 = require("@fluidframework/protocol-definitions");
18
18
  const driver_utils_1 = require("@fluidframework/driver-utils");
19
19
  const container_utils_1 = require("@fluidframework/container-utils");
20
20
  const deltaQueue_1 = require("./deltaQueue");
21
- const MaxReconnectDelayInMs = 8000;
22
- const InitialReconnectDelayInMs = 1000;
23
- const DefaultChunkSize = 16 * 1024;
24
- function getNackReconnectInfo(nackContent) {
25
- const message = `Nack (${nackContent.type}): ${nackContent.message}`;
26
- const canRetry = nackContent.code !== 403;
27
- const retryAfterMs = nackContent.retryAfter !== undefined ? nackContent.retryAfter * 1000 : undefined;
28
- return driver_utils_1.createGenericNetworkError(`nack [${nackContent.code}]`, message, canRetry, retryAfterMs, { statusCode: nackContent.code });
29
- }
30
- const createReconnectError = (fluidErrorCode, err) => telemetry_utils_1.wrapError(err, (errorMessage) => new driver_utils_1.GenericNetworkError(fluidErrorCode, errorMessage, true /* canRetry */));
31
- var ReconnectMode;
32
- (function (ReconnectMode) {
33
- ReconnectMode["Never"] = "Never";
34
- ReconnectMode["Disabled"] = "Disabled";
35
- ReconnectMode["Enabled"] = "Enabled";
36
- })(ReconnectMode = exports.ReconnectMode || (exports.ReconnectMode = {}));
37
- /**
38
- * Implementation of IDocumentDeltaConnection that does not support submitting
39
- * or receiving ops. Used in storage-only mode.
40
- */
41
- class NoDeltaStream extends common_utils_1.TypedEventEmitter {
42
- constructor() {
43
- super(...arguments);
44
- this.clientId = "storage-only client";
45
- this.claims = {
46
- scopes: [protocol_definitions_1.ScopeType.DocRead],
47
- };
48
- this.mode = "read";
49
- this.existing = true;
50
- this.maxMessageSize = 0;
51
- this.version = "";
52
- this.initialMessages = [];
53
- this.initialSignals = [];
54
- this.initialClients = [];
55
- this.serviceConfiguration = {
56
- maxMessageSize: 0,
57
- blockSize: 0,
58
- summary: undefined,
59
- };
60
- this.checkpointSequenceNumber = undefined;
61
- this._disposed = false;
62
- }
63
- submit(messages) {
64
- this.emit("nack", this.clientId, messages.map((operation) => {
65
- return {
66
- operation,
67
- content: { message: "Cannot submit with storage-only connection", code: 403 },
68
- };
69
- }));
70
- }
71
- submitSignal(message) {
72
- this.emit("nack", this.clientId, {
73
- operation: message,
74
- content: { message: "Cannot submit signal with storage-only connection", code: 403 },
75
- });
76
- }
77
- get disposed() { return this._disposed; }
78
- dispose() { this._disposed = true; }
79
- }
80
21
  /**
81
22
  * Manages the flow of both inbound and outbound messages. This class ensures that shared objects receive delta
82
23
  * messages in order regardless of possible network conditions or timings causing out of order delivery.
83
24
  */
84
25
  class DeltaManager extends common_utils_1.TypedEventEmitter {
85
- constructor(serviceProvider, client, logger, reconnectAllowed, _active) {
26
+ constructor(serviceProvider, logger, _active, createConnectionManager) {
86
27
  super();
87
28
  this.serviceProvider = serviceProvider;
88
- this.client = client;
89
29
  this.logger = logger;
90
30
  this._active = _active;
91
- // tracks host requiring read-only mode.
92
- this._forceReadonly = false;
93
31
  this.pending = [];
94
32
  // The minimum sequence number and last sequence number received from the server
95
33
  this.minSequenceNumber = 0;
@@ -106,82 +44,30 @@ class DeltaManager extends common_utils_1.TypedEventEmitter {
106
44
  this.baseTerm = 0;
107
45
  // The sequence number we initially loaded from
108
46
  this.initSequenceNumber = 0;
109
- this.clientSequenceNumber = 0;
110
- this.clientSequenceNumberObserved = 0;
111
- // Counts the number of noops sent by the client which may not be acked.
112
- this.trailingNoopCount = 0;
113
47
  this.closed = false;
114
- this.deltaStreamDelayId = uuid_1.v4();
115
- this.deltaStorageDelayId = uuid_1.v4();
116
- this.messageBuffer = [];
117
- this.connectFirstConnection = true;
118
48
  this.throttlingIdSet = new Set();
119
49
  this.timeTillThrottling = 0;
120
- this.connectionStateProps = {};
121
- // True if current connection has checkpoint information
122
- // I.e. we know how far behind the client was at the time of establishing connection
123
- this._hasCheckpointSequenceNumber = false;
124
50
  this.closeAbortController = new abort_controller_1.default();
125
- // True if there is pending (async) reconnection from "read" to "write"
126
- this.pendingReconnect = false;
127
- // downgrade "write" connection to "read"
128
- this.downgradedConnection = false;
129
- this.opHandler = (documentId, messagesArg) => {
130
- const messages = Array.isArray(messagesArg) ? messagesArg : [messagesArg];
131
- this.enqueueMessages(messages, "opHandler");
132
- };
133
- this.signalHandler = (message) => {
134
- this._inboundSignal.push(message);
135
- };
136
- // Always connect in write mode after getting nacked.
137
- this.nackHandler = (documentId, messages) => {
138
- const message = messages[0];
139
- // TODO: we should remove this check when service updates?
140
- // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
141
- if (this._readonlyPermissions) {
142
- this.close(driver_utils_1.createWriteError("writeOnReadOnlyDocument"));
143
- }
144
- // check message.content for Back-compat with old service.
145
- const reconnectInfo = message.content !== undefined
146
- ? getNackReconnectInfo(message.content) :
147
- driver_utils_1.createGenericNetworkError("nackReasonUnknown", undefined, true);
148
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
149
- this.reconnectOnError("write", reconnectInfo);
150
- };
151
- // Connection mode is always read on disconnect/error unless the system mode was write.
152
- this.disconnectHandler = (disconnectReason) => {
153
- // Note: we might get multiple disconnect calls on same socket, as early disconnect notification
154
- // ("server_disconnect", ODSP-specific) is mapped to "disconnect"
155
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
156
- this.reconnectOnError(this.defaultReconnectionMode, createReconnectError("dmDocumentDeltaConnectionDisconnected", disconnectReason));
157
- };
158
- this.errorHandler = (error) => {
159
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
160
- this.reconnectOnError(this.defaultReconnectionMode, createReconnectError("dmDocumentDeltaConnectionError", error));
161
- };
162
- this.pongHandler = (latency) => {
163
- this.emit("pong", latency);
51
+ this.deltaStorageDelayId = uuid_1.v4();
52
+ this.deltaStreamDelayId = uuid_1.v4();
53
+ this.messageBuffer = [];
54
+ const props = {
55
+ incomingOpHandler: (messages, reason) => this.enqueueMessages(messages, reason),
56
+ signalHandler: (message) => this._inboundSignal.push(message),
57
+ reconnectionDelayHandler: (delayMs, error) => this.emitDelayInfo(this.deltaStreamDelayId, delayMs, error),
58
+ closeHandler: (error) => this.close(error),
59
+ disconnectHandler: (reason) => this.disconnectHandler(reason),
60
+ connectHandler: (connection) => this.connectHandler(connection),
61
+ pongHandler: (latency) => this.emit("pong", latency),
62
+ readonlyChangeHandler: (readonly) => telemetry_utils_1.safeRaiseEvent(this, this.logger, "readonly", readonly),
164
63
  };
165
- this.clientDetails = this.client.details;
166
- this.defaultReconnectionMode = this.client.mode;
167
- this._reconnectMode = reconnectAllowed ? ReconnectMode.Enabled : ReconnectMode.Never;
64
+ this.connectionManager = createConnectionManager(props);
168
65
  this._inbound = new deltaQueue_1.DeltaQueue((op) => {
169
66
  this.processInboundMessage(op);
170
67
  });
171
68
  this._inbound.on("error", (error) => {
172
69
  this.close(container_utils_1.CreateProcessingError(error, "deltaManagerInboundErrorHandler", this.lastMessage));
173
70
  });
174
- // Outbound message queue. The outbound queue is represented as a queue of an array of ops. Ops contained
175
- // within an array *must* fit within the maxMessageSize and are guaranteed to be ordered sequentially.
176
- this._outbound = new deltaQueue_1.DeltaQueue((messages) => {
177
- if (this.connection === undefined) {
178
- throw new Error("Attempted to submit an outbound message without connection");
179
- }
180
- this.connection.submit(messages);
181
- });
182
- this._outbound.on("error", (error) => {
183
- this.close(telemetry_utils_1.normalizeError(error));
184
- });
185
71
  // Inbound signal queue
186
72
  this._inboundSignal = new deltaQueue_1.DeltaQueue((message) => {
187
73
  if (this.handler === undefined) {
@@ -202,21 +88,9 @@ class DeltaManager extends common_utils_1.TypedEventEmitter {
202
88
  get active() { return this._active(); }
203
89
  get disposed() { return this.closed; }
204
90
  get IDeltaSender() { return this; }
205
- /**
206
- * Tells if current connection has checkpoint information.
207
- * I.e. we know how far behind the client was at the time of establishing connection
208
- */
209
- get hasCheckpointSequenceNumber() {
210
- // Valid to be called only if we have active connection.
211
- common_utils_1.assert(this.connection !== undefined, 0x0df /* "Missing active connection" */);
212
- return this._hasCheckpointSequenceNumber;
213
- }
214
91
  get inbound() {
215
92
  return this._inbound;
216
93
  }
217
- get outbound() {
218
- return this._outbound;
219
- }
220
94
  get inboundSignal() {
221
95
  return this._inboundSignal;
222
96
  }
@@ -238,39 +112,22 @@ class DeltaManager extends common_utils_1.TypedEventEmitter {
238
112
  get minimumSequenceNumber() {
239
113
  return this.minSequenceNumber;
240
114
  }
241
- get maxMessageSize() {
242
- var _a, _b, _c;
243
- 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;
244
- }
245
- get version() {
246
- if (this.connection === undefined) {
247
- throw new Error("Cannot check version without a connection");
248
- }
249
- return this.connection.version;
250
- }
251
- get serviceConfiguration() {
252
- var _a;
253
- return (_a = this.connection) === null || _a === void 0 ? void 0 : _a.serviceConfiguration;
254
- }
255
- get scopes() {
256
- var _a;
257
- return (_a = this.connection) === null || _a === void 0 ? void 0 : _a.claims.scopes;
258
- }
259
- get socketDocumentId() {
260
- var _a;
261
- return (_a = this.connection) === null || _a === void 0 ? void 0 : _a.claims.documentId;
262
- }
263
115
  /**
264
- * The current connection mode, initially read.
116
+ * Tells if current connection has checkpoint information.
117
+ * I.e. we know how far behind the client was at the time of establishing connection
265
118
  */
266
- get connectionMode() {
267
- var _a;
268
- 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?" */);
269
- if (this.connection === undefined || this.downgradedConnection) {
270
- return "read";
271
- }
272
- return this.connection.mode;
273
- }
119
+ get hasCheckpointSequenceNumber() {
120
+ // Valid to be called only if we have active connection.
121
+ common_utils_1.assert(this.connectionManager.connected, 0x0df /* "Missing active connection" */);
122
+ return this._checkpointSequenceNumber !== undefined;
123
+ }
124
+ // Forwarding connection manager properties / IDeltaManager implementation
125
+ get maxMessageSize() { return this.connectionManager.maxMessageSize; }
126
+ get version() { return this.connectionManager.version; }
127
+ get serviceConfiguration() { return this.connectionManager.serviceConfiguration; }
128
+ get outbound() { return this.connectionManager.outbound; }
129
+ get readOnlyInfo() { return this.connectionManager.readOnlyInfo; }
130
+ get clientDetails() { return this.connectionManager.clientDetails; }
274
131
  /**
275
132
  * Tells if container is in read-only mode.
276
133
  * Data stores should listen for "readonly" notifications and disallow user
@@ -282,118 +139,50 @@ class DeltaManager extends common_utils_1.TypedEventEmitter {
282
139
  * @deprecated - use readOnlyInfo
283
140
  */
284
141
  get readonly() {
285
- if (this._forceReadonly) {
286
- return true;
287
- }
288
- return this._readonlyPermissions;
289
- }
290
- /**
291
- * Tells if user has no write permissions for file in storage
292
- * It is undefined if we have not yet established websocket connection
293
- * and do not know if user has write access to a file.
294
- * @deprecated - use readOnlyInfo
295
- */
296
- get readonlyPermissions() {
297
- return this._readonlyPermissions;
298
- }
299
- get readOnlyInfo() {
300
- const storageOnly = this.connection !== undefined && this.connection instanceof NoDeltaStream;
301
- if (storageOnly || this._forceReadonly || this._readonlyPermissions === true) {
302
- return {
303
- readonly: true,
304
- forced: this._forceReadonly,
305
- permissions: this._readonlyPermissions,
306
- storageOnly,
307
- };
308
- }
309
- return { readonly: this._readonlyPermissions };
310
- }
311
- /**
312
- * Automatic reconnecting enabled or disabled.
313
- * If set to Never, then reconnecting will never be allowed.
314
- */
315
- get reconnectMode() {
316
- return this._reconnectMode;
142
+ return this.readOnlyInfo.readonly;
317
143
  }
318
- shouldJoinWrite() {
319
- // We don't have to wait for ack for topmost NoOps. So subtract those.
320
- return this.clientSequenceNumberObserved < (this.clientSequenceNumber - this.trailingNoopCount);
321
- }
322
- /**
323
- * Returns set of props that can be logged in telemetry that provide some insights / statistics
324
- * about current or last connection (if there is no connection at the moment)
325
- */
326
- connectionProps() {
327
- const common = {
328
- sequenceNumber: this.lastSequenceNumber,
144
+ submit(type, contents, batch = false, metadata) {
145
+ // Start adding trace for the op.
146
+ const traces = [
147
+ {
148
+ action: "start",
149
+ service: "client",
150
+ timestamp: Date.now(),
151
+ }
152
+ ];
153
+ const messagePartial = {
154
+ contents: JSON.stringify(contents),
155
+ metadata,
156
+ referenceSequenceNumber: this.lastProcessedSequenceNumber,
157
+ traces,
158
+ type,
329
159
  };
330
- if (this.connection !== undefined) {
331
- return Object.assign(Object.assign({}, common), { connectionMode: this.connectionMode });
160
+ if (!batch) {
161
+ this.flush();
332
162
  }
333
- else {
334
- return Object.assign(Object.assign({}, common), {
335
- // Report how many ops this client sent in last disconnected session
336
- sentOps: this.clientSequenceNumber });
163
+ const message = this.connectionManager.prepareMessageToSend(messagePartial);
164
+ if (message === undefined) {
165
+ return -1;
337
166
  }
338
- }
339
- /**
340
- * Enables or disables automatic reconnecting.
341
- * Will throw an error if reconnectMode set to Never.
342
- */
343
- setAutoReconnect(mode) {
344
- common_utils_1.assert(mode !== ReconnectMode.Never && this._reconnectMode !== ReconnectMode.Never, 0x278 /* "API is not supported for non-connecting or closed container" */);
345
- this._reconnectMode = mode;
346
- if (mode !== ReconnectMode.Enabled) {
347
- // immediately disconnect - do not rely on service eventually dropping connection.
348
- this.disconnectFromDeltaStream("setAutoReconnect");
167
+ this.messageBuffer.push(message);
168
+ if (!batch) {
169
+ this.flush();
349
170
  }
171
+ this.emit("submitOp", message);
172
+ return message.clientSequenceNumber;
350
173
  }
351
- /**
352
- * Sends signal to runtime (and data stores) to be read-only.
353
- * Hosts may have read only views, indicating to data stores that no edits are allowed.
354
- * This is independent from this._readonlyPermissions (permissions) and this.connectionMode
355
- * (server can return "write" mode even when asked for "read")
356
- * Leveraging same "readonly" event as runtime & data stores should behave the same in such case
357
- * as in read-only permissions.
358
- * But this.active can be used by some DDSes to figure out if ops can be sent
359
- * (for example, read-only view still participates in code proposals / upgrades decisions)
360
- *
361
- * Forcing Readonly does not prevent DDS from generating ops. It is up to user code to honour
362
- * the readonly flag. If ops are generated, they will accumulate locally and not be sent. If
363
- * there are pending in the outbound queue, it will stop sending until force readonly is
364
- * cleared.
365
- *
366
- * @param readonly - set or clear force readonly.
367
- */
368
- forceReadonly(readonly) {
369
- if (readonly !== this._forceReadonly) {
370
- this.logger.sendTelemetryEvent({
371
- eventName: "ForceReadOnly",
372
- value: readonly,
373
- });
374
- }
375
- const oldValue = this.readonly;
376
- this._forceReadonly = readonly;
377
- if (oldValue !== this.readonly) {
378
- common_utils_1.assert(this._reconnectMode !== ReconnectMode.Never, 0x279 /* "API is not supported for non-connecting or closed container" */);
379
- let reconnect = false;
380
- if (this.readonly === true) {
381
- // If we switch to readonly while connected, we should disconnect first
382
- // See comment in the "readonly" event handler to deltaManager set up by
383
- // the ContainerRuntime constructor
384
- if (this.shouldJoinWrite()) {
385
- // If we have pending changes, then we will never send them - it smells like
386
- // host logic error.
387
- this.logger.sendErrorEvent({ eventName: "ForceReadonlyPendingChanged" });
388
- }
389
- reconnect = this.disconnectFromDeltaStream("Force readonly");
390
- }
391
- telemetry_utils_1.safeRaiseEvent(this, this.logger, "readonly", this.readonly);
392
- if (reconnect) {
393
- // reconnect if we disconnected from before.
394
- this.triggerConnect({ reason: "forceReadonly", mode: "read", fetchOpsFromStorage: false });
395
- }
174
+ submitSignal(content) { return this.connectionManager.submitSignal(content); }
175
+ flush() {
176
+ if (this.messageBuffer.length === 0) {
177
+ return;
396
178
  }
179
+ // The prepareFlush event allows listeners to append metadata to the batch prior to submission.
180
+ this.emit("prepareSend", this.messageBuffer);
181
+ this.connectionManager.sendMessages(this.messageBuffer);
182
+ this.messageBuffer = [];
183
+ }
184
+ get connectionProps() {
185
+ return Object.assign({ sequenceNumber: this.lastSequenceNumber }, this.connectionManager.connectionProps);
397
186
  }
398
187
  /**
399
188
  * Log error event with a bunch of internal to DeltaManager information about state of op processing
@@ -403,20 +192,46 @@ class DeltaManager extends common_utils_1.TypedEventEmitter {
403
192
  */
404
193
  logConnectionIssue(event) {
405
194
  var _a;
406
- common_utils_1.assert(this.connection !== undefined, 0x238 /* "called only in connected state" */);
195
+ common_utils_1.assert(this.connectionManager.connected, 0x238 /* "called only in connected state" */);
407
196
  const pendingSorted = this.pending.sort((a, b) => a.sequenceNumber - b.sequenceNumber);
408
197
  this.logger.sendErrorEvent(Object.assign(Object.assign(Object.assign(Object.assign({}, event), {
409
198
  // This directly tells us if fetching ops is in flight, and thus likely the reason of
410
199
  // stalled op processing
411
200
  fetchReason: this.fetchReason,
412
201
  // A bunch of useful sequence numbers to understand if we are holding some ops from processing
413
- lastQueuedSequenceNumber: this.lastQueuedSequenceNumber, lastProcessedSequenceNumber: this.lastProcessedSequenceNumber, lastObserved: this.lastObservedSeqNumber }), this.connectionStateProps), { pendingOps: this.pending.length, pendingFirst: (_a = pendingSorted[0]) === null || _a === void 0 ? void 0 : _a.sequenceNumber, haveHandler: this.handler !== undefined, inboundLength: this.inbound.length, inboundPaused: this.inbound.paused }));
202
+ lastQueuedSequenceNumber: this.lastQueuedSequenceNumber, lastProcessedSequenceNumber: this.lastProcessedSequenceNumber, lastObserved: this.lastObservedSeqNumber }), this.connectionManager.connectionVerboseProps), { pendingOps: this.pending.length, pendingFirst: (_a = pendingSorted[0]) === null || _a === void 0 ? void 0 : _a.sequenceNumber, haveHandler: this.handler !== undefined, inboundLength: this.inbound.length, inboundPaused: this.inbound.paused }));
414
203
  }
415
- set_readonlyPermissions(readonly) {
416
- const oldValue = this.readonly;
417
- this._readonlyPermissions = readonly;
418
- if (oldValue !== this.readonly) {
419
- telemetry_utils_1.safeRaiseEvent(this, this.logger, "readonly", this.readonly);
204
+ connectHandler(connection) {
205
+ this.refreshDelayInfo(this.deltaStreamDelayId);
206
+ const props = this.connectionManager.connectionVerboseProps;
207
+ props.connectionLastQueuedSequenceNumber = this.lastQueuedSequenceNumber;
208
+ props.connectionLastObservedSeqNumber = this.lastObservedSeqNumber;
209
+ const checkpointSequenceNumber = connection.checkpointSequenceNumber;
210
+ this._checkpointSequenceNumber = checkpointSequenceNumber;
211
+ if (checkpointSequenceNumber !== undefined) {
212
+ this.updateLatestKnownOpSeqNumber(checkpointSequenceNumber);
213
+ }
214
+ // We cancel all ops on lost of connectivity, and rely on DDSes to resubmit them.
215
+ // Semantics are not well defined for batches (and they are broken right now on disconnects anyway),
216
+ // but it's safe to assume (until better design is put into place) that batches should not exist
217
+ // across multiple connections. Right now we assume runtime will not submit any ops in disconnected
218
+ // state. As requirements change, so should these checks.
219
+ common_utils_1.assert(this.messageBuffer.length === 0, 0x0e9 /* "messageBuffer is not empty on new connection" */);
220
+ this.emit("connect", connection, checkpointSequenceNumber !== undefined ?
221
+ this.lastObservedSeqNumber - this.lastSequenceNumber : undefined);
222
+ // If we got some initial ops, then we know the gap and call above fetched ops to fill it.
223
+ // Same is true for "write" mode even if we have no ops - we will get "join" own op very very soon.
224
+ // However if we are connecting as view-only, then there is no good signal to realize if client is behind.
225
+ // Thus we have to hit storage to see if any ops are there.
226
+ if (checkpointSequenceNumber !== undefined) {
227
+ // We know how far we are behind (roughly). If it's non-zero gap, fetch ops right away.
228
+ if (checkpointSequenceNumber > this.lastQueuedSequenceNumber) {
229
+ this.fetchMissingDeltas("AfterConnection");
230
+ }
231
+ // we do not know the gap, and we will not learn about it if socket is quite - have to ask.
232
+ }
233
+ else if (connection.mode === "read") {
234
+ this.fetchMissingDeltas("AfterReadConnection");
420
235
  }
421
236
  }
422
237
  dispose() {
@@ -457,66 +272,16 @@ class DeltaManager extends common_utils_1.TypedEventEmitter {
457
272
  // (which in most cases will happen when we are done processing cached ops)
458
273
  if (cacheOnly) {
459
274
  // fire and forget
460
- this.fetchMissingDeltas("DocumentOpen", this.lastQueuedSequenceNumber);
275
+ this.fetchMissingDeltas("DocumentOpen");
461
276
  }
462
277
  }
463
278
  // Ensure there is no need to call this.processPendingOps() at the end of boot sequence
464
279
  common_utils_1.assert(this.fetchReason !== undefined || this.pending.length === 0, 0x269 /* "pending ops are not dropped" */);
465
280
  }
466
- static detailsFromConnection(connection) {
467
- return {
468
- claims: connection.claims,
469
- clientId: connection.clientId,
470
- existing: connection.existing,
471
- checkpointSequenceNumber: connection.checkpointSequenceNumber,
472
- get initialClients() { return connection.initialClients; },
473
- maxMessageSize: connection.serviceConfiguration.maxMessageSize,
474
- mode: connection.mode,
475
- serviceConfiguration: connection.serviceConfiguration,
476
- version: connection.version,
477
- };
478
- }
479
- async connect(args) {
480
- const connection = await this.connectCore(args);
481
- return DeltaManager.detailsFromConnection(connection);
482
- }
483
- /**
484
- * Start the connection. Any error should result in container being close.
485
- * And report the error if it excape for any reason.
486
- * @param args - The connection arguments
487
- */
488
- triggerConnect(args) {
489
- common_utils_1.assert(this.connection === undefined, 0x239 /* "called only in disconnected state" */);
490
- if (this.reconnectMode !== ReconnectMode.Enabled) {
491
- return;
492
- }
493
- this.connectCore(args).catch((err) => {
494
- // Errors are raised as "error" event and close container.
495
- // Have a catch-all case in case we missed something
496
- if (!this.closed) {
497
- this.logger.sendErrorEvent({ eventName: "ConnectException" }, err);
498
- }
499
- });
500
- }
501
- async connectCore(args) {
502
- var _a, _b, _c;
503
- common_utils_1.assert(!this.closed, 0x26a /* "not closed" */);
504
- if (this.connection !== undefined) {
505
- return this.connection;
506
- }
507
- if (this.connectionP !== undefined) {
508
- return this.connectionP;
509
- }
281
+ connect(args) {
282
+ var _a;
510
283
  const fetchOpsFromStorage = (_a = args.fetchOpsFromStorage) !== null && _a !== void 0 ? _a : true;
511
- let requestedMode = (_b = args.mode) !== null && _b !== void 0 ? _b : this.defaultReconnectionMode;
512
- // if we have any non-acked ops from last connection, reconnect as "write".
513
- // without that we would connect in view-only mode, which will result in immediate
514
- // firing of "connected" event from Container and switch of current clientId (as tracked
515
- // by all DDSes). This will make it impossible to figure out if ops actually made it through,
516
- // so DDSes will immediately resubmit all pending ops, and some of them will be duplicates, corrupting document
517
- if (this.shouldJoinWrite()) {
518
- requestedMode = "write";
519
- }
284
+ telemetry_utils_1.logIfFalse(this.handler !== undefined || !fetchOpsFromStorage, this.logger, "CantFetchWithoutBaseline"); // can't fetch if no baseline
520
285
  // Note: There is race condition here.
521
286
  // We want to issue request to storage as soon as possible, to
522
287
  // reduce latency of becoming current, thus this code here.
@@ -526,214 +291,11 @@ class DeltaManager extends common_utils_1.TypedEventEmitter {
526
291
  // own "join" message and realize any gap client has in ops.
527
292
  // But for view-only connection, we have no such signal, and with no traffic
528
293
  // on the wire, we might be always behind.
529
- // See comment at the end of setupNewSuccessfulConnection()
530
- telemetry_utils_1.logIfFalse(this.handler !== undefined || !fetchOpsFromStorage, this.logger, "CantFetchWithoutBaseline"); // can't fetch if no baseline
294
+ // See comment at the end of "connect" handler
531
295
  if (fetchOpsFromStorage) {
532
- this.fetchMissingDeltas(args.reason, this.lastQueuedSequenceNumber);
533
- }
534
- const docService = this.serviceProvider();
535
- if (docService === undefined) {
536
- throw new Error("Container is not attached");
537
- }
538
- if (((_c = docService.policies) === null || _c === void 0 ? void 0 : _c.storageOnly) === true) {
539
- const connection = new NoDeltaStream();
540
- this.connectionP = Promise.resolve(connection); // to keep setupNewSuccessfulConnection happy
541
- this.setupNewSuccessfulConnection(connection, "read");
542
- return connection;
543
- }
544
- // The promise returned from connectCore will settle with a resolved connection or reject with error
545
- const connectCore = async () => {
546
- let connection;
547
- let delayMs = InitialReconnectDelayInMs;
548
- let connectRepeatCount = 0;
549
- const connectStartTime = common_utils_1.performance.now();
550
- let lastError;
551
- // This loop will keep trying to connect until successful, with a delay between each iteration.
552
- while (connection === undefined) {
553
- if (this.closed) {
554
- throw new Error("Attempting to connect a closed DeltaManager");
555
- }
556
- connectRepeatCount++;
557
- try {
558
- this.client.mode = requestedMode;
559
- connection = await docService.connectToDeltaStream(this.client);
560
- if (connection.disposed) {
561
- // Nobody observed this connection, so drop it on the floor and retry.
562
- this.logger.sendTelemetryEvent({ eventName: "ReceivedClosedConnection" });
563
- connection = undefined;
564
- }
565
- }
566
- catch (origError) {
567
- if (typeof origError === "object" && origError !== null &&
568
- (origError === null || origError === void 0 ? void 0 : origError.errorType) === driver_utils_1.DeltaStreamConnectionForbiddenError.errorType) {
569
- connection = new NoDeltaStream();
570
- requestedMode = "read";
571
- break;
572
- }
573
- // Socket.io error when we connect to wrong socket, or hit some multiplexing bug
574
- if (!driver_utils_1.canRetryOnError(origError)) {
575
- const error = telemetry_utils_1.normalizeError(origError);
576
- this.close(error);
577
- throw error;
578
- }
579
- // Log error once - we get too many errors in logs when we are offline,
580
- // and unfortunately there is no reliable way to detect that.
581
- if (connectRepeatCount === 1) {
582
- driver_utils_1.logNetworkFailure(this.logger, {
583
- delay: delayMs,
584
- eventName: "DeltaConnectionFailureToConnect",
585
- duration: telemetry_utils_1.TelemetryLogger.formatTick(common_utils_1.performance.now() - connectStartTime),
586
- }, origError);
587
- }
588
- lastError = origError;
589
- const retryDelayFromError = driver_utils_1.getRetryDelayFromError(origError);
590
- delayMs = retryDelayFromError !== null && retryDelayFromError !== void 0 ? retryDelayFromError : Math.min(delayMs * 2, MaxReconnectDelayInMs);
591
- if (retryDelayFromError !== undefined) {
592
- this.emitDelayInfo(this.deltaStreamDelayId, retryDelayFromError, origError);
593
- }
594
- await driver_utils_1.waitForConnectedState(delayMs);
595
- }
596
- }
597
- // If we retried more than once, log an event about how long it took
598
- if (connectRepeatCount > 1) {
599
- this.logger.sendTelemetryEvent({
600
- eventName: "MultipleDeltaConnectionFailures",
601
- attempts: connectRepeatCount,
602
- duration: telemetry_utils_1.TelemetryLogger.formatTick(common_utils_1.performance.now() - connectStartTime),
603
- }, lastError);
604
- }
605
- this.setupNewSuccessfulConnection(connection, requestedMode);
606
- return connection;
607
- };
608
- // This promise settles as soon as we know the outcome of the connection attempt
609
- // Set it upfront, such that if connection is established (NoDeltaConnection) or rejected (bug in
610
- // connectToDeltaStream() implementation - throwing exception vs. returning rejected promise) in
611
- // synchronous way, we have this.connectionP setup for all the code to assert correctness of the flow.
612
- const deferred = new common_utils_1.Deferred();
613
- this.connectionP = deferred.promise;
614
- // Regardless of how the connection attempt concludes, we'll clear the promise and remove the listener
615
- // Reject the connection promise if the DeltaManager gets closed during connection
616
- const cleanupAndReject = (error) => {
617
- this.connectionP = undefined;
618
- this.removeListener("closed", cleanupAndReject);
619
- // This error came from some logic error in this file. Fail-fast to learn and fix the issue faster
620
- this.close(error);
621
- deferred.reject(error);
622
- };
623
- this.on("closed", cleanupAndReject);
624
- // Attempt the connection
625
- connectCore().then((connection) => {
626
- this.removeListener("closed", cleanupAndReject);
627
- deferred.resolve(connection);
628
- }).catch(cleanupAndReject);
629
- return this.connectionP;
630
- }
631
- flush() {
632
- if (this.messageBuffer.length === 0) {
633
- return;
634
- }
635
- // The prepareFlush event allows listeners to append metadata to the batch prior to submission.
636
- this.emit("prepareSend", this.messageBuffer);
637
- this._outbound.push(this.messageBuffer);
638
- this.messageBuffer = [];
639
- }
640
- /**
641
- * Submits the given delta returning the client sequence number for the message. Contents is the actual
642
- * contents of the message. appData is optional metadata that can be attached to the op by the app.
643
- *
644
- * If batch is set to true then the submit will be batched - and as a result guaranteed to be ordered sequentially
645
- * in the global sequencing space. The batch will be flushed either when flush is called or when a non-batched
646
- * op is submitted.
647
- */
648
- submit(type, contents, batch = false, metadata) {
649
- // TODO need to fail if gets too large
650
- // const serializedContent = JSON.stringify(this.messageBuffer);
651
- // const maxOpSize = this.context.deltaManager.maxMessageSize;
652
- var _a, _b;
653
- if (this.readonly === true) {
654
- common_utils_1.assert(this.readOnlyInfo.readonly === true, 0x1f0 /* "Unexpected mismatch in readonly" */);
655
- const error = new container_utils_1.GenericError("deltaManagerReadonlySubmit", undefined /* error */, {
656
- readonly: this.readOnlyInfo.readonly,
657
- forcedReadonly: this.readOnlyInfo.forced,
658
- readonlyPermissions: this.readOnlyInfo.permissions,
659
- storageOnly: this.readOnlyInfo.storageOnly,
660
- });
661
- this.close(error);
662
- return -1;
663
- }
664
- // reset clientSequenceNumber if we are using new clientId.
665
- // we keep info about old connection as long as possible to be able to account for all non-acked ops
666
- // that we pick up on next connection.
667
- common_utils_1.assert(!!this.connection, 0x0e4 /* "Lost old connection!" */);
668
- if (this.lastSubmittedClientId !== ((_a = this.connection) === null || _a === void 0 ? void 0 : _a.clientId)) {
669
- this.lastSubmittedClientId = (_b = this.connection) === null || _b === void 0 ? void 0 : _b.clientId;
670
- this.clientSequenceNumber = 0;
671
- this.clientSequenceNumberObserved = 0;
672
- }
673
- // If connection is "read" or implicit "read" (got leave op for "write" connection),
674
- // then op can't make it through - we will get a nack if op is sent.
675
- // We can short-circuit this process.
676
- // Note that we also want nacks to be rare and be treated as catastrophic failures.
677
- // Be careful with reentrancy though - disconnected event should not be be raised in the
678
- // middle of the current workflow, but rather on clean stack!
679
- if (this.connectionMode === "read") {
680
- if (!this.pendingReconnect) {
681
- this.pendingReconnect = true;
682
- Promise.resolve().then(async () => {
683
- if (this.pendingReconnect) { // still valid?
684
- return this.reconnectOnErrorCore("write", // connectionMode
685
- "Switch to write");
686
- }
687
- })
688
- .catch(() => { });
689
- }
690
- // Can return -1 here, but no other path does it (other than error path in Container),
691
- // so it's better not to introduce new states.
692
- return ++this.clientSequenceNumber;
693
- }
694
- const service = this.clientDetails.type === undefined || this.clientDetails.type === ""
695
- ? "unknown"
696
- : this.clientDetails.type;
697
- // Start adding trace for the op.
698
- const traces = [
699
- {
700
- action: "start",
701
- service,
702
- timestamp: Date.now(),
703
- }
704
- ];
705
- const message = {
706
- clientSequenceNumber: ++this.clientSequenceNumber,
707
- contents: JSON.stringify(contents),
708
- metadata,
709
- referenceSequenceNumber: this.lastProcessedSequenceNumber,
710
- traces,
711
- type,
712
- };
713
- if (type === protocol_definitions_1.MessageType.NoOp) {
714
- this.trailingNoopCount++;
715
- }
716
- else {
717
- this.trailingNoopCount = 0;
718
- }
719
- this.emit("submitOp", message);
720
- if (!batch) {
721
- this.flush();
722
- this.messageBuffer.push(message);
723
- this.flush();
724
- }
725
- else {
726
- this.messageBuffer.push(message);
727
- }
728
- return message.clientSequenceNumber;
729
- }
730
- submitSignal(content) {
731
- if (this.connection !== undefined) {
732
- this.connection.submitSignal(content);
733
- }
734
- else {
735
- this.logger.sendErrorEvent({ eventName: "submitSignalDisconnected" });
296
+ this.fetchMissingDeltas(args.reason);
736
297
  }
298
+ this.connectionManager.connect(args.mode);
737
299
  }
738
300
  async getDeltas(from, // inclusive
739
301
  to, // exclusive
@@ -745,9 +307,6 @@ class DeltaManager extends common_utils_1.TypedEventEmitter {
745
307
  if (this.deltaStorage === undefined) {
746
308
  this.deltaStorage = await docService.connectToDeltaStorage();
747
309
  }
748
- common_utils_1.assert(this.closeAbortController.signal.onabort === null, 0x1e8 /* "reentrancy" */);
749
- const controller = new abort_controller_1.default();
750
- this.closeAbortController.signal.onabort = () => controller.abort();
751
310
  let cancelFetch;
752
311
  if (to !== undefined) {
753
312
  const lastExpectedOp = to - 1; // make it inclusive!
@@ -755,7 +314,7 @@ class DeltaManager extends common_utils_1.TypedEventEmitter {
755
314
  // received through delta stream. Validate that before moving forward.
756
315
  if (this.lastQueuedSequenceNumber >= lastExpectedOp) {
757
316
  this.logger.sendPerformanceEvent(Object.assign({ reason: this.fetchReason, eventName: "ExtraStorageCall", early: true, from,
758
- to }, this.connectionStateProps));
317
+ to }, this.connectionManager.connectionVerboseProps));
759
318
  return;
760
319
  }
761
320
  // Be prepared for the case where webSocket would receive the ops that we are trying to fill through
@@ -772,6 +331,7 @@ class DeltaManager extends common_utils_1.TypedEventEmitter {
772
331
  // That said, if we have socket connection, make sure we got ops up to checkpointSequenceNumber!
773
332
  cancelFetch = (op) => op.sequenceNumber >= this.lastObservedSeqNumber;
774
333
  }
334
+ const controller = new abort_controller_1.default();
775
335
  let opsFromFetch = false;
776
336
  const opListener = (op) => {
777
337
  common_utils_1.assert(op.sequenceNumber === this.lastQueuedSequenceNumber, 0x23a /* "seq#'s" */);
@@ -783,11 +343,13 @@ class DeltaManager extends common_utils_1.TypedEventEmitter {
783
343
  this._inbound.off("push", opListener);
784
344
  }
785
345
  };
786
- this._inbound.on("push", opListener);
787
346
  try {
347
+ this._inbound.on("push", opListener);
348
+ common_utils_1.assert(this.closeAbortController.signal.onabort === null, 0x1e8 /* "reentrancy" */);
349
+ this.closeAbortController.signal.onabort = () => controller.abort();
788
350
  const stream = this.deltaStorage.fetchMessages(from, // inclusive
789
351
  to, // exclusive
790
- controller.signal, cacheOnly);
352
+ controller.signal, cacheOnly, this.fetchReason);
791
353
  // eslint-disable-next-line no-constant-condition
792
354
  while (true) {
793
355
  const result = await stream.read();
@@ -804,9 +366,9 @@ class DeltaManager extends common_utils_1.TypedEventEmitter {
804
366
  }
805
367
  }
806
368
  finally {
807
- common_utils_1.assert(!opsFromFetch, 0x289 /* "logic error" */);
808
369
  this.closeAbortController.signal.onabort = null;
809
370
  this._inbound.off("push", opListener);
371
+ common_utils_1.assert(!opsFromFetch, 0x289 /* "logic error" */);
810
372
  }
811
373
  }
812
374
  /**
@@ -817,13 +379,9 @@ class DeltaManager extends common_utils_1.TypedEventEmitter {
817
379
  return;
818
380
  }
819
381
  this.closed = true;
820
- // Ensure that things like triggerConnect() will short circuit
821
- this._reconnectMode = ReconnectMode.Never;
382
+ this.connectionManager.dispose(error);
822
383
  this.closeAbortController.abort();
823
- // This raises "disconnect" event if we have active connection.
824
- this.disconnectFromDeltaStream(error !== undefined ? `${error.message}` : "Container closed");
825
384
  this._inbound.clear();
826
- this._outbound.clear();
827
385
  this._inboundSignal.clear();
828
386
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
829
387
  this._inbound.pause();
@@ -831,10 +389,6 @@ class DeltaManager extends common_utils_1.TypedEventEmitter {
831
389
  this._inboundSignal.pause();
832
390
  // Drop pending messages - this will ensure catchUp() does not go into infinite loop
833
391
  this.pending = [];
834
- // Notify everyone we are in read-only state.
835
- // Useful for data stores in case we hit some critical error,
836
- // to switch to a mode where user edits are not accepted
837
- this.set_readonlyPermissions(true);
838
392
  // This needs to be the last thing we do (before removing listeners), as it causes
839
393
  // Container to dispose context and break ability of data stores / runtime to "hear"
840
394
  // from delta manager, including notification (above) about readonly state.
@@ -847,6 +401,20 @@ class DeltaManager extends common_utils_1.TypedEventEmitter {
847
401
  this.timeTillThrottling = 0;
848
402
  }
849
403
  }
404
+ disconnectHandler(reason) {
405
+ if (this.messageBuffer.length > 0) {
406
+ // Behavior is not well defined here RE batches across connections / disconnect.
407
+ // DeltaManager overall policy - drop all ops on disconnection and rely on
408
+ // container runtime to deal with resubmitting any ops that did not make it through.
409
+ // So drop them, but also raise error event to look into details.
410
+ this.logger.sendErrorEvent({
411
+ eventName: "OpenBatchOnDisconnect",
412
+ length: this.messageBuffer.length,
413
+ });
414
+ this.messageBuffer.length = 0;
415
+ }
416
+ this.emit("disconnect", reason);
417
+ }
850
418
  /**
851
419
  * Emit info about a delay in service communication on account of throttling.
852
420
  * @param id - Id of the connection that is delayed
@@ -862,177 +430,6 @@ class DeltaManager extends common_utils_1.TypedEventEmitter {
862
430
  this.emit("throttled", throttlingWarning);
863
431
  }
864
432
  }
865
- /**
866
- * Once we've successfully gotten a connection, we need to set up state, attach event listeners, and process
867
- * initial messages.
868
- * @param connection - The newly established connection
869
- */
870
- setupNewSuccessfulConnection(connection, requestedMode) {
871
- // Old connection should have been cleaned up before establishing a new one
872
- common_utils_1.assert(this.connection === undefined, 0x0e6 /* "old connection exists on new connection setup" */);
873
- common_utils_1.assert(this.connectionP !== undefined || this.closed, 0x27f /* "reentrancy may result in incorrect behavior" */);
874
- common_utils_1.assert(!connection.disposed, 0x28a /* "can't be disposed - Callers need to ensure that!" */);
875
- this.connectionP = undefined;
876
- this.connection = connection;
877
- // Does information in scopes & mode matches?
878
- // If we asked for "write" and got "read", then file is read-only
879
- // But if we ask read, server can still give us write.
880
- const readonly = !connection.claims.scopes.includes(protocol_definitions_1.ScopeType.DocWrite);
881
- // This connection mode validation logic is moving to the driver layer in 0.44. These two asserts can be
882
- // removed after those packages have released and become ubiquitous.
883
- common_utils_1.assert(requestedMode === "read" || readonly === (this.connectionMode === "read"), 0x0e7 /* "claims/connectionMode mismatch" */);
884
- common_utils_1.assert(!readonly || this.connectionMode === "read", 0x0e8 /* "readonly perf with write connection" */);
885
- this.set_readonlyPermissions(readonly);
886
- this.refreshDelayInfo(this.deltaStreamDelayId);
887
- if (this.closed) {
888
- // Raise proper events, Log telemetry event and close connection.
889
- this.disconnectFromDeltaStream(`Disconnect on close`);
890
- return;
891
- }
892
- // We cancel all ops on lost of connectivity, and rely on DDSes to resubmit them.
893
- // Semantics are not well defined for batches (and they are broken right now on disconnects anyway),
894
- // but it's safe to assume (until better design is put into place) that batches should not exist
895
- // across multiple connections. Right now we assume runtime will not submit any ops in disconnected
896
- // state. As requirements change, so should these checks.
897
- common_utils_1.assert(this.messageBuffer.length === 0, 0x0e9 /* "messageBuffer is not empty on new connection" */);
898
- this._outbound.resume();
899
- connection.on("op", this.opHandler);
900
- connection.on("signal", this.signalHandler);
901
- connection.on("nack", this.nackHandler);
902
- connection.on("disconnect", this.disconnectHandler);
903
- connection.on("error", this.errorHandler);
904
- connection.on("pong", this.pongHandler);
905
- // Initial messages are always sorted. However, due to early op handler installed by drivers and appending those
906
- // ops to initialMessages, resulting set is no longer sorted, which would result in client hitting storage to
907
- // fill in gap. We will recover by cancelling this request once we process remaining ops, but it's a waste that
908
- // we could avoid
909
- const initialMessages = connection.initialMessages.sort((a, b) => a.sequenceNumber - b.sequenceNumber);
910
- this.connectionStateProps = {
911
- connectionLastQueuedSequenceNumber: this.lastQueuedSequenceNumber,
912
- connectionLastObservedSeqNumber: this.lastObservedSeqNumber,
913
- clientId: connection.clientId,
914
- mode: connection.mode,
915
- };
916
- this._hasCheckpointSequenceNumber = false;
917
- // Some storages may provide checkpointSequenceNumber to identify how far client is behind.
918
- const checkpointSequenceNumber = connection.checkpointSequenceNumber;
919
- if (checkpointSequenceNumber !== undefined) {
920
- this._hasCheckpointSequenceNumber = true;
921
- this.updateLatestKnownOpSeqNumber(checkpointSequenceNumber);
922
- }
923
- // Update knowledge of how far we are behind, before raising "connect" event
924
- // This is duplication of what enqueueMessages() does, but we have to raise event before we get there,
925
- // so duplicating update logic here as well.
926
- const last = initialMessages.length > 0 ? initialMessages[initialMessages.length - 1].sequenceNumber : -1;
927
- if (initialMessages.length > 0) {
928
- this._hasCheckpointSequenceNumber = true;
929
- this.updateLatestKnownOpSeqNumber(last);
930
- }
931
- // Notify of the connection
932
- // WARNING: This has to happen before processInitialMessages() call below.
933
- // If not, we may not update Container.pendingClientId in time before seeing our own join session op.
934
- this.emit("connect", DeltaManager.detailsFromConnection(connection), this._hasCheckpointSequenceNumber ? this.lastObservedSeqNumber - this.lastSequenceNumber : undefined);
935
- this.enqueueMessages(initialMessages, this.connectFirstConnection ? "InitialOps" : "ReconnectOps");
936
- if (connection.initialSignals !== undefined) {
937
- for (const signal of connection.initialSignals) {
938
- this._inboundSignal.push(signal);
939
- }
940
- }
941
- // If we got some initial ops, then we know the gap and call above fetched ops to fill it.
942
- // Same is true for "write" mode even if we have no ops - we will get self "join" ops very very soon.
943
- // However if we are connecting as view-only, then there is no good signal to realize if client is behind.
944
- // Thus we have to hit storage to see if any ops are there.
945
- if (initialMessages.length === 0) {
946
- if (checkpointSequenceNumber !== undefined) {
947
- // We know how far we are behind (roughly). If it's non-zero gap, fetch ops right away.
948
- if (checkpointSequenceNumber > this.lastQueuedSequenceNumber) {
949
- this.fetchMissingDeltas("AfterConnection", this.lastQueuedSequenceNumber);
950
- }
951
- // we do not know the gap, and we will not learn about it if socket is quite - have to ask.
952
- }
953
- else if (connection.mode === "read") {
954
- this.fetchMissingDeltas("AfterReadConnection", this.lastQueuedSequenceNumber);
955
- }
956
- }
957
- else {
958
- this.connectionStateProps.connectionInitialOpsFrom = initialMessages[0].sequenceNumber;
959
- this.connectionStateProps.connectionInitialOpsTo = last + 1;
960
- }
961
- this.connectFirstConnection = false;
962
- }
963
- /**
964
- * Disconnect the current connection.
965
- * @param reason - Text description of disconnect reason to emit with disconnect event
966
- */
967
- disconnectFromDeltaStream(reason) {
968
- this.pendingReconnect = false;
969
- this.downgradedConnection = false;
970
- if (this.connection === undefined) {
971
- return false;
972
- }
973
- common_utils_1.assert(this.connectionP === undefined, 0x27b /* "reentrancy may result in incorrect behavior" */);
974
- const connection = this.connection;
975
- // Avoid any re-entrancy - clear object reference
976
- this.connection = undefined;
977
- // Remove listeners first so we don't try to retrigger this flow accidentally through reconnectOnError
978
- connection.off("op", this.opHandler);
979
- connection.off("signal", this.signalHandler);
980
- connection.off("nack", this.nackHandler);
981
- connection.off("disconnect", this.disconnectHandler);
982
- connection.off("error", this.errorHandler);
983
- connection.off("pong", this.pongHandler);
984
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
985
- this._outbound.pause();
986
- this._outbound.clear();
987
- this.emit("disconnect", reason);
988
- connection.dispose();
989
- this.connectionStateProps = {};
990
- return true;
991
- }
992
- /**
993
- * Disconnect the current connection and reconnect.
994
- * @param connection - The connection that wants to reconnect - no-op if it's different from this.connection
995
- * @param requestedMode - Read or write
996
- * @param error - Error reconnect information including whether or not to reconnect
997
- * @returns A promise that resolves when the connection is reestablished or we stop trying
998
- */
999
- async reconnectOnError(requestedMode, error) {
1000
- return this.reconnectOnErrorCore(requestedMode, error.message, error);
1001
- }
1002
- /**
1003
- * Disconnect the current connection and reconnect.
1004
- * @param connection - The connection that wants to reconnect - no-op if it's different from this.connection
1005
- * @param requestedMode - Read or write
1006
- * @param error - Error reconnect information including whether or not to reconnect
1007
- * @returns A promise that resolves when the connection is reestablished or we stop trying
1008
- */
1009
- async reconnectOnErrorCore(requestedMode, disconnectMessage, error) {
1010
- // We quite often get protocol errors before / after observing nack/disconnect
1011
- // we do not want to run through same sequence twice.
1012
- // If we're already disconnected/disconnecting it's not appropriate to call this again.
1013
- common_utils_1.assert(this.connection !== undefined, 0x0eb /* "Missing connection for reconnect" */);
1014
- this.disconnectFromDeltaStream(disconnectMessage);
1015
- const canRetry = error !== undefined ? driver_utils_1.canRetryOnError(error) : true;
1016
- // If reconnection is not an option, close the DeltaManager
1017
- if (this.reconnectMode === ReconnectMode.Never || !canRetry) {
1018
- // Do not raise container error if we are closing just because we lost connection.
1019
- // Those errors (like IdleDisconnect) would show up in telemetry dashboards and
1020
- // are very misleading, as first initial reaction - some logic is broken.
1021
- this.close(canRetry ? undefined : error);
1022
- }
1023
- // If closed then we can't reconnect
1024
- if (this.closed) {
1025
- return;
1026
- }
1027
- if (this.reconnectMode === ReconnectMode.Enabled) {
1028
- const delayMs = error !== undefined ? driver_utils_1.getRetryDelayFromError(error) : undefined;
1029
- if (delayMs !== undefined) {
1030
- this.emitDelayInfo(this.deltaStreamDelayId, delayMs, error);
1031
- await driver_utils_1.waitForConnectedState(delayMs);
1032
- }
1033
- this.triggerConnect({ reason: "reconnect", mode: requestedMode, fetchOpsFromStorage: false });
1034
- }
1035
- }
1036
433
  // returns parts of message (in string format) that should never change for a given message.
1037
434
  // Used for message comparison. It attempts to avoid comparing fields that potentially may differ.
1038
435
  // for example, it's not clear if serverMetadata or timestamp property is a property of message or server state.
@@ -1044,7 +441,7 @@ class DeltaManager extends common_utils_1.TypedEventEmitter {
1044
441
  return `${m.clientId}-${m.type}-${m.minimumSequenceNumber}-${m.referenceSequenceNumber}-${m.timestamp}`;
1045
442
  }
1046
443
  enqueueMessages(messages, reason, allowGaps = false) {
1047
- var _a, _b, _c;
444
+ var _a, _b;
1048
445
  if (this.handler === undefined) {
1049
446
  // We did not setup handler yet.
1050
447
  // This happens when we connect to web socket faster than we get attributes for container
@@ -1104,7 +501,7 @@ class DeltaManager extends common_utils_1.TypedEventEmitter {
1104
501
  // correctly take into account pending ops.
1105
502
  if (eventName !== undefined) {
1106
503
  this.logger.sendPerformanceEvent(Object.assign({ eventName,
1107
- reason, previousReason: this.prevEnqueueMessagesReason, from, to: last + 1, length: messages.length, fetchReason: this.fetchReason, duplicate: duplicate > 0 ? duplicate : undefined, initialGap: initialGap !== 0 ? initialGap : undefined, gap: gap > 0 ? gap : undefined, firstMissing, dmInitialSeqNumber: this.initialSequenceNumber }, this.connectionStateProps));
504
+ reason, previousReason: this.prevEnqueueMessagesReason, from, to: last + 1, length: messages.length, fetchReason: this.fetchReason, duplicate: duplicate > 0 ? duplicate : undefined, initialGap: initialGap !== 0 ? initialGap : undefined, gap: gap > 0 ? gap : undefined, firstMissing, dmInitialSeqNumber: this.initialSequenceNumber }, this.connectionManager.connectionVerboseProps));
1108
505
  }
1109
506
  }
1110
507
  this.updateLatestKnownOpSeqNumber(messages[messages.length - 1].sequenceNumber);
@@ -1120,7 +517,7 @@ class DeltaManager extends common_utils_1.TypedEventEmitter {
1120
517
  const message2 = this.comparableMessagePayload(message);
1121
518
  if (message1 !== message2) {
1122
519
  const error = new driver_utils_1.NonRetryableError("twoMessagesWithSameSeqNumAndDifferentPayload", undefined, driver_definitions_1.DriverErrorType.fileOverwrittenInStorage, {
1123
- clientId: (_c = this.connection) === null || _c === void 0 ? void 0 : _c.clientId,
520
+ clientId: this.connectionManager.clientId,
1124
521
  sequenceNumber: message.sequenceNumber,
1125
522
  message1,
1126
523
  message2,
@@ -1131,7 +528,7 @@ class DeltaManager extends common_utils_1.TypedEventEmitter {
1131
528
  }
1132
529
  else if (message.sequenceNumber !== this.lastQueuedSequenceNumber + 1) {
1133
530
  this.pending.push(message);
1134
- this.fetchMissingDeltas(reason, this.lastQueuedSequenceNumber, message.sequenceNumber);
531
+ this.fetchMissingDeltas(reason, message.sequenceNumber);
1135
532
  }
1136
533
  else {
1137
534
  this.lastQueuedSequenceNumber = message.sequenceNumber;
@@ -1145,57 +542,34 @@ class DeltaManager extends common_utils_1.TypedEventEmitter {
1145
542
  this.prevEnqueueMessagesReason = this.pending.length > 0 ? "unknown" : reason;
1146
543
  }
1147
544
  processInboundMessage(message) {
1148
- var _a, _b, _c;
1149
545
  const startTime = Date.now();
1150
546
  this.lastProcessedMessage = message;
1151
547
  // All non-system messages are coming from some client, and should have clientId
1152
548
  // System messages may have no clientId (but some do, like propose, noop, summarize)
1153
549
  common_utils_1.assert(message.clientId !== undefined
1154
550
  || protocol_base_1.isSystemMessage(message), 0x0ed /* "non-system message have to have clientId" */);
1155
- // if we have connection, and message is local, then we better treat is as local!
1156
- common_utils_1.assert(this.connection === undefined
1157
- || this.connection.clientId !== message.clientId
1158
- || this.lastSubmittedClientId === message.clientId, 0x0ee /* "Not accounting local messages correctly" */);
1159
- if (this.lastSubmittedClientId !== undefined && this.lastSubmittedClientId === message.clientId) {
1160
- const clientSequenceNumber = message.clientSequenceNumber;
1161
- common_utils_1.assert(this.clientSequenceNumberObserved < clientSequenceNumber, 0x0ef /* "client seq# not growing" */);
1162
- common_utils_1.assert(clientSequenceNumber <= this.clientSequenceNumber, 0x0f0 /* "Incoming local client seq# > generated by this client" */);
1163
- this.clientSequenceNumberObserved = clientSequenceNumber;
1164
- }
1165
551
  // TODO Remove after SPO picks up the latest build.
1166
552
  if (typeof message.contents === "string"
1167
553
  && message.contents !== ""
1168
554
  && message.type !== protocol_definitions_1.MessageType.ClientLeave) {
1169
555
  message.contents = JSON.parse(message.contents);
1170
556
  }
1171
- if (message.type === protocol_definitions_1.MessageType.ClientLeave) {
1172
- const systemLeaveMessage = message;
1173
- const clientId = JSON.parse(systemLeaveMessage.data);
1174
- if (clientId === ((_a = this.connection) === null || _a === void 0 ? void 0 : _a.clientId)) {
1175
- // We have been kicked out from quorum
1176
- this.logger.sendPerformanceEvent({ eventName: "ReadConnectionTransition" });
1177
- this.downgradedConnection = true;
1178
- common_utils_1.assert(this.connectionMode === "read", 0x27c /* "effective connectionMode should be 'read' after downgrade" */);
1179
- }
1180
- }
557
+ this.connectionManager.beforeProcessingIncomingOp(message);
1181
558
  // Add final ack trace.
1182
559
  if (message.traces !== undefined && message.traces.length > 0) {
1183
- const service = this.clientDetails.type === undefined || this.clientDetails.type === ""
1184
- ? "unknown"
1185
- : this.clientDetails.type;
1186
560
  message.traces.push({
1187
561
  action: "end",
1188
- service,
562
+ service: "client",
1189
563
  timestamp: Date.now(),
1190
564
  });
1191
565
  }
1192
566
  // Watch the minimum sequence number and be ready to update as needed
1193
567
  if (this.minSequenceNumber > message.minimumSequenceNumber) {
1194
- throw new container_utils_1.DataCorruptionError("msnMovesBackwards", Object.assign(Object.assign({}, extractLogSafeMessageProperties(message)), { clientId: (_b = this.connection) === null || _b === void 0 ? void 0 : _b.clientId }));
568
+ throw new container_utils_1.DataCorruptionError("msnMovesBackwards", Object.assign(Object.assign({}, extractLogSafeMessageProperties(message)), { clientId: this.connectionManager.clientId }));
1195
569
  }
1196
570
  this.minSequenceNumber = message.minimumSequenceNumber;
1197
571
  if (message.sequenceNumber !== this.lastProcessedSequenceNumber + 1) {
1198
- throw new container_utils_1.DataCorruptionError("nonSequentialSequenceNumber", Object.assign(Object.assign({}, extractLogSafeMessageProperties(message)), { clientId: (_c = this.connection) === null || _c === void 0 ? void 0 : _c.clientId }));
572
+ throw new container_utils_1.DataCorruptionError("nonSequentialSequenceNumber", Object.assign(Object.assign({}, extractLogSafeMessageProperties(message)), { clientId: this.connectionManager.clientId }));
1199
573
  }
1200
574
  this.lastProcessedSequenceNumber = message.sequenceNumber;
1201
575
  // a bunch of code assumes that this is true
@@ -1217,15 +591,15 @@ class DeltaManager extends common_utils_1.TypedEventEmitter {
1217
591
  /**
1218
592
  * Retrieves the missing deltas between the given sequence numbers
1219
593
  */
1220
- fetchMissingDeltas(reasonArg, lastKnowOp, to) {
1221
- this.fetchMissingDeltasCore(reasonArg, false /* cacheOnly */, lastKnowOp, to).catch((error) => {
594
+ fetchMissingDeltas(reasonArg, to) {
595
+ this.fetchMissingDeltasCore(reasonArg, false /* cacheOnly */, to).catch((error) => {
1222
596
  this.logger.sendErrorEvent({ eventName: "fetchMissingDeltasException" }, error);
1223
597
  });
1224
598
  }
1225
599
  /**
1226
600
  * Retrieves the missing deltas between the given sequence numbers
1227
601
  */
1228
- async fetchMissingDeltasCore(reason, cacheOnly, lastKnowOp, to) {
602
+ async fetchMissingDeltasCore(reason, cacheOnly, to) {
1229
603
  var _a;
1230
604
  // Exit out early if we're already fetching deltas
1231
605
  if (this.fetchReason !== undefined) {
@@ -1237,12 +611,11 @@ class DeltaManager extends common_utils_1.TypedEventEmitter {
1237
611
  }
1238
612
  if (this.handler === undefined) {
1239
613
  // We do not poses yet any information
1240
- common_utils_1.assert(lastKnowOp === 0, 0x26b /* "initial state" */);
614
+ common_utils_1.assert(this.lastQueuedSequenceNumber === 0, 0x26b /* "initial state" */);
1241
615
  return;
1242
616
  }
1243
617
  try {
1244
- common_utils_1.assert(lastKnowOp === this.lastQueuedSequenceNumber, 0x0f1 /* "from arg" */);
1245
- let from = lastKnowOp + 1;
618
+ let from = this.lastQueuedSequenceNumber + 1;
1246
619
  const n = (_a = this.previouslyProcessedMessage) === null || _a === void 0 ? void 0 : _a.sequenceNumber;
1247
620
  if (n !== undefined) {
1248
621
  // If we already processed at least one op, then we have this.previouslyProcessedMessage populated
@@ -1250,7 +623,7 @@ class DeltaManager extends common_utils_1.TypedEventEmitter {
1250
623
  // Knowing about this mechanism, we could ask for op we already observed to increase validation.
1251
624
  // This is especially useful when coming out of offline mode or loading from
1252
625
  // very old cached (by client / driver) snapshot.
1253
- common_utils_1.assert(n === lastKnowOp, 0x0f2 /* "previouslyProcessedMessage" */);
626
+ common_utils_1.assert(n === this.lastQueuedSequenceNumber, 0x0f2 /* "previouslyProcessedMessage" */);
1254
627
  common_utils_1.assert(from > 1, 0x0f3 /* "not positive" */);
1255
628
  from--;
1256
629
  }
@@ -1298,7 +671,7 @@ class DeltaManager extends common_utils_1.TypedEventEmitter {
1298
671
  // (the other 50%), and thus these errors below should be looked at even if code below results in
1299
672
  // recovery.
1300
673
  if (this.lastQueuedSequenceNumber < this.lastObservedSeqNumber) {
1301
- this.fetchMissingDeltas("OpsBehind", this.lastQueuedSequenceNumber);
674
+ this.fetchMissingDeltas("OpsBehind");
1302
675
  }
1303
676
  }
1304
677
  }