@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
@@ -6,92 +6,53 @@
6
6
  import { default as AbortController } from "abort-controller";
7
7
  import { v4 as uuid } from "uuid";
8
8
  import {
9
- IDisposable,
10
9
  ITelemetryLogger,
11
10
  IEventProvider,
12
11
  ITelemetryProperties,
13
12
  ITelemetryErrorEvent,
14
13
  } from "@fluidframework/common-definitions";
15
14
  import {
16
- IConnectionDetails,
17
15
  IDeltaHandlerStrategy,
18
16
  IDeltaManager,
19
17
  IDeltaManagerEvents,
20
18
  IDeltaQueue,
21
19
  ICriticalContainerError,
22
20
  IThrottlingWarning,
23
- ReadOnlyInfo,
21
+ IConnectionDetails,
24
22
  } from "@fluidframework/container-definitions";
25
- import { assert, Deferred, performance, TypedEventEmitter } from "@fluidframework/common-utils";
23
+ import { assert, TypedEventEmitter } from "@fluidframework/common-utils";
26
24
  import {
27
- TelemetryLogger,
28
- safeRaiseEvent,
29
- logIfFalse,
30
25
  normalizeError,
31
- wrapError,
26
+ logIfFalse,
27
+ safeRaiseEvent,
32
28
  } from "@fluidframework/telemetry-utils";
33
29
  import {
34
30
  IDocumentDeltaStorageService,
35
31
  IDocumentService,
36
- IDocumentDeltaConnection,
37
- IDocumentDeltaConnectionEvents,
38
- DriverError,
39
32
  DriverErrorType,
40
33
  } from "@fluidframework/driver-definitions";
41
34
  import { isSystemMessage } from "@fluidframework/protocol-base";
42
35
  import {
43
- ConnectionMode,
44
- IClient,
45
- IClientConfiguration,
46
- IClientDetails,
47
36
  IDocumentMessage,
48
- INack,
49
- INackContent,
50
37
  ISequencedDocumentMessage,
51
- ISignalClient,
52
38
  ISignalMessage,
53
- ITokenClaims,
54
- ITrace,
55
39
  MessageType,
56
- ScopeType,
57
- ISequencedDocumentSystemMessage,
40
+ ITrace,
41
+ ConnectionMode,
58
42
  } from "@fluidframework/protocol-definitions";
59
43
  import {
60
- canRetryOnError,
61
- createWriteError,
62
- createGenericNetworkError,
63
- getRetryDelayFromError,
64
- logNetworkFailure,
65
- waitForConnectedState,
66
44
  NonRetryableError,
67
- DeltaStreamConnectionForbiddenError,
68
- GenericNetworkError,
69
45
  } from "@fluidframework/driver-utils";
70
46
  import {
71
47
  ThrottlingWarning,
72
48
  CreateProcessingError,
73
49
  DataCorruptionError,
74
- GenericError,
75
50
  } from "@fluidframework/container-utils";
76
51
  import { DeltaQueue } from "./deltaQueue";
77
-
78
- const MaxReconnectDelayInMs = 8000;
79
- const InitialReconnectDelayInMs = 1000;
80
- const DefaultChunkSize = 16 * 1024;
81
-
82
- function getNackReconnectInfo(nackContent: INackContent) {
83
- const message = `Nack (${nackContent.type}): ${nackContent.message}`;
84
- const canRetry = nackContent.code !== 403;
85
- const retryAfterMs = nackContent.retryAfter !== undefined ? nackContent.retryAfter * 1000 : undefined;
86
- return createGenericNetworkError(
87
- `nack [${nackContent.code}]`, message, canRetry, retryAfterMs, { statusCode: nackContent.code });
88
- }
89
-
90
- const createReconnectError = (fluidErrorCode: string, err: any) =>
91
- wrapError(
92
- err,
93
- (errorMessage: string) => new GenericNetworkError(fluidErrorCode, errorMessage, true /* canRetry */),
94
- );
52
+ import {
53
+ IConnectionManagerFactoryArgs,
54
+ IConnectionManager,
55
+ } from "./contracts";
95
56
 
