From 2635b9302e6ad9ad3e90ab5cdae0d12fceffa6b5 Mon Sep 17 00:00:00 2001 From: Tony Murphy Date: Wed, 27 May 2026 15:42:05 -0700 Subject: [PATCH 1/2] perf(ordered-collection): back SnapshotableQueue with DoublyLinkedList --- .../dds/ordered-collection/src/consensusQueue.ts | 3 ++- .../ordered-collection/src/snapshotableArray.ts | 16 +++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) 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..1fd0c130b12b 100644 --- a/packages/dds/ordered-collection/src/snapshotableArray.ts +++ b/packages/dds/ordered-collection/src/snapshotableArray.ts @@ -3,18 +3,24 @@ * 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[] = []; +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 { From 4fd6d364b715746ef7d8573b469161fb0ce63a6b Mon Sep 17 00:00:00 2001 From: Tony Murphy Date: Wed, 27 May 2026 17:18:40 -0700 Subject: [PATCH 2/2] test(ordered-collection): lock snapshot iteration contract + name doc --- .../src/snapshotableArray.ts | 8 +++++ .../src/test/snapshotableArray.spec.ts | 34 +++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 packages/dds/ordered-collection/src/test/snapshotableArray.spec.ts diff --git a/packages/dds/ordered-collection/src/snapshotableArray.ts b/packages/dds/ordered-collection/src/snapshotableArray.ts index 1fd0c130b12b..e33a9a112891 100644 --- a/packages/dds/ordered-collection/src/snapshotableArray.ts +++ b/packages/dds/ordered-collection/src/snapshotableArray.ts @@ -5,6 +5,14 @@ import { assert, DoublyLinkedList } from "@fluidframework/core-utils/internal"; +/** + * 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(); 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); + }); +});