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