@fluidframework/merge-tree 2.31.0 → 2.32.0
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/CHANGELOG.md +4 -0
- package/dist/client.d.ts +7 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +153 -44
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/mergeTree.d.ts +17 -5
- package/dist/mergeTree.d.ts.map +1 -1
- package/dist/mergeTree.js +188 -79
- package/dist/mergeTree.js.map +1 -1
- package/dist/mergeTreeNodes.d.ts +16 -18
- package/dist/mergeTreeNodes.d.ts.map +1 -1
- package/dist/mergeTreeNodes.js +6 -0
- package/dist/mergeTreeNodes.js.map +1 -1
- package/dist/perspective.d.ts +9 -0
- package/dist/perspective.d.ts.map +1 -1
- package/dist/perspective.js +14 -1
- package/dist/perspective.js.map +1 -1
- package/dist/segmentInfos.d.ts +32 -4
- package/dist/segmentInfos.d.ts.map +1 -1
- package/dist/segmentInfos.js +3 -1
- package/dist/segmentInfos.js.map +1 -1
- package/dist/sortedSegmentSet.d.ts +1 -0
- package/dist/sortedSegmentSet.d.ts.map +1 -1
- package/dist/sortedSegmentSet.js +3 -0
- package/dist/sortedSegmentSet.js.map +1 -1
- package/dist/test/beastTest.spec.js +5 -5
- package/dist/test/beastTest.spec.js.map +1 -1
- package/dist/test/client.localReference.spec.js +3 -3
- package/dist/test/client.localReference.spec.js.map +1 -1
- package/dist/test/client.rollback.spec.js +17 -0
- package/dist/test/client.rollback.spec.js.map +1 -1
- package/dist/test/clientTestHelper.d.ts +100 -0
- package/dist/test/clientTestHelper.d.ts.map +1 -0
- package/dist/test/clientTestHelper.js +196 -0
- package/dist/test/clientTestHelper.js.map +1 -0
- package/dist/test/mergeTree.annotate.spec.js +12 -12
- package/dist/test/mergeTree.annotate.spec.js.map +1 -1
- package/dist/test/mergeTree.markRangeRemoved.deltaCallback.spec.js +1 -1
- package/dist/test/mergeTree.markRangeRemoved.deltaCallback.spec.js.map +1 -1
- package/dist/test/obliterate.concurrent.spec.js +93 -90
- package/dist/test/obliterate.concurrent.spec.js.map +1 -1
- package/dist/test/obliterate.deltaCallback.spec.js +121 -116
- package/dist/test/obliterate.deltaCallback.spec.js.map +1 -1
- package/dist/test/obliterate.rangeExpansion.spec.js +29 -79
- package/dist/test/obliterate.rangeExpansion.spec.js.map +1 -1
- package/dist/test/obliterate.reconnect.spec.js +235 -58
- package/dist/test/obliterate.reconnect.spec.js.map +1 -1
- package/dist/test/testClient.js +1 -1
- package/dist/test/testClient.js.map +1 -1
- package/dist/test/testUtils.d.ts +13 -0
- package/dist/test/testUtils.d.ts.map +1 -1
- package/dist/test/testUtils.js +22 -1
- package/dist/test/testUtils.js.map +1 -1
- package/lib/client.d.ts +7 -1
- package/lib/client.d.ts.map +1 -1
- package/lib/client.js +155 -46
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/mergeTree.d.ts +17 -5
- package/lib/mergeTree.d.ts.map +1 -1
- package/lib/mergeTree.js +192 -83
- package/lib/mergeTree.js.map +1 -1
- package/lib/mergeTreeNodes.d.ts +16 -18
- package/lib/mergeTreeNodes.d.ts.map +1 -1
- package/lib/mergeTreeNodes.js +7 -1
- package/lib/mergeTreeNodes.js.map +1 -1
- package/lib/perspective.d.ts +9 -0
- package/lib/perspective.d.ts.map +1 -1
- package/lib/perspective.js +12 -0
- package/lib/perspective.js.map +1 -1
- package/lib/segmentInfos.d.ts +32 -4
- package/lib/segmentInfos.d.ts.map +1 -1
- package/lib/segmentInfos.js +2 -1
- package/lib/segmentInfos.js.map +1 -1
- package/lib/sortedSegmentSet.d.ts +1 -0
- package/lib/sortedSegmentSet.d.ts.map +1 -1
- package/lib/sortedSegmentSet.js +3 -0
- package/lib/sortedSegmentSet.js.map +1 -1
- package/lib/test/beastTest.spec.js +5 -5
- package/lib/test/beastTest.spec.js.map +1 -1
- package/lib/test/client.localReference.spec.js +3 -3
- package/lib/test/client.localReference.spec.js.map +1 -1
- package/lib/test/client.rollback.spec.js +18 -1
- package/lib/test/client.rollback.spec.js.map +1 -1
- package/lib/test/clientTestHelper.d.ts +100 -0
- package/lib/test/clientTestHelper.d.ts.map +1 -0
- package/lib/test/clientTestHelper.js +192 -0
- package/lib/test/clientTestHelper.js.map +1 -0
- package/lib/test/mergeTree.annotate.spec.js +12 -12
- package/lib/test/mergeTree.annotate.spec.js.map +1 -1
- package/lib/test/mergeTree.markRangeRemoved.deltaCallback.spec.js +1 -1
- package/lib/test/mergeTree.markRangeRemoved.deltaCallback.spec.js.map +1 -1
- package/lib/test/obliterate.concurrent.spec.js +93 -90
- package/lib/test/obliterate.concurrent.spec.js.map +1 -1
- package/lib/test/obliterate.deltaCallback.spec.js +121 -116
- package/lib/test/obliterate.deltaCallback.spec.js.map +1 -1
- package/lib/test/obliterate.rangeExpansion.spec.js +1 -51
- package/lib/test/obliterate.rangeExpansion.spec.js.map +1 -1
- package/lib/test/obliterate.reconnect.spec.js +236 -59
- package/lib/test/obliterate.reconnect.spec.js.map +1 -1
- package/lib/test/testClient.js +1 -1
- package/lib/test/testClient.js.map +1 -1
- package/lib/test/testUtils.d.ts +13 -0
- package/lib/test/testUtils.d.ts.map +1 -1
- package/lib/test/testUtils.js +20 -0
- package/lib/test/testUtils.js.map +1 -1
- package/package.json +19 -18
- package/src/client.ts +286 -55
- package/src/index.ts +1 -1
- package/src/mergeTree.ts +265 -98
- package/src/mergeTreeNodes.ts +24 -18
- package/src/perspective.ts +21 -0
- package/src/segmentInfos.ts +48 -6
- package/src/sortedSegmentSet.ts +4 -0
- package/dist/test/partialSyncHelper.d.ts +0 -42
- package/dist/test/partialSyncHelper.d.ts.map +0 -1
- package/dist/test/partialSyncHelper.js +0 -96
- package/dist/test/partialSyncHelper.js.map +0 -1
- package/dist/test/reconnectHelper.d.ts +0 -50
- package/dist/test/reconnectHelper.d.ts.map +0 -1
- package/dist/test/reconnectHelper.js +0 -106
- package/dist/test/reconnectHelper.js.map +0 -1
- package/lib/test/partialSyncHelper.d.ts +0 -42
- package/lib/test/partialSyncHelper.d.ts.map +0 -1
- package/lib/test/partialSyncHelper.js +0 -92
- package/lib/test/partialSyncHelper.js.map +0 -1
- package/lib/test/reconnectHelper.d.ts +0 -50
- package/lib/test/reconnectHelper.d.ts.map +0 -1
- package/lib/test/reconnectHelper.js +0 -102
- package/lib/test/reconnectHelper.js.map +0 -1
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
|
|
3
|
+
* Licensed under the MIT License.
|
|
4
|
+
*/
|
|
5
|
+
import { ISequencedDocumentMessage } from "@fluidframework/driver-definitions/internal";
|
|
6
|
+
import type { IMergeTreeOptions, InteriorSequencePlace } from "../index.js";
|
|
7
|
+
import type { TestClient } from "./testClient.js";
|
|
8
|
+
import { TestClientLogger } from "./testClientLogger.js";
|
|
9
|
+
declare const ClientIds: readonly ["A", "B", "C", "D"];
|
|
10
|
+
type ClientName = (typeof ClientIds)[number];
|
|
11
|
+
/**
|
|
12
|
+
* Helper for authoring tests which perform operations on a number of clients.
|
|
13
|
+
* This class essentially serves the role of the server in that it maintains a sequencing order of ops as well as information
|
|
14
|
+
* about the latest op that each client has applied.
|
|
15
|
+
*
|
|
16
|
+
* When using this class, *do not* perform operations which may submit ops on clients directly: it does not currently wire up client events
|
|
17
|
+
* to the server. Instead, use methods on the helper and pass the client name they should apply to as the first argument.
|
|
18
|
+
*
|
|
19
|
+
* It is analogous to MockContainerRuntimeFactory in the test-runtime-utils package, though the APIs are not completely equivalent
|
|
20
|
+
* (see for example differences noted on disconnect and reconnect).
|
|
21
|
+
*
|
|
22
|
+
* This helper is also designed to support advancing only some clients to a given sequence number
|
|
23
|
+
* (not all clients must be synchronized at the same time).
|
|
24
|
+
*
|
|
25
|
+
* This allows testing sequences of operations where clients have varying refSeqs, rather than having all clients advance refSeq
|
|
26
|
+
* in lockstep.
|
|
27
|
+
*
|
|
28
|
+
* @remarks
|
|
29
|
+
* If we wired the server up to "delta" events on the client, it would be reasonable to rename this to `MockServer` and the API for tests
|
|
30
|
+
* would be a bit cleaner.
|
|
31
|
+
*/
|
|
32
|
+
export declare class ClientTestHelper {
|
|
33
|
+
clients: Record<ClientName, TestClient> & {
|
|
34
|
+
all: TestClient[];
|
|
35
|
+
};
|
|
36
|
+
idxFromName(name: ClientName): number;
|
|
37
|
+
logger: TestClientLogger;
|
|
38
|
+
ops: ISequencedDocumentMessage[];
|
|
39
|
+
clientToLastAppliedSeq: Map<"A" | "B" | "C" | "D", number>;
|
|
40
|
+
perClientOps: ISequencedDocumentMessage[][];
|
|
41
|
+
private readonly disconnectedClientOps;
|
|
42
|
+
private seq;
|
|
43
|
+
constructor(options?: IMergeTreeOptions);
|
|
44
|
+
private addMessage;
|
|
45
|
+
insertText(clientName: ClientName, pos: number, text: string): void;
|
|
46
|
+
removeRange(clientName: ClientName, start: number, end: number): void;
|
|
47
|
+
obliterateRange(clientName: ClientName, start: number | InteriorSequencePlace, end: number | InteriorSequencePlace): void;
|
|
48
|
+
advanceClientToSeq(clientName: ClientName, seq: number): void;
|
|
49
|
+
/**
|
|
50
|
+
* Sends all known ops to the procieded client ids.
|
|
51
|
+
*/
|
|
52
|
+
advanceClients(...clientNames: ClientName[]): void;
|
|
53
|
+
processAllOps(): void;
|
|
54
|
+
/**
|
|
55
|
+
* Causes the provided clients to "disconnect" by isolating their outbound queue from the op stream.
|
|
56
|
+
* These clients will still receive ops from this helper just like all other clients.
|
|
57
|
+
* When a client reconnects (see {@link reconnect}), the helper will rebase all of the client's pending ops (that they submitted since
|
|
58
|
+
* `disconnect` was called) and re-add them to the op stream.
|
|
59
|
+
*
|
|
60
|
+
* BEWARE: This is slightly different from what the mocks in test-runtime-utils do in two ways:
|
|
61
|
+
*
|
|
62
|
+
* 1. Those mocks actually wipe outstanding ops submitted by the disconnected clients from the op stream and rebases those as well.
|
|
63
|
+
* 2. Those mocks freeze the inbound queue of the disconnected clients as well as the outbound queue.
|
|
64
|
+
*
|
|
65
|
+
* E.g. (pseudocode; syntax is different for those mocks and these):
|
|
66
|
+
*
|
|
67
|
+
* ```typescript
|
|
68
|
+
* B.submitLocalOp1()
|
|
69
|
+
* B.submitLocalOp2()
|
|
70
|
+
* disconnect(B)
|
|
71
|
+
* A.submitLocalOp1()
|
|
72
|
+
* B.submitLocalOp3()
|
|
73
|
+
* processAllOps()
|
|
74
|
+
* ```
|
|
75
|
+
*
|
|
76
|
+
* the test-runtime-mocks will...
|
|
77
|
+
*
|
|
78
|
+
* - have none of B's local ops be receieved by A in the processAllOps() line
|
|
79
|
+
* - have B not receive A's local op 1
|
|
80
|
+
*
|
|
81
|
+
* whereas this helper will...
|
|
82
|
+
*
|
|
83
|
+
* - have B's local ops 1 and 2 be received by A, but not its third local op
|
|
84
|
+
* - have B receive acks for its first two local ops as well as A's op in the processAllOps() line
|
|
85
|
+
*
|
|
86
|
+
* This operation is a no-op if called multiple times on the same client with no intervening reconnects.
|
|
87
|
+
*/
|
|
88
|
+
disconnect(...clientNames: ClientName[]): void;
|
|
89
|
+
/**
|
|
90
|
+
* Reconnects clients which have been disconnected. No-ops on any client which is already connected.
|
|
91
|
+
* For disconnected clients, any pending ops they have will be resubmitted using the resubmit flow, see {@link disconnect} for details and an example.
|
|
92
|
+
*
|
|
93
|
+
* @remarks
|
|
94
|
+
* If reconnecting multiple clients, the order that their ops will be resubmitted in will match the order in the array
|
|
95
|
+
*/
|
|
96
|
+
reconnect(...clientNames: ClientName[]): void;
|
|
97
|
+
private isDisconnected;
|
|
98
|
+
}
|
|
99
|
+
export {};
|
|
100
|
+
//# sourceMappingURL=clientTestHelper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clientTestHelper.d.ts","sourceRoot":"","sources":["../../src/test/clientTestHelper.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,EAAE,yBAAyB,EAAE,MAAM,6CAA6C,CAAC;AAExF,OAAO,KAAK,EAAgB,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAG1F,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,gBAAgB,EAA+B,MAAM,uBAAuB,CAAC;AAEtF,QAAA,MAAM,SAAS,+BAAgC,CAAC;AAChD,KAAK,UAAU,GAAG,CAAC,OAAO,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC;AAY7C;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,gBAAgB;IAC5B,OAAO,EAAE,MAAM,CAAC,UAAU,EAAE,UAAU,CAAC,GAAG;QAAE,GAAG,EAAE,UAAU,EAAE,CAAA;KAAE,CAAC;IAEhE,WAAW,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM;IAIrC,MAAM,EAAE,gBAAgB,CAAC;IAEzB,GAAG,EAAE,yBAAyB,EAAE,CAAM;IACtC,sBAAsB,qCAAiC;IAEvD,YAAY,EAAE,yBAAyB,EAAE,EAAE,CAAC;IAC5C,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAGlC;IAEJ,OAAO,CAAC,GAAG,CAAa;gBAEL,OAAO,GAAE,iBAAsB;IAgBlD,OAAO,CAAC,UAAU;IAmBX,UAAU,CAAC,UAAU,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAQnE,WAAW,CAAC,UAAU,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI;IAKrE,eAAe,CACrB,UAAU,EAAE,UAAU,EACtB,KAAK,EAAE,MAAM,GAAG,qBAAqB,EACrC,GAAG,EAAE,MAAM,GAAG,qBAAqB,GACjC,IAAI;IAKA,kBAAkB,CAAC,UAAU,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI;IAwBpE;;OAEG;IACI,cAAc,CAAC,GAAG,WAAW,EAAE,UAAU,EAAE,GAAG,IAAI;IAOlD,aAAa,IAAI,IAAI;IAO5B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAiCG;IACI,UAAU,CAAC,GAAG,WAAW,EAAE,UAAU,EAAE,GAAG,IAAI;IAUrD;;;;;;OAMG;IACI,SAAS,CAAC,GAAG,WAAW,EAAE,UAAU,EAAE,GAAG,IAAI;IAepD,OAAO,CAAC,cAAc;CAGtB"}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
|
|
3
|
+
* Licensed under the MIT License.
|
|
4
|
+
*/
|
|
5
|
+
import { strict as assert } from "node:assert";
|
|
6
|
+
import { TestClientLogger, createClientsAtInitialState } from "./testClientLogger.js";
|
|
7
|
+
const ClientIds = ["A", "B", "C", "D"];
|
|
8
|
+
function isClientId(s) {
|
|
9
|
+
return ClientIds.includes(s);
|
|
10
|
+
}
|
|
11
|
+
function clientNameOf(client) {
|
|
12
|
+
const { longClientId } = client;
|
|
13
|
+
assert(isClientId(longClientId), "Client ID is not a valid client name");
|
|
14
|
+
return longClientId;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Helper for authoring tests which perform operations on a number of clients.
|
|
18
|
+
* This class essentially serves the role of the server in that it maintains a sequencing order of ops as well as information
|
|
19
|
+
* about the latest op that each client has applied.
|
|
20
|
+
*
|
|
21
|
+
* When using this class, *do not* perform operations which may submit ops on clients directly: it does not currently wire up client events
|
|
22
|
+
* to the server. Instead, use methods on the helper and pass the client name they should apply to as the first argument.
|
|
23
|
+
*
|
|
24
|
+
* It is analogous to MockContainerRuntimeFactory in the test-runtime-utils package, though the APIs are not completely equivalent
|
|
25
|
+
* (see for example differences noted on disconnect and reconnect).
|
|
26
|
+
*
|
|
27
|
+
* This helper is also designed to support advancing only some clients to a given sequence number
|
|
28
|
+
* (not all clients must be synchronized at the same time).
|
|
29
|
+
*
|
|
30
|
+
* This allows testing sequences of operations where clients have varying refSeqs, rather than having all clients advance refSeq
|
|
31
|
+
* in lockstep.
|
|
32
|
+
*
|
|
33
|
+
* @remarks
|
|
34
|
+
* If we wired the server up to "delta" events on the client, it would be reasonable to rename this to `MockServer` and the API for tests
|
|
35
|
+
* would be a bit cleaner.
|
|
36
|
+
*/
|
|
37
|
+
export class ClientTestHelper {
|
|
38
|
+
idxFromName(name) {
|
|
39
|
+
return (name.codePointAt(0) ?? 0) - ("A".codePointAt(0) ?? 0);
|
|
40
|
+
}
|
|
41
|
+
constructor(options = {}) {
|
|
42
|
+
this.ops = [];
|
|
43
|
+
this.clientToLastAppliedSeq = new Map();
|
|
44
|
+
this.disconnectedClientOps = new Map();
|
|
45
|
+
this.seq = 0;
|
|
46
|
+
this.clients = createClientsAtInitialState({
|
|
47
|
+
initialState: "",
|
|
48
|
+
options: {
|
|
49
|
+
mergeTreeEnableObliterate: true,
|
|
50
|
+
mergeTreeEnableObliterateReconnect: true,
|
|
51
|
+
...options,
|
|
52
|
+
},
|
|
53
|
+
}, ...ClientIds);
|
|
54
|
+
this.logger = new TestClientLogger(this.clients.all);
|
|
55
|
+
this.perClientOps = this.clients.all.map(() => []);
|
|
56
|
+
}
|
|
57
|
+
addMessage(client, op) {
|
|
58
|
+
const disconnectedQueue = this.disconnectedClientOps.get(clientNameOf(client));
|
|
59
|
+
if (disconnectedQueue === undefined) {
|
|
60
|
+
const message = client.makeOpMessage(op, ++this.seq);
|
|
61
|
+
this.ops.push(message);
|
|
62
|
+
// This implementation (specifically, that of applying ops / synchronizing clients) assumes messages
|
|
63
|
+
// are pushed sequentially starting with seq 1.
|
|
64
|
+
assert(message.sequenceNumber === this.ops.length, "Partial sync test helper invariant violated");
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
// Client is not currently connected.
|
|
68
|
+
const segmentGroup = client.peekPendingSegmentGroups();
|
|
69
|
+
assert(segmentGroup !== undefined, "Client should have a pending segment group");
|
|
70
|
+
disconnectedQueue.push({ op, segmentGroup });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
insertText(clientName, pos, text) {
|
|
74
|
+
const client = this.clients[clientName];
|
|
75
|
+
const insertOp = client.insertTextLocal(pos, text);
|
|
76
|
+
if (insertOp) {
|
|
77
|
+
this.addMessage(client, insertOp);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
removeRange(clientName, start, end) {
|
|
81
|
+
const client = this.clients[clientName];
|
|
82
|
+
this.addMessage(client, client.removeRangeLocal(start, end));
|
|
83
|
+
}
|
|
84
|
+
obliterateRange(clientName, start, end) {
|
|
85
|
+
const client = this.clients[clientName];
|
|
86
|
+
this.addMessage(client, client.obliterateRangeLocal(start, end));
|
|
87
|
+
}
|
|
88
|
+
advanceClientToSeq(clientName, seq) {
|
|
89
|
+
const client = this.clients[clientName];
|
|
90
|
+
const lastApplied = this.clientToLastAppliedSeq.get(clientName);
|
|
91
|
+
assert(seq > 0, "Can only advance clients to sequence numbers that exist");
|
|
92
|
+
assert(this.ops.length >= seq, "Cannot attempt to advance clients to sequence numbers that don't yet exist");
|
|
93
|
+
let startIndex;
|
|
94
|
+
if (lastApplied === undefined) {
|
|
95
|
+
startIndex = 0;
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
if (lastApplied >= seq) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
startIndex = lastApplied;
|
|
102
|
+
}
|
|
103
|
+
for (let i = startIndex; i < seq; i++) {
|
|
104
|
+
const nextMessage = this.ops[i];
|
|
105
|
+
client.applyMsg(nextMessage);
|
|
106
|
+
this.clientToLastAppliedSeq.set(clientName, nextMessage.sequenceNumber);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Sends all known ops to the procieded client ids.
|
|
111
|
+
*/
|
|
112
|
+
advanceClients(...clientNames) {
|
|
113
|
+
const latestSeq = this.ops[this.ops.length - 1].sequenceNumber;
|
|
114
|
+
for (const name of clientNames) {
|
|
115
|
+
this.advanceClientToSeq(name, latestSeq);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
processAllOps() {
|
|
119
|
+
const latestSeq = this.ops[this.ops.length - 1].sequenceNumber;
|
|
120
|
+
for (const name of ClientIds) {
|
|
121
|
+
this.advanceClientToSeq(name, latestSeq);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Causes the provided clients to "disconnect" by isolating their outbound queue from the op stream.
|
|
126
|
+
* These clients will still receive ops from this helper just like all other clients.
|
|
127
|
+
* When a client reconnects (see {@link reconnect}), the helper will rebase all of the client's pending ops (that they submitted since
|
|
128
|
+
* `disconnect` was called) and re-add them to the op stream.
|
|
129
|
+
*
|
|
130
|
+
* BEWARE: This is slightly different from what the mocks in test-runtime-utils do in two ways:
|
|
131
|
+
*
|
|
132
|
+
* 1. Those mocks actually wipe outstanding ops submitted by the disconnected clients from the op stream and rebases those as well.
|
|
133
|
+
* 2. Those mocks freeze the inbound queue of the disconnected clients as well as the outbound queue.
|
|
134
|
+
*
|
|
135
|
+
* E.g. (pseudocode; syntax is different for those mocks and these):
|
|
136
|
+
*
|
|
137
|
+
* ```typescript
|
|
138
|
+
* B.submitLocalOp1()
|
|
139
|
+
* B.submitLocalOp2()
|
|
140
|
+
* disconnect(B)
|
|
141
|
+
* A.submitLocalOp1()
|
|
142
|
+
* B.submitLocalOp3()
|
|
143
|
+
* processAllOps()
|
|
144
|
+
* ```
|
|
145
|
+
*
|
|
146
|
+
* the test-runtime-mocks will...
|
|
147
|
+
*
|
|
148
|
+
* - have none of B's local ops be receieved by A in the processAllOps() line
|
|
149
|
+
* - have B not receive A's local op 1
|
|
150
|
+
*
|
|
151
|
+
* whereas this helper will...
|
|
152
|
+
*
|
|
153
|
+
* - have B's local ops 1 and 2 be received by A, but not its third local op
|
|
154
|
+
* - have B receive acks for its first two local ops as well as A's op in the processAllOps() line
|
|
155
|
+
*
|
|
156
|
+
* This operation is a no-op if called multiple times on the same client with no intervening reconnects.
|
|
157
|
+
*/
|
|
158
|
+
disconnect(...clientNames) {
|
|
159
|
+
for (const clientName of clientNames) {
|
|
160
|
+
const client = this.clients[clientName];
|
|
161
|
+
if (this.isDisconnected(client)) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
this.disconnectedClientOps.set(clientName, []);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Reconnects clients which have been disconnected. No-ops on any client which is already connected.
|
|
169
|
+
* For disconnected clients, any pending ops they have will be resubmitted using the resubmit flow, see {@link disconnect} for details and an example.
|
|
170
|
+
*
|
|
171
|
+
* @remarks
|
|
172
|
+
* If reconnecting multiple clients, the order that their ops will be resubmitted in will match the order in the array
|
|
173
|
+
*/
|
|
174
|
+
reconnect(...clientNames) {
|
|
175
|
+
for (const clientName of clientNames) {
|
|
176
|
+
const submittedOps = this.disconnectedClientOps.get(clientName);
|
|
177
|
+
if (submittedOps === undefined) {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
this.disconnectedClientOps.delete(clientName);
|
|
181
|
+
const client = this.clients[clientName];
|
|
182
|
+
for (const { op, segmentGroup } of submittedOps) {
|
|
183
|
+
const rebasedOp = client.regeneratePendingOp(op, segmentGroup);
|
|
184
|
+
this.addMessage(client, rebasedOp);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
isDisconnected(client) {
|
|
189
|
+
return this.disconnectedClientOps.has(clientNameOf(client));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
//# sourceMappingURL=clientTestHelper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clientTestHelper.js","sourceRoot":"","sources":["../../src/test/clientTestHelper.ts"],"names":[],"mappings":"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","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n */\n\nimport { strict as assert } from \"node:assert\";\n\nimport { ISequencedDocumentMessage } from \"@fluidframework/driver-definitions/internal\";\n\nimport type { IMergeTreeOp, IMergeTreeOptions, InteriorSequencePlace } from \"../index.js\";\nimport type { SegmentGroup } from \"../mergeTreeNodes.js\";\n\nimport type { TestClient } from \"./testClient.js\";\nimport { TestClientLogger, createClientsAtInitialState } from \"./testClientLogger.js\";\n\nconst ClientIds = [\"A\", \"B\", \"C\", \"D\"] as const;\ntype ClientName = (typeof ClientIds)[number];\n\nfunction isClientId(s: unknown): s is ClientName {\n\treturn ClientIds.includes(s as ClientName);\n}\n\nfunction clientNameOf(client: TestClient): ClientName {\n\tconst { longClientId } = client;\n\tassert(isClientId(longClientId), \"Client ID is not a valid client name\");\n\treturn longClientId;\n}\n\n/**\n * Helper for authoring tests which perform operations on a number of clients.\n * This class essentially serves the role of the server in that it maintains a sequencing order of ops as well as information\n * about the latest op that each client has applied.\n *\n * When using this class, *do not* perform operations which may submit ops on clients directly: it does not currently wire up client events\n * to the server. Instead, use methods on the helper and pass the client name they should apply to as the first argument.\n *\n * It is analogous to MockContainerRuntimeFactory in the test-runtime-utils package, though the APIs are not completely equivalent\n * (see for example differences noted on disconnect and reconnect).\n *\n * This helper is also designed to support advancing only some clients to a given sequence number\n * (not all clients must be synchronized at the same time).\n *\n * This allows testing sequences of operations where clients have varying refSeqs, rather than having all clients advance refSeq\n * in lockstep.\n *\n * @remarks\n * If we wired the server up to \"delta\" events on the client, it would be reasonable to rename this to `MockServer` and the API for tests\n * would be a bit cleaner.\n */\nexport class ClientTestHelper {\n\tclients: Record<ClientName, TestClient> & { all: TestClient[] };\n\n\tidxFromName(name: ClientName): number {\n\t\treturn (name.codePointAt(0) ?? 0) - (\"A\".codePointAt(0) ?? 0);\n\t}\n\n\tlogger: TestClientLogger;\n\n\tops: ISequencedDocumentMessage[] = [];\n\tclientToLastAppliedSeq = new Map<ClientName, number>();\n\n\tperClientOps: ISequencedDocumentMessage[][];\n\tprivate readonly disconnectedClientOps = new Map<\n\t\tClientName,\n\t\t{ op: IMergeTreeOp; segmentGroup: SegmentGroup }[]\n\t>();\n\n\tprivate seq: number = 0;\n\n\tpublic constructor(options: IMergeTreeOptions = {}) {\n\t\tthis.clients = createClientsAtInitialState(\n\t\t\t{\n\t\t\t\tinitialState: \"\",\n\t\t\t\toptions: {\n\t\t\t\t\tmergeTreeEnableObliterate: true,\n\t\t\t\t\tmergeTreeEnableObliterateReconnect: true,\n\t\t\t\t\t...options,\n\t\t\t\t},\n\t\t\t},\n\t\t\t...ClientIds,\n\t\t);\n\t\tthis.logger = new TestClientLogger(this.clients.all);\n\t\tthis.perClientOps = this.clients.all.map(() => []);\n\t}\n\n\tprivate addMessage(client: TestClient, op: IMergeTreeOp): void {\n\t\tconst disconnectedQueue = this.disconnectedClientOps.get(clientNameOf(client));\n\t\tif (disconnectedQueue === undefined) {\n\t\t\tconst message = client.makeOpMessage(op, ++this.seq);\n\t\t\tthis.ops.push(message);\n\t\t\t// This implementation (specifically, that of applying ops / synchronizing clients) assumes messages\n\t\t\t// are pushed sequentially starting with seq 1.\n\t\t\tassert(\n\t\t\t\tmessage.sequenceNumber === this.ops.length,\n\t\t\t\t\"Partial sync test helper invariant violated\",\n\t\t\t);\n\t\t} else {\n\t\t\t// Client is not currently connected.\n\t\t\tconst segmentGroup = client.peekPendingSegmentGroups();\n\t\t\tassert(segmentGroup !== undefined, \"Client should have a pending segment group\");\n\t\t\tdisconnectedQueue.push({ op, segmentGroup });\n\t\t}\n\t}\n\n\tpublic insertText(clientName: ClientName, pos: number, text: string): void {\n\t\tconst client = this.clients[clientName];\n\t\tconst insertOp = client.insertTextLocal(pos, text);\n\t\tif (insertOp) {\n\t\t\tthis.addMessage(client, insertOp);\n\t\t}\n\t}\n\n\tpublic removeRange(clientName: ClientName, start: number, end: number): void {\n\t\tconst client = this.clients[clientName];\n\t\tthis.addMessage(client, client.removeRangeLocal(start, end));\n\t}\n\n\tpublic obliterateRange(\n\t\tclientName: ClientName,\n\t\tstart: number | InteriorSequencePlace,\n\t\tend: number | InteriorSequencePlace,\n\t): void {\n\t\tconst client = this.clients[clientName];\n\t\tthis.addMessage(client, client.obliterateRangeLocal(start, end));\n\t}\n\n\tpublic advanceClientToSeq(clientName: ClientName, seq: number): void {\n\t\tconst client = this.clients[clientName];\n\t\tconst lastApplied = this.clientToLastAppliedSeq.get(clientName);\n\t\tassert(seq > 0, \"Can only advance clients to sequence numbers that exist\");\n\t\tassert(\n\t\t\tthis.ops.length >= seq,\n\t\t\t\"Cannot attempt to advance clients to sequence numbers that don't yet exist\",\n\t\t);\n\t\tlet startIndex: number;\n\t\tif (lastApplied === undefined) {\n\t\t\tstartIndex = 0;\n\t\t} else {\n\t\t\tif (lastApplied >= seq) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tstartIndex = lastApplied;\n\t\t}\n\t\tfor (let i = startIndex; i < seq; i++) {\n\t\t\tconst nextMessage = this.ops[i];\n\t\t\tclient.applyMsg(nextMessage);\n\t\t\tthis.clientToLastAppliedSeq.set(clientName, nextMessage.sequenceNumber);\n\t\t}\n\t}\n\n\t/**\n\t * Sends all known ops to the procieded client ids.\n\t */\n\tpublic advanceClients(...clientNames: ClientName[]): void {\n\t\tconst latestSeq = this.ops[this.ops.length - 1].sequenceNumber;\n\t\tfor (const name of clientNames) {\n\t\t\tthis.advanceClientToSeq(name, latestSeq);\n\t\t}\n\t}\n\n\tpublic processAllOps(): void {\n\t\tconst latestSeq = this.ops[this.ops.length - 1].sequenceNumber;\n\t\tfor (const name of ClientIds) {\n\t\t\tthis.advanceClientToSeq(name, latestSeq);\n\t\t}\n\t}\n\n\t/**\n\t * Causes the provided clients to \"disconnect\" by isolating their outbound queue from the op stream.\n\t * These clients will still receive ops from this helper just like all other clients.\n\t * When a client reconnects (see {@link reconnect}), the helper will rebase all of the client's pending ops (that they submitted since\n\t * `disconnect` was called) and re-add them to the op stream.\n\t *\n\t * BEWARE: This is slightly different from what the mocks in test-runtime-utils do in two ways:\n\t *\n\t * 1. Those mocks actually wipe outstanding ops submitted by the disconnected clients from the op stream and rebases those as well.\n\t * 2. Those mocks freeze the inbound queue of the disconnected clients as well as the outbound queue.\n\t *\n\t * E.g. (pseudocode; syntax is different for those mocks and these):\n\t *\n\t * ```typescript\n\t * B.submitLocalOp1()\n\t * B.submitLocalOp2()\n\t * disconnect(B)\n\t * A.submitLocalOp1()\n\t * B.submitLocalOp3()\n\t * processAllOps()\n\t * ```\n\t *\n\t * the test-runtime-mocks will...\n\t *\n\t * - have none of B's local ops be receieved by A in the processAllOps() line\n\t * - have B not receive A's local op 1\n\t *\n\t * whereas this helper will...\n\t *\n\t * - have B's local ops 1 and 2 be received by A, but not its third local op\n\t * - have B receive acks for its first two local ops as well as A's op in the processAllOps() line\n\t *\n\t * This operation is a no-op if called multiple times on the same client with no intervening reconnects.\n\t */\n\tpublic disconnect(...clientNames: ClientName[]): void {\n\t\tfor (const clientName of clientNames) {\n\t\t\tconst client = this.clients[clientName];\n\t\t\tif (this.isDisconnected(client)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tthis.disconnectedClientOps.set(clientName, []);\n\t\t}\n\t}\n\n\t/**\n\t * Reconnects clients which have been disconnected. No-ops on any client which is already connected.\n\t * For disconnected clients, any pending ops they have will be resubmitted using the resubmit flow, see {@link disconnect} for details and an example.\n\t *\n\t * @remarks\n\t * If reconnecting multiple clients, the order that their ops will be resubmitted in will match the order in the array\n\t */\n\tpublic reconnect(...clientNames: ClientName[]): void {\n\t\tfor (const clientName of clientNames) {\n\t\t\tconst submittedOps = this.disconnectedClientOps.get(clientName);\n\t\t\tif (submittedOps === undefined) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tthis.disconnectedClientOps.delete(clientName);\n\t\t\tconst client = this.clients[clientName];\n\t\t\tfor (const { op, segmentGroup } of submittedOps) {\n\t\t\t\tconst rebasedOp = client.regeneratePendingOp(op, segmentGroup);\n\t\t\t\tthis.addMessage(client, rebasedOp);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate isDisconnected(client: TestClient): boolean {\n\t\treturn this.disconnectedClientOps.has(clientNameOf(client));\n\t}\n}\n"]}
|
|
@@ -111,7 +111,7 @@ describe("MergeTree", () => {
|
|
|
111
111
|
assert.equal(splitSegment.properties?.propertySource, "local");
|
|
112
112
|
assert.equal(splitSegment.properties?.secondChange, 1);
|
|
113
113
|
assert.equal(splitSegment.properties?.splitOnly, 1);
|
|
114
|
-
mergeTree.
|
|
114
|
+
mergeTree.ackOp({
|
|
115
115
|
op: {
|
|
116
116
|
pos1: annotateStart,
|
|
117
117
|
pos2: annotateEnd,
|
|
@@ -130,7 +130,7 @@ describe("MergeTree", () => {
|
|
|
130
130
|
assert.equal(splitSegment.properties?.propertySource, "local");
|
|
131
131
|
assert.equal(splitSegment.properties?.secondChange, 1);
|
|
132
132
|
assert.equal(splitSegment.properties?.splitOnly, 1);
|
|
133
|
-
mergeTree.
|
|
133
|
+
mergeTree.ackOp({
|
|
134
134
|
op: {
|
|
135
135
|
pos1: annotateStart,
|
|
136
136
|
pos2: annotateEnd,
|
|
@@ -149,7 +149,7 @@ describe("MergeTree", () => {
|
|
|
149
149
|
assert.equal(splitSegment.properties?.propertySource, "local");
|
|
150
150
|
assert.equal(splitSegment.properties?.secondChange, 1);
|
|
151
151
|
assert.equal(splitSegment.properties?.splitOnly, 1);
|
|
152
|
-
mergeTree.
|
|
152
|
+
mergeTree.ackOp({
|
|
153
153
|
op: {
|
|
154
154
|
pos1: splitPos,
|
|
155
155
|
pos2: annotateEnd,
|
|
@@ -180,7 +180,7 @@ describe("MergeTree", () => {
|
|
|
180
180
|
assert.equal(segment.properties?.remoteProperty, 1);
|
|
181
181
|
});
|
|
182
182
|
it("sequenced local", () => {
|
|
183
|
-
mergeTree.
|
|
183
|
+
mergeTree.ackOp({
|
|
184
184
|
op: {
|
|
185
185
|
pos1: annotateStart,
|
|
186
186
|
pos2: annotateEnd,
|
|
@@ -197,7 +197,7 @@ describe("MergeTree", () => {
|
|
|
197
197
|
assert.equal(segment.properties?.propertySource, "local");
|
|
198
198
|
});
|
|
199
199
|
it("sequenced local before remote", () => {
|
|
200
|
-
mergeTree.
|
|
200
|
+
mergeTree.ackOp({
|
|
201
201
|
op: {
|
|
202
202
|
pos1: annotateStart,
|
|
203
203
|
pos2: annotateEnd,
|
|
@@ -236,7 +236,7 @@ describe("MergeTree", () => {
|
|
|
236
236
|
assert.equal(segment.properties?.propertySource, "local2");
|
|
237
237
|
assert.equal(segment.properties?.secondSource, 1);
|
|
238
238
|
assert.equal(segment.properties?.thirdSource, 1);
|
|
239
|
-
mergeTree.
|
|
239
|
+
mergeTree.ackOp({
|
|
240
240
|
op: {
|
|
241
241
|
pos1: annotateStart,
|
|
242
242
|
pos2: annotateEnd,
|
|
@@ -250,7 +250,7 @@ describe("MergeTree", () => {
|
|
|
250
250
|
assert.equal(segment.properties?.propertySource, "local2");
|
|
251
251
|
assert.equal(segment.properties?.secondSource, 1);
|
|
252
252
|
assert.equal(segment.properties?.thirdSource, 1);
|
|
253
|
-
mergeTree.
|
|
253
|
+
mergeTree.ackOp({
|
|
254
254
|
op: {
|
|
255
255
|
pos1: annotateStart,
|
|
256
256
|
pos2: annotateEnd,
|
|
@@ -264,7 +264,7 @@ describe("MergeTree", () => {
|
|
|
264
264
|
assert.equal(segment.properties?.propertySource, "local2");
|
|
265
265
|
assert.equal(segment.properties?.secondSource, 1);
|
|
266
266
|
assert.equal(segment.properties?.thirdSource, 1);
|
|
267
|
-
mergeTree.
|
|
267
|
+
mergeTree.ackOp({
|
|
268
268
|
op: {
|
|
269
269
|
pos1: annotateStart,
|
|
270
270
|
pos2: annotateEnd,
|
|
@@ -283,7 +283,7 @@ describe("MergeTree", () => {
|
|
|
283
283
|
mergeTree.annotateRange(annotateStart, annotateEnd, {
|
|
284
284
|
props: { secondSource: "local2" },
|
|
285
285
|
}, mergeTree.localPerspective, mergeTree.collabWindow.mintNextLocalOperationStamp(), undefined);
|
|
286
|
-
mergeTree.
|
|
286
|
+
mergeTree.ackOp({
|
|
287
287
|
op: {
|
|
288
288
|
pos1: annotateStart,
|
|
289
289
|
pos2: annotateEnd,
|
|
@@ -344,7 +344,7 @@ describe("MergeTree", () => {
|
|
|
344
344
|
assert(segmentInfo.segment?.segmentGroups?.empty !== false);
|
|
345
345
|
mergeTree.annotateRange(annotateStart, annotateEnd, props, mergeTree.localPerspective, mergeTree.collabWindow.mintNextLocalOperationStamp(), undefined);
|
|
346
346
|
assert.equal(segmentInfo.segment?.segmentGroups?.size, 1);
|
|
347
|
-
mergeTree.
|
|
347
|
+
mergeTree.ackOp({
|
|
348
348
|
op: {
|
|
349
349
|
pos1: annotateStart,
|
|
350
350
|
pos2: annotateEnd,
|
|
@@ -387,7 +387,7 @@ describe("MergeTree", () => {
|
|
|
387
387
|
assert.equal(segment.properties?.remoteProperty, 1);
|
|
388
388
|
});
|
|
389
389
|
it("sequenced local before remote", () => {
|
|
390
|
-
mergeTree.
|
|
390
|
+
mergeTree.ackOp({
|
|
391
391
|
op: {
|
|
392
392
|
pos1: annotateStart,
|
|
393
393
|
pos2: annotateEnd,
|
|
@@ -411,7 +411,7 @@ describe("MergeTree", () => {
|
|
|
411
411
|
mergeTree.annotateRange(annotateStart, annotateEnd, {
|
|
412
412
|
props: { secondSource: "local2" },
|
|
413
413
|
}, mergeTree.localPerspective, mergeTree.collabWindow.mintNextLocalOperationStamp(), undefined);
|
|
414
|
-
mergeTree.
|
|
414
|
+
mergeTree.ackOp({
|
|
415
415
|
op: {
|
|
416
416
|
pos1: annotateStart,
|
|
417
417
|
pos2: annotateEnd,
|