From 0bbdda953a8dbdc9def066eed8eab186599df4f8 Mon Sep 17 00:00:00 2001 From: brrichards Date: Fri, 17 Apr 2026 19:28:28 +0000 Subject: [PATCH 1/5] basicChunkCursor bug fix --- .../chunked-forest/basicChunk.ts | 39 ++++++--- .../chunked-forest/basicChunk.spec.ts | 84 ++++++++++++++++++- 2 files changed, 108 insertions(+), 15 deletions(-) diff --git a/packages/dds/tree/src/feature-libraries/chunked-forest/basicChunk.ts b/packages/dds/tree/src/feature-libraries/chunked-forest/basicChunk.ts index 2ed31df86b6f..abb8cec27fb7 100644 --- a/packages/dds/tree/src/feature-libraries/chunked-forest/basicChunk.ts +++ b/packages/dds/tree/src/feature-libraries/chunked-forest/basicChunk.ts @@ -169,7 +169,7 @@ export class BasicChunkCursor extends SynchronousCursor implements ChunkedCursor // Compute the number of nodes deep the current depth is. // We want the floor of the result, which can computed using a bitwise shift assuming the depth is less than 2^31, which seems safe. // eslint-disable-next-line no-bitwise - const halfHeight = (this.siblingStack.length + 1) >> 1; + const halfHeight = this.siblingStack.length >> 1; assert( this.indexOfChunkStack.length === halfHeight, 0x51c /* unexpected indexOfChunkStack */, @@ -203,8 +203,15 @@ export class BasicChunkCursor extends SynchronousCursor implements ChunkedCursor return this.indexStack[height] ?? oob(); } - private getStackedNode(height: number): BasicChunk { - const index = this.getStackedNodeIndex(height); + private getStackedChunkIndex(height: number): number { + assert(height % 2 === 1, "must be node height"); + assert(height >= 0, "must not be above root"); + // eslint-disable-next-line no-bitwise + return this.indexOfChunkStack[height >> 1] ?? oob(); + } + + private getStackedChunk(height: number): BasicChunk { + const index = this.getStackedChunkIndex(height); return (this.siblingStack[height] as readonly TreeChunk[])[index] as BasicChunk; } @@ -322,6 +329,11 @@ export class BasicChunkCursor extends SynchronousCursor implements ChunkedCursor assert(this.mode === CursorLocationType.Nodes, 0x528 /* must be in nodes mode */); this.siblingStack.push(this.siblings); this.indexStack.push(this.index); + // Save the chunk array position of the current node. When siblings contain + // multi node chunks, the flat node index diverges from the array position, + // so getField needs this to locate the parent in the sibling array. + this.indexOfChunkStack.push(this.indexOfChunk); + this.indexWithinChunkStack.push(this.indexWithinChunk); // For fields, siblings are only used for key lookup and // nextField and which has arbitrary iteration order, @@ -355,6 +367,8 @@ export class BasicChunkCursor extends SynchronousCursor implements ChunkedCursor this.siblingStack.push(this.siblings); this.indexStack.push(this.index); + this.indexOfChunkStack.push(this.indexOfChunk); + this.indexWithinChunkStack.push(this.indexWithinChunk); this.index = 0; this.siblings = [...fields.keys()]; // TODO: avoid this copy return true; @@ -422,8 +436,6 @@ export class BasicChunkCursor extends SynchronousCursor implements ChunkedCursor } this.siblingStack.push(this.siblings); this.indexStack.push(this.index); - this.indexOfChunkStack.push(this.indexOfChunk); - this.indexWithinChunkStack.push(this.indexWithinChunk); this.index = 0; this.siblings = siblings; this.indexOfChunk = 0; @@ -486,6 +498,10 @@ export class BasicChunkCursor extends SynchronousCursor implements ChunkedCursor this.siblings = this.siblingStack.pop() ?? fail(0xaf0 /* Unexpected siblingStack.length */); this.index = this.indexStack.pop() ?? fail(0xaf1 /* Unexpected indexStack.length */); + this.indexOfChunk = + this.indexOfChunkStack.pop() ?? fail("Unexpected indexOfChunkStack.length"); + this.indexWithinChunk = + this.indexWithinChunkStack.pop() ?? fail("Unexpected indexWithinChunkStack.length"); } public exitNode(): void { @@ -502,16 +518,15 @@ export class BasicChunkCursor extends SynchronousCursor implements ChunkedCursor this.siblings = this.siblingStack.pop() ?? fail(0xaf2 /* Unexpected siblingStack.length */); this.index = this.indexStack.pop() ?? fail(0xaf3 /* Unexpected indexStack.length */); - this.indexOfChunk = - this.indexOfChunkStack.pop() ?? fail(0xaf4 /* Unexpected indexOfChunkStack.length */); - this.indexWithinChunk = - this.indexWithinChunkStack.pop() ?? - fail(0xaf5 /* Unexpected indexWithinChunkStack.length */); + // At the Fields level these aren't semantically used, but reset for consistent state + // (so a fully-iterated cursor matches a fresh cursor at the same logical position). + this.indexOfChunk = 0; + this.indexWithinChunk = 0; } private getNode(): BasicChunk { assert(this.mode === CursorLocationType.Nodes, 0x52f /* can only get node when in node */); - return (this.siblings as TreeChunk[])[this.index] as BasicChunk; + return (this.siblings as TreeChunk[])[this.indexOfChunk] as BasicChunk; } private getField(): readonly TreeChunk[] { @@ -522,7 +537,7 @@ export class BasicChunkCursor extends SynchronousCursor implements ChunkedCursor this.mode === CursorLocationType.Fields, 0x530 /* can only get field when in fields */, ); - const parent = this.getStackedNode(this.indexStack.length - 1); + const parent = this.getStackedChunk(this.siblingStack.length - 1); const key: FieldKey = this.getFieldKey(); const field = parent.fields.get(key) ?? []; return field; diff --git a/packages/dds/tree/src/test/feature-libraries/chunked-forest/basicChunk.spec.ts b/packages/dds/tree/src/test/feature-libraries/chunked-forest/basicChunk.spec.ts index 10e1ddca2a2c..32d8bd6e1781 100644 --- a/packages/dds/tree/src/test/feature-libraries/chunked-forest/basicChunk.spec.ts +++ b/packages/dds/tree/src/test/feature-libraries/chunked-forest/basicChunk.spec.ts @@ -7,13 +7,17 @@ import { strict as assert } from "node:assert"; import { EmptyKey, + type FieldKey, type ITreeCursor, type ITreeCursorSynchronous, type JsonableTree, type ChunkedCursor, } from "../../../core/index.js"; -// eslint-disable-next-line import-x/no-internal-modules -import { BasicChunk } from "../../../feature-libraries/chunked-forest/basicChunk.js"; +import { + BasicChunk, + BasicChunkCursor, + // eslint-disable-next-line import-x/no-internal-modules +} from "../../../feature-libraries/chunked-forest/basicChunk.js"; import { basicChunkTree, basicOnlyChunkPolicy, @@ -22,9 +26,14 @@ import { // eslint-disable-next-line import-x/no-internal-modules } from "../../../feature-libraries/chunked-forest/chunkTree.js"; // eslint-disable-next-line import-x/no-internal-modules -import { uniformChunk } from "../../../feature-libraries/chunked-forest/index.js"; +import { dummyRoot, uniformChunk } from "../../../feature-libraries/chunked-forest/index.js"; // eslint-disable-next-line import-x/no-internal-modules import { SequenceChunk } from "../../../feature-libraries/chunked-forest/sequenceChunk.js"; +import { + TreeShape, + UniformChunk, + // eslint-disable-next-line import-x/no-internal-modules +} from "../../../feature-libraries/chunked-forest/uniformChunk.js"; import { type TreeChunk, chunkTree, @@ -155,6 +164,75 @@ describe("basic chunk", () => { assert(!cursor.atChunkRoot()); }); + it("getField resolves parent via chunk array index when preceded by a multi-node chunk", () => { + // A root field with 3 logical nodes split across 2 chunks: + // chunks[0]: UniformChunk with 2 number nodes. + // chunks[1]: BasicChunk holding a single subField leaf. + const numberShape = new TreeShape(brand(numberSchema.identifier), true, []); + const uniform = new UniformChunk(numberShape.withTopLevelLength(2), [10, 20]); + + const subField: FieldKey = brand("subField"); + const subLeaf = new BasicChunk(brand(numberSchema.identifier), new Map(), 42); + const trailingBasic = new BasicChunk(brand("Trailing"), new Map([[subField, [subLeaf]]])); + + const cursor = new BasicChunkCursor( + [uniform, trailingBasic], + // siblingStack, indexStack, indexOfChunkStack, indexWithinChunkStack + [], + [], + [], + [], + [dummyRoot], + // index, indexOfChunk, indexWithinChunk + 0, + 0, + 0, + undefined, + ); + + // Logical node index 2 maps to chunk array index 1 (the trailing BasicChunk). + cursor.enterNode(2); + + // enterField drives getField, which on main looks up the parent at the + // logical node index (2) instead of the chunk array index (1). siblings[2] + // is out of bounds in the 2-chunk array, so the cast returns undefined and + // the subsequent field read throws. + cursor.enterField(subField); + assert.doesNotThrow(() => cursor.getFieldLength()); + }); + + it("getField resolves parent via chunk array index when preceded by two multi-node chunks", () => { + // Root field with 5 logical nodes across 3 chunks: + // chunks[0]: UniformChunk with 2 number nodes. + // chunks[1]: UniformChunk with 2 number nodes. + // chunks[2]: BasicChunk with a single subField leaf. + const numberShape = new TreeShape(brand(numberSchema.identifier), true, []); + const uniformA = new UniformChunk(numberShape.withTopLevelLength(2), [10, 20]); + const uniformB = new UniformChunk(numberShape.withTopLevelLength(2), [30, 40]); + + const subField: FieldKey = brand("subField"); + const subLeaf = new BasicChunk(brand(numberSchema.identifier), new Map(), 99); + const trailingBasic = new BasicChunk(brand("Trailing"), new Map([[subField, [subLeaf]]])); + + const cursor = new BasicChunkCursor( + [uniformA, uniformB, trailingBasic], + [], + [], + [], + [], + [dummyRoot], + 0, + 0, + 0, + undefined, + ); + + // Logical node index 4 maps to chunk array index 2 (the trailing BasicChunk). + cursor.enterNode(4); + cursor.enterField(subField); + assert.doesNotThrow(() => cursor.getFieldLength()); + }); + describe("SequenceChunk", () => { it("root", () => { validateChunkCursor(new SequenceChunk([numericBasicChunk(0)]), numberSequenceField(1)); From 8b79cd2689308fc41d182db2ce83cf98e7f4a290 Mon Sep 17 00:00:00 2001 From: brrichards Date: Mon, 20 Apr 2026 18:10:41 +0000 Subject: [PATCH 2/5] address feedback --- .../chunked-forest/basicChunk.ts | 35 ++++++++++++++----- .../chunked-forest/basicChunk.spec.ts | 33 ++++++++++++----- 2 files changed, 51 insertions(+), 17 deletions(-) diff --git a/packages/dds/tree/src/feature-libraries/chunked-forest/basicChunk.ts b/packages/dds/tree/src/feature-libraries/chunked-forest/basicChunk.ts index abb8cec27fb7..e079eb124e37 100644 --- a/packages/dds/tree/src/feature-libraries/chunked-forest/basicChunk.ts +++ b/packages/dds/tree/src/feature-libraries/chunked-forest/basicChunk.ts @@ -166,10 +166,19 @@ export class BasicChunkCursor extends SynchronousCursor implements ChunkedCursor if (this.nestedCursor !== undefined) { return this.nestedCursor.mode; } - // Compute the number of nodes deep the current depth is. - // We want the floor of the result, which can computed using a bitwise shift assuming the depth is less than 2^31, which seems safe. - // eslint-disable-next-line no-bitwise - const halfHeight = this.siblingStack.length >> 1; + this.assertChunkStacksMatchNodeDepth(); + return this.siblingStack.length % 2 === 0 + ? CursorLocationType.Fields + : CursorLocationType.Nodes; + } + + /** + * Asserts that the node-only stacks (`indexOfChunkStack` and `indexWithinChunkStack`) are in sync with `siblingStack`. + * Since `siblingStack` interleaves field and node levels while the node-only stacks are pushed/popped only on node-level transitions, + * their length should always equal the number of node levels traversed. + */ + private assertChunkStacksMatchNodeDepth(): void { + const halfHeight = this.getNodeOnlyHeightFromHeight(); assert( this.indexOfChunkStack.length === halfHeight, 0x51c /* unexpected indexOfChunkStack */, @@ -178,9 +187,6 @@ export class BasicChunkCursor extends SynchronousCursor implements ChunkedCursor this.indexWithinChunkStack.length === halfHeight, 0x51d /* unexpected indexWithinChunkStack */, ); - return this.siblingStack.length % 2 === 0 - ? CursorLocationType.Fields - : CursorLocationType.Nodes; } public getFieldKey(): FieldKey { @@ -206,8 +212,7 @@ export class BasicChunkCursor extends SynchronousCursor implements ChunkedCursor private getStackedChunkIndex(height: number): number { assert(height % 2 === 1, "must be node height"); assert(height >= 0, "must not be above root"); - // eslint-disable-next-line no-bitwise - return this.indexOfChunkStack[height >> 1] ?? oob(); + return this.indexOfChunkStack[this.getNodeOnlyHeightFromHeight(height)] ?? oob(); } private getStackedChunk(height: number): BasicChunk { @@ -215,6 +220,13 @@ export class BasicChunkCursor extends SynchronousCursor implements ChunkedCursor return (this.siblingStack[height] as readonly TreeChunk[])[index] as BasicChunk; } + // Returns the floor of half the height passed in, or half of `siblingStack.length` if no height is provided. + private getNodeOnlyHeightFromHeight(height: number = this.siblingStack.length): number { + // The bitwise shift computes the floor, which is valid assuming the depth is less than 2^31, which seems safe. + // eslint-disable-next-line no-bitwise + return height >> 1; + } + public getFieldLength(): number { if (this.nestedCursor !== undefined) { return this.nestedCursor.getFieldLength(); @@ -342,6 +354,7 @@ export class BasicChunkCursor extends SynchronousCursor implements ChunkedCursor // at the cost of an allocation here. this.index = 0; this.siblings = [key]; + this.assertChunkStacksMatchNodeDepth(); } public nextField(): boolean { @@ -371,6 +384,7 @@ export class BasicChunkCursor extends SynchronousCursor implements ChunkedCursor this.indexWithinChunkStack.push(this.indexWithinChunk); this.index = 0; this.siblings = [...fields.keys()]; // TODO: avoid this copy + this.assertChunkStacksMatchNodeDepth(); return true; } @@ -440,6 +454,7 @@ export class BasicChunkCursor extends SynchronousCursor implements ChunkedCursor this.siblings = siblings; this.indexOfChunk = 0; this.indexWithinChunk = 0; + this.assertChunkStacksMatchNodeDepth(); this.initNestedCursor(); return true; } @@ -502,6 +517,7 @@ export class BasicChunkCursor extends SynchronousCursor implements ChunkedCursor this.indexOfChunkStack.pop() ?? fail("Unexpected indexOfChunkStack.length"); this.indexWithinChunk = this.indexWithinChunkStack.pop() ?? fail("Unexpected indexWithinChunkStack.length"); + this.assertChunkStacksMatchNodeDepth(); } public exitNode(): void { @@ -522,6 +538,7 @@ export class BasicChunkCursor extends SynchronousCursor implements ChunkedCursor // (so a fully-iterated cursor matches a fresh cursor at the same logical position). this.indexOfChunk = 0; this.indexWithinChunk = 0; + this.assertChunkStacksMatchNodeDepth(); } private getNode(): BasicChunk { diff --git a/packages/dds/tree/src/test/feature-libraries/chunked-forest/basicChunk.spec.ts b/packages/dds/tree/src/test/feature-libraries/chunked-forest/basicChunk.spec.ts index 32d8bd6e1781..d78f28cdab82 100644 --- a/packages/dds/tree/src/test/feature-libraries/chunked-forest/basicChunk.spec.ts +++ b/packages/dds/tree/src/test/feature-libraries/chunked-forest/basicChunk.spec.ts @@ -190,15 +190,21 @@ describe("basic chunk", () => { undefined, ); - // Logical node index 2 maps to chunk array index 1 (the trailing BasicChunk). + // Transitions from field to firstNode and then increments to nodeIndex 2, + // which maps to chunk array index 1 (the trailing BasicChunk). cursor.enterNode(2); - // enterField drives getField, which on main looks up the parent at the - // logical node index (2) instead of the chunk array index (1). siblings[2] - // is out of bounds in the 2-chunk array, so the cast returns undefined and - // the subsequent field read throws. + // Entering the subfield of the trailing BasicChunk calls enterField, which is followed by + // getField, which looks up the current node (whose field we should get). This must + // handle the case where the index of the node does not match the index of the chunk + // containing the node in the siblings array held at the top of `siblingStack`. + // In this test, the node index is 2 and the chunk index is 1. cursor.enterField(subField); - assert.doesNotThrow(() => cursor.getFieldLength()); + // Validates the cursor is positioned on the subField holding the subLeaf, + // then enters it to confirm its value (42). + assert.equal(cursor.getFieldLength(), 1); + cursor.firstNode(); + assert.equal(cursor.value, 42); }); it("getField resolves parent via chunk array index when preceded by two multi-node chunks", () => { @@ -227,10 +233,21 @@ describe("basic chunk", () => { undefined, ); - // Logical node index 4 maps to chunk array index 2 (the trailing BasicChunk). + // Transitions from field to firstNode and then increments to nodeIndex 4, + // which maps to chunk array index 2 (the trailing BasicChunk). cursor.enterNode(4); + + // Entering the subfield of the trailing BasicChunk calls enterField, which is followed by + // getField, which looks up the current node (whose field we should get). This must + // handle the case where the index of the node does not match the index of the chunk + // containing the node in the siblings array held at the top of `siblingStack`. + // In this test, the node index is 4 and the chunk index is 2. cursor.enterField(subField); - assert.doesNotThrow(() => cursor.getFieldLength()); + // Validates the cursor is positioned on the subField holding the subLeaf, + // then enters it to confirm its value (99). + assert.equal(cursor.getFieldLength(), 1); + cursor.firstNode(); + assert.equal(cursor.value, 99); }); describe("SequenceChunk", () => { From 6d112ef0183064f866d413c86e6e7fe8d87c8c27 Mon Sep 17 00:00:00 2001 From: brrichards Date: Tue, 21 Apr 2026 16:55:31 +0000 Subject: [PATCH 3/5] Update docs --- .../chunked-forest/basicChunk.ts | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/dds/tree/src/feature-libraries/chunked-forest/basicChunk.ts b/packages/dds/tree/src/feature-libraries/chunked-forest/basicChunk.ts index e079eb124e37..101edb79b916 100644 --- a/packages/dds/tree/src/feature-libraries/chunked-forest/basicChunk.ts +++ b/packages/dds/tree/src/feature-libraries/chunked-forest/basicChunk.ts @@ -220,7 +220,15 @@ export class BasicChunkCursor extends SynchronousCursor implements ChunkedCursor return (this.siblingStack[height] as readonly TreeChunk[])[index] as BasicChunk; } - // Returns the floor of half the height passed in, or half of `siblingStack.length` if no height is provided. + /** + * Converts a {@link height}, which contains field and node levels, into the corresponding depth/index + * for the node-only stacks ({@link indexOfChunkStack} and {@link indexWithinChunkStack}), which are + * only pushed on node-level transitions. + * + * @param height - A depth in {@link siblingStack} to convert. Defaults to {@link siblingStack}'s + * current length, which gives the current depth of the node-only stacks. + * @returns `floor(height / 2)` — the number of node levels at or below the given stack height. + */ private getNodeOnlyHeightFromHeight(height: number = this.siblingStack.length): number { // The bitwise shift computes the floor, which is valid assuming the depth is less than 2^31, which seems safe. // eslint-disable-next-line no-bitwise @@ -546,6 +554,16 @@ export class BasicChunkCursor extends SynchronousCursor implements ChunkedCursor return (this.siblings as TreeChunk[])[this.indexOfChunk] as BasicChunk; } + /** + * Returns the chunks that make up the field the cursor is currently in. + * + * @remarks At the root, this is {@link root} directly. Otherwise, the cursor must be in + * {@link CursorLocationType.Fields} mode, and the result is looked up on the parent node using + * the current field key. The parent node is the {@link BasicChunk} in the node array at the top + * of {@link siblingStack} while we are in {@link CursorLocationType.Fields} mode. We need the + * parent since a field's chunks are stored on the parent node's {@link BasicChunk.fields} map, + * not on the cursor itself. + */ private getField(): readonly TreeChunk[] { if (this.siblingStack.length === 0) { return this.root; From 7c238af69a915d2c5a144a207d36cead83af06ae Mon Sep 17 00:00:00 2001 From: brrichards Date: Tue, 21 Apr 2026 17:12:07 +0000 Subject: [PATCH 4/5] Small fix --- .../chunked-forest/basicChunk.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/dds/tree/src/feature-libraries/chunked-forest/basicChunk.ts b/packages/dds/tree/src/feature-libraries/chunked-forest/basicChunk.ts index 101edb79b916..d29ba36daa46 100644 --- a/packages/dds/tree/src/feature-libraries/chunked-forest/basicChunk.ts +++ b/packages/dds/tree/src/feature-libraries/chunked-forest/basicChunk.ts @@ -555,14 +555,16 @@ export class BasicChunkCursor extends SynchronousCursor implements ChunkedCursor } /** - * Returns the chunks that make up the field the cursor is currently in. + * Resolves the chunks that make up the field the cursor is currently in. At the root, this is + * {@link root} directly. Otherwise, the cursor must be in {@link CursorLocationType.Fields} mode, + * and the result is looked up on the parent node using the current field key. * - * @remarks At the root, this is {@link root} directly. Otherwise, the cursor must be in - * {@link CursorLocationType.Fields} mode, and the result is looked up on the parent node using - * the current field key. The parent node is the {@link BasicChunk} in the node array at the top - * of {@link siblingStack} while we are in {@link CursorLocationType.Fields} mode. We need the - * parent since a field's chunks are stored on the parent node's {@link BasicChunk.fields} map, - * not on the cursor itself. + * @remarks The parent node is the {@link BasicChunk} in the node array at the top of + * {@link siblingStack} while we are in {@link CursorLocationType.Fields} mode. We need the parent + * since a field's chunks are stored on the parent node's {@link BasicChunk.fields} map, not on + * the cursor itself. + * + * @returns The chunks that make up the field the cursor is currently in. */ private getField(): readonly TreeChunk[] { if (this.siblingStack.length === 0) { From 62b266a353714550333b020ed7086028084ba6af Mon Sep 17 00:00:00 2001 From: brrichards Date: Wed, 22 Apr 2026 15:56:00 +0000 Subject: [PATCH 5/5] Add debugAsserts and update docs --- .../chunked-forest/basicChunk.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/dds/tree/src/feature-libraries/chunked-forest/basicChunk.ts b/packages/dds/tree/src/feature-libraries/chunked-forest/basicChunk.ts index d29ba36daa46..4ebaa68996f5 100644 --- a/packages/dds/tree/src/feature-libraries/chunked-forest/basicChunk.ts +++ b/packages/dds/tree/src/feature-libraries/chunked-forest/basicChunk.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. */ -import { assert, oob, fail } from "@fluidframework/core-utils/internal"; +import { assert, oob, fail, debugAssert } from "@fluidframework/core-utils/internal"; import { CursorLocationType, @@ -217,7 +217,9 @@ export class BasicChunkCursor extends SynchronousCursor implements ChunkedCursor private getStackedChunk(height: number): BasicChunk { const index = this.getStackedChunkIndex(height); - return (this.siblingStack[height] as readonly TreeChunk[])[index] as BasicChunk; + const chunk = (this.siblingStack[height] as readonly TreeChunk[])[index]; + debugAssert(() => chunk instanceof BasicChunk || "only basic chunks are expected"); + return chunk as BasicChunk; } /** @@ -551,7 +553,9 @@ export class BasicChunkCursor extends SynchronousCursor implements ChunkedCursor private getNode(): BasicChunk { assert(this.mode === CursorLocationType.Nodes, 0x52f /* can only get node when in node */); - return (this.siblings as TreeChunk[])[this.indexOfChunk] as BasicChunk; + const chunk = (this.siblings as TreeChunk[])[this.indexOfChunk]; + debugAssert(() => chunk instanceof BasicChunk || "only basic chunks are expected"); + return chunk as BasicChunk; } /** @@ -559,11 +563,6 @@ export class BasicChunkCursor extends SynchronousCursor implements ChunkedCursor * {@link root} directly. Otherwise, the cursor must be in {@link CursorLocationType.Fields} mode, * and the result is looked up on the parent node using the current field key. * - * @remarks The parent node is the {@link BasicChunk} in the node array at the top of - * {@link siblingStack} while we are in {@link CursorLocationType.Fields} mode. We need the parent - * since a field's chunks are stored on the parent node's {@link BasicChunk.fields} map, not on - * the cursor itself. - * * @returns The chunks that make up the field the cursor is currently in. */ private getField(): readonly TreeChunk[] { @@ -574,6 +573,10 @@ export class BasicChunkCursor extends SynchronousCursor implements ChunkedCursor this.mode === CursorLocationType.Fields, 0x530 /* can only get field when in fields */, ); + // The parent node is the `BasicChunk` in the node array at the top of + // `siblingStack` while we are in `CursorLocationType.Fields` mode. We need the parent + // since a field's chunks are stored on the parent node's `BasicChunk.fields` map, not on + // the cursor itself. const parent = this.getStackedChunk(this.siblingStack.length - 1); const key: FieldKey = this.getFieldKey(); const field = parent.fields.get(key) ?? [];