96
57
  export interface IConnectionArgs {
97
58
  mode?: ConnectionMode;
@@ -99,12 +60,6 @@ export interface IConnectionArgs {
99
60
  reason: string;
100
61
  }
101
62
 
102
- export enum ReconnectMode {
103
- Never = "Never",
104
- Disabled = "Disabled",
105
- Enabled = "Enabled",
106
- }
107
-
108
63
  /**
109
64
  * Includes events emitted by the concrete implementation DeltaManager
110
65
  * but not exposed on the public interface IDeltaManager
@@ -114,82 +69,24 @@ export interface IDeltaManagerInternalEvents extends IDeltaManagerEvents {
114
69
  (event: "closed", listener: (error?: ICriticalContainerError) => void);
115
70
  }
116
71
 
117
- /**
118
- * Implementation of IDocumentDeltaConnection that does not support submitting
119
- * or receiving ops. Used in storage-only mode.
120
- */
121
- class NoDeltaStream
122
- extends TypedEventEmitter<IDocumentDeltaConnectionEvents>
123
- implements IDocumentDeltaConnection, IDisposable
124
- {
125
- clientId: string = "storage-only client";
126
- claims: ITokenClaims = {
127
- scopes: [ScopeType.DocRead],
128
- } as any;
129
- mode: ConnectionMode = "read";
130
- existing: boolean = true;
131
- maxMessageSize: number = 0;
132
- version: string = "";
133
- initialMessages: ISequencedDocumentMessage[] = [];
134
- initialSignals: ISignalMessage[] = [];
135
- initialClients: ISignalClient[] = [];
136
- serviceConfiguration: IClientConfiguration = {
137
- maxMessageSize: 0,
138
- blockSize: 0,
139
- summary: undefined as any,
140
- };
141
- checkpointSequenceNumber?: number | undefined = undefined;
142
- submit(messages: IDocumentMessage[]): void {
143
- this.emit("nack", this.clientId, messages.map((operation) => {
144
- return {
145
- operation,
146
- content: { message: "Cannot submit with storage-only connection", code: 403 },
147
- };
148
- }));
149
- }
150
- submitSignal(message: any): void {
151
- this.emit("nack", this.clientId, {
152
- operation: message,
153
- content: { message: "Cannot submit signal with storage-only connection", code: 403 },
154
- });
155
- }
156
-
157
- private _disposed = false;
158
- public get disposed() { return this._disposed; }
159
- public dispose() { this._disposed = true; }
160
- }
161
-
162
72
  /**
163
73
  * Manages the flow of both inbound and outbound messages. This class ensures that shared objects receive delta
164
74
  * messages in order regardless of possible network conditions or timings causing out of order delivery.
165
75
  */
166
- export class DeltaManager
76
+ export class DeltaManager<TConnectionManager extends IConnectionManager>
167
77
  extends TypedEventEmitter<IDeltaManagerInternalEvents>
168
78
  implements
169
79
  IDeltaManager<ISequencedDocumentMessage, IDocumentMessage>,
170
80
  IEventProvider<IDeltaManagerInternalEvents>
171
81
  {
82
+ public readonly connectionManager: TConnectionManager;
83
+
172
84
  public get active(): boolean { return this._active(); }
173
85
 
174
86
  public get disposed() { return this.closed; }
175
87
 
176
- public readonly clientDetails: IClientDetails;
177
88
  public get IDeltaSender() { return this; }
178
89
 
179
- /**
180
- * Controls whether the DeltaManager will automatically reconnect to the delta stream after receiving a disconnect.
181
- */
182
- private _reconnectMode: ReconnectMode;
183
-
184
- // file ACL - whether user has only read-only access to a file
185
- private _readonlyPermissions: boolean | undefined;
186
-
187
- // tracks host requiring read-only mode.
188
- private _forceReadonly = false;
189
-
190
- // Connection mode used when reconnecting on error or disconnect.
191
- private readonly defaultReconnectionMode: ConnectionMode;
192
-
193
90
  private pending: ISequencedDocumentMessage[] = [];
194
91
  private fetchReason: string | undefined;
195
92
 
@@ -217,62 +114,28 @@ export class DeltaManager
217
114
 
218
115
  private readonly _inbound: DeltaQueue<ISequencedDocumentMessage>;
219
116
  private readonly _inboundSignal: DeltaQueue<ISignalMessage>;
220
- private readonly _outbound: DeltaQueue<IDocumentMessage[]>;
221
-
222
- private connectionP: Promise<IDocumentDeltaConnection> | undefined;
223
- private connection: IDocumentDeltaConnection | undefined;
224
- private clientSequenceNumber = 0;
225
- private clientSequenceNumberObserved = 0;
226
- // Counts the number of noops sent by the client which may not be acked.
227
- private trailingNoopCount = 0;
228
- private closed = false;
229
- private readonly deltaStreamDelayId = uuid();
230
- private readonly deltaStorageDelayId = uuid();
231
117
 
232
- // track clientId used last time when we sent any ops
233
- private lastSubmittedClientId: string | undefined;
118
+ private closed = false;
234
119
 
235
120
  private handler: IDeltaHandlerStrategy | undefined;
236
121
  private deltaStorage: IDocumentDeltaStorageService | undefined;
237
122
 
238
- private messageBuffer: IDocumentMessage[] = [];
239
-
240
- private connectFirstConnection = true;
241
123
  private readonly throttlingIdSet = new Set<string>();
242
124
  private timeTillThrottling: number = 0;
243
125
 
244
- private connectionStateProps: Record<string, string | number> = {};
245
-
246
- // True if current connection has checkpoint information
247
- // I.e. we know how far behind the client was at the time of establishing connection
248
- private _hasCheckpointSequenceNumber = false;
249
-
250
126
  private readonly closeAbortController = new AbortController();
251
127
 
252
- // True if there is pending (async) reconnection from "read" to "write"
253
- private pendingReconnect = false;
128
+ private readonly deltaStorageDelayId = uuid();
129
+ private readonly deltaStreamDelayId = uuid();
254
130
 
255
- // downgrade "write" connection to "read"
256
- private downgradedConnection = false;
131
+ private messageBuffer: IDocumentMessage[] = [];
257
132
 
258
- /**
259
- * Tells if current connection has checkpoint information.
260
- * I.e. we know how far behind the client was at the time of establishing connection
261
- */
262
- public get hasCheckpointSequenceNumber() {
263
- // Valid to be called only if we have active connection.
264
- assert(this.connection !== undefined, 0x0df /* "Missing active connection" */);
265
- return this._hasCheckpointSequenceNumber;
266
- }
133
+ private _checkpointSequenceNumber: number | undefined;
267
134
 
268
135
  public get inbound(): IDeltaQueue<ISequencedDocumentMessage> {
269
136
  return this._inbound;
270
137
  }
271
138
 
272
- public get outbound(): IDeltaQueue<IDocumentMessage[]> {
273
- return this._outbound;
274
- }
275
-
276
139
  public get inboundSignal(): IDeltaQueue<ISignalMessage> {
277
140
  return this._inboundSignal;
278
141
  }
@@ -301,42 +164,24 @@ export class DeltaManager
301
164
  return this.minSequenceNumber;
302
165
  }
303
166
 
304
- public get maxMessageSize(): number {
305
- return this.connection?.serviceConfiguration?.maxMessageSize
306
- ?? DefaultChunkSize;
307
- }
308
-
309
- public get version(): string {
310
- if (this.connection === undefined) {
311
- throw new Error("Cannot check version without a connection");
312
- }
313
- return this.connection.version;
314
- }
315
-
316
- public get serviceConfiguration(): IClientConfiguration | undefined {
317
- return this.connection?.serviceConfiguration;
318
- }
319
-
320
- public get scopes(): string[] | undefined {
321
- return this.connection?.claims.scopes;
322
- }
323
-
324
- public get socketDocumentId(): string | undefined {
325
- return this.connection?.claims.documentId;
326
- }
327
-
328
167
  /**
329
- * The current connection mode, initially read.
168
+ * Tells if current connection has checkpoint information.
169
+ * I.e. we know how far behind the client was at the time of establishing connection
330
170
  */
331
- public get connectionMode(): ConnectionMode {
332
- assert(!this.downgradedConnection || this.connection?.mode === "write",
333
- 0x277 /* "Did we forget to reset downgradedConnection on new connection?" */);
334
- if (this.connection === undefined || this.downgradedConnection) {
335
- return "read";
336
- }
337
- return this.connection.mode;
171
+ public get hasCheckpointSequenceNumber() {
172
+ // Valid to be called only if we have active connection.
173
+ assert(this.connectionManager.connected, 0x0df /* "Missing active connection" */);
174
+ return this._checkpointSequenceNumber !== undefined;
338
175
  }
339
176
 
177
+ // Forwarding connection manager properties / IDeltaManager implementation
178
+ public get maxMessageSize(): number { return this.connectionManager.maxMessageSize; }
179
+ public get version() { return this.connectionManager.version; }
180
+ public get serviceConfiguration() { return this.connectionManager.serviceConfiguration; }
181
+ public get outbound() { return this.connectionManager.outbound; }
182
+ public get readOnlyInfo() { return this.connectionManager.readOnlyInfo; }
183
+ public get clientDetails() { return this.connectionManager.clientDetails; }
184
+
340
185
  /**
341
186
  * Tells if container is in read-only mode.
342
187
  * Data stores should listen for "readonly" notifications and disallow user
@@ -348,137 +193,64 @@ export class DeltaManager
348
193
  * @deprecated - use readOnlyInfo
349
194
  */
350
195
  public get readonly() {
351
- if (this._forceReadonly) {
352
- return true;
353
- }
354
- return this._readonlyPermissions;
196
+ return this.readOnlyInfo.readonly;
355
197
  }
356
198
 
357
- /**
358
- * Tells if user has no write permissions for file in storage
359
- * It is undefined if we have not yet established websocket connection
360
- * and do not know if user has write access to a file.
361
- * @deprecated - use readOnlyInfo
362
- */
363
- public get readonlyPermissions() {
364
- return this._readonlyPermissions;
365
- }
199
+ public submit(type: MessageType, contents: any, batch = false, metadata?: any) {
200
+ // Start adding trace for the op.
201
+ const traces: ITrace[] = [
202
+ {
203
+ action: "start",
204
+ service: "client",
205
+ timestamp: Date.now(),
206
+ }];
366
207
 
367
- public get readOnlyInfo(): ReadOnlyInfo {
368
- const storageOnly = this.connection !== undefined && this.connection instanceof NoDeltaStream;
369
- if (storageOnly || this._forceReadonly || this._readonlyPermissions === true) {
370
- return {
371
- readonly: true,
372
- forced: this._forceReadonly,
373
- permissions: this._readonlyPermissions,
374
- storageOnly,
375
- };
208
+ const messagePartial: Omit<IDocumentMessage, "clientSequenceNumber"> = {
209
+ contents: JSON.stringify(contents),
210
+ metadata,
211
+ referenceSequenceNumber: this.lastProcessedSequenceNumber,
212
+ traces,
213
+ type,
214
+ };
215
+
216
+ if (!batch) {
217
+ this.flush();
376
218
  }
377
219
 
378
- return { readonly: this._readonlyPermissions };
379
- }
220
+ const message = this.connectionManager.prepareMessageToSend(messagePartial);
221
+ if (message === undefined) {
222
+ return -1;
223
+ }
380
224
 
381
- /**
382
- * Automatic reconnecting enabled or disabled.
383
- * If set to Never, then reconnecting will never be allowed.
384
- */
385
- public get reconnectMode(): ReconnectMode {
386
- return this._reconnectMode;
387
- }
225
+ this.messageBuffer.push(message);
388
226
 
389
- public shouldJoinWrite(): boolean {
390
- // We don't have to wait for ack for topmost NoOps. So subtract those.
391
- return this.clientSequenceNumberObserved < (this.clientSequenceNumber - this.trailingNoopCount);
392
- }
393
- /**
394
- * Returns set of props that can be logged in telemetry that provide some insights / statistics
395
- * about current or last connection (if there is no connection at the moment)
396
- */
397
- public connectionProps(): ITelemetryProperties {
398
- const common = {
399
- sequenceNumber: this.lastSequenceNumber,
400
- };
401
- if (this.connection !== undefined) {
402
- return {
403
- ...common,
404
- connectionMode: this.connectionMode,
405
- };
406
- } else {
407
- return {
408
- ...common,
409
- // Report how many ops this client sent in last disconnected session
410
- sentOps: this.clientSequenceNumber,
411
- };
227
+ if (!batch) {
228
+ this.flush();
412
229
  }
413
- }
414
230
 
415
- /**
416
- * Enables or disables automatic reconnecting.
417
- * Will throw an error if reconnectMode set to Never.
418
- */
419
- public setAutoReconnect(mode: ReconnectMode): void {
420
- assert(mode !== ReconnectMode.Never && this._reconnectMode !== ReconnectMode.Never,
421
- 0x278 /* "API is not supported for non-connecting or closed container" */);
231
+ this.emit("submitOp", message);
232
+ return message.clientSequenceNumber;
233
+ }
422
234
 
423
- this._reconnectMode = mode;
235
+ public submitSignal(content: any) { return this.connectionManager.submitSignal(content); }
424
236
 
425
- if (mode !== ReconnectMode.Enabled) {
426
- // immediately disconnect - do not rely on service eventually dropping connection.
427
- this.disconnectFromDeltaStream("setAutoReconnect");
237
+ public flush() {
238
+ if (this.messageBuffer.length === 0) {
239
+ return;
428
240
  }
429
- }
430
241
 
431
- /**
432
- * Sends signal to runtime (and data stores) to be read-only.
433
- * Hosts may have read only views, indicating to data stores that no edits are allowed.
434
- * This is independent from this._readonlyPermissions (permissions) and this.connectionMode
435
- * (server can return "write" mode even when asked for "read")
436
- * Leveraging same "readonly" event as runtime & data stores should behave the same in such case
437
- * as in read-only permissions.
438
- * But this.active can be used by some DDSes to figure out if ops can be sent
439
- * (for example, read-only view still participates in code proposals / upgrades decisions)
440
- *
441
- * Forcing Readonly does not prevent DDS from generating ops. It is up to user code to honour
442
- * the readonly flag. If ops are generated, they will accumulate locally and not be sent. If
443
- * there are pending in the outbound queue, it will stop sending until force readonly is
444
- * cleared.
445
- *
446
- * @param readonly - set or clear force readonly.
447
- */
448
- public forceReadonly(readonly: boolean) {
449
- if (readonly !== this._forceReadonly) {
450
- this.logger.sendTelemetryEvent({
451
- eventName: "ForceReadOnly",
452
- value: readonly,
453
- });
454
- }
455
- const oldValue = this.readonly;
456
- this._forceReadonly = readonly;
457
-
458
- if (oldValue !== this.readonly) {
459
- assert(this._reconnectMode !== ReconnectMode.Never,
460
- 0x279 /* "API is not supported for non-connecting or closed container" */);
461
-
462
- let reconnect = false;
463
- if (this.readonly === true) {
464
- // If we switch to readonly while connected, we should disconnect first
465
- // See comment in the "readonly" event handler to deltaManager set up by
466
- // the ContainerRuntime constructor
467
-
468
- if (this.shouldJoinWrite()) {
469
- // If we have pending changes, then we will never send them - it smells like
470
- // host logic error.
471
- this.logger.sendErrorEvent({ eventName: "ForceReadonlyPendingChanged" });
472
- }
242
+ // The prepareFlush event allows listeners to append metadata to the batch prior to submission.
243
+ this.emit("prepareSend", this.messageBuffer);
473
244
 
474
- reconnect = this.disconnectFromDeltaStream("Force readonly");
475
- }
476
- safeRaiseEvent(this, this.logger, "readonly", this.readonly);
477
- if (reconnect) {
478
- // reconnect if we disconnected from before.
479
- this.triggerConnect({ reason: "forceReadonly", mode: "read", fetchOpsFromStorage: false });
480
- }
481
- }
245
+ this.connectionManager.sendMessages(this.messageBuffer);
246
+ this.messageBuffer = [];
247
+ }
248
+
249
+ public get connectionProps(): ITelemetryProperties {
250
+ return {
251
+ sequenceNumber: this.lastSequenceNumber,
252
+ ...this.connectionManager.connectionProps,
253
+ };
482
254
  }
483
255
 
484
256
  /**
@@ -488,7 +260,7 @@ export class DeltaManager
488
260
  * @param event - Event to log.
489
261
  */
490
262
  public logConnectionIssue(event: ITelemetryErrorEvent) {
491
- assert(this.connection !== undefined, 0x238 /* "called only in connected state" */);
263
+ assert(this.connectionManager.connected, 0x238 /* "called only in connected state" */);
492
264
 
493
265
  const pendingSorted = this.pending.sort((a, b) => a.sequenceNumber - b.sequenceNumber);
494
266
  this.logger.sendErrorEvent({
@@ -501,7 +273,7 @@ export class DeltaManager
501
273
  lastProcessedSequenceNumber: this.lastProcessedSequenceNumber, // same as above, but after processing
502
274
  lastObserved: this.lastObservedSeqNumber, // last sequence we ever saw; may have gaps with above.
503
275
  // connection info
504
- ...this.connectionStateProps,
276
+ ...this.connectionManager.connectionVerboseProps,
505
277
  pendingOps: this.pending.length, // Do we have any pending ops?
506
278
  pendingFirst: pendingSorted[0]?.sequenceNumber, // is the first pending op the one that we are missing?
507
279
  haveHandler: this.handler !== undefined, // do we have handler installed?
@@ -510,26 +282,27 @@ export class DeltaManager
510
282
  });
511
283
  }
512
284
 
513
- private set_readonlyPermissions(readonly: boolean) {
514
- const oldValue = this.readonly;
515
- this._readonlyPermissions = readonly;
516
- if (oldValue !== this.readonly) {
517
- safeRaiseEvent(this, this.logger, "readonly", this.readonly);
518
- }
519
- }
520
-
521
285
  constructor(
522
286
  private readonly serviceProvider: () => IDocumentService | undefined,
523
- private client: IClient,
524
287
  private readonly logger: ITelemetryLogger,
525
- reconnectAllowed: boolean,
526
288
  private readonly _active: () => boolean,
289
+ createConnectionManager: (props: IConnectionManagerFactoryArgs) => TConnectionManager,
527
290
  ) {
528
291
  super();
292
+ const props: IConnectionManagerFactoryArgs = {
293
+ incomingOpHandler:(messages: ISequencedDocumentMessage[], reason: string) =>
294
+ this.enqueueMessages(messages, reason),
295
+ signalHandler: (message: ISignalMessage) => this._inboundSignal.push(message),
296
+ reconnectionDelayHandler: (delayMs: number, error: unknown) =>
297
+ this.emitDelayInfo(this.deltaStreamDelayId, delayMs, error),
298
+ closeHandler: (error: any) => this.close(error),
299
+ disconnectHandler: (reason: string) => this.disconnectHandler(reason),
300
+ connectHandler: (connection: IConnectionDetails) => this.connectHandler(connection),
301
+ pongHandler: (latency: number) => this.emit("pong", latency),
302
+ readonlyChangeHandler: (readonly?: boolean) => safeRaiseEvent(this, this.logger, "readonly", readonly),
303
+ };
529
304
 
530
- this.clientDetails = this.client.details;
531
- this.defaultReconnectionMode = this.client.mode;
532
- this._reconnectMode = reconnectAllowed ? ReconnectMode.Enabled : ReconnectMode.Never;
305
+ this.connectionManager = createConnectionManager(props);
533
306
 
534
307
  this._inbound = new DeltaQueue<ISequencedDocumentMessage>(
535
308
  (op) => {
@@ -540,20 +313,6 @@ export class DeltaManager
540
313
  this.close(CreateProcessingError(error, "deltaManagerInboundErrorHandler", this.lastMessage));
541
314
  });
542
315
 
543
- // Outbound message queue. The outbound queue is represented as a queue of an array of ops. Ops contained
544
- // within an array *must* fit within the maxMessageSize and are guaranteed to be ordered sequentially.
545
- this._outbound = new DeltaQueue<IDocumentMessage[]>(
546
- (messages) => {
547
- if (this.connection === undefined) {
548
- throw new Error("Attempted to submit an outbound message without connection");
549
- }
550
- this.connection.submit(messages);
551
- });
552
-
553
- this._outbound.on("error", (error) => {
554
- this.close(normalizeError(error));
555
- });
556
-
557
316
  // Inbound signal queue
558
317
  this._inboundSignal = new DeltaQueue<ISignalMessage>((message) => {
559
318
  if (this.handler === undefined) {
@@ -574,6 +333,47 @@ export class DeltaManager
574
333
  // - inbound & inboundSignal are resumed in attachOpHandler() when we have handler setup
575
334
  }
576
335
 
336
+ private connectHandler(connection: IConnectionDetails) {
337
+ this.refreshDelayInfo(this.deltaStreamDelayId);
338
+
339
+ const props = this.connectionManager.connectionVerboseProps;
340
+ props.connectionLastQueuedSequenceNumber = this.lastQueuedSequenceNumber;
341
+ props.connectionLastObservedSeqNumber = this.lastObservedSeqNumber;
342
+
343
+ const checkpointSequenceNumber = connection.checkpointSequenceNumber;
344
+ this._checkpointSequenceNumber = checkpointSequenceNumber;
345
+ if (checkpointSequenceNumber !== undefined) {
346
+ this.updateLatestKnownOpSeqNumber(checkpointSequenceNumber);
347
+ }
348
+
349
+ // We cancel all ops on lost of connectivity, and rely on DDSes to resubmit them.
350
+ // Semantics are not well defined for batches (and they are broken right now on disconnects anyway),
351
+ // but it's safe to assume (until better design is put into place) that batches should not exist
352
+ // across multiple connections. Right now we assume runtime will not submit any ops in disconnected
353
+ // state. As requirements change, so should these checks.
354
+ assert(this.messageBuffer.length === 0, 0x0e9 /* "messageBuffer is not empty on new connection" */);
355
+
356
+ this.emit(
357
+ "connect",
358
+ connection,
359
+ checkpointSequenceNumber !== undefined ?
360
+ this.lastObservedSeqNumber - this.lastSequenceNumber : undefined);
361
+
362
+ // If we got some initial ops, then we know the gap and call above fetched ops to fill it.
363
+ // Same is true for "write" mode even if we have no ops - we will get "join" own op very very soon.
364
+ // However if we are connecting as view-only, then there is no good signal to realize if client is behind.
365
+ // Thus we have to hit storage to see if any ops are there.
366
+ if (checkpointSequenceNumber !== undefined) {
367
+ // We know how far we are behind (roughly). If it's non-zero gap, fetch ops right away.
368
+ if (checkpointSequenceNumber > this.lastQueuedSequenceNumber) {
369
+ this.fetchMissingDeltas("AfterConnection");
370
+ }
371
+ // we do not know the gap, and we will not learn about it if socket is quite - have to ask.
372
+ } else if (connection.mode === "read") {
373
+ this.fetchMissingDeltas("AfterReadConnection");
374
+ }
375
+ }
376
+
577
377
  public dispose() {
578
378
  throw new Error("Not implemented.");
579
379
  }
@@ -625,7 +425,7 @@ export class DeltaManager
625
425
  // (which in most cases will happen when we are done processing cached ops)
626
426
  if (cacheOnly) {
627
427
  // fire and forget
628
- this.fetchMissingDeltas("DocumentOpen", this.lastQueuedSequenceNumber);
428
+ this.fetchMissingDeltas("DocumentOpen");
629
429
  }
630
430
  }
631
431
 
@@ -633,66 +433,12 @@ export class DeltaManager
633
433
  assert(this.fetchReason !== undefined || this.pending.length === 0, 0x269 /* "pending ops are not dropped" */);
634
434
  }
635
435
 
636
- private static detailsFromConnection(connection: IDocumentDeltaConnection): IConnectionDetails {
637
- return {
638
- claims: connection.claims,
639
- clientId: connection.clientId,
640
- existing: connection.existing,
641
- checkpointSequenceNumber: connection.checkpointSequenceNumber,
642
- get initialClients() { return connection.initialClients; },
643
- maxMessageSize: connection.serviceConfiguration.maxMessageSize,
644
- mode: connection.mode,
645
- serviceConfiguration: connection.serviceConfiguration,
646
- version: connection.version,
647
- };
648
- }
649
-
650
- public async connect(args: IConnectionArgs): Promise<IConnectionDetails> {
651
- const connection = await this.connectCore(args);
652
- return DeltaManager.detailsFromConnection(connection);
653
- }
654
-
655
- /**
656
- * Start the connection. Any error should result in container being close.
657
- * And report the error if it excape for any reason.
658
- * @param args - The connection arguments
659
- */
660
- private triggerConnect(args: IConnectionArgs) {
661
- assert(this.connection === undefined, 0x239 /* "called only in disconnected state" */);
662
- if (this.reconnectMode !== ReconnectMode.Enabled) {
663
- return;
664
- }
665
- this.connectCore(args).catch((err) => {
666
- // Errors are raised as "error" event and close container.
667
- // Have a catch-all case in case we missed something
668
- if (!this.closed) {
669
- this.logger.sendErrorEvent({ eventName: "ConnectException" }, err);
670
- }
671
- });
672
- }
673
-
674
- private async connectCore(args: IConnectionArgs): Promise<IDocumentDeltaConnection> {
675
- assert(!this.closed, 0x26a /* "not closed" */);
676
-
677
- if (this.connection !== undefined) {
678
- return this.connection;
679
- }
680
-
681
- if (this.connectionP !== undefined) {
682
- return this.connectionP;
683
- }
684
-
436
+ public connect(args: IConnectionArgs) {
685
437
  const fetchOpsFromStorage = args.fetchOpsFromStorage ?? true;
686
- let requestedMode = args.mode ?? this.defaultReconnectionMode;
687
-
688
- // if we have any non-acked ops from last connection, reconnect as "write".
689
- // without that we would connect in view-only mode, which will result in immediate
690
- // firing of "connected" event from Container and switch of current clientId (as tracked
691
- // by all DDSes). This will make it impossible to figure out if ops actually made it through,
692
- // so DDSes will immediately resubmit all pending ops, and some of them will be duplicates, corrupting document
693
- if (this.shouldJoinWrite()) {
694
- requestedMode = "write";
695
- }
438
+ logIfFalse(
439
+ this.handler !== undefined || !fetchOpsFromStorage,
440
+ this.logger,
441
+ "CantFetchWithoutBaseline"); // can't fetch if no baseline
696
442
 
697
443
  // Note: There is race condition here.
698
444
  // We want to issue request to storage as soon as possible, to
@@ -703,253 +449,12 @@ export class DeltaManager
703
449
  // own "join" message and realize any gap client has in ops.
704
450
  // But for view-only connection, we have no such signal, and with no traffic
705
451
  // on the wire, we might be always behind.
706
- // See comment at the end of setupNewSuccessfulConnection()
707
- logIfFalse(
708
- this.handler !== undefined || !fetchOpsFromStorage,
709
- this.logger,
710
- "CantFetchWithoutBaseline"); // can't fetch if no baseline
452
+ // See comment at the end of "connect" handler
711
453
  if (fetchOpsFromStorage) {
712
- this.fetchMissingDeltas(args.reason, this.lastQueuedSequenceNumber);
713
- }
714
-
715
- const docService = this.serviceProvider();
716
- if (docService === undefined) {
717
- throw new Error("Container is not attached");
718
- }
719
-
720
- if (docService.policies?.storageOnly === true) {
721
- const connection = new NoDeltaStream();
722
- this.connectionP = Promise.resolve(connection); // to keep setupNewSuccessfulConnection happy
723
- this.setupNewSuccessfulConnection(connection, "read");
724
- return connection;
725
- }
726
-
727
- // The promise returned from connectCore will settle with a resolved connection or reject with error
728
- const connectCore = async () => {
729
- let connection: IDocumentDeltaConnection | undefined;
730
- let delayMs = InitialReconnectDelayInMs;
731
- let connectRepeatCount = 0;
732
- const connectStartTime = performance.now();
733
- let lastError: any;
734
-
735
- // This loop will keep trying to connect until successful, with a delay between each iteration.
736
- while (connection === undefined) {
737
- if (this.closed) {
738
- throw new Error("Attempting to connect a closed DeltaManager");
739
- }
740
- connectRepeatCount++;
741
-
742
- try {
743
- this.client.mode = requestedMode;
744
- connection = await docService.connectToDeltaStream(this.client);
745
-
746
- if (connection.disposed) {
747
- // Nobody observed this connection, so drop it on the floor and retry.
748
- this.logger.sendTelemetryEvent({ eventName: "ReceivedClosedConnection" });
749
- connection = undefined;
750
- }
751
- } catch (origError) {
752
- if (typeof origError === "object" && origError !== null &&
753
- origError?.errorType === DeltaStreamConnectionForbiddenError.errorType) {
754
- connection = new NoDeltaStream();
755
- requestedMode = "read";
756
- break;
757
- }
758
-
759
- // Socket.io error when we connect to wrong socket, or hit some multiplexing bug
760
- if (!canRetryOnError(origError)) {
761
- const error = normalizeError(origError);
762
- this.close(error);
763
- throw error;
764
- }
765
-
766
- // Log error once - we get too many errors in logs when we are offline,
767
- // and unfortunately there is no reliable way to detect that.
768
- if (connectRepeatCount === 1) {
769
- logNetworkFailure(
770
- this.logger,
771
- {
772
- delay: delayMs, // milliseconds
773
- eventName: "DeltaConnectionFailureToConnect",
774
- duration: TelemetryLogger.formatTick(performance.now() - connectStartTime),
775
- },
776
- origError);
777
- }
778
-
779
- lastError = origError;
780
-
781
- const retryDelayFromError = getRetryDelayFromError(origError);
782
- delayMs = retryDelayFromError ?? Math.min(delayMs * 2, MaxReconnectDelayInMs);
783
-
784
- if (retryDelayFromError !== undefined) {
785
- this.emitDelayInfo(this.deltaStreamDelayId, retryDelayFromError, origError);
786
- }
787
- await waitForConnectedState(delayMs);
788
- }
789
- }
790
-
791
- // If we retried more than once, log an event about how long it took
792
- if (connectRepeatCount > 1) {
793
- this.logger.sendTelemetryEvent(
794
- {
795
- eventName: "MultipleDeltaConnectionFailures",
796
- attempts: connectRepeatCount,
797
- duration: TelemetryLogger.formatTick(performance.now() - connectStartTime),
798
- },
799
- lastError,
800
- );
801
- }
802
-
803
- this.setupNewSuccessfulConnection(connection, requestedMode);
804
-
805
- return connection;
806
- };
807
-
808
- // This promise settles as soon as we know the outcome of the connection attempt
809
- // Set it upfront, such that if connection is established (NoDeltaConnection) or rejected (bug in
810
- // connectToDeltaStream() implementation - throwing exception vs. returning rejected promise) in
811
- // synchronous way, we have this.connectionP setup for all the code to assert correctness of the flow.
812
- const deferred = new Deferred<IDocumentDeltaConnection>();
813
- this.connectionP = deferred.promise;
814
-
815
- // Regardless of how the connection attempt concludes, we'll clear the promise and remove the listener
816
- // Reject the connection promise if the DeltaManager gets closed during connection
817
- const cleanupAndReject = (error) => {
818
- this.connectionP = undefined;
819
- this.removeListener("closed", cleanupAndReject);
820
- // This error came from some logic error in this file. Fail-fast to learn and fix the issue faster
821
- this.close(error);
822
- deferred.reject(error);
823
- };
824
- this.on("closed", cleanupAndReject);
825
-
826
- // Attempt the connection
827
- connectCore().then((connection) => {
828
- this.removeListener("closed", cleanupAndReject);
829
- deferred.resolve(connection);
830
- }).catch(cleanupAndReject);
831
-
832
- return this.connectionP;
833
- }
834
-
835
- public flush() {
836
- if (this.messageBuffer.length === 0) {
837
- return;
838
- }
839
-
840
- // The prepareFlush event allows listeners to append metadata to the batch prior to submission.
841
- this.emit("prepareSend", this.messageBuffer);
842
-
843
- this._outbound.push(this.messageBuffer);
844
- this.messageBuffer = [];
845
- }
846
-
847
- /**
848
- * Submits the given delta returning the client sequence number for the message. Contents is the actual
849
- * contents of the message. appData is optional metadata that can be attached to the op by the app.
850
- *
851
- * If batch is set to true then the submit will be batched - and as a result guaranteed to be ordered sequentially
852
- * in the global sequencing space. The batch will be flushed either when flush is called or when a non-batched
853
- * op is submitted.
854
- */
855
- public submit(type: MessageType, contents: any, batch = false, metadata?: any): number {
856
- // TODO need to fail if gets too large
857
- // const serializedContent = JSON.stringify(this.messageBuffer);
858
- // const maxOpSize = this.context.deltaManager.maxMessageSize;
859
-
860
- if (this.readonly === true) {
861
- assert(this.readOnlyInfo.readonly === true, 0x1f0 /* "Unexpected mismatch in readonly" */);
862
- const error = new GenericError("deltaManagerReadonlySubmit", undefined /* error */, {
863
- readonly: this.readOnlyInfo.readonly,
864
- forcedReadonly: this.readOnlyInfo.forced,
865
- readonlyPermissions: this.readOnlyInfo.permissions,
866
- storageOnly: this.readOnlyInfo.storageOnly,
867
- });
868
- this.close(error);
869
- return -1;
870
- }
871
-
872
- // reset clientSequenceNumber if we are using new clientId.
873
- // we keep info about old connection as long as possible to be able to account for all non-acked ops
874
- // that we pick up on next connection.
875
- assert(!!this.connection, 0x0e4 /* "Lost old connection!" */);
876
- if (this.lastSubmittedClientId !== this.connection?.clientId) {
877
- this.lastSubmittedClientId = this.connection?.clientId;
878
- this.clientSequenceNumber = 0;
879
- this.clientSequenceNumberObserved = 0;
880
- }
881
-
882
- // If connection is "read" or implicit "read" (got leave op for "write" connection),
883
- // then op can't make it through - we will get a nack if op is sent.
884
- // We can short-circuit this process.
885
- // Note that we also want nacks to be rare and be treated as catastrophic failures.
886
- // Be careful with reentrancy though - disconnected event should not be be raised in the
887
- // middle of the current workflow, but rather on clean stack!
888
- if (this.connectionMode === "read") {
889
- if (!this.pendingReconnect) {
890
- this.pendingReconnect = true;
891
- Promise.resolve().then(async () => {
892
- if (this.pendingReconnect) { // still valid?
893
- return this.reconnectOnErrorCore(
894
- "write", // connectionMode
895
- "Switch to write", // message
896
- );
897
- }
898
- })
899
- .catch(() => {});
900
- }
901
-
902
- // Can return -1 here, but no other path does it (other than error path in Container),
903
- // so it's better not to introduce new states.
904
- return ++this.clientSequenceNumber;
905
- }
906
-
907
- const service = this.clientDetails.type === undefined || this.clientDetails.type === ""
908
- ? "unknown"
909
- : this.clientDetails.type;
910
-
911
- // Start adding trace for the op.
912
- const traces: ITrace[] = [
913
- {
914
- action: "start",
915
- service,
916
- timestamp: Date.now(),
917
- }];
918
-
919
- const message: IDocumentMessage = {
920
- clientSequenceNumber: ++this.clientSequenceNumber,
921
- contents: JSON.stringify(contents),
922
- metadata,
923
- referenceSequenceNumber: this.lastProcessedSequenceNumber,
924
- traces,
925
- type,
926
- };
927
-
928
- if (type === MessageType.NoOp) {
929
- this.trailingNoopCount++;
930
- } else {
931
- this.trailingNoopCount = 0;
454
+ this.fetchMissingDeltas(args.reason);
932
455
  }
933
456
 
934
- this.emit("submitOp", message);
935
-
936
- if (!batch) {
937
- this.flush();
938
- this.messageBuffer.push(message);
939
- this.flush();
940
- } else {
941
- this.messageBuffer.push(message);
942
- }
943
-
944
- return message.clientSequenceNumber;
945
- }
946
-
947
- public submitSignal(content: any) {
948
- if (this.connection !== undefined) {
949
- this.connection.submitSignal(content);
950
- } else {
951
- this.logger.sendErrorEvent({ eventName: "submitSignalDisconnected" });
952
- }
457
+ this.connectionManager.connect(args.mode);
953
458
  }
954
459
 
955
460
  private async getDeltas(
@@ -967,10 +472,6 @@ export class DeltaManager
967
472
  this.deltaStorage = await docService.connectToDeltaStorage();
968
473
  }
969
474
 
970
- assert(this.closeAbortController.signal.onabort === null, 0x1e8 /* "reentrancy" */);
971
- const controller = new AbortController();
972
- this.closeAbortController.signal.onabort = () => controller.abort();
973
-
974
475
  let cancelFetch: (op: ISequencedDocumentMessage) => boolean;
975
476
 
976
477
  if (to !== undefined) {
@@ -985,7 +486,7 @@ export class DeltaManager
985
486
  early: true,
986
487
  from,
987
488
  to,
988
- ...this.connectionStateProps,
489
+ ...this.connectionManager.connectionVerboseProps,
989
490
  });
990
491
  return;
991
492
  }
@@ -1004,6 +505,7 @@ export class DeltaManager
1004
505
  cancelFetch = (op: ISequencedDocumentMessage) => op.sequenceNumber >= this.lastObservedSeqNumber;
1005
506
  }
1006
507
 
508
+ const controller = new AbortController();
1007
509
  let opsFromFetch = false;
1008
510
 
1009
511
  const opListener = (op: ISequencedDocumentMessage) => {
@@ -1017,14 +519,17 @@ export class DeltaManager
1017
519
  }
1018
520
  };
1019
521
 
1020
- this._inbound.on("push", opListener);
1021
-
1022
522
  try {
523
+ this._inbound.on("push", opListener);
524
+ assert(this.closeAbortController.signal.onabort === null, 0x1e8 /* "reentrancy" */);
525
+ this.closeAbortController.signal.onabort = () => controller.abort();
526
+
1023
527
  const stream = this.deltaStorage.fetchMessages(
1024
528
  from, // inclusive
1025
529
  to, // exclusive
1026
530
  controller.signal,
1027
- cacheOnly);
531
+ cacheOnly,
532
+ this.fetchReason);
1028
533
 
1029
534
  // eslint-disable-next-line no-constant-condition
1030
535
  while (true) {
@@ -1040,9 +545,9 @@ export class DeltaManager
1040
545
  }
1041
546
  }
1042
547
  } finally {
1043
- assert(!opsFromFetch, 0x289 /* "logic error" */);
1044
548
  this.closeAbortController.signal.onabort = null;
1045
549
  this._inbound.off("push", opListener);
550
+ assert(!opsFromFetch, 0x289 /* "logic error" */);
1046
551
  }
1047
552
  }
1048
553
 
@@ -1054,16 +559,12 @@ export class DeltaManager
1054
559
  return;
1055
560
  }
