@fluidframework/container-loader 0.53.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/container.d.ts +2 -1
- package/dist/container.d.ts.map +1 -1
- package/dist/container.js +25 -28
- package/dist/container.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 -135
- package/dist/deltaManager.d.ts.map +1 -1
- package/dist/deltaManager.js +142 -767
- package/dist/deltaManager.js.map +1 -1
- package/dist/loader.d.ts +9 -4
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +5 -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/container.d.ts +2 -1
- package/lib/container.d.ts.map +1 -1
- package/lib/container.js +25 -28
- package/lib/container.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 -135
- package/lib/deltaManager.d.ts.map +1 -1
- package/lib/deltaManager.js +146 -771
- package/lib/deltaManager.js.map +1 -1
- package/lib/loader.d.ts +9 -4
- package/lib/loader.d.ts.map +1 -1
- package/lib/loader.js +6 -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 +8 -8
- package/src/connectionManager.ts +892 -0
- package/src/container.ts +39 -39
- package/src/contracts.ts +156 -0
- package/src/deltaManager.ts +181 -978
- package/src/loader.ts +31 -9
- package/src/packageVersion.ts +1 -1
package/src/deltaManager.ts
CHANGED
|
@@ -6,94 +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
|
-
);
|
|
95
|
-
|
|
96
|
-
const fatalConnectErrorProp = { fatalConnectError: true };
|
|
52
|
+
import {
|
|
53
|
+
IConnectionManagerFactoryArgs,
|
|
54
|
+
IConnectionManager,
|
|
55
|
+
} from "./contracts";
|
|
97
56
|
|
|
98
57
|
export interface IConnectionArgs {
|
|
99
58
|
mode?: ConnectionMode;
|
|
@@ -101,12 +60,6 @@ export interface IConnectionArgs {
|
|
|
101
60
|
reason: string;
|
|
102
61
|
}
|
|
103
62
|
|
|
104
|
-
export enum ReconnectMode {
|
|
105
|
-
Never = "Never",
|
|
106
|
-
Disabled = "Disabled",
|
|
107
|
-
Enabled = "Enabled",
|
|
108
|
-
}
|
|
109
|
-
|
|
110
63
|
/**
|
|
111
64
|
* Includes events emitted by the concrete implementation DeltaManager
|
|
112
65
|
* but not exposed on the public interface IDeltaManager
|
|
@@ -116,82 +69,24 @@ export interface IDeltaManagerInternalEvents extends IDeltaManagerEvents {
|
|
|
116
69
|
(event: "closed", listener: (error?: ICriticalContainerError) => void);
|
|
117
70
|
}
|
|
118
71
|
|
|
119
|
-
/**
|
|
120
|
-
* Implementation of IDocumentDeltaConnection that does not support submitting
|
|
121
|
-
* or receiving ops. Used in storage-only mode.
|
|
122
|
-
*/
|
|
123
|
-
class NoDeltaStream
|
|
124
|
-
extends TypedEventEmitter<IDocumentDeltaConnectionEvents>
|
|
125
|
-
implements IDocumentDeltaConnection, IDisposable
|
|
126
|
-
{
|
|
127
|
-
clientId: string = "storage-only client";
|
|
128
|
-
claims: ITokenClaims = {
|
|
129
|
-
scopes: [ScopeType.DocRead],
|
|
130
|
-
} as any;
|
|
131
|
-
mode: ConnectionMode = "read";
|
|
132
|
-
existing: boolean = true;
|
|
133
|
-
maxMessageSize: number = 0;
|
|
134
|
-
version: string = "";
|
|
135
|
-
initialMessages: ISequencedDocumentMessage[] = [];
|
|
136
|
-
initialSignals: ISignalMessage[] = [];
|
|
137
|
-
initialClients: ISignalClient[] = [];
|
|
138
|
-
serviceConfiguration: IClientConfiguration = {
|
|
139
|
-
maxMessageSize: 0,
|
|
140
|
-
blockSize: 0,
|
|
141
|
-
summary: undefined as any,
|
|
142
|
-
};
|
|
143
|
-
checkpointSequenceNumber?: number | undefined = undefined;
|
|
144
|
-
submit(messages: IDocumentMessage[]): void {
|
|
145
|
-
this.emit("nack", this.clientId, messages.map((operation) => {
|
|
146
|
-
return {
|
|
147
|
-
operation,
|
|
148
|
-
content: { message: "Cannot submit with storage-only connection", code: 403 },
|
|
149
|
-
};
|
|
150
|
-
}));
|
|
151
|
-
}
|
|
152
|
-
submitSignal(message: any): void {
|
|
153
|
-
this.emit("nack", this.clientId, {
|
|
154
|
-
operation: message,
|
|
155
|
-
content: { message: "Cannot submit signal with storage-only connection", code: 403 },
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
private _disposed = false;
|
|
160
|
-
public get disposed() { return this._disposed; }
|
|
161
|
-
public dispose() { this._disposed = true; }
|
|
162
|
-
}
|
|
163
|
-
|
|
164
72
|
/**
|
|
165
73
|
* Manages the flow of both inbound and outbound messages. This class ensures that shared objects receive delta
|
|
166
74
|
* messages in order regardless of possible network conditions or timings causing out of order delivery.
|
|
167
75
|
*/
|
|
168
|
-
export class DeltaManager
|
|
76
|
+
export class DeltaManager<TConnectionManager extends IConnectionManager>
|
|
169
77
|
extends TypedEventEmitter<IDeltaManagerInternalEvents>
|
|
170
78
|
implements
|
|
171
79
|
IDeltaManager<ISequencedDocumentMessage, IDocumentMessage>,
|
|
172
80
|
IEventProvider<IDeltaManagerInternalEvents>
|
|
173
81
|
{
|
|
82
|
+
public readonly connectionManager: TConnectionManager;
|
|
83
|
+
|
|
174
84
|
public get active(): boolean { return this._active(); }
|
|
175
85
|
|
|
176
86
|
public get disposed() { return this.closed; }
|
|
177
87
|
|
|
178
|
-
public readonly clientDetails: IClientDetails;
|
|
179
88
|
public get IDeltaSender() { return this; }
|
|
180
89
|
|
|
181
|
-
/**
|
|
182
|
-
* Controls whether the DeltaManager will automatically reconnect to the delta stream after receiving a disconnect.
|
|
183
|
-
*/
|
|
184
|
-
private _reconnectMode: ReconnectMode;
|
|
185
|
-
|
|
186
|
-
// file ACL - whether user has only read-only access to a file
|
|
187
|
-
private _readonlyPermissions: boolean | undefined;
|
|
188
|
-
|
|
189
|
-
// tracks host requiring read-only mode.
|
|
190
|
-
private _forceReadonly = false;
|
|
191
|
-
|
|
192
|
-
// Connection mode used when reconnecting on error or disconnect.
|
|
193
|
-
private readonly defaultReconnectionMode: ConnectionMode;
|
|
194
|
-
|
|
195
90
|
private pending: ISequencedDocumentMessage[] = [];
|
|
196
91
|
private fetchReason: string | undefined;
|
|
197
92
|
|
|
@@ -219,62 +114,28 @@ export class DeltaManager
|
|
|
219
114
|
|
|
220
115
|
private readonly _inbound: DeltaQueue<ISequencedDocumentMessage>;
|
|
221
116
|
private readonly _inboundSignal: DeltaQueue<ISignalMessage>;
|
|
222
|
-
private readonly _outbound: DeltaQueue<IDocumentMessage[]>;
|
|
223
|
-
|
|
224
|
-
private connectionP: Promise<IDocumentDeltaConnection> | undefined;
|
|
225
|
-
private connection: IDocumentDeltaConnection | undefined;
|
|
226
|
-
private clientSequenceNumber = 0;
|
|
227
|
-
private clientSequenceNumberObserved = 0;
|
|
228
|
-
// Counts the number of noops sent by the client which may not be acked.
|
|
229
|
-
private trailingNoopCount = 0;
|
|
230
|
-
private closed = false;
|
|
231
|
-
private readonly deltaStreamDelayId = uuid();
|
|
232
|
-
private readonly deltaStorageDelayId = uuid();
|
|
233
117
|
|
|
234
|
-
|
|
235
|
-
private lastSubmittedClientId: string | undefined;
|
|
118
|
+
private closed = false;
|
|
236
119
|
|
|
237
120
|
private handler: IDeltaHandlerStrategy | undefined;
|
|
238
121
|
private deltaStorage: IDocumentDeltaStorageService | undefined;
|
|
239
122
|
|
|
240
|
-
private messageBuffer: IDocumentMessage[] = [];
|
|
241
|
-
|
|
242
|
-
private connectFirstConnection = true;
|
|
243
123
|
private readonly throttlingIdSet = new Set<string>();
|
|
244
124
|
private timeTillThrottling: number = 0;
|
|
245
125
|
|
|
246
|
-
private connectionStateProps: Record<string, string | number> = {};
|
|
247
|
-
|
|
248
|
-
// True if current connection has checkpoint information
|
|
249
|
-
// I.e. we know how far behind the client was at the time of establishing connection
|
|
250
|
-
private _hasCheckpointSequenceNumber = false;
|
|
251
|
-
|
|
252
126
|
private readonly closeAbortController = new AbortController();
|
|
253
127
|
|
|
254
|
-
|
|
255
|
-
private
|
|
128
|
+
private readonly deltaStorageDelayId = uuid();
|
|
129
|
+
private readonly deltaStreamDelayId = uuid();
|
|
256
130
|
|
|
257
|
-
|
|
258
|
-
private downgradedConnection = false;
|
|
131
|
+
private messageBuffer: IDocumentMessage[] = [];
|
|
259
132
|
|
|
260
|
-
|
|
261
|
-
* Tells if current connection has checkpoint information.
|
|
262
|
-
* I.e. we know how far behind the client was at the time of establishing connection
|
|
263
|
-
*/
|
|
264
|
-
public get hasCheckpointSequenceNumber() {
|
|
265
|
-
// Valid to be called only if we have active connection.
|
|
266
|
-
assert(this.connection !== undefined, 0x0df /* "Missing active connection" */);
|
|
267
|
-
return this._hasCheckpointSequenceNumber;
|
|
268
|
-
}
|
|
133
|
+
private _checkpointSequenceNumber: number | undefined;
|
|
269
134
|
|
|
270
135
|
public get inbound(): IDeltaQueue<ISequencedDocumentMessage> {
|
|
271
136
|
return this._inbound;
|
|
272
137
|
}
|
|
273
138
|
|
|
274
|
-
public get outbound(): IDeltaQueue<IDocumentMessage[]> {
|
|
275
|
-
return this._outbound;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
139
|
public get inboundSignal(): IDeltaQueue<ISignalMessage> {
|
|
279
140
|
return this._inboundSignal;
|
|
280
141
|
}
|
|
@@ -303,42 +164,24 @@ export class DeltaManager
|
|
|
303
164
|
return this.minSequenceNumber;
|
|
304
165
|
}
|
|
305
166
|
|
|
306
|
-
public get maxMessageSize(): number {
|
|
307
|
-
return this.connection?.serviceConfiguration?.maxMessageSize
|
|
308
|
-
?? DefaultChunkSize;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
public get version(): string {
|
|
312
|
-
if (this.connection === undefined) {
|
|
313
|
-
throw new Error("Cannot check version without a connection");
|
|
314
|
-
}
|
|
315
|
-
return this.connection.version;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
public get serviceConfiguration(): IClientConfiguration | undefined {
|
|
319
|
-
return this.connection?.serviceConfiguration;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
public get scopes(): string[] | undefined {
|
|
323
|
-
return this.connection?.claims.scopes;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
public get socketDocumentId(): string | undefined {
|
|
327
|
-
return this.connection?.claims.documentId;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
167
|
/**
|
|
331
|
-
*
|
|
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
|
|
332
170
|
*/
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
return "read";
|
|
338
|
-
}
|
|
339
|
-
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;
|
|
340
175
|
}
|
|
341
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
|
+
|
|
342
185
|
/**
|
|
343
186
|
* Tells if container is in read-only mode.
|
|
344
187
|
* Data stores should listen for "readonly" notifications and disallow user
|
|
@@ -349,129 +192,65 @@ export class DeltaManager
|
|
|
349
192
|
* and do not know if user has write access to a file.
|
|
350
193
|
* @deprecated - use readOnlyInfo
|
|
351
194
|
*/
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
return true;
|
|
355
|
-
}
|
|
356
|
-
return this._readonlyPermissions;
|
|
195
|
+
public get readonly() {
|
|
196
|
+
return this.readOnlyInfo.readonly;
|
|
357
197
|
}
|
|
358
198
|
|
|
359
|
-
public
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
};
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
return { readonly: this._readonlyPermissions };
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
/**
|
|
374
|
-
* Automatic reconnecting enabled or disabled.
|
|
375
|
-
* If set to Never, then reconnecting will never be allowed.
|
|
376
|
-
*/
|
|
377
|
-
public get reconnectMode(): ReconnectMode {
|
|
378
|
-
return this._reconnectMode;
|
|
379
|
-
}
|
|
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
|
+
}];
|
|
380
207
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
* about current or last connection (if there is no connection at the moment)
|
|
388
|
-
*/
|
|
389
|
-
public connectionProps(): ITelemetryProperties {
|
|
390
|
-
const common = {
|
|
391
|
-
sequenceNumber: this.lastSequenceNumber,
|
|
208
|
+
const messagePartial: Omit<IDocumentMessage, "clientSequenceNumber"> = {
|
|
209
|
+
contents: JSON.stringify(contents),
|
|
210
|
+
metadata,
|
|
211
|
+
referenceSequenceNumber: this.lastProcessedSequenceNumber,
|
|
212
|
+
traces,
|
|
213
|
+
type,
|
|
392
214
|
};
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
connectionMode: this.connectionMode,
|
|
397
|
-
relayServiceAgent: this.connection.relayServiceAgent,
|
|
398
|
-
};
|
|
399
|
-
} else {
|
|
400
|
-
return {
|
|
401
|
-
...common,
|
|
402
|
-
// Report how many ops this client sent in last disconnected session
|
|
403
|
-
sentOps: this.clientSequenceNumber,
|
|
404
|
-
};
|
|
215
|
+
|
|
216
|
+
if (!batch) {
|
|
217
|
+
this.flush();
|
|
405
218
|
}
|
|
406
|
-
}
|
|
407
219
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
public setAutoReconnect(mode: ReconnectMode): void {
|
|
413
|
-
assert(mode !== ReconnectMode.Never && this._reconnectMode !== ReconnectMode.Never,
|
|
414
|
-
0x278 /* "API is not supported for non-connecting or closed container" */);
|
|
220
|
+
const message = this.connectionManager.prepareMessageToSend(messagePartial);
|
|
221
|
+
if (message === undefined) {
|
|
222
|
+
return -1;
|
|
223
|
+
}
|
|
415
224
|
|
|
416
|
-
this.
|
|
225
|
+
this.messageBuffer.push(message);
|
|
417
226
|
|
|
418
|
-
if (
|
|
419
|
-
|
|
420
|
-
this.disconnectFromDeltaStream("setAutoReconnect");
|
|
227
|
+
if (!batch) {
|
|
228
|
+
this.flush();
|
|
421
229
|
}
|
|
230
|
+
|
|
231
|
+
this.emit("submitOp", message);
|
|
232
|
+
return message.clientSequenceNumber;
|
|
422
233
|
}
|
|
423
234
|
|
|
424
|
-
|
|
425
|
-
* Sends signal to runtime (and data stores) to be read-only.
|
|
426
|
-
* Hosts may have read only views, indicating to data stores that no edits are allowed.
|
|
427
|
-
* This is independent from this._readonlyPermissions (permissions) and this.connectionMode
|
|
428
|
-
* (server can return "write" mode even when asked for "read")
|
|
429
|
-
* Leveraging same "readonly" event as runtime & data stores should behave the same in such case
|
|
430
|
-
* as in read-only permissions.
|
|
431
|
-
* But this.active can be used by some DDSes to figure out if ops can be sent
|
|
432
|
-
* (for example, read-only view still participates in code proposals / upgrades decisions)
|
|
433
|
-
*
|
|
434
|
-
* Forcing Readonly does not prevent DDS from generating ops. It is up to user code to honour
|
|
435
|
-
* the readonly flag. If ops are generated, they will accumulate locally and not be sent. If
|
|
436
|
-
* there are pending in the outbound queue, it will stop sending until force readonly is
|
|
437
|
-
* cleared.
|
|
438
|
-
*
|
|
439
|
-
* @param readonly - set or clear force readonly.
|
|
440
|
-
*/
|
|
441
|
-
public forceReadonly(readonly: boolean) {
|
|
442
|
-
if (readonly !== this._forceReadonly) {
|
|
443
|
-
this.logger.sendTelemetryEvent({
|
|
444
|
-
eventName: "ForceReadOnly",
|
|
445
|
-
value: readonly,
|
|
446
|
-
});
|
|
447
|
-
}
|
|
448
|
-
const oldValue = this.readOnlyInfo.readonly;
|
|
449
|
-
this._forceReadonly = readonly;
|
|
450
|
-
|
|
451
|
-
if (oldValue !== this.readOnlyInfo.readonly) {
|
|
452
|
-
assert(this._reconnectMode !== ReconnectMode.Never,
|
|
453
|
-
0x279 /* "API is not supported for non-connecting or closed container" */);
|
|
454
|
-
|
|
455
|
-
let reconnect = false;
|
|
456
|
-
if (this.readOnlyInfo.readonly === true) {
|
|
457
|
-
// If we switch to readonly while connected, we should disconnect first
|
|
458
|
-
// See comment in the "readonly" event handler to deltaManager set up by
|
|
459
|
-
// the ContainerRuntime constructor
|
|
460
|
-
|
|
461
|
-
if (this.shouldJoinWrite()) {
|
|
462
|
-
// If we have pending changes, then we will never send them - it smells like
|
|
463
|
-
// host logic error.
|
|
464
|
-
this.logger.sendErrorEvent({ eventName: "ForceReadonlyPendingChanged" });
|
|
465
|
-
}
|
|
235
|
+
public submitSignal(content: any) { return this.connectionManager.submitSignal(content); }
|
|
466
236
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
if (reconnect) {
|
|
471
|
-
// reconnect if we disconnected from before.
|
|
472
|
-
this.triggerConnect({ reason: "forceReadonly", mode: "read", fetchOpsFromStorage: false });
|
|
473
|
-
}
|
|
237
|
+
public flush() {
|
|
238
|
+
if (this.messageBuffer.length === 0) {
|
|
239
|
+
return;
|
|
474
240
|
}
|
|
241
|
+
|
|
242
|
+
// The prepareFlush event allows listeners to append metadata to the batch prior to submission.
|
|
243
|
+
this.emit("prepareSend", this.messageBuffer);
|
|
244
|
+
|
|
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
|
+
};
|
|
475
254
|
}
|
|
476
255
|
|
|
477
256
|
/**
|
|
@@ -481,7 +260,7 @@ export class DeltaManager
|
|
|
481
260
|
* @param event - Event to log.
|
|
482
261
|
*/
|
|
483
262
|
public logConnectionIssue(event: ITelemetryErrorEvent) {
|
|
484
|
-
assert(this.
|
|
263
|
+
assert(this.connectionManager.connected, 0x238 /* "called only in connected state" */);
|
|
485
264
|
|
|
486
265
|
const pendingSorted = this.pending.sort((a, b) => a.sequenceNumber - b.sequenceNumber);
|
|
487
266
|
this.logger.sendErrorEvent({
|
|
@@ -494,7 +273,7 @@ export class DeltaManager
|
|
|
494
273
|
lastProcessedSequenceNumber: this.lastProcessedSequenceNumber, // same as above, but after processing
|
|
495
274
|
lastObserved: this.lastObservedSeqNumber, // last sequence we ever saw; may have gaps with above.
|
|
496
275
|
// connection info
|
|
497
|
-
...this.
|
|
276
|
+
...this.connectionManager.connectionVerboseProps,
|
|
498
277
|
pendingOps: this.pending.length, // Do we have any pending ops?
|
|
499
278
|
pendingFirst: pendingSorted[0]?.sequenceNumber, // is the first pending op the one that we are missing?
|
|
500
279
|
haveHandler: this.handler !== undefined, // do we have handler installed?
|
|
@@ -503,26 +282,27 @@ export class DeltaManager
|
|
|
503
282
|
});
|
|
504
283
|
}
|
|
505
284
|
|
|
506
|
-
private set_readonlyPermissions(readonly: boolean) {
|
|
507
|
-
const oldValue = this.readOnlyInfo.readonly;
|
|
508
|
-
this._readonlyPermissions = readonly;
|
|
509
|
-
if (oldValue !== this.readOnlyInfo.readonly) {
|
|
510
|
-
safeRaiseEvent(this, this.logger, "readonly", this.readOnlyInfo.readonly);
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
|
|
514
285
|
constructor(
|
|
515
286
|
private readonly serviceProvider: () => IDocumentService | undefined,
|
|
516
|
-
private client: IClient,
|
|
517
287
|
private readonly logger: ITelemetryLogger,
|
|
518
|
-
reconnectAllowed: boolean,
|
|
519
288
|
private readonly _active: () => boolean,
|
|
289
|
+
createConnectionManager: (props: IConnectionManagerFactoryArgs) => TConnectionManager,
|
|
520
290
|
) {
|
|
521
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
|
+
};
|
|
522
304
|
|
|
523
|
-
this.
|
|
524
|
-
this.defaultReconnectionMode = this.client.mode;
|
|
525
|
-
this._reconnectMode = reconnectAllowed ? ReconnectMode.Enabled : ReconnectMode.Never;
|
|
305
|
+
this.connectionManager = createConnectionManager(props);
|
|
526
306
|
|
|
527
307
|
this._inbound = new DeltaQueue<ISequencedDocumentMessage>(
|
|
528
308
|
(op) => {
|
|
@@ -533,20 +313,6 @@ export class DeltaManager
|
|
|
533
313
|
this.close(CreateProcessingError(error, "deltaManagerInboundErrorHandler", this.lastMessage));
|
|
534
314
|
});
|
|
535
315
|
|
|
536
|
-
// Outbound message queue. The outbound queue is represented as a queue of an array of ops. Ops contained
|
|
537
|
-
// within an array *must* fit within the maxMessageSize and are guaranteed to be ordered sequentially.
|
|
538
|
-
this._outbound = new DeltaQueue<IDocumentMessage[]>(
|
|
539
|
-
(messages) => {
|
|
540
|
-
if (this.connection === undefined) {
|
|
541
|
-
throw new Error("Attempted to submit an outbound message without connection");
|
|
542
|
-
}
|
|
543
|
-
this.connection.submit(messages);
|
|
544
|
-
});
|
|
545
|
-
|
|
546
|
-
this._outbound.on("error", (error) => {
|
|
547
|
-
this.close(normalizeError(error));
|
|
548
|
-
});
|
|
549
|
-
|
|
550
316
|
// Inbound signal queue
|
|
551
317
|
this._inboundSignal = new DeltaQueue<ISignalMessage>((message) => {
|
|
552
318
|
if (this.handler === undefined) {
|
|
@@ -567,6 +333,47 @@ export class DeltaManager
|
|
|
567
333
|
// - inbound & inboundSignal are resumed in attachOpHandler() when we have handler setup
|
|
568
334
|
}
|
|
569
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
|
+
|
|
570
377
|
public dispose() {
|
|
571
378
|
throw new Error("Not implemented.");
|
|
572
379
|
}
|
|
@@ -618,7 +425,7 @@ export class DeltaManager
|
|
|
618
425
|
// (which in most cases will happen when we are done processing cached ops)
|
|
619
426
|
if (cacheOnly) {
|
|
620
427
|
// fire and forget
|
|
621
|
-
this.fetchMissingDeltas("DocumentOpen"
|
|
428
|
+
this.fetchMissingDeltas("DocumentOpen");
|
|
622
429
|
}
|
|
623
430
|
}
|
|
624
431
|
|
|
@@ -626,65 +433,12 @@ export class DeltaManager
|
|
|
626
433
|
assert(this.fetchReason !== undefined || this.pending.length === 0, 0x269 /* "pending ops are not dropped" */);
|
|
627
434
|
}
|
|
628
435
|
|
|
629
|
-
|
|
630
|
-
return {
|
|
631
|
-
claims: connection.claims,
|
|
632
|
-
clientId: connection.clientId,
|
|
633
|
-
existing: connection.existing,
|
|
634
|
-
checkpointSequenceNumber: connection.checkpointSequenceNumber,
|
|
635
|
-
get initialClients() { return connection.initialClients; },
|
|
636
|
-
mode: connection.mode,
|
|
637
|
-
serviceConfiguration: connection.serviceConfiguration,
|
|
638
|
-
version: connection.version,
|
|
639
|
-
};
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
public async connect(args: IConnectionArgs): Promise<IConnectionDetails> {
|
|
643
|
-
const connection = await this.connectCore(args);
|
|
644
|
-
return DeltaManager.detailsFromConnection(connection);
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
/**
|
|
648
|
-
* Start the connection. Any error should result in container being close.
|
|
649
|
-
* And report the error if it excape for any reason.
|
|
650
|
-
* @param args - The connection arguments
|
|
651
|
-
*/
|
|
652
|
-
private triggerConnect(args: IConnectionArgs) {
|
|
653
|
-
assert(this.connection === undefined, 0x239 /* "called only in disconnected state" */);
|
|
654
|
-
if (this.reconnectMode !== ReconnectMode.Enabled) {
|
|
655
|
-
return;
|
|
656
|
-
}
|
|
657
|
-
this.connectCore(args).catch((err) => {
|
|
658
|
-
// Errors are raised as "error" event and close container.
|
|
659
|
-
// Have a catch-all case in case we missed something
|
|
660
|
-
if (!this.closed) {
|
|
661
|
-
this.logger.sendErrorEvent({ eventName: "ConnectException" }, err);
|
|
662
|
-
}
|
|
663
|
-
});
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
private async connectCore(args: IConnectionArgs): Promise<IDocumentDeltaConnection> {
|
|
667
|
-
assert(!this.closed, 0x26a /* "not closed" */);
|
|
668
|
-
|
|
669
|
-
if (this.connection !== undefined) {
|
|
670
|
-
return this.connection;
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
if (this.connectionP !== undefined) {
|
|
674
|
-
return this.connectionP;
|
|
675
|
-
}
|
|
676
|
-
|
|
436
|
+
public connect(args: IConnectionArgs) {
|
|
677
437
|
const fetchOpsFromStorage = args.fetchOpsFromStorage ?? true;
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
// firing of "connected" event from Container and switch of current clientId (as tracked
|
|
683
|
-
// by all DDSes). This will make it impossible to figure out if ops actually made it through,
|
|
684
|
-
// so DDSes will immediately resubmit all pending ops, and some of them will be duplicates, corrupting document
|
|
685
|
-
if (this.shouldJoinWrite()) {
|
|
686
|
-
requestedMode = "write";
|
|
687
|
-
}
|
|
438
|
+
logIfFalse(
|
|
439
|
+
this.handler !== undefined || !fetchOpsFromStorage,
|
|
440
|
+
this.logger,
|
|
441
|
+
"CantFetchWithoutBaseline"); // can't fetch if no baseline
|
|
688
442
|
|
|
689
443
|
// Note: There is race condition here.
|
|
690
444
|
// We want to issue request to storage as soon as possible, to
|
|
@@ -695,253 +449,12 @@ export class DeltaManager
|
|
|
695
449
|
// own "join" message and realize any gap client has in ops.
|
|
696
450
|
// But for view-only connection, we have no such signal, and with no traffic
|
|
697
451
|
// on the wire, we might be always behind.
|
|
698
|
-
// See comment at the end of
|
|
699
|
-
logIfFalse(
|
|
700
|
-
this.handler !== undefined || !fetchOpsFromStorage,
|
|
701
|
-
this.logger,
|
|
702
|
-
"CantFetchWithoutBaseline"); // can't fetch if no baseline
|
|
452
|
+
// See comment at the end of "connect" handler
|
|
703
453
|
if (fetchOpsFromStorage) {
|
|
704
|
-
this.fetchMissingDeltas(args.reason
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
const docService = this.serviceProvider();
|
|
708
|
-
assert(docService !== undefined, 0x2a7 /* "Container is not attached" */);
|
|
709
|
-
|
|
710
|
-
if (docService.policies?.storageOnly === true) {
|
|
711
|
-
const connection = new NoDeltaStream();
|
|
712
|
-
this.connectionP = Promise.resolve(connection); // to keep setupNewSuccessfulConnection happy
|
|
713
|
-
this.setupNewSuccessfulConnection(connection, "read");
|
|
714
|
-
return connection;
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
// The promise returned from connectCore will settle with a resolved connection or reject with error
|
|
718
|
-
const connectCore = async () => {
|
|
719
|
-
let connection: IDocumentDeltaConnection | undefined;
|
|
720
|
-
let delayMs = InitialReconnectDelayInMs;
|
|
721
|
-
let connectRepeatCount = 0;
|
|
722
|
-
const connectStartTime = performance.now();
|
|
723
|
-
let lastError: any;
|
|
724
|
-
|
|
725
|
-
// This loop will keep trying to connect until successful, with a delay between each iteration.
|
|
726
|
-
while (connection === undefined) {
|
|
727
|
-
if (this.closed) {
|
|
728
|
-
throw new Error("Attempting to connect a closed DeltaManager");
|
|
729
|
-
}
|
|
730
|
-
connectRepeatCount++;
|
|
731
|
-
|
|
732
|
-
try {
|
|
733
|
-
this.client.mode = requestedMode;
|
|
734
|
-
connection = await docService.connectToDeltaStream(this.client);
|
|
735
|
-
|
|
736
|
-
if (connection.disposed) {
|
|
737
|
-
// Nobody observed this connection, so drop it on the floor and retry.
|
|
738
|
-
this.logger.sendTelemetryEvent({ eventName: "ReceivedClosedConnection" });
|
|
739
|
-
connection = undefined;
|
|
740
|
-
}
|
|
741
|
-
} catch (origError) {
|
|
742
|
-
if (typeof origError === "object" && origError !== null &&
|
|
743
|
-
origError?.errorType === DeltaStreamConnectionForbiddenError.errorType) {
|
|
744
|
-
connection = new NoDeltaStream();
|
|
745
|
-
requestedMode = "read";
|
|
746
|
-
break;
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
// Socket.io error when we connect to wrong socket, or hit some multiplexing bug
|
|
750
|
-
if (!canRetryOnError(origError)) {
|
|
751
|
-
const error = normalizeError(origError, { props: fatalConnectErrorProp });
|
|
752
|
-
this.close(error);
|
|
753
|
-
throw error;
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
// Log error once - we get too many errors in logs when we are offline,
|
|
757
|
-
// and unfortunately there is no reliable way to detect that.
|
|
758
|
-
if (connectRepeatCount === 1) {
|
|
759
|
-
logNetworkFailure(
|
|
760
|
-
this.logger,
|
|
761
|
-
{
|
|
762
|
-
delay: delayMs, // milliseconds
|
|
763
|
-
eventName: "DeltaConnectionFailureToConnect",
|
|
764
|
-
duration: TelemetryLogger.formatTick(performance.now() - connectStartTime),
|
|
765
|
-
},
|
|
766
|
-
origError);
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
lastError = origError;
|
|
770
|
-
|
|
771
|
-
const retryDelayFromError = getRetryDelayFromError(origError);
|
|
772
|
-
delayMs = retryDelayFromError ?? Math.min(delayMs * 2, MaxReconnectDelayInMs);
|
|
773
|
-
|
|
774
|
-
if (retryDelayFromError !== undefined) {
|
|
775
|
-
this.emitDelayInfo(this.deltaStreamDelayId, retryDelayFromError, origError);
|
|
776
|
-
}
|
|
777
|
-
await waitForConnectedState(delayMs);
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
// If we retried more than once, log an event about how long it took
|
|
782
|
-
if (connectRepeatCount > 1) {
|
|
783
|
-
this.logger.sendTelemetryEvent(
|
|
784
|
-
{
|
|
785
|
-
eventName: "MultipleDeltaConnectionFailures",
|
|
786
|
-
attempts: connectRepeatCount,
|
|
787
|
-
duration: TelemetryLogger.formatTick(performance.now() - connectStartTime),
|
|
788
|
-
},
|
|
789
|
-
lastError,
|
|
790
|
-
);
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
this.setupNewSuccessfulConnection(connection, requestedMode);
|
|
794
|
-
|
|
795
|
-
return connection;
|
|
796
|
-
};
|
|
797
|
-
|
|
798
|
-
// This promise settles as soon as we know the outcome of the connection attempt
|
|
799
|
-
// Set it upfront, such that if connection is established (NoDeltaConnection) or rejected (bug in
|
|
800
|
-
// connectToDeltaStream() implementation - throwing exception vs. returning rejected promise) in
|
|
801
|
-
// synchronous way, we have this.connectionP setup for all the code to assert correctness of the flow.
|
|
802
|
-
const deferred = new Deferred<IDocumentDeltaConnection>();
|
|
803
|
-
this.connectionP = deferred.promise;
|
|
804
|
-
|
|
805
|
-
// Regardless of how the connection attempt concludes, we'll clear the promise and remove the listener
|
|
806
|
-
// Reject the connection promise if the DeltaManager gets closed during connection
|
|
807
|
-
const cleanupAndReject = (error) => {
|
|
808
|
-
this.connectionP = undefined;
|
|
809
|
-
this.removeListener("closed", cleanupAndReject);
|
|
810
|
-
|
|
811
|
-
// This error came from some logic error in this file. Fail-fast to learn and fix the issue faster
|
|
812
|
-
const normalizedError = normalizeError(error, { props: fatalConnectErrorProp });
|
|
813
|
-
this.close(normalizedError);
|
|
814
|
-
deferred.reject(normalizedError);
|
|
815
|
-
};
|
|
816
|
-
this.on("closed", cleanupAndReject);
|
|
817
|
-
|
|
818
|
-
// Attempt the connection
|
|
819
|
-
connectCore().then((connection) => {
|
|
820
|
-
this.removeListener("closed", cleanupAndReject);
|
|
821
|
-
deferred.resolve(connection);
|
|
822
|
-
}).catch(cleanupAndReject);
|
|
823
|
-
|
|
824
|
-
return this.connectionP;
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
public flush() {
|
|
828
|
-
if (this.messageBuffer.length === 0) {
|
|
829
|
-
return;
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
// The prepareFlush event allows listeners to append metadata to the batch prior to submission.
|
|
833
|
-
this.emit("prepareSend", this.messageBuffer);
|
|
834
|
-
|
|
835
|
-
this._outbound.push(this.messageBuffer);
|
|
836
|
-
this.messageBuffer = [];
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
/**
|
|
840
|
-
* Submits the given delta returning the client sequence number for the message. Contents is the actual
|
|
841
|
-
* contents of the message. appData is optional metadata that can be attached to the op by the app.
|
|
842
|
-
*
|
|
843
|
-
* If batch is set to true then the submit will be batched - and as a result guaranteed to be ordered sequentially
|
|
844
|
-
* in the global sequencing space. The batch will be flushed either when flush is called or when a non-batched
|
|
845
|
-
* op is submitted.
|
|
846
|
-
*/
|
|
847
|
-
public submit(type: MessageType, contents: any, batch = false, metadata?: any): number {
|
|
848
|
-
// TODO need to fail if gets too large
|
|
849
|
-
// const serializedContent = JSON.stringify(this.messageBuffer);
|
|
850
|
-
// const maxOpSize = this.context.deltaManager.maxMessageSize;
|
|
851
|
-
|
|
852
|
-
if (this.readOnlyInfo.readonly === true) {
|
|
853
|
-
assert(this.readOnlyInfo.readonly === true, 0x1f0 /* "Unexpected mismatch in readonly" */);
|
|
854
|
-
const error = new GenericError("deltaManagerReadonlySubmit", undefined /* error */, {
|
|
855
|
-
readonly: this.readOnlyInfo.readonly,
|
|
856
|
-
forcedReadonly: this.readOnlyInfo.forced,
|
|
857
|
-
readonlyPermissions: this.readOnlyInfo.permissions,
|
|
858
|
-
storageOnly: this.readOnlyInfo.storageOnly,
|
|
859
|
-
});
|
|
860
|
-
this.close(error);
|
|
861
|
-
return -1;
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
// reset clientSequenceNumber if we are using new clientId.
|
|
865
|
-
// we keep info about old connection as long as possible to be able to account for all non-acked ops
|
|
866
|
-
// that we pick up on next connection.
|
|
867
|
-
assert(!!this.connection, 0x0e4 /* "Lost old connection!" */);
|
|
868
|
-
if (this.lastSubmittedClientId !== this.connection?.clientId) {
|
|
869
|
-
this.lastSubmittedClientId = this.connection?.clientId;
|
|
870
|
-
this.clientSequenceNumber = 0;
|
|
871
|
-
this.clientSequenceNumberObserved = 0;
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
// If connection is "read" or implicit "read" (got leave op for "write" connection),
|
|
875
|
-
// then op can't make it through - we will get a nack if op is sent.
|
|
876
|
-
// We can short-circuit this process.
|
|
877
|
-
// Note that we also want nacks to be rare and be treated as catastrophic failures.
|
|
878
|
-
// Be careful with reentrancy though - disconnected event should not be be raised in the
|
|
879
|
-
// middle of the current workflow, but rather on clean stack!
|
|
880
|
-
if (this.connectionMode === "read" || this.downgradedConnection) {
|
|
881
|
-
if (!this.pendingReconnect) {
|
|
882
|
-
this.pendingReconnect = true;
|
|
883
|
-
Promise.resolve().then(async () => {
|
|
884
|
-
if (this.pendingReconnect) { // still valid?
|
|
885
|
-
return this.reconnectOnErrorCore(
|
|
886
|
-
"write", // connectionMode
|
|
887
|
-
"Switch to write", // message
|
|
888
|
-
);
|
|
889
|
-
}
|
|
890
|
-
})
|
|
891
|
-
.catch(() => {});
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
// Can return -1 here, but no other path does it (other than error path in Container),
|
|
895
|
-
// so it's better not to introduce new states.
|
|
896
|
-
return ++this.clientSequenceNumber;
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
const service = this.clientDetails.type === undefined || this.clientDetails.type === ""
|
|
900
|
-
? "unknown"
|
|
901
|
-
: this.clientDetails.type;
|
|
902
|
-
|
|
903
|
-
// Start adding trace for the op.
|
|
904
|
-
const traces: ITrace[] = [
|
|
905
|
-
{
|
|
906
|
-
action: "start",
|
|
907
|
-
service,
|
|
908
|
-
timestamp: Date.now(),
|
|
909
|
-
}];
|
|
910
|
-
|
|
911
|
-
const message: IDocumentMessage = {
|
|
912
|
-
clientSequenceNumber: ++this.clientSequenceNumber,
|
|
913
|
-
contents: JSON.stringify(contents),
|
|
914
|
-
metadata,
|
|
915
|
-
referenceSequenceNumber: this.lastProcessedSequenceNumber,
|
|
916
|
-
traces,
|
|
917
|
-
type,
|
|
918
|
-
};
|
|
919
|
-
|
|
920
|
-
if (type === MessageType.NoOp) {
|
|
921
|
-
this.trailingNoopCount++;
|
|
922
|
-
} else {
|
|
923
|
-
this.trailingNoopCount = 0;
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
this.emit("submitOp", message);
|
|
927
|
-
|
|
928
|
-
if (!batch) {
|
|
929
|
-
this.flush();
|
|
930
|
-
this.messageBuffer.push(message);
|
|
931
|
-
this.flush();
|
|
932
|
-
} else {
|
|
933
|
-
this.messageBuffer.push(message);
|
|
454
|
+
this.fetchMissingDeltas(args.reason);
|
|
934
455
|
}
|
|
935
456
|
|
|
936
|
-
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
public submitSignal(content: any) {
|
|
940
|
-
if (this.connection !== undefined) {
|
|
941
|
-
this.connection.submitSignal(content);
|
|
942
|
-
} else {
|
|
943
|
-
this.logger.sendErrorEvent({ eventName: "submitSignalDisconnected" });
|
|
944
|
-
}
|
|
457
|
+
this.connectionManager.connect(args.mode);
|
|
945
458
|
}
|
|
946
459
|
|
|
947
460
|
private async getDeltas(
|
|
@@ -959,10 +472,6 @@ export class DeltaManager
|
|
|
959
472
|
this.deltaStorage = await docService.connectToDeltaStorage();
|
|
960
473
|
}
|
|
961
474
|
|
|
962
|
-
assert(this.closeAbortController.signal.onabort === null, 0x1e8 /* "reentrancy" */);
|
|
963
|
-
const controller = new AbortController();
|
|
964
|
-
this.closeAbortController.signal.onabort = () => controller.abort();
|
|
965
|
-
|
|
966
475
|
let cancelFetch: (op: ISequencedDocumentMessage) => boolean;
|
|
967
476
|
|
|
968
477
|
if (to !== undefined) {
|
|
@@ -977,7 +486,7 @@ export class DeltaManager
|
|
|
977
486
|
early: true,
|
|
978
487
|
from,
|
|
979
488
|
to,
|
|
980
|
-
...this.
|
|
489
|
+
...this.connectionManager.connectionVerboseProps,
|
|
981
490
|
});
|
|
982
491
|
return;
|
|
983
492
|
}
|
|
@@ -996,6 +505,7 @@ export class DeltaManager
|
|
|
996
505
|
cancelFetch = (op: ISequencedDocumentMessage) => op.sequenceNumber >= this.lastObservedSeqNumber;
|
|
997
506
|
}
|
|
998
507
|
|
|
508
|
+
const controller = new AbortController();
|
|
999
509
|
let opsFromFetch = false;
|
|
1000
510
|
|
|
1001
511
|
const opListener = (op: ISequencedDocumentMessage) => {
|
|
@@ -1009,9 +519,11 @@ export class DeltaManager
|
|
|
1009
519
|
}
|
|
1010
520
|
};
|
|
1011
521
|
|
|
1012
|
-
this._inbound.on("push", opListener);
|
|
1013
|
-
|
|
1014
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
|
+
|
|
1015
527
|
const stream = this.deltaStorage.fetchMessages(
|
|
1016
528
|
from, // inclusive
|
|
1017
529
|
to, // exclusive
|
|
@@ -1033,9 +545,9 @@ export class DeltaManager
|
|
|
1033
545
|
}
|
|
1034
546
|
}
|
|
1035
547
|
} finally {
|
|
1036
|
-
assert(!opsFromFetch, 0x289 /* "logic error" */);
|
|
1037
548
|
this.closeAbortController.signal.onabort = null;
|
|
1038
549
|
this._inbound.off("push", opListener);
|
|
550
|
+
assert(!opsFromFetch, 0x289 /* "logic error" */);
|
|
1039
551
|
}
|
|
1040
552
|
}
|
|
1041
553
|
|
|
@@ -1047,20 +559,12 @@ export class DeltaManager
|
|
|
1047
559
|
return;
|
|
1048
560
|
}
|
|
1049
561
|
this.closed = true;
|
|
1050
|
-
// Ensure that things like triggerConnect() will short circuit
|
|
1051
|
-
this._reconnectMode = ReconnectMode.Never;
|
|
1052
562
|
|
|
1053
|
-
this.
|
|
563
|
+
this.connectionManager.dispose(error);
|
|
1054
564
|
|
|
1055
|
-
|
|
1056
|
-
? `Closing DeltaManager (${error.message})`
|
|
1057
|
-
: "Closing DeltaManager";
|
|
1058
|
-
|
|
1059
|
-
// This raises "disconnect" event if we have active connection.
|
|
1060
|
-
this.disconnectFromDeltaStream(disconnectReason);
|
|
565
|
+
this.closeAbortController.abort();
|
|
1061
566
|
|
|
1062
567
|
this._inbound.clear();
|
|
1063
|
-
this._outbound.clear();
|
|
1064
568
|
this._inboundSignal.clear();
|
|
1065
569
|
|
|
1066
570
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
@@ -1071,11 +575,6 @@ export class DeltaManager
|
|
|
1071
575
|
// Drop pending messages - this will ensure catchUp() does not go into infinite loop
|
|
1072
576
|
this.pending = [];
|
|
1073
577
|
|
|
1074
|
-
// Notify everyone we are in read-only state.
|
|
1075
|
-
// Useful for data stores in case we hit some critical error,
|
|
1076
|
-
// to switch to a mode where user edits are not accepted
|
|
1077
|
-
this.set_readonlyPermissions(true);
|
|
1078
|
-
|
|
1079
578
|
// This needs to be the last thing we do (before removing listeners), as it causes
|
|
1080
579
|
// Container to dispose context and break ability of data stores / runtime to "hear"
|
|
1081
580
|
// from delta manager, including notification (above) about readonly state.
|
|
@@ -1091,6 +590,21 @@ export class DeltaManager
|
|
|
1091
590
|
}
|
|
1092
591
|
}
|
|
1093
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
|
+
|
|
1094
608
|
/**
|
|
1095
609
|
* Emit info about a delay in service communication on account of throttling.
|
|
1096
610
|
* @param id - Id of the connection that is delayed
|
|
@@ -1113,286 +627,6 @@ export class DeltaManager
|
|
|
1113
627
|
}
|
|
1114
628
|
}
|
|
1115
629
|
|
|
1116
|
-
private readonly opHandler = (documentId: string, messagesArg: ISequencedDocumentMessage[]) => {
|
|
1117
|
-
const messages = Array.isArray(messagesArg) ? messagesArg : [messagesArg];
|
|
1118
|
-
this.enqueueMessages(messages, "opHandler");
|
|
1119
|
-
};
|
|
1120
|
-
|
|
1121
|
-
private readonly signalHandler = (message: ISignalMessage) => {
|
|
1122
|
-
this._inboundSignal.push(message);
|
|
1123
|
-
};
|
|
1124
|
-
|
|
1125
|
-
// Always connect in write mode after getting nacked.
|
|
1126
|
-
private readonly nackHandler = (documentId: string, messages: INack[]) => {
|
|
1127
|
-
const message = messages[0];
|
|
1128
|
-
// TODO: we should remove this check when service updates?
|
|
1129
|
-
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
|
1130
|
-
if (this._readonlyPermissions) {
|
|
1131
|
-
this.close(createWriteError("writeOnReadOnlyDocument"));
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
|
-
// check message.content for Back-compat with old service.
|
|
1135
|
-
const reconnectInfo = message.content !== undefined
|
|
1136
|
-
? getNackReconnectInfo(message.content) :
|
|
1137
|
-
createGenericNetworkError("nackReasonUnknown", undefined, true);
|
|
1138
|
-
|
|
1139
|
-
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
1140
|
-
this.reconnectOnError(
|
|
1141
|
-
"write",
|
|
1142
|
-
reconnectInfo,
|
|
1143
|
-
);
|
|
1144
|
-
};
|
|
1145
|
-
|
|
1146
|
-
// Connection mode is always read on disconnect/error unless the system mode was write.
|
|
1147
|
-
private readonly disconnectHandler = (disconnectReason) => {
|
|
1148
|
-
// Note: we might get multiple disconnect calls on same socket, as early disconnect notification
|
|
1149
|
-
// ("server_disconnect", ODSP-specific) is mapped to "disconnect"
|
|
1150
|
-
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
1151
|
-
this.reconnectOnError(
|
|
1152
|
-
this.defaultReconnectionMode,
|
|
1153
|
-
createReconnectError("dmDocumentDeltaConnectionDisconnected", disconnectReason),
|
|
1154
|
-
);
|
|
1155
|
-
};
|
|
1156
|
-
|
|
1157
|
-
private readonly errorHandler = (error) => {
|
|
1158
|
-
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
1159
|
-
this.reconnectOnError(
|
|
1160
|
-
this.defaultReconnectionMode,
|
|
1161
|
-
createReconnectError("dmDocumentDeltaConnectionError", error),
|
|
1162
|
-
);
|
|
1163
|
-
};
|
|
1164
|
-
|
|
1165
|
-
private readonly pongHandler = (latency: number) => {
|
|
1166
|
-
this.emit("pong", latency);
|
|
1167
|
-
};
|
|
1168
|
-
|
|
1169
|
-
/**
|
|
1170
|
-
* Once we've successfully gotten a connection, we need to set up state, attach event listeners, and process
|
|
1171
|
-
* initial messages.
|
|
1172
|
-
* @param connection - The newly established connection
|
|
1173
|
-
*/
|
|
1174
|
-
private setupNewSuccessfulConnection(connection: IDocumentDeltaConnection, requestedMode: ConnectionMode) {
|
|
1175
|
-
// Old connection should have been cleaned up before establishing a new one
|
|
1176
|
-
assert(this.connection === undefined, 0x0e6 /* "old connection exists on new connection setup" */);
|
|
1177
|
-
assert(this.connectionP !== undefined || this.closed,
|
|
1178
|
-
0x27f /* "reentrancy may result in incorrect behavior" */);
|
|
1179
|
-
assert(!connection.disposed, 0x28a /* "can't be disposed - Callers need to ensure that!" */);
|
|
1180
|
-
|
|
1181
|
-
this.connectionP = undefined;
|
|
1182
|
-
this.connection = connection;
|
|
1183
|
-
|
|
1184
|
-
// Does information in scopes & mode matches?
|
|
1185
|
-
// If we asked for "write" and got "read", then file is read-only
|
|
1186
|
-
// But if we ask read, server can still give us write.
|
|
1187
|
-
const readonly = !connection.claims.scopes.includes(ScopeType.DocWrite);
|
|
1188
|
-
|
|
1189
|
-
// This connection mode validation logic is moving to the driver layer in 0.44. These two asserts can be
|
|
1190
|
-
// removed after those packages have released and become ubiquitous.
|
|
1191
|
-
assert(requestedMode === "read" || readonly === (this.connectionMode === "read"),
|
|
1192
|
-
0x0e7 /* "claims/connectionMode mismatch" */);
|
|
1193
|
-
assert(!readonly || this.connectionMode === "read", 0x0e8 /* "readonly perf with write connection" */);
|
|
1194
|
-
|
|
1195
|
-
this.set_readonlyPermissions(readonly);
|
|
1196
|
-
|
|
1197
|
-
this.refreshDelayInfo(this.deltaStreamDelayId);
|
|
1198
|
-
|
|
1199
|
-
if (this.closed) {
|
|
1200
|
-
// Raise proper events, Log telemetry event and close connection.
|
|
1201
|
-
this.disconnectFromDeltaStream("DeltaManager already closed");
|
|
1202
|
-
return;
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
// We cancel all ops on lost of connectivity, and rely on DDSes to resubmit them.
|
|
1206
|
-
// Semantics are not well defined for batches (and they are broken right now on disconnects anyway),
|
|
1207
|
-
// but it's safe to assume (until better design is put into place) that batches should not exist
|
|
1208
|
-
// across multiple connections. Right now we assume runtime will not submit any ops in disconnected
|
|
1209
|
-
// state. As requirements change, so should these checks.
|
|
1210
|
-
assert(this.messageBuffer.length === 0, 0x0e9 /* "messageBuffer is not empty on new connection" */);
|
|
1211
|
-
|
|
1212
|
-
this._outbound.resume();
|
|
1213
|
-
|
|
1214
|
-
connection.on("op", this.opHandler);
|
|
1215
|
-
connection.on("signal", this.signalHandler);
|
|
1216
|
-
connection.on("nack", this.nackHandler);
|
|
1217
|
-
connection.on("disconnect", this.disconnectHandler);
|
|
1218
|
-
connection.on("error", this.errorHandler);
|
|
1219
|
-
connection.on("pong", this.pongHandler);
|
|
1220
|
-
|
|
1221
|
-
// Initial messages are always sorted. However, due to early op handler installed by drivers and appending those
|
|
1222
|
-
// ops to initialMessages, resulting set is no longer sorted, which would result in client hitting storage to
|
|
1223
|
-
// fill in gap. We will recover by cancelling this request once we process remaining ops, but it's a waste that
|
|
1224
|
-
// we could avoid
|
|
1225
|
-
const initialMessages = connection.initialMessages.sort((a, b) => a.sequenceNumber - b.sequenceNumber);
|
|
1226
|
-
|
|
1227
|
-
this.connectionStateProps = {
|
|
1228
|
-
connectionLastQueuedSequenceNumber : this.lastQueuedSequenceNumber,
|
|
1229
|
-
connectionLastObservedSeqNumber: this.lastObservedSeqNumber,
|
|
1230
|
-
clientId: connection.clientId,
|
|
1231
|
-
mode: connection.mode,
|
|
1232
|
-
};
|
|
1233
|
-
if (connection.relayServiceAgent !== undefined) {
|
|
1234
|
-
this.connectionStateProps.relayServiceAgent = connection.relayServiceAgent;
|
|
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 (!canRetry) {
|
|
1372
|
-
this.close(normalizeError(error, { props: fatalConnectErrorProp }));
|
|
1373
|
-
} else if (this.reconnectMode === ReconnectMode.Never) {
|
|
1374
|
-
// Do not raise container error if we are closing just because we lost connection.
|
|
1375
|
-
// Those errors (like IdleDisconnect) would show up in telemetry dashboards and
|
|
1376
|
-
// are very misleading, as first initial reaction - some logic is broken.
|
|
1377
|
-
this.close();
|
|
1378
|
-
}
|
|
1379
|
-
|
|
1380
|
-
// If closed then we can't reconnect
|
|
1381
|
-
if (this.closed) {
|
|
1382
|
-
return;
|
|
1383
|
-
}
|
|
1384
|
-
|
|
1385
|
-
if (this.reconnectMode === ReconnectMode.Enabled) {
|
|
1386
|
-
const delayMs = error !== undefined ? getRetryDelayFromError(error) : undefined;
|
|
1387
|
-
if (delayMs !== undefined) {
|
|
1388
|
-
this.emitDelayInfo(this.deltaStreamDelayId, delayMs, error);
|
|
1389
|
-
await waitForConnectedState(delayMs);
|
|
1390
|
-
}
|
|
1391
|
-
|
|
1392
|
-
this.triggerConnect({ reason: "reconnect", mode: requestedMode, fetchOpsFromStorage: false });
|
|
1393
|
-
}
|
|
1394
|
-
}
|
|
1395
|
-
|
|
1396
630
|
// returns parts of message (in string format) that should never change for a given message.
|
|
1397
631
|
// Used for message comparison. It attempts to avoid comparing fields that potentially may differ.
|
|
1398
632
|
// for example, it's not clear if serverMetadata or timestamp property is a property of message or server state.
|
|
@@ -1486,7 +720,7 @@ export class DeltaManager
|
|
|
1486
720
|
gap: gap > 0 ? gap : undefined,
|
|
1487
721
|
firstMissing,
|
|
1488
722
|
dmInitialSeqNumber: this.initialSequenceNumber,
|
|
1489
|
-
...this.
|
|
723
|
+
...this.connectionManager.connectionVerboseProps,
|
|
1490
724
|
});
|
|
1491
725
|
}
|
|
1492
726
|
}
|
|
@@ -1511,7 +745,7 @@ export class DeltaManager
|
|
|
1511
745
|
undefined,
|
|
1512
746
|
DriverErrorType.fileOverwrittenInStorage,
|
|
1513
747
|
{
|
|
1514
|
-
clientId: this.
|
|
748
|
+
clientId: this.connectionManager.clientId,
|
|
1515
749
|
sequenceNumber: message.sequenceNumber,
|
|
1516
750
|
message1,
|
|
1517
751
|
message2,
|
|
@@ -1522,7 +756,7 @@ export class DeltaManager
|
|
|
1522
756
|
}
|
|
1523
757
|
} else if (message.sequenceNumber !== this.lastQueuedSequenceNumber + 1) {
|
|
1524
758
|
this.pending.push(message);
|
|
1525
|
-
this.fetchMissingDeltas(reason,
|
|
759
|
+
this.fetchMissingDeltas(reason, message.sequenceNumber);
|
|
1526
760
|
} else {
|
|
1527
761
|
this.lastQueuedSequenceNumber = message.sequenceNumber;
|
|
1528
762
|
this.previouslyProcessedMessage = message;
|
|
@@ -1548,24 +782,6 @@ export class DeltaManager
|
|
|
1548
782
|
0x0ed /* "non-system message have to have clientId" */,
|
|
1549
783
|
);
|
|
1550
784
|
|
|
1551
|
-
// if we have connection, and message is local, then we better treat is as local!
|
|
1552
|
-
assert(
|
|
1553
|
-
this.connection === undefined
|
|
1554
|
-
|| this.connection.clientId !== message.clientId
|
|
1555
|
-
|| this.lastSubmittedClientId === message.clientId,
|
|
1556
|
-
0x0ee /* "Not accounting local messages correctly" */,
|
|
1557
|
-
);
|
|
1558
|
-
|
|
1559
|
-
if (this.lastSubmittedClientId !== undefined && this.lastSubmittedClientId === message.clientId) {
|
|
1560
|
-
const clientSequenceNumber = message.clientSequenceNumber;
|
|
1561
|
-
|
|
1562
|
-
assert(this.clientSequenceNumberObserved < clientSequenceNumber, 0x0ef /* "client seq# not growing" */);
|
|
1563
|
-
assert(clientSequenceNumber <= this.clientSequenceNumber,
|
|
1564
|
-
0x0f0 /* "Incoming local client seq# > generated by this client" */);
|
|
1565
|
-
|
|
1566
|
-
this.clientSequenceNumberObserved = clientSequenceNumber;
|
|
1567
|
-
}
|
|
1568
|
-
|
|
1569
785
|
// TODO Remove after SPO picks up the latest build.
|
|
1570
786
|
if (
|
|
1571
787
|
typeof message.contents === "string"
|
|
@@ -1575,24 +791,13 @@ export class DeltaManager
|
|
|
1575
791
|
message.contents = JSON.parse(message.contents);
|
|
1576
792
|
}
|
|
1577
793
|
|
|
1578
|
-
|
|
1579
|
-
const systemLeaveMessage = message as ISequencedDocumentSystemMessage;
|
|
1580
|
-
const clientId = JSON.parse(systemLeaveMessage.data) as string;
|
|
1581
|
-
if (clientId === this.connection?.clientId) {
|
|
1582
|
-
// We have been kicked out from quorum
|
|
1583
|
-
this.logger.sendPerformanceEvent({ eventName: "ReadConnectionTransition" });
|
|
1584
|
-
this.downgradedConnection = true;
|
|
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
|
}
|