@fluidframework/test-utils 0.49.0 → 0.50.0-41540

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.
@@ -1,508 +0,0 @@
1
- /*!
2
- * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
- * Licensed under the MIT License.
4
- */
5
-
6
- import { strict as assert } from "assert";
7
- import { IDeltaManager } from "@fluidframework/container-definitions";
8
- import {
9
- IDocumentMessage,
10
- ISequencedDocumentMessage,
11
- ISequencedDocumentSystemMessage,
12
- MessageType,
13
- } from "@fluidframework/protocol-definitions";
14
- import { debug } from "./debug";
15
-
16
- // An IDeltaManager alias to be used within this class.
17
- export type DeltaManager = IDeltaManager<ISequencedDocumentMessage, IDocumentMessage>;
18
-
19
- class DeltaManagerToggle {
20
- private inboundPauseP: Promise<void> | undefined;
21
- private outboundPauseP: Promise<void> | undefined;
22
- constructor(public readonly deltaManager: DeltaManager) {
23
- }
24
-
25
- public async togglePauseAll() {
26
- return Promise.all([this.togglePauseInbound(), this.togglePauseOutbound()]);
27
- }
28
-
29
- public toggleResumeAll() {
30
- this.toggleResumeInbound();
31
- this.toggleResumeOutbound();
32
- }
33
- public async togglePauseInbound() {
34
- if (!this.inboundPauseP) {
35
- this.inboundPauseP = this.deltaManager.inbound.pause();
36
- }
37
- return this.inboundPauseP;
38
- }
39
-
40
- public async togglePauseOutbound() {
41
- if (!this.outboundPauseP) {
42
- this.outboundPauseP = this.deltaManager.outbound.pause();
43
- }
44
- return this.outboundPauseP;
45
- }
46
-
47
- public toggleResumeInbound() {
48
- if (this.inboundPauseP) {
49
- this.inboundPauseP = undefined;
50
- this.deltaManager.inbound.resume();
51
- }
52
- }
53
-
54
- public toggleResumeOutbound() {
55
- if (this.outboundPauseP) {
56
- this.outboundPauseP = undefined;
57
- this.deltaManager.outbound.resume();
58
- }
59
- }
60
-
61
- public get inboundPaused() {
62
- return this.inboundPauseP !== undefined;
63
- }
64
- }
65
-
66
- /**
67
- * Monitor for DeltaManager, and track in/out ops to figure out whether there are
68
- * outstanding ops that the server hasn't ack yet. Used by the OpProcessingController
69
- * to wait for all the ops has round tripped.
70
- *
71
- * For outbound, we monitor ops leaving the outbound queue on the "op" event.
72
- * For inbound, we monitor the first moment we see an op coming back on the "push" event.
73
- *
74
- * It also monitor connect and disconnect state so that we can refresh the tracking and clientId
75
- *
76
- * The monitor ignores ops generated by the server. It also don't track NoOp since the server
77
- * might coalesce them with other ops, or a single NoOp, or delay it if it don't think it is necessary
78
- */
79
- class DeltaManagerMonitor extends DeltaManagerToggle {
80
- private pendingCount: number = 0;
81
- public clientId: string | undefined;
82
- public readMode = true;
83
- private firstClientSequenceNumber: number = -1;
84
- private lastOutbound: IDocumentMessage | undefined;
85
- private readonly pendingLeaveClientIds = new Set<string>();
86
- private readonly lastInboundPerClient = new Map<string, ISequencedDocumentMessage>();
87
- private pendingWriteConnection = false;
88
-
89
- /**
90
- * Determines if this monitor should expect work/ops from the outbound monitor.
91
- * @param outbound - the monitor who's outbound to consider
92
- */
93
- public expectingInboundFrom(outbound: DeltaManagerMonitor): boolean {
94
- // there should be no outstanding work for disposed delta managers
95
- if (this.deltaManager.disposed || outbound.deltaManager.disposed) {
96
- return false;
97
- }
98
- // if there is no last outbound, we are not waiting for anything
99
- if (outbound.lastOutbound === undefined
100
- || outbound.clientId === undefined) {
101
- return false;
102
- }
103
- // if out inbound is paused we are not expecting to receive anything more
104
- if (this.inboundPaused) {
105
- return false;
106
- }
107
-
108
- // if outbound is ourself, return if we having pending work
109
- if (this === outbound) {
110
- return this.hasPendingWork();
111
- }
112
-
113
- // check if we are waiting to see a message from outbound
114
- const lastInboundForOutbound = this.lastInboundPerClient.get(outbound.clientId);
115
- if (lastInboundForOutbound !== undefined) {
116
- return outbound.lastOutbound.clientSequenceNumber > lastInboundForOutbound.clientSequenceNumber;
117
- }
118
-
119
- // has pending work will be true for outbound until it receives it's own seq
120
- // this check ensures the other client has seen the same ops as the outbound
121
- return outbound.latestSequenceNumber > this.latestSequenceNumber;
122
- }
123
-
124
- constructor(deltaManager: DeltaManager) {
125
- super(deltaManager);
126
-
127
- // The deltaManager may be connected already, need to get the clientId.
128
- // TODO: hackery to get the clientId from the delta manager, find a better way
129
- const anyDeltaManager = deltaManager as any;
130
- // Unwrap the proxy if there is any
131
- const fullDeltaManager = (anyDeltaManager.deltaManager ?? anyDeltaManager);
132
- const id = fullDeltaManager.connection?.clientId;
133
- if (id !== undefined) {
134
- this.connect(id);
135
- }
136
-
137
- deltaManager.on("connect", (details) => this.connect(details.clientId));
138
- deltaManager.on("disconnect", (reason) => {
139
- assert(this.clientId !== undefined);
140
- this.trace("DIS");
141
- this.clientId = undefined;
142
- // Once disconnected, the runtime is going to keep track of ops and replay as necessary
143
- // Clear the pending count and start anew
144
- this.pendingCount = 0;
145
- this.firstClientSequenceNumber = -1;
146
- this.lastOutbound = undefined;
147
- });
148
- deltaManager.outbound.on("op", this.outbound.bind(this));
149
- deltaManager.inbound.on("push", this.inbound.bind(this));
150
- }
151
-
152
- public get latestSequenceNumber() {
153
- return this.deltaManager.lastSequenceNumber;
154
- }
155
-
156
- public hasPendingWork() {
157
- return !this.deltaManager.disposed
158
- && (this.pendingWriteConnection || this.pendingCount !== 0 || this.pendingLeaveClientIds.size !== 0);
159
- }
160
-
161
- private connect(clientId: string) {
162
- this.clientId = clientId;
163
- this.readMode = !this.deltaManager.active;
164
- this.trace("CON");
165
- }
166
- private inbound(message: ISequencedDocumentMessage) {
167
- if (message.clientId) {
168
- this.lastInboundPerClient.set(message.clientId, message);
169
- }
170
- if (message.type === MessageType.ClientLeave) {
171
- const systemLeaveMessage = message as ISequencedDocumentSystemMessage;
172
- const clientId = JSON.parse(systemLeaveMessage.data) as string;
173
- this.lastInboundPerClient.delete(clientId);
174
- this.pendingLeaveClientIds.delete(clientId);
175
- }
176
-
177
- if (this.clientId === undefined) {
178
- // Ignore message when we are not connected.
179
- return;
180
- }
181
-
182
- if (message.clientId === undefined || message.clientId !== this.clientId) {
183
- this.trace("SEQ", message.type);
184
- return;
185
- }
186
-
187
- if (this.firstClientSequenceNumber === -1 || this.firstClientSequenceNumber > message.clientSequenceNumber) {
188
- this.trace("SEQ", message.type);
189
- // if we haven't seen any outbound or the message is before the outbound message that we have seen,
190
- // then message is sent before we start monitoring, ignore.
191
- return;
192
- }
193
-
194
- // Need to filter system messages
195
- switch (message.type) {
196
- case MessageType.ClientJoin:
197
- case MessageType.ClientLeave:
198
- assert(false, "join and leave message shouldn't have clientId");
199
- // These are generated by the server, don't count
200
- case MessageType.NoOp:
201
- case MessageType.NoClient:
202
- this.trace("SEQ", message.type);
203
- break;
204
- default:
205
- assert(this.pendingCount);
206
- this.pendingCount--;
207
- this.trace("IN", message.type);
208
- }
209
- }
210
-
211
- private outbound(messages: IDocumentMessage[]) {
212
- assert(this.clientId);
213
- assert(messages.length);
214
- if (this.firstClientSequenceNumber === -1) {
215
- // save the client sequence number of the first outbound message we see
216
- // to exclude any message that was sent before we start monitoring the delta manager
217
- this.firstClientSequenceNumber = messages[0].clientSequenceNumber;
218
- }
219
- // if we are not active, the outbound with nack, and we will reconnect write
220
- // this flag tracks the process. after reconnection, the op will be resubmitted
221
- // on the write connection and reset this flag
222
- this.pendingWriteConnection = !this.deltaManager.active;
223
- for (const message of messages) {
224
- // No-op's are not directly broadcast
225
- // the server coaleses and send it's own
226
- // no-op if no user messages arrive
227
- // to bump min seq
228
- if (message.type !== MessageType.NoOp) {
229
- this.pendingCount++;
230
- this.lastOutbound = message;
231
- }
232
- this.trace("OUT", message.type);
233
- }
234
- }
235
-
236
- public trace(action: string, op?: string) {
237
- debug(`DeltaConnectionMonitor: ${action.padEnd(3)}: ${this.clientId} `
238
- + `pending:${this.pendingCount} seq:${this.latestSequenceNumber} ${op ?? ""}`);
239
- }
240
-
241
- public onClientDisconnect(clientId: string) {
242
- // Keep track of a list of clientIds that we expect leave message from
243
- this.pendingLeaveClientIds.add(clientId);
244
- }
245
- }
246
- /**
247
- * @deprecated OpProcessingController has been improved to not need server information and work against other servers.
248
- * So this is no longer necessary, and allows this and test to be run against different endpoints.
249
- */
250
- export interface IDeltaConnectionServerMonitor {
251
- hasPendingWork(): Promise<boolean>;
252
- }
253
-
254
- /**
255
- * Class with access to the local delta connection server and delta managers that can control op processing.
256
- *
257
- * @deprecated Can be removed \>=0.38. Replaced with LoaderContainerTracker
258
- */
259
- export class OpProcessingController {
260
- /**
261
- * Yields control in the JavaScript event loop.
262
- */
263
- public static async yield(): Promise<void> {
264
- await new Promise<void>((resolve) => {
265
- setTimeout(resolve, 0);
266
- });
267
- }
268
-
269
- private readonly deltaManagerMonitors = new Map<DeltaManager, DeltaManagerMonitor>();
270
-
271
- private isNormalProcessingPaused = false;
272
-
273
- /*
274
- * Is processing being deterministically controlled, or are changes allowed to flow freely?
275
- */
276
- public get isProcessingControlled(): boolean {
277
- return this.isNormalProcessingPaused;
278
- }
279
-
280
- /**
281
- * @param deltaConnectionServerMonitor - delta connection server monitor to tell whether we have
282
- * pending work
283
- */
284
- public constructor(private readonly deltaConnectionServerMonitor?: IDeltaConnectionServerMonitor) { }
285
-
286
- /**
287
- * Add a collection of delta managers by adding them to the local collection.
288
- * @param deltaManagers - Array of deltaManagers to add
289
- */
290
- public addDeltaManagers(...deltaManagers: DeltaManager[]) {
291
- deltaManagers.forEach((deltaManager) => {
292
- const monitorSetup = (monitor1: DeltaManagerMonitor, monitor2: DeltaManagerMonitor) => {
293
- if (monitor1.clientId !== undefined && monitor1.deltaManager.active) {
294
- const clientId = monitor1.clientId;
295
- monitor1.deltaManager.once("disconnect", () => {
296
- monitor2.onClientDisconnect(clientId);
297
- });
298
- }
299
- monitor1.deltaManager.on("connect", (details) => {
300
- if (monitor1.deltaManager.active) {
301
- monitor1.deltaManager.once("disconnect", () => {
302
- monitor2.onClientDisconnect(details.clientId);
303
- });
304
- }
305
- });
306
- };
307
-
308
- // Wire up event listener so we can keep track of leave message that we expects
309
- const newMonitor = new DeltaManagerMonitor(deltaManager);
310
- for (const monitor of this.deltaManagerMonitors.values()) {
311
- monitorSetup(newMonitor, monitor);
312
- monitorSetup(monitor, newMonitor);
313
- }
314
-
315
- this.deltaManagerMonitors.set(deltaManager, newMonitor);
316
- });
317
- }
318
-
319
- /**
320
- * Processes incoming and outgoing op) of the given delta managers.
321
- * It validates the delta managers and resumes its inbound and outbound queues. It then keeps yielding
322
- * the JS event loop until all the ops have been processed by the server and by the delta managers.
323
- *
324
- * @param deltaMangers - Array of delta managers whose ops to process. If no delta manager is provided, it
325
- * processes the ops for all the delta managers in our collection.
326
- */
327
- public async process(...deltaMangers: DeltaManager[]): Promise<void> {
328
- const monitors = this.mapDeltaManagerMonitor(deltaMangers);
329
-
330
- // Pause the queues of all the delta managers in our collection to make sure that we only process the ops of
331
- // the requested delta managers.
332
- await this.pauseAllDeltaManagerQueues();
333
-
334
- // Resume the delta queues so that we can process incoming and outgoing ops.
335
- monitors.forEach((monitor) => monitor.toggleResumeAll());
336
-
337
- // Wait for all pending ops to be processed.
338
- await this.yieldWhileDeltaManagersHaveWork(
339
- monitors,
340
- (deltaManager) => !deltaManager.inbound.idle || !deltaManager.outbound.idle);
341
- }
342
-
343
- /**
344
- * Processes incoming ops of the given delta managers.
345
- * It validates the delta managers and resumes its inbound queue. It then keeps yielding the JS event loop until
346
- * all the ops have been processed by the server and by the delta managers.
347
- *
348
- * @param deltaMangers - Array of delta managers whose incoming ops to process. If no delta manager is provided, it
349
- * processes the ops for all the delta managers in our collection.
350
- */
351
- public async processIncoming(...deltaMangers: DeltaManager[]): Promise<void> {
352
- const monitors = this.mapDeltaManagerMonitor(deltaMangers);
353
-
354
- // Pause the queues of all the delta managers in our collection to make sure that we only process the incoming
355
- // ops of the requested delta managers.
356
- await this.pauseAllDeltaManagerQueues();
357
-
358
- // Resume the inbound delta queue so that we can process incoming ops.
359
- monitors.forEach((monitor) => {
360
- monitor.toggleResumeInbound();
361
- });
362
-
363
- // Wait for all pending incoming ops to be processed.
364
- await this.yieldWhileDeltaManagersHaveWork(
365
- monitors,
366
- (deltaManager) => !deltaManager.inbound.idle);
367
- }
368
-
369
- /**
370
- * Processes outgoing ops of the given delta managers.
371
- * It validates the delta managers and resumes its outbound queue. It then keeps yielding the JS event loop until
372
- * all the ops have been processed by the server and by the delta managers.
373
- *
374
- * @param deltaMangers - Array of delta managers whose outgoing ops to process. If no delta manager is provided, it
375
- * processes the ops for all the delta managers in our collection.
376
- */
377
- public async processOutgoing(...deltaMangers: DeltaManager[]): Promise<void> {
378
- const monitors = this.mapDeltaManagerMonitor(deltaMangers);
379
-
380
- // Pause the queues of all the delta managers in our collection to make sure that we only process the outgoing
381
- // ops of the requested delta managers.
382
- await this.pauseAllDeltaManagerQueues();
383
-
384
- // Resume the outbound delta queue so that we can process outgoing ops.
385
- monitors.forEach((monitor) => {
386
- monitor.toggleResumeOutbound();
387
- });
388
-
389
- // Wait for all pending outgoing ops to be processed.
390
- await this.yieldWhileDeltaManagersHaveWork(
391
- monitors,
392
- (deltaManager) => !deltaManager.outbound.idle);
393
- }
394
-
395
- /**
396
- * Pauses the delta processing for controlled testing by pausing the inbound and outbound queues of the delta
397
- * managers.
398
- *
399
- * @param deltaMangers - Array of delta managers whose processing to pause. If no delta manager is provided, it
400
- * pauses the processing of all the delta managers in our collection.
401
- */
402
- public async pauseProcessing(...deltaMangers: DeltaManager[]) {
403
- const monitors = this.mapDeltaManagerMonitor(deltaMangers);
404
-
405
- // Pause the inbound and outbound delta queues.
406
- await this.pauseDeltaManagerQueues(monitors);
407
-
408
- this.isNormalProcessingPaused = true;
409
- }
410
-
411
- /**
412
- * Resumes the delta processing after a pauseProcessing calls by resuming the inbound and outbound queues of
413
- * the delta managers.
414
- *
415
- * @param deltaMangers - Array of delta managers whose processing to resume. If no delta manager is provided, it
416
- * resumes the processing of all the delta managers in our collection.
417
- */
418
- public resumeProcessing(...deltaMangers: DeltaManager[]) {
419
- const monitors = this.mapDeltaManagerMonitor(deltaMangers);
420
-
421
- // Resume the inbound and outbound delta queues.
422
- monitors.forEach((monitor) => monitor.toggleResumeAll());
423
-
424
- this.isNormalProcessingPaused = false;
425
- }
426
-
427
- /**
428
- * Map a list of DeltaManager to its monitor. Throw an error if the delta manager is not in our collection
429
- * @param deltaMangers - The delta managers to get the monitors for
430
- */
431
- private mapDeltaManagerMonitor(deltaMangers: DeltaManager[]) {
432
- if (deltaMangers.length === 0) {
433
- // If no delta managers are provided, process all delta managers in our collection.
434
- return Array.from(this.deltaManagerMonitors.values());
435
- }
436
-
437
- return deltaMangers.map((deltaManager) => {
438
- const monitor = this.deltaManagerMonitors.get(deltaManager);
439
- assert(monitor, "All delta managers must be added to deterministically control processing");
440
- return monitor;
441
- });
442
- }
443
-
444
- /**
445
- * It keeps yielding the JS event loop until all the ops have been processed by the server and by the passed
446
- * delta managers.
447
- * @param monitors - The delta managers should ops have to be processed.
448
- * @param hasWork - Function that tells if the delta manager has pending work or not.
449
- */
450
- private async yieldWhileDeltaManagersHaveWork(
451
- monitors: Iterable<DeltaManagerMonitor>,
452
- hasWork: (deltaManagers: DeltaManager) => boolean,
453
- ): Promise<void> {
454
- let working: boolean;
455
- do {
456
- await OpProcessingController.yield();
457
- working = false;
458
- if (await this.deltaConnectionServerMonitor?.hasPendingWork() === true) {
459
- working = true;
460
- } else {
461
- for (const monitor of monitors) {
462
- if (!monitor.deltaManager.disposed) {
463
- if (monitor.hasPendingWork() || hasWork(monitor.deltaManager)) {
464
- working = true;
465
- break;
466
- }
467
-
468
- for (const outBoundMonitor of monitors) {
469
- if (monitor !== outBoundMonitor) {
470
- if (monitor.expectingInboundFrom(outBoundMonitor)) {
471
- working = true;
472
- break;
473
- }
474
- }
475
- }
476
- if (working === true) {
477
- break;
478
- }
479
- }
480
- }
481
- }
482
- } while (working);
483
-
484
- // If deterministically controlling events, need to pause before continuing
485
- if (this.isNormalProcessingPaused) {
486
- await this.pauseDeltaManagerQueues(monitors);
487
- }
488
- }
489
-
490
- /**
491
- * Pauses the inbound and outbound queues of all the delta managers given
492
- * @param monitors - The delta managers should ops have to be processed.
493
- */
494
- private async pauseDeltaManagerQueues(monitors: Iterable<DeltaManagerToggle>) {
495
- const p: Promise<[void, void]>[] = [];
496
- for (const monitor of monitors) {
497
- p.push(monitor.togglePauseAll());
498
- }
499
- return Promise.all(p);
500
- }
501
-
502
- /**
503
- * Pauses the inbound and outbound queues of all the delta managers in our collection.
504
- */
505
- private async pauseAllDeltaManagerQueues() {
506
- return this.pauseDeltaManagerQueues(this.deltaManagerMonitors.values());
507
- }
508
- }