1056
561
  this.closed = true;
1057
- // Ensure that things like triggerConnect() will short circuit
1058
- this._reconnectMode = ReconnectMode.Never;
1059
562
 
1060
- this.closeAbortController.abort();
563
+ this.connectionManager.dispose(error);
1061
564
 
1062
- // This raises "disconnect" event if we have active connection.
1063
- this.disconnectFromDeltaStream(error !== undefined ? `${error.message}` : "Container closed");
565
+ this.closeAbortController.abort();
1064
566
 
1065
567
  this._inbound.clear();
1066
- this._outbound.clear();
1067
568
  this._inboundSignal.clear();
1068
569
 
1069
570
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
@@ -1074,11 +575,6 @@ export class DeltaManager
1074
575
  // Drop pending messages - this will ensure catchUp() does not go into infinite loop
1075
576
  this.pending = [];
1076
577
 
1077
- // Notify everyone we are in read-only state.
1078
- // Useful for data stores in case we hit some critical error,
1079
- // to switch to a mode where user edits are not accepted
1080
- this.set_readonlyPermissions(true);
1081
-
1082
578
  // This needs to be the last thing we do (before removing listeners), as it causes
1083
579
  // Container to dispose context and break ability of data stores / runtime to "hear"
1084
580
  // from delta manager, including notification (above) about readonly state.
