diff --git a/crates/web-client/src/models/account_storage.rs b/crates/web-client/src/models/account_storage.rs index df3f70c44..df73c27a6 100644 --- a/crates/web-client/src/models/account_storage.rs +++ b/crates/web-client/src/models/account_storage.rs @@ -1,6 +1,7 @@ use idxdb_store::account::JsStorageMapEntry; use miden_client::account::{AccountStorage as NativeAccountStorage, StorageSlot}; use wasm_bindgen::prelude::*; +use wasm_bindgen_futures::js_sys; use crate::models::word::Word; @@ -27,6 +28,9 @@ impl AccountStorage { /// Get all key-value pairs from the map slot at `index`. /// Returns `undefined` if the slot isn't a map or `index` is out of bounds (0-255). /// Returns `[]` if the map exists but is empty. + /// + /// WARNING: This method allocates the entire map into memory. + /// For large maps, use `forEachMapEntry` instead for better memory efficiency. #[wasm_bindgen(js_name = "getMapEntries")] pub fn get_map_entries(&self, index: u8) -> Option> { let slots = self.0.slots(); @@ -43,6 +47,42 @@ impl AccountStorage { _ => None, } } + + /// Stream all key-value pairs from the map slot at `index` via a callback function. + /// This is a memory-efficient alternative to `getMapEntries` for large maps. + /// + /// The callback receives a `JsStorageMapEntry` object with `root`, `key`, and `value` fields. + /// Entries are processed one at a time without allocating an intermediate vector. + /// + /// Returns an error if: + /// - The slot at `index` is not a map + /// - `index` is out of bounds (0-255) + /// - The callback throws an error during execution + #[wasm_bindgen(js_name = "forEachMapEntry")] + pub fn for_each_map_entry( + &self, + index: u8, + callback: &js_sys::Function, + ) -> Result<(), JsValue> { + let slots = self.0.slots(); + match slots.get(index as usize) { + Some(StorageSlot::Map(map)) => { + for (key, value) in map.entries() { + let entry = JsStorageMapEntry { + root: map.root().to_hex(), + key: key.to_hex(), + value: value.to_hex(), + }; + + callback + .call1(&JsValue::UNDEFINED, &JsValue::from(entry)) + .map_err(|e| JsValue::from_str(&format!("Callback failed: {e:?}")))?; + } + Ok(()) + }, + _ => Err(JsValue::from_str("Invalid map index or slot is not a map")), + } + } } // CONVERSIONS diff --git a/crates/web-client/test/new_transactions.test.ts b/crates/web-client/test/new_transactions.test.ts index ac6dbda26..4709810bb 100644 --- a/crates/web-client/test/new_transactions.test.ts +++ b/crates/web-client/test/new_transactions.test.ts @@ -1336,10 +1336,19 @@ export const testStorageMap = async (page: Page): Promise => { ); } + // Test forEachMapEntry (iterator-based approach) + const forEachEntries = []; + (accountStorage as any).forEachMapEntry(1, (entry: any) => { + forEachEntries.push(entry); + }); + return { initialMapValue: normalizeHexWord(initialMapValue), finalMapValue: normalizeHexWord(finalMapValue), mapEntries: mapEntriesData, + forEachEntriesCount: forEachEntries.length, + forEachMatchesGetMapEntries: + forEachEntries.length === (mapEntries?.length || 0), }; }); }; @@ -1347,8 +1356,13 @@ export const testStorageMap = async (page: Page): Promise => { test.describe("storage map test", () => { test.setTimeout(50000); test("storage map is updated correctly in transaction", async ({ page }) => { - let { initialMapValue, finalMapValue, mapEntries } = - await testStorageMap(page); + let { + initialMapValue, + finalMapValue, + mapEntries, + forEachEntriesCount, + forEachMatchesGetMapEntries, + } = await testStorageMap(page); expect(initialMapValue).toBe("1"); expect(finalMapValue).toBe("2"); @@ -1357,5 +1371,9 @@ test.describe("storage map test", () => { expect(mapEntries.hasExpectedEntry).toBe(true); expect(mapEntries.expectedKey).toBeDefined(); expect(mapEntries.expectedValue).toBe("2"); + + // Test forEachMapEntry() functionality + expect(forEachEntriesCount).toBeGreaterThan(1); + expect(forEachMatchesGetMapEntries).toBe(true); }); }); diff --git a/crates/web-client/yarn.lock b/crates/web-client/yarn.lock index a7174ee6d..eaac5ca2b 100644 --- a/crates/web-client/yarn.lock +++ b/crates/web-client/yarn.lock @@ -33,12 +33,14 @@ "@jridgewell/trace-mapping" "0.3.9" "@gerrit0/mini-shiki@^3.12.0": - version "3.12.2" - dependencies: - "@shikijs/engine-oniguruma" "^3.12.2" - "@shikijs/langs" "^3.12.2" - "@shikijs/themes" "^3.12.2" - "@shikijs/types" "^3.12.2" + version "3.13.0" + resolved "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-3.13.0.tgz" + integrity sha512-mCrNvZNYNrwKer5PWLF6cOc0OEe2eKzgy976x+IT2tynwJYl+7UpHTSeXQJGijgTcoOf+f359L946unWlYRnsg== + dependencies: + "@shikijs/engine-oniguruma" "^3.13.0" + "@shikijs/langs" "^3.13.0" + "@shikijs/themes" "^3.13.0" + "@shikijs/types" "^3.13.0" "@shikijs/vscode-textmate" "^10.0.2" "@iarna/toml@^2.2.5": @@ -163,24 +165,32 @@ estree-walker "^2.0.2" picomatch "^4.0.2" -"@shikijs/engine-oniguruma@^3.12.2": - version "3.12.2" +"@shikijs/engine-oniguruma@^3.13.0": + version "3.13.0" + resolved "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.13.0.tgz" + integrity sha512-O42rBGr4UDSlhT2ZFMxqM7QzIU+IcpoTMzb3W7AlziI1ZF7R8eS2M0yt5Ry35nnnTX/LTLXFPUjRFCIW+Operg== dependencies: - "@shikijs/types" "3.12.2" + "@shikijs/types" "3.13.0" "@shikijs/vscode-textmate" "^10.0.2" -"@shikijs/langs@^3.12.2": - version "3.12.2" +"@shikijs/langs@^3.13.0": + version "3.13.0" + resolved "https://registry.npmjs.org/@shikijs/langs/-/langs-3.13.0.tgz" + integrity sha512-672c3WAETDYHwrRP0yLy3W1QYB89Hbpj+pO4KhxK6FzIrDI2FoEXNiNCut6BQmEApYLfuYfpgOZaqbY+E9b8wQ== dependencies: - "@shikijs/types" "3.12.2" + "@shikijs/types" "3.13.0" -"@shikijs/themes@^3.12.2": - version "3.12.2" +"@shikijs/themes@^3.13.0": + version "3.13.0" + resolved "https://registry.npmjs.org/@shikijs/themes/-/themes-3.13.0.tgz" + integrity sha512-Vxw1Nm1/Od8jyA7QuAenaV78BG2nSr3/gCGdBkLpfLscddCkzkL36Q5b67SrLLfvAJTOUzW39x4FHVCFriPVgg== dependencies: - "@shikijs/types" "3.12.2" + "@shikijs/types" "3.13.0" -"@shikijs/types@^3.12.2", "@shikijs/types@3.12.2": - version "3.12.2" +"@shikijs/types@^3.13.0", "@shikijs/types@3.13.0": + version "3.13.0" + resolved "https://registry.npmjs.org/@shikijs/types/-/types-3.13.0.tgz" + integrity sha512-oM9P+NCFri/mmQ8LoFGVfVyemm5Hi27330zuOBp0annwJdKH1kOLndw3zCtAVDehPLg9fKqoEx3Ht/wNZxolfw== dependencies: "@shikijs/vscode-textmate" "^10.0.2" "@types/hast" "^3.0.4" @@ -290,6 +300,8 @@ "@wasm-tool/rollup-plugin-rust@^3.0.3": version "3.0.5" + resolved "https://registry.npmjs.org/@wasm-tool/rollup-plugin-rust/-/rollup-plugin-rust-3.0.5.tgz" + integrity sha512-L3TyDtmrmAY4XdlZAZKMomDManfrKHLQHjDeFNJKi4LjPQRwWUnPoS0yV44NWy+pKXVKlv7wbdkaTmhiFJRmLg== dependencies: "@iarna/toml" "^2.2.5" "@rollup/pluginutils" "^5.1.4" @@ -2270,9 +2282,13 @@ typed-query-selector@^2.12.0: typedoc-plugin-markdown@^4.8.1: version "4.8.1" + resolved "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-4.8.1.tgz" + integrity sha512-ug7fc4j0SiJxSwBGLncpSo8tLvrT9VONvPUQqQDTKPxCoFQBADLli832RGPtj6sfSVJebNSrHZQRUdEryYH/7g== typedoc@^0.28.1, typedoc@0.28.x: - version "0.28.12" + version "0.28.13" + resolved "https://registry.npmjs.org/typedoc/-/typedoc-0.28.13.tgz" + integrity sha512-dNWY8msnYB2a+7Audha+aTF1Pu3euiE7ySp53w8kEsXoYw7dMouV5A1UsTUY345aB152RHnmRMDiovuBi7BD+w== dependencies: "@gerrit0/mini-shiki" "^3.12.0" lunr "^2.3.9" @@ -2408,6 +2424,8 @@ yallist@^5.0.0: yaml@^2.8.1: version "2.8.1" + resolved "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz" + integrity sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw== yargs-parser@^20.2.2, yargs-parser@^20.2.9: version "20.2.9" diff --git a/docs/src/web-client/api/classes/AccountStorage.md b/docs/src/web-client/api/classes/AccountStorage.md index 0590046f8..c8e38705c 100644 --- a/docs/src/web-client/api/classes/AccountStorage.md +++ b/docs/src/web-client/api/classes/AccountStorage.md @@ -18,6 +18,37 @@ *** +### forEachMapEntry() + +> **forEachMapEntry**(`index`, `callback`): `void` + +Stream all key-value pairs from the map slot at `index` via a callback function. +This is a memory-efficient alternative to `getMapEntries` for large maps. + +The callback receives a `JsStorageMapEntry` object with `root`, `key`, and `value` fields. +Entries are processed one at a time without allocating an intermediate vector. + +Returns an error if: +- The slot at `index` is not a map +- `index` is out of bounds (0-255) +- The callback throws an error during execution + +#### Parameters + +##### index + +`number` + +##### callback + +`Function` + +#### Returns + +`void` + +*** + ### free() > **free**(): `void` @@ -52,6 +83,9 @@ Get all key-value pairs from the map slot at `index`. Returns `undefined` if the slot isn't a map or `index` is out of bounds (0-255). Returns `[]` if the map exists but is empty. +WARNING: This method allocates the entire map into memory. +For large maps, use `forEachMapEntry` instead for better memory efficiency. + #### Parameters ##### index