@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.
- package/dist/connectionManager.d.ts +153 -0
- package/dist/connectionManager.d.ts.map +1 -0
- package/dist/connectionManager.js +664 -0
- package/dist/connectionManager.js.map +1 -0
- package/dist/connectionStateHandler.d.ts +1 -0
- package/dist/connectionStateHandler.d.ts.map +1 -1
- package/dist/connectionStateHandler.js +6 -0
- package/dist/connectionStateHandler.js.map +1 -1
- package/dist/container.d.ts +2 -22
- package/dist/container.d.ts.map +1 -1
- package/dist/container.js +121 -151
- package/dist/container.js.map +1 -1
- package/dist/containerContext.d.ts +1 -0
- package/dist/containerContext.d.ts.map +1 -1
- package/dist/containerContext.js +4 -0
- package/dist/containerContext.js.map +1 -1
- package/dist/contracts.d.ts +112 -0
- package/dist/contracts.d.ts.map +1 -0
- package/dist/contracts.js +14 -0
- package/dist/contracts.js.map +1 -0
- package/dist/deltaManager.d.ts +26 -142
- package/dist/deltaManager.d.ts.map +1 -1
- package/dist/deltaManager.js +143 -770
- package/dist/deltaManager.js.map +1 -1
- package/dist/loader.d.ts +14 -4
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +10 -4
- package/dist/loader.js.map +1 -1
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.d.ts.map +1 -1
- package/dist/packageVersion.js +1 -1
- package/dist/packageVersion.js.map +1 -1
- package/dist/protocolTreeDocumentStorageService.d.ts +2 -2
- package/dist/protocolTreeDocumentStorageService.d.ts.map +1 -1
- package/lib/connectionManager.d.ts +153 -0
- package/lib/connectionManager.d.ts.map +1 -0
- package/lib/connectionManager.js +660 -0
- package/lib/connectionManager.js.map +1 -0
- package/lib/connectionStateHandler.d.ts +1 -0
- package/lib/connectionStateHandler.d.ts.map +1 -1
- package/lib/connectionStateHandler.js +6 -0
- package/lib/connectionStateHandler.js.map +1 -1
- package/lib/container.d.ts +2 -22
- package/lib/container.d.ts.map +1 -1
- package/lib/container.js +122 -152
- package/lib/container.js.map +1 -1
- package/lib/containerContext.d.ts +1 -0
- package/lib/containerContext.d.ts.map +1 -1
- package/lib/containerContext.js +4 -0
- package/lib/containerContext.js.map +1 -1
- package/lib/contracts.d.ts +112 -0
- package/lib/contracts.d.ts.map +1 -0
- package/lib/contracts.js +11 -0
- package/lib/contracts.js.map +1 -0
- package/lib/deltaManager.d.ts +26 -142
- package/lib/deltaManager.d.ts.map +1 -1
- package/lib/deltaManager.js +147 -774
- package/lib/deltaManager.js.map +1 -1
- package/lib/loader.d.ts +14 -4
- package/lib/loader.d.ts.map +1 -1
- package/lib/loader.js +11 -5
- package/lib/loader.js.map +1 -1
- package/lib/packageVersion.d.ts +1 -1
- package/lib/packageVersion.d.ts.map +1 -1
- package/lib/packageVersion.js +1 -1
- package/lib/packageVersion.js.map +1 -1
- package/lib/protocolTreeDocumentStorageService.d.ts +2 -2
- package/lib/protocolTreeDocumentStorageService.d.ts.map +1 -1
- package/package.json +9 -9
- package/src/connectionManager.ts +892 -0
- package/src/connectionStateHandler.ts +8 -0
- package/src/container.ts +165 -187
- package/src/containerContext.ts +4 -0
- package/src/contracts.ts +156 -0
- package/src/deltaManager.ts +181 -978
- package/src/loader.ts +59 -27
- package/src/packageVersion.ts +1 -1
package/src/deltaManager.ts
CHANGED
|
@@ -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
|
-
|
|
21
|
+
IConnectionDetails,
|
|
24
22
|
} from "@fluidframework/container-definitions";
|
|
25
|
-
import { assert,
|
|
23
|
+
import { assert, TypedEventEmitter } from "@fluidframework/common-utils";
|
|
26
24
|
import {
|
|
27
|
-
TelemetryLogger,
|
|
28
|
-
safeRaiseEvent,
|
|
29
|
-
logIfFalse,
|
|
30
25
|
normalizeError,
|
|
31
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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
|
-
|
|
253
|
-
private
|
|
128
|
+
private readonly deltaStorageDelayId = uuid();
|
|
129
|
+
private readonly deltaStreamDelayId = uuid();
|
|
254
130
|
|
|
255
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
352
|
-
return true;
|
|
353
|
-
}
|
|
354
|
-
return this._readonlyPermissions;
|
|
196
|
+
return this.readOnlyInfo.readonly;
|
|
355
197
|
}
|
|
356
198
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
-
|
|
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
|
-
|
|
390
|
-
|
|
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
|
-
|
|
417
|
-
|
|
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
|
-
|
|
235
|
+
public submitSignal(content: any) { return this.connectionManager.submitSignal(content); }
|
|
424
236
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
237
|
+
public flush() {
|
|
238
|
+
if (this.messageBuffer.length === 0) {
|
|
239
|
+
return;
|
|
428
240
|
}
|
|
429
|
-
}
|
|
430
241
|
|
|
431
|
-
|
|
432
|
-
|
|
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
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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"
|
|
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
|
-
|
|
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
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
563
|
+
this.connectionManager.dispose(error);
|
|
1061
564
|
|
|
1062
|
-
|
|
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.
|
|
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.
|
|
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,
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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,
|
|
1643
|
-
this.fetchMissingDeltasCore(reasonArg, false /* cacheOnly */,
|
|
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(
|
|
873
|
+
assert(this.lastQueuedSequenceNumber === 0, 0x26b /* "initial state" */);
|
|
1670
874
|
return;
|
|
1671
875
|
}
|
|
1672
876
|
|
|
1673
877
|
try {
|
|
1674
|
-
|
|
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 ===
|
|
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"
|
|
943
|
+
this.fetchMissingDeltas("OpsBehind");
|
|
1741
944
|
}
|
|
1742
945
|
}
|
|
1743
946
|
}
|