@@ -1094,6 +590,21 @@ export class DeltaManager
1094
590
  }
1095
591
  }
1096
592
 
593
+ private disconnectHandler(reason: string) {
594
+ if (this.messageBuffer.length > 0) {
595
+ // Behavior is not well defined here RE batches across connections / disconnect.
596
+ // DeltaManager overall policy - drop all ops on disconnection and rely on
597
+ // container runtime to deal with resubmitting any ops that did not make it through.
598
+ // So drop them, but also raise error event to look into details.
599
+ this.logger.sendErrorEvent({
600
+ eventName: "OpenBatchOnDisconnect",
601
+ length: this.messageBuffer.length,
602
+ });
603
+ this.messageBuffer.length = 0;
604
+ }
605
+ this.emit("disconnect", reason);
606
+ }
607
+
1097
608
  /**
1098
609
  * Emit info about a delay in service communication on account of throttling.
1099
610
  * @param id - Id of the connection that is delayed
@@ -1116,281 +627,6 @@ export class DeltaManager
1116
627
  }
1117
628
  }
1118
629
 
1119
- private readonly opHandler = (documentId: string, messagesArg: ISequencedDocumentMessage[]) => {
1120
- const messages = Array.isArray(messagesArg) ? messagesArg : [messagesArg];
1121
- this.enqueueMessages(messages, "opHandler");
1122
- };
1123
-
1124
- private readonly signalHandler = (message: ISignalMessage) => {
1125
- this._inboundSignal.push(message);
1126
- };
1127
-
1128
- // Always connect in write mode after getting nacked.
1129
- private readonly nackHandler = (documentId: string, messages: INack[]) => {
1130
- const message = messages[0];
1131
- // TODO: we should remove this check when service updates?
1132
- // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
1133
- if (this._readonlyPermissions) {
1134
- this.close(createWriteError("writeOnReadOnlyDocument"));
1135
- }
1136
-
1137
- // check message.content for Back-compat with old service.
1138
- const reconnectInfo = message.content !== undefined
1139
- ? getNackReconnectInfo(message.content) :
1140
- createGenericNetworkError("nackReasonUnknown", undefined, true);
1141
-
1142
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
1143
- this.reconnectOnError(
1144
- "write",
1145
- reconnectInfo,
1146
- );
1147
- };
1148
-
1149
- // Connection mode is always read on disconnect/error unless the system mode was write.
1150
- private readonly disconnectHandler = (disconnectReason) => {
1151
- // Note: we might get multiple disconnect calls on same socket, as early disconnect notification
1152
- // ("server_disconnect", ODSP-specific) is mapped to "disconnect"
1153
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
1154
- this.reconnectOnError(
1155
- this.defaultReconnectionMode,
1156
- createReconnectError("dmDocumentDeltaConnectionDisconnected", disconnectReason),
1157
- );
1158
- };
1159
-
1160
- private readonly errorHandler = (error) => {
1161
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
1162
- this.reconnectOnError(
1163
- this.defaultReconnectionMode,
1164
- createReconnectError("dmDocumentDeltaConnectionError", error),
1165
- );
1166
- };
1167
-
1168
- private readonly pongHandler = (latency: number) => {
1169
- this.emit("pong", latency);
1170
- };
1171
-
1172
- /**
1173
- * Once we've successfully gotten a connection, we need to set up state, attach event listeners, and process
1174
- * initial messages.
1175
- * @param connection - The newly established connection
1176
- */
1177
- private setupNewSuccessfulConnection(connection: IDocumentDeltaConnection, requestedMode: ConnectionMode) {
1178
- // Old connection should have been cleaned up before establishing a new one
1179
- assert(this.connection === undefined, 0x0e6 /* "old connection exists on new connection setup" */);
1180
- assert(this.connectionP !== undefined || this.closed,
1181
- 0x27f /* "reentrancy may result in incorrect behavior" */);
1182
- assert(!connection.disposed, 0x28a /* "can't be disposed - Callers need to ensure that!" */);
1183
-
1184
- this.connectionP = undefined;
1185
- this.connection = connection;
1186
-
1187
- // Does information in scopes & mode matches?
1188
- // If we asked for "write" and got "read", then file is read-only
1189
- // But if we ask read, server can still give us write.
1190
- const readonly = !connection.claims.scopes.includes(ScopeType.DocWrite);
1191
-
1192
- // This connection mode validation logic is moving to the driver layer in 0.44. These two asserts can be
1193
- // removed after those packages have released and become ubiquitous.
1194
- assert(requestedMode === "read" || readonly === (this.connectionMode === "read"),
1195
- 0x0e7 /* "claims/connectionMode mismatch" */);
1196
- assert(!readonly || this.connectionMode === "read", 0x0e8 /* "readonly perf with write connection" */);
1197
-
1198
- this.set_readonlyPermissions(readonly);
1199
-
1200
- this.refreshDelayInfo(this.deltaStreamDelayId);
1201
-
1202
- if (this.closed) {
1203
- // Raise proper events, Log telemetry event and close connection.
1204
- this.disconnectFromDeltaStream(`Disconnect on close`);
1205
- return;
1206
- }
1207
-
1208
- // We cancel all ops on lost of connectivity, and rely on DDSes to resubmit them.
1209
- // Semantics are not well defined for batches (and they are broken right now on disconnects anyway),
1210
- // but it's safe to assume (until better design is put into place) that batches should not exist
1211
- // across multiple connections. Right now we assume runtime will not submit any ops in disconnected
1212
- // state. As requirements change, so should these checks.
1213
- assert(this.messageBuffer.length === 0, 0x0e9 /* "messageBuffer is not empty on new connection" */);
1214
-
1215
- this._outbound.resume();
1216
-
1217
- connection.on("op", this.opHandler);
1218
- connection.on("signal", this.signalHandler);
1219
- connection.on("nack", this.nackHandler);
1220
- connection.on("disconnect", this.disconnectHandler);
1221
- connection.on("error", this.errorHandler);
1222
- connection.on("pong", this.pongHandler);
1223
-
1224
- // Initial messages are always sorted. However, due to early op handler installed by drivers and appending those
1225
- // ops to initialMessages, resulting set is no longer sorted, which would result in client hitting storage to
1226
- // fill in gap. We will recover by cancelling this request once we process remaining ops, but it's a waste that
1227
- // we could avoid
1228
- const initialMessages = connection.initialMessages.sort((a, b) => a.sequenceNumber - b.sequenceNumber);
1229
-
1230
- this.connectionStateProps = {
1231
- connectionLastQueuedSequenceNumber : this.lastQueuedSequenceNumber,
1232
- connectionLastObservedSeqNumber: this.lastObservedSeqNumber,
1233
- clientId: connection.clientId,
1234
- mode: connection.mode,
1235
- };
1236
- this._hasCheckpointSequenceNumber = false;
1237
-
1238
- // Some storages may provide checkpointSequenceNumber to identify how far client is behind.
1239
- const checkpointSequenceNumber = connection.checkpointSequenceNumber;
1240
- if (checkpointSequenceNumber !== undefined) {
1241
- this._hasCheckpointSequenceNumber = true;
1242
- this.updateLatestKnownOpSeqNumber(checkpointSequenceNumber);
1243
- }
1244
-
1245
- // Update knowledge of how far we are behind, before raising "connect" event
1246
- // This is duplication of what enqueueMessages() does, but we have to raise event before we get there,
1247
- // so duplicating update logic here as well.
1248
- const last = initialMessages.length > 0 ? initialMessages[initialMessages.length - 1].sequenceNumber : -1;
1249
- if (initialMessages.length > 0) {
1250
- this._hasCheckpointSequenceNumber = true;
1251
- this.updateLatestKnownOpSeqNumber(last);
1252
- }
1253
-
1254
- // Notify of the connection
1255
- // WARNING: This has to happen before processInitialMessages() call below.
1256
- // If not, we may not update Container.pendingClientId in time before seeing our own join session op.
1257
- this.emit(
1258
- "connect",
1259
- DeltaManager.detailsFromConnection(connection),
1260
- this._hasCheckpointSequenceNumber ? this.lastObservedSeqNumber - this.lastSequenceNumber : undefined);
1261
-
1262
- this.enqueueMessages(
1263
- initialMessages,
1264
- this.connectFirstConnection ? "InitialOps" : "ReconnectOps");
1265
-
1266
- if (connection.initialSignals !== undefined) {
1267
- for (const signal of connection.initialSignals) {
1268
- this._inboundSignal.push(signal);
1269
- }
1270
- }
1271
-
1272
- // If we got some initial ops, then we know the gap and call above fetched ops to fill it.
1273
- // Same is true for "write" mode even if we have no ops - we will get self "join" ops very very soon.
1274
- // However if we are connecting as view-only, then there is no good signal to realize if client is behind.
1275
- // Thus we have to hit storage to see if any ops are there.
1276
- if (initialMessages.length === 0) {
1277
- if (checkpointSequenceNumber !== undefined) {
1278
- // We know how far we are behind (roughly). If it's non-zero gap, fetch ops right away.
1279
- if (checkpointSequenceNumber > this.lastQueuedSequenceNumber) {
1280
- this.fetchMissingDeltas("AfterConnection", this.lastQueuedSequenceNumber);
1281
- }
1282
- // we do not know the gap, and we will not learn about it if socket is quite - have to ask.
1283
- } else if (connection.mode === "read") {
1284
- this.fetchMissingDeltas("AfterReadConnection", this.lastQueuedSequenceNumber);
1285
- }
1286
- } else {
1287
- this.connectionStateProps.connectionInitialOpsFrom = initialMessages[0].sequenceNumber;
1288
- this.connectionStateProps.connectionInitialOpsTo = last + 1;
1289
- }
1290
-
1291
- this.connectFirstConnection = false;
1292
- }
1293
-
1294
- /**
1295
- * Disconnect the current connection.
1296
- * @param reason - Text description of disconnect reason to emit with disconnect event
1297
- */
1298
- private disconnectFromDeltaStream(reason: string) {
1299
- this.pendingReconnect = false;
1300
- this.downgradedConnection = false;
1301
-
1302
- if (this.connection === undefined) {
1303
- return false;
1304
- }
1305
-
1306
- assert(this.connectionP === undefined, 0x27b /* "reentrancy may result in incorrect behavior" */);
1307
-
1308
- const connection = this.connection;
1309
- // Avoid any re-entrancy - clear object reference
1310
- this.connection = undefined;
1311
-
1312
- // Remove listeners first so we don't try to retrigger this flow accidentally through reconnectOnError
1313
- connection.off("op", this.opHandler);
1314
- connection.off("signal", this.signalHandler);
1315
- connection.off("nack", this.nackHandler);
1316
- connection.off("disconnect", this.disconnectHandler);
1317
- connection.off("error", this.errorHandler);
1318
- connection.off("pong", this.pongHandler);
1319
-
1320
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
1321
- this._outbound.pause();
1322
- this._outbound.clear();
1323
- this.emit("disconnect", reason);
1324
-
1325
- connection.dispose();
1326
-
1327
- this.connectionStateProps = {};
1328
-
1329
- return true;
1330
- }
1331
-
1332
- /**
1333
- * Disconnect the current connection and reconnect.
1334
- * @param connection - The connection that wants to reconnect - no-op if it's different from this.connection
1335
- * @param requestedMode - Read or write
1336
- * @param error - Error reconnect information including whether or not to reconnect
1337
- * @returns A promise that resolves when the connection is reestablished or we stop trying
1338
- */
1339
- private async reconnectOnError(
1340
- requestedMode: ConnectionMode,
1341
- error: DriverError,
1342
- ) {
1343
- return this.reconnectOnErrorCore(
1344
- requestedMode,
1345
- error.message,
1346
- error);
1347
- }
1348
-
1349
- /**
1350
- * Disconnect the current connection and reconnect.
1351
- * @param connection - The connection that wants to reconnect - no-op if it's different from this.connection
1352
- * @param requestedMode - Read or write
1353
- * @param error - Error reconnect information including whether or not to reconnect
1354
- * @returns A promise that resolves when the connection is reestablished or we stop trying
1355
- */
1356
- private async reconnectOnErrorCore(
1357
- requestedMode: ConnectionMode,
1358
- disconnectMessage: string,
1359
- error?: DriverError,
1360
- ) {
1361
- // We quite often get protocol errors before / after observing nack/disconnect
1362
- // we do not want to run through same sequence twice.
1363
- // If we're already disconnected/disconnecting it's not appropriate to call this again.
1364
- assert(this.connection !== undefined, 0x0eb /* "Missing connection for reconnect" */);
1365
-
1366
- this.disconnectFromDeltaStream(disconnectMessage);
1367
-
1368
- const canRetry = error !== undefined ? canRetryOnError(error) : true;
1369
-
1370
- // If reconnection is not an option, close the DeltaManager
1371
- if (this.reconnectMode === ReconnectMode.Never || !canRetry) {
1372
- // Do not raise container error if we are closing just because we lost connection.
1373
- // Those errors (like IdleDisconnect) would show up in telemetry dashboards and
1374
- // are very misleading, as first initial reaction - some logic is broken.
1375
- this.close(canRetry ? undefined : error);
1376
- }
1377
-
1378
- // If closed then we can't reconnect
1379
- if (this.closed) {
1380
- return;
1381
- }
1382
-
1383
- if (this.reconnectMode === ReconnectMode.Enabled) {
1384
- const delayMs = error !== undefined ? getRetryDelayFromError(error) : undefined;
1385
- if (delayMs !== undefined) {
1386
- this.emitDelayInfo(this.deltaStreamDelayId, delayMs, error);
1387
- await waitForConnectedState(delayMs);
1388
- }
1389
-
1390
- this.triggerConnect({ reason: "reconnect", mode: requestedMode, fetchOpsFromStorage: false });
1391
- }
1392
- }
1393
-
1394
630
  // returns parts of message (in string format) that should never change for a given message.
