@fluidframework/id-compressor 2.0.0-rc.1.0.3 → 2.0.0-rc.2.0.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/{.eslintrc.js → .eslintrc.cjs} +5 -5
- package/{.mocharc.js → .mocharc.cjs} +1 -1
- package/CHANGELOG.md +8 -0
- package/{api-extractor-esm.json → api-extractor-cjs.json} +5 -1
- package/api-extractor-lint.json +1 -1
- package/api-extractor.json +1 -1
- package/api-report/id-compressor.api.md +5 -42
- package/dist/finalSpace.d.ts +2 -2
- package/dist/finalSpace.d.ts.map +1 -1
- package/dist/finalSpace.js +4 -4
- package/dist/finalSpace.js.map +1 -1
- package/dist/id-compressor-alpha.d.ts +117 -112
- package/dist/id-compressor-beta.d.ts +84 -77
- package/dist/id-compressor-public.d.ts +84 -77
- package/dist/id-compressor-untrimmed.d.ts +117 -112
- package/dist/idCompressor.d.ts +15 -19
- package/dist/idCompressor.d.ts.map +1 -1
- package/dist/idCompressor.js +107 -94
- package/dist/idCompressor.js.map +1 -1
- package/dist/identifiers.d.ts +1 -1
- package/dist/identifiers.d.ts.map +1 -1
- package/dist/identifiers.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -10
- package/dist/index.js.map +1 -1
- package/dist/package.json +3 -0
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.js +1 -1
- package/dist/packageVersion.js.map +1 -1
- package/dist/persistanceUtilities.d.ts +1 -1
- package/dist/persistanceUtilities.d.ts.map +1 -1
- package/dist/persistanceUtilities.js.map +1 -1
- package/dist/sessionSpaceNormalizer.d.ts +2 -2
- package/dist/sessionSpaceNormalizer.d.ts.map +1 -1
- package/dist/sessionSpaceNormalizer.js +4 -4
- package/dist/sessionSpaceNormalizer.js.map +1 -1
- package/dist/sessions.d.ts +2 -2
- package/dist/sessions.d.ts.map +1 -1
- package/dist/sessions.js +12 -12
- package/dist/sessions.js.map +1 -1
- package/dist/test/appendOnlySortedMap.perf.spec.d.ts +6 -0
- package/dist/test/appendOnlySortedMap.perf.spec.d.ts.map +1 -0
- package/dist/test/appendOnlySortedMap.perf.spec.js.map +1 -0
- package/dist/test/appendOnlySortedMap.spec.d.ts +6 -0
- package/dist/test/appendOnlySortedMap.spec.d.ts.map +1 -0
- package/dist/test/appendOnlySortedMap.spec.js.map +1 -0
- package/dist/test/idCompressor.perf.spec.d.ts +6 -0
- package/dist/test/idCompressor.perf.spec.d.ts.map +1 -0
- package/dist/test/idCompressor.perf.spec.js.map +1 -0
- package/dist/test/idCompressor.spec.d.ts +6 -0
- package/dist/test/idCompressor.spec.d.ts.map +1 -0
- package/dist/test/idCompressor.spec.js.map +1 -0
- package/dist/test/idCompressorTestUtilities.d.ts +245 -0
- package/dist/test/idCompressorTestUtilities.d.ts.map +1 -0
- package/dist/test/idCompressorTestUtilities.js +589 -0
- package/dist/test/idCompressorTestUtilities.js.map +1 -0
- package/dist/test/index.d.ts +9 -0
- package/dist/test/index.d.ts.map +1 -0
- package/dist/test/index.js +13 -0
- package/dist/test/index.js.map +1 -0
- package/dist/test/numericUuid.spec.d.ts +6 -0
- package/dist/test/numericUuid.spec.d.ts.map +1 -0
- package/dist/test/numericUuid.spec.js.map +1 -0
- package/dist/test/sessionSpaceNormalizer.spec.d.ts +6 -0
- package/dist/test/sessionSpaceNormalizer.spec.d.ts.map +1 -0
- package/dist/test/sessionSpaceNormalizer.spec.js.map +1 -0
- package/dist/test/snapshots/dirname.cjs +16 -0
- package/dist/test/snapshots/dirname.cjs.map +1 -0
- package/dist/test/snapshots/dirname.d.cts +6 -0
- package/dist/test/snapshots/dirname.d.cts.map +1 -0
- package/{lib/types/idCompressor.mjs → dist/test/snapshots/summary.spec.d.ts} +1 -1
- package/dist/test/snapshots/summary.spec.d.ts.map +1 -0
- package/dist/test/snapshots/summary.spec.js.map +1 -0
- package/dist/test/testCommon.d.ts +90 -0
- package/dist/test/testCommon.d.ts.map +1 -0
- package/dist/test/testCommon.js +90 -0
- package/dist/test/testCommon.js.map +1 -0
- package/dist/tsdoc-metadata.json +1 -1
- package/dist/types/idCompressor.d.ts +134 -67
- package/dist/types/idCompressor.d.ts.map +1 -1
- package/dist/types/idCompressor.js.map +1 -1
- package/dist/types/index.d.ts +3 -3
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/persisted-types/0.0.1.d.ts +1 -1
- package/dist/types/persisted-types/0.0.1.d.ts.map +1 -1
- package/dist/types/persisted-types/0.0.1.js.map +1 -1
- package/dist/types/persisted-types/index.d.ts +1 -1
- package/dist/types/persisted-types/index.d.ts.map +1 -1
- package/dist/types/persisted-types/index.js.map +1 -1
- package/dist/utilities.d.ts +2 -2
- package/dist/utilities.d.ts.map +1 -1
- package/dist/utilities.js.map +1 -1
- package/lib/{appendOnlySortedMap.d.mts → appendOnlySortedMap.d.ts} +1 -1
- package/lib/appendOnlySortedMap.d.ts.map +1 -0
- package/lib/{appendOnlySortedMap.mjs → appendOnlySortedMap.js} +3 -1
- package/lib/appendOnlySortedMap.js.map +1 -0
- package/lib/{finalSpace.d.mts → finalSpace.d.ts} +3 -3
- package/lib/finalSpace.d.ts.map +1 -0
- package/lib/{finalSpace.mjs → finalSpace.js} +2 -2
- package/lib/finalSpace.js.map +1 -0
- package/lib/{id-compressor-alpha.d.mts → id-compressor-alpha.d.ts} +117 -112
- package/lib/id-compressor-beta.d.ts +179 -0
- package/lib/id-compressor-public.d.ts +179 -0
- package/lib/{id-compressor-untrimmed.d.mts → id-compressor-untrimmed.d.ts} +117 -112
- package/lib/{idCompressor.d.mts → idCompressor.d.ts} +16 -20
- package/lib/idCompressor.d.ts.map +1 -0
- package/lib/{idCompressor.mjs → idCompressor.js} +54 -41
- package/lib/idCompressor.js.map +1 -0
- package/lib/{identifiers.d.mts → identifiers.d.ts} +2 -2
- package/lib/identifiers.d.ts.map +1 -0
- package/lib/{identifiers.mjs → identifiers.js} +1 -1
- package/lib/identifiers.js.map +1 -0
- package/lib/{index.d.mts → index.d.ts} +7 -4
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +10 -0
- package/lib/index.js.map +1 -0
- package/lib/{packageVersion.d.mts → packageVersion.d.ts} +2 -2
- package/lib/packageVersion.d.ts.map +1 -0
- package/lib/{packageVersion.mjs → packageVersion.js} +2 -2
- package/lib/packageVersion.js.map +1 -0
- package/lib/{persistanceUtilities.d.mts → persistanceUtilities.d.ts} +2 -2
- package/lib/persistanceUtilities.d.ts.map +1 -0
- package/lib/{persistanceUtilities.mjs → persistanceUtilities.js} +1 -1
- package/lib/persistanceUtilities.js.map +1 -0
- package/lib/{sessionSpaceNormalizer.d.mts → sessionSpaceNormalizer.d.ts} +3 -3
- package/lib/sessionSpaceNormalizer.d.ts.map +1 -0
- package/lib/{sessionSpaceNormalizer.mjs → sessionSpaceNormalizer.js} +3 -3
- package/lib/sessionSpaceNormalizer.js.map +1 -0
- package/lib/{sessions.d.mts → sessions.d.ts} +3 -3
- package/lib/sessions.d.ts.map +1 -0
- package/lib/{sessions.mjs → sessions.js} +2 -2
- package/lib/sessions.js.map +1 -0
- package/lib/test/appendOnlySortedMap.perf.spec.d.ts +6 -0
- package/lib/test/appendOnlySortedMap.perf.spec.d.ts.map +1 -0
- package/lib/test/appendOnlySortedMap.perf.spec.js +49 -0
- package/lib/test/appendOnlySortedMap.perf.spec.js.map +1 -0
- package/lib/test/appendOnlySortedMap.spec.d.ts +6 -0
- package/lib/test/appendOnlySortedMap.spec.d.ts.map +1 -0
- package/lib/test/appendOnlySortedMap.spec.js +166 -0
- package/lib/test/appendOnlySortedMap.spec.js.map +1 -0
- package/lib/test/idCompressor.perf.spec.d.ts +6 -0
- package/lib/test/idCompressor.perf.spec.d.ts.map +1 -0
- package/lib/test/idCompressor.perf.spec.js +281 -0
- package/lib/test/idCompressor.perf.spec.js.map +1 -0
- package/lib/test/idCompressor.spec.d.ts +6 -0
- package/lib/test/idCompressor.spec.d.ts.map +1 -0
- package/lib/test/idCompressor.spec.js +953 -0
- package/lib/test/idCompressor.spec.js.map +1 -0
- package/lib/test/idCompressorTestUtilities.d.ts +245 -0
- package/lib/test/idCompressorTestUtilities.d.ts.map +1 -0
- package/lib/test/idCompressorTestUtilities.js +576 -0
- package/lib/test/idCompressorTestUtilities.js.map +1 -0
- package/lib/test/index.d.ts +9 -0
- package/lib/test/index.d.ts.map +1 -0
- package/lib/test/index.js +9 -0
- package/lib/test/index.js.map +1 -0
- package/lib/test/numericUuid.spec.d.ts +6 -0
- package/lib/test/numericUuid.spec.d.ts.map +1 -0
- package/lib/test/numericUuid.spec.js +74 -0
- package/lib/test/numericUuid.spec.js.map +1 -0
- package/lib/test/sessionSpaceNormalizer.spec.d.ts +6 -0
- package/lib/test/sessionSpaceNormalizer.spec.d.ts.map +1 -0
- package/lib/test/sessionSpaceNormalizer.spec.js +43 -0
- package/lib/test/sessionSpaceNormalizer.spec.js.map +1 -0
- package/lib/test/snapshots/dirname.cjs +16 -0
- package/lib/test/snapshots/dirname.cjs.map +1 -0
- package/lib/test/snapshots/dirname.d.cts +6 -0
- package/lib/test/snapshots/dirname.d.cts.map +1 -0
- package/lib/test/snapshots/summary.spec.d.ts +6 -0
- package/lib/test/snapshots/summary.spec.d.ts.map +1 -0
- package/lib/test/snapshots/summary.spec.js +127 -0
- package/lib/test/snapshots/summary.spec.js.map +1 -0
- package/lib/test/testCommon.d.ts +90 -0
- package/lib/test/testCommon.d.ts.map +1 -0
- package/lib/test/testCommon.js +80 -0
- package/lib/test/testCommon.js.map +1 -0
- package/lib/types/{idCompressor.d.mts → idCompressor.d.ts} +135 -68
- package/lib/types/idCompressor.d.ts.map +1 -0
- package/lib/types/{identifiers.mjs → idCompressor.js} +1 -1
- package/lib/types/idCompressor.js.map +1 -0
- package/lib/types/{identifiers.d.mts → identifiers.d.ts} +1 -1
- package/lib/types/identifiers.d.ts.map +1 -0
- package/lib/types/identifiers.js +6 -0
- package/lib/types/identifiers.js.map +1 -0
- package/lib/types/{index.d.mts → index.d.ts} +4 -4
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/{persisted-types/0.0.1.mjs → index.js} +1 -1
- package/lib/types/index.js.map +1 -0
- package/lib/types/persisted-types/{0.0.1.d.mts → 0.0.1.d.ts} +2 -2
- package/lib/types/persisted-types/0.0.1.d.ts.map +1 -0
- package/lib/types/persisted-types/{index.mjs → 0.0.1.js} +1 -1
- package/lib/types/persisted-types/0.0.1.js.map +1 -0
- package/lib/types/persisted-types/{index.d.mts → index.d.ts} +2 -2
- package/lib/types/persisted-types/index.d.ts.map +1 -0
- package/lib/types/{index.mjs → persisted-types/index.js} +1 -1
- package/lib/types/persisted-types/index.js.map +1 -0
- package/lib/{utilities.d.mts → utilities.d.ts} +3 -3
- package/lib/utilities.d.ts.map +1 -0
- package/lib/{utilities.mjs → utilities.js} +2 -1
- package/lib/utilities.js.map +1 -0
- package/package.json +87 -21
- package/src/finalSpace.ts +2 -2
- package/src/idCompressor.ts +75 -59
- package/src/identifiers.ts +1 -1
- package/src/index.ts +3 -3
- package/src/packageVersion.ts +1 -1
- package/src/persistanceUtilities.ts +1 -1
- package/src/sessionSpaceNormalizer.ts +3 -3
- package/src/sessions.ts +3 -3
- package/src/test/idCompressorTestUtilities.ts +921 -0
- package/src/test/index.ts +10 -0
- package/src/test/snapshots/dirname.cts +13 -0
- package/src/test/snapshots/files/compressor-with-finalized-range-from-one-client +1 -0
- package/src/test/snapshots/files/compressor-with-finalized-range-from-one-client-client2 +1 -0
- package/src/test/snapshots/files/compressors-with-finalized-ranges-from-two-clients +1 -0
- package/src/test/snapshots/files/compressors-with-finalized-ranges-from-two-clients-client2 +1 -0
- package/src/test/snapshots/files/empty-compressor-summary +1 -0
- package/src/test/snapshots/files/expansion-semantics +1 -0
- package/src/test/snapshots/files/expansion-semantics-client2 +1 -0
- package/src/test/testCommon.ts +143 -0
- package/src/test/tsconfig.cjs.json +12 -0
- package/src/test/tsconfig.json +20 -0
- package/src/types/idCompressor.ts +147 -72
- package/src/types/index.ts +8 -3
- package/src/types/persisted-types/0.0.1.ts +1 -1
- package/src/types/persisted-types/index.ts +1 -1
- package/src/utilities.ts +2 -2
- package/tsconfig.cjs.json +7 -0
- package/tsconfig.json +2 -5
- package/lib/appendOnlySortedMap.d.mts.map +0 -1
- package/lib/appendOnlySortedMap.mjs.map +0 -1
- package/lib/finalSpace.d.mts.map +0 -1
- package/lib/finalSpace.mjs.map +0 -1
- package/lib/id-compressor-beta.d.mts +0 -172
- package/lib/id-compressor-public.d.mts +0 -172
- package/lib/idCompressor.d.mts.map +0 -1
- package/lib/idCompressor.mjs.map +0 -1
- package/lib/identifiers.d.mts.map +0 -1
- package/lib/identifiers.mjs.map +0 -1
- package/lib/index.d.mts.map +0 -1
- package/lib/index.mjs +0 -7
- package/lib/index.mjs.map +0 -1
- package/lib/packageVersion.d.mts.map +0 -1
- package/lib/packageVersion.mjs.map +0 -1
- package/lib/persistanceUtilities.d.mts.map +0 -1
- package/lib/persistanceUtilities.mjs.map +0 -1
- package/lib/sessionSpaceNormalizer.d.mts.map +0 -1
- package/lib/sessionSpaceNormalizer.mjs.map +0 -1
- package/lib/sessions.d.mts.map +0 -1
- package/lib/sessions.mjs.map +0 -1
- package/lib/types/idCompressor.d.mts.map +0 -1
- package/lib/types/idCompressor.mjs.map +0 -1
- package/lib/types/identifiers.d.mts.map +0 -1
- package/lib/types/identifiers.mjs.map +0 -1
- package/lib/types/index.d.mts.map +0 -1
- package/lib/types/index.mjs.map +0 -1
- package/lib/types/persisted-types/0.0.1.d.mts.map +0 -1
- package/lib/types/persisted-types/0.0.1.mjs.map +0 -1
- package/lib/types/persisted-types/index.d.mts.map +0 -1
- package/lib/types/persisted-types/index.mjs.map +0 -1
- package/lib/utilities.d.mts.map +0 -1
- package/lib/utilities.mjs.map +0 -1
|
@@ -0,0 +1,921 @@
|
|
|
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 {
|
|
8
|
+
Generator,
|
|
9
|
+
createWeightedGenerator,
|
|
10
|
+
interleave,
|
|
11
|
+
makeRandom,
|
|
12
|
+
performFuzzActions as performFuzzActionsBase,
|
|
13
|
+
repeat,
|
|
14
|
+
SaveInfo,
|
|
15
|
+
take,
|
|
16
|
+
BaseFuzzTestState,
|
|
17
|
+
} from "@fluid-private/stochastic-test-utils";
|
|
18
|
+
import { ITelemetryBaseLogger } from "@fluidframework/core-interfaces";
|
|
19
|
+
import {
|
|
20
|
+
createIdCompressor,
|
|
21
|
+
IdCreationRange,
|
|
22
|
+
OpSpaceCompressedId,
|
|
23
|
+
SerializedIdCompressorWithNoSession,
|
|
24
|
+
SerializedIdCompressorWithOngoingSession,
|
|
25
|
+
SessionId,
|
|
26
|
+
SessionSpaceCompressedId,
|
|
27
|
+
StableId,
|
|
28
|
+
type IIdCompressor,
|
|
29
|
+
type IIdCompressorCore,
|
|
30
|
+
} from "..//index.js";
|
|
31
|
+
import { IdCompressor } from "../idCompressor.js";
|
|
32
|
+
import { assertIsSessionId, createSessionId } from "../utilities.js";
|
|
33
|
+
import {
|
|
34
|
+
FinalCompressedId,
|
|
35
|
+
getOrCreate,
|
|
36
|
+
incrementStableId,
|
|
37
|
+
isFinalId,
|
|
38
|
+
isLocalId,
|
|
39
|
+
ReadonlyIdCompressor,
|
|
40
|
+
fail,
|
|
41
|
+
} from "./testCommon.js";
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A readonly `Map` which is known to contain a value for every possible key
|
|
45
|
+
*/
|
|
46
|
+
export interface ClosedMap<K, V> extends Omit<Map<K, V>, "delete" | "clear"> {
|
|
47
|
+
get(key: K): V;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Identifies a compressor in a network */
|
|
51
|
+
export enum Client {
|
|
52
|
+
Client1 = "Client1",
|
|
53
|
+
Client2 = "Client2",
|
|
54
|
+
Client3 = "Client3",
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Identifies categories of compressors */
|
|
58
|
+
export enum MetaClient {
|
|
59
|
+
All = "All",
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Identifies a compressor inside the network but outside the three specially tracked clients. */
|
|
63
|
+
export enum OutsideClient {
|
|
64
|
+
Remote = "Remote",
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Used to attribute actions to clients in a distributed collaboration session.
|
|
69
|
+
* `Local` implies a local and unsequenced operation. All others imply sequenced operations.
|
|
70
|
+
*/
|
|
71
|
+
export type OriginatingClient = Client | OutsideClient;
|
|
72
|
+
export const OriginatingClient = { ...Client, ...OutsideClient };
|
|
73
|
+
|
|
74
|
+
/** Identifies a compressor to which to send an operation */
|
|
75
|
+
export type DestinationClient = Client | MetaClient;
|
|
76
|
+
export const DestinationClient = { ...Client, ...MetaClient };
|
|
77
|
+
|
|
78
|
+
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
|
|
79
|
+
export class CompressorFactory {
|
|
80
|
+
/**
|
|
81
|
+
* Creates a new compressor with the supplied cluster capacity.
|
|
82
|
+
*/
|
|
83
|
+
public static createCompressor(
|
|
84
|
+
client: Client,
|
|
85
|
+
clusterCapacity = 5,
|
|
86
|
+
logger?: ITelemetryBaseLogger,
|
|
87
|
+
): IdCompressor {
|
|
88
|
+
return CompressorFactory.createCompressorWithSession(
|
|
89
|
+
sessionIds.get(client),
|
|
90
|
+
clusterCapacity,
|
|
91
|
+
logger,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Creates a new compressor with the supplied cluster capacity.
|
|
97
|
+
*/
|
|
98
|
+
public static createCompressorWithSession(
|
|
99
|
+
sessionId: SessionId,
|
|
100
|
+
clusterCapacity = 5,
|
|
101
|
+
logger?: ITelemetryBaseLogger,
|
|
102
|
+
): IdCompressor {
|
|
103
|
+
const compressor = createIdCompressor(sessionId, logger) as IdCompressor;
|
|
104
|
+
// eslint-disable-next-line @typescript-eslint/dot-notation
|
|
105
|
+
compressor["nextRequestedClusterSize"] = clusterCapacity;
|
|
106
|
+
return compressor;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Utility for building a huge compressor.
|
|
112
|
+
* Build via the compressor factory.
|
|
113
|
+
*/
|
|
114
|
+
export function buildHugeCompressor(
|
|
115
|
+
numSessions = 10000,
|
|
116
|
+
capacity = 10,
|
|
117
|
+
numClustersPerSession = 3,
|
|
118
|
+
): IdCompressor {
|
|
119
|
+
const compressor = CompressorFactory.createCompressorWithSession(createSessionId(), capacity);
|
|
120
|
+
const sessions: SessionId[] = [];
|
|
121
|
+
for (let i = 0; i < numSessions; i++) {
|
|
122
|
+
sessions.push(createSessionId());
|
|
123
|
+
}
|
|
124
|
+
for (let i = 0; i < numSessions * numClustersPerSession; i++) {
|
|
125
|
+
const sessionId = sessions[i % numSessions];
|
|
126
|
+
if (Math.random() > 0.1) {
|
|
127
|
+
for (let j = 0; j < Math.round(capacity / 2); j++) {
|
|
128
|
+
compressor.generateCompressedId();
|
|
129
|
+
}
|
|
130
|
+
compressor.finalizeCreationRange(compressor.takeNextCreationRange());
|
|
131
|
+
}
|
|
132
|
+
compressor.finalizeCreationRange({
|
|
133
|
+
sessionId,
|
|
134
|
+
ids: {
|
|
135
|
+
firstGenCount: Math.floor(i / numSessions) * capacity + 1,
|
|
136
|
+
count: capacity,
|
|
137
|
+
requestedClusterSize: capacity,
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return compressor;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* A closed map from NamedClient to T.
|
|
146
|
+
*/
|
|
147
|
+
export type ClientMap<T> = ClosedMap<Client, T>;
|
|
148
|
+
|
|
149
|
+
function makeSessionIds(): ClientMap<SessionId> {
|
|
150
|
+
const stableIds = new Map<Client, SessionId>();
|
|
151
|
+
const clients = Object.values(Client);
|
|
152
|
+
for (let i = 0; i < clients.length; i++) {
|
|
153
|
+
// Place session uuids roughly in the middle of uuid space to increase odds of encountering interesting
|
|
154
|
+
// orderings in sorted collections
|
|
155
|
+
const sessionId = assertIsSessionId(`88888888-8888-4888-b${i}88-888888888888`);
|
|
156
|
+
stableIds.set(clients[i], sessionId);
|
|
157
|
+
}
|
|
158
|
+
return stableIds as ClientMap<SessionId>;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* An array of session ID strings corresponding to all non-local `Client` entries.
|
|
163
|
+
*/
|
|
164
|
+
export const sessionIds = makeSessionIds();
|
|
165
|
+
|
|
166
|
+
/** Information about a generated ID in a network to be validated by tests */
|
|
167
|
+
export interface TestIdData {
|
|
168
|
+
readonly id: SessionSpaceCompressedId;
|
|
169
|
+
readonly originatingClient: OriginatingClient;
|
|
170
|
+
readonly sessionId: SessionId;
|
|
171
|
+
readonly isSequenced: boolean;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Simulates a network of ID compressors.
|
|
176
|
+
* Not suitable for performance testing.
|
|
177
|
+
*/
|
|
178
|
+
export class IdCompressorTestNetwork {
|
|
179
|
+
/** The compressors used in this network */
|
|
180
|
+
private readonly compressors: ClientMap<IdCompressor>;
|
|
181
|
+
/** The log of operations seen by the server so far. Append-only. */
|
|
182
|
+
private readonly serverOperations: [
|
|
183
|
+
creationRange: IdCreationRange,
|
|
184
|
+
opSpaceIds: OpSpaceCompressedId[],
|
|
185
|
+
clientFrom: OriginatingClient,
|
|
186
|
+
sessionIdFrom: SessionId,
|
|
187
|
+
][] = [];
|
|
188
|
+
/** An index into `serverOperations` for each client which represents how many operations have been delivered to that client */
|
|
189
|
+
private readonly clientProgress: ClientMap<number>;
|
|
190
|
+
/** All ids (local and sequenced) that a client has created or received, in order. */
|
|
191
|
+
private readonly idLogs: ClientMap<TestIdData[]>;
|
|
192
|
+
/** All ids that a client has received from the server, in order. */
|
|
193
|
+
private readonly sequencedIdLogs: ClientMap<TestIdData[]>;
|
|
194
|
+
|
|
195
|
+
public constructor(public readonly initialClusterSize = 5) {
|
|
196
|
+
const compressors = new Map<Client, IdCompressor>();
|
|
197
|
+
const clientProgress = new Map<Client, number>();
|
|
198
|
+
const clientIds = new Map<Client, TestIdData[]>();
|
|
199
|
+
const clientSequencedIds = new Map<Client, TestIdData[]>();
|
|
200
|
+
for (const client of Object.values(Client)) {
|
|
201
|
+
const compressor = CompressorFactory.createCompressor(client, initialClusterSize);
|
|
202
|
+
compressors.set(client, compressor);
|
|
203
|
+
clientProgress.set(client, 0);
|
|
204
|
+
clientIds.set(client, []);
|
|
205
|
+
clientSequencedIds.set(client, []);
|
|
206
|
+
}
|
|
207
|
+
this.compressors = compressors as ClientMap<IdCompressor>;
|
|
208
|
+
this.clientProgress = clientProgress as ClientMap<number>;
|
|
209
|
+
this.idLogs = clientIds as ClientMap<TestIdData[]>;
|
|
210
|
+
this.sequencedIdLogs = clientSequencedIds as ClientMap<TestIdData[]>;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Returns the number of undelivered operations for the given client that are in flight in the network.
|
|
215
|
+
*/
|
|
216
|
+
public getPendingOperations(destination: Client): number {
|
|
217
|
+
return this.serverOperations.length - this.clientProgress.get(destination);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Returns an immutable handle to a compressor in the network.
|
|
222
|
+
*/
|
|
223
|
+
public getCompressor(client: Client): ReadonlyIdCompressor {
|
|
224
|
+
const compressors = this.compressors;
|
|
225
|
+
const handler = {
|
|
226
|
+
get<P extends keyof IdCompressor>(_: unknown, property: P): IdCompressor[P] {
|
|
227
|
+
return compressors.get(client)[property];
|
|
228
|
+
},
|
|
229
|
+
set<P extends keyof IdCompressor>(
|
|
230
|
+
_: unknown,
|
|
231
|
+
property: P,
|
|
232
|
+
value: IdCompressor[P],
|
|
233
|
+
): boolean {
|
|
234
|
+
compressors.get(client)[property] = value;
|
|
235
|
+
return true;
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
return new Proxy<IdCompressor>({} as unknown as IdCompressor, handler);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Returns a mutable handle to a compressor in the network. Use of mutation methods will break the network invariants and
|
|
243
|
+
* should only be used if the network will not be used again.
|
|
244
|
+
*/
|
|
245
|
+
public getCompressorUnsafe(client: Client): IdCompressor {
|
|
246
|
+
return this.getCompressor(client) as IdCompressor;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Returns a mutable handle to a compressor in the network. Use of mutation methods will break the network invariants and
|
|
251
|
+
* should only be used if the network will not be used again. Additionally, the returned compressor will be invalidated/unusable
|
|
252
|
+
* if any network operations cause it to be regenerated (serialization/deserialization, etc.).
|
|
253
|
+
*/
|
|
254
|
+
public getCompressorUnsafeNoProxy(client: Client): IdCompressor {
|
|
255
|
+
return this.compressors.get(client);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Returns data for all IDs created and received by this client, including ack's of their own (i.e. their own IDs will appear twice)
|
|
260
|
+
*/
|
|
261
|
+
public getIdLog(client: Client): readonly TestIdData[] {
|
|
262
|
+
return this.idLogs.get(client);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Returns data for all IDs received by this client, including ack's of their own.
|
|
267
|
+
*/
|
|
268
|
+
public getSequencedIdLog(client: Client): readonly TestIdData[] {
|
|
269
|
+
return this.sequencedIdLogs.get(client);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Get all compressors for the given destination
|
|
274
|
+
*/
|
|
275
|
+
public getTargetCompressors(clientTo: DestinationClient): [Client, IdCompressor][] {
|
|
276
|
+
return clientTo === MetaClient.All
|
|
277
|
+
? [...this.compressors.entries()]
|
|
278
|
+
: ([[clientTo, this.getCompressor(clientTo)]] as [Client, IdCompressor][]);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Changes the capacity request amount for a client. It will take effect immediately.
|
|
283
|
+
*/
|
|
284
|
+
public changeCapacity(client: Client, newClusterCapacity: number): void {
|
|
285
|
+
// eslint-disable-next-line @typescript-eslint/dot-notation
|
|
286
|
+
this.compressors.get(client)["nextRequestedClusterSize"] = newClusterCapacity;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
private addNewId(
|
|
290
|
+
client: Client,
|
|
291
|
+
id: SessionSpaceCompressedId,
|
|
292
|
+
originatingClient: OriginatingClient,
|
|
293
|
+
sessionIdFrom: SessionId,
|
|
294
|
+
isSequenced: boolean,
|
|
295
|
+
): void {
|
|
296
|
+
const idData = {
|
|
297
|
+
id,
|
|
298
|
+
originatingClient,
|
|
299
|
+
sessionId: sessionIdFrom,
|
|
300
|
+
isSequenced,
|
|
301
|
+
};
|
|
302
|
+
const clientIds = this.idLogs.get(client);
|
|
303
|
+
clientIds.push(idData);
|
|
304
|
+
if (isSequenced) {
|
|
305
|
+
const sequencedIds = this.sequencedIdLogs.get(client);
|
|
306
|
+
sequencedIds.push(idData);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Allocates a new range of local IDs and enqueues them for future delivery via a `testIdDelivery` action.
|
|
312
|
+
* Calls to this method determine the total order of delivery, regardless of when `deliverOperations` is called.
|
|
313
|
+
*/
|
|
314
|
+
public allocateAndSendIds(clientFrom: Client, numIds: number): OpSpaceCompressedId[] {
|
|
315
|
+
return this.allocateAndSendIdsFromRemoteClient(
|
|
316
|
+
clientFrom,
|
|
317
|
+
sessionIds.get(clientFrom),
|
|
318
|
+
numIds,
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Same contract as `allocateAndSendIds`, but the originating client will be a client with the supplied sessionId.
|
|
324
|
+
*/
|
|
325
|
+
public allocateAndSendIdsFromRemoteClient(
|
|
326
|
+
clientFrom: OriginatingClient,
|
|
327
|
+
sessionIdFrom: SessionId,
|
|
328
|
+
numIds: number,
|
|
329
|
+
): OpSpaceCompressedId[] {
|
|
330
|
+
assert(numIds > 0, "Must allocate a non-zero number of IDs");
|
|
331
|
+
if (clientFrom === OriginatingClient.Remote) {
|
|
332
|
+
const range: IdCreationRange = {
|
|
333
|
+
sessionId: sessionIdFrom,
|
|
334
|
+
ids: {
|
|
335
|
+
firstGenCount: 1,
|
|
336
|
+
count: numIds,
|
|
337
|
+
requestedClusterSize: this.getCompressor(Client.Client1)[
|
|
338
|
+
// eslint-disable-next-line @typescript-eslint/dot-notation
|
|
339
|
+
"nextRequestedClusterSize"
|
|
340
|
+
],
|
|
341
|
+
},
|
|
342
|
+
};
|
|
343
|
+
const opSpaceIds: OpSpaceCompressedId[] = [];
|
|
344
|
+
for (let i = 0; i < numIds; i++) {
|
|
345
|
+
opSpaceIds.push(-(i + 1) as OpSpaceCompressedId);
|
|
346
|
+
}
|
|
347
|
+
this.serverOperations.push([range, opSpaceIds, clientFrom, sessionIdFrom]);
|
|
348
|
+
return opSpaceIds;
|
|
349
|
+
} else {
|
|
350
|
+
assert(sessionIdFrom === sessionIds.get(clientFrom));
|
|
351
|
+
const compressor = this.compressors.get(clientFrom);
|
|
352
|
+
const sessionSpaceIds = generateCompressedIds(compressor, numIds);
|
|
353
|
+
for (let i = 0; i < numIds; i++) {
|
|
354
|
+
this.addNewId(clientFrom, sessionSpaceIds[i], clientFrom, sessionIdFrom, false);
|
|
355
|
+
}
|
|
356
|
+
const opSpaceIds = sessionSpaceIds.map((id) => compressor.normalizeToOpSpace(id));
|
|
357
|
+
const creationRange = compressor.takeNextCreationRange();
|
|
358
|
+
this.serverOperations.push([creationRange, opSpaceIds, clientFrom, sessionIdFrom]);
|
|
359
|
+
return opSpaceIds;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Delivers all undelivered ID ranges from the server to the target clients.
|
|
365
|
+
*/
|
|
366
|
+
public deliverOperations(clientTakingDelivery: Client, opsToDeliver?: number): void;
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Delivers all undelivered ID ranges from the server to the target clients.
|
|
370
|
+
*/
|
|
371
|
+
public deliverOperations(clientTakingDelivery: DestinationClient): void;
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Delivers all undelivered ID ranges from the server to the target clients.
|
|
375
|
+
*/
|
|
376
|
+
public deliverOperations(clientTakingDelivery: DestinationClient, opsToDeliver?: number): void {
|
|
377
|
+
let opIndexBound: number;
|
|
378
|
+
if (clientTakingDelivery === DestinationClient.All) {
|
|
379
|
+
assert(opsToDeliver === undefined);
|
|
380
|
+
opIndexBound = this.serverOperations.length;
|
|
381
|
+
} else {
|
|
382
|
+
opIndexBound =
|
|
383
|
+
opsToDeliver !== undefined
|
|
384
|
+
? this.clientProgress.get(clientTakingDelivery) + opsToDeliver
|
|
385
|
+
: this.serverOperations.length;
|
|
386
|
+
}
|
|
387
|
+
for (const [clientTo, compressorTo] of this.getTargetCompressors(clientTakingDelivery)) {
|
|
388
|
+
for (let i = this.clientProgress.get(clientTo); i < opIndexBound; i++) {
|
|
389
|
+
const [range, opSpaceIds, clientFrom, sessionIdFrom] = this.serverOperations[i];
|
|
390
|
+
compressorTo.finalizeCreationRange(range);
|
|
391
|
+
|
|
392
|
+
const ids = range.ids;
|
|
393
|
+
if (ids !== undefined) {
|
|
394
|
+
for (const id of opSpaceIds) {
|
|
395
|
+
const sessionSpaceId = compressorTo.normalizeToSessionSpace(
|
|
396
|
+
id,
|
|
397
|
+
range.sessionId,
|
|
398
|
+
);
|
|
399
|
+
this.addNewId(clientTo, sessionSpaceId, clientFrom, sessionIdFrom, true);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
this.clientProgress.set(clientTo, opIndexBound);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Simulate a client disconnecting (and serializing), then reconnecting (and deserializing)
|
|
410
|
+
*/
|
|
411
|
+
public goOfflineThenResume(client: Client): void {
|
|
412
|
+
const compressor = this.compressors.get(client);
|
|
413
|
+
const [_, resumedCompressor] = roundtrip(compressor, true);
|
|
414
|
+
this.compressors.set(client, resumedCompressor);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Ensure general validity of the network state. Useful for calling periodically or at the end of test scenarios.
|
|
419
|
+
*/
|
|
420
|
+
public assertNetworkState(): void {
|
|
421
|
+
const sequencedLogs = Object.values(Client).map(
|
|
422
|
+
(client) => [this.compressors.get(client), this.getSequencedIdLog(client)] as const,
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
// First, ensure all clients each generated a unique ID for each of their own calls to generate.
|
|
426
|
+
for (const [compressor, ids] of sequencedLogs) {
|
|
427
|
+
const allUuids = new Set<StableId | string>();
|
|
428
|
+
for (const idData of ids) {
|
|
429
|
+
const uuid = compressor.decompress(idData.id);
|
|
430
|
+
assert.strictEqual(!allUuids.has(uuid), true, "Duplicate UUID generated.");
|
|
431
|
+
allUuids.add(uuid);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const maxLogLength = sequencedLogs
|
|
436
|
+
.map(([_, data]) => data.length)
|
|
437
|
+
.reduce((p, n) => Math.max(p, n));
|
|
438
|
+
|
|
439
|
+
function getNextLogWithEntryAt(logsIndex: number, entryIndex: number): number | undefined {
|
|
440
|
+
for (let i = logsIndex; i < sequencedLogs.length; i++) {
|
|
441
|
+
const log = sequencedLogs[i];
|
|
442
|
+
if (log[1].length > entryIndex) {
|
|
443
|
+
return i;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return undefined;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const uuids = new Set<StableId>();
|
|
450
|
+
const finalIds = new Set<FinalCompressedId>();
|
|
451
|
+
const idIndicesAggregator = new Map<SessionId, number>();
|
|
452
|
+
|
|
453
|
+
function* getIdLogEntries(
|
|
454
|
+
columnIndex: number,
|
|
455
|
+
): Iterable<
|
|
456
|
+
[
|
|
457
|
+
current: [compressor: IdCompressor, idData: TestIdData],
|
|
458
|
+
next?: [compressor: IdCompressor, idData: TestIdData],
|
|
459
|
+
]
|
|
460
|
+
> {
|
|
461
|
+
let current = getNextLogWithEntryAt(0, columnIndex);
|
|
462
|
+
while (current !== undefined) {
|
|
463
|
+
const next = getNextLogWithEntryAt(current + 1, columnIndex);
|
|
464
|
+
const [compressor, log] = sequencedLogs[current];
|
|
465
|
+
if (next === undefined) {
|
|
466
|
+
yield [[compressor, log[columnIndex]]];
|
|
467
|
+
} else {
|
|
468
|
+
const [compressorNext, logNext] = sequencedLogs[next];
|
|
469
|
+
yield [
|
|
470
|
+
[compressor, log[columnIndex]],
|
|
471
|
+
[compressorNext, logNext[columnIndex]],
|
|
472
|
+
];
|
|
473
|
+
}
|
|
474
|
+
current = next;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
for (let i = 0; i < maxLogLength; i++) {
|
|
479
|
+
let idCreatorCount = 0;
|
|
480
|
+
let originatingSession: SessionId | undefined;
|
|
481
|
+
for (const [current, next] of getIdLogEntries(i)) {
|
|
482
|
+
const [compressorA, idDataA] = current;
|
|
483
|
+
const sessionSpaceIdA = idDataA.id;
|
|
484
|
+
const idIndex = getOrCreate(idIndicesAggregator, idDataA.sessionId, () => 0);
|
|
485
|
+
originatingSession ??= idDataA.sessionId;
|
|
486
|
+
assert(
|
|
487
|
+
idDataA.sessionId === originatingSession,
|
|
488
|
+
"Test infra gave wrong originating client to TestIdData",
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
// Only one client should have this ID as local in its session space, as only one client could have created this ID
|
|
492
|
+
if (isLocalId(sessionSpaceIdA)) {
|
|
493
|
+
if (originatingSession !== OriginatingClient.Remote) {
|
|
494
|
+
assert.strictEqual(
|
|
495
|
+
idDataA.sessionId,
|
|
496
|
+
this.compressors.get(idDataA.originatingClient as Client)
|
|
497
|
+
.localSessionId,
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
idCreatorCount++;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const uuidASessionSpace = compressorA.decompress(sessionSpaceIdA);
|
|
504
|
+
assert.strictEqual(
|
|
505
|
+
uuidASessionSpace,
|
|
506
|
+
incrementStableId(idDataA.sessionId, idIndex),
|
|
507
|
+
);
|
|
508
|
+
assert.strictEqual(compressorA.recompress(uuidASessionSpace), sessionSpaceIdA);
|
|
509
|
+
uuids.add(uuidASessionSpace);
|
|
510
|
+
const opSpaceIdA = compressorA.normalizeToOpSpace(sessionSpaceIdA);
|
|
511
|
+
if (!isFinalId(opSpaceIdA)) {
|
|
512
|
+
fail("IDs should have been finalized.");
|
|
513
|
+
}
|
|
514
|
+
const reNormalizedIdA = compressorA.normalizeToSessionSpace(
|
|
515
|
+
opSpaceIdA,
|
|
516
|
+
compressorA.localSessionId,
|
|
517
|
+
);
|
|
518
|
+
assert.strictEqual(reNormalizedIdA, sessionSpaceIdA);
|
|
519
|
+
finalIds.add(opSpaceIdA);
|
|
520
|
+
const uuidAOpSpace = compressorA.decompress(reNormalizedIdA);
|
|
521
|
+
|
|
522
|
+
assert.strictEqual(uuidASessionSpace, uuidAOpSpace);
|
|
523
|
+
|
|
524
|
+
if (next !== undefined) {
|
|
525
|
+
const [compressorB, idDataB] = next;
|
|
526
|
+
const sessionSpaceIdB = idDataB.id;
|
|
527
|
+
|
|
528
|
+
const uuidBSessionSpace = compressorB.decompress(sessionSpaceIdB);
|
|
529
|
+
assert.strictEqual(uuidASessionSpace, uuidBSessionSpace);
|
|
530
|
+
const opSpaceIdB = compressorB.normalizeToOpSpace(sessionSpaceIdB);
|
|
531
|
+
if (opSpaceIdA !== opSpaceIdB) {
|
|
532
|
+
compressorB.normalizeToOpSpace(sessionSpaceIdB);
|
|
533
|
+
compressorA.normalizeToOpSpace(sessionSpaceIdA);
|
|
534
|
+
}
|
|
535
|
+
assert.strictEqual(opSpaceIdA, opSpaceIdB);
|
|
536
|
+
if (!isFinalId(opSpaceIdB)) {
|
|
537
|
+
fail("IDs should have been finalized.");
|
|
538
|
+
}
|
|
539
|
+
const uuidBOpSpace = compressorB.decompress(
|
|
540
|
+
opSpaceIdB as unknown as SessionSpaceCompressedId,
|
|
541
|
+
);
|
|
542
|
+
assert.strictEqual(uuidAOpSpace, uuidBOpSpace);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
assert(idCreatorCount <= 1, "Only one client can create an ID.");
|
|
547
|
+
assert.strictEqual(uuids.size, finalIds.size);
|
|
548
|
+
assert(originatingSession !== undefined, "Expected originating client to be defined");
|
|
549
|
+
idIndicesAggregator.set(
|
|
550
|
+
originatingSession,
|
|
551
|
+
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
|
|
552
|
+
(idIndicesAggregator.get(originatingSession) ??
|
|
553
|
+
fail("Expected pre-existing index for originating client")) + 1,
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
for (const [compressor] of sequencedLogs) {
|
|
558
|
+
expectSerializes(compressor);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Roundtrips the supplied compressor through serialization and deserialization.
|
|
565
|
+
*/
|
|
566
|
+
export function roundtrip(
|
|
567
|
+
compressor: ReadonlyIdCompressor,
|
|
568
|
+
withSession: true,
|
|
569
|
+
): [SerializedIdCompressorWithOngoingSession, IdCompressor];
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Roundtrips the supplied compressor through serialization and deserialization.
|
|
573
|
+
*/
|
|
574
|
+
export function roundtrip(
|
|
575
|
+
compressor: ReadonlyIdCompressor,
|
|
576
|
+
withSession: false,
|
|
577
|
+
): [SerializedIdCompressorWithNoSession, IdCompressor];
|
|
578
|
+
|
|
579
|
+
export function roundtrip(
|
|
580
|
+
compressor: ReadonlyIdCompressor,
|
|
581
|
+
withSession: boolean,
|
|
582
|
+
): [SerializedIdCompressorWithOngoingSession | SerializedIdCompressorWithNoSession, IdCompressor] {
|
|
583
|
+
if (withSession) {
|
|
584
|
+
const serialized = compressor.serialize(withSession);
|
|
585
|
+
return [serialized, IdCompressor.deserialize(serialized)];
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const nonLocalSerialized = compressor.serialize(withSession);
|
|
589
|
+
return [nonLocalSerialized, IdCompressor.deserialize(nonLocalSerialized, createSessionId())];
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Asserts that the supplied compressor correctly roundtrips through serialization/deserialization.
|
|
594
|
+
*/
|
|
595
|
+
export function expectSerializes(
|
|
596
|
+
compressor: ReadonlyIdCompressor,
|
|
597
|
+
): [SerializedIdCompressorWithNoSession, SerializedIdCompressorWithOngoingSession] {
|
|
598
|
+
function expectSerializesWithSession(
|
|
599
|
+
withSession: boolean,
|
|
600
|
+
): SerializedIdCompressorWithOngoingSession | SerializedIdCompressorWithNoSession {
|
|
601
|
+
let serialized:
|
|
602
|
+
| SerializedIdCompressorWithOngoingSession
|
|
603
|
+
| SerializedIdCompressorWithNoSession;
|
|
604
|
+
let deserialized: IdCompressor;
|
|
605
|
+
if (withSession) {
|
|
606
|
+
[serialized, deserialized] = roundtrip(compressor, true);
|
|
607
|
+
} else {
|
|
608
|
+
[serialized, deserialized] = roundtrip(compressor, false);
|
|
609
|
+
}
|
|
610
|
+
assert.strictEqual(compressor.equals(deserialized, withSession), true);
|
|
611
|
+
return serialized;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
return [
|
|
615
|
+
expectSerializesWithSession(false) as SerializedIdCompressorWithNoSession,
|
|
616
|
+
expectSerializesWithSession(true) as SerializedIdCompressorWithOngoingSession,
|
|
617
|
+
];
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Merges 'from' into 'to', and returns 'to'.
|
|
622
|
+
*/
|
|
623
|
+
export function mergeArrayMaps<K, V>(
|
|
624
|
+
to: Pick<Map<K, V[]>, "get" | "set">,
|
|
625
|
+
from: ReadonlyMap<K, V[]>,
|
|
626
|
+
): Pick<Map<K, V[]>, "get" | "set"> {
|
|
627
|
+
for (const [key, value] of from.entries()) {
|
|
628
|
+
const entry = to.get(key);
|
|
629
|
+
if (entry !== undefined) {
|
|
630
|
+
entry.push(...value);
|
|
631
|
+
} else {
|
|
632
|
+
to.set(key, [...value]);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
return to;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
interface AllocateIds {
|
|
639
|
+
type: "allocateIds";
|
|
640
|
+
client: Client;
|
|
641
|
+
numIds: number;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
interface AllocateOutsideIds {
|
|
645
|
+
type: "allocateOutsideIds";
|
|
646
|
+
sessionId: SessionId;
|
|
647
|
+
numIds: number;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
interface DeliverAllOperations {
|
|
651
|
+
type: "deliverAllOperations";
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
interface DeliverSomeOperations {
|
|
655
|
+
type: "deliverSomeOperations";
|
|
656
|
+
client: Client;
|
|
657
|
+
count: number;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
interface ChangeCapacity {
|
|
661
|
+
type: "changeCapacity";
|
|
662
|
+
client: Client;
|
|
663
|
+
newSize: number;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Represents intent to go offline then resume.
|
|
667
|
+
interface Reconnect {
|
|
668
|
+
type: "reconnect";
|
|
669
|
+
client: Client;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
interface Validate {
|
|
673
|
+
type: "validate";
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
type Operation =
|
|
677
|
+
| AllocateIds
|
|
678
|
+
| AllocateOutsideIds
|
|
679
|
+
| DeliverSomeOperations
|
|
680
|
+
| DeliverAllOperations
|
|
681
|
+
| ChangeCapacity
|
|
682
|
+
| Reconnect
|
|
683
|
+
| Validate;
|
|
684
|
+
|
|
685
|
+
interface FuzzTestState extends BaseFuzzTestState {
|
|
686
|
+
network: IdCompressorTestNetwork;
|
|
687
|
+
activeClients: Client[];
|
|
688
|
+
selectableClients: Client[];
|
|
689
|
+
clusterSize: number;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
export interface OperationGenerationConfig {
|
|
693
|
+
/** maximum cluster size of the network. Default: 25 */
|
|
694
|
+
maxClusterSize?: number;
|
|
695
|
+
/** Number of ops between validation ops. Default: 200 */
|
|
696
|
+
validateInterval?: number;
|
|
697
|
+
/** Fraction of ID allocations that are from an outside client (not Client1/2/3). */
|
|
698
|
+
outsideAllocationFraction?: number;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
const defaultOptions = {
|
|
702
|
+
maxClusterSize: 25,
|
|
703
|
+
validateInterval: 200,
|
|
704
|
+
outsideAllocationFraction: 0.1,
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
export function makeOpGenerator(
|
|
708
|
+
options: OperationGenerationConfig,
|
|
709
|
+
): Generator<Operation, FuzzTestState> {
|
|
710
|
+
const { maxClusterSize, validateInterval, outsideAllocationFraction } = {
|
|
711
|
+
...defaultOptions,
|
|
712
|
+
...options,
|
|
713
|
+
};
|
|
714
|
+
assert(outsideAllocationFraction >= 0 && outsideAllocationFraction <= 1);
|
|
715
|
+
|
|
716
|
+
function allocateIdsGenerator({
|
|
717
|
+
activeClients,
|
|
718
|
+
clusterSize,
|
|
719
|
+
random,
|
|
720
|
+
}: FuzzTestState): AllocateIds {
|
|
721
|
+
const client = random.pick(activeClients);
|
|
722
|
+
const maxIdsPerUsage = clusterSize * 2;
|
|
723
|
+
const numIds = Math.floor(random.real(0, 1) ** 3 * maxIdsPerUsage) + 1;
|
|
724
|
+
return {
|
|
725
|
+
type: "allocateIds",
|
|
726
|
+
client,
|
|
727
|
+
numIds,
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function allocateOutsideIdsGenerator({
|
|
732
|
+
clusterSize,
|
|
733
|
+
random,
|
|
734
|
+
}: FuzzTestState): AllocateOutsideIds {
|
|
735
|
+
const maxIdsPerUsage = clusterSize * 2;
|
|
736
|
+
const numIds = Math.floor(random.real(0, 1) ** 3 * maxIdsPerUsage) + 1;
|
|
737
|
+
return {
|
|
738
|
+
type: "allocateOutsideIds",
|
|
739
|
+
sessionId: createSessionId(),
|
|
740
|
+
numIds,
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function changeCapacityGenerator({ random, activeClients }: FuzzTestState): ChangeCapacity {
|
|
745
|
+
return {
|
|
746
|
+
type: "changeCapacity",
|
|
747
|
+
client: random.pick(activeClients),
|
|
748
|
+
newSize: Math.min(
|
|
749
|
+
Math.floor(random.real(0, 1) ** 2 * maxClusterSize) + 1,
|
|
750
|
+
maxClusterSize,
|
|
751
|
+
),
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function deliverAllOperationsGenerator(): DeliverAllOperations {
|
|
756
|
+
return {
|
|
757
|
+
type: "deliverAllOperations",
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function deliverSomeOperationsGenerator({
|
|
762
|
+
random,
|
|
763
|
+
selectableClients,
|
|
764
|
+
network,
|
|
765
|
+
}: FuzzTestState): DeliverSomeOperations {
|
|
766
|
+
const pendingClients = selectableClients.filter((c) => network.getPendingOperations(c) > 0);
|
|
767
|
+
if (pendingClients.length === 0) {
|
|
768
|
+
return {
|
|
769
|
+
type: "deliverSomeOperations",
|
|
770
|
+
client: random.pick(selectableClients),
|
|
771
|
+
count: 0,
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
const client = random.pick(pendingClients);
|
|
775
|
+
return {
|
|
776
|
+
type: "deliverSomeOperations",
|
|
777
|
+
client,
|
|
778
|
+
count: random.integer(1, network.getPendingOperations(client)),
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function reconnectGenerator({ activeClients, random }: FuzzTestState): Reconnect {
|
|
783
|
+
return { type: "reconnect", client: random.pick(activeClients) };
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
const allocationWeight = 16;
|
|
787
|
+
return interleave(
|
|
788
|
+
createWeightedGenerator<Operation, FuzzTestState>([
|
|
789
|
+
[changeCapacityGenerator, 1],
|
|
790
|
+
[allocateIdsGenerator, Math.round(allocationWeight * (1 - outsideAllocationFraction))],
|
|
791
|
+
[allocateOutsideIdsGenerator, Math.round(allocationWeight * outsideAllocationFraction)],
|
|
792
|
+
[deliverAllOperationsGenerator, 2],
|
|
793
|
+
[deliverSomeOperationsGenerator, 6],
|
|
794
|
+
[reconnectGenerator, 1],
|
|
795
|
+
]),
|
|
796
|
+
take(1, repeat<Operation, FuzzTestState>({ type: "validate" })),
|
|
797
|
+
validateInterval,
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* Performs random actions on a test network.
|
|
803
|
+
* @param generator - the generator used to provide operations
|
|
804
|
+
* @param network - the test network to test
|
|
805
|
+
* @param seed - the seed for the random generation of the fuzz actions
|
|
806
|
+
* @param observerClient - if provided, this client will never generate local ids
|
|
807
|
+
* @param synchronizeAtEnd - if provided, all client will have all operations delivered from the server at the end of the test
|
|
808
|
+
* @param validator - if provided, this callback will be invoked periodically during the fuzz test.
|
|
809
|
+
*/
|
|
810
|
+
export function performFuzzActions(
|
|
811
|
+
generator: Generator<Operation, FuzzTestState>,
|
|
812
|
+
network: IdCompressorTestNetwork,
|
|
813
|
+
seed: number,
|
|
814
|
+
observerClient?: Client,
|
|
815
|
+
synchronizeAtEnd: boolean = true,
|
|
816
|
+
validator?: (network: IdCompressorTestNetwork) => void,
|
|
817
|
+
saveInfo?: SaveInfo,
|
|
818
|
+
): void {
|
|
819
|
+
const random = makeRandom(seed);
|
|
820
|
+
const selectableClients: Client[] = network
|
|
821
|
+
.getTargetCompressors(MetaClient.All)
|
|
822
|
+
.map(([client]) => client);
|
|
823
|
+
|
|
824
|
+
const initialState: FuzzTestState = {
|
|
825
|
+
random,
|
|
826
|
+
network,
|
|
827
|
+
activeClients: selectableClients.filter((c) => c !== observerClient),
|
|
828
|
+
selectableClients,
|
|
829
|
+
clusterSize: network.initialClusterSize,
|
|
830
|
+
};
|
|
831
|
+
|
|
832
|
+
performFuzzActionsBase(
|
|
833
|
+
generator,
|
|
834
|
+
{
|
|
835
|
+
allocateIds: (state, { client, numIds }) => {
|
|
836
|
+
network.allocateAndSendIdsFromRemoteClient(client, sessionIds.get(client), numIds);
|
|
837
|
+
return state;
|
|
838
|
+
},
|
|
839
|
+
allocateOutsideIds: (state, { sessionId, numIds }) => {
|
|
840
|
+
network.allocateAndSendIdsFromRemoteClient(
|
|
841
|
+
OriginatingClient.Remote,
|
|
842
|
+
sessionId,
|
|
843
|
+
numIds,
|
|
844
|
+
);
|
|
845
|
+
return state;
|
|
846
|
+
},
|
|
847
|
+
changeCapacity: (state, op) => {
|
|
848
|
+
network.changeCapacity(op.client, op.newSize);
|
|
849
|
+
return { ...state, clusterSize: op.newSize };
|
|
850
|
+
},
|
|
851
|
+
deliverSomeOperations: (state, op) => {
|
|
852
|
+
network.deliverOperations(op.client, op.count);
|
|
853
|
+
return state;
|
|
854
|
+
},
|
|
855
|
+
deliverAllOperations: (state) => {
|
|
856
|
+
network.deliverOperations(DestinationClient.All);
|
|
857
|
+
return state;
|
|
858
|
+
},
|
|
859
|
+
reconnect: (state, { client }) => {
|
|
860
|
+
network.goOfflineThenResume(client);
|
|
861
|
+
return state;
|
|
862
|
+
},
|
|
863
|
+
validate: (state) => {
|
|
864
|
+
network.deliverOperations(DestinationClient.All);
|
|
865
|
+
validator?.(network);
|
|
866
|
+
return state;
|
|
867
|
+
},
|
|
868
|
+
},
|
|
869
|
+
initialState,
|
|
870
|
+
saveInfo,
|
|
871
|
+
);
|
|
872
|
+
|
|
873
|
+
if (synchronizeAtEnd) {
|
|
874
|
+
network.deliverOperations(DestinationClient.All);
|
|
875
|
+
validator?.(network);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* Helper to generate a fixed number of IDs.
|
|
881
|
+
*/
|
|
882
|
+
export function generateCompressedIds(
|
|
883
|
+
compressor: IdCompressor,
|
|
884
|
+
count: number,
|
|
885
|
+
): SessionSpaceCompressedId[] {
|
|
886
|
+
const ids: SessionSpaceCompressedId[] = [];
|
|
887
|
+
for (let i = 0; i < count; i++) {
|
|
888
|
+
ids.push(compressor.generateCompressedId());
|
|
889
|
+
}
|
|
890
|
+
return ids;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/**
|
|
894
|
+
* Creates a compressor that only produces final IDs.
|
|
895
|
+
* It should only be used for testing purposes.
|
|
896
|
+
*/
|
|
897
|
+
export function createAlwaysFinalizedIdCompressor(
|
|
898
|
+
logger?: ITelemetryBaseLogger,
|
|
899
|
+
): IIdCompressor & IIdCompressorCore;
|
|
900
|
+
/**
|
|
901
|
+
* Creates a compressor that only produces final IDs.
|
|
902
|
+
* It should only be used for testing purposes.
|
|
903
|
+
*/
|
|
904
|
+
export function createAlwaysFinalizedIdCompressor(
|
|
905
|
+
sessionId: SessionId,
|
|
906
|
+
logger?: ITelemetryBaseLogger,
|
|
907
|
+
): IIdCompressor & IIdCompressorCore;
|
|
908
|
+
export function createAlwaysFinalizedIdCompressor(
|
|
909
|
+
sessionIdOrLogger?: SessionId | ITelemetryBaseLogger,
|
|
910
|
+
loggerOrUndefined?: ITelemetryBaseLogger,
|
|
911
|
+
): IIdCompressor & IIdCompressorCore {
|
|
912
|
+
const compressor =
|
|
913
|
+
sessionIdOrLogger === undefined
|
|
914
|
+
? createIdCompressor()
|
|
915
|
+
: typeof sessionIdOrLogger === "string"
|
|
916
|
+
? createIdCompressor(sessionIdOrLogger, loggerOrUndefined)
|
|
917
|
+
: createIdCompressor(sessionIdOrLogger);
|
|
918
|
+
// Permanently put the compressor in a ghost session
|
|
919
|
+
(compressor as IdCompressor).startGhostSession(createSessionId());
|
|
920
|
+
return compressor;
|
|
921
|
+
}
|