From c15c01b68194b7e51b672f2b270139955f063d78 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Wed, 12 Mar 2025 15:23:36 +0700 Subject: [PATCH] feat: forEach() api for ArrayComposite() --- packages/ssz/src/view/arrayBasic.ts | 8 +++- packages/ssz/src/view/arrayComposite.ts | 16 ++++++-- packages/ssz/src/viewDU/arrayBasic.ts | 8 +++- packages/ssz/src/viewDU/arrayComposite.ts | 38 ++++++++++++++++-- .../unit/byType/listComposite/tree.test.ts | 39 +++++++++++++++++-- 5 files changed, 93 insertions(+), 16 deletions(-) diff --git a/packages/ssz/src/view/arrayBasic.ts b/packages/ssz/src/view/arrayBasic.ts index 48cbf44e..b74da6e9 100644 --- a/packages/ssz/src/view/arrayBasic.ts +++ b/packages/ssz/src/view/arrayBasic.ts @@ -87,14 +87,18 @@ export class ArrayBasicTreeView> extends /** * Get all values of this array as Basic element type values, from index zero to `this.length - 1` + * @param values optional output parameter, if is provided it must be an array of the same length as this array */ - getAll(): ValueOf[] { + getAll(values?: ValueOf[]): ValueOf[] { + if (values && values.length !== this.length) { + throw Error(`Expected ${this.length} values, got ${values.length}`); + } const length = this.length; const chunksNode = this.type.tree_getChunksNode(this.node); const chunkCount = Math.ceil(length / this.type.itemsPerChunk); const leafNodes = getNodesAtDepth(chunksNode, this.type.chunkDepth, 0, chunkCount) as LeafNode[]; - const values = new Array>(length); + values = values ?? new Array>(length); const itemsPerChunk = this.type.itemsPerChunk; // Prevent many access in for loop below const lenFullNodes = Math.floor(length / itemsPerChunk); const remainder = length % itemsPerChunk; diff --git a/packages/ssz/src/view/arrayComposite.ts b/packages/ssz/src/view/arrayComposite.ts index 87912aa9..0c8c2064 100644 --- a/packages/ssz/src/view/arrayComposite.ts +++ b/packages/ssz/src/view/arrayComposite.ts @@ -73,12 +73,16 @@ export class ArrayCompositeTreeView< * Returns an array of views of all elements in the array, from index zero to `this.length - 1`. * The returned views don't have a parent hook to this View's Tree, so changes in the returned views won't be * propagated upwards. To get linked element Views use `this.get()` + * @param views optional output parameter, if is provided it must be an array of the same length as this array */ - getAllReadonly(): CompositeView[] { + getAllReadonly(views?: CompositeView[]): CompositeView[] { + if (views && views.length !== this.length) { + throw Error(`Expected ${this.length} views, got ${views.length}`); + } const length = this.length; const chunksNode = this.type.tree_getChunksNode(this.node); const nodes = getNodesAtDepth(chunksNode, this.type.chunkDepth, 0, length); - const views = new Array>(length); + views = views ?? new Array>(length); for (let i = 0; i < length; i++) { // TODO: Optimize views[i] = this.type.elementType.getView(new Tree(nodes[i])); @@ -90,12 +94,16 @@ export class ArrayCompositeTreeView< * Returns an array of values of all elements in the array, from index zero to `this.length - 1`. * The returned values are not Views so any changes won't be propagated upwards. * To get linked element Views use `this.get()` + * @param values optional output parameter, if is provided it must be an array of the same length as this array */ - getAllReadonlyValues(): ValueOf[] { + getAllReadonlyValues(values?: ValueOf[]): ValueOf[] { + if (values && values.length !== this.length) { + throw Error(`Expected ${this.length} values, got ${values.length}`); + } const length = this.length; const chunksNode = this.type.tree_getChunksNode(this.node); const nodes = getNodesAtDepth(chunksNode, this.type.chunkDepth, 0, length); - const values = new Array>(length); + values = values ?? new Array>(length); for (let i = 0; i < length; i++) { values[i] = this.type.elementType.tree_toValue(nodes[i]); } diff --git a/packages/ssz/src/viewDU/arrayBasic.ts b/packages/ssz/src/viewDU/arrayBasic.ts index 492092ac..5be0926d 100644 --- a/packages/ssz/src/viewDU/arrayBasic.ts +++ b/packages/ssz/src/viewDU/arrayBasic.ts @@ -109,8 +109,12 @@ export class ArrayBasicTreeViewDU> extend /** * Get all values of this array as Basic element type values, from index zero to `this.length - 1` + * @param values optional output parameter, if is provided it must be an array of the same length as this array */ - getAll(): ValueOf[] { + getAll(values?: ValueOf[]): ValueOf[] { + if (values && values.length !== this._length) { + throw Error(`Expected ${this._length} values, got ${values.length}`); + } if (!this.nodesPopulated) { const nodesPrev = this.nodes; const chunksNode = this.type.tree_getChunksNode(this.node); @@ -125,7 +129,7 @@ export class ArrayBasicTreeViewDU> extend this.nodesPopulated = true; } - const values = new Array>(this._length); + values = values ?? new Array>(this._length); const itemsPerChunk = this.type.itemsPerChunk; // Prevent many access in for loop below const lenFullNodes = Math.floor(this._length / itemsPerChunk); const remainder = this._length % itemsPerChunk; diff --git a/packages/ssz/src/viewDU/arrayComposite.ts b/packages/ssz/src/viewDU/arrayComposite.ts index aa7e2be4..1596b98f 100644 --- a/packages/ssz/src/viewDU/arrayComposite.ts +++ b/packages/ssz/src/viewDU/arrayComposite.ts @@ -147,11 +147,15 @@ export class ArrayCompositeTreeViewDU< /** * Returns all elements at every index, if an index is modified it will return the modified view. * No need to commit() before calling this function. + * @param views optional output parameter, if is provided it must be an array of the same length as this array */ - getAllReadonly(): CompositeViewDU[] { + getAllReadonly(views?: CompositeViewDU[]): CompositeViewDU[] { + if (views && views.length !== this._length) { + throw Error(`Expected ${this._length} views, got ${views.length}`); + } this.populateAllOldNodes(); - const views = new Array>(this._length); + views = views ?? new Array>(this._length); for (let i = 0; i < this._length; i++) { // this will get pending change first, if not it will get from the `this.nodes` array views[i] = this.getReadonly(i); @@ -159,19 +163,45 @@ export class ArrayCompositeTreeViewDU< return views; } + /** + * Apply `fn` to each ViewDU in the array. + * Similar to getAllReadOnly(), no need to commit() before calling this function. + * if an item is modified it will return the modified view. + */ + forEach(fn: (viewDU: CompositeViewDU, index: number) => void): void { + this.populateAllOldNodes(); + for (let i = 0; i < this._length; i++) { + fn(this.getReadonly(i), i); + } + } + /** * WARNING: Returns all commited changes, if there are any pending changes commit them beforehand + * @param values optional output parameter, if is provided it must be an array of the same length as this array */ - getAllReadonlyValues(): ValueOf[] { + getAllReadonlyValues(values?: ValueOf[]): ValueOf[] { + if (values && values.length !== this._length) { + throw Error(`Expected ${this._length} values, got ${values.length}`); + } this.populateAllNodes(); - const values = new Array>(this._length); + values = values ?? new Array>(this._length); for (let i = 0; i < this._length; i++) { values[i] = this.type.elementType.tree_toValue(this.nodes[i]); } return values; } + /** + * Apply `fn` to each value in the array + */ + forEachValue(fn: (value: ValueOf, index: number) => void): void { + this.populateAllNodes(); + for (let i = 0; i < this._length; i++) { + fn(this.type.elementType.tree_toValue(this.nodes[i]), i); + } + } + /** * When we need to compute HashComputations (hcByLevel != null): * - if old _rootNode is hashed, then only need to put pending changes to hcByLevel diff --git a/packages/ssz/test/unit/byType/listComposite/tree.test.ts b/packages/ssz/test/unit/byType/listComposite/tree.test.ts index cfbfb734..afc8b899 100644 --- a/packages/ssz/test/unit/byType/listComposite/tree.test.ts +++ b/packages/ssz/test/unit/byType/listComposite/tree.test.ts @@ -1,5 +1,6 @@ import {describe, it, expect, beforeEach} from "vitest"; import { + ByteVectorType, CompositeView, ContainerNodeStructType, ContainerType, @@ -354,23 +355,53 @@ describe("ListCompositeType ViewDU batchHashTreeRoot", () => { } }); -describe("ListCompositeType getAllReadOnly - no commit", () => { - it("getAllReadOnly() without commit", () => { +describe("ListCompositeType", () => { + let listView: ListCompositeTreeViewDU; + + beforeEach(() => { const listType = new ListCompositeType(ssz.Root, 1024); const listLength = 2; const list = Array.from({length: listLength}, (_, i) => Buffer.alloc(32, i)); - const listView = listType.toViewDU(list); + listView = listType.toViewDU(list); expect(listView.getAllReadonly()).to.deep.equal(list); + }); + it("getAllReadOnly()", () => { // modify listView.set(0, Buffer.alloc(32, 1)); // push listView.push(Buffer.alloc(32, 1)); // getAllReadOnly() without commit, now all items should be the same - expect(listView.getAllReadonly()).to.deep.equal(Array.from({length: 3}, () => Buffer.alloc(32, 1))); + const all = listView.getAllReadonly(); + expect(all).to.deep.equal(Array.from({length: 3}, () => Buffer.alloc(32, 1))); + + const out = new Array(3).fill(Buffer.alloc(32, 0)); + // getAllReadonly() with "out" parameter of the same length + const all2 = listView.getAllReadonly(out); + expect(all2 === out).to.be.true; + expect(out).to.deep.equal(all); // getAllReadOnlyValues() will throw expect(() => listView.getAllReadonlyValues()).toThrow("Must commit changes before reading all nodes"); }); + + it("forEach()", () => { + listView.forEach((item, i) => expect(item).to.deep.equal(Buffer.alloc(32, i))); + }); + + it("getAllReadonlyValues()", () => { + // no param + expect(listView.getAllReadonlyValues()).to.deep.equal(Array.from({length: 2}, (_, i) => Buffer.alloc(32, i))); + const out = new Array(2).fill(Buffer.alloc(32, 0)); + + // with "out" param + const all = listView.getAllReadonlyValues(out); + expect(all === out).to.be.true; + expect(out).to.be.deep.equal(Array.from({length: 2}, (_, i) => Buffer.alloc(32, i))); + }); + + it("forEachValue()", () => { + listView.forEachValue((item, i) => expect(item).to.deep.equal(Buffer.alloc(32, i))); + }); });