1395
631
  // Used for message comparison. It attempts to avoid comparing fields that potentially may differ.
1396
632
  // for example, it's not clear if serverMetadata or timestamp property is a property of message or server state.
@@ -1484,7 +720,7 @@ export class DeltaManager
1484
720
  gap: gap > 0 ? gap : undefined,
1485
721
  firstMissing,
1486
722
  dmInitialSeqNumber: this.initialSequenceNumber,
1487
- ...this.connectionStateProps,
723
+ ...this.connectionManager.connectionVerboseProps,
1488
724
  });
1489
725
  }
1490
726
  }
@@ -1509,7 +745,7 @@ export class DeltaManager
1509
745
  undefined,
1510
746
  DriverErrorType.fileOverwrittenInStorage,
1511
747
  {
1512
- clientId: this.connection?.clientId,
748
+ clientId: this.connectionManager.clientId,
1513
749
  sequenceNumber: message.sequenceNumber,
1514
750
  message1,
1515
751
  message2,
@@ -1520,7 +756,7 @@ export class DeltaManager
1520
756
  }
1521
757
  } else if (message.sequenceNumber !== this.lastQueuedSequenceNumber + 1) {
1522
758
  this.pending.push(message);
1523
- this.fetchMissingDeltas(reason, this.lastQueuedSequenceNumber, message.sequenceNumber);
759
+ this.fetchMissingDeltas(reason, message.sequenceNumber);
1524
760
  } else {
1525
761
  this.lastQueuedSequenceNumber = message.sequenceNumber;
1526
762
  this.previouslyProcessedMessage = message;
@@ -1546,24 +782,6 @@ export class DeltaManager
1546
782
  0x0ed /* "non-system message have to have clientId" */,
1547
783
  );
