diff --git a/packages/dds/ordered-collection/src/consensusQueue.ts b/packages/dds/ordered-collection/src/consensusQueue.ts index 006ff51e02b7..7d8362d59002 100644 --- a/packages/dds/ordered-collection/src/consensusQueue.ts +++ b/packages/dds/ordered-collection/src/consensusQueue.ts @@ -25,7 +25,8 @@ class SnapshotableQueue extends SnapshotableArray implements IOrderedColle if (this.size() === 0) { throw new Error("SnapshotableQueue is empty"); } - return this.data.shift() as T; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return this.data.shift()!.data; } } diff --git a/packages/dds/ordered-collection/src/snapshotableArray.ts b/packages/dds/ordered-collection/src/snapshotableArray.ts index 687e9406525e..e33a9a112891 100644 --- a/packages/dds/ordered-collection/src/snapshotableArray.ts +++ b/packages/dds/ordered-collection/src/snapshotableArray.ts @@ -3,18 +3,32 @@ * Licensed under the MIT License. */ -import { assert } from "@fluidframework/core-utils/internal"; +import { assert, DoublyLinkedList } from "@fluidframework/core-utils/internal"; -export class SnapshotableArray extends Array { - protected data: T[] = []; +/** + * Base class for a snapshotable, ordered collection. + * + * Note: the historical "Array" suffix predates the current `DoublyLinkedList` backing — the + * collection is no longer backed by (and does not extend) a JS Array. The name is retained to + * avoid a broader rename across consumers; iteration order is still the contract that callers + * (notably the snapshot path via {@link asArray}) rely on. + */ +export class SnapshotableArray { + protected data: DoublyLinkedList = new DoublyLinkedList(); public asArray(): T[] { - return this.data; + const result: T[] = []; + for (const node of this.data) { + result.push(node.data); + } + return result; } public async loadFrom(from: T[]): Promise { assert(this.data.length === 0, 0x06b /* "Loading snapshot into a non-empty collection" */); - this.data = from; + for (const value of from) { + this.data.push(value); + } } public size(): number { diff --git a/packages/dds/ordered-collection/src/test/snapshotableArray.spec.ts b/packages/dds/ordered-collection/src/test/snapshotableArray.spec.ts new file mode 100644 index 000000000000..16cb2395276a --- /dev/null +++ b/packages/dds/ordered-collection/src/test/snapshotableArray.spec.ts @@ -0,0 +1,34 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import { strict as assert } from "node:assert"; + +import type { IOrderedCollection } from "../interfaces.js"; +import { SnapshotableArray } from "../snapshotableArray.js"; + +/** + * Minimal queue subclass mirroring the in-tree `SnapshotableQueue` so we can exercise + * removal against the protected `data` list without depending on the full + * ConsensusQueue runtime plumbing. + */ +class TestQueue extends SnapshotableArray implements IOrderedCollection { + public add(value: T): void { + this.data.push(value); + } + public remove(): T { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return this.data.shift()!.data; + } +} + +describe("SnapshotableArray", () => { + it("asArray() reflects iteration order after loadFrom + remove (snapshot contract)", async () => { + const queue = new TestQueue(); + await queue.loadFrom(["a", "b", "c"]); + queue.remove(); + assert.deepEqual(queue.asArray(), ["b", "c"]); + assert.equal(queue.size(), 2); + }); +});