Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/dds/ordered-collection/src/consensusQueue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ class SnapshotableQueue<T> extends SnapshotableArray<T> 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;
}
}

Expand Down
24 changes: 19 additions & 5 deletions packages/dds/ordered-collection/src/snapshotableArray.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> 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<T> {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deep Review: The class name SnapshotableArray was chosen in #1395 (vladsud, 2020-03-05) specifically because it was an Array subclass. This PR removes the extends Array — the PR description acknowledges it ("SnapshotableArray no longer extends Array — it is now a plain class") but the name stays. Both blind solution-space proposers independently expected the class to still be array-backed, so a future reader is likely to hit the same surprise.

The symbol is internal to the package (not exported via api-report; only consumer is SnapshotableQueue in consensusQueue.ts), so renaming in-PR is cheap — e.g. SnapshotableCollection or SnapshotableList. If the rename is deferred, a one-line doc comment noting the legacy name predates the linked-list backing would save the next reader from re-deriving "why is this called Array?".

protected data: DoublyLinkedList<T> = new DoublyLinkedList<T>();

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<void> {
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);
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deep Review: asArray() changes from return this.data (alias) to materializing a fresh T[] by iterating the list; loadFrom changes from this.data = from (alias) to per-element push. Both surfaces newly depend on DoublyLinkedList iteration order matching insertion order, and that's the sole load-bearing invariant in this area — the snapshot wire format. #1395 has no inline review history on these methods and no targeted unit test locks the contract.

There is strong transitive coverage: DoublyLinkedList iteration order is unit-tested (list.spec.ts:50-57, :71-80) and the ordered-collection fuzz model compares asArray() across clients (fuzzUtils.ts:150-159, :182-196). That lowers — but does not erase — the value of a local lock.

Suggest one ~10-line unit test in this PR: loadFrom([a,b,c])remove() → assert asArray() returns [b,c] and size() === 2. Locks the new iteration-based snapshot contract against future DoublyLinkedList or refactor regressions, and directly substantiates the "byte-identical wire format" claim. Alternatively, if the existing fuzz consistency check (fuzzUtils.ts:150-196) already exercises a loadFrom → partial remove()asArray() cycle after a snapshot/load, calling that out in the PR description is sufficient.

}

public size(): number {
Expand Down
34 changes: 34 additions & 0 deletions packages/dds/ordered-collection/src/test/snapshotableArray.spec.ts
Original file line number Diff line number Diff line change
@@ -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<T> extends SnapshotableArray<T> implements IOrderedCollection<T> {
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<string>();
await queue.loadFrom(["a", "b", "c"]);
queue.remove();
assert.deepEqual(queue.asArray(), ["b", "c"]);
assert.equal(queue.size(), 2);
});
});
Loading