@fluidframework/container-loader 0.53.0-46105 → 0.54.1
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 -768
- 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 -772
- 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 +9 -9
- package/src/connectionManager.ts +892 -0
- package/src/container.ts +39 -39
- package/src/contracts.ts +156 -0
- package/src/deltaManager.ts +181 -979
- 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,66 +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
|
-
maxMessageSize: connection.serviceConfiguration.maxMessageSize,
|
|
637
|
-
mode: connection.mode,
|
|
638
|
-
serviceConfiguration: connection.serviceConfiguration,
|
|
639
|
-
version: connection.version,
|
|
640
|
-
};
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
public async connect(args: IConnectionArgs): Promise<IConnectionDetails> {
|
|
644
|
-
const connection = await this.connectCore(args);
|
|
645
|
-
return DeltaManager.detailsFromConnection(connection);
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
/**
|
|
649
|
-
* Start the connection. Any error should result in container being close.
|
|
650
|
-
* And report the error if it excape for any reason.
|
|
651
|
-
* @param args - The connection arguments
|
|
652
|
-
*/
|
|
653
|
-
private triggerConnect(args: IConnectionArgs) {
|
|
654
|
-
assert(this.connection === undefined, 0x239 /* "called only in disconnected state" */);
|
|
655
|
-
if (this.reconnectMode !== ReconnectMode.Enabled) {
|
|
656
|
-
return;
|
|
657
|
-
}
|
|
658
|
-
this.connectCore(args).catch((err) => {
|
|
659
|
-
// Errors are raised as "error" event and close container.
|
|
660
|
-
// Have a catch-all case in case we missed something
|
|
661
|
-
if (!this.closed) {
|
|
662
|
-
this.logger.sendErrorEvent({ eventName: "ConnectException" }, err);
|
|
663
|
-
}
|
|
664
|
-
});
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
private async connectCore(args: IConnectionArgs): Promise<IDocumentDeltaConnection> {
|
|
668
|
-
assert(!this.closed, 0x26a /* "not closed" */);
|
|
669
|
-
|
|
670
|
-
if (this.connection !== undefined) {
|
|
671
|
-
return this.connection;
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
if (this.connectionP !== undefined) {
|
|
675
|
-
return this.connectionP;
|
|
676
|
-
}
|
|
677
|
-
|
|
436
|
+
public connect(args: IConnectionArgs) {
|
|
678
437
|
const fetchOpsFromStorage = args.fetchOpsFromStorage ?? true;
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
// firing of "connected" event from Container and switch of current clientId (as tracked
|
|
684
|
-
// by all DDSes). This will make it impossible to figure out if ops actually made it through,
|
|
685
|
-
// so DDSes will immediately resubmit all pending ops, and some of them will be duplicates, corrupting document
|
|
686
|
-
if (this.shouldJoinWrite()) {
|
|
687
|
-
requestedMode = "write";
|
|
688
|
-
}
|
|
438
|
+
logIfFalse(
|
|
439
|
+
this.handler !== undefined || !fetchOpsFromStorage,
|
|
440
|
+
this.logger,
|
|
441
|
+
"CantFetchWithoutBaseline"); // can't fetch if no baseline
|
|
689
442
|
|
|
690
443
|
// Note: There is race condition here.
|
|
691
444
|
// We want to issue request to storage as soon as possible, to
|
|
@@ -696,253 +449,12 @@ export class DeltaManager
|
|
|
696
449
|
// own "join" message and realize any gap client has in ops.
|
|
697
450
|
// But for view-only connection, we have no such signal, and with no traffic
|
|
698
451
|
// on the wire, we might be always behind.
|
|
699
|
-
// See comment at the end of
|
|
700
|
-
logIfFalse(
|
|
701
|
-
this.handler !== undefined || !fetchOpsFromStorage,
|
|
702
|
-
this.logger,
|
|
703
|
-
"CantFetchWithoutBaseline"); // can't fetch if no baseline
|
|
452
|
+
// See comment at the end of "connect" handler
|
|
704
453
|
if (fetchOpsFromStorage) {
|
|
705
|
-
this.fetchMissingDeltas(args.reason
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
const docService = this.serviceProvider();
|
|
709
|
-
assert(docService !== undefined, 0x2a7 /* "Container is not attached" */);
|
|
710
|
-
|
|
711
|
-
if (docService.policies?.storageOnly === true) {
|
|
712
|
-
const connection = new NoDeltaStream();
|
|
713
|
-
this.connectionP = Promise.resolve(connection); // to keep setupNewSuccessfulConnection happy
|
|
714
|
-
this.setupNewSuccessfulConnection(connection, "read");
|
|
715
|
-
return connection;
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
// The promise returned from connectCore will settle with a resolved connection or reject with error
|
|
719
|
-
const connectCore = async () => {
|
|
720
|
-
let connection: IDocumentDeltaConnection | undefined;
|
|
721
|
-
let delayMs = InitialReconnectDelayInMs;
|
|
722
|
-
let connectRepeatCount = 0;
|
|
723
|
-
const connectStartTime = performance.now();
|
|
724
|
-
let lastError: any;
|
|
725
|
-
|
|
726
|
-
// This loop will keep trying to connect until successful, with a delay between each iteration.
|
|
727
|
-
while (connection === undefined) {
|
|
728
|
-
if (this.closed) {
|
|
729
|
-
throw new Error("Attempting to connect a closed DeltaManager");
|
|
730
|
-
}
|
|
731
|
-
connectRepeatCount++;
|
|
732
|
-
|
|
733
|
-
try {
|
|
734
|
-
this.client.mode = requestedMode;
|
|
735
|
-
connection = await docService.connectToDeltaStream(this.client);
|
|
736
|
-
|
|
737
|
-
if (connection.disposed) {
|
|
738
|
-
// Nobody observed this connection, so drop it on the floor and retry.
|
|
739
|
-
this.logger.sendTelemetryEvent({ eventName: "ReceivedClosedConnection" });
|
|
740
|
-
connection = undefined;
|
|
741
|
-
}
|
|
742
|
-
} catch (origError) {
|
|
743
|
-
if (typeof origError === "object" && origError !== null &&
|
|
744
|
-
origError?.errorType === DeltaStreamConnectionForbiddenError.errorType) {
|
|
745
|
-
connection = new NoDeltaStream();
|
|
746
|
-
requestedMode = "read";
|
|
747
|
-
break;
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
// Socket.io error when we connect to wrong socket, or hit some multiplexing bug
|
|
751
|
-
if (!canRetryOnError(origError)) {
|
|
752
|
-
const error = normalizeError(origError, { props: fatalConnectErrorProp });
|
|
753
|
-
this.close(error);
|
|
754
|
-
throw error;
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
// Log error once - we get too many errors in logs when we are offline,
|
|
758
|
-
// and unfortunately there is no reliable way to detect that.
|
|
759
|
-
if (connectRepeatCount === 1) {
|
|
760
|
-
logNetworkFailure(
|
|
761
|
-
this.logger,
|
|
762
|
-
{
|
|
763
|
-
delay: delayMs, // milliseconds
|
|
764
|
-
eventName: "DeltaConnectionFailureToConnect",
|
|
765
|
-
duration: TelemetryLogger.formatTick(performance.now() - connectStartTime),
|
|
766
|
-
},
|
|
767
|
-
origError);
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
lastError = origError;
|
|
771
|
-
|
|
772
|
-
const retryDelayFromError = getRetryDelayFromError(origError);
|
|
773
|
-
delayMs = retryDelayFromError ?? Math.min(delayMs * 2, MaxReconnectDelayInMs);
|
|
774
|
-
|
|
775
|
-
if (retryDelayFromError !== undefined) {
|
|
776
|
-
this.emitDelayInfo(this.deltaStreamDelayId, retryDelayFromError, origError);
|
|
777
|
-
}
|
|
778
|
-
await waitForConnectedState(delayMs);
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
// If we retried more than once, log an event about how long it took
|
|
783
|
-
if (connectRepeatCount > 1) {
|
|
784
|
-
this.logger.sendTelemetryEvent(
|
|
785
|
-
{
|
|
786
|
-
eventName: "MultipleDeltaConnectionFailures",
|
|
787
|
-
attempts: connectRepeatCount,
|
|
788
|
-
duration: TelemetryLogger.formatTick(performance.now() - connectStartTime),
|
|
789
|
-
},
|
|
790
|
-
lastError,
|
|
791
|
-
);
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
this.setupNewSuccessfulConnection(connection, requestedMode);
|
|
795
|
-
|
|
796
|
-
return connection;
|
|
797
|
-
};
|
|
798
|
-
|
|
799
|
-
// This promise settles as soon as we know the outcome of the connection attempt
|
|
800
|
-
// Set it upfront, such that if connection is established (NoDeltaConnection) or rejected (bug in
|
|
801
|
-
// connectToDeltaStream() implementation - throwing exception vs. returning rejected promise) in
|
|
802
|
-
// synchronous way, we have this.connectionP setup for all the code to assert correctness of the flow.
|
|
803
|
-
const deferred = new Deferred<IDocumentDeltaConnection>();
|
|
804
|
-
this.connectionP = deferred.promise;
|
|
805
|
-
|
|
806
|
-
// Regardless of how the connection attempt concludes, we'll clear the promise and remove the listener
|
|
807
|
-
// Reject the connection promise if the DeltaManager gets closed during connection
|
|
808
|
-
const cleanupAndReject = (error) => {
|
|
809
|
-
this.connectionP = undefined;
|
|
810
|
-
this.removeListener("closed", cleanupAndReject);
|
|
811
|
-
|
|
812
|
-
// This error came from some logic error in this file. Fail-fast to learn and fix the issue faster
|
|
813
|
-
const normalizedError = normalizeError(error, { props: fatalConnectErrorProp });
|
|
814
|
-
this.close(normalizedError);
|
|
815
|
-
deferred.reject(normalizedError);
|
|
816
|
-
};
|
|
817
|
-
this.on("closed", cleanupAndReject);
|
|
818
|
-
|
|
819
|
-
// Attempt the connection
|
|
820
|
-
connectCore().then((connection) => {
|
|
821
|
-
this.removeListener("closed", cleanupAndReject);
|
|
822
|
-
deferred.resolve(connection);
|
|
823
|
-
}).catch(cleanupAndReject);
|
|
824
|
-
|
|
825
|
-
return this.connectionP;
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
public flush() {
|
|
829
|
-
if (this.messageBuffer.length === 0) {
|
|
830
|
-
return;
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
// The prepareFlush event allows listeners to append metadata to the batch prior to submission.
|
|
834
|
-
this.emit("prepareSend", this.messageBuffer);
|
|
835
|
-
|
|
836
|
-
this._outbound.push(this.messageBuffer);
|
|
837
|
-
this.messageBuffer = [];
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
/**
|
|
841
|
-
* Submits the given delta returning the client sequence number for the message. Contents is the actual
|
|
842
|
-
* contents of the message. appData is optional metadata that can be attached to the op by the app.
|
|
843
|
-
*
|
|
844
|
-
* If batch is set to true then the submit will be batched - and as a result guaranteed to be ordered sequentially
|
|
845
|
-
* in the global sequencing space. The batch will be flushed either when flush is called or when a non-batched
|
|
846
|
-
* op is submitted.
|
|
847
|
-
*/
|
|
848
|
-
public submit(type: MessageType, contents: any, batch = false, metadata?: any): number {
|
|
849
|
-
// TODO need to fail if gets too large
|
|
850
|
-
// const serializedContent = JSON.stringify(this.messageBuffer);
|
|
851
|
-
// const maxOpSize = this.context.deltaManager.maxMessageSize;
|
|
852
|
-
|
|
853
|
-
if (this.readOnlyInfo.readonly === true) {
|
|
854
|
-
assert(this.readOnlyInfo.readonly === true, 0x1f0 /* "Unexpected mismatch in readonly" */);
|
|
855
|
-
const error = new GenericError("deltaManagerReadonlySubmit", undefined /* error */, {
|
|
856
|
-
readonly: this.readOnlyInfo.readonly,
|
|
857
|
-
forcedReadonly: this.readOnlyInfo.forced,
|
|
858
|
-
readonlyPermissions: this.readOnlyInfo.permissions,
|
|
859
|
-
storageOnly: this.readOnlyInfo.storageOnly,
|
|
860
|
-
});
|
|
861
|
-
this.close(error);
|
|
862
|
-
return -1;
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
// reset clientSequenceNumber if we are using new clientId.
|
|
866
|
-
// we keep info about old connection as long as possible to be able to account for all non-acked ops
|
|
867
|
-
// that we pick up on next connection.
|
|
868
|
-
assert(!!this.connection, 0x0e4 /* "Lost old connection!" */);
|
|
869
|
-
if (this.lastSubmittedClientId !== this.connection?.clientId) {
|
|
870
|
-
this.lastSubmittedClientId = this.connection?.clientId;
|
|
871
|
-
this.clientSequenceNumber = 0;
|
|
872
|
-
this.clientSequenceNumberObserved = 0;
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
// If connection is "read" or implicit "read" (got leave op for "write" connection),
|
|
876
|
-
// then op can't make it through - we will get a nack if op is sent.
|
|
877
|
-
// We can short-circuit this process.
|
|
878
|
-
// Note that we also want nacks to be rare and be treated as catastrophic failures.
|
|
879
|
-
// Be careful with reentrancy though - disconnected event should not be be raised in the
|
|
880
|
-
// middle of the current workflow, but rather on clean stack!
|
|
881
|
-
if (this.connectionMode === "read" || this.downgradedConnection) {
|
|
882
|
-
if (!this.pendingReconnect) {
|
|
883
|
-
this.pendingReconnect = true;
|
|
884
|
-
Promise.resolve().then(async () => {
|
|
885
|
-
if (this.pendingReconnect) { // still valid?
|
|
886
|
-
return this.reconnectOnErrorCore(
|
|
887
|
-
"write", // connectionMode
|
|
888
|
-
"Switch to write", // message
|
|
889
|
-
);
|
|
890
|
-
}
|
|
891
|
-
})
|
|
892
|
-
.catch(() => {});
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
// Can return -1 here, but no other path does it (other than error path in Container),
|
|
896
|
-
// so it's better not to introduce new states.
|
|
897
|
-
return ++this.clientSequenceNumber;
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
const service = this.clientDetails.type === undefined || this.clientDetails.type === ""
|
|
901
|
-
? "unknown"
|
|
902
|
-
: this.clientDetails.type;
|
|
903
|
-
|
|
904
|
-
// Start adding trace for the op.
|
|
905
|
-
const traces: ITrace[] = [
|
|
906
|
-
{
|
|
907
|
-
action: "start",
|
|
908
|
-
service,
|
|
909
|
-
timestamp: Date.now(),
|
|
910
|
-
}];
|
|
911
|
-
|
|
912
|
-
const message: IDocumentMessage = {
|
|
913
|
-
clientSequenceNumber: ++this.clientSequenceNumber,
|
|
914
|
-
contents: JSON.stringify(contents),
|
|
915
|
-
metadata,
|
|
916
|
-
referenceSequenceNumber: this.lastProcessedSequenceNumber,
|
|
917
|
-
traces,
|
|
918
|
-
type,
|
|
919
|
-
};
|
|
920
|
-
|
|
921
|
-
if (type === MessageType.NoOp) {
|
|
922
|
-
this.trailingNoopCount++;
|
|
923
|
-
} else {
|
|
924
|
-
this.trailingNoopCount = 0;
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
this.emit("submitOp", message);
|
|
928
|
-
|
|
929
|
-
if (!batch) {
|
|
930
|
-
this.flush();
|
|
931
|
-
this.messageBuffer.push(message);
|
|
932
|
-
this.flush();
|
|
933
|
-
} else {
|
|
934
|
-
this.messageBuffer.push(message);
|
|
454
|
+
this.fetchMissingDeltas(args.reason);
|
|
935
455
|
}
|
|
936
456
|
|
|
937
|
-
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
public submitSignal(content: any) {
|
|
941
|
-
if (this.connection !== undefined) {
|
|
942
|
-
this.connection.submitSignal(content);
|
|
943
|
-
} else {
|
|
944
|
-
this.logger.sendErrorEvent({ eventName: "submitSignalDisconnected" });
|
|
945
|
-
}
|
|
457
|
+
this.connectionManager.connect(args.mode);
|
|
946
458
|
}
|
|
947
459
|
|
|
948
460
|
private async getDeltas(
|
|
@@ -960,10 +472,6 @@ export class DeltaManager
|
|
|
960
472
|
this.deltaStorage = await docService.connectToDeltaStorage();
|
|
961
473
|
}
|
|
962
474
|
|
|
963
|
-
assert(this.closeAbortController.signal.onabort === null, 0x1e8 /* "reentrancy" */);
|
|
964
|
-
const controller = new AbortController();
|
|
965
|
-
this.closeAbortController.signal.onabort = () => controller.abort();
|
|
966
|
-
|
|
967
475
|
let cancelFetch: (op: ISequencedDocumentMessage) => boolean;
|
|
968
476
|
|
|
969
477
|
if (to !== undefined) {
|
|
@@ -978,7 +486,7 @@ export class DeltaManager
|
|
|
978
486
|
early: true,
|
|
979
487
|
from,
|
|
980
488
|
to,
|
|
981
|
-
...this.
|
|
489
|
+
...this.connectionManager.connectionVerboseProps,
|
|
982
490
|
});
|
|
983
491
|
return;
|
|
984
492
|
}
|
|
@@ -997,6 +505,7 @@ export class DeltaManager
|
|
|
997
505
|
cancelFetch = (op: ISequencedDocumentMessage) => op.sequenceNumber >= this.lastObservedSeqNumber;
|
|
998
506
|
}
|
|
999
507
|
|
|
508
|
+
const controller = new AbortController();
|
|
1000
509
|
let opsFromFetch = false;
|
|
1001
510
|
|
|
1002
511
|
const opListener = (op: ISequencedDocumentMessage) => {
|
|
@@ -1010,9 +519,11 @@ export class DeltaManager
|
|
|
1010
519
|
}
|
|
1011
520
|
};
|
|
1012
521
|
|
|
1013
|
-
this._inbound.on("push", opListener);
|
|
1014
|
-
|
|
1015
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
|
+
|
|
1016
527
|
const stream = this.deltaStorage.fetchMessages(
|
|
1017
528
|
from, // inclusive
|
|
1018
529
|
to, // exclusive
|
|
@@ -1034,9 +545,9 @@ export class DeltaManager
|
|
|
1034
545
|
}
|
|
1035
546
|
}
|
|
1036
547
|
} finally {
|
|
1037
|
-
assert(!opsFromFetch, 0x289 /* "logic error" */);
|
|
1038
548
|
this.closeAbortController.signal.onabort = null;
|
|
1039
549
|
this._inbound.off("push", opListener);
|
|
550
|
+
assert(!opsFromFetch, 0x289 /* "logic error" */);
|
|
1040
551
|
}
|
|
1041
552
|
}
|
|
1042
553
|
|
|
@@ -1048,20 +559,12 @@ export class DeltaManager
|
|
|
1048
559
|
return;
|
|
1049
560
|
}
|
|
1050
561
|
this.closed = true;
|
|
1051
|
-
// Ensure that things like triggerConnect() will short circuit
|
|
1052
|
-
this._reconnectMode = ReconnectMode.Never;
|
|
1053
562
|
|
|
1054
|
-
this.
|
|
563
|
+
this.connectionManager.dispose(error);
|
|
1055
564
|
|
|
1056
|
-
|
|
1057
|
-
? `Closing DeltaManager (${error.message})`
|
|
1058
|
-
: "Closing DeltaManager";
|
|
1059
|
-
|
|
1060
|
-
// This raises "disconnect" event if we have active connection.
|
|
1061
|
-
this.disconnectFromDeltaStream(disconnectReason);
|
|
565
|
+
this.closeAbortController.abort();
|
|
1062
566
|
|
|
1063
567
|
this._inbound.clear();
|
|
1064
|
-
this._outbound.clear();
|
|
1065
568
|
this._inboundSignal.clear();
|
|
1066
569
|
|
|
1067
570
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
@@ -1072,11 +575,6 @@ export class DeltaManager
|
|
|
1072
575
|
// Drop pending messages - this will ensure catchUp() does not go into infinite loop
|
|
1073
576
|
this.pending = [];
|
|
1074
577
|
|
|
1075
|
-
// Notify everyone we are in read-only state.
|
|
1076
|
-
// Useful for data stores in case we hit some critical error,
|
|
1077
|
-
// to switch to a mode where user edits are not accepted
|
|
1078
|
-
this.set_readonlyPermissions(true);
|
|
1079
|
-
|
|
1080
578
|
// This needs to be the last thing we do (before removing listeners), as it causes
|
|
1081
579
|
// Container to dispose context and break ability of data stores / runtime to "hear"
|
|
1082
580
|
// from delta manager, including notification (above) about readonly state.
|
|
@@ -1092,6 +590,21 @@ export class DeltaManager
|
|
|
1092
590
|
}
|
|
1093
591
|
}
|
|
1094
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
|
+
|
|
1095
608
|
/**
|
|
1096
609
|
* Emit info about a delay in service communication on account of throttling.
|
|
1097
610
|
* @param id - Id of the connection that is delayed
|
|
@@ -1114,286 +627,6 @@ export class DeltaManager
|
|
|
1114
627
|
}
|
|
1115
628
|
}
|
|
1116
629
|
|
|
1117
|
-
private readonly opHandler = (documentId: string, messagesArg: ISequencedDocumentMessage[]) => {
|
|
1118
|
-
const messages = Array.isArray(messagesArg) ? messagesArg : [messagesArg];
|
|
1119
|
-
this.enqueueMessages(messages, "opHandler");
|
|
1120
|
-
};
|
|
1121
|
-
|
|
1122
|
-
private readonly signalHandler = (message: ISignalMessage) => {
|
|
1123
|
-
this._inboundSignal.push(message);
|
|
1124
|
-
};
|
|
1125
|
-
|
|
1126
|
-
// Always connect in write mode after getting nacked.
|
|
1127
|
-
private readonly nackHandler = (documentId: string, messages: INack[]) => {
|
|
1128
|
-
const message = messages[0];
|
|
1129
|
-
// TODO: we should remove this check when service updates?
|
|
1130
|
-
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
|
1131
|
-
if (this._readonlyPermissions) {
|
|
1132
|
-
this.close(createWriteError("writeOnReadOnlyDocument"));
|
|
1133
|
-
}
|
|
1134
|
-
|
|
1135
|
-
// check message.content for Back-compat with old service.
|
|
1136
|
-
const reconnectInfo = message.content !== undefined
|
|
1137
|
-
? getNackReconnectInfo(message.content) :
|
|
1138
|
-
createGenericNetworkError("nackReasonUnknown", undefined, true);
|
|
1139
|
-
|
|
1140
|
-
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
1141
|
-
this.reconnectOnError(
|
|
1142
|
-
"write",
|
|
1143
|
-
reconnectInfo,
|
|
1144
|
-
);
|
|
1145
|
-
};
|
|
1146
|
-
|
|
1147
|
-
// Connection mode is always read on disconnect/error unless the system mode was write.
|
|
1148
|
-
private readonly disconnectHandler = (disconnectReason) => {
|
|
1149
|
-
// Note: we might get multiple disconnect calls on same socket, as early disconnect notification
|
|
1150
|
-
// ("server_disconnect", ODSP-specific) is mapped to "disconnect"
|
|
1151
|
-
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
1152
|
-
this.reconnectOnError(
|
|
1153
|
-
this.defaultReconnectionMode,
|
|
1154
|
-
createReconnectError("dmDocumentDeltaConnectionDisconnected", disconnectReason),
|
|
1155
|
-
);
|
|
1156
|
-
};
|
|
1157
|
-
|
|
1158
|
-
private readonly errorHandler = (error) => {
|
|
1159
|
-
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
1160
|
-
this.reconnectOnError(
|
|
1161
|
-
this.defaultReconnectionMode,
|
|
1162
|
-
createReconnectError("dmDocumentDeltaConnectionError", error),
|
|
1163
|
-
);
|
|
1164
|
-
};
|
|
1165
|
-
|
|
1166
|
-
private readonly pongHandler = (latency: number) => {
|
|
1167
|
-
this.emit("pong", latency);
|
|
1168
|
-
};
|
|
1169
|
-
|
|
1170
|
-
/**
|
|
1171
|
-
* Once we've successfully gotten a connection, we need to set up state, attach event listeners, and process
|
|
1172
|
-
* initial messages.
|
|
1173
|
-
* @param connection - The newly established connection
|
|
1174
|
-
*/
|
|
1175
|
-
private setupNewSuccessfulConnection(connection: IDocumentDeltaConnection, requestedMode: ConnectionMode) {
|
|
1176
|
-
// Old connection should have been cleaned up before establishing a new one
|
|
1177
|
-
assert(this.connection === undefined, 0x0e6 /* "old connection exists on new connection setup" */);
|
|
1178
|
-
assert(this.connectionP !== undefined || this.closed,
|
|
1179
|
-
0x27f /* "reentrancy may result in incorrect behavior" */);
|
|
1180
|
-
assert(!connection.disposed, 0x28a /* "can't be disposed - Callers need to ensure that!" */);
|
|
1181
|
-
|
|
1182
|
-
this.connectionP = undefined;
|
|
1183
|
-
this.connection = connection;
|
|
1184
|
-
|
|
1185
|
-
// Does information in scopes & mode matches?
|
|
1186
|
-
// If we asked for "write" and got "read", then file is read-only
|
|
1187
|
-
// But if we ask read, server can still give us write.
|
|
1188
|
-
const readonly = !connection.claims.scopes.includes(ScopeType.DocWrite);
|
|
1189
|
-
|
|
1190
|
-
// This connection mode validation logic is moving to the driver layer in 0.44. These two asserts can be
|
|
1191
|
-
// removed after those packages have released and become ubiquitous.
|
|
1192
|
-
assert(requestedMode === "read" || readonly === (this.connectionMode === "read"),
|
|
1193
|
-
0x0e7 /* "claims/connectionMode mismatch" */);
|
|
1194
|
-
assert(!readonly || this.connectionMode === "read", 0x0e8 /* "readonly perf with write connection" */);
|
|
1195
|
-
|
|
1196
|
-
this.set_readonlyPermissions(readonly);
|
|
1197
|
-
|
|
1198
|
-
this.refreshDelayInfo(this.deltaStreamDelayId);
|
|
1199
|
-
|
|
1200
|
-
if (this.closed) {
|
|
1201
|
-
// Raise proper events, Log telemetry event and close connection.
|
|
1202
|
-
this.disconnectFromDeltaStream("DeltaManager already closed");
|
|
1203
|
-
return;
|
|
1204
|
-
}
|
|
1205
|
-
|
|
1206
|
-
// We cancel all ops on lost of connectivity, and rely on DDSes to resubmit them.
|
|
1207
|
-
// Semantics are not well defined for batches (and they are broken right now on disconnects anyway),
|
|
1208
|
-
// but it's safe to assume (until better design is put into place) that batches should not exist
|
|
1209
|
-
// across multiple connections. Right now we assume runtime will not submit any ops in disconnected
|
|
1210
|
-
// state. As requirements change, so should these checks.
|
|
1211
|
-
assert(this.messageBuffer.length === 0, 0x0e9 /* "messageBuffer is not empty on new connection" */);
|
|
1212
|
-
|
|
1213
|
-
this._outbound.resume();
|
|
1214
|
-
|
|
1215
|
-
connection.on("op", this.opHandler);
|
|
1216
|
-
connection.on("signal", this.signalHandler);
|
|
1217
|
-
connection.on("nack", this.nackHandler);
|
|
1218
|
-
connection.on("disconnect", this.disconnectHandler);
|
|
1219
|
-
connection.on("error", this.errorHandler);
|
|
1220
|
-
connection.on("pong", this.pongHandler);
|
|
1221
|
-
|
|
1222
|
-
// Initial messages are always sorted. However, due to early op handler installed by drivers and appending those
|
|
1223
|
-
// ops to initialMessages, resulting set is no longer sorted, which would result in client hitting storage to
|
|
1224
|
-
// fill in gap. We will recover by cancelling this request once we process remaining ops, but it's a waste that
|
|
1225
|
-
// we could avoid
|
|
1226
|
-
const initialMessages = connection.initialMessages.sort((a, b) => a.sequenceNumber - b.sequenceNumber);
|
|
1227
|
-
|
|
1228
|
-
this.connectionStateProps = {
|
|
1229
|
-
connectionLastQueuedSequenceNumber : this.lastQueuedSequenceNumber,
|
|
1230
|
-
connectionLastObservedSeqNumber: this.lastObservedSeqNumber,
|
|
1231
|
-
clientId: connection.clientId,
|
|
1232
|
-
mode: connection.mode,
|
|
1233
|
-
};
|
|
1234
|
-
if (connection.relayServiceAgent !== undefined) {
|
|
1235
|
-
this.connectionStateProps.relayServiceAgent = connection.relayServiceAgent;
|
|
1236
|
-
}
|
|
1237
|
-
this._hasCheckpointSequenceNumber = false;
|
|
1238
|
-
|
|
1239
|
-
// Some storages may provide checkpointSequenceNumber to identify how far client is behind.
|
|
1240
|
-
const checkpointSequenceNumber = connection.checkpointSequenceNumber;
|
|
1241
|
-
if (checkpointSequenceNumber !== undefined) {
|
|
1242
|
-
this._hasCheckpointSequenceNumber = true;
|
|
1243
|
-
this.updateLatestKnownOpSeqNumber(checkpointSequenceNumber);
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
|
-
// Update knowledge of how far we are behind, before raising "connect" event
|
|
1247
|
-
// This is duplication of what enqueueMessages() does, but we have to raise event before we get there,
|
|
1248
|
-
// so duplicating update logic here as well.
|
|
1249
|
-
const last = initialMessages.length > 0 ? initialMessages[initialMessages.length - 1].sequenceNumber : -1;
|
|
1250
|
-
if (initialMessages.length > 0) {
|
|
1251
|
-
this._hasCheckpointSequenceNumber = true;
|
|
1252
|
-
this.updateLatestKnownOpSeqNumber(last);
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
// Notify of the connection
|
|
1256
|
-
// WARNING: This has to happen before processInitialMessages() call below.
|
|
1257
|
-
// If not, we may not update Container.pendingClientId in time before seeing our own join session op.
|
|
1258
|
-
this.emit(
|
|
1259
|
-
"connect",
|
|
1260
|
-
DeltaManager.detailsFromConnection(connection),
|
|
1261
|
-
this._hasCheckpointSequenceNumber ? this.lastObservedSeqNumber - this.lastSequenceNumber : undefined);
|
|
1262
|
-
|
|
1263
|
-
this.enqueueMessages(
|
|
1264
|
-
initialMessages,
|
|
1265
|
-
this.connectFirstConnection ? "InitialOps" : "ReconnectOps");
|
|
1266
|
-
|
|
1267
|
-
if (connection.initialSignals !== undefined) {
|
|
1268
|
-
for (const signal of connection.initialSignals) {
|
|
1269
|
-
this._inboundSignal.push(signal);
|
|
1270
|
-
}
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
// If we got some initial ops, then we know the gap and call above fetched ops to fill it.
|
|
1274
|
-
// Same is true for "write" mode even if we have no ops - we will get self "join" ops very very soon.
|
|
1275
|
-
// However if we are connecting as view-only, then there is no good signal to realize if client is behind.
|
|
1276
|
-
// Thus we have to hit storage to see if any ops are there.
|
|
1277
|
-
if (initialMessages.length === 0) {
|
|
1278
|
-
if (checkpointSequenceNumber !== undefined) {
|
|
1279
|
-
// We know how far we are behind (roughly). If it's non-zero gap, fetch ops right away.
|
|
1280
|
-
if (checkpointSequenceNumber > this.lastQueuedSequenceNumber) {
|
|
1281
|
-
this.fetchMissingDeltas("AfterConnection", this.lastQueuedSequenceNumber);
|
|
1282
|
-
}
|
|
1283
|
-
// we do not know the gap, and we will not learn about it if socket is quite - have to ask.
|
|
1284
|
-
} else if (connection.mode === "read") {
|
|
1285
|
-
this.fetchMissingDeltas("AfterReadConnection", this.lastQueuedSequenceNumber);
|
|
1286
|
-
}
|
|
1287
|
-
} else {
|
|
1288
|
-
this.connectionStateProps.connectionInitialOpsFrom = initialMessages[0].sequenceNumber;
|
|
1289
|
-
this.connectionStateProps.connectionInitialOpsTo = last + 1;
|
|
1290
|
-
}
|
|
1291
|
-
|
|
1292
|
-
this.connectFirstConnection = false;
|
|
1293
|
-
}
|
|
1294
|
-
|
|
1295
|
-
/**
|
|
1296
|
-
* Disconnect the current connection.
|
|
1297
|
-
* @param reason - Text description of disconnect reason to emit with disconnect event
|
|
1298
|
-
*/
|
|
1299
|
-
private disconnectFromDeltaStream(reason: string) {
|
|
1300
|
-
this.pendingReconnect = false;
|
|
1301
|
-
this.downgradedConnection = false;
|
|
1302
|
-
|
|
1303
|
-
if (this.connection === undefined) {
|
|
1304
|
-
return false;
|
|
1305
|
-
}
|
|
1306
|
-
|
|
1307
|
-
assert(this.connectionP === undefined, 0x27b /* "reentrancy may result in incorrect behavior" */);
|
|
1308
|
-
|
|
1309
|
-
const connection = this.connection;
|
|
1310
|
-
// Avoid any re-entrancy - clear object reference
|
|
1311
|
-
this.connection = undefined;
|
|
1312
|
-
|
|
1313
|
-
// Remove listeners first so we don't try to retrigger this flow accidentally through reconnectOnError
|
|
1314
|
-
connection.off("op", this.opHandler);
|
|
1315
|
-
connection.off("signal", this.signalHandler);
|
|
1316
|
-
connection.off("nack", this.nackHandler);
|
|
1317
|
-
connection.off("disconnect", this.disconnectHandler);
|
|
1318
|
-
connection.off("error", this.errorHandler);
|
|
1319
|
-
connection.off("pong", this.pongHandler);
|
|
1320
|
-
|
|
1321
|
-
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
1322
|
-
this._outbound.pause();
|
|
1323
|
-
this._outbound.clear();
|
|
1324
|
-
this.emit("disconnect", reason);
|
|
1325
|
-
|
|
1326
|
-
connection.dispose();
|
|
1327
|
-
|
|
1328
|
-
this.connectionStateProps = {};
|
|
1329
|
-
|
|
1330
|
-
return true;
|
|
1331
|
-
}
|
|
1332
|
-
|
|
1333
|
-
/**
|
|
1334
|
-
* Disconnect the current connection and reconnect.
|
|
1335
|
-
* @param connection - The connection that wants to reconnect - no-op if it's different from this.connection
|
|
1336
|
-
* @param requestedMode - Read or write
|
|
1337
|
-
* @param error - Error reconnect information including whether or not to reconnect
|
|
1338
|
-
* @returns A promise that resolves when the connection is reestablished or we stop trying
|
|
1339
|
-
*/
|
|
1340
|
-
private async reconnectOnError(
|
|
1341
|
-
requestedMode: ConnectionMode,
|
|
1342
|
-
error: DriverError,
|
|
1343
|
-
) {
|
|
1344
|
-
return this.reconnectOnErrorCore(
|
|
1345
|
-
requestedMode,
|
|
1346
|
-
error.message,
|
|
1347
|
-
error);
|
|
1348
|
-
}
|
|
1349
|
-
|
|
1350
|
-
/**
|
|
1351
|
-
* Disconnect the current connection and reconnect.
|
|
1352
|
-
* @param connection - The connection that wants to reconnect - no-op if it's different from this.connection
|
|
1353
|
-
* @param requestedMode - Read or write
|
|
1354
|
-
* @param error - Error reconnect information including whether or not to reconnect
|
|
1355
|
-
* @returns A promise that resolves when the connection is reestablished or we stop trying
|
|
1356
|
-
*/
|
|
1357
|
-
private async reconnectOnErrorCore(
|
|
1358
|
-
requestedMode: ConnectionMode,
|
|
1359
|
-
disconnectMessage: string,
|
|
1360
|
-
error?: DriverError,
|
|
1361
|
-
) {
|
|
1362
|
-
// We quite often get protocol errors before / after observing nack/disconnect
|
|
1363
|
-
// we do not want to run through same sequence twice.
|
|
1364
|
-
// If we're already disconnected/disconnecting it's not appropriate to call this again.
|
|
1365
|
-
assert(this.connection !== undefined, 0x0eb /* "Missing connection for reconnect" */);
|
|
1366
|
-
|
|
1367
|
-
this.disconnectFromDeltaStream(disconnectMessage);
|
|
1368
|
-
|
|
1369
|
-
const canRetry = error !== undefined ? canRetryOnError(error) : true;
|
|
1370
|
-
|
|
1371
|
-
// If reconnection is not an option, close the DeltaManager
|
|
1372
|
-
if (!canRetry) {
|
|
1373
|
-
this.close(normalizeError(error, { props: fatalConnectErrorProp }));
|
|
1374
|
-
} else if (this.reconnectMode === ReconnectMode.Never) {
|
|
1375
|
-
// Do not raise container error if we are closing just because we lost connection.
|
|
1376
|
-
// Those errors (like IdleDisconnect) would show up in telemetry dashboards and
|
|
1377
|
-
// are very misleading, as first initial reaction - some logic is broken.
|
|
1378
|
-
this.close();
|
|
1379
|
-
}
|
|
1380
|
-
|
|
1381
|
-
// If closed then we can't reconnect
|
|
1382
|
-
if (this.closed) {
|
|
1383
|
-
return;
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
if (this.reconnectMode === ReconnectMode.Enabled) {
|
|
1387
|
-
const delayMs = error !== undefined ? getRetryDelayFromError(error) : undefined;
|
|
1388
|
-
if (delayMs !== undefined) {
|
|
1389
|
-
this.emitDelayInfo(this.deltaStreamDelayId, delayMs, error);
|
|
1390
|
-
await waitForConnectedState(delayMs);
|
|
1391
|
-
}
|
|
1392
|
-
|
|
1393
|
-
this.triggerConnect({ reason: "reconnect", mode: requestedMode, fetchOpsFromStorage: false });
|
|
1394
|
-
}
|
|
1395
|
-
}
|
|
1396
|
-
|
|
1397
630
|
// returns parts of message (in string format) that should never change for a given message.
|
|
1398
631
|
// Used for message comparison. It attempts to avoid comparing fields that potentially may differ.
|
|
1399
632
|
// for example, it's not clear if serverMetadata or timestamp property is a property of message or server state.
|
|
@@ -1487,7 +720,7 @@ export class DeltaManager
|
|
|
1487
720
|
gap: gap > 0 ? gap : undefined,
|
|
1488
721
|
firstMissing,
|
|
1489
722
|
dmInitialSeqNumber: this.initialSequenceNumber,
|
|
1490
|
-
...this.
|
|
723
|
+
...this.connectionManager.connectionVerboseProps,
|
|
1491
724
|
});
|
|
1492
725
|
}
|
|
1493
726
|
}
|
|
@@ -1512,7 +745,7 @@ export class DeltaManager
|
|
|
1512
745
|
undefined,
|
|
1513
746
|
DriverErrorType.fileOverwrittenInStorage,
|
|
1514
747
|
{
|
|
1515
|
-
clientId: this.
|
|
748
|
+
clientId: this.connectionManager.clientId,
|
|
1516
749
|
sequenceNumber: message.sequenceNumber,
|
|
1517
750
|
message1,
|
|
1518
751
|
message2,
|
|
@@ -1523,7 +756,7 @@ export class DeltaManager
|
|
|
1523
756
|
}
|
|
1524
757
|
} else if (message.sequenceNumber !== this.lastQueuedSequenceNumber + 1) {
|
|
1525
758
|
this.pending.push(message);
|
|
1526
|
-
this.fetchMissingDeltas(reason,
|
|
759
|
+
this.fetchMissingDeltas(reason, message.sequenceNumber);
|
|
1527
760
|
} else {
|
|
1528
761
|
this.lastQueuedSequenceNumber = message.sequenceNumber;
|
|
1529
762
|
this.previouslyProcessedMessage = message;
|
|
@@ -1549,24 +782,6 @@ export class DeltaManager
|
|
|
1549
782
|
0x0ed /* "non-system message have to have clientId" */,
|
|
1550
783
|
);
|
|
1551
784
|
|
|
1552
|
-
// if we have connection, and message is local, then we better treat is as local!
|
|
1553
|
-
assert(
|
|
1554
|
-
this.connection === undefined
|
|
1555
|
-
|| this.connection.clientId !== message.clientId
|
|
1556
|
-
|| this.lastSubmittedClientId === message.clientId,
|
|
1557
|
-
0x0ee /* "Not accounting local messages correctly" */,
|
|
1558
|
-
);
|
|
1559
|
-
|
|
1560
|
-
if (this.lastSubmittedClientId !== undefined && this.lastSubmittedClientId === message.clientId) {
|
|
1561
|
-
const clientSequenceNumber = message.clientSequenceNumber;
|
|
1562
|
-
|
|
1563
|
-
assert(this.clientSequenceNumberObserved < clientSequenceNumber, 0x0ef /* "client seq# not growing" */);
|
|
1564
|
-
assert(clientSequenceNumber <= this.clientSequenceNumber,
|
|
1565
|
-
0x0f0 /* "Incoming local client seq# > generated by this client" */);
|
|
1566
|
-
|
|
1567
|
-
this.clientSequenceNumberObserved = clientSequenceNumber;
|
|
1568
|
-
}
|
|
1569
|
-
|
|
1570
785
|
// TODO Remove after SPO picks up the latest build.
|
|
1571
786
|
if (
|
|
1572
787
|
typeof message.contents === "string"
|
|
@@ -1576,24 +791,13 @@ export class DeltaManager
|
|
|
1576
791
|
message.contents = JSON.parse(message.contents);
|
|
1577
792
|
}
|
|
1578
793
|
|
|
1579
|
-
|
|
1580
|
-
const systemLeaveMessage = message as ISequencedDocumentSystemMessage;
|
|
1581
|
-
const clientId = JSON.parse(systemLeaveMessage.data) as string;
|
|
1582
|
-
if (clientId === this.connection?.clientId) {
|
|
1583
|
-
// We have been kicked out from quorum
|
|
1584
|
-
this.logger.sendPerformanceEvent({ eventName: "ReadConnectionTransition" });
|
|
1585
|
-
this.downgradedConnection = true;
|
|
1586
|
-
}
|
|
1587
|
-
}
|
|
794
|
+
this.connectionManager.beforeProcessingIncomingOp(message);
|
|
1588
795
|
|
|
1589
796
|
// Add final ack trace.
|
|
1590
797
|
if (message.traces !== undefined && message.traces.length > 0) {
|
|
1591
|
-
const service = this.clientDetails.type === undefined || this.clientDetails.type === ""
|
|
1592
|
-
? "unknown"
|
|
1593
|
-
: this.clientDetails.type;
|
|
1594
798
|
message.traces.push({
|
|
1595
799
|
action: "end",
|
|
1596
|
-
service,
|
|
800
|
+
service: "client",
|
|
1597
801
|
timestamp: Date.now(),
|
|
1598
802
|
});
|
|
1599
803
|
}
|
|
@@ -1602,7 +806,7 @@ export class DeltaManager
|
|
|
1602
806
|
if (this.minSequenceNumber > message.minimumSequenceNumber) {
|
|
1603
807
|
throw new DataCorruptionError("msnMovesBackwards", {
|
|
1604
808
|
...extractLogSafeMessageProperties(message),
|
|
1605
|
-
clientId: this.
|
|
809
|
+
clientId: this.connectionManager.clientId,
|
|
1606
810
|
});
|
|
1607
811
|
}
|
|
1608
812
|
this.minSequenceNumber = message.minimumSequenceNumber;
|
|
@@ -1610,7 +814,7 @@ export class DeltaManager
|
|
|
1610
814
|
if (message.sequenceNumber !== this.lastProcessedSequenceNumber + 1) {
|
|
1611
815
|
throw new DataCorruptionError("nonSequentialSequenceNumber", {
|
|
1612
816
|
...extractLogSafeMessageProperties(message),
|
|
1613
|
-
clientId: this.
|
|
817
|
+
clientId: this.connectionManager.clientId,
|
|
1614
818
|
});
|
|
1615
819
|
}
|
|
1616
820
|
this.lastProcessedSequenceNumber = message.sequenceNumber;
|
|
@@ -1640,8 +844,8 @@ export class DeltaManager
|
|
|
1640
844
|
/**
|
|
1641
845
|
* Retrieves the missing deltas between the given sequence numbers
|
|
1642
846
|
*/
|
|
1643
|
-
private fetchMissingDeltas(reasonArg: string,
|
|
1644
|
-
this.fetchMissingDeltasCore(reasonArg, false /* cacheOnly */,
|
|
847
|
+
private fetchMissingDeltas(reasonArg: string, to?: number) {
|
|
848
|
+
this.fetchMissingDeltasCore(reasonArg, false /* cacheOnly */, to).catch((error) => {
|
|
1645
849
|
this.logger.sendErrorEvent({ eventName: "fetchMissingDeltasException" }, error);
|
|
1646
850
|
});
|
|
1647
851
|
}
|
|
@@ -1652,7 +856,6 @@ export class DeltaManager
|
|
|
1652
856
|
private async fetchMissingDeltasCore(
|
|
1653
857
|
reason: string,
|
|
1654
858
|
cacheOnly: boolean,
|
|
1655
|
-
lastKnowOp: number,
|
|
1656
859
|
to?: number)
|
|
1657
860
|
{
|
|
1658
861
|
// Exit out early if we're already fetching deltas
|
|
@@ -1667,13 +870,12 @@ export class DeltaManager
|
|
|
1667
870
|
|
|
1668
871
|
if (this.handler === undefined) {
|
|
1669
872
|
// We do not poses yet any information
|
|
1670
|
-
assert(
|
|
873
|
+
assert(this.lastQueuedSequenceNumber === 0, 0x26b /* "initial state" */);
|
|
1671
874
|
return;
|
|
1672
875
|
}
|
|
1673
876
|
|
|
1674
877
|
try {
|
|
1675
|
-
|
|
1676
|
-
let from = lastKnowOp + 1;
|
|
878
|
+
let from = this.lastQueuedSequenceNumber + 1;
|
|
1677
879
|
|
|
1678
880
|
const n = this.previouslyProcessedMessage?.sequenceNumber;
|
|
1679
881
|
if (n !== undefined) {
|
|
@@ -1682,7 +884,7 @@ export class DeltaManager
|
|
|
1682
884
|
// Knowing about this mechanism, we could ask for op we already observed to increase validation.
|
|
1683
885
|
// This is especially useful when coming out of offline mode or loading from
|
|
1684
886
|
// very old cached (by client / driver) snapshot.
|
|
1685
|
-
assert(n ===
|
|
887
|
+
assert(n === this.lastQueuedSequenceNumber, 0x0f2 /* "previouslyProcessedMessage" */);
|
|
1686
888
|
assert(from > 1, 0x0f3 /* "not positive" */);
|
|
1687
889
|
from--;
|
|
1688
890
|
}
|
|
@@ -1738,7 +940,7 @@ export class DeltaManager
|
|
|
1738
940
|
// (the other 50%), and thus these errors below should be looked at even if code below results in
|
|
1739
941
|
// recovery.
|
|
1740
942
|
if (this.lastQueuedSequenceNumber < this.lastObservedSeqNumber) {
|
|
1741
|
-
this.fetchMissingDeltas("OpsBehind"
|
|
943
|
+
this.fetchMissingDeltas("OpsBehind");
|
|
1742
944
|
}
|
|
1743
945
|
}
|
|
1744
946
|
}
|