diff --git a/packages/runtime/id-compressor/.eslintrc.cjs b/packages/runtime/id-compressor/.eslintrc.cjs index 0cfb8c689f85..7551e98ea5d1 100644 --- a/packages/runtime/id-compressor/.eslintrc.cjs +++ b/packages/runtime/id-compressor/.eslintrc.cjs @@ -4,10 +4,7 @@ */ module.exports = { - extends: [ - require.resolve("@fluidframework/eslint-config-fluid/minimal-deprecated"), - "prettier", - ], + extends: [require.resolve("@fluidframework/eslint-config-fluid/recommended"), "prettier"], parserOptions: { project: ["./tsconfig.json", "./src/test/tsconfig.json"], }, diff --git a/packages/runtime/id-compressor/src/appendOnlySortedMap.ts b/packages/runtime/id-compressor/src/appendOnlySortedMap.ts index 1ee13c3a72fa..d6740193de04 100644 --- a/packages/runtime/id-compressor/src/appendOnlySortedMap.ts +++ b/packages/runtime/id-compressor/src/appendOnlySortedMap.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. */ -/* eslint-disable tsdoc/syntax */ /* eslint-disable no-bitwise */ import { assert } from "@fluidframework/core-utils/internal"; @@ -20,6 +19,7 @@ export class AppendOnlySortedMap { public constructor(protected readonly comparator: (a: K, b: K) => number) {} /** + * Gets the size of the map. * @returns the number of entries in this map */ public get size(): number { @@ -27,6 +27,7 @@ export class AppendOnlySortedMap { } /** + * Gets the min key in the map. * @returns the min key in the map. */ public minKey(): K | undefined { @@ -34,6 +35,7 @@ export class AppendOnlySortedMap { } /** + * Gets the max key in the map. * @returns the max key in the map. */ public maxKey(): K | undefined { @@ -41,6 +43,7 @@ export class AppendOnlySortedMap { } /** + * Gets the min value in the map. * @returns the min value in the map. */ public minValue(): V | undefined { @@ -48,14 +51,16 @@ export class AppendOnlySortedMap { } /** - * @returns the min value in the map. + * Gets the max value in the map. + * @returns the max value in the map. */ public maxValue(): V | undefined { return this.elements[this.elements.length - 1] as V | undefined; } /** - * @returns the min key in the map. + * Gets the first key/value pair in the map. + * @returns the first pair if it exists, or undefined otherwise. */ public first(): [K, V] | undefined { const { elements } = this; @@ -67,7 +72,8 @@ export class AppendOnlySortedMap { } /** - * @returns the max key in the map. + * Gets the last key/value pair in the map. + * @returns the last pair if it exists, or undefined otherwise. */ public last(): [K, V] | undefined { const { elements } = this; @@ -80,7 +86,9 @@ export class AppendOnlySortedMap { } /** - * Returns the element at the insertion index. + * Gets the entry at the specified index. + * @param index - the entry index + * @returns the key/value pair if it exists, or undefined otherwise. */ public getAtIndex(index: number): [K, V] | undefined { const realIndex = index * 2; @@ -92,6 +100,7 @@ export class AppendOnlySortedMap { } /** + * Gets the entries in the map. * @returns an iterable of the entries in the map. */ public *entries(): IterableIterator { @@ -102,6 +111,7 @@ export class AppendOnlySortedMap { } /** + * Gets the keys in the map. * @returns an iterable of the keys in the map. */ public *keys(): IterableIterator { @@ -112,6 +122,7 @@ export class AppendOnlySortedMap { } /** + * Gets the values in the map. * @returns an iterable of the values in the map. */ public *values(): IterableIterator { @@ -122,6 +133,7 @@ export class AppendOnlySortedMap { } /** + * Gets the entries in the map, reversed. * @returns an iterable of the entries in the map, reversed. */ public *entriesReversed(): IterableIterator { @@ -132,9 +144,9 @@ export class AppendOnlySortedMap { } /** - * Adds a new key/value pair to the map. `key` must be > to all keys in the map. - * @param key - the key to add. - * @param value - the value to add. + * Appends a new key/value pair at the end of the map. `key` must be greater than all other keys in the map. + * @param key - the key to add + * @param value - the value to add */ public append(key: K, value: V): void { const { elements } = this; @@ -142,15 +154,14 @@ export class AppendOnlySortedMap { if (length !== 0 && this.comparator(key, this.maxKey() as K) <= 0) { throw new Error("Inserted key must be > all others in the map."); } - elements.push(key); - elements.push(value); + elements.push(key, value); } /** - * Replaces the last key/value pair with the given one. If the map is empty, it simply appends. - * `key` must be > to all keys in the map prior to the one replaced. - * @param key - the key to add. - * @param value - the value to add. + * Replaces the last key/value pair with a new one. If the map is empty, the new pair is appended. + * 'key' must be greater than all other keys in the map. + * @param key - the key to set + * @param value - the value to set */ public replaceLast(key: K, value: V): void { const { elements, comparator } = this; @@ -162,13 +173,13 @@ export class AppendOnlySortedMap { throw new Error("Inserted key must be > all others in the map."); } } - elements.push(key); - elements.push(value); + elements.push(key, value); } /** - * @param key - the key to lookup. - * @returns the value associated with `key` if such an entry exists, and undefined otherwise. + * Gets the value associated with a given key. + * @param key - the key to lookup + * @returns the value if it exists, or undefined otherwise. */ public get(key: K): V | undefined { const index = AppendOnlySortedMap.keyIndexOf(this.elements, key, this.comparator); @@ -179,15 +190,16 @@ export class AppendOnlySortedMap { } /** - * @param key - the key to lookup. - * @returns the entry associated with `key` if such an entry exists, the entry associated with the next lower key if such an entry - * exists, and undefined otherwise. + * Gets the pair associated with the given key or the next smaller key. + * @param key - the key to lookup + * @returns the pair if it exists, or undefined otherwise. */ public getPairOrNextLower(key: K): readonly [K, V] | undefined { return this.getPairOrNextLowerBy(key, this.comparator); } /** + * Gets the pair associated with the given key or the next higher key. * @param key - the key to lookup. * @returns the entry associated with `key` if such an entry exists, the entry associated with the next higher key if such an entry * exists, and undefined otherwise. @@ -293,6 +305,12 @@ export class AppendOnlySortedMap { return keyIndex; } + /** + * Gets the pair associated with the given key or next higher key. + * @param search - the search value + * @param comparator - a comparison function + * @returns the pair if it exists, or undefined otherwise. + */ protected getPairOrNextHigherBy( search: T, comparator: (search: T, key: K, value: V) => number, diff --git a/packages/runtime/id-compressor/src/finalSpace.ts b/packages/runtime/id-compressor/src/finalSpace.ts index ad9b6d4a2b36..46b05687d079 100644 --- a/packages/runtime/id-compressor/src/finalSpace.ts +++ b/packages/runtime/id-compressor/src/finalSpace.ts @@ -29,7 +29,7 @@ export class FinalSpace { return this.clusterList[this.clusterList.length - 1]; } - public addCluster(newCluster: IdCluster) { + public addCluster(newCluster: IdCluster): void { const lastCluster = this.getLastCluster(); assert( lastCluster === undefined || @@ -40,7 +40,7 @@ export class FinalSpace { } /** - * @returns the upper bound (exclusive) of finalized IDs in final space, i.e. one greater than the last final ID in the last cluster. + * Gets the upper bound (exclusive) of finalized IDs in final space, i.e. one greater than the last final ID in the last cluster. * Note: this does not include allocated but unfinalized space in clusters. */ public getFinalizedIdLimit(): FinalCompressedId { @@ -51,7 +51,7 @@ export class FinalSpace { } /** - * @returns the upper bound (exclusive) of allocated IDs in final space, i.e. one greater than the last final ID in the last cluster. + * Gets the upper bound (exclusive) of allocated IDs in final space, i.e. one greater than the last final ID in the last cluster. * Note: this does includes all allocated IDs in clusters. */ public getAllocatedIdLimit(): FinalCompressedId { @@ -62,8 +62,9 @@ export class FinalSpace { } public equals(other: FinalSpace): boolean { - for (const [index, value] of Object.entries(this.clusterList)) { - if (!clustersEqual(value, other.clusterList[index])) { + for (let i = 0; i < this.clusterList.length; i++) { + const cluster = this.clusterList[i] as IdCluster; + if (!clustersEqual(cluster, other.clusterList[i] as IdCluster)) { return false; } } diff --git a/packages/runtime/id-compressor/src/idCompressor.ts b/packages/runtime/id-compressor/src/idCompressor.ts index a4c4ec146fb8..d38ae7452ed9 100644 --- a/packages/runtime/id-compressor/src/idCompressor.ts +++ b/packages/runtime/id-compressor/src/idCompressor.ts @@ -64,7 +64,7 @@ import { * The version of IdCompressor that is currently persisted. * This should not be changed without careful consideration to compatibility. */ -const currentWrittenVersion = 2.0; +const currentWrittenVersion = 2; function rangeFinalizationError(expectedStart: number, actualStart: number): LoggingError { return new LoggingError("Ranges finalized out of order", { @@ -202,7 +202,7 @@ export class IdCompressor implements IIdCompressor, IIdCompressorCore { /** * {@inheritdoc IIdCompressorCore.beginGhostSession} */ - public beginGhostSession(ghostSessionId: SessionId, ghostSessionCallback: () => void) { + public beginGhostSession(ghostSessionId: SessionId, ghostSessionCallback: () => void): void { this.startGhostSession(ghostSessionId); try { ghostSessionCallback(); @@ -554,7 +554,8 @@ export class IdCompressor implements IIdCompressor, IIdCompressorCore { !this.ongoingGhostSession, 0x8a9 /* IdCompressor should not be operated normally when in a ghost session */, ); - const { normalizer, finalSpace, sessions } = this; + const { normalizer, finalSpace, sessions, localGenCount, logger, nextRangeBaseGenCount } = + this; const sessionIndexMap = new Map(); let sessionIndex = 0; for (const session of sessions.sessions()) { @@ -568,7 +569,7 @@ export class IdCompressor implements IIdCompressor, IIdCompressorCore { ? 1 + // generated ID count 1 + // next range base genCount 1 + // count of normalizer pairs - this.normalizer.idRanges.size * 2 // pairs + normalizer.idRanges.size * 2 // pairs : 0; // Layout size, in 8 byte increments const totalSize = @@ -592,7 +593,7 @@ export class IdCompressor implements IIdCompressor, IIdCompressorCore { index = writeNumericUuid(serializedUint, index, session.sessionUuid); } - finalSpace.clusters.forEach((cluster) => { + for (const cluster of finalSpace.clusters) { index = writeNumber( serializedFloat, index, @@ -600,11 +601,11 @@ export class IdCompressor implements IIdCompressor, IIdCompressorCore { ); index = writeNumber(serializedFloat, index, cluster.capacity); index = writeNumber(serializedFloat, index, cluster.count); - }); + } if (hasLocalState) { - index = writeNumber(serializedFloat, index, this.localGenCount); - index = writeNumber(serializedFloat, index, this.nextRangeBaseGenCount); + index = writeNumber(serializedFloat, index, localGenCount); + index = writeNumber(serializedFloat, index, nextRangeBaseGenCount); index = writeNumber(serializedFloat, index, normalizer.idRanges.size); for (const [leadingGenCount, count] of normalizer.idRanges.entries()) { index = writeNumber(serializedFloat, index, leadingGenCount); @@ -613,7 +614,7 @@ export class IdCompressor implements IIdCompressor, IIdCompressorCore { } assert(index === totalSize, 0x75b /* Serialized size was incorrectly calculated. */); - this.logger?.sendTelemetryEvent({ + logger?.sendTelemetryEvent({ eventName: "RuntimeIdCompressor:SerializedIdCompressorSize", size: serializedFloat.byteLength, clusterCount: finalSpace.clusters.length, @@ -652,12 +653,15 @@ export class IdCompressor implements IIdCompressor, IIdCompressorCore { }; const version = readNumber(index); switch (version) { - case 1.0: + case 1: { throw new Error("IdCompressor version 1.0 is no longer supported."); - case 2.0: + } + case 2: { return IdCompressor.deserialize2_0(index, sessionId, logger); - default: + } + default: { throw new Error("Unknown IdCompressor serialized version."); + } } } @@ -673,17 +677,17 @@ export class IdCompressor implements IIdCompressor, IIdCompressorCore { // Sessions let sessionOffset = 0; const sessions: [NumericUuid, Session][] = []; - if (!hasLocalState) { + if (hasLocalState) { + assert( + sessionId === undefined, + 0x75e /* Local state should not exist in serialized form. */, + ); + } else { // If !hasLocalState, there won't be a serialized local session ID so insert one at the beginning assert(sessionId !== undefined, 0x75d /* Local session ID is undefined. */); const localSessionNumeric = numericUuidFromStableId(sessionId); sessions.push([localSessionNumeric, new Session(localSessionNumeric)]); sessionOffset = 1; - } else { - assert( - sessionId === undefined, - 0x75e /* Local state should not exist in serialized form. */, - ); } for (let i = 0; i < sessionCount; i++) { diff --git a/packages/runtime/id-compressor/src/identifiers.ts b/packages/runtime/id-compressor/src/identifiers.ts index d394c51b812d..791474b16468 100644 --- a/packages/runtime/id-compressor/src/identifiers.ts +++ b/packages/runtime/id-compressor/src/identifiers.ts @@ -26,7 +26,7 @@ export type LocalCompressedId = number & { } & SessionSpaceCompressedId; // Same brand as CompressedId, as local IDs are always locally normalized /** - * @returns true if the supplied ID is a final ID. + * Returns true if the supplied ID is a final ID. */ export function isFinalId( id: SessionSpaceCompressedId | OpSpaceCompressedId, diff --git a/packages/runtime/id-compressor/src/sessions.ts b/packages/runtime/id-compressor/src/sessions.ts index 377178848381..11b4e72af46f 100644 --- a/packages/runtime/id-compressor/src/sessions.ts +++ b/packages/runtime/id-compressor/src/sessions.ts @@ -131,7 +131,7 @@ export class Sessions { } public equals(other: Sessions, includeLocalState: boolean): boolean { - const checkIsSubset = (sessionsA: Sessions, sessionsB: Sessions) => { + const checkIsSubset = (sessionsA: Sessions, sessionsB: Sessions): boolean => { const first = sessionsA.sessions().next(); const firstSessionThis = first.done ? undefined : first.value; for (const [stableId, session] of sessionsA.sessionCache.entries()) { @@ -296,7 +296,7 @@ export class Session { public equals(other: Session): boolean { for (const [index, value] of Object.entries(this.clusterChain)) { - if (!clustersEqual(value, other.clusterChain[index])) { + if (!clustersEqual(value, other.clusterChain[index] as IdCluster)) { return false; } } diff --git a/packages/runtime/id-compressor/src/test/appendOnlySortedMap.perf.spec.ts b/packages/runtime/id-compressor/src/test/appendOnlySortedMap.perf.spec.ts index 2d2c2f3bb4f9..6ab99312e652 100644 --- a/packages/runtime/id-compressor/src/test/appendOnlySortedMap.perf.spec.ts +++ b/packages/runtime/id-compressor/src/test/appendOnlySortedMap.perf.spec.ts @@ -9,13 +9,15 @@ import { BenchmarkType, benchmark } from "@fluid-tools/benchmark"; import { AppendOnlySortedMap } from "../appendOnlySortedMap.js"; import { compareFiniteNumbers } from "../utilities.js"; -function runAppendOnlyMapPerfTests(mapBuilder: () => AppendOnlySortedMap) { +function runAppendOnlyMapPerfTests( + mapBuilder: () => AppendOnlySortedMap, +): void { const type = BenchmarkType.Measurement; let map: AppendOnlySortedMap; let rand: IRandom; const keyChoices: number[] = []; let localChoice = 0; - const before = () => { + const before = (): void => { rand = makeRandom(42); map = mapBuilder(); let curKey = 0; diff --git a/packages/runtime/id-compressor/src/test/appendOnlySortedMap.spec.ts b/packages/runtime/id-compressor/src/test/appendOnlySortedMap.spec.ts index 2377322083bf..b5e2482d25b2 100644 --- a/packages/runtime/id-compressor/src/test/appendOnlySortedMap.spec.ts +++ b/packages/runtime/id-compressor/src/test/appendOnlySortedMap.spec.ts @@ -5,14 +5,15 @@ /* eslint-disable no-bitwise */ -import { strict as assert } from "assert"; +// eslint-disable-next-line import/no-nodejs-modules +import { strict as assert } from "node:assert"; import { AppendOnlySortedMap } from "../appendOnlySortedMap.js"; import { compareFiniteNumbers } from "../utilities.js"; import { assertNotUndefined } from "./testCommon.js"; -function runAppendOnlyMapTests(mapBuilder: () => AppendOnlySortedMap) { +function runAppendOnlyMapTests(mapBuilder: () => AppendOnlySortedMap): void { it("detects out-of-order keys", () => { const map = mapBuilder(); map.append(0, 0); @@ -74,7 +75,7 @@ function runAppendOnlyMapTests(mapBuilder: () => AppendOnlySortedMap { - [99, 100].forEach((elementCount) => { + for (const elementCount of [99, 100]) { const map = mapBuilder(); for (let i = 0; i < elementCount; i++) { map.append(i * 2, i * 2); @@ -86,11 +87,11 @@ function runAppendOnlyMapTests(mapBuilder: () => AppendOnlySortedMap { - [99, 100].forEach((elementCount) => { + for (const elementCount of [99, 100]) { const map = mapBuilder(); for (let i = 0; i < elementCount; i++) { map.append(i * 2, i * 2); @@ -102,7 +103,7 @@ function runAppendOnlyMapTests(mapBuilder: () => AppendOnlySortedMap { diff --git a/packages/runtime/id-compressor/src/test/idCompressor.perf.spec.ts b/packages/runtime/id-compressor/src/test/idCompressor.perf.spec.ts index 2b786aaca646..79042e514e11 100644 --- a/packages/runtime/id-compressor/src/test/idCompressor.perf.spec.ts +++ b/packages/runtime/id-compressor/src/test/idCompressor.perf.spec.ts @@ -94,17 +94,18 @@ describe("IdCompressor Perf", () => { const log = network.getIdLog(client); for (let i = log.length - 1; i > 0; i--) { const { id, originatingClient } = log[i]; - if (originatingClient === client) { - if ((eagerFinal && isFinalId(id)) || (!eagerFinal && isLocalId(id))) { - assert(eagerFinal === isFinalId(id), "Not local/final as requested."); - return id; - } + if ( + originatingClient === client && + ((eagerFinal && isFinalId(id)) || (!eagerFinal && isLocalId(id))) + ) { + assert(eagerFinal === isFinalId(id), "Not local/final as requested."); + return id; } } fail("no ID found in log"); } - function benchmarkWithFlag(creator: (flag: boolean) => void) { + function benchmarkWithFlag(creator: (flag: boolean) => void): void { for (const flag of [true, false]) { creator(flag); } @@ -235,7 +236,7 @@ describe("IdCompressor Perf", () => { for (let clusterCount = 0; clusterCount < 5; clusterCount++) { network.allocateAndSendIds( localClient, - // eslint-disable-next-line @typescript-eslint/dot-notation + // eslint-disable-next-line @typescript-eslint/dot-notation, @typescript-eslint/no-unsafe-argument perfCompressor["nextRequestedClusterSize"], ); network.allocateAndSendIds( diff --git a/packages/runtime/id-compressor/src/test/idCompressor.spec.ts b/packages/runtime/id-compressor/src/test/idCompressor.spec.ts index acc2e659efe3..47b9a1cfb11a 100644 --- a/packages/runtime/id-compressor/src/test/idCompressor.spec.ts +++ b/packages/runtime/id-compressor/src/test/idCompressor.spec.ts @@ -3,7 +3,8 @@ * Licensed under the MIT License. */ -import { strict as assert } from "assert"; +/* eslint-disable import/no-nodejs-modules */ +import { strict as assert } from "node:assert"; import { bufferToString, stringToBuffer } from "@fluid-internal/client-utils"; import { take } from "@fluid-private/stochastic-test-utils"; @@ -229,10 +230,10 @@ describe("IdCompressor", () => { // Assert everything is unique and consistent. const ids = new Set(); const uuids = new Set(); - [id1_1, id1_2, id2_1, id2_2, id3_1, id3_2, id4_1, id4_2].forEach((id) => { + for (const id of [id1_1, id1_2, id2_1, id2_2, id3_1, id3_2, id4_1, id4_2]) { ids.add(id); uuids.add(compressor.decompress(id)); - }); + } assert.equal(ids.size, 8); assert.equal(uuids.size, 8); }); @@ -274,7 +275,7 @@ describe("IdCompressor", () => { // All generated IDs should have aligned finals (even though range3 has not been finalized) const allIds: SessionSpaceCompressedId[] = [id1_1, id1_2, id2_1, id2_2, id2_3, id3_1]; - allIds.forEach((id) => assert(isFinalId(compressor.normalizeToOpSpace(id)))); + for (const id of allIds) assert(isFinalId(compressor.normalizeToOpSpace(id))); compressor.finalizeCreationRange(range3); @@ -286,10 +287,10 @@ describe("IdCompressor", () => { // Assert everything is unique and consistent. const ids = new Set(); const uuids = new Set(); - allIds.forEach((id) => { + for (const id of allIds) { ids.add(id); uuids.add(compressor.decompress(id)); - }); + } assert.equal(ids.size, 7); assert.equal(uuids.size, 7); }); @@ -321,18 +322,18 @@ describe("IdCompressor", () => { { title: "with more IDs than fit in a cluster", idCount: clusterSize * 2 }, ]; - tests.forEach(({ title, idCount }) => { + for (const { title, idCount } of tests) { it(title, () => { const compressor = CompressorFactory.createCompressor(Client.Client1); generateCompressedIds(compressor, idCount); const range = compressor.takeNextCreationRange(); - if (range.ids !== undefined) { - assert.equal(range.ids.count, idCount); - } else { + if (range.ids === undefined) { assert.equal(idCount, 0); + } else { + assert.equal(range.ids.count, idCount); } }); - }); + } it("with the correct local ranges", () => { const compressor = CompressorFactory.createCompressor(Client.Client1, 1); @@ -357,6 +358,7 @@ describe("IdCompressor", () => { assert.deepEqual(ids3, [3, -5]); assert.deepEqual(range3.ids?.localIdRanges, [[5, 1]]); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any (range3 as any).ids.requestedClusterSize = 4; const ids4 = generateCompressedIds(compressor, 2); compressor.finalizeCreationRange(range3); @@ -470,7 +472,9 @@ describe("IdCompressor", () => { () => compressor.finalizeCreationRange(batchRange), (e: Error) => e.message === "Ranges finalized out of order" && + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any (e as any).expectedStart === -4 && + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any (e as any).actualStart === -1, ); }); @@ -485,7 +489,9 @@ describe("IdCompressor", () => { () => compressor.finalizeCreationRange(secondRange), (e: Error) => e.message === "Ranges finalized out of order" && + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any (e as any).expectedStart === -1 && + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any (e as any).actualStart === -2, ); }); @@ -500,9 +506,9 @@ describe("IdCompressor", () => { } compressor.finalizeCreationRange(compressor.takeNextCreationRange()); const opIds = new Set(); - ids.forEach((id) => opIds.add(compressor.normalizeToOpSpace(id))); + for (const id of ids) opIds.add(compressor.normalizeToOpSpace(id)); assert.equal(ids.size, opIds.size); - opIds.forEach((id) => assert.equal(isFinalId(id), true)); + for (const id of opIds) assert.equal(isFinalId(id), true); } } }); @@ -957,7 +963,7 @@ describe("IdCompressor", () => { const base64Content = compressor.serialize(false); const floatView = new Float64Array(stringToBuffer(base64Content, "base64")); // Change the version to 1.0 - floatView[0] = 1.0; + floatView[0] = 1; const docString1 = bufferToString( floatView.buffer, "base64", @@ -1086,7 +1092,7 @@ describe("IdCompressor", () => { const opSpaceIds = network.allocateAndSendIds(Client.Client1, 1); // Mimic sending a reference to an ID that hasn't been acked yet, such as in a slow network const id = opSpaceIds[0]; - const getSessionNormalizedId = () => + const getSessionNormalizedId = (): SessionSpaceCompressedId => compressor2.normalizeToSessionSpace(id, compressor1.localSessionId); assert.throws( getSessionNormalizedId, @@ -1142,48 +1148,43 @@ describe("IdCompressor", () => { // Client 1 makes three IDs network.allocateAndSendIds(Client.Client1, 3); - network.getIdLog(Client.Client1).forEach(({ id }) => assert(isLocalId(id))); + for (const { id } of network.getIdLog(Client.Client1)) assert(isLocalId(id)); // Client 1's IDs have not been acked so have no op space equivalent - network - .getIdLog(Client.Client1) - .forEach((idData) => assert(isLocalId(compressor1.normalizeToOpSpace(idData.id)))); + for (const idData of network.getIdLog(Client.Client1)) + assert(isLocalId(compressor1.normalizeToOpSpace(idData.id))); // Client 1's IDs are acked network.deliverOperations(Client.Client1); - network.getIdLog(Client.Client1).forEach(({ id }) => assert(isLocalId(id))); + for (const { id } of network.getIdLog(Client.Client1)) assert(isLocalId(id)); // Client 2 makes three IDs network.allocateAndSendIds(Client.Client2, 3); - network.getIdLog(Client.Client2).forEach(({ id }) => assert(isLocalId(id))); + for (const { id } of network.getIdLog(Client.Client2)) assert(isLocalId(id)); // Client 1 receives Client 2's IDs network.deliverOperations(Client.Client1); - network - .getIdLog(Client.Client1) - .slice(-3) - .forEach(({ id }) => assert(isFinalId(id))); + for (const { id } of network.getIdLog(Client.Client1).slice(-3)) assert(isFinalId(id)); // All IDs have been acked or are from another client, and therefore have a final form in op space - network - .getIdLog(Client.Client1) - .forEach(({ id }) => assert(isFinalId(compressor1.normalizeToOpSpace(id)))); + for (const { id } of network.getIdLog(Client.Client1)) + assert(isFinalId(compressor1.normalizeToOpSpace(id))); // Compression should preserve ID space correctness - network.getIdLog(Client.Client1).forEach((idData) => { + for (const idData of network.getIdLog(Client.Client1)) { const roundtripped = compressor1.recompress(compressor1.decompress(idData.id)); assert.equal(Math.sign(roundtripped), Math.sign(idData.id)); - }); + } - network.getIdLog(Client.Client1).forEach((idData) => { + for (const idData of network.getIdLog(Client.Client1)) { const opNormalized = compressor1.normalizeToOpSpace(idData.id); assert.equal( Math.sign(compressor1.normalizeToSessionSpace(opNormalized, idData.sessionId)), Math.sign(idData.id), ); - }); + } }); itNetwork("produces consistent IDs with large fuzz input", (network) => { @@ -1222,7 +1223,8 @@ describe("IdCompressor", () => { }); itNetwork("can finalize a range when the current cluster is full", 5, (network) => { - const clusterCapacity = network.getCompressor( + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const clusterCapacity: number = network.getCompressor( Client.Client1, // eslint-disable-next-line @typescript-eslint/dot-notation )["nextRequestedClusterSize"]; @@ -1233,7 +1235,8 @@ describe("IdCompressor", () => { }); itNetwork("can finalize a range that spans multiple clusters", 5, (network) => { - const clusterCapacity = network.getCompressor( + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const clusterCapacity: number = network.getCompressor( Client.Client1, // eslint-disable-next-line @typescript-eslint/dot-notation )["nextRequestedClusterSize"]; @@ -1314,7 +1317,8 @@ describe("IdCompressor", () => { "can resume a session and interact with multiple other clients", 3, (network) => { - const clusterSize = network.getCompressor( + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const clusterSize: number = network.getCompressor( Client.Client1, // eslint-disable-next-line @typescript-eslint/dot-notation )["nextRequestedClusterSize"]; @@ -1389,7 +1393,7 @@ function createNetworkTestFunction( function describeNetwork( title: string, its: (itFunc: NetworkTestFunction & NetworkTestFunctionWithCapacity) => void, -) { +): void { describe(title, () => { its(createNetworkTestFunction(false)); }); diff --git a/packages/runtime/id-compressor/src/test/idCompressorTestUtilities.ts b/packages/runtime/id-compressor/src/test/idCompressorTestUtilities.ts index b8f82d83f082..83e1cf3d46c3 100644 --- a/packages/runtime/id-compressor/src/test/idCompressorTestUtilities.ts +++ b/packages/runtime/id-compressor/src/test/idCompressorTestUtilities.ts @@ -3,7 +3,8 @@ * Licensed under the MIT License. */ -import { strict as assert } from "assert"; +// eslint-disable-next-line import/no-nodejs-modules +import { strict as assert } from "node:assert"; import { BaseFuzzTestState, @@ -51,19 +52,25 @@ export interface ClosedMap extends Omit, "delete" | "clear"> { get(key: K): V; } -/** Identifies a compressor in a network */ +/** + * Identifies a compressor in a network + */ export enum Client { Client1 = "Client1", Client2 = "Client2", Client3 = "Client3", } -/** Identifies categories of compressors */ +/** + * Identifies categories of compressors + */ export enum MetaClient { All = "All", } -/** Identifies a compressor inside the network but outside the three specially tracked clients. */ +/** + * Identifies a compressor inside the network but outside the three specially tracked clients. + */ export enum OutsideClient { Remote = "Remote", } @@ -75,7 +82,9 @@ export enum OutsideClient { export type OriginatingClient = Client | OutsideClient; export const OriginatingClient = { ...Client, ...OutsideClient }; -/** Identifies a compressor to which to send an operation */ +/** + * Identifies a compressor to which to send an operation + */ export type DestinationClient = Client | MetaClient; export const DestinationClient = { ...Client, ...MetaClient }; @@ -171,7 +180,9 @@ function makeSessionIds(): ClientMap { */ export const sessionIds = makeSessionIds(); -/** Information about a generated ID in a network to be validated by tests */ +/** + * Information about a generated ID in a network to be validated by tests + */ export interface TestIdData { readonly id: SessionSpaceCompressedId; readonly originatingClient: OriginatingClient; @@ -184,20 +195,30 @@ export interface TestIdData { * Not suitable for performance testing. */ export class IdCompressorTestNetwork { - /** The compressors used in this network */ + /** + * The compressors used in this network + */ private readonly compressors: ClientMap; - /** The log of operations seen by the server so far. Append-only. */ + /** + * The log of operations seen by the server so far. Append-only. + */ private readonly serverOperations: [ creationRange: IdCreationRange, opSpaceIds: OpSpaceCompressedId[], clientFrom: OriginatingClient, sessionIdFrom: SessionId, ][] = []; - /** An index into `serverOperations` for each client which represents how many operations have been delivered to that client */ + /** + * An index into `serverOperations` for each client which represents how many operations have been delivered to that client + */ private readonly clientProgress: ClientMap; - /** All ids (local and sequenced) that a client has created or received, in order. */ + /** + * All ids (local and sequenced) that a client has created or received, in order. + */ private readonly idLogs: ClientMap; - /** All ids that a client has received from the server, in order. */ + /** + * All ids that a client has received from the server, in order. + */ private readonly sequencedIdLogs: ClientMap; public constructor(public readonly initialClusterSize = 5) { @@ -341,6 +362,7 @@ export class IdCompressorTestNetwork { ids: { firstGenCount: 1, count: numIds, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment requestedClusterSize: this.getCompressor(Client.Client1)[ // eslint-disable-next-line @typescript-eslint/dot-notation "nextRequestedClusterSize" @@ -391,9 +413,9 @@ export class IdCompressorTestNetwork { opIndexBound = this.serverOperations.length; } else { opIndexBound = - opsToDeliver !== undefined - ? this.clientProgress.get(clientTakingDelivery) + opsToDeliver - : this.serverOperations.length; + opsToDeliver === undefined + ? this.serverOperations.length + : this.clientProgress.get(clientTakingDelivery) + opsToDeliver; } for (const [clientTo, compressorTo] of this.getTargetCompressors(clientTakingDelivery)) { for (let i = this.clientProgress.get(clientTo); i < opIndexBound; i++) { @@ -452,38 +474,38 @@ export class IdCompressorTestNetwork { }; // Ensure creation ranges for clients we track contain the correct local ID ranges - this.serverOperations.forEach(([range, opSpaceIds, clientFrom]) => { + for (const [range, opSpaceIds, clientFrom] of this.serverOperations) { if (clientFrom !== OriginatingClient.Remote) { const localIdsInCreationRange = getLocalIdsInRange(range, opSpaceIds); let localCount = 0; - opSpaceIds.forEach((id) => { + for (const id of opSpaceIds) { if (isLocalId(id)) { localCount++; assert(localIdsInCreationRange.has(id), "Local ID not in creation range"); } - }); + } assert.strictEqual( localCount, localIdsInCreationRange.size, "Local ID count mismatch", ); } - }); + } const undeliveredRanges = new Map(); - this.clientProgress.forEach((progress, client) => { + for (const [client, progress] of this.clientProgress.entries()) { const ranges = this.serverOperations .slice(progress) .filter((op) => op[2] === client) .map(([range]) => range); undeliveredRanges.set(client, ranges); - }); - undeliveredRanges.forEach((ranges, client) => { + } + for (const [client, ranges] of undeliveredRanges.entries()) { const compressor = this.compressors.get(client); let firstGenCount: number | undefined; let totalCount = 0; const unionedLocalRanges = new SessionSpaceNormalizer(); - ranges.forEach((range) => { + for (const range of ranges) { assert(range.sessionId === compressor.localSessionId); if (range.ids !== undefined) { // initialize firstGenCount if not set @@ -491,18 +513,21 @@ export class IdCompressorTestNetwork { firstGenCount = range.ids.firstGenCount; } totalCount += range.ids.count; - range.ids.localIdRanges.forEach(([genCount, count]) => { + for (const [genCount, count] of range.ids.localIdRanges) { unionedLocalRanges.addLocalRange(genCount, count); - }); + } } - }); + } const retakenRange = compressor.takeUnfinalizedCreationRange(); - if (retakenRange.ids !== undefined) { + if (retakenRange.ids === undefined) { + assert.strictEqual(totalCount, 0); + assert.strictEqual(unionedLocalRanges.idRanges.size, 0); + } else { const retakenLocalIds = new SessionSpaceNormalizer(); - retakenRange.ids.localIdRanges.forEach(([genCount, count]) => { + for (const [genCount, count] of retakenRange.ids.localIdRanges) { retakenLocalIds.addLocalRange(genCount, count); - }); + } assert.strictEqual( retakenLocalIds.equals(unionedLocalRanges), true, @@ -510,11 +535,8 @@ export class IdCompressorTestNetwork { ); assert.strictEqual(retakenRange.ids.count, totalCount, "Count mismatch"); assert.strictEqual(retakenRange.ids.firstGenCount, firstGenCount, "Count mismatch"); - } else { - assert.strictEqual(totalCount, 0); - assert.strictEqual(unionedLocalRanges.idRanges.size, 0); } - }); + } // First, ensure all clients each generated a unique ID for each of their own calls to generate. for (const [compressor, ids] of sequencedLogs) { @@ -528,6 +550,7 @@ export class IdCompressorTestNetwork { const maxLogLength = sequencedLogs .map(([_, data]) => data.length) + // eslint-disable-next-line unicorn/no-array-reduce .reduce((p, n) => Math.max(p, n)); function getNextLogWithEntryAt(logsIndex: number, entryIndex: number): number | undefined { @@ -680,8 +703,8 @@ export function roundtrip( ] { // preserve the capacity request as this property is normally private and resets // to a default on construction (deserialization) - // eslint-disable-next-line @typescript-eslint/dot-notation - const capacity = compressor["nextRequestedClusterSize"]; + // eslint-disable-next-line @typescript-eslint/dot-notation, @typescript-eslint/no-unsafe-assignment + const capacity: number = compressor["nextRequestedClusterSize"]; if (withSession) { const serialized = compressor.serialize(withSession); const roundtripped = IdCompressor.deserialize(serialized); @@ -732,10 +755,10 @@ export function mergeArrayMaps( ): Pick, "get" | "set"> { for (const [key, value] of from.entries()) { const entry = to.get(key); - if (entry !== undefined) { - entry.push(...value); - } else { + if (entry === undefined) { to.set(key, [...value]); + } else { + entry.push(...value); } } return to; @@ -796,11 +819,17 @@ interface FuzzTestState extends BaseFuzzTestState { } export interface OperationGenerationConfig { - /** maximum cluster size of the network. Default: 25 */ + /** + * maximum cluster size of the network. Default: 25 + */ maxClusterSize?: number; - /** Number of ops between validation ops. Default: 200 */ + /** + * Number of ops between validation ops. Default: 200 + */ validateInterval?: number; - /** Fraction of ID allocations that are from an outside client (not Client1/2/3). */ + /** + * Fraction of ID allocations that are from an outside client (not Client1/2/3). + */ outsideAllocationFraction?: number; } diff --git a/packages/runtime/id-compressor/src/test/numericUuid.spec.ts b/packages/runtime/id-compressor/src/test/numericUuid.spec.ts index 1954c5a76205..c75fecaa8cf9 100644 --- a/packages/runtime/id-compressor/src/test/numericUuid.spec.ts +++ b/packages/runtime/id-compressor/src/test/numericUuid.spec.ts @@ -3,7 +3,8 @@ * Licensed under the MIT License. */ -import { strict as assert } from "assert"; +// eslint-disable-next-line import/no-nodejs-modules +import { strict as assert } from "node:assert"; import { StableId } from "../index.js"; import { readNumericUuid, writeNumericUuid } from "../persistanceUtilities.js"; diff --git a/packages/runtime/id-compressor/src/test/sessionSpaceNormalizer.spec.ts b/packages/runtime/id-compressor/src/test/sessionSpaceNormalizer.spec.ts index 46028bf1c416..5b737c50357a 100644 --- a/packages/runtime/id-compressor/src/test/sessionSpaceNormalizer.spec.ts +++ b/packages/runtime/id-compressor/src/test/sessionSpaceNormalizer.spec.ts @@ -3,7 +3,8 @@ * Licensed under the MIT License. */ -import { strict as assert } from "assert"; +/* eslint-disable import/no-nodejs-modules */ +import { strict as assert } from "node:assert"; import { SessionSpaceNormalizer } from "../sessionSpaceNormalizer.js"; diff --git a/packages/runtime/id-compressor/src/test/snapshots/summary.spec.ts b/packages/runtime/id-compressor/src/test/snapshots/summary.spec.ts index 1e2673770868..4fa1944d03f6 100644 --- a/packages/runtime/id-compressor/src/test/snapshots/summary.spec.ts +++ b/packages/runtime/id-compressor/src/test/snapshots/summary.spec.ts @@ -5,9 +5,9 @@ /* eslint-disable import/no-nodejs-modules */ -import { strict as assert } from "assert"; -import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs"; -import path from "path"; +import { strict as assert } from "node:assert"; +import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import path from "node:path"; import { SessionId, createIdCompressor } from "../../index.js"; @@ -77,7 +77,7 @@ function takeSnapshot(data: string, suffix: string = ""): void { writeFileSync(fullFile, data); } else { assert(exists, `test snapshot file does not exist: "${fullFile}"`); - const pastData = readFileSync(fullFile, "utf-8"); + const pastData = readFileSync(fullFile, "utf8"); assert.equal(data, pastData, `snapshot different for "${currentTestName}"`); } } diff --git a/packages/runtime/id-compressor/src/test/testCommon.ts b/packages/runtime/id-compressor/src/test/testCommon.ts index 60e624d831c0..4af38ddfb281 100644 --- a/packages/runtime/id-compressor/src/test/testCommon.ts +++ b/packages/runtime/id-compressor/src/test/testCommon.ts @@ -44,6 +44,7 @@ export type LocalCompressedId = number & { } & SessionSpaceCompressedId; // Same brand as CompressedId, as local IDs are always locally normalized /** + * Returns true if the supplied ID is a final ID. * @returns true if the supplied ID is a final ID. */ export function isFinalId(id: CompressedId): id is FinalCompressedId { @@ -51,6 +52,7 @@ export function isFinalId(id: CompressedId): id is FinalCompressedId { } /** + * Returns true if the supplied ID is a local ID. * @returns true if the supplied ID is a local ID. */ export function isLocalId(id: CompressedId): id is LocalCompressedId { @@ -91,7 +93,9 @@ export function incrementStableId(stableId: StableId, offset: number): StableId return stableIdFromNumericUuid(offsetNumericUuid(numericUuidFromStableId(stableId), offset)); } -/** An immutable view of an `IdCompressor` */ +/** + * An immutable view of an `IdCompressor` + */ export type ReadonlyIdCompressor = Omit< IdCompressor, | "generateCompressedId" diff --git a/packages/runtime/id-compressor/src/utilities.ts b/packages/runtime/id-compressor/src/utilities.ts index b41ec962ec68..66f570ab0348 100644 --- a/packages/runtime/id-compressor/src/utilities.ts +++ b/packages/runtime/id-compressor/src/utilities.ts @@ -10,7 +10,7 @@ import { v4 } from "uuid"; import { LocalCompressedId, NumericUuid } from "./identifiers.js"; import { SessionId, StableId } from "./types/index.js"; -const hexadecimalCharCodes = Array.from("09afAF").map((c) => c.charCodeAt(0)) as [ +const hexadecimalCharCodes = [..."09afAF"].map((c) => c.codePointAt(0)) as [ zero: number, nine: number, a: number, @@ -76,17 +76,19 @@ export function isStableId(str: string): str is StableId { case 8: case 13: case 18: - case 23: + case 23: { if (str.charAt(i) !== "-") { return false; } break; + } - case 14: + case 14: { if (str.charAt(i) !== "4") { return false; } break; + } case 19: { const char = str.charAt(i); @@ -96,11 +98,14 @@ export function isStableId(str: string): str is StableId { break; } - default: - if (!isHexadecimalCharacter(str.charCodeAt(i))) { + default: { + const codePoint = str.codePointAt(i); + assert(codePoint !== undefined, "Unexpected undefined code point"); + if (!isHexadecimalCharacter(codePoint)) { return false; } break; + } } } @@ -172,10 +177,10 @@ export function stableIdFromNumericUuid(numericUuid: NumericUuid): StableId { const uuidU128 = upperMasked | versionMask | middieBittiesMasked | variantMask | lowerMasked; // Pad to 32 characters, inserting leading zeroes if needed const uuidString = uuidU128.toString(16).padStart(32, "0"); - return `${uuidString.substring(0, 8)}-${uuidString.substring(8, 12)}-${uuidString.substring( + return `${uuidString.slice(0, 8)}-${uuidString.slice(8, 12)}-${uuidString.slice( 12, 16, - )}-${uuidString.substring(16, 20)}-${uuidString.substring(20, 32)}` as StableId; + )}-${uuidString.slice(16, 20)}-${uuidString.slice(20, 32)}` as StableId; } export function offsetNumericUuid(numericUuid: NumericUuid, offset: number): NumericUuid {