@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,196 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*!
|
|
3
|
+
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
|
|
4
|
+
* Licensed under the MIT License.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.ClientTestHelper = void 0;
|
|
8
|
+
const node_assert_1 = require("node:assert");
|
|
9
|
+
const testClientLogger_js_1 = require("./testClientLogger.js");
|
|
10
|
+
const ClientIds = ["A", "B", "C", "D"];
|
|
11
|
+
function isClientId(s) {
|
|
12
|
+
return ClientIds.includes(s);
|
|
13
|
+
}
|
|
14
|
+
function clientNameOf(client) {
|
|
15
|
+
const { longClientId } = client;
|
|
16
|
+
(0, node_assert_1.strict)(isClientId(longClientId), "Client ID is not a valid client name");
|
|
17
|
+
return longClientId;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Helper for authoring tests which perform operations on a number of clients.
|
|
21
|
+
* This class essentially serves the role of the server in that it maintains a sequencing order of ops as well as information
|
|
22
|
+
* about the latest op that each client has applied.
|
|
23
|
+
*
|
|
24
|
+
* When using this class, *do not* perform operations which may submit ops on clients directly: it does not currently wire up client events
|
|
25
|
+
* to the server. Instead, use methods on the helper and pass the client name they should apply to as the first argument.
|
|
26
|
+
*
|
|
27
|
+
* It is analogous to MockContainerRuntimeFactory in the test-runtime-utils package, though the APIs are not completely equivalent
|
|
28
|
+
* (see for example differences noted on disconnect and reconnect).
|
|
29
|
+
*
|
|
30
|
+
* This helper is also designed to support advancing only some clients to a given sequence number
|
|
31
|
+
* (not all clients must be synchronized at the same time).
|
|
32
|
+
*
|
|
33
|
+
* This allows testing sequences of operations where clients have varying refSeqs, rather than having all clients advance refSeq
|
|
34
|
+
* in lockstep.
|
|
35
|
+
*
|
|
36
|
+
* @remarks
|
|
37
|
+
* 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
|
|
38
|
+
* would be a bit cleaner.
|
|
39
|
+
*/
|
|
40
|
+
class ClientTestHelper {
|
|
41
|
+
idxFromName(name) {
|
|
42
|
+
return (name.codePointAt(0) ?? 0) - ("A".codePointAt(0) ?? 0);
|
|
43
|
+
}
|
|
44
|
+
constructor(options = {}) {
|
|
45
|
+
this.ops = [];
|
|
46
|
+
this.clientToLastAppliedSeq = new Map();
|
|
47
|
+
this.disconnectedClientOps = new Map();
|
|
48
|
+
this.seq = 0;
|
|
49
|
+
this.clients = (0, testClientLogger_js_1.createClientsAtInitialState)({
|
|
50
|
+
initialState: "",
|
|
51
|
+
options: {
|
|
52
|
+
mergeTreeEnableObliterate: true,
|
|
53
|
+
mergeTreeEnableObliterateReconnect: true,
|
|
54
|
+
...options,
|
|
55
|
+
},
|
|
56
|
+
}, ...ClientIds);
|
|
57
|
+
this.logger = new testClientLogger_js_1.TestClientLogger(this.clients.all);
|
|
58
|
+
this.perClientOps = this.clients.all.map(() => []);
|
|
59
|
+
}
|
|
60
|
+
addMessage(client, op) {
|
|
61
|
+
const disconnectedQueue = this.disconnectedClientOps.get(clientNameOf(client));
|
|
62
|
+
if (disconnectedQueue === undefined) {
|
|
63
|
+
const message = client.makeOpMessage(op, ++this.seq);
|
|
64
|
+
this.ops.push(message);
|
|
65
|
+
// This implementation (specifically, that of applying ops / synchronizing clients) assumes messages
|
|
66
|
+
// are pushed sequentially starting with seq 1.
|
|
67
|
+
(0, node_assert_1.strict)(message.sequenceNumber === this.ops.length, "Partial sync test helper invariant violated");
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
// Client is not currently connected.
|
|
71
|
+
const segmentGroup = client.peekPendingSegmentGroups();
|
|
72
|
+
(0, node_assert_1.strict)(segmentGroup !== undefined, "Client should have a pending segment group");
|
|
73
|
+
disconnectedQueue.push({ op, segmentGroup });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
insertText(clientName, pos, text) {
|
|
77
|
+
const client = this.clients[clientName];
|
|
78
|
+
const insertOp = client.insertTextLocal(pos, text);
|
|
79
|
+
if (insertOp) {
|
|
80
|
+
this.addMessage(client, insertOp);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
removeRange(clientName, start, end) {
|
|
84
|
+
const client = this.clients[clientName];
|
|
85
|
+
this.addMessage(client, client.removeRangeLocal(start, end));
|
|
86
|
+
}
|
|
87
|
+
obliterateRange(clientName, start, end) {
|
|
88
|
+
const client = this.clients[clientName];
|
|
89
|
+
this.addMessage(client, client.obliterateRangeLocal(start, end));
|
|
90
|
+
}
|
|
91
|
+
advanceClientToSeq(clientName, seq) {
|
|
92
|
+
const client = this.clients[clientName];
|
|
93
|
+
const lastApplied = this.clientToLastAppliedSeq.get(clientName);
|
|
94
|
+
(0, node_assert_1.strict)(seq > 0, "Can only advance clients to sequence numbers that exist");
|
|
95
|
+
(0, node_assert_1.strict)(this.ops.length >= seq, "Cannot attempt to advance clients to sequence numbers that don't yet exist");
|
|
96
|
+
let startIndex;
|
|
97
|
+
if (lastApplied === undefined) {
|
|
98
|
+
startIndex = 0;
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
if (lastApplied >= seq) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
startIndex = lastApplied;
|
|
105
|
+
}
|
|
106
|
+
for (let i = startIndex; i < seq; i++) {
|
|
107
|
+
const nextMessage = this.ops[i];
|
|
108
|
+
client.applyMsg(nextMessage);
|
|
109
|
+
this.clientToLastAppliedSeq.set(clientName, nextMessage.sequenceNumber);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Sends all known ops to the procieded client ids.
|
|
114
|
+
*/
|
|
115
|
+
advanceClients(...clientNames) {
|
|
116
|
+
const latestSeq = this.ops[this.ops.length - 1].sequenceNumber;
|
|
117
|
+
for (const name of clientNames) {
|
|
118
|
+
this.advanceClientToSeq(name, latestSeq);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
processAllOps() {
|
|
122
|
+
const latestSeq = this.ops[this.ops.length - 1].sequenceNumber;
|
|
123
|
+
for (const name of ClientIds) {
|
|
124
|
+
this.advanceClientToSeq(name, latestSeq);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Causes the provided clients to "disconnect" by isolating their outbound queue from the op stream.
|
|
129
|
+
* These clients will still receive ops from this helper just like all other clients.
|
|
130
|
+
* When a client reconnects (see {@link reconnect}), the helper will rebase all of the client's pending ops (that they submitted since
|
|
131
|
+
* `disconnect` was called) and re-add them to the op stream.
|
|
132
|
+
*
|
|
133
|
+
* BEWARE: This is slightly different from what the mocks in test-runtime-utils do in two ways:
|
|
134
|
+
*
|
|
135
|
+
* 1. Those mocks actually wipe outstanding ops submitted by the disconnected clients from the op stream and rebases those as well.
|
|
136
|
+
* 2. Those mocks freeze the inbound queue of the disconnected clients as well as the outbound queue.
|
|
137
|
+
*
|
|
138
|
+
* E.g. (pseudocode; syntax is different for those mocks and these):
|
|
139
|
+
*
|
|
140
|
+
* ```typescript
|
|
141
|
+
* B.submitLocalOp1()
|
|
142
|
+
* B.submitLocalOp2()
|
|
143
|
+
* disconnect(B)
|
|
144
|
+
* A.submitLocalOp1()
|
|
145
|
+
* B.submitLocalOp3()
|
|
146
|
+
* processAllOps()
|
|
147
|
+
* ```
|
|
148
|
+
*
|
|
149
|
+
* the test-runtime-mocks will...
|
|
150
|
+
*
|
|
151
|
+
* - have none of B's local ops be receieved by A in the processAllOps() line
|
|
152
|
+
* - have B not receive A's local op 1
|
|
153
|
+
*
|
|
154
|
+
* whereas this helper will...
|
|
155
|
+
*
|
|
156
|
+
* - have B's local ops 1 and 2 be received by A, but not its third local op
|
|
157
|
+
* - have B receive acks for its first two local ops as well as A's op in the processAllOps() line
|
|
158
|
+
*
|
|
159
|
+
* This operation is a no-op if called multiple times on the same client with no intervening reconnects.
|
|
160
|
+
*/
|
|
161
|
+
disconnect(...clientNames) {
|
|
162
|
+
for (const clientName of clientNames) {
|
|
163
|
+
const client = this.clients[clientName];
|
|
164
|
+
if (this.isDisconnected(client)) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
this.disconnectedClientOps.set(clientName, []);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Reconnects clients which have been disconnected. No-ops on any client which is already connected.
|
|
172
|
+
* For disconnected clients, any pending ops they have will be resubmitted using the resubmit flow, see {@link disconnect} for details and an example.
|
|
173
|
+
*
|
|
174
|
+
* @remarks
|
|
175
|
+
* If reconnecting multiple clients, the order that their ops will be resubmitted in will match the order in the array
|
|
176
|
+
*/
|
|
177
|
+
reconnect(...clientNames) {
|
|
178
|
+
for (const clientName of clientNames) {
|
|
179
|
+
const submittedOps = this.disconnectedClientOps.get(clientName);
|
|
180
|
+
if (submittedOps === undefined) {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
this.disconnectedClientOps.delete(clientName);
|
|
184
|
+
const client = this.clients[clientName];
|
|
185
|
+
for (const { op, segmentGroup } of submittedOps) {
|
|
186
|
+
const rebasedOp = client.regeneratePendingOp(op, segmentGroup);
|
|
187
|
+
this.addMessage(client, rebasedOp);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
isDisconnected(client) {
|
|
192
|
+
return this.disconnectedClientOps.has(clientNameOf(client));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
exports.ClientTestHelper = ClientTestHelper;
|
|
196
|
+
//# 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"]}
|
|
@@ -113,7 +113,7 @@ describe("MergeTree", () => {
|
|
|
113
113
|
node_assert_1.strict.equal(splitSegment.properties?.propertySource, "local");
|
|
114
114
|
node_assert_1.strict.equal(splitSegment.properties?.secondChange, 1);
|
|
115
115
|
node_assert_1.strict.equal(splitSegment.properties?.splitOnly, 1);
|
|
116
|
-
mergeTree.
|
|
116
|
+
mergeTree.ackOp({
|
|
117
117
|
op: {
|
|
118
118
|
pos1: annotateStart,
|
|
119
119
|
pos2: annotateEnd,
|
|
@@ -132,7 +132,7 @@ describe("MergeTree", () => {
|
|
|
132
132
|
node_assert_1.strict.equal(splitSegment.properties?.propertySource, "local");
|
|
133
133
|
node_assert_1.strict.equal(splitSegment.properties?.secondChange, 1);
|
|
134
134
|
node_assert_1.strict.equal(splitSegment.properties?.splitOnly, 1);
|
|
135
|
-
mergeTree.
|
|
135
|
+
mergeTree.ackOp({
|
|
136
136
|
op: {
|
|
137
137
|
pos1: annotateStart,
|
|
138
138
|
pos2: annotateEnd,
|
|
@@ -151,7 +151,7 @@ describe("MergeTree", () => {
|
|
|
151
151
|
node_assert_1.strict.equal(splitSegment.properties?.propertySource, "local");
|
|
152
152
|
node_assert_1.strict.equal(splitSegment.properties?.secondChange, 1);
|
|
153
153
|
node_assert_1.strict.equal(splitSegment.properties?.splitOnly, 1);
|
|
154
|
-
mergeTree.
|
|
154
|
+
mergeTree.ackOp({
|
|
155
155
|
op: {
|
|
156
156
|
pos1: splitPos,
|
|
157
157
|
pos2: annotateEnd,
|
|
@@ -182,7 +182,7 @@ describe("MergeTree", () => {
|
|
|
182
182
|
node_assert_1.strict.equal(segment.properties?.remoteProperty, 1);
|
|
183
183
|
});
|
|
184
184
|
it("sequenced local", () => {
|
|
185
|
-
mergeTree.
|
|
185
|
+
mergeTree.ackOp({
|
|
186
186
|
op: {
|
|
187
187
|
pos1: annotateStart,
|
|
188
188
|
pos2: annotateEnd,
|
|
@@ -199,7 +199,7 @@ describe("MergeTree", () => {
|
|
|
199
199
|
node_assert_1.strict.equal(segment.properties?.propertySource, "local");
|
|
200
200
|
});
|
|
201
201
|
it("sequenced local before remote", () => {
|
|
202
|
-
mergeTree.
|
|
202
|
+
mergeTree.ackOp({
|
|
203
203
|
op: {
|
|
204
204
|
pos1: annotateStart,
|
|
205
205
|
pos2: annotateEnd,
|
|
@@ -238,7 +238,7 @@ describe("MergeTree", () => {
|
|
|
238
238
|
node_assert_1.strict.equal(segment.properties?.propertySource, "local2");
|
|
239
239
|
node_assert_1.strict.equal(segment.properties?.secondSource, 1);
|
|
240
240
|
node_assert_1.strict.equal(segment.properties?.thirdSource, 1);
|
|
241
|
-
mergeTree.
|
|
241
|
+
mergeTree.ackOp({
|
|
242
242
|
op: {
|
|
243
243
|
pos1: annotateStart,
|
|
244
244
|
pos2: annotateEnd,
|
|
@@ -252,7 +252,7 @@ describe("MergeTree", () => {
|
|
|
252
252
|
node_assert_1.strict.equal(segment.properties?.propertySource, "local2");
|
|
253
253
|
node_assert_1.strict.equal(segment.properties?.secondSource, 1);
|
|
254
254
|
node_assert_1.strict.equal(segment.properties?.thirdSource, 1);
|
|
255
|
-
mergeTree.
|
|
255
|
+
mergeTree.ackOp({
|
|
256
256
|
op: {
|
|
257
257
|
pos1: annotateStart,
|
|
258
258
|
pos2: annotateEnd,
|
|
@@ -266,7 +266,7 @@ describe("MergeTree", () => {
|
|
|
266
266
|
node_assert_1.strict.equal(segment.properties?.propertySource, "local2");
|
|
267
267
|
node_assert_1.strict.equal(segment.properties?.secondSource, 1);
|
|
268
268
|
node_assert_1.strict.equal(segment.properties?.thirdSource, 1);
|
|
269
|
-
mergeTree.
|
|
269
|
+
mergeTree.ackOp({
|
|
270
270
|
op: {
|
|
271
271
|
pos1: annotateStart,
|
|
272
272
|
pos2: annotateEnd,
|
|
@@ -285,7 +285,7 @@ describe("MergeTree", () => {
|
|
|
285
285
|
mergeTree.annotateRange(annotateStart, annotateEnd, {
|
|
286
286
|
props: { secondSource: "local2" },
|
|
287
287
|
}, mergeTree.localPerspective, mergeTree.collabWindow.mintNextLocalOperationStamp(), undefined);
|
|
288
|
-
mergeTree.
|
|
288
|
+
mergeTree.ackOp({
|
|
289
289
|
op: {
|
|
290
290
|
pos1: annotateStart,
|
|
291
291
|
pos2: annotateEnd,
|
|
@@ -346,7 +346,7 @@ describe("MergeTree", () => {
|
|
|
346
346
|
(0, node_assert_1.strict)(segmentInfo.segment?.segmentGroups?.empty !== false);
|
|
347
347
|
mergeTree.annotateRange(annotateStart, annotateEnd, props, mergeTree.localPerspective, mergeTree.collabWindow.mintNextLocalOperationStamp(), undefined);
|
|
348
348
|
node_assert_1.strict.equal(segmentInfo.segment?.segmentGroups?.size, 1);
|
|
349
|
-
mergeTree.
|
|
349
|
+
mergeTree.ackOp({
|
|
350
350
|
op: {
|
|
351
351
|
pos1: annotateStart,
|
|
352
352
|
pos2: annotateEnd,
|
|
@@ -389,7 +389,7 @@ describe("MergeTree", () => {
|
|
|
389
389
|
node_assert_1.strict.equal(segment.properties?.remoteProperty, 1);
|
|
390
390
|
});
|
|
391
391
|
it("sequenced local before remote", () => {
|
|
392
|
-
mergeTree.
|
|
392
|
+
mergeTree.ackOp({
|
|
393
393
|
op: {
|
|
394
394
|
pos1: annotateStart,
|
|
395
395
|
pos2: annotateEnd,
|
|
@@ -413,7 +413,7 @@ describe("MergeTree", () => {
|
|
|
413
413
|
mergeTree.annotateRange(annotateStart, annotateEnd, {
|
|
414
414
|
props: { secondSource: "local2" },
|
|
415
415
|
}, mergeTree.localPerspective, mergeTree.collabWindow.mintNextLocalOperationStamp(), undefined);
|
|
416
|
-
mergeTree.
|
|
416
|
+
mergeTree.ackOp({
|
|
417
417
|
op: {
|
|
418
418
|
pos1: annotateStart,
|
|
419
419
|
pos2: annotateEnd,
|