1548
784
 
1549
- // if we have connection, and message is local, then we better treat is as local!
1550
- assert(
1551
- this.connection === undefined
1552
- || this.connection.clientId !== message.clientId
1553
- || this.lastSubmittedClientId === message.clientId,
1554
- 0x0ee /* "Not accounting local messages correctly" */,
1555
- );
1556
-
1557
- if (this.lastSubmittedClientId !== undefined && this.lastSubmittedClientId === message.clientId) {
1558
- const clientSequenceNumber = message.clientSequenceNumber;
1559
-
1560
- assert(this.clientSequenceNumberObserved < clientSequenceNumber, 0x0ef /* "client seq# not growing" */);
1561
- assert(clientSequenceNumber <= this.clientSequenceNumber,
1562
- 0x0f0 /* "Incoming local client seq# > generated by this client" */);
1563
-
1564
- this.clientSequenceNumberObserved = clientSequenceNumber;
1565
- }
1566
-
1567
785
  // TODO Remove after SPO picks up the latest build.
1568
786
  if (
1569
787
  typeof message.contents === "string"
@@ -1573,26 +791,13 @@ export class DeltaManager
1573
791
  message.contents = JSON.parse(message.contents);
1574
792
  }
1575
793
 
1576
- if (message.type === MessageType.ClientLeave) {
1577
- const systemLeaveMessage = message as ISequencedDocumentSystemMessage;
1578
- const clientId = JSON.parse(systemLeaveMessage.data) as string;
1579
- if (clientId === this.connection?.clientId) {
1580
- // We have been kicked out from quorum
1581
- this.logger.sendPerformanceEvent({ eventName: "ReadConnectionTransition" });
1582
- this.downgradedConnection = true;
1583
- assert(this.connectionMode === "read",
1584
- 0x27c /* "effective connectionMode should be 'read' after downgrade" */);
1585
- }
1586
- }
794
+ this.connectionManager.beforeProcessingIncomingOp(message);
1587
795
 
1588
796
  // Add final ack trace.
1589
797
  if (message.traces !== undefined && message.traces.length > 0) {
1590
- const service = this.clientDetails.type === undefined || this.clientDetails.type === ""
1591
- ? "unknown"
1592
- : this.clientDetails.type;
1593
798
  message.traces.push({
1594
799
  action: "end",
1595
- service,
800
+ service: "client",
1596
801
  timestamp: Date.now(),
1597
802
  });
1598
803
  }
@@ -1601,7 +806,7 @@ export class DeltaManager
1601
806
  if (this.minSequenceNumber > message.minimumSequenceNumber) {
1602
807
  throw new DataCorruptionError("msnMovesBackwards", {
1603
808
  ...extractLogSafeMessageProperties(message),
1604
- clientId: this.connection?.clientId,
809
+ clientId: this.connectionManager.clientId,
1605
810
  });
1606
811
  }
1607
812
  this.minSequenceNumber = message.minimumSequenceNumber;
@@ -1609,7 +814,7 @@ export class DeltaManager
1609
814
  if (message.sequenceNumber !== this.lastProcessedSequenceNumber + 1) {
1610
815
  throw new DataCorruptionError("nonSequentialSequenceNumber", {
1611
816
  ...extractLogSafeMessageProperties(message),
1612
- clientId: this.connection?.clientId,
817
+ clientId: this.connectionManager.clientId,
1613
818
  });
1614
819
  }
1615
820
  this.lastProcessedSequenceNumber = message.sequenceNumber;
@@ -1639,8 +844,8 @@ export class DeltaManager
1639
844
  /**
1640
845
  * Retrieves the missing deltas between the given sequence numbers
1641
846
  */
1642
- private fetchMissingDeltas(reasonArg: string, lastKnowOp: number, to?: number) {
1643
- this.fetchMissingDeltasCore(reasonArg, false /* cacheOnly */, lastKnowOp, to).catch((error) => {
847
+ private fetchMissingDeltas(reasonArg: string, to?: number) {
848
+ this.fetchMissingDeltasCore(reasonArg, false /* cacheOnly */, to).catch((error) => {
1644
849
  this.logger.sendErrorEvent({ eventName: "fetchMissingDeltasException" }, error);
1645
850
  });
1646
851
  }
@@ -1651,7 +856,6 @@ export class DeltaManager
1651
856
  private async fetchMissingDeltasCore(
1652
857
  reason: string,
1653
858
  cacheOnly: boolean,
1654
- lastKnowOp: number,
1655
859
  to?: number)
1656
860
  {
1657
861
  // Exit out early if we're already fetching deltas
@@ -1666,13 +870,12 @@ export class DeltaManager
1666
870
 
1667
871
  if (this.handler === undefined) {
1668
872
  // We do not poses yet any information
1669
- assert(lastKnowOp === 0, 0x26b /* "initial state" */);
873
+ assert(this.lastQueuedSequenceNumber === 0, 0x26b /* "initial state" */);
1670
874
  return;
1671
875
  }
1672
876
 
1673
877
  try {
1674
- assert(lastKnowOp === this.lastQueuedSequenceNumber, 0x0f1 /* "from arg" */);
1675
- let from = lastKnowOp + 1;
878
+ let from = this.lastQueuedSequenceNumber + 1;
1676
879
 
1677
880
  const n = this.previouslyProcessedMessage?.sequenceNumber;
1678
881
  if (n !== undefined) {
@@ -1681,7 +884,7 @@ export class DeltaManager
1681
884
  // Knowing about this mechanism, we could ask for op we already observed to increase validation.
1682
885
  // This is especially useful when coming out of offline mode or loading from
1683
886
  // very old cached (by client / driver) snapshot.
1684
- assert(n === lastKnowOp, 0x0f2 /* "previouslyProcessedMessage" */);
887
+ assert(n === this.lastQueuedSequenceNumber, 0x0f2 /* "previouslyProcessedMessage" */);
1685
888
  assert(from > 1, 0x0f3 /* "not positive" */);
1686
889
  from--;
1687
890
  }
@@ -1737,7 +940,7 @@ export class DeltaManager
1737
940
  // (the other 50%), and thus these errors below should be looked at even if code below results in
1738
941
  // recovery.
1739
942
  if (this.lastQueuedSequenceNumber < this.lastObservedSeqNumber) {
1740
- this.fetchMissingDeltas("OpsBehind", this.lastQueuedSequenceNumber);
943
+ this.fetchMissingDeltas("OpsBehind");
1741
944
  }
1742
945
  }
1743
946
  }