├── .github ├── dependabot.yml └── workflows │ └── test-and-release.yml ├── .gitignore ├── CHANGELOG.md ├── README.md ├── bit-utils.js ├── examples ├── level-backed.js └── memory-backed.js ├── iamap.js ├── interface.ts ├── package.json ├── test ├── basic-test.js ├── bit-utils-test.js ├── common.js ├── errors-test.js ├── interface.ts ├── largeish-test.js └── serialization-test.js ├── tsconfig.json └── types ├── bit-utils.d.ts ├── bit-utils.d.ts.map ├── iamap.d.ts ├── iamap.d.ts.map ├── interface.d.ts ├── interface.d.ts.map └── test ├── basic-test.d.ts ├── basic-test.d.ts.map ├── bit-utils-test.d.ts ├── bit-utils-test.d.ts.map ├── common.d.ts ├── common.d.ts.map ├── errors-test.d.ts ├── errors-test.d.ts.map ├── interface.d.ts ├── interface.d.ts.map ├── largeish-test.d.ts ├── largeish-test.d.ts.map ├── serialization-test.d.ts └── serialization-test.d.ts.map /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'github-actions' 4 | directory: '/' 5 | schedule: 6 | interval: 'daily' 7 | commit-message: 8 | prefix: 'chore' 9 | include: 'scope' 10 | - package-ecosystem: 'npm' 11 | directory: '/' 12 | schedule: 13 | interval: 'daily' 14 | commit-message: 15 | prefix: 'chore' 16 | include: 'scope' 17 | -------------------------------------------------------------------------------- /.github/workflows/test-and-release.yml: -------------------------------------------------------------------------------- 1 | name: Test & Maybe Release 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | strategy: 6 | fail-fast: false 7 | matrix: 8 | node: [16.x, 18.x, current, lts/*] 9 | os: [macos-latest, ubuntu-latest] 10 | runs-on: ${{ matrix.os }} 11 | steps: 12 | - name: Checkout Repository 13 | uses: actions/checkout@v4 14 | - name: Use Node.js ${{ matrix.node }} 15 | uses: actions/setup-node@v4.0.1 16 | with: 17 | node-version: ${{ matrix.node }} 18 | - name: Install Dependencies 19 | run: | 20 | npm install --no-progress 21 | - name: Run tests 22 | run: | 23 | npm test 24 | test-windows: 25 | strategy: 26 | fail-fast: false 27 | matrix: 28 | node: [16.x, 18.x, current, lts/*] 29 | os: [windows-latest] 30 | runs-on: ${{ matrix.os }} 31 | steps: 32 | - name: Checkout Repository 33 | uses: actions/checkout@v4 34 | - name: Use Node.js ${{ matrix.node }} 35 | uses: actions/setup-node@v4.0.1 36 | with: 37 | node-version: ${{ matrix.node }} 38 | - name: Install Dependencies 39 | run: | 40 | npm install --no-progress 41 | - name: Run tests 42 | run: | 43 | npm run test:node 44 | release: 45 | name: Release 46 | needs: [test, test-windows] 47 | runs-on: ubuntu-latest 48 | if: github.event_name == 'push' && github.ref == 'refs/heads/master' 49 | steps: 50 | - name: Checkout 51 | uses: actions/checkout@v4 52 | with: 53 | fetch-depth: 0 54 | - name: Setup Node.js 55 | uses: actions/setup-node@v4.0.1 56 | with: 57 | node-version: lts/* 58 | - name: Install dependencies 59 | run: | 60 | npm install --no-progress --no-package-lock --no-save 61 | - name: Build 62 | run: | 63 | npm run build 64 | - name: Install plugins 65 | run: | 66 | npm install \ 67 | @semantic-release/commit-analyzer \ 68 | conventional-changelog-conventionalcommits \ 69 | @semantic-release/release-notes-generator \ 70 | @semantic-release/npm \ 71 | @semantic-release/github \ 72 | @semantic-release/git \ 73 | @semantic-release/changelog \ 74 | --no-progress --no-package-lock --no-save 75 | - name: Release 76 | env: 77 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 78 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 79 | run: npx semantic-release 80 | 81 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | .nyc_output/ 4 | coverage/ 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [4.0.0](https://github.com/rvagg/iamap/compare/v3.0.9...v4.0.0) (2024-01-10) 2 | 3 | 4 | ### ⚠ BREAKING CHANGES 5 | 6 | * convert to ESM, adapt to chai@5 7 | 8 | ### Features 9 | 10 | * convert to ESM, adapt to chai@5 ([eb40014](https://github.com/rvagg/iamap/commit/eb40014ec87663d3d88ae26cb2b804302e4810e8)) 11 | 12 | 13 | ### Trivial Changes 14 | 15 | * **deps-dev:** bump chai from 4.3.10 to 5.0.0 ([fbb63d4](https://github.com/rvagg/iamap/commit/fbb63d4bf5485ef9cf519403a93e82a83a89622e)) 16 | 17 | ## [3.0.9](https://github.com/rvagg/iamap/compare/v3.0.8...v3.0.9) (2024-01-04) 18 | 19 | 20 | ### Trivial Changes 21 | 22 | * **deps-dev:** bump c8 from 8.0.1 to 9.0.0 ([e02d893](https://github.com/rvagg/iamap/commit/e02d8934f119ddbb4ef1b805a6177291cbed07dd)) 23 | 24 | ## [3.0.8](https://github.com/rvagg/iamap/compare/v3.0.7...v3.0.8) (2024-01-01) 25 | 26 | 27 | ### Trivial Changes 28 | 29 | * **deps:** bump actions/setup-node from 4.0.0 to 4.0.1 ([758bd43](https://github.com/rvagg/iamap/commit/758bd43fed1515f381ad682134d2a3c28f3ba62c)) 30 | 31 | ## [3.0.7](https://github.com/rvagg/iamap/compare/v3.0.6...v3.0.7) (2023-11-27) 32 | 33 | 34 | ### Trivial Changes 35 | 36 | * **deps-dev:** bump typescript from 5.2.2 to 5.3.2 ([55383b2](https://github.com/rvagg/iamap/commit/55383b244df36a00f762e21a08a581707388b33c)) 37 | 38 | ## [3.0.6](https://github.com/rvagg/iamap/compare/v3.0.5...v3.0.6) (2023-10-25) 39 | 40 | 41 | ### Trivial Changes 42 | 43 | * **deps:** bump actions/checkout from 3 to 4 ([22e1a01](https://github.com/rvagg/iamap/commit/22e1a01f613d906f42b7f00c5fceb18d42eda60b)) 44 | * **deps:** bump actions/setup-node from 3.8.1 to 4.0.0 ([20c253f](https://github.com/rvagg/iamap/commit/20c253f8c411774323e8f1bf7cbd5a9a1c2c58fa)) 45 | 46 | ## [3.0.5](https://github.com/rvagg/iamap/compare/v3.0.4...v3.0.5) (2023-08-25) 47 | 48 | 49 | ### Trivial Changes 50 | 51 | * **deps-dev:** bump typescript from 5.1.6 to 5.2.2 ([bb25512](https://github.com/rvagg/iamap/commit/bb25512139796cfb08a656d3570227be351b1f81)) 52 | 53 | ## [3.0.4](https://github.com/rvagg/iamap/compare/v3.0.3...v3.0.4) (2023-08-17) 54 | 55 | 56 | ### Trivial Changes 57 | 58 | * **deps:** bump actions/setup-node from 3.8.0 to 3.8.1 ([f9e18c2](https://github.com/rvagg/iamap/commit/f9e18c24cd51f6104ac497cf8eca6dd7c74dcbaa)) 59 | 60 | ## [3.0.3](https://github.com/rvagg/iamap/compare/v3.0.2...v3.0.3) (2023-08-14) 61 | 62 | 63 | ### Trivial Changes 64 | 65 | * **deps:** bump actions/setup-node from 3.7.0 to 3.8.0 ([19861df](https://github.com/rvagg/iamap/commit/19861df2f03f7f1de8b706683aedbe760e548d13)) 66 | 67 | ## [3.0.2](https://github.com/rvagg/iamap/compare/v3.0.1...v3.0.2) (2023-07-05) 68 | 69 | 70 | ### Trivial Changes 71 | 72 | * **deps:** bump actions/setup-node from 3.6.0 to 3.7.0 ([f03c716](https://github.com/rvagg/iamap/commit/f03c71654a10b19e4f81d0a44b63272752ffcec5)) 73 | 74 | ## [3.0.1](https://github.com/rvagg/iamap/compare/v3.0.0...v3.0.1) (2023-06-14) 75 | 76 | 77 | ### Trivial Changes 78 | 79 | * **deps-dev:** bump c8 from 7.14.0 to 8.0.0 ([e2d2865](https://github.com/rvagg/iamap/commit/e2d28657c93acf77c24ee3bb19d63987fcad75d9)) 80 | 81 | ## [3.0.0](https://github.com/rvagg/iamap/compare/v2.0.17...v3.0.0) (2023-06-05) 82 | 83 | 84 | ### ⚠ BREAKING CHANGES 85 | 86 | * update Node.js versions, drop 14.x 87 | 88 | ### Bug Fixes 89 | 90 | * remove blank line ([8628281](https://github.com/rvagg/iamap/commit/86282810b9a52110d01149ef060446ab4e31e47e)) 91 | 92 | 93 | ### Trivial Changes 94 | 95 | * **deps-dev:** bump typescript from 5.0.4 to 5.1.3 ([5891171](https://github.com/rvagg/iamap/commit/5891171c4187618afd810488053dfe5fcbffa38a)) 96 | * update Node.js versions, drop 14.x ([2cc1d2f](https://github.com/rvagg/iamap/commit/2cc1d2f84c86dce1e6bc7fc160812e0b433f1df7)) 97 | 98 | ## [2.0.17](https://github.com/rvagg/iamap/compare/v2.0.16...v2.0.17) (2023-05-17) 99 | 100 | 101 | ### Trivial Changes 102 | 103 | * **deps-dev:** bump polendina from 3.1.0 to 3.2.1 ([aad4c5c](https://github.com/rvagg/iamap/commit/aad4c5c1879b8bc460e981eb5ac87afd371a0d7d)) 104 | 105 | ## [2.0.16](https://github.com/rvagg/iamap/compare/v2.0.15...v2.0.16) (2023-03-17) 106 | 107 | 108 | ### Bug Fixes 109 | 110 | * release with Node.js 18 ([5265dfc](https://github.com/rvagg/iamap/commit/5265dfce7ca7c5a4b9021f67c6d3286b01adbc74)) 111 | 112 | 113 | ### Trivial Changes 114 | 115 | * **deps-dev:** bump typescript from 4.9.5 to 5.0.2 ([1df8cd8](https://github.com/rvagg/iamap/commit/1df8cd8b811d8c611cdae9488b4448214dac0a9d)) 116 | 117 | ## [2.0.15](https://github.com/rvagg/iamap/compare/v2.0.14...v2.0.15) (2023-01-06) 118 | 119 | 120 | ### Trivial Changes 121 | 122 | * **deps:** bump actions/setup-node from 3.5.1 to 3.6.0 ([#55](https://github.com/rvagg/iamap/issues/55)) ([81703b5](https://github.com/rvagg/iamap/commit/81703b50e8c73273acd3c154ecceec338ff3a35a)) 123 | 124 | ## [2.0.14](https://github.com/rvagg/iamap/compare/v2.0.13...v2.0.14) (2022-12-09) 125 | 126 | 127 | ### Trivial Changes 128 | 129 | * **deps-dev:** bump typescript from 4.8.4 to 4.9.4 ([#54](https://github.com/rvagg/iamap/issues/54)) ([d6361cc](https://github.com/rvagg/iamap/commit/d6361ccbb59e7f16f297c8a00dca46826a6d4724)) 130 | * **no-release:** bump @types/mocha from 9.1.1 to 10.0.0 ([#51](https://github.com/rvagg/iamap/issues/51)) ([16546b2](https://github.com/rvagg/iamap/commit/16546b2bf110c280a055743011de6fcd2919e014)) 131 | * **no-release:** bump actions/setup-node from 3.4.1 to 3.5.0 ([#50](https://github.com/rvagg/iamap/issues/50)) ([536c48e](https://github.com/rvagg/iamap/commit/536c48e9fe1cfd89792a6b8b4e2d01058eff35e8)) 132 | * **no-release:** bump actions/setup-node from 3.5.0 to 3.5.1 ([#52](https://github.com/rvagg/iamap/issues/52)) ([089a482](https://github.com/rvagg/iamap/commit/089a482bdc3f3fe72a4e12c8d348a1876dc5882b)) 133 | 134 | ## [2.0.13](https://github.com/rvagg/iamap/compare/v2.0.12...v2.0.13) (2022-08-27) 135 | 136 | 137 | ### Trivial Changes 138 | 139 | * **deps-dev:** bump typescript from 4.7.4 to 4.8.2 ([#49](https://github.com/rvagg/iamap/issues/49)) ([cc5c3f7](https://github.com/rvagg/iamap/commit/cc5c3f77bfdf12ccf5e251374ddd9efbaf80ba10)) 140 | * **no-release:** bump actions/setup-node from 3.2.0 to 3.3.0 ([#46](https://github.com/rvagg/iamap/issues/46)) ([1f1a1c6](https://github.com/rvagg/iamap/commit/1f1a1c67977bc568be79cfcec4654f150a476afb)) 141 | * **no-release:** bump actions/setup-node from 3.3.0 to 3.4.0 ([#47](https://github.com/rvagg/iamap/issues/47)) ([48ca2f5](https://github.com/rvagg/iamap/commit/48ca2f5d0f9e19d535d906b6750371f3c81a3a20)) 142 | * **no-release:** bump actions/setup-node from 3.4.0 to 3.4.1 ([#48](https://github.com/rvagg/iamap/issues/48)) ([297eb08](https://github.com/rvagg/iamap/commit/297eb0804369f114b45352ecb48ba592eb2d29f1)) 143 | 144 | ### [2.0.12](https://github.com/rvagg/iamap/compare/v2.0.11...v2.0.12) (2022-05-25) 145 | 146 | 147 | ### Trivial Changes 148 | 149 | * **deps-dev:** bump typescript from 4.6.4 to 4.7.2 ([d3393c1](https://github.com/rvagg/iamap/commit/d3393c14b98403560cf5476a34f6852138104941)) 150 | * **no-release:** bump actions/setup-node from 3.0.0 to 3.1.0 ([#38](https://github.com/rvagg/iamap/issues/38)) ([af574b6](https://github.com/rvagg/iamap/commit/af574b6a5c464560a52fd0f7653b075b44bdb639)) 151 | * **no-release:** bump actions/setup-node from 3.1.0 to 3.1.1 ([#39](https://github.com/rvagg/iamap/issues/39)) ([1c75ab8](https://github.com/rvagg/iamap/commit/1c75ab80ceb7af55b80cbbb6d47df9e469d61fda)) 152 | * **no-release:** bump actions/setup-node from 3.1.1 to 3.2.0 ([#44](https://github.com/rvagg/iamap/issues/44)) ([4df52e6](https://github.com/rvagg/iamap/commit/4df52e6df5815445157beaefefeece8eb815f9e9)) 153 | * **no-release:** bump mocha from 9.2.2 to 10.0.0 ([#42](https://github.com/rvagg/iamap/issues/42)) ([38ea15a](https://github.com/rvagg/iamap/commit/38ea15a73ff054be9743e8cd9e7ebb1951abe8ed)) 154 | * **no-release:** bump polendina from 2.0.15 to 3.0.0 ([#41](https://github.com/rvagg/iamap/issues/41)) ([b14f130](https://github.com/rvagg/iamap/commit/b14f130c832fa2d4010affd08b47b1521997ab8f)) 155 | * **no-release:** bump polendina from 3.0.0 to 3.1.0 ([#43](https://github.com/rvagg/iamap/issues/43)) ([48f2814](https://github.com/rvagg/iamap/commit/48f2814dc51fcda74f7bdf5da0e75acb319ffbaa)) 156 | * **no-release:** bump standard from 16.0.4 to 17.0.0 ([#40](https://github.com/rvagg/iamap/issues/40)) ([4645569](https://github.com/rvagg/iamap/commit/4645569eb7ea1572f42302142963911fbb3509ac)) 157 | 158 | ### [2.0.11](https://github.com/rvagg/iamap/compare/v2.0.10...v2.0.11) (2022-03-02) 159 | 160 | 161 | ### Trivial Changes 162 | 163 | * **deps-dev:** bump typescript from 4.5.5 to 4.6.2 ([#37](https://github.com/rvagg/iamap/issues/37)) ([aeae491](https://github.com/rvagg/iamap/commit/aeae49162d02bd631ad7b31de53d3b19fa7d7cd0)) 164 | * **no-release:** bump actions/checkout from 2.4.0 to 3 ([#36](https://github.com/rvagg/iamap/issues/36)) ([05c52c7](https://github.com/rvagg/iamap/commit/05c52c7baba29a90316eb16c076dc89485b3c9b0)) 165 | * **no-release:** bump actions/setup-node from 2.5.0 to 2.5.1 ([#34](https://github.com/rvagg/iamap/issues/34)) ([db1abb1](https://github.com/rvagg/iamap/commit/db1abb1685f3f7c37ed761b0a6e5e658ce5eae1e)) 166 | * **no-release:** bump actions/setup-node from 2.5.1 to 3.0.0 ([#35](https://github.com/rvagg/iamap/issues/35)) ([1599aaa](https://github.com/rvagg/iamap/commit/1599aaa962556237af30cecc8e384a646d3c3e6d)) 167 | 168 | ### [2.0.10](https://github.com/rvagg/iamap/compare/v2.0.9...v2.0.10) (2021-12-14) 169 | 170 | 171 | ### Trivial Changes 172 | 173 | * **no-release:** bump actions/setup-node from 2.4.1 to 2.5.0 ([#29](https://github.com/rvagg/iamap/issues/29)) ([88cdd7c](https://github.com/rvagg/iamap/commit/88cdd7ca6d906da8a54a34ff1c28b0b64d42cb51)) 174 | * udpate deps, test in webpack5 ([#33](https://github.com/rvagg/iamap/issues/33)) ([7a3bde5](https://github.com/rvagg/iamap/commit/7a3bde5745ce5635507d4b452a8da4f8af1f3cef)) 175 | 176 | ### [2.0.9](https://github.com/rvagg/iamap/compare/v2.0.8...v2.0.9) (2021-11-04) 177 | 178 | 179 | ### Trivial Changes 180 | 181 | * **deps:** bump actions/checkout from 2.3.5 to 2.4.0 ([1279157](https://github.com/rvagg/iamap/commit/1279157b9124efc846e721a64213e348d3de1ecf)) 182 | 183 | ### [2.0.8](https://github.com/rvagg/iamap/compare/v2.0.7...v2.0.8) (2021-10-18) 184 | 185 | 186 | ### Trivial Changes 187 | 188 | * **deps:** bump actions/checkout from 2.3.4 to 2.3.5 ([990cdc0](https://github.com/rvagg/iamap/commit/990cdc0a0140a894be67c8228ccbcb3764680879)) 189 | 190 | ### [2.0.7](https://github.com/rvagg/iamap/compare/v2.0.6...v2.0.7) (2021-09-28) 191 | 192 | 193 | ### Trivial Changes 194 | 195 | * **deps:** bump actions/setup-node from 2.4.0 to 2.4.1 ([28fcb9e](https://github.com/rvagg/iamap/commit/28fcb9e172516375aa8cbe477b8774a8cc96e067)) 196 | 197 | ### [2.0.6](https://github.com/rvagg/iamap/compare/v2.0.5...v2.0.6) (2021-09-16) 198 | 199 | 200 | ### Bug Fixes 201 | 202 | * correctly type save() and load() types as async ([da03346](https://github.com/rvagg/iamap/commit/da03346cb419143f81eeead536f82536c8f5580f)), closes [#22](https://github.com/rvagg/iamap/issues/22) 203 | 204 | ### [2.0.5](https://github.com/rvagg/iamap/compare/v2.0.4...v2.0.5) (2021-08-05) 205 | 206 | 207 | ### Trivial Changes 208 | 209 | * **deps:** bump actions/setup-node from 2.3.2 to 2.4.0 ([6a6af26](https://github.com/rvagg/iamap/commit/6a6af26df9e2af157cedb47c4a8492c83fc84f9e)) 210 | 211 | ### [2.0.4](https://github.com/rvagg/iamap/compare/v2.0.3...v2.0.4) (2021-08-05) 212 | 213 | 214 | ### Trivial Changes 215 | 216 | * **deps:** bump actions/setup-node from 2.3.1 to 2.3.2 ([f59a212](https://github.com/rvagg/iamap/commit/f59a2125d6de575b51b1c7e2a6673a8646891eb1)) 217 | 218 | ### [2.0.3](https://github.com/rvagg/iamap/compare/v2.0.2...v2.0.3) (2021-08-03) 219 | 220 | 221 | ### Trivial Changes 222 | 223 | * **deps:** bump actions/setup-node from 2.3.0 to 2.3.1 ([b1f55f5](https://github.com/rvagg/iamap/commit/b1f55f5f58a81c73e53bc30aa377b459f222a1a2)) 224 | 225 | ### [2.0.2](https://github.com/rvagg/iamap/compare/v2.0.1...v2.0.2) (2021-07-23) 226 | 227 | 228 | ### Trivial Changes 229 | 230 | * **deps-dev:** bump @types/mocha from 8.2.3 to 9.0.0 ([2914eed](https://github.com/rvagg/iamap/commit/2914eededad1846e66f6e5feb743b00258708783)) 231 | 232 | ### [2.0.1](https://github.com/rvagg/iamap/compare/v2.0.0...v2.0.1) (2021-07-20) 233 | 234 | 235 | ### Trivial Changes 236 | 237 | * **deps:** bump actions/setup-node from 2.1.5 to 2.3.0 ([8501f98](https://github.com/rvagg/iamap/commit/8501f9876f5f1dce18374ff942f8cf9fedc6e5fe)) 238 | 239 | ## [2.0.0](https://github.com/rvagg/iamap/compare/v1.0.0...v2.0.0) (2021-07-07) 240 | 241 | 242 | ### ⚠ BREAKING CHANGES 243 | 244 | * migrate serialized form to match IPLD HashMap spec 245 | * remove block-by-block traversals 246 | * use integer multicodec codes for `hashAlg` 247 | 248 | ### Features 249 | 250 | * add type definitions and build:types script ([fa75267](https://github.com/rvagg/iamap/commit/fa75267533077b22aecf595597ceda7dd864c609)) 251 | * allow asynchronous hasher function ([58d58ed](https://github.com/rvagg/iamap/commit/58d58edb2f9588ca21d025f2927f3fc6fe091dc9)) 252 | * migrate serialized form to match IPLD HashMap spec ([a8002b0](https://github.com/rvagg/iamap/commit/a8002b0ae95758993897ef3d78cb59e72221a7ff)) 253 | * remove block-by-block traversals ([ddc1227](https://github.com/rvagg/iamap/commit/ddc1227225a33a015cdc0bcdb8ca363ca6b5eb9b)) 254 | * use integer multicodec codes for `hashAlg` ([5140627](https://github.com/rvagg/iamap/commit/51406275bdeacccc900b2aae59f79bb6818df24d)) 255 | 256 | 257 | ### Bug Fixes 258 | 259 | * **doc:** clean up docs for README autogen ([813a853](https://github.com/rvagg/iamap/commit/813a853016ad6355ed19763ee5376a14514a38ed)) 260 | * broken byteCompare ([8f26675](https://github.com/rvagg/iamap/commit/8f266750f41e87d54933721f5c79eb0f20466041)) 261 | * coverage ignores for stricter type guards ([7e3fa6a](https://github.com/rvagg/iamap/commit/7e3fa6a3fd05330f3c7d7f87b13caaf9c9002a51)) 262 | * update and fix examples ([daef015](https://github.com/rvagg/iamap/commit/daef015533314246a8cfa658dc49718900404c08)) 263 | 264 | 265 | ### Trivial Changes 266 | 267 | * **perf:** cache hash of key ([f7c3d05](https://github.com/rvagg/iamap/commit/f7c3d05af2c43912e368b76b0ef073146de813fa)) 268 | * add github actions - dependabot / test / semantic-release ([fb382a5](https://github.com/rvagg/iamap/commit/fb382a5de23108fa3b916b669b3f73868412aeda)) 269 | * add typechecking to tests ([fbefbfd](https://github.com/rvagg/iamap/commit/fbefbfdf65f615231d9558572a733c1300ce3b7b)) 270 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IAMap 2 | 3 | An **I**mmutable **A**synchronous **Map**. 4 | 5 | ## Warning 6 | 7 | This is both **experimental** and a **work in progress**. The current form is not likely to be the final form. No guarantees are provided that serialised versions of today's version will be loadable in the future. This project may even be archived if significantly improved forms are discovered or derived. 8 | 9 | However, rich documentation is provided as an invitation for collaboration to work on that final form; or as inspiration for alternative approaches to this problem space. 10 | 11 | _Caveat emptor for versions less than 1.0.0._ 12 | 13 | ## Contents 14 | 15 | - [About](#about) 16 | - [Immutability](#immutability) 17 | - [Consistency](#consistency) 18 | - [Algorithm](#algorithm) 19 | - [Pending questions](#pending-questions) 20 | - [Examples](#examples) 21 | - [API](#api) 22 | - [License and Copyright](#license-and-copyright) 23 | 24 | ## About 25 | 26 | IAMap provides a `Map`-like interface that can organise data in a storage system that does not lend itself to organisation, such as a content addressed storage system like [IPFS](https://ipfs.io/) where you have to know the address of an element before you can fetch it. 27 | 28 | As a single entity, an IAMap instance is a collection of elements which are either entries (or buckets / arrays of entries) or are references to child nodes which themselves are IAMap instances. Large collections will form a deep graph of IAMap nodes referenced by a single IAMap root instance. The entries are key/value pairs, where the values could either be plain JavaScript objects (as long as they are serialisable themselves) or be references to objects within the datastore (or perhaps elsewhere!). Each node in an IAMap graph of nodes is serialised into the datastore. Therefore, a single ID / address / reference to the root node is all that is required to find elements within the collection and collections may be _very_ large. 29 | 30 | An IAMap is, therefore, a layer that provides a helpful key/value store on top of a storage system that does not inherently provide indexing facilities that allows easy fetch-by-key. In a content addressed store, such as IPFS, every item can only be fetched by its address, which is a hash of its content. Since those addresses may be derived from links within other pieces of content, we can build a data structure that becomes content and contains those links. Rather than `Store->Get(addressOfValue)` we can `Store->Get(iaMapRoot)->Get(key)` since there are many cases where `addressOfValue` is not known, or derivable ahead of time, but we can keep a single root address and use it to find any `key` within the generated IAMap structure. 31 | 32 | The interface bears resemblance to a `Map` but with some crucial differences: asynchronous calls and immutability: 33 | 34 | ```js 35 | import * as iamap from 'iamap' 36 | 37 | // instantiate with a backing store and a hash function 38 | let map = await iamap.create(store, { hashAlg: 'murmur3-32' }) 39 | // mutations create new copies 40 | map = await map.set('foo', 'bar') 41 | assert(await map.get('foo') === 'bar') 42 | map = await map.delete('foo', 'bar') 43 | assert(await map.get('foo') === null) 44 | ``` 45 | 46 | ### IPLD 47 | 48 | [IPLD](http://ipld.io/) is the data layer of IPFS. One aim of this project is to work toward useful primitives that will allow more complex applications to be built that do not necessarily relate to the current IPFS file-focused use-case. 49 | 50 | While IAMap is intended to operate on top of IPLD, it is intentionally built independent from it such that it could be used across any other datastore that presents similar challenges to storing and retrieving structured data. 51 | 52 | ## Immutability 53 | 54 | IAMap instances cannot be mutated, once instantiated, you cannot (or should not) modify its properties. Therefore, mutation requires the creation of new instances. Every `map.set()` and `map.delete()` operation will result in a new IAMap root node, which will have a new, unique identifier. New instances created by mutations essentially perform a copy-on-write (CoW), so only the modified node and its parents are impacted, all reference to unmodified nodes remain intact as links. 55 | 56 | Mutation on a large data set may involve the creation of many new internal nodes, as references between nodes form part of the "content" and therefore require new identifiers. This is handled transparently but users should be aware that many intermediate nodes are created in a backing store during mutation operations. 57 | 58 | ## Consistency 59 | 60 | IAMap instances are (or should be!) consistent for any given set of key/value pairs. This holds regardless of the order of `set()` operations or the use of `delete()` operations to derive the final form. When serialized in content addressable storage, an IAMap root node referencing the same set of key/value pairs should share the same identifier since their content is the same. 61 | 62 | ## Algorithm 63 | 64 | IAMap implements a [Hash Array Mapped Trie (HAMT)](https://en.wikipedia.org/wiki/Hash_array_mapped_trie), it uses the mutation semantics the [CHAMP](https://michael.steindorfer.name/publications/oopsla15.pdf) (Com-pressed Hash-Array Mapped Prefix-tree) variant but adds buckets to entries and does not use separate `datamap` and `nodemap` properties, but rather a single `map` as per a traditional HAMT algorithm. 65 | 66 | This implementation is related to the [Peergos](https://peergos.org/) [CHAMP Java implementaion](https://github.com/Peergos/Peergos/blob/master/src/peergos/shared/hamt/Champ.java) and the [implementation](https://github.com/msteindorfer/oopsla15-artifact/) provided with the original CHAMP OOPSLA'15 paper. 67 | 68 | It is also similar to the current [go-hamt-ipld](https://github.com/ipfs/go-hamt-ipld/) and the intention is that this, and the Go IPLD implementation will converge via a shared specification such that they will be able to read each other's data stored in IPLD. 69 | 70 | ### Algorithm Summary 71 | 72 | Keys are hashed when inserted, fetched, modified or deleted in a HAMT. Each node of a HAMT sits at a particular level, or "depth". Each depth takes a different section of the hash to determine an index for the key. e.g. if each level takes 8-bits of the hash, then depth=0 takes the first 8 bits to form an index, which will be from 0 to 255. At depth=1, we take the _next_ 8 bits to determine a new index, and so on, until a place for the entry is found. An entry is inserted at the top-most node that has room for it. Each node can hold as many _elements_ as the hash portion (or "prefix") allows. So if 8-bits are used for each level, then each node can hold 256 elements. In the case of IAMap, each of those elements can also be "buckets", or an array of elements. When a new element is inserted, the insertion first attempts at the root node, which is depth=0. The root node will be considered "full" if there is no space at the index determined by the hash at depth=0. A classic HAMT will be "full" if that index already contains an entry. In this implementation, "full" means that the bucket at that index has reached the maximum size allowed. Once full, the element at that index is replaced with a new child-node, whose depth is incremented. All entries previously in the bucket at that index are then inserted into this new child node—however because the depth is incremented, a new portion of the hash of each element is used to determine its index. In this way, each node in the graph will contain a collection of either buckets (or entries) and references to child nodes. A good hash algorithm will distribute roughly evenly, making a densely packed data structure, where the density and height can be controlled by the number of bits of the hash used at each level and the maximum size of the buckets. 73 | 74 | CHAMP adds some optimisations that increase performance on in-memory HAMTs (it's not clear that these extend to HAMTs that are backed by a non-memory datastore) and also delete semantics such that there is a canonical graph for any given set of key/value pairs. The CHAMP optimisation of separating `datamap` and `nodemap` is not applied here as the impact on traversal performance is only realisable for fully in-memory data structures. 75 | 76 | Clear as mud? The code is heavily documented and you should be able to follow the algorithm in code form if you are so inclined. 77 | 78 | ## Examples 79 | 80 | ### [examples/memory-backed.js](./examples/memory-backed.js) 81 | 82 | Use a JavaScript `Map` as an example backing store. IDs are created by `JSON.stringify()`ing the `toSerializable()` form of an IAMap node and taking a hash of that string. 83 | 84 | Running this application will scan all directories within this repository and look for `package.json` files. If valid, it will store them keyed by `name@version`. Once the scan has completed, it will iterate over all entries of the IAMap and print out the `name@version` along with the `description` of each package. 85 | 86 | This is not a realistic example because you can just use a `Map` directly, but it's useful for demonstrating the basics of how it works and can form the basis of something more sophisticated, such as a database-backed store. 87 | 88 | ### [examples/level-backed.js](./examples/level-backed.js) 89 | 90 | A more sophisticated example that uses LevelDB to simulate a content addressable store. Objects are naively encoded as [CBOR](https://cbor.io/) and given [CIDs](https://github.com/multiformats/cid) via [ipld-dag-cbor](https://github.com/ipld/js-ipld-dag-cbor). 91 | 92 | This example uses IAMap to crate an index of all module names `require()`'d by .js files in a given directory (and any of its subdirectories). You can then use that index to find a list of files that use a particular module. 93 | 94 | The primary data structure uses IAMap to index lists of files by module name, so a `get(moduleName)` will fetch a list of files. To allow for large lists, the values in the primary IAMap are CIDs of secondary IAMaps, each of which is used like a Set to store arbitrarily large lists of files. So this example demonstrates IAMap as a standard Map and as a Set and there are as many Sets as there are modules found. 95 | 96 | _(Note: directories with many 10's of thousands of .js files will be slow to index, be patient or try first on a smaller set of files.)_ 97 | 98 | ## API 99 | 100 | ### Contents 101 | 102 | * [Warning](#warning) 103 | * [Contents](#contents) 104 | * [About](#about) 105 | * [IPLD](#ipld) 106 | * [Immutability](#immutability) 107 | * [Consistency](#consistency) 108 | * [Algorithm](#algorithm) 109 | * [Algorithm Summary](#algorithm-summary) 110 | * [Examples](#examples) 111 | * [examples/memory-backed.js](#examplesmemory-backedjs) 112 | * [examples/level-backed.js](#exampleslevel-backedjs) 113 | * [API](#api) 114 | * [Contents](#contents-1) 115 | * [`async iamap.create(store, options[, map][, depth][, data])`](#async-iamapcreatestore-options-map-depth-data) 116 | * [`async iamap.load(store, id[, depth][, options])`](#async-iamaploadstore-id-depth-options) 117 | * [`iamap.registerHasher(hashAlg, hashBytes, hasher)`](#iamapregisterhasherhashalg-hashbytes-hasher) 118 | * [`async IAMap#set(key, value[, _cachedHash])`](#async-iamapsetkey-value-_cachedhash) 119 | * [`async IAMap#get(key[, _cachedHash])`](#async-iamapgetkey-_cachedhash) 120 | * [`async IAMap#has(key)`](#async-iamaphaskey) 121 | * [`async IAMap#delete(key[, _cachedHash])`](#async-iamapdeletekey-_cachedhash) 122 | * [`async IAMap#size()`](#async-iamapsize) 123 | * [`async * IAMap#keys()`](#async--iamapkeys) 124 | * [`async * IAMap#values()`](#async--iamapvalues) 125 | * [`async * IAMap#entries()`](#async--iamapentries) 126 | * [`async * IAMap#ids()`](#async--iamapids) 127 | * [`IAMap#toSerializable()`](#iamaptoserializable) 128 | * [`IAMap#directEntryCount()`](#iamapdirectentrycount) 129 | * [`IAMap#directNodeCount()`](#iamapdirectnodecount) 130 | * [`async IAMap#isInvariant()`](#async-iamapisinvariant) 131 | * [`IAMap#fromChildSerializable(id, serializable[, depth])`](#iamapfromchildserializableid-serializable-depth) 132 | * [`iamap.isRootSerializable(serializable)`](#iamapisrootserializableserializable) 133 | * [`iamap.isSerializable(serializable)`](#iamapisserializableserializable) 134 | * [`iamap.fromSerializable(store, id, serializable[, options][, depth])`](#iamapfromserializablestore-id-serializable-options-depth) 135 | * [`IAMap.isIAMap(node)`](#iamapisiamapnode) 136 | * [License and Copyright](#license-and-copyright) 137 | 138 | 139 | ### `async iamap.create(store, options[, map][, depth][, data])` 140 | 141 | * `store` `(Store)`: A backing store for this Map. The store should be able to save and load a serialised 142 | form of a single node of a IAMap which is provided as a plain object representation. `store.save(node)` takes 143 | a serialisable node and should return a content address / ID for the node. `store.load(id)` serves the inverse 144 | purpose, taking a content address / ID as provided by a `save()` operation and returning the serialised form 145 | of a node which can be instantiated by IAMap. In addition, two identifier handling methods are needed: 146 | `store.isEqual(id1, id2)` is required to check the equality of the two content addresses / IDs 147 | (which may be custom for that data type). `store.isLink(obj)` is used to determine if an object is a link type 148 | that can be used for `load()` operations on the store. It is important that link types be different to standard 149 | JavaScript arrays and don't share properties used by the serialized form of an IAMap (e.g. such that a 150 | `typeof obj === 'object' && Array.isArray(obj.data)`) .This is because a node data element may either be a link to 151 | a child node, or an inlined child node, so `isLink()` should be able to determine if an object is a link, and if not, 152 | `Array.isArray(obj)` will determine if that data element is a bucket of elements, or the above object check be able 153 | to determine that an inline child node exists at the data element. 154 | The `store` object should take the following form: 155 | `{ async save(node):id, async load(id):node, isEqual(id,id):boolean, isLink(obj):boolean }` 156 | A `store` should throw an appropriately informative error when a node that is requested does not exist in the backing 157 | store. 158 | 159 | Options: 160 | - hashAlg (number) - A [multicodec](https://github.com/multiformats/multicodec/blob/master/table.csv) 161 | hash function identifier, e.g. `0x23` for `murmur3-32`. Hash functions must be registered with [`iamap.registerHasher`](#iamap__registerHasher). 162 | - bitWidth (number, default 8) - The number of bits to extract from the hash to form a data element index at 163 | each level of the Map, e.g. a bitWidth of 5 will extract 5 bits to be used as the data element index, since 2^5=32, 164 | each node will store up to 32 data elements (child nodes and/or entry buckets). The maximum depth of the Map is 165 | determined by `floor((hashBytes * 8) / bitWidth)` where `hashBytes` is the number of bytes the hash function 166 | produces, e.g. `hashBytes=32` and `bitWidth=5` yields a maximum depth of 51 nodes. The maximum `bitWidth` 167 | currently allowed is `8` which will store 256 data elements in each node. 168 | - bucketSize (number, default 5) - The maximum number of collisions acceptable at each level of the Map. A 169 | collision in the `bitWidth` index at a given depth will result in entries stored in a bucket (array). Once the 170 | bucket exceeds `bucketSize`, a new child node is created for that index and all entries in the bucket are 171 | pushed 172 | * `options` `(Options)`: Options for this IAMap 173 | * `map` `(Uint8Array, optional)`: for internal use 174 | * `depth` `(number, optional)`: for internal use 175 | * `data` `(Element[], optional)`: for internal use 176 | 177 | ```js 178 | let map = await iamap.create(store, options) 179 | ``` 180 | 181 | Create a new IAMap instance with a backing store. This operation is asynchronous and returns a `Promise` that 182 | resolves to a `IAMap` instance. 183 | 184 | 185 | ### `async iamap.load(store, id[, depth][, options])` 186 | 187 | * `store` `(Store)`: A backing store for this Map. See [`iamap.create`](#iamap__create). 188 | * `id` `(any)`: An content address / ID understood by the backing `store`. 189 | * `depth` `(number, optional, default=`0`)` 190 | * `options` `(Options, optional)` 191 | 192 | ```js 193 | let map = await iamap.load(store, id) 194 | ``` 195 | 196 | Create a IAMap instance loaded from a serialised form in a backing store. See [`iamap.create`](#iamap__create). 197 | 198 | 199 | ### `iamap.registerHasher(hashAlg, hashBytes, hasher)` 200 | 201 | * `hashAlg` `(number)`: A [multicodec](https://github.com/multiformats/multicodec/blob/master/table.csv) hash 202 | function identifier **number**, e.g. `0x23` for `murmur3-32`. 203 | * `hashBytes` `(number)`: The number of bytes to use from the result of the `hasher()` function (e.g. `32`) 204 | * `hasher` `(Hasher)`: A hash function that takes a `Uint8Array` derived from the `key` values used for this 205 | Map and returns a `Uint8Array` (or a `Uint8Array`-like, such that each data element of the array contains a single byte value). The function 206 | may or may not be asynchronous but will be called with an `await`. 207 | 208 | ```js 209 | iamap.registerHasher(hashAlg, hashBytes, hasher) 210 | ``` 211 | 212 | Register a new hash function. IAMap has no hash functions by default, at least one is required to create a new 213 | IAMap. 214 | 215 | 216 | ### `async IAMap#set(key, value[, _cachedHash])` 217 | 218 | * `key` `(string|Uint8Array)`: A key for the `value` being set whereby that same `value` may 219 | be retrieved with a `get()` operation with the same `key`. The type of the `key` object should either be a 220 | `Uint8Array` or be convertable to a `Uint8Array` via `TextEncoder. 221 | * `value` `(any)`: Any value that can be stored in the backing store. A value could be a serialisable object 222 | or an address or content address or other kind of link to the actual value. 223 | * `_cachedHash` `(Uint8Array, optional)`: for internal use 224 | 225 | * Returns: `Promise>`: A `Promise` containing a new `IAMap` that contains the new key/value pair. 226 | 227 | Asynchronously create a new `IAMap` instance identical to this one but with `key` set to `value`. 228 | 229 | 230 | ### `async IAMap#get(key[, _cachedHash])` 231 | 232 | * `key` `(string|Uint8Array)`: A key for the value being sought. See [`IAMap#set`](#IAMap_set) for 233 | details about acceptable `key` types. 234 | * `_cachedHash` `(Uint8Array, optional)`: for internal use 235 | 236 | * Returns: `Promise`: A `Promise` that resolves to the value being sought if that value exists within this `IAMap`. If the 237 | key is not found in this `IAMap`, the `Promise` will resolve to `undefined`. 238 | 239 | Asynchronously find and return a value for the given `key` if it exists within this `IAMap`. 240 | 241 | 242 | ### `async IAMap#has(key)` 243 | 244 | * `key` `(string|Uint8Array)`: A key to check for existence within this `IAMap`. See 245 | [`IAMap#set`](#IAMap_set) for details about acceptable `key` types. 246 | 247 | * Returns: `Promise`: A `Promise` that resolves to either `true` or `false` depending on whether the `key` exists 248 | within this `IAMap`. 249 | 250 | Asynchronously find and return a boolean indicating whether the given `key` exists within this `IAMap` 251 | 252 | 253 | ### `async IAMap#delete(key[, _cachedHash])` 254 | 255 | * `key` `(string|Uint8Array)`: A key to remove. See [`IAMap#set`](#IAMap_set) for details about 256 | acceptable `key` types. 257 | * `_cachedHash` `(Uint8Array, optional)`: for internal use 258 | 259 | * Returns: `Promise>`: A `Promise` that resolves to a new `IAMap` instance without the given `key` or the same `IAMap` 260 | instance if `key` does not exist within it. 261 | 262 | Asynchronously create a new `IAMap` instance identical to this one but with `key` and its associated 263 | value removed. If the `key` does not exist within this `IAMap`, this instance of `IAMap` is returned. 264 | 265 | 266 | ### `async IAMap#size()` 267 | 268 | * Returns: `Promise`: A `Promise` with a `number` indicating the number of key/value pairs within this `IAMap` instance. 269 | 270 | Asynchronously count the number of key/value pairs contained within this `IAMap`, including its children. 271 | 272 | 273 | ### `async * IAMap#keys()` 274 | 275 | * Returns: `AsyncGenerator`: An async iterator that yields keys. All keys will be in `Uint8Array` format regardless of which 276 | format they were inserted via `set()`. 277 | 278 | Asynchronously emit all keys that exist within this `IAMap`, including its children. This will cause a full 279 | traversal of all nodes. 280 | 281 | 282 | ### `async * IAMap#values()` 283 | 284 | * Returns: `AsyncGenerator`: An async iterator that yields values. 285 | 286 | Asynchronously emit all values that exist within this `IAMap`, including its children. This will cause a full 287 | traversal of all nodes. 288 | 289 | 290 | ### `async * IAMap#entries()` 291 | 292 | * Returns: `AsyncGenerator<{key: Uint8Array, value: any}>`: An async iterator that yields objects with the properties `key` and `value`. 293 | 294 | Asynchronously emit all { key, value } pairs that exist within this `IAMap`, including its children. This will 295 | cause a full traversal of all nodes. 296 | 297 | 298 | ### `async * IAMap#ids()` 299 | 300 | * Returns: `AsyncGenerator`: An async iterator that yields the ID of this `IAMap` and all of its children. The type of ID is 301 | determined by the backing store which is responsible for generating IDs upon `save()` operations. 302 | 303 | Asynchronously emit the IDs of this `IAMap` and all of its children. 304 | 305 | 306 | ### `IAMap#toSerializable()` 307 | 308 | * Returns: `SerializedNode|SerializedRoot`: An object representing the internal state of this local `IAMap` node, including its links to child nodes 309 | if any. 310 | 311 | Returns a serialisable form of this `IAMap` node. The internal representation of this local node is copied into a plain 312 | JavaScript `Object` including a representation of its data array that the key/value pairs it contains as well as 313 | the identifiers of child nodes. 314 | Root nodes (depth==0) contain the full map configuration information, while intermediate and leaf nodes contain only 315 | data that cannot be inferred by traversal from a root node that already has this data (hashAlg and bucketSize -- bitWidth 316 | is inferred by the length of the `map` byte array). 317 | The backing store can use this representation to create a suitable serialised form. When loading from the backing store, 318 | `IAMap` expects to receive an object with the same layout from which it can instantiate a full `IAMap` object. Where 319 | root nodes contain the full set of data and intermediate and leaf nodes contain just the required data. 320 | For content addressable backing stores, it is expected that the same data in this serialisable form will always produce 321 | the same identifier. 322 | Note that the `map` property is a `Uint8Array` so will need special handling for some serialization forms (e.g. JSON). 323 | 324 | Root node form: 325 | ``` 326 | { 327 | hashAlg: number 328 | bucketSize: number 329 | hamt: [Uint8Array, Array] 330 | } 331 | ``` 332 | 333 | Intermediate and leaf node form: 334 | ``` 335 | [Uint8Array, Array] 336 | ``` 337 | 338 | The `Uint8Array` in both forms is the 'map' used to identify the presence of an element in this node. 339 | 340 | The second element in the tuple in both forms, `Array`, is an elements array a mix of either buckets or links: 341 | 342 | * A bucket is an array of two elements, the first being a `key` of type `Uint8Array` and the second a `value` 343 | or whatever type has been provided in `set()` operations for this `IAMap`. 344 | * A link is an object of the type that the backing store provides upon `save()` operations and can be identified 345 | with `isLink()` calls. 346 | 347 | Buckets and links are differentiated by their "kind": a bucket is an array while a link is a "link" kind as dictated 348 | by the backing store. We use `Array.isArray()` and `store.isLink()` to perform this differentiation. 349 | 350 | 351 | ### `IAMap#directEntryCount()` 352 | 353 | * Returns: `number`: A number representing the number of local entries. 354 | 355 | Calculate the number of entries locally stored by this node. Performs a scan of local buckets and adds up 356 | their size. 357 | 358 | 359 | ### `IAMap#directNodeCount()` 360 | 361 | * Returns: `number`: A number representing the number of direct child nodes 362 | 363 | Calculate the number of child nodes linked by this node. Performs a scan of the local entries and tallies up the 364 | ones containing links to child nodes. 365 | 366 | 367 | ### `async IAMap#isInvariant()` 368 | 369 | * Returns: `Promise`: A Promise with a boolean value indicating whether this IAMap is correctly formatted. 370 | 371 | Asynchronously perform a check on this node and its children that it is in canonical format for the current data. 372 | As this uses `size()` to calculate the total number of entries in this node and its children, it performs a full 373 | scan of nodes and therefore incurs a load and deserialisation cost for each child node. 374 | A `false` result from this method suggests a flaw in the implemetation. 375 | 376 | 377 | ### `IAMap#fromChildSerializable(id, serializable[, depth])` 378 | 379 | * `id` `(any)`: An optional ID for the instantiated IAMap node. See [`iamap.fromSerializable`](#iamap__fromSerializable). 380 | * `serializable` `(any)`: The serializable form of an IAMap node to be instantiated. 381 | * `depth` `(number, optional, default=`0`)`: The depth of the IAMap node. See [`iamap.fromSerializable`](#iamap__fromSerializable). 382 | 383 | A convenience shortcut to [`iamap.fromSerializable`](#iamap__fromSerializable) that uses this IAMap node instance's backing `store` and 384 | configuration `options`. Intended to be used to instantiate child IAMap nodes from a root IAMap node. 385 | 386 | 387 | ### `iamap.isRootSerializable(serializable)` 388 | 389 | * `serializable` `(any)`: An object that may be a serialisable form of an IAMap root node 390 | 391 | * Returns: `boolean`: An indication that the serialisable form is or is not an IAMap root node 392 | 393 | Determine if a serializable object is an IAMap root type, can be used to assert whether a data block is 394 | an IAMap before trying to instantiate it. 395 | 396 | 397 | ### `iamap.isSerializable(serializable)` 398 | 399 | * `serializable` `(any)`: An object that may be a serialisable form of an IAMap node 400 | 401 | * Returns: `boolean`: An indication that the serialisable form is or is not an IAMap node 402 | 403 | Determine if a serializable object is an IAMap node type, can be used to assert whether a data block is 404 | an IAMap node before trying to instantiate it. 405 | This should pass for both root nodes as well as child nodes 406 | 407 | 408 | ### `iamap.fromSerializable(store, id, serializable[, options][, depth])` 409 | 410 | * `store` `(Store)`: A backing store for this Map. See [`iamap.create`](#iamap__create). 411 | * `id` `(any)`: An optional ID for the instantiated IAMap node. Unlike [`iamap.create`](#iamap__create), 412 | `fromSerializable()` does not `save()` a newly created IAMap node so an ID is not generated for it. If one is 413 | required for downstream purposes it should be provided, if the value is `null` or `undefined`, `node.id` will 414 | be `null` but will remain writable. 415 | * `serializable` `(any)`: The serializable form of an IAMap node to be instantiated 416 | * `options` `(Options, optional, default=`null`)`: An options object for IAMap child node instantiation. Will be ignored for root 417 | node instantiation (where `depth` = `0`) See [`iamap.create`](#iamap__create). 418 | * `depth` `(number, optional, default=`0`)`: The depth of the IAMap node. Where `0` is the root node and any `>0` number is a child 419 | node. 420 | 421 | * Returns: `IAMap` 422 | 423 | Instantiate an IAMap from a valid serialisable form of an IAMap node. The serializable should be the same as 424 | produced by [`IAMap#toSerializable`](#IAMap_toSerializable). 425 | Serialised forms of root nodes must satisfy both [`iamap.isRootSerializable`](#iamap__isRootSerializable) and [`iamap.isSerializable`](#iamap__isSerializable). For 426 | root nodes, the `options` parameter will be ignored and the `depth` parameter must be the default value of `0`. 427 | Serialised forms of non-root nodes must satisfy [`iamap.isSerializable`](#iamap__isSerializable) and have a valid `options` parameter and 428 | a non-`0` `depth` parameter. 429 | 430 | 431 | ### `IAMap.isIAMap(node)` 432 | 433 | * `node` `(IAMap|any)` 434 | 435 | * Returns: `boolean` 436 | 437 | ## License and Copyright 438 | 439 | Copyright 2019 Rod Vagg 440 | 441 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 442 | 443 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 444 | -------------------------------------------------------------------------------- /bit-utils.js: -------------------------------------------------------------------------------- 1 | // Copyright Rod Vagg; Licensed under the Apache License, Version 2.0, see README.md for more information 2 | 3 | import bitSequence from 'bit-sequence' 4 | 5 | /** 6 | * @param {Uint8Array} hash 7 | * @param {number} depth 8 | * @param {number} nbits 9 | * @returns {number} 10 | */ 11 | export function mask (hash, depth, nbits) { 12 | return bitSequence(hash, depth * nbits, nbits) 13 | } 14 | 15 | /** 16 | * set the `position` bit in the given `bitmap` to be `set` (truthy=1, falsey=0) 17 | * @param {Uint8Array} bitmap 18 | * @param {number} position 19 | * @param {boolean|0|1} set 20 | * @returns {Uint8Array} 21 | */ 22 | export function setBit (bitmap, position, set) { 23 | // if we assume that `bitmap` is already the opposite of `set`, we could skip this check 24 | const byte = Math.floor(position / 8) 25 | const offset = position % 8 26 | const has = bitmapHas(bitmap, undefined, byte, offset) 27 | if ((set && !has) || (!set && has)) { 28 | const newBitmap = Uint8Array.from(bitmap) 29 | let b = bitmap[byte] 30 | if (set) { 31 | b |= (1 << offset) 32 | } else { 33 | b ^= (1 << offset) 34 | } 35 | newBitmap[byte] = b 36 | return newBitmap 37 | } 38 | return bitmap 39 | } 40 | 41 | /** 42 | * check whether `bitmap` has a `1` at the given `position` bit 43 | * @param {Uint8Array} bitmap 44 | * @param {number} [position] 45 | * @param {number} [byte] 46 | * @param {number} [offset] 47 | * @returns {boolean} 48 | */ 49 | export function bitmapHas (bitmap, position, byte, offset) { 50 | if (typeof byte !== 'number' || typeof offset !== 'number') { 51 | /* c8 ignore next 3 */ 52 | if (position === undefined) { 53 | throw new Error('`position` expected') 54 | } 55 | byte = Math.floor(position / 8) 56 | offset = position % 8 57 | } 58 | return ((bitmap[byte] >> offset) & 1) === 1 59 | } 60 | 61 | /** 62 | * count how many `1` bits are in `bitmap up until `position` 63 | * tells us where in the compacted element array an element should live 64 | * TODO: optimize with a popcount on a `position` shifted bitmap? 65 | * assumes bitmapHas(bitmap, position) == true, hence the i 16 | - build an index of require()'d modules inside .js files contained in 'dir' (recursively searched) 17 | - returns a map ID that you can use for --search and --stats 18 | 19 | level-backed.js --search 20 | - search an index, identified by 'indexId', for all files that require() a 'module' 21 | 22 | level-backed.js --stats 23 | - print some basic stats of the index identified by 'indexId' 24 | 25 | We use a LevelDB data store keyed by content identifiers (CIDs) storing CBOR encoded objets to simulate 26 | a content-addressed backing store. Obviously LevelDB is a good enough key/value store on its own, this 27 | is just an example! However, this approach could be used to efficiently generate a set of { CID, block } 28 | pairs to be fed into another data store like IPFS. 29 | 30 | In the process of building the data structures, we generate a lot of intermediate nodes that are not 31 | used in the final form of the map. Our data store is simulating an append-only store so there is not 32 | delete option and we'll end up with a lot of cruft. We can use `map.ids()` (as well as `map.values()` 33 | where the values are CIDs, which they are in this case) to extract the CIDs that comprise the final 34 | form and therefore need to be preserved. The rest could be discarded. 35 | 36 | Our basic map structure comprises a set of key/value pairs, where the key is a module name (string) and 37 | the value _represents_ a list of all files that use that module. As that list of files can be quite 38 | large (and therefore result in large CBOR encoded blocks if encoded directly), we instead store a CID 39 | identifying secondary IAMap that is used like a Set, where its `keys()` yield a list of files. So we are 40 | storing IAMap's within an IAMap and using the root as a Map and the values are Sets. 41 | 42 | */ 43 | 44 | const assert = require('assert') 45 | const fs = require('fs') 46 | const path = require('path') 47 | const { Transform } = require('stream') 48 | const murmurhash3 = require('murmurhash3js-revisited') 49 | const level = require('level') 50 | const { CID } = require('multiformats/cid') 51 | const Block = require('multiformats/block') 52 | const { sha256 } = require('multiformats/hashes/sha2') 53 | const dagCbor = require('@ipld/dag-cbor') 54 | const split2 = require('split2') 55 | const iamap = require('../') 56 | 57 | const dbLocation = '/tmp/iamap-level-example.db' 58 | const store = { 59 | stats: { 60 | loads: 0, 61 | saves: 0 62 | }, 63 | // we're going to store Base58-encoded string representations of CIDs as our keys and 64 | // CBOR (binary) encoded objects as the values 65 | backingDb: level(dbLocation, { keyEncoding: 'ascii', valueEncoding: 'binary' }), 66 | encode: async (obj) => { 67 | return await Block.encode({ value: obj, codec: dagCbor, hasher: sha256 }) 68 | }, 69 | decode: async (bytes) => { 70 | return (await Block.decode({ bytes, codec: dagCbor, hasher: sha256 })).value 71 | }, 72 | 73 | // These next 3 methods are used by IAMap, they are part of the required IAMap `store` interface 74 | save: async (value) => { 75 | // Save some arbitrary object to our store. When IAMap uses this it's saving a plain object 76 | // representation of an IAMap node. See IAMap#toSerializable() for information on that form. 77 | store.stats.saves++ 78 | const { cid, bytes } = await store.encode(value) 79 | await store.backingDb.put(cid.toString(), bytes) 80 | return cid 81 | }, 82 | // Load some arbitrary object from our store. When IAMap uses this, it's expecting a plain object 83 | // representation of an IAMap that it can deserialise. See IAMap#fromSerializable(). 84 | load: async (id) => { 85 | store.stats.loads++ 86 | assert(CID.asCID(id) != null) 87 | const block = await store.backingDb.get(id.toString()) 88 | return store.decode(block) 89 | }, 90 | // Equality test two identifiers, IAMap uses this and because save() returns CIDs we're comparing those 91 | isEqual: (id1, id2) => { 92 | return id1.equals(id2) 93 | }, 94 | isLink (obj) { 95 | return typeof obj === 'number' 96 | } 97 | } 98 | 99 | // Register a murmur3-32 hasher with IAMap 100 | function murmurHasher (key) { 101 | // key is a `Uint8Array` 102 | const b = new Uint8Array(4) 103 | const view = new DataView(b.buffer) 104 | view.setUint32(0, murmurhash3.x86.hash32(key), true) 105 | // we now have a 4-byte hash 106 | return b 107 | } 108 | // Names must match a multicodec name, see https://github.com/multiformats/multicodec/blob/master/table.csv 109 | iamap.registerHasher(0x23 /* 'murmur3-32' */, 32, murmurHasher) 110 | 111 | // recursive async iterator that finds and emits all package.json files found from our parent directory downward 112 | async function * findJs (dir) { 113 | const files = await fs.promises.readdir(dir) 114 | for (const f of files) { 115 | const fp = path.join(dir, f) 116 | const stat = await fs.promises.stat(fp) 117 | if (stat.isFile() && f.endsWith('.js')) { 118 | yield fp 119 | } 120 | if (stat.isDirectory()) { 121 | yield * findJs(fp) 122 | } 123 | } 124 | } 125 | 126 | // Given a directory, find all of the .js files in it and match every instance of require() and extract 127 | // the module being used. We're ignoring modules starting with '.' and also anything after '/' if used. 128 | // Emit [ file, module ] pairs for every valid require() found. 129 | async function * findRequires (dir) { 130 | const requireRe = /require\(['"]([^.][^'"/]*)/g 131 | 132 | for await (let file of findJs(dir)) { 133 | file = path.resolve(process.cwd(), file) // absolute 134 | yield * fs.createReadStream(file) 135 | .pipe(split2({ objectMode: true })) 136 | .pipe(new Transform({ 137 | objectMode: true, 138 | transform (line, enc, callback) { 139 | let match 140 | while ((match = requireRe.exec(line)) != null) { 141 | this.push([file, match[1]]) 142 | } 143 | callback() 144 | } 145 | })) 146 | } 147 | } 148 | 149 | // Simple utility to create or load an IAMap. If `id` is not supplied it'll make a new one, otherwise 150 | // it assumes its a CID 151 | async function createMap (id) { 152 | if (id) { // existing 153 | if (typeof id === 'string') { 154 | id = CID.parse(id) 155 | } 156 | return iamap.load(store, id) 157 | } 158 | // new map with default options, our hasher and custom store 159 | return iamap.create(store, { hashAlg: 0x23 /* 'murmur3-32' */ }) 160 | } 161 | 162 | // --index 163 | async function buildIndex (dir) { 164 | console.log(`Using database at ${dbLocation}`) 165 | process.stdout.write('Building index ') 166 | 167 | let map = await createMap() 168 | 169 | let c = 0 170 | for await (const req of findRequires(dir)) { 171 | if (++c % 1000 === 0) { 172 | process.stdout.write('.') 173 | } 174 | 175 | const [file, mod] = req // findRequires() emits pairs in an array 176 | const listId = await map.get(mod) 177 | let list 178 | if (!listId) { // new module, make a new Set out of a new IAMap 179 | list = await createMap() 180 | } else { // module we've seen before, load it 181 | list = await createMap(listId) 182 | } 183 | // update the Set with `file`, note the `list =` because of the mutation 184 | list = await list.set(file, true) // `true` because we don't care about the value here, we're using it as a Set 185 | // put the new Set's ID as the value of `mod` in our main IAMap, note the `map =` because of the mutation 186 | map = await map.set(mod, list.id) 187 | } 188 | 189 | console.log(`\nComplete! Scanned ${c} files, Map root is ${map.id}`) 190 | console.log(`Search by running again with \`--search ${map.id} \``) 191 | } 192 | 193 | // --search 194 | async function search (mapId, mod) { 195 | console.log(`Using database at ${dbLocation}`) 196 | const map = await createMap(mapId) 197 | const listId = await map.get(mod) 198 | if (listId) { 199 | // if `mod` was found, we should now have an ID of a separate IAMap that is used as a Set 200 | const list = await createMap(listId) 201 | console.log(`'${mod}' is found in:`) 202 | const textDecoder = new TextDecoder() 203 | for await (const f of list.keys()) { // we stored files as keys, so only list the keys 204 | console.log(` ${textDecoder.decode(f)}`) 205 | } 206 | } else { 207 | console.log(`'${mod}' not found`) 208 | } 209 | } 210 | 211 | // --stats 212 | async function stats (mapId) { 213 | console.log(`Using database at ${dbLocation}`) 214 | const map = await createMap(mapId) 215 | let size = 0 // could use map.size() for this 216 | let nodes = 0 217 | let maxDepth = 0 218 | let maxUsedCount = 0 219 | let maxUsed 220 | let files = 0 221 | // map.ids() gives us IDs of the root node and all of its children 222 | for await (const id of map.ids()) { 223 | // instantiate a detached node, we wouldn't normally do this and we certainly wouldn't mutate this 224 | const node = await createMap(id) 225 | nodes++ 226 | if (node.depth > maxDepth) { 227 | maxDepth = node.depth 228 | } 229 | size += node.directEntryCount() // direct entries within buckets of this node (only) 230 | } 231 | 232 | for await (const entry of map.entries()) { // map.entries() gives us every { key, value } pair in this map 233 | const list = await createMap(entry.value) // every value in our map is a CID of a new IAMap used as a Set 234 | const listSize = await list.size() // list.size() is the number of entries in this IAMap (Set) 235 | files += listSize 236 | if (listSize > maxUsedCount) { 237 | maxUsedCount = listSize 238 | maxUsed = entry.key // the key was the module name 239 | } 240 | } 241 | 242 | console.log(`Map comprises ${nodes} nodes, with a maximum depth of ${maxDepth + 1}, holding ${size} entries referencing ${files} files`) 243 | console.log(`Most used module is '${new TextDecoder().decode(maxUsed)}' with ${maxUsedCount} files`) 244 | } 245 | 246 | function printUsage () { 247 | console.error(`Usage: 248 | 249 | level-backed.js --index 250 | - build an index of require()'d modules inside .js files contained in 'dir' (recursively searched) 251 | - returns a map ID that you can use for --search and --stats 252 | 253 | level-backed.js --search 254 | - search an index, identified by 'indexId', for all files that require() a 'module' 255 | 256 | level-backed.js --stats 257 | - print some basic stats of the index identified by 'indexId'`) 258 | } 259 | 260 | if (process.argv[2] === '--index') { 261 | if (!process.argv[3]) { 262 | printUsage() 263 | } 264 | buildIndex(process.argv[3]).catch((err) => console.error(err)) 265 | } else if (process.argv[2] === '--search') { 266 | if (process.argv.length !== 5) { 267 | printUsage() 268 | } 269 | search(process.argv[3], process.argv[4]).catch((err) => console.error(err)) 270 | } else if (process.argv[2] === '--stats') { 271 | if (process.argv.length !== 4) { 272 | printUsage() 273 | } 274 | stats(process.argv[3]).catch((err) => console.error(err)) 275 | } else { 276 | printUsage() 277 | } 278 | -------------------------------------------------------------------------------- /examples/memory-backed.js: -------------------------------------------------------------------------------- 1 | // Copyright Rod Vagg; Licensed under the Apache License, Version 2.0, see README.md for more information 2 | 3 | const fs = require('fs').promises 4 | const path = require('path') 5 | const murmurhash3 = require('murmurhash3js-revisited') 6 | const iamap = require('../') 7 | 8 | /* 9 | 10 | An example memory-backed IAMap 11 | This is not a realistic example because you can just use a `Map` directly, but it's useful for demonstrating 12 | the basics of how it works and can form the basis of something more sophisticated, such as a database-backed 13 | store. 14 | 15 | */ 16 | 17 | // create a fresh memory-backed store on demand 18 | function memoryStore () { 19 | // We're using a hash function here to generate identifiers, this should not be confused with the 'codec' 20 | // that IAMap takes as an option. This is purely for the generation of content hashes, such that identically 21 | // shaped objects passed through `save()` generate the same hash. A content addressed storage system would 22 | // normally perform this function for you. 23 | function hash (obj) { 24 | const stringified = JSON.stringify(obj) 25 | const buf = new TextEncoder().encode(stringified) // murmurhash3js-revisited takes bytes[] 26 | return murmurhash3.x86.hash32(buf) // returns an number 27 | } 28 | const map = new Map() // where objects get stored 29 | 30 | return { 31 | save (obj) { // this can be async 32 | const id = hash(obj) 33 | map.set(id, obj) 34 | return id 35 | }, 36 | load (id) { // this can be async 37 | return map.get(id) 38 | }, 39 | // this needs to work for the type of objects returned by `save()`, which is numbers for our `hash()` function 40 | // so it's a straight compare. If you had a more complex identifier, such as an object or a byte array, you'd 41 | // perform an appropriate comparison here. 42 | isEqual (id1, id2) { 43 | return id1 === id2 44 | }, 45 | isLink (obj) { 46 | return typeof obj === 'number' 47 | } 48 | } 49 | } 50 | 51 | // IAMap doesn't know how to produce a hash for keys by itself, it needs a hash function. We do that by passing in 52 | // a hash function via `registerHasher()`. The hash function should work on an array of bytes (`Uint8Array`) and return 53 | // an array of bytes whose length matches the `hashLength` that we provide to `registerHasher()`. 54 | function murmurHasher (key) { 55 | // key is a `Uint8Array` 56 | const b = new Uint8Array(4) 57 | const view = new DataView(b.buffer) 58 | view.setUint32(0, murmurhash3.x86.hash32(key), true) 59 | // we now have a 4-byte hash 60 | return b 61 | } 62 | 63 | // Names must match a multicodec name, see https://github.com/multiformats/multicodec/blob/master/table.csv 64 | iamap.registerHasher(0x23 /* 'murmur3-32' */, 32, murmurHasher) 65 | 66 | async function memoryBacked () { 67 | const store = memoryStore() // new store 68 | let map = await iamap.create(store, { hashAlg: 0x23 }) // new map with default options, our hasher and custom store 69 | 70 | for await (const pkg of findPackages(path.join(__dirname, '..'))) { 71 | // Store a string key and a JavaScript object as a value, this will work for our store but if we needed to store it 72 | // elsewhere our store.save() is going to be in trouble because the keys and values will show up like they are here. 73 | // In some cases, introducing a link-layer might be in order, only insert links as values and allow them to easily 74 | // resolve in a wrapper layer or somewhere else 75 | // Note also that we are overwriting the `map` object here—IAMap is immutable so the mutation methods return an entirely 76 | // new instance with the changes (CoW style) 77 | map = await map.set(`${pkg.name}@${pkg.version}`, pkg) 78 | } 79 | 80 | const textDecoder = new TextDecoder() 81 | // iterate with `entries()` which has no guarantees of meaningful order 82 | for await (const entry of map.entries()) { 83 | console.log(`${textDecoder.decode(entry.key)}${Array(Math.max(0, 30 - entry.key.length)).join(' ')} ${entry.value.description}`) 84 | } 85 | 86 | console.log(`IAMap serialized in store as ${map.id}`) 87 | } 88 | 89 | // recursive async iterator that finds and emits all package.json files found from our parent directory downward 90 | async function * findPackages (dir) { 91 | const files = await fs.readdir(dir) 92 | for (const f of files) { 93 | const fp = path.join(dir, f) 94 | if (f === 'package.json') { 95 | try { 96 | const pkg = JSON.parse(await fs.readFile(fp, 'utf8')) 97 | if (pkg.version && pkg.name) { 98 | yield pkg 99 | } 100 | } catch (e) {} 101 | } 102 | if ((await fs.stat(fp)).isDirectory()) { 103 | yield * findPackages(fp) 104 | } 105 | } 106 | } 107 | 108 | memoryBacked().catch((err) => console.error(err)) 109 | -------------------------------------------------------------------------------- /iamap.js: -------------------------------------------------------------------------------- 1 | // Copyright Rod Vagg; Licensed under the Apache License, Version 2.0, see README.md for more information 2 | 3 | import { mask, setBit, bitmapHas, index } from './bit-utils.js' 4 | 5 | const defaultBitWidth = 8 // 2^8 = 256 buckets or children per node 6 | const defaultBucketSize = 5 // array size for a bucket of values 7 | 8 | /** 9 | * @template T 10 | * @typedef {import('./interface').Store} Store 11 | */ 12 | /** 13 | * @typedef {import('./interface').Config} Config 14 | * @typedef {import('./interface').Options} Options 15 | * @typedef {import('./interface').SerializedKV} SerializedKV 16 | * @typedef {import('./interface').SerializedElement} SerializedElement 17 | * @typedef {import('./interface').SerializedNode} SerializedNode 18 | * @typedef {import('./interface').SerializedRoot} SerializedRoot 19 | * @typedef {(inp:Uint8Array)=>(Uint8Array|Promise)} Hasher 20 | * @typedef {{ hasher: Hasher, hashBytes: number }[]} Registry 21 | * @typedef {(link:any)=>boolean} IsLink 22 | * @typedef {readonly Element[]} ReadonlyElement 23 | * @typedef {{data?: { found: boolean, elementAt: number, element: Element, bucketIndex?: number, bucketEntry?: KV }, link?: { elementAt: number, element: Element }}} FoundElement 24 | */ 25 | 26 | /** 27 | * @type {Registry} 28 | * @ignore 29 | */ 30 | const hasherRegistry = [] 31 | 32 | const textEncoder = new TextEncoder() 33 | 34 | /** 35 | * @ignore 36 | * @param {boolean} condition 37 | * @param {string} [message] 38 | */ 39 | function assert (condition, message) { 40 | if (!condition) { 41 | throw new Error(message || 'Unexpected error') 42 | } 43 | } 44 | 45 | /** 46 | * ```js 47 | * let map = await iamap.create(store, options) 48 | * ``` 49 | * 50 | * Create a new IAMap instance with a backing store. This operation is asynchronous and returns a `Promise` that 51 | * resolves to a `IAMap` instance. 52 | * 53 | * @name iamap.create 54 | * @function 55 | * @async 56 | * @template T 57 | * @param {Store} store - A backing store for this Map. The store should be able to save and load a serialised 58 | * form of a single node of a IAMap which is provided as a plain object representation. `store.save(node)` takes 59 | * a serialisable node and should return a content address / ID for the node. `store.load(id)` serves the inverse 60 | * purpose, taking a content address / ID as provided by a `save()` operation and returning the serialised form 61 | * of a node which can be instantiated by IAMap. In addition, two identifier handling methods are needed: 62 | * `store.isEqual(id1, id2)` is required to check the equality of the two content addresses / IDs 63 | * (which may be custom for that data type). `store.isLink(obj)` is used to determine if an object is a link type 64 | * that can be used for `load()` operations on the store. It is important that link types be different to standard 65 | * JavaScript arrays and don't share properties used by the serialized form of an IAMap (e.g. such that a 66 | * `typeof obj === 'object' && Array.isArray(obj.data)`) .This is because a node data element may either be a link to 67 | * a child node, or an inlined child node, so `isLink()` should be able to determine if an object is a link, and if not, 68 | * `Array.isArray(obj)` will determine if that data element is a bucket of elements, or the above object check be able 69 | * to determine that an inline child node exists at the data element. 70 | * The `store` object should take the following form: 71 | * `{ async save(node):id, async load(id):node, isEqual(id,id):boolean, isLink(obj):boolean }` 72 | * A `store` should throw an appropriately informative error when a node that is requested does not exist in the backing 73 | * store. 74 | * 75 | * Options: 76 | * - hashAlg (number) - A [multicodec](https://github.com/multiformats/multicodec/blob/master/table.csv) 77 | * hash function identifier, e.g. `0x23` for `murmur3-32`. Hash functions must be registered with {@link iamap.registerHasher}. 78 | * - bitWidth (number, default 8) - The number of bits to extract from the hash to form a data element index at 79 | * each level of the Map, e.g. a bitWidth of 5 will extract 5 bits to be used as the data element index, since 2^5=32, 80 | * each node will store up to 32 data elements (child nodes and/or entry buckets). The maximum depth of the Map is 81 | * determined by `floor((hashBytes * 8) / bitWidth)` where `hashBytes` is the number of bytes the hash function 82 | * produces, e.g. `hashBytes=32` and `bitWidth=5` yields a maximum depth of 51 nodes. The maximum `bitWidth` 83 | * currently allowed is `8` which will store 256 data elements in each node. 84 | * - bucketSize (number, default 5) - The maximum number of collisions acceptable at each level of the Map. A 85 | * collision in the `bitWidth` index at a given depth will result in entries stored in a bucket (array). Once the 86 | * bucket exceeds `bucketSize`, a new child node is created for that index and all entries in the bucket are 87 | * pushed 88 | * 89 | * @param {Options} options - Options for this IAMap 90 | * @param {Uint8Array} [map] - for internal use 91 | * @param {number} [depth] - for internal use 92 | * @param {Element[]} [data] - for internal use 93 | */ 94 | export async function create (store, options, map, depth, data) { 95 | // map, depth and data are intended for internal use 96 | const newNode = new IAMap(store, options, map, depth, data) 97 | return save(store, newNode) 98 | } 99 | 100 | /** 101 | * ```js 102 | * let map = await iamap.load(store, id) 103 | * ``` 104 | * 105 | * Create a IAMap instance loaded from a serialised form in a backing store. See {@link iamap.create}. 106 | * 107 | * @name iamap.load 108 | * @function 109 | * @async 110 | * @template T 111 | * @param {Store} store - A backing store for this Map. See {@link iamap.create}. 112 | * @param {any} id - An content address / ID understood by the backing `store`. 113 | * @param {number} [depth=0] 114 | * @param {Options} [options] 115 | */ 116 | export async function load (store, id, depth = 0, options) { 117 | // depth and options are internal arguments that the user doesn't need to interact with 118 | if (depth !== 0 && typeof options !== 'object') { 119 | throw new Error('Cannot load() without options at depth > 0') 120 | } 121 | const serialized = await store.load(id) 122 | return fromSerializable(store, id, serialized, options, depth) 123 | } 124 | 125 | /** 126 | * ```js 127 | * iamap.registerHasher(hashAlg, hashBytes, hasher) 128 | * ``` 129 | * 130 | * Register a new hash function. IAMap has no hash functions by default, at least one is required to create a new 131 | * IAMap. 132 | * 133 | * @name iamap.registerHasher 134 | * @function 135 | * @param {number} hashAlg - A [multicodec](https://github.com/multiformats/multicodec/blob/master/table.csv) hash 136 | * function identifier **number**, e.g. `0x23` for `murmur3-32`. 137 | * @param {number} hashBytes - The number of bytes to use from the result of the `hasher()` function (e.g. `32`) 138 | * @param {Hasher} hasher - A hash function that takes a `Uint8Array` derived from the `key` values used for this 139 | * Map and returns a `Uint8Array` (or a `Uint8Array`-like, such that each data element of the array contains a single byte value). The function 140 | * may or may not be asynchronous but will be called with an `await`. 141 | */ 142 | export function registerHasher (hashAlg, hashBytes, hasher) { 143 | if (!Number.isInteger(hashAlg)) { 144 | throw new Error('Invalid `hashAlg`') 145 | } 146 | if (!Number.isInteger(hashBytes)) { 147 | throw new TypeError('Invalid `hashBytes`') 148 | } 149 | if (typeof hasher !== 'function') { 150 | throw new TypeError('Invalid `hasher` function }') 151 | } 152 | hasherRegistry[hashAlg] = { hashBytes, hasher } 153 | } 154 | 155 | // simple stable key/value representation 156 | /** 157 | * @ignore 158 | */ 159 | class KV { 160 | /** 161 | * @ignore 162 | * @param {Uint8Array} key 163 | * @param {any} value 164 | */ 165 | constructor (key, value) { 166 | this.key = key 167 | this.value = value 168 | } 169 | 170 | /** 171 | * @ignore 172 | * @returns {SerializedKV} 173 | */ 174 | toSerializable () { 175 | return [this.key, this.value] 176 | } 177 | } 178 | 179 | /** 180 | * @ignore 181 | * @param {SerializedKV} obj 182 | * @returns {KV} 183 | */ 184 | KV.fromSerializable = function (obj) { 185 | assert(Array.isArray(obj)) 186 | assert(obj.length === 2) 187 | return new KV(obj[0], obj[1]) 188 | } 189 | 190 | // a element in the data array that each node holds, each element could be either a container of 191 | // an array (bucket) of KVs or a link to a child node 192 | class Element { 193 | /** 194 | * @ignore 195 | * @param {KV[]} [bucket] 196 | * @param {any} [link] 197 | */ 198 | constructor (bucket, link) { 199 | this.bucket = bucket || null 200 | this.link = link !== undefined ? link : null 201 | assert((this.bucket === null) === (this.link !== null)) 202 | } 203 | 204 | /** 205 | * @ignore 206 | * @returns {SerializedElement} 207 | */ 208 | toSerializable () { 209 | if (this.bucket) { 210 | return this.bucket.map((c) => { 211 | return c.toSerializable() 212 | }) 213 | } else { 214 | assert(!IAMap.isIAMap(this.link)) // TODO: inline here 215 | return this.link 216 | } 217 | } 218 | } 219 | 220 | /** 221 | * @ignore 222 | * @param {IsLink} isLink 223 | * @param {any} obj 224 | * @returns {Element} 225 | */ 226 | Element.fromSerializable = function (isLink, obj) { 227 | if (isLink(obj)) { 228 | return new Element(undefined, obj) 229 | } else if (Array.isArray(obj)) { 230 | return new Element(obj.map(KV.fromSerializable)) 231 | } 232 | throw new Error('Unexpected error: badly formed data element') 233 | } 234 | 235 | /** 236 | * Immutable Asynchronous Map 237 | * 238 | * The `IAMap` constructor should not be used directly. Use `iamap.create()` or `iamap.load()` to instantiate. 239 | * 240 | * @class 241 | * @template T 242 | * @property {any} id - A unique identifier for this `IAMap` instance. IDs are generated by the backing store and 243 | * are returned on `save()` operations. 244 | * @property {number} config.hashAlg - The hash function used by this `IAMap` instance. See {@link iamap.create} for more 245 | * details. 246 | * @property {number} config.bitWidth - The number of bits used at each level of this `IAMap`. See {@link iamap.create} 247 | * for more details. 248 | * @property {number} config.bucketSize - TThe maximum number of collisions acceptable at each level of the Map. 249 | * @property {Uint8Array} [map=Uint8Array] - Bitmap indicating which slots are occupied by data entries or child node links, 250 | * each data entry contains an bucket of entries. Must be the appropriate size for `config.bitWidth` 251 | * (`2 ** config.bitWith / 8` bytes). 252 | * @property {number} [depth=0] - Depth of the current node in the IAMap, `depth` is used to extract bits from the 253 | * key hashes to locate slots 254 | * @property {Array} [data=[]] - Array of data elements (an internal `Element` type), each of which contains a 255 | * bucket of entries or an ID of a child node 256 | * See {@link iamap.create} for more details. 257 | */ 258 | export class IAMap { 259 | /** 260 | * @ignore 261 | * @param {Store} store 262 | * @param {Options} [options] 263 | * @param {Uint8Array} [map] 264 | * @param {number} [depth] 265 | * @param {Element[]} [data] 266 | */ 267 | constructor (store, options, map, depth, data) { 268 | if (!store || typeof store.save !== 'function' || 269 | typeof store.load !== 'function' || 270 | typeof store.isLink !== 'function' || 271 | typeof store.isEqual !== 'function') { 272 | throw new TypeError('Invalid `store` option, must be of type: { save(node):id, load(id):node, isEqual(id,id):boolean, isLink(obj):boolean }') 273 | } 274 | this.store = store 275 | 276 | /** 277 | * @ignore 278 | * @type {any|null} 279 | */ 280 | this.id = null 281 | this.config = buildConfig(options) 282 | 283 | const hashBytes = hasherRegistry[this.config.hashAlg].hashBytes 284 | 285 | if (map !== undefined && !(map instanceof Uint8Array)) { 286 | throw new TypeError('`map` must be a Uint8Array') 287 | } 288 | const mapLength = Math.ceil(Math.pow(2, this.config.bitWidth) / 8) 289 | if (map !== undefined && map.length !== mapLength) { 290 | throw new Error('`map` must be a Uint8Array of length ' + mapLength) 291 | } 292 | this.map = map || new Uint8Array(mapLength) 293 | 294 | if (depth !== undefined && (!Number.isInteger(depth) || depth < 0)) { 295 | throw new TypeError('`depth` must be an integer >= 0') 296 | } 297 | this.depth = depth || 0 298 | if (this.depth > Math.floor((hashBytes * 8) / this.config.bitWidth)) { 299 | // our hasher only has `hashBytes` to work with and we take off `bitWidth` bits with each level 300 | // e.g. 32-byte hash gives us a maximum depth of 51 levels 301 | throw new Error('Overflow: maximum tree depth reached') 302 | } 303 | 304 | /** 305 | * @ignore 306 | * @type {ReadonlyElement} 307 | */ 308 | this.data = Object.freeze(data || []) 309 | for (const e of this.data) { 310 | if (!(e instanceof Element)) { 311 | throw new TypeError('`data` array must contain only `Element` types') 312 | } 313 | } 314 | } 315 | 316 | /** 317 | * Asynchronously create a new `IAMap` instance identical to this one but with `key` set to `value`. 318 | * 319 | * @param {(string|Uint8Array)} key - A key for the `value` being set whereby that same `value` may 320 | * be retrieved with a `get()` operation with the same `key`. The type of the `key` object should either be a 321 | * `Uint8Array` or be convertable to a `Uint8Array` via `TextEncoder. 322 | * @param {any} value - Any value that can be stored in the backing store. A value could be a serialisable object 323 | * or an address or content address or other kind of link to the actual value. 324 | * @param {Uint8Array} [_cachedHash] - for internal use 325 | * @returns {Promise>} A `Promise` containing a new `IAMap` that contains the new key/value pair. 326 | * @async 327 | */ 328 | async set (key, value, _cachedHash) { 329 | if (!(key instanceof Uint8Array)) { 330 | key = textEncoder.encode(key) 331 | } 332 | const hash = _cachedHash instanceof Uint8Array ? _cachedHash : await hasher(this)(key) 333 | const bitpos = mask(hash, this.depth, this.config.bitWidth) 334 | 335 | if (bitmapHas(this.map, bitpos)) { // should be in a bucket in this node 336 | const { data, link } = findElement(this, bitpos, key) 337 | if (data) { 338 | if (data.found) { 339 | /* c8 ignore next 3 */ 340 | if (data.bucketIndex === undefined || data.bucketEntry === undefined) { 341 | throw new Error('Unexpected error') 342 | } 343 | if (data.bucketEntry.value === value) { 344 | return this // no change, identical value 345 | } 346 | // replace entry for this key with a new value 347 | // note that === will fail for two complex objects representing the same data so we may end up 348 | // with a node of the same ID anyway 349 | return updateBucket(this, data.elementAt, data.bucketIndex, key, value) 350 | } else { 351 | /* c8 ignore next 3 */ 352 | if (!data.element.bucket) { 353 | throw new Error('Unexpected error') 354 | } 355 | if (data.element.bucket.length >= this.config.bucketSize) { 356 | // too many collisions at this level, replace a bucket with a child node 357 | return (await replaceBucketWithNode(this, data.elementAt)).set(key, value, hash) 358 | } 359 | // insert into the bucket and sort it 360 | return updateBucket(this, data.elementAt, -1, key, value) 361 | } 362 | } else if (link) { 363 | const child = await load(this.store, link.element.link, this.depth + 1, this.config) 364 | assert(!!child) 365 | const newChild = await child.set(key, value, hash) 366 | return updateNode(this, link.elementAt, newChild) 367 | /* c8 ignore next 3 */ 368 | } else { 369 | throw new Error('Unexpected error') 370 | } 371 | } else { // we don't have an element for this hash portion, make one 372 | return addNewElement(this, bitpos, key, value) 373 | } 374 | } 375 | 376 | /** 377 | * Asynchronously find and return a value for the given `key` if it exists within this `IAMap`. 378 | * 379 | * @param {string|Uint8Array} key - A key for the value being sought. See {@link IAMap#set} for 380 | * details about acceptable `key` types. 381 | * @param {Uint8Array} [_cachedHash] - for internal use 382 | * @returns {Promise} A `Promise` that resolves to the value being sought if that value exists within this `IAMap`. If the 383 | * key is not found in this `IAMap`, the `Promise` will resolve to `undefined`. 384 | * @async 385 | */ 386 | async get (key, _cachedHash) { 387 | if (!(key instanceof Uint8Array)) { 388 | key = textEncoder.encode(key) 389 | } 390 | const hash = _cachedHash instanceof Uint8Array ? _cachedHash : await hasher(this)(key) 391 | const bitpos = mask(hash, this.depth, this.config.bitWidth) 392 | if (bitmapHas(this.map, bitpos)) { // should be in a bucket in this node 393 | const { data, link } = findElement(this, bitpos, key) 394 | if (data) { 395 | if (data.found) { 396 | /* c8 ignore next 3 */ 397 | if (data.bucketIndex === undefined || data.bucketEntry === undefined) { 398 | throw new Error('Unexpected error') 399 | } 400 | return data.bucketEntry.value 401 | } 402 | return undefined // not found 403 | } else if (link) { 404 | const child = await load(this.store, link.element.link, this.depth + 1, this.config) 405 | assert(!!child) 406 | return await child.get(key, hash) 407 | /* c8 ignore next 3 */ 408 | } else { 409 | throw new Error('Unexpected error') 410 | } 411 | } else { // we don't have an element for this hash portion, not found 412 | return undefined 413 | } 414 | 415 | /* 416 | const traversal = traverseGet(this, key, this.store.isEqual, this.store.isLink, this.depth) 417 | while (true) { 418 | const nextId = traversal.traverse() 419 | if (!nextId) { 420 | return traversal.value() 421 | } 422 | const child = await this.store.load(nextId) 423 | assert(!!child) 424 | traversal.next(child) 425 | } 426 | */ 427 | } 428 | 429 | /** 430 | * Asynchronously find and return a boolean indicating whether the given `key` exists within this `IAMap` 431 | * 432 | * @param {string|Uint8Array} key - A key to check for existence within this `IAMap`. See 433 | * {@link IAMap#set} for details about acceptable `key` types. 434 | * @returns {Promise} A `Promise` that resolves to either `true` or `false` depending on whether the `key` exists 435 | * within this `IAMap`. 436 | * @async 437 | */ 438 | async has (key) { 439 | return (await this.get(key)) !== undefined 440 | } 441 | 442 | /** 443 | * Asynchronously create a new `IAMap` instance identical to this one but with `key` and its associated 444 | * value removed. If the `key` does not exist within this `IAMap`, this instance of `IAMap` is returned. 445 | * 446 | * @param {string|Uint8Array} key - A key to remove. See {@link IAMap#set} for details about 447 | * acceptable `key` types. 448 | * @param {Uint8Array} [_cachedHash] - for internal use 449 | * @returns {Promise>} A `Promise` that resolves to a new `IAMap` instance without the given `key` or the same `IAMap` 450 | * instance if `key` does not exist within it. 451 | * @async 452 | */ 453 | async delete (key, _cachedHash) { 454 | if (!(key instanceof Uint8Array)) { 455 | key = textEncoder.encode(key) 456 | } 457 | const hash = _cachedHash instanceof Uint8Array ? _cachedHash : await hasher(this)(key) 458 | assert(hash instanceof Uint8Array) 459 | const bitpos = mask(hash, this.depth, this.config.bitWidth) 460 | 461 | if (bitmapHas(this.map, bitpos)) { // should be in a bucket in this node 462 | const { data, link } = findElement(this, bitpos, key) 463 | if (data) { 464 | if (data.found) { 465 | /* c8 ignore next 3 */ 466 | if (data.bucketIndex === undefined) { 467 | throw new Error('Unexpected error') 468 | } 469 | if (this.depth !== 0 && this.directNodeCount() === 0 && this.directEntryCount() === this.config.bucketSize + 1) { 470 | // current node will only have this.config.bucketSize entries spread across its buckets 471 | // and no child nodes, so wrap up the remaining nodes in a fresh IAMap at depth 0, it will 472 | // bubble up to either become the new root node or be unpacked by a higher level 473 | return collapseIntoSingleBucket(this, hash, data.elementAt, data.bucketIndex) 474 | } else { 475 | // we'll either have more entries left than this.config.bucketSize or we're at the root node 476 | // so this is a simple bucket removal, no collapsing needed (root nodes can't be collapsed) 477 | const lastInBucket = this.data.length === 1 478 | // we have at least one child node or too many entries in buckets to be collapsed 479 | const newData = removeFromBucket(this.data, data.elementAt, lastInBucket, data.bucketIndex) 480 | let newMap = this.map 481 | if (lastInBucket) { 482 | newMap = setBit(newMap, bitpos, false) 483 | } 484 | return create(this.store, this.config, newMap, this.depth, newData) 485 | } 486 | } else { 487 | // key would be located here according to hash, but we don't have it 488 | return this 489 | } 490 | } else if (link) { 491 | const child = await load(this.store, link.element.link, this.depth + 1, this.config) 492 | assert(!!child) 493 | const newChild = await child.delete(key, hash) 494 | if (this.store.isEqual(newChild.id, link.element.link)) { // no modification 495 | return this 496 | } 497 | 498 | assert(newChild.data.length > 0) // something probably went wrong in the map block above 499 | 500 | if (newChild.directNodeCount() === 0 && newChild.directEntryCount() === this.config.bucketSize) { 501 | // child got collapsed 502 | if (this.directNodeCount() === 1 && this.directEntryCount() === 0) { 503 | // we only had one node to collapse and the child was collapsible so end up acting the same 504 | // as the child, bubble it up and it either becomes the new root or finds a parent to collapse 505 | // in to (next section) 506 | return newChild 507 | } else { 508 | // extract data elements from this returned node and merge them into ours 509 | return collapseNodeInline(this, bitpos, newChild) 510 | } 511 | } else { 512 | // simple node replacement with edited child 513 | return updateNode(this, link.elementAt, newChild) 514 | } 515 | /* c8 ignore next 3 */ 516 | } else { 517 | throw new Error('Unexpected error') 518 | } 519 | } else { // we don't have an element for this hash portion 520 | return this 521 | } 522 | } 523 | 524 | /** 525 | * Asynchronously count the number of key/value pairs contained within this `IAMap`, including its children. 526 | * 527 | * @returns {Promise} A `Promise` with a `number` indicating the number of key/value pairs within this `IAMap` instance. 528 | * @async 529 | */ 530 | async size () { 531 | let c = 0 532 | for (const e of this.data) { 533 | if (e.bucket) { 534 | c += e.bucket.length 535 | } else { 536 | const child = await load(this.store, e.link, this.depth + 1, this.config) 537 | c += await child.size() 538 | } 539 | } 540 | return c 541 | } 542 | 543 | /** 544 | * Asynchronously emit all keys that exist within this `IAMap`, including its children. This will cause a full 545 | * traversal of all nodes. 546 | * 547 | * @returns {AsyncGenerator} An async iterator that yields keys. All keys will be in `Uint8Array` format regardless of which 548 | * format they were inserted via `set()`. 549 | * @async 550 | */ 551 | async * keys () { 552 | for (const e of this.data) { 553 | if (e.bucket) { 554 | for (const kv of e.bucket) { 555 | yield kv.key 556 | } 557 | } else { 558 | const child = await load(this.store, e.link, this.depth + 1, this.config) 559 | yield * child.keys() 560 | } 561 | } 562 | 563 | // yield * traverseKV(this, 'keys', this.store.isLink) 564 | } 565 | 566 | /** 567 | * Asynchronously emit all values that exist within this `IAMap`, including its children. This will cause a full 568 | * traversal of all nodes. 569 | * 570 | * @returns {AsyncGenerator} An async iterator that yields values. 571 | * @async 572 | */ 573 | async * values () { 574 | for (const e of this.data) { 575 | if (e.bucket) { 576 | for (const kv of e.bucket) { 577 | yield kv.value 578 | } 579 | } else { 580 | const child = await load(this.store, e.link, this.depth + 1, this.config) 581 | yield * child.values() 582 | } 583 | } 584 | 585 | // yield * traverseKV(this, 'values', this.store.isLink) 586 | } 587 | 588 | /** 589 | * Asynchronously emit all { key, value } pairs that exist within this `IAMap`, including its children. This will 590 | * cause a full traversal of all nodes. 591 | * 592 | * @returns {AsyncGenerator<{ key: Uint8Array, value: any}>} An async iterator that yields objects with the properties `key` and `value`. 593 | * @async 594 | */ 595 | async * entries () { 596 | for (const e of this.data) { 597 | if (e.bucket) { 598 | for (const kv of e.bucket) { 599 | yield { key: kv.key, value: kv.value } 600 | } 601 | } else { 602 | const child = await load(this.store, e.link, this.depth + 1, this.config) 603 | yield * child.entries() 604 | } 605 | } 606 | 607 | // yield * traverseKV(this, 'entries', this.store.isLink) 608 | } 609 | 610 | /** 611 | * Asynchronously emit the IDs of this `IAMap` and all of its children. 612 | * 613 | * @returns {AsyncGenerator} An async iterator that yields the ID of this `IAMap` and all of its children. The type of ID is 614 | * determined by the backing store which is responsible for generating IDs upon `save()` operations. 615 | */ 616 | async * ids () { 617 | yield this.id 618 | for (const e of this.data) { 619 | if (e.link) { 620 | const child = await load(this.store, e.link, this.depth + 1, this.config) 621 | yield * child.ids() 622 | } 623 | } 624 | } 625 | 626 | /** 627 | * Returns a serialisable form of this `IAMap` node. The internal representation of this local node is copied into a plain 628 | * JavaScript `Object` including a representation of its data array that the key/value pairs it contains as well as 629 | * the identifiers of child nodes. 630 | * Root nodes (depth==0) contain the full map configuration information, while intermediate and leaf nodes contain only 631 | * data that cannot be inferred by traversal from a root node that already has this data (hashAlg and bucketSize -- bitWidth 632 | * is inferred by the length of the `map` byte array). 633 | * The backing store can use this representation to create a suitable serialised form. When loading from the backing store, 634 | * `IAMap` expects to receive an object with the same layout from which it can instantiate a full `IAMap` object. Where 635 | * root nodes contain the full set of data and intermediate and leaf nodes contain just the required data. 636 | * For content addressable backing stores, it is expected that the same data in this serialisable form will always produce 637 | * the same identifier. 638 | * Note that the `map` property is a `Uint8Array` so will need special handling for some serialization forms (e.g. JSON). 639 | * 640 | * Root node form: 641 | * ``` 642 | * { 643 | * hashAlg: number 644 | * bucketSize: number 645 | * hamt: [Uint8Array, Array] 646 | * } 647 | * ``` 648 | * 649 | * Intermediate and leaf node form: 650 | * ``` 651 | * [Uint8Array, Array] 652 | * ``` 653 | * 654 | * The `Uint8Array` in both forms is the 'map' used to identify the presence of an element in this node. 655 | * 656 | * The second element in the tuple in both forms, `Array`, is an elements array a mix of either buckets or links: 657 | * 658 | * * A bucket is an array of two elements, the first being a `key` of type `Uint8Array` and the second a `value` 659 | * or whatever type has been provided in `set()` operations for this `IAMap`. 660 | * * A link is an object of the type that the backing store provides upon `save()` operations and can be identified 661 | * with `isLink()` calls. 662 | * 663 | * Buckets and links are differentiated by their "kind": a bucket is an array while a link is a "link" kind as dictated 664 | * by the backing store. We use `Array.isArray()` and `store.isLink()` to perform this differentiation. 665 | * 666 | * @returns {SerializedNode|SerializedRoot} An object representing the internal state of this local `IAMap` node, including its links to child nodes 667 | * if any. 668 | */ 669 | toSerializable () { 670 | const map = this.map 671 | const data = this.data.map((/** @type {Element} */ e) => { 672 | return e.toSerializable() 673 | }) 674 | /** 675 | * @ignore 676 | * @type {SerializedNode} 677 | */ 678 | const hamt = [map, data] 679 | if (this.depth !== 0) { 680 | return hamt 681 | } 682 | /** 683 | * @ignore 684 | * @type {SerializedElement} 685 | */ 686 | return { 687 | hashAlg: this.config.hashAlg, 688 | bucketSize: this.config.bucketSize, 689 | hamt 690 | } 691 | } 692 | 693 | /** 694 | * Calculate the number of entries locally stored by this node. Performs a scan of local buckets and adds up 695 | * their size. 696 | * 697 | * @returns {number} A number representing the number of local entries. 698 | */ 699 | directEntryCount () { 700 | return this.data.reduce((/** @type {number} */ p, /** @type {Element} */ c) => { 701 | return p + (c.bucket ? c.bucket.length : 0) 702 | }, 0) 703 | } 704 | 705 | /** 706 | * Calculate the number of child nodes linked by this node. Performs a scan of the local entries and tallies up the 707 | * ones containing links to child nodes. 708 | * 709 | * @returns {number} A number representing the number of direct child nodes 710 | */ 711 | directNodeCount () { 712 | return this.data.reduce((/** @type {number} */ p, /** @type {Element} */ c) => { 713 | return p + (c.link ? 1 : 0) 714 | }, 0) 715 | } 716 | 717 | /** 718 | * Asynchronously perform a check on this node and its children that it is in canonical format for the current data. 719 | * As this uses `size()` to calculate the total number of entries in this node and its children, it performs a full 720 | * scan of nodes and therefore incurs a load and deserialisation cost for each child node. 721 | * A `false` result from this method suggests a flaw in the implemetation. 722 | * 723 | * @async 724 | * @returns {Promise} A Promise with a boolean value indicating whether this IAMap is correctly formatted. 725 | */ 726 | async isInvariant () { 727 | const size = await this.size() 728 | const entryArity = this.directEntryCount() 729 | const nodeArity = this.directNodeCount() 730 | const arity = entryArity + nodeArity 731 | let sizePredicate = 2 // 2 == 'more than one' 732 | if (nodeArity === 0) { 733 | sizePredicate = Math.min(2, entryArity) // 0, 1 or 2=='more than one' 734 | } 735 | 736 | const inv1 = size - entryArity >= 2 * (arity - entryArity) 737 | const inv2 = arity === 0 ? sizePredicate === 0 : true 738 | const inv3 = (arity === 1 && entryArity === 1) ? sizePredicate === 1 : true 739 | const inv4 = arity >= 2 ? sizePredicate === 2 : true 740 | const inv5 = nodeArity >= 0 && entryArity >= 0 && ((entryArity + nodeArity) === arity) 741 | 742 | return inv1 && inv2 && inv3 && inv4 && inv5 743 | } 744 | 745 | /** 746 | * A convenience shortcut to {@link iamap.fromSerializable} that uses this IAMap node instance's backing `store` and 747 | * configuration `options`. Intended to be used to instantiate child IAMap nodes from a root IAMap node. 748 | * 749 | * @param {any} id An optional ID for the instantiated IAMap node. See {@link iamap.fromSerializable}. 750 | * @param {any} serializable The serializable form of an IAMap node to be instantiated. 751 | * @param {number} [depth=0] The depth of the IAMap node. See {@link iamap.fromSerializable}. 752 | */ 753 | fromChildSerializable (id, serializable, depth) { 754 | return fromSerializable(this.store, id, serializable, this.config, depth) 755 | } 756 | } 757 | 758 | /** 759 | * store a new node and assign it an ID 760 | * @ignore 761 | * @template T 762 | * @param {Store} store 763 | * @param {IAMap} newNode 764 | * @returns {Promise>} 765 | */ 766 | async function save (store, newNode) { 767 | const id = await store.save(newNode.toSerializable()) 768 | newNode.id = id 769 | return newNode 770 | } 771 | 772 | /** 773 | * // utility function to avoid duplication since it's used across get(), set() and delete() 774 | * { bucket: { found: true, elementAt, element, bucketIndex, bucketEntry } } 775 | * { bucket: { found: false, elementAt, element } } 776 | * { link: { elementAt, element } } 777 | * @ignore 778 | * @template T 779 | * @param {IAMap} node 780 | * @param {number} bitpos 781 | * @param {Uint8Array} key 782 | * @returns {FoundElement} 783 | */ 784 | function findElement (node, bitpos, key) { 785 | const elementAt = index(node.map, bitpos) 786 | const element = node.data[elementAt] 787 | assert(!!element) 788 | if (element.bucket) { // data element 789 | for (let bucketIndex = 0; bucketIndex < element.bucket.length; bucketIndex++) { 790 | const bucketEntry = element.bucket[bucketIndex] 791 | if (byteCompare(bucketEntry.key, key) === 0) { 792 | return { data: { found: true, elementAt, element, bucketIndex, bucketEntry } } 793 | } 794 | } 795 | return { data: { found: false, elementAt, element } } 796 | } 797 | assert(!!element.link) 798 | return { link: { elementAt, element } } 799 | } 800 | 801 | /** 802 | * new element for this node, i.e. first time this hash portion has been seen here 803 | * @ignore 804 | * @template T 805 | * @param {IAMap} node 806 | * @param {number} bitpos 807 | * @param {Uint8Array} key 808 | * @param {any} value 809 | * @returns {Promise>} 810 | */ 811 | async function addNewElement (node, bitpos, key, value) { 812 | const insertAt = index(node.map, bitpos) 813 | const newData = node.data.slice() 814 | newData.splice(insertAt, 0, new Element([new KV(key, value)])) 815 | const newMap = setBit(node.map, bitpos, true) 816 | return create(node.store, node.config, newMap, node.depth, newData) 817 | } 818 | 819 | /** 820 | * update an existing bucket with a new k/v pair 821 | * @ignore 822 | * @template T 823 | * @param {IAMap} node 824 | * @param {number} elementAt 825 | * @param {number} bucketAt 826 | * @param {Uint8Array} key 827 | * @param {any} value 828 | * @returns {Promise>} 829 | */ 830 | async function updateBucket (node, elementAt, bucketAt, key, value) { 831 | const oldElement = node.data[elementAt] 832 | /* c8 ignore next 3 */ 833 | if (!oldElement.bucket) { 834 | throw new Error('Unexpected error') 835 | } 836 | const newElement = new Element(oldElement.bucket.slice()) 837 | const newKv = new KV(key, value) 838 | /* c8 ignore next 3 */ 839 | if (!newElement.bucket) { 840 | throw new Error('Unexpected error') 841 | } 842 | if (bucketAt === -1) { 843 | newElement.bucket.push(newKv) 844 | // in-bucket sort is required to maintain a canonical state 845 | newElement.bucket.sort((/** @type {KV} */ a, /** @type {KV} */ b) => byteCompare(a.key, b.key)) 846 | } else { 847 | newElement.bucket[bucketAt] = newKv 848 | } 849 | const newData = node.data.slice() 850 | newData[elementAt] = newElement 851 | return create(node.store, node.config, node.map, node.depth, newData) 852 | } 853 | 854 | /** 855 | * overflow of a bucket means it has to be replaced with a child node, tricky surgery 856 | * @ignore 857 | * @template T 858 | * @param {IAMap} node 859 | * @param {number} elementAt 860 | * @returns {Promise>} 861 | */ 862 | async function replaceBucketWithNode (node, elementAt) { 863 | let newNode = new IAMap(node.store, node.config, undefined, node.depth + 1) 864 | const element = node.data[elementAt] 865 | assert(!!element) 866 | /* c8 ignore next 3 */ 867 | if (!element.bucket) { 868 | throw new Error('Unexpected error') 869 | } 870 | for (const c of element.bucket) { 871 | newNode = await newNode.set(c.key, c.value) 872 | } 873 | newNode = await save(node.store, newNode) 874 | const newData = node.data.slice() 875 | newData[elementAt] = new Element(undefined, newNode.id) 876 | return create(node.store, node.config, node.map, node.depth, newData) 877 | } 878 | 879 | /** 880 | * similar to addNewElement() but for new child nodes 881 | * @ignore 882 | * @template T 883 | * @param {IAMap} node 884 | * @param {number} elementAt 885 | * @param {IAMap} newChild 886 | * @returns {Promise>} 887 | */ 888 | async function updateNode (node, elementAt, newChild) { 889 | assert(!!newChild.id) 890 | const newElement = new Element(undefined, newChild.id) 891 | const newData = node.data.slice() 892 | newData[elementAt] = newElement 893 | return create(node.store, node.config, node.map, node.depth, newData) 894 | } 895 | 896 | // take a node, extract all of its local entries and put them into a new node with a single 897 | // bucket; used for collapsing a node and sending it upward 898 | /** 899 | * @ignore 900 | * @template T 901 | * @param {IAMap} node 902 | * @param {Uint8Array} hash 903 | * @param {number} elementAt 904 | * @param {number} bucketIndex 905 | * @returns {Promise>} 906 | */ 907 | function collapseIntoSingleBucket (node, hash, elementAt, bucketIndex) { 908 | // pretend it's depth=0 (it may end up being) and only 1 bucket 909 | const newMap = setBit(new Uint8Array(node.map.length), mask(hash, 0, node.config.bitWidth), true) 910 | /** 911 | * @ignore 912 | * @type {KV[]} 913 | */ 914 | const newBucket = node.data.reduce((/** @type {KV[]} */ p, /** @type {Element} */ c, /** @type {number} */ i) => { 915 | if (i === elementAt) { 916 | /* c8 ignore next 3 */ 917 | if (!c.bucket) { 918 | throw new Error('Unexpected error') 919 | } 920 | if (c.bucket.length === 1) { // only element in bucket, skip it 921 | return p 922 | } else { 923 | // there's more in this bucket, make a temporary one, remove it and concat it 924 | const tmpBucket = c.bucket.slice() 925 | tmpBucket.splice(bucketIndex, 1) 926 | return p.concat(tmpBucket) 927 | } 928 | } else { 929 | /* c8 ignore next 3 */ 930 | if (!c.bucket) { 931 | throw new Error('Unexpected error') 932 | } 933 | return p.concat(c.bucket) 934 | } 935 | }, /** @type {KV[]} */ []) 936 | newBucket.sort((a, b) => byteCompare(a.key, b.key)) 937 | const newElement = new Element(newBucket) 938 | return create(node.store, node.config, newMap, 0, [newElement]) 939 | } 940 | 941 | // simple delete from an existing bucket in this node 942 | /** 943 | * @ignore 944 | * @param {ReadonlyElement} data 945 | * @param {number} elementAt 946 | * @param {boolean} lastInBucket 947 | * @param {number} bucketIndex 948 | * @returns {Element[]} 949 | */ 950 | function removeFromBucket (data, elementAt, lastInBucket, bucketIndex) { 951 | const newData = data.slice() 952 | if (!lastInBucket) { 953 | // bucket will not be empty, remove only the element from it 954 | const oldElement = data[elementAt] 955 | /* c8 ignore next 3 */ 956 | if (!oldElement.bucket) { 957 | throw new Error('Unexpected error') 958 | } 959 | const newElement = new Element(oldElement.bucket.slice()) 960 | /* c8 ignore next 3 */ 961 | if (!newElement.bucket) { 962 | throw new Error('Unexpected error') 963 | } 964 | newElement.bucket.splice(bucketIndex, 1) 965 | newData.splice(elementAt, 1, newElement) // replace old bucket 966 | } else { 967 | // empty bucket, just remove it 968 | newData.splice(elementAt, 1) 969 | } 970 | return newData 971 | } 972 | 973 | /** 974 | * a node has bubbled up from a recursive delete() and we need to extract its 975 | * contents and insert it into ours 976 | * @ignore 977 | * @template T 978 | * @param {IAMap} node 979 | * @param {number} bitpos 980 | * @param {IAMap} newNode 981 | * @returns {Promise>} 982 | */ 983 | async function collapseNodeInline (node, bitpos, newNode) { 984 | // assume the newNode has a single bucket and it's sorted and ready to replace the place 985 | // it had in node's element array 986 | assert(newNode.data.length === 1) 987 | /* c8 ignore next 3 */ 988 | if (!newNode.data[0].bucket) { 989 | throw new Error('Unexpected error') 990 | } 991 | const newBucket = newNode.data[0].bucket.slice() 992 | const newElement = new Element(newBucket) 993 | const elementIndex = index(node.map, bitpos) 994 | const newData = node.data.slice() 995 | newData[elementIndex] = newElement 996 | 997 | return create(node.store, node.config, node.map, node.depth, newData) 998 | } 999 | 1000 | /** 1001 | * @ignore 1002 | * @param {Options} [options] 1003 | * @returns {Config} 1004 | */ 1005 | function buildConfig (options) { 1006 | /** 1007 | * @ignore 1008 | * @type {Config} 1009 | */ 1010 | const config = {} 1011 | 1012 | if (!options) { 1013 | throw new TypeError('Invalid `options` object') 1014 | } 1015 | 1016 | if (!Number.isInteger(options.hashAlg)) { 1017 | throw new TypeError('Invalid `hashAlg` option') 1018 | } 1019 | if (!hasherRegistry[options.hashAlg]) { 1020 | throw new TypeError(`Unknown hashAlg: '${options.hashAlg}'`) 1021 | } 1022 | config.hashAlg = options.hashAlg 1023 | 1024 | if (options.bitWidth !== undefined) { 1025 | if (Number.isInteger(options.bitWidth)) { 1026 | if (options.bitWidth < 3 || options.bitWidth > 16) { 1027 | throw new TypeError('Invalid `bitWidth` option, must be between 3 and 16') 1028 | } 1029 | config.bitWidth = options.bitWidth 1030 | } else { 1031 | throw new TypeError('Invalid `bitWidth` option') 1032 | } 1033 | } else { 1034 | config.bitWidth = defaultBitWidth 1035 | } 1036 | 1037 | if (options.bucketSize !== undefined) { 1038 | if (Number.isInteger(options.bucketSize)) { 1039 | if (options.bucketSize < 2) { 1040 | throw new TypeError('Invalid `bucketSize` option') 1041 | } 1042 | config.bucketSize = options.bucketSize 1043 | } else { 1044 | throw new TypeError('Invalid `bucketSize` option') 1045 | } 1046 | } else { 1047 | config.bucketSize = defaultBucketSize 1048 | } 1049 | 1050 | return config 1051 | } 1052 | 1053 | /** 1054 | * Determine if a serializable object is an IAMap root type, can be used to assert whether a data block is 1055 | * an IAMap before trying to instantiate it. 1056 | * 1057 | * @name iamap.isRootSerializable 1058 | * @function 1059 | * @param {any} serializable An object that may be a serialisable form of an IAMap root node 1060 | * @returns {boolean} An indication that the serialisable form is or is not an IAMap root node 1061 | */ 1062 | export function isRootSerializable (serializable) { 1063 | return typeof serializable === 'object' && 1064 | Number.isInteger(serializable.hashAlg) && 1065 | Number.isInteger(serializable.bucketSize) && 1066 | Array.isArray(serializable.hamt) && 1067 | isSerializable(serializable.hamt) 1068 | } 1069 | 1070 | /** 1071 | * Determine if a serializable object is an IAMap node type, can be used to assert whether a data block is 1072 | * an IAMap node before trying to instantiate it. 1073 | * This should pass for both root nodes as well as child nodes 1074 | * 1075 | * @name iamap.isSerializable 1076 | * @function 1077 | * @param {any} serializable An object that may be a serialisable form of an IAMap node 1078 | * @returns {boolean} An indication that the serialisable form is or is not an IAMap node 1079 | */ 1080 | export function isSerializable (serializable) { 1081 | if (Array.isArray(serializable)) { 1082 | return serializable.length === 2 && serializable[0] instanceof Uint8Array && Array.isArray(serializable[1]) 1083 | } 1084 | return isRootSerializable(serializable) 1085 | } 1086 | 1087 | /** 1088 | * Instantiate an IAMap from a valid serialisable form of an IAMap node. The serializable should be the same as 1089 | * produced by {@link IAMap#toSerializable}. 1090 | * Serialised forms of root nodes must satisfy both {@link iamap.isRootSerializable} and {@link iamap.isSerializable}. For 1091 | * root nodes, the `options` parameter will be ignored and the `depth` parameter must be the default value of `0`. 1092 | * Serialised forms of non-root nodes must satisfy {@link iamap.isSerializable} and have a valid `options` parameter and 1093 | * a non-`0` `depth` parameter. 1094 | * 1095 | * @name iamap.fromSerializable 1096 | * @function 1097 | * @template T 1098 | * @param {Store} store A backing store for this Map. See {@link iamap.create}. 1099 | * @param {any} id An optional ID for the instantiated IAMap node. Unlike {@link iamap.create}, 1100 | * `fromSerializable()` does not `save()` a newly created IAMap node so an ID is not generated for it. If one is 1101 | * required for downstream purposes it should be provided, if the value is `null` or `undefined`, `node.id` will 1102 | * be `null` but will remain writable. 1103 | * @param {any} serializable The serializable form of an IAMap node to be instantiated 1104 | * @param {Options} [options=null] An options object for IAMap child node instantiation. Will be ignored for root 1105 | * node instantiation (where `depth` = `0`) See {@link iamap.create}. 1106 | * @param {number} [depth=0] The depth of the IAMap node. Where `0` is the root node and any `>0` number is a child 1107 | * node. 1108 | * @returns {IAMap} 1109 | */ 1110 | export function fromSerializable (store, id, serializable, options, depth = 0) { 1111 | /** 1112 | * @ignore 1113 | * @type {SerializedNode} 1114 | */ 1115 | let hamt 1116 | if (depth === 0) { // even if options were supplied, ignore them and use what's in the serializable 1117 | if (!isRootSerializable(serializable)) { 1118 | throw new Error('Loaded object does not appear to be an IAMap root (depth==0)') 1119 | } 1120 | // don't use passed-in options 1121 | options = serializableToOptions(serializable) 1122 | hamt = serializable.hamt 1123 | } else { 1124 | if (!isSerializable(serializable)) { 1125 | throw new Error('Loaded object does not appear to be an IAMap node (depth>0)') 1126 | } 1127 | hamt = serializable 1128 | } 1129 | const data = hamt[1].map(Element.fromSerializable.bind(null, store.isLink)) 1130 | const node = new IAMap(store, options, hamt[0], depth, data) 1131 | if (id != null) { 1132 | node.id = id 1133 | } 1134 | return node 1135 | } 1136 | 1137 | /** 1138 | * @ignore 1139 | * @param {any} serializable 1140 | * @returns {Config} 1141 | */ 1142 | function serializableToOptions (serializable) { 1143 | return { 1144 | hashAlg: serializable.hashAlg, 1145 | bitWidth: Math.log2(serializable.hamt[0].length * 8), // inverse of (2**bitWidth) / 8 1146 | bucketSize: serializable.bucketSize 1147 | } 1148 | } 1149 | 1150 | /** 1151 | * @template T 1152 | * @param {IAMap | any} node 1153 | * @returns {boolean} 1154 | */ 1155 | IAMap.isIAMap = function isIAMap (node) { 1156 | return node instanceof IAMap 1157 | } 1158 | 1159 | /** 1160 | * internal utility to fetch a map instance's hash function 1161 | * 1162 | * @ignore 1163 | * @template T 1164 | * @param {IAMap} map 1165 | * @returns {Hasher} 1166 | */ 1167 | function hasher (map) { 1168 | return hasherRegistry[map.config.hashAlg].hasher 1169 | } 1170 | 1171 | /** 1172 | * @ignore 1173 | * @param {Uint8Array} b1 1174 | * @param {Uint8Array} b2 1175 | * @returns {number} 1176 | */ 1177 | function byteCompare (b1, b2) { 1178 | let x = b1.length 1179 | let y = b2.length 1180 | 1181 | for (let i = 0, len = Math.min(x, y); i < len; ++i) { 1182 | if (b1[i] !== b2[i]) { 1183 | x = b1[i] 1184 | y = b2[i] 1185 | break 1186 | } 1187 | } 1188 | if (x < y) { 1189 | return -1 1190 | } 1191 | if (y < x) { 1192 | return 1 1193 | } 1194 | return 0 1195 | } 1196 | -------------------------------------------------------------------------------- /interface.ts: -------------------------------------------------------------------------------- 1 | // store using a link type `T` 2 | export interface Store { 3 | save(node: any): Promise, 4 | load(id: T): Promise, 5 | isLink(link: T): boolean, 6 | isEqual(link1: T, link2: T): boolean, 7 | } 8 | 9 | export interface Options { 10 | bitWidth?: number, 11 | bucketSize?: number, 12 | hashAlg: number 13 | } 14 | 15 | export interface Config { 16 | bitWidth: number, 17 | bucketSize: number, 18 | hashAlg: number 19 | } 20 | 21 | export type SerializedKV = [Uint8Array, any] 22 | 23 | export type SerializedElement = SerializedKV | any /* link */ 24 | 25 | type NodeMap = Uint8Array 26 | type NodeData = SerializedElement[] 27 | 28 | export type SerializedNode = [NodeMap, NodeData] 29 | 30 | export interface SerializedRoot { 31 | hashAlg: number, 32 | bucketSize: number, 33 | hamt: SerializedNode 34 | } 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iamap", 3 | "version": "4.0.0", 4 | "description": "An **I**mmutable **A**synchronous **Map**.", 5 | "type": "module", 6 | "main": "iamap.js", 7 | "types": "./types/iamap.d.ts", 8 | "scripts": { 9 | "lint": "standard *.js test/*.js", 10 | "build": "npm run docs && npm run build:types", 11 | "build:types": "tsc --build", 12 | "test:node": "c8 --check-coverage --branches 100 --functions 100 --lines 100 mocha test/*-test.js", 13 | "test:browser": "polendina --page --worker --serviceworker --cleanup test/*-test.js", 14 | "test": "npm run lint && npm run build:types && npm run test:node && npm run test:browser", 15 | "coverage": "c8 --reporter=html mocha test/*-test.js && npx st -d coverage -p 8080", 16 | "docs": "jsdoc4readme --readme iamap.js" 17 | }, 18 | "author": "Rod (http://r.va.gg/)", 19 | "license": "Apache-2.0", 20 | "dependencies": { 21 | "bit-sequence": "^1.1.0" 22 | }, 23 | "devDependencies": { 24 | "@rvagg/chai-as-promised": "^8.0.0", 25 | "@types/chai": "^4.3.11", 26 | "@types/mocha": "^10.0.6", 27 | "c8": "^9.0.0", 28 | "chai": "^5.0.0", 29 | "jsdoc4readme": "^1.4.0", 30 | "mocha": "^10.2.0", 31 | "murmurhash3js-revisited": "^3.0.0", 32 | "polendina": "^3.2.1", 33 | "standard": "^17.1.0", 34 | "typescript": "^5.3.3" 35 | }, 36 | "repository": { 37 | "type": "git", 38 | "url": "git://github.com/rvagg/iamap.git" 39 | }, 40 | "typesVersions": { 41 | "*": { 42 | "*": [ 43 | "types/*" 44 | ], 45 | "types/*": [ 46 | "types/*" 47 | ] 48 | } 49 | }, 50 | "release": { 51 | "branches": [ 52 | "master" 53 | ], 54 | "plugins": [ 55 | [ 56 | "@semantic-release/commit-analyzer", 57 | { 58 | "preset": "conventionalcommits", 59 | "releaseRules": [ 60 | { 61 | "breaking": true, 62 | "release": "major" 63 | }, 64 | { 65 | "revert": true, 66 | "release": "patch" 67 | }, 68 | { 69 | "type": "feat", 70 | "release": "minor" 71 | }, 72 | { 73 | "type": "fix", 74 | "release": "patch" 75 | }, 76 | { 77 | "type": "chore", 78 | "release": "patch" 79 | }, 80 | { 81 | "type": "docs", 82 | "release": "patch" 83 | }, 84 | { 85 | "type": "test", 86 | "release": "patch" 87 | }, 88 | { 89 | "scope": "no-release", 90 | "release": false 91 | } 92 | ] 93 | } 94 | ], 95 | [ 96 | "@semantic-release/release-notes-generator", 97 | { 98 | "preset": "conventionalcommits", 99 | "presetConfig": { 100 | "types": [ 101 | { 102 | "type": "feat", 103 | "section": "Features" 104 | }, 105 | { 106 | "type": "fix", 107 | "section": "Bug Fixes" 108 | }, 109 | { 110 | "type": "chore", 111 | "section": "Trivial Changes" 112 | }, 113 | { 114 | "type": "docs", 115 | "section": "Trivial Changes" 116 | }, 117 | { 118 | "type": "test", 119 | "section": "Tests" 120 | } 121 | ] 122 | } 123 | } 124 | ], 125 | "@semantic-release/changelog", 126 | "@semantic-release/npm", 127 | "@semantic-release/github", 128 | "@semantic-release/git" 129 | ] 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /test/basic-test.js: -------------------------------------------------------------------------------- 1 | // Copyright Rod Vagg; Licensed under the Apache License, Version 2.0, see README.md for more information 2 | 3 | /* eslint-env mocha */ 4 | 5 | import { assert } from 'chai' 6 | import { murmurHasher, identityHasher, memoryStore, fromHex, toHex } from './common.js' 7 | import * as iamap from '../iamap.js' 8 | 9 | iamap.registerHasher(0x23 /* 'murmur3-32' */, 32, murmurHasher) 10 | iamap.registerHasher(0x00 /* 'identity' */, 32, identityHasher) // not recommended 11 | 12 | describe('Basics', () => { 13 | it('empty object', async () => { 14 | const store = memoryStore() 15 | const map = await iamap.create(store, { hashAlg: 0x23 /* 'murmur3-32' */ }) 16 | assert.deepEqual(map.toSerializable(), { 17 | hashAlg: 0x23 /* 'murmur3-32' */, 18 | bucketSize: 5, 19 | hamt: [new Uint8Array(32) /* 2**8, bitWidth of 8 */, []] 20 | }) 21 | assert.strictEqual(store.map.size, 1) 22 | assert.strictEqual(store.saves, 1) 23 | assert.strictEqual(store.loads, 0) 24 | 25 | assert.strictEqual(await map.size(), 0) 26 | assert.strictEqual(await map.isInvariant(), true) 27 | }) 28 | 29 | it('test basic set/get', async () => { 30 | const store = memoryStore() 31 | const map = await iamap.create(store, { hashAlg: 0x23 /* 'murmur3-32' */ }) 32 | assert.strictEqual(toHex(map.map), '0'.repeat(64)) // sanity 33 | const newMap = await map.set('foo', 'bar') 34 | 35 | assert.strictEqual(await newMap.get('foo'), 'bar') 36 | assert.isUndefined(await map.get('foo')) 37 | assert.strictEqual(await newMap.has('foo'), true) 38 | assert.strictEqual(await map.has('nope'), false) 39 | assert.strictEqual(await map.has('foo'), false) 40 | 41 | // original map isn't mutated 42 | assert.deepEqual(map.toSerializable(), { 43 | hashAlg: 0x23 /* 'murmur3-32' */, 44 | bucketSize: 5, 45 | hamt: [new Uint8Array(32), []] 46 | }) 47 | assert.deepEqual(newMap.toSerializable(), { 48 | hashAlg: 0x23 /* 'murmur3-32' */, 49 | bucketSize: 5, 50 | hamt: [Uint8Array.from(newMap.map), [[[new TextEncoder().encode('foo'), 'bar']]]] 51 | }) 52 | assert.notStrictEqual(toHex(newMap.map), '0'.repeat(64)) 53 | assert.strictEqual(store.map.size, 2) 54 | assert.strictEqual(store.saves, 2) 55 | assert.strictEqual(store.loads, 0) 56 | 57 | assert.strictEqual(await map.size(), 0) 58 | assert.strictEqual(await newMap.size(), 1) 59 | assert.strictEqual(await map.isInvariant(), true) 60 | assert.strictEqual(await newMap.isInvariant(), true) 61 | }) 62 | 63 | it('test basic set/set-same/get', async () => { 64 | const store = memoryStore() 65 | const map = await iamap.create(store, { hashAlg: 0x23 /* 'murmur3-32' */ }) 66 | assert.strictEqual(toHex(map.map), '0'.repeat(64)) // sanity 67 | const newMap1 = await map.set('foo', 'bar') 68 | const newMap2 = await newMap1.set('foo', 'bar') 69 | 70 | assert.strictEqual(await newMap1.get('foo'), 'bar') 71 | assert.isUndefined(await map.get('foo')) 72 | assert.strictEqual(newMap1, newMap2) // identity match, should be the same object 73 | 74 | // original map isn't mutated 75 | assert.deepEqual(map.toSerializable(), { 76 | hashAlg: 0x23 /* 'murmur3-32' */, 77 | bucketSize: 5, 78 | hamt: [new Uint8Array(32), []] 79 | }) 80 | assert.deepEqual(newMap1.toSerializable(), { 81 | hashAlg: 0x23 /* 'murmur3-32' */, 82 | bucketSize: 5, 83 | hamt: [Uint8Array.from(newMap1.map), [[[new TextEncoder().encode('foo'), 'bar']]]] 84 | }) 85 | assert.notStrictEqual(toHex(newMap1.map), '0'.repeat(64)) 86 | assert.strictEqual(store.map.size, 2) 87 | assert.strictEqual(store.saves, 2) 88 | assert.strictEqual(store.loads, 0) 89 | 90 | assert.strictEqual(await map.size(), 0) 91 | assert.strictEqual(await map.isInvariant(), true) 92 | assert.strictEqual(await newMap1.size(), 1) 93 | assert.strictEqual(await newMap1.isInvariant(), true) 94 | assert.strictEqual(await newMap2.size(), 1) 95 | assert.strictEqual(await newMap2.isInvariant(), true) 96 | }) 97 | 98 | it('test basic set/update/get', async () => { 99 | const store = memoryStore() 100 | const map = await iamap.create(store, { hashAlg: 0x23 /* 'murmur3-32' */ }) 101 | assert.strictEqual(toHex(map.map), '0'.repeat(64)) // sanity 102 | const newMap1 = await map.set('foo', 'bar') 103 | const newMap2 = await newMap1.set('foo', 'baz') 104 | 105 | assert.strictEqual(await newMap1.get('foo'), 'bar') 106 | assert.strictEqual(await newMap2.get('foo'), 'baz') 107 | assert.isUndefined(await map.get('foo')) 108 | assert.notStrictEqual(newMap1, newMap2) // identity not match 109 | 110 | // original map isn't mutated 111 | assert.deepEqual(map.toSerializable(), { 112 | hashAlg: 0x23 /* 'murmur3-32' */, 113 | bucketSize: 5, 114 | hamt: [new Uint8Array(32), []] 115 | }) 116 | assert.deepEqual(newMap1.toSerializable(), { 117 | hashAlg: 0x23 /* 'murmur3-32' */, 118 | bucketSize: 5, 119 | hamt: [Uint8Array.from(newMap1.map), [[[new TextEncoder().encode('foo'), 'bar']]]] 120 | }) 121 | assert.notStrictEqual(toHex(newMap1.map), '0'.repeat(64)) 122 | assert.deepEqual(newMap2.toSerializable(), { 123 | hashAlg: 0x23 /* 'murmur3-32' */, 124 | bucketSize: 5, 125 | hamt: [Uint8Array.from(newMap1.map), [[[new TextEncoder().encode('foo'), 'baz']]]] 126 | }) 127 | assert.notStrictEqual(toHex(newMap1.map), '0'.repeat(64)) 128 | assert.strictEqual(store.map.size, 3) 129 | assert.strictEqual(store.saves, 3) 130 | assert.strictEqual(store.loads, 0) 131 | 132 | assert.strictEqual(await map.size(), 0) 133 | assert.strictEqual(await map.isInvariant(), true) 134 | assert.strictEqual(await newMap1.size(), 1) 135 | assert.strictEqual(await newMap1.isInvariant(), true) 136 | assert.strictEqual(await newMap2.size(), 1) 137 | assert.strictEqual(await newMap2.isInvariant(), true) 138 | }) 139 | 140 | it('test basic set/get/delete', async () => { 141 | const store = memoryStore() 142 | const map = await iamap.create(store, { hashAlg: 0x23 /* 'murmur3-32' */ }) 143 | const setMap = await map.set('foo', 'bar') 144 | const deleteMap = await setMap.delete('foo') 145 | 146 | assert.isUndefined(await deleteMap.get('foo')) 147 | assert.strictEqual(await setMap.get('foo'), 'bar') 148 | assert.isUndefined(await map.get('foo')) 149 | assert.strictEqual(await setMap.delete('nope'), setMap) // identity match, same map returned 150 | 151 | // original map isn't mutated 152 | assert.deepEqual(map.toSerializable(), { 153 | hashAlg: 0x23 /* 'murmur3-32' */, 154 | bucketSize: 5, 155 | hamt: [new Uint8Array(32), []] 156 | }) 157 | assert.deepEqual(setMap.toSerializable(), { 158 | hashAlg: 0x23 /* 'murmur3-32' */, 159 | bucketSize: 5, 160 | hamt: [Uint8Array.from(setMap.map), [[[new TextEncoder().encode('foo'), 'bar']]]] 161 | }) 162 | // should be back to square one 163 | assert.deepEqual(deleteMap.toSerializable(), map.toSerializable()) 164 | // 3 saves but only 2 entries because deleteMap is a duplicate of map 165 | assert.strictEqual(store.map.size, 2) 166 | assert.strictEqual(store.saves, 3) 167 | assert.strictEqual(store.loads, 0) 168 | 169 | assert.strictEqual(await map.size(), 0) 170 | assert.strictEqual(await map.isInvariant(), true) 171 | assert.strictEqual(await setMap.size(), 1) 172 | assert.strictEqual(await setMap.isInvariant(), true) 173 | assert.strictEqual(await deleteMap.size(), 0) 174 | assert.strictEqual(await deleteMap.isInvariant(), true) 175 | }) 176 | 177 | /* 178 | * NOTE ABOUT IDENTITY HASH TESTS 179 | * With identity hashes we can control the index at each level but we have to construct the 180 | * key carefully. If we choose a bitWidth of 4, that's 2 halves of an 8 bit number, so we 181 | * can put together 2 depth indexes by shifting the first one to the left by 4 bits and adding 182 | * the second to it. So `5 << 4 | 2` sets up a 2 indexes, 5 and 2, represented in 4 bits each. 183 | */ 184 | 185 | it('test predictable single level fill', async () => { 186 | const store = memoryStore() 187 | let map = await iamap.create(store, { hashAlg: 0x00 /* 'identity' */, bitWidth: 4, bucketSize: 3 }) 188 | // bitWidth of 4 yields 16 buckets, we can use 'identity' hash to feed keys that we know 189 | // will go into certain slots 190 | for (let i = 0; i < 16; i++) { 191 | map = await map.set(Uint8Array.from([i << 4, 0]), `value0x${i}`) 192 | } 193 | 194 | for (let i = 0; i < 16; i++) { 195 | assert.strictEqual(await map.get(Uint8Array.from([i << 4, 0])), `value0x${i}`) 196 | } 197 | 198 | // inspect internals 199 | assert.strictEqual(map.data.length, 16) 200 | map.data.forEach((e, i) => { 201 | assert.strictEqual(e.link, null) 202 | assert.ok(Array.isArray(e.bucket)) 203 | // @ts-ignore 204 | assert.strictEqual(e.bucket.length, 1) 205 | // @ts-ignore 206 | assert.strictEqual(e.bucket[0].value, `value0x${i}`) 207 | }) 208 | 209 | // fill it right up 210 | for (let i = 0; i < 16; i++) { 211 | map = await map.set(Uint8Array.from([i << 4, 1]), `value1x${i}`) 212 | map = await map.set(Uint8Array.from([i << 4, 2]), `value2x${i}`) 213 | } 214 | 215 | for (let i = 0; i < 16; i++) { 216 | for (let j = 0; j < 3; j++) { 217 | assert.strictEqual(await map.get(Uint8Array.from([i << 4, j])), `value${j}x${i}`) 218 | } 219 | } 220 | 221 | // inspect internals, we should have 16 buckets with 3 entries each, filling up a single node with no children 222 | assert.strictEqual(map.data.length, 16) 223 | map.data.forEach((e, i) => { 224 | assert.strictEqual(e.link, null) 225 | assert.ok(Array.isArray(e.bucket)) 226 | // @ts-ignore 227 | assert.strictEqual(e.bucket.length, 3) 228 | // @ts-ignore 229 | assert.strictEqual(e.bucket[0].value, `value0x${i}`) 230 | // @ts-ignore 231 | assert.strictEqual(e.bucket[1].value, `value1x${i}`) 232 | // @ts-ignore 233 | assert.strictEqual(e.bucket[2].value, `value2x${i}`) 234 | }) 235 | }) 236 | 237 | it('test predictable fill vertical and collapse', async () => { 238 | const store = memoryStore() 239 | const options = { hashAlg: 0x00 /* 'identity' */, bitWidth: 4, bucketSize: 2 } 240 | let map = await iamap.create(store, options) 241 | 242 | const k = (2 << 4) | 2 243 | // an 8-bit value with `2` in each of the 4-bit halves, for a `bitWidth` of 4 we are going to collide at 244 | // the position `2` of each level that we provide it 245 | 246 | map = await map.set(Uint8Array.from([k, k, k, 1 << 4]), 'pos2+1') 247 | map = await map.set(Uint8Array.from([k, k, k, 2 << 4]), 'pos2+2') 248 | 249 | // check that we have filled our first level, even though we asked for position 2, `data` is compressed so it still 250 | // only has one element 251 | async function validateBaseForm (/** @type {iamap.IAMap} */ map) { 252 | assert.strictEqual(await map.get(Uint8Array.from([k, k, k, 1 << 4])), 'pos2+1') 253 | assert.strictEqual(await map.get(Uint8Array.from([k, k, k, 2 << 4])), 'pos2+2') 254 | 255 | assert.strictEqual(toHex(map.map), toHex(Uint8Array.from([0b100, 0]))) // data at position 2 but not 1 or 0 256 | assert.strictEqual(map.data.length, 1) 257 | assert.strictEqual(map.data[0].link, null) 258 | assert.ok(Array.isArray(map.data[0].bucket)) 259 | // @ts-ignore 260 | assert.strictEqual(map.data[0].bucket.length, 2) 261 | // @ts-ignore 262 | assert.strictEqual(map.data[0].bucket[0].value, 'pos2+1') 263 | // @ts-ignore 264 | assert.strictEqual(map.data[0].bucket[1].value, 'pos2+2') 265 | 266 | assert.strictEqual(await map.isInvariant(), true) 267 | assert.strictEqual(await map.size(), 2) 268 | } 269 | await validateBaseForm(map) 270 | 271 | // the more we push in with `k` the more we collide and force creation of child nodes to contain them 272 | 273 | map = await map.set(Uint8Array.from([k, k, k, 3 << 4]), 'pos2+3') 274 | 275 | assert.strictEqual(toHex(map.map), toHex(Uint8Array.from([0b100, 0]))) // position 2 276 | assert.strictEqual(map.data.length, 1) 277 | assert.strictEqual(map.data[0].bucket, null) 278 | assert.strictEqual(typeof map.data[0].link, 'number') // what's returned by store.save() 279 | 280 | let child = map 281 | // we can traverse down 5 more levels on the first, and only element 282 | // because of [k,k,k,k] - each k is 8 bytes so 2 levels of 4 bytes each 283 | // the 6th level should be where we find our data because we have non-colliding hash portions 284 | for (let i = 0; i < 6; i++) { 285 | assert.strictEqual(toHex(child.map), toHex(Uint8Array.from([0b100, 0]))) // position 2 286 | assert.strictEqual(child.data.length, 1) 287 | assert.strictEqual(child.data[0].bucket, null) 288 | assert.strictEqual(typeof child.data[0].link, 'number') 289 | child = await iamap.load(store, child.data[0].link, i + 1, options) 290 | } 291 | // at the 7th level they all have a different hash portion: 1,2,3 so they should be in separate buckets 292 | assert.strictEqual(child.data.length, 3) 293 | assert.strictEqual(toHex(child.map), toHex(Uint8Array.from([0b1110, 0]))) // data at positions 1,2,3, but not 0 294 | for (let i = 0; i < 3; i++) { 295 | assert.strictEqual(child.data[i].link, null) 296 | assert.ok(Array.isArray(child.data[i].bucket)) 297 | // @ts-ignore 298 | assert.strictEqual(child.data[i].bucket.length, 1) 299 | // @ts-ignore 300 | assert.strictEqual(child.data[i].bucket[0].value, `pos2+${i + 1}`) 301 | } 302 | 303 | assert.strictEqual(await map.isInvariant(), true) 304 | assert.strictEqual(await map.size(), 3) 305 | 306 | // while we have a deep tree, let's test a delete for a missing element at a known deep node 307 | assert.strictEqual(await map.delete(Uint8Array.from([k, k, k, 4 << 4])), map) 308 | 309 | // delete 'pos2+3' and we should be back where we started with just the two at the top level in the same bucket 310 | map = await map.delete(Uint8Array.from([k, k, k, 3 << 4])) 311 | await validateBaseForm(map) 312 | 313 | // put the awkward one back to re-create the 7-node depth 314 | map = await map.set(Uint8Array.from([k, k, k, 3 << 4]), 'pos2+3') 315 | // put one at level 5 so we don't collapse all the way 316 | map = await map.set(Uint8Array.from([k, k, 0, 0]), 'pos2+0+0') 317 | assert.strictEqual(await map.size(), 4) 318 | // delete awkward 3rd 319 | map = await map.delete(Uint8Array.from([k, k, k, 3 << 4])) 320 | 321 | assert.strictEqual(await map.get(Uint8Array.from([k, k, k, 1 << 4])), 'pos2+1') 322 | assert.strictEqual(await map.get(Uint8Array.from([k, k, k, 2 << 4])), 'pos2+2') 323 | assert.strictEqual(await map.get(Uint8Array.from([k, k, 0, 0])), 'pos2+0+0') 324 | 325 | assert.strictEqual(await map.size(), 3) 326 | 327 | child = map 328 | // 4 levels should be the same 329 | for (let i = 0; i < 4; i++) { 330 | assert.strictEqual(toHex(child.map), toHex(Uint8Array.from([0b100, 0]))) // position 2 331 | assert.strictEqual(child.data.length, 1) 332 | assert.strictEqual(child.data[0].bucket, null) 333 | assert.strictEqual(typeof child.data[0].link, 'number') 334 | child = await iamap.load(store, child.data[0].link, i + 1, options) 335 | } 336 | 337 | assert.strictEqual(toHex(child.map), toHex(Uint8Array.from([0b101, 0]))) // data at position 2 and 0 338 | assert.strictEqual(child.data.length, 2) 339 | assert.strictEqual(child.data[0].link, null) 340 | assert.ok(Array.isArray(child.data[0].bucket)) 341 | // @ts-ignore 342 | assert.strictEqual(child.data[0].bucket.length, 1) 343 | // @ts-ignore 344 | assert.strictEqual(child.data[0].bucket[0].value, 'pos2+0+0') 345 | assert.strictEqual(child.data[1].link, null) 346 | assert.ok(Array.isArray(child.data[1].bucket)) 347 | // @ts-ignore 348 | assert.strictEqual(child.data[1].bucket.length, 2) 349 | // @ts-ignore 350 | assert.strictEqual(child.data[1].bucket[0].value, 'pos2+1') 351 | // @ts-ignore 352 | assert.strictEqual(child.data[1].bucket[1].value, 'pos2+2') 353 | 354 | assert.strictEqual(await map.isInvariant(), true) 355 | assert.strictEqual(await map.size(), 3) 356 | }) 357 | 358 | it('test predictable fill vertical, switched delete', async () => { 359 | const store = memoryStore() 360 | const options = { hashAlg: 0x00 /* 'identity' */, bitWidth: 4, bucketSize: 2 } 361 | let map = await iamap.create(store, options) 362 | const k = (2 << 4) | 2 363 | // 3 entries at the lowest node, one part way back up, like last test 364 | map = await map.set(Uint8Array.from([k, k, k, 1 << 4]), 'pos2+1') 365 | map = await map.set(Uint8Array.from([k, k, k, 2 << 4]), 'pos2+2') 366 | map = await map.set(Uint8Array.from([k, k, k, 3 << 4]), 'pos2+3') 367 | map = await map.set(Uint8Array.from([k, k, 0, 0]), 'pos2+0+0') 368 | 369 | // now delete one of the lowest to force a different tree form at the mid level 370 | map = await map.delete(Uint8Array.from([k, k, k, 2 << 4])) 371 | 372 | let child = map 373 | // 4 levels should be the same 374 | for (let i = 0; i < 4; i++) { 375 | assert.strictEqual(toHex(child.map), toHex(Uint8Array.from([0b100, 0]))) // position 2 376 | assert.strictEqual(child.data.length, 1) 377 | assert.strictEqual(child.data[0].bucket, null) 378 | assert.strictEqual(typeof child.data[0].link, 'number') 379 | child = await iamap.load(store, child.data[0].link, i + 1, options) 380 | } 381 | 382 | // last level should have 2 buckets but with a bucket in 0 and a node in 2 383 | assert.strictEqual(toHex(child.map), toHex(Uint8Array.from([0b101, 0]))) // data at position 2 and 0 384 | assert.strictEqual(child.data.length, 2) 385 | assert.strictEqual(child.data[0].link, null) 386 | assert.ok(Array.isArray(child.data[0].bucket)) 387 | // @ts-ignore 388 | assert.strictEqual(child.data[0].bucket.length, 1) 389 | // @ts-ignore 390 | assert.strictEqual(child.data[0].bucket[0].value, 'pos2+0+0') 391 | assert.strictEqual(child.data[1].link, null) 392 | assert.ok(Array.isArray(child.data[1].bucket)) 393 | // @ts-ignore 394 | assert.strictEqual(child.data[1].bucket.length, 2) 395 | // @ts-ignore 396 | assert.strictEqual(child.data[1].bucket[0].value, 'pos2+1') 397 | // @ts-ignore 398 | assert.strictEqual(child.data[1].bucket[1].value, 'pos2+3') 399 | 400 | assert.strictEqual(await map.isInvariant(), true) 401 | assert.strictEqual(await map.size(), 3) 402 | }) 403 | 404 | it('test predictable fill vertical, larger buckets', async () => { 405 | const store = memoryStore() 406 | const options = { hashAlg: 0x00 /* 'identity' */, bitWidth: 4, bucketSize: 4 } 407 | let map = await iamap.create(store, options) 408 | const k = (6 << 4) | 6 // let's try index 6 now 409 | 410 | // we're trying to trigger a compaction of a node which has a bucket of >1 entries, the first 411 | // 4 entries here form a bucket at the lowest node, the 5th is in its own bucket 412 | // removing one of the 4 should collapse that node up into the parent node, but no further 413 | // because there will be >4 thanks to the last 4 in this list 414 | map = await map.set(Uint8Array.from([k, (1 << 4) | 1, 0]), 'pos6+1+1') 415 | const pos612key = Uint8Array.from([k, (1 << 4) | 1, 1 << 4]) 416 | map = await map.set(pos612key, 'pos6+1+2') 417 | map = await map.set(Uint8Array.from([k, (1 << 4) | 1, 2 << 4]), 'pos6+1+3') 418 | map = await map.set(Uint8Array.from([k, (1 << 4) | 1, 3 << 4]), 'pos6+1+4') 419 | map = await map.set(Uint8Array.from([k, (1 << 4) | 2, 5 << 4]), 'pos6+1+5') 420 | map = await map.set(Uint8Array.from([k, 2 << 4]), 'pos6+2') 421 | map = await map.set(Uint8Array.from([k, 3 << 4]), 'pos6+3') 422 | map = await map.set(Uint8Array.from([k, 4 << 4]), 'pos6+4') 423 | map = await map.set(Uint8Array.from([k, 5 << 4]), 'pos6+5') 424 | 425 | // now delete one of the lowest to force a different tree form at the mid level 426 | map = await map.delete(pos612key) 427 | 428 | let child = map 429 | // 4 levels should be the same 430 | for (let i = 0; i < 2; i++) { 431 | assert.strictEqual(toHex(child.map), toHex(Uint8Array.from([0b1000000, 0]))) // position 6 432 | assert.strictEqual(child.data.length, 1) 433 | assert.strictEqual(child.data[0].bucket, null) 434 | assert.strictEqual(typeof child.data[0].link, 'number') 435 | child = await iamap.load(store, child.data[0].link, i + 1, options) 436 | } 437 | 438 | // last level should have 2 buckets but with a bucket in 0 and a node in 2 439 | assert.strictEqual(toHex(child.map), toHex(Uint8Array.from([0b111110, 0]))) // data in postions 1-5 440 | assert.strictEqual(child.data.length, 5) 441 | assert.strictEqual(child.data[1].link, null) 442 | assert.ok(Array.isArray(child.data[1].bucket)) 443 | assert.strictEqual(child.data[0].link, null) 444 | assert.strictEqual(child.data[1].link, null) 445 | assert.strictEqual(child.data[2].link, null) 446 | assert.strictEqual(child.data[3].link, null) 447 | assert.strictEqual(child.data[4].link, null) 448 | assert.ok(Array.isArray(child.data[0].bucket)) 449 | // @ts-ignore 450 | assert.strictEqual(child.data[0].bucket.length, 4) 451 | // @ts-ignore 452 | assert.strictEqual(child.data[0].bucket[0].value, 'pos6+1+1') 453 | // @ts-ignore 454 | assert.strictEqual(child.data[0].bucket[1].value, 'pos6+1+3') 455 | // @ts-ignore 456 | assert.strictEqual(child.data[0].bucket[2].value, 'pos6+1+4') 457 | // @ts-ignore 458 | assert.strictEqual(child.data[0].bucket[3].value, 'pos6+1+5') 459 | // @ts-ignore 460 | assert.strictEqual(child.data[1].bucket[0].value, 'pos6+2') 461 | // @ts-ignore 462 | assert.strictEqual(child.data[2].bucket[0].value, 'pos6+3') 463 | // @ts-ignore 464 | assert.strictEqual(child.data[3].bucket[0].value, 'pos6+4') 465 | // @ts-ignore 466 | assert.strictEqual(child.data[4].bucket[0].value, 'pos6+5') 467 | 468 | assert.strictEqual(await map.isInvariant(), true) 469 | assert.strictEqual(await map.size(), 8) 470 | }) 471 | 472 | it('test keys, values, entries', async () => { 473 | const store = memoryStore() 474 | // use the identity hash from the predictable fill test(s) to spread things out a bit 475 | let map = await iamap.create(store, { hashAlg: 0x00 /* 'identity' */, bitWidth: 4, bucketSize: 2 }) 476 | const k = (2 << 4) | 2 477 | const ids = [] 478 | map = await map.set(Uint8Array.from([k, k, k, 1 << 4]), 'pos2+1') 479 | ids.push(map.id) 480 | assert.strictEqual(await map.size(), 1) 481 | map = await map.set(Uint8Array.from([k, k, k, 2 << 4]), 'pos2+2') 482 | ids.push(map.id) 483 | assert.strictEqual(await map.size(), 2) 484 | map = await map.set(Uint8Array.from([k, k, k, 3 << 4]), 'pos2+3') 485 | ids.push(map.id) 486 | assert.strictEqual(await map.size(), 3) 487 | map = await map.set(Uint8Array.from([k, k, 0, 0]), 'pos2+0+0') 488 | ids.push(map.id) 489 | assert.strictEqual(await map.size(), 4) 490 | 491 | // you can't normally know the order but in this case it's predictable cause we control the hash 492 | const expectedKeys = ['22220000', '22222210', '22222220', '22222230'] 493 | const expectedValues = ['pos2+0+0', 'pos2+1', 'pos2+2', 'pos2+3'] 494 | const expectedEntries = expectedKeys.map((k, i) => { return { key: fromHex(k), value: expectedValues[i] } }) 495 | 496 | let actual = [] 497 | for await (const k of map.keys()) { 498 | actual.push(toHex(k)) 499 | } 500 | assert.deepEqual(actual, expectedKeys) 501 | 502 | actual = [] 503 | for await (const v of map.values()) { 504 | actual.push(v) 505 | } 506 | assert.deepEqual(actual, expectedValues) 507 | 508 | actual = [] 509 | for await (const e of map.entries()) { 510 | actual.push(e) 511 | } 512 | assert.deepEqual(actual, expectedEntries) 513 | 514 | let idCount = 0 515 | for await (const id of map.ids()) { 516 | // this is a bit lame but much easier than reverse engineering the hash of the stringified serialized form! 517 | assert.ok(store.map.has(id)) 518 | idCount++ 519 | } 520 | assert.strictEqual(idCount, 7) // 7 nodes deep 521 | }) 522 | }) 523 | -------------------------------------------------------------------------------- /test/bit-utils-test.js: -------------------------------------------------------------------------------- 1 | // Copyright Rod Vagg; Licensed under the Apache License, Version 2.0, see README.md for more information 2 | 3 | /* eslint-env mocha */ 4 | 5 | import { assert } from 'chai' 6 | import { toHex } from './common.js' 7 | import { mask, bitmapHas, index, setBit } from '../bit-utils.js' 8 | 9 | describe('Bit utils', () => { 10 | it('mask', () => { 11 | assert.strictEqual(mask(Uint8Array.from([0b11111111]), 0, 5), 0b11111) 12 | assert.strictEqual(mask(Uint8Array.from([0b10101010]), 0, 5), 0b10101) 13 | assert.strictEqual(mask(Uint8Array.from([0b10000000]), 0, 5), 0b10000) 14 | assert.strictEqual(mask(Uint8Array.from([0b00010000]), 0, 5), 0b00010) 15 | assert.strictEqual(mask(Uint8Array.from([0b10000100, 0b10010000]), 0, 9), 0b100001001) 16 | assert.strictEqual(mask(Uint8Array.from([0b10101010, 0b10101010]), 0, 9), 0b101010101) 17 | assert.strictEqual(mask(Uint8Array.from([0b10000100, 0b10010000]), 1, 5), 0b10010) 18 | assert.strictEqual(mask(Uint8Array.from([0b10101010, 0b10101010]), 1, 5), 0b01010) 19 | assert.strictEqual(mask(Uint8Array.from([0b10000100, 0b10010000]), 2, 5), 0b01000) 20 | assert.strictEqual(mask(Uint8Array.from([0b10101010, 0b10101010]), 2, 5), 0b10101) 21 | assert.strictEqual(mask(Uint8Array.from([0b10000100, 0b10010000, 0b10000100, 0b10000100]), 3, 5), 0b01000) 22 | assert.strictEqual(mask(Uint8Array.from([0b10101010, 0b10101010, 0b10101010, 0b10101010]), 3, 5), 0b01010) 23 | assert.strictEqual(mask(Uint8Array.from([0b10000100, 0b10010000, 0b10000100, 0b10000100]), 4, 5), 0b01001) 24 | assert.strictEqual(mask(Uint8Array.from([0b10101010, 0b10101010, 0b10101010, 0b10101010]), 4, 5), 0b10101) 25 | }) 26 | 27 | it('bitmapHas', () => { 28 | assert.ok(!bitmapHas(Uint8Array.from([0b0]), 0)) 29 | assert.ok(!bitmapHas(Uint8Array.from([0b0]), 1)) 30 | assert.ok(bitmapHas(Uint8Array.from([0b1]), 0)) 31 | assert.ok(!bitmapHas(Uint8Array.from([0b1]), 1)) 32 | assert.ok(!bitmapHas(Uint8Array.from([0b101010]), 2)) 33 | assert.ok(bitmapHas(Uint8Array.from([0b101010]), 3)) 34 | assert.ok(!bitmapHas(Uint8Array.from([0b101010]), 4)) 35 | assert.ok(bitmapHas(Uint8Array.from([0b101010]), 5)) 36 | assert.ok(bitmapHas(Uint8Array.from([0b100000]), 5)) 37 | assert.ok(bitmapHas(Uint8Array.from([0b0100000]), 5)) 38 | assert.ok(bitmapHas(Uint8Array.from([0b00100000]), 5)) 39 | }) 40 | 41 | it('index', () => { 42 | assert.strictEqual(index(Uint8Array.from([0b111111]), 0), 0) 43 | assert.strictEqual(index(Uint8Array.from([0b111111]), 1), 1) 44 | assert.strictEqual(index(Uint8Array.from([0b111111]), 2), 2) 45 | assert.strictEqual(index(Uint8Array.from([0b111111]), 4), 4) 46 | assert.strictEqual(index(Uint8Array.from([0b111100]), 2), 0) 47 | assert.strictEqual(index(Uint8Array.from([0b111101]), 4), 3) 48 | assert.strictEqual(index(Uint8Array.from([0b111001]), 4), 2) 49 | assert.strictEqual(index(Uint8Array.from([0b111000]), 4), 1) 50 | assert.strictEqual(index(Uint8Array.from([0b110000]), 4), 0) 51 | // new node, no bitmask, insertion at the start 52 | assert.strictEqual(index(Uint8Array.from([0b000000]), 0), 0) 53 | assert.strictEqual(index(Uint8Array.from([0b000000]), 1), 0) 54 | assert.strictEqual(index(Uint8Array.from([0b000000]), 2), 0) 55 | assert.strictEqual(index(Uint8Array.from([0b000000]), 3), 0) 56 | }) 57 | 58 | it('setBit', () => { 59 | assert.strictEqual(toHex(setBit(Uint8Array.from([0]), 0, 1)), toHex(Uint8Array.from([0b00000001]))) 60 | assert.strictEqual(toHex(setBit(Uint8Array.from([0]), 1, 1)), toHex(Uint8Array.from([0b00000010]))) 61 | assert.strictEqual(toHex(setBit(Uint8Array.from([0]), 7, 1)), toHex(Uint8Array.from([0b10000000]))) 62 | assert.strictEqual(toHex(setBit(Uint8Array.from([0b11111111]), 0, 1)), toHex(Uint8Array.from([0b11111111]))) 63 | assert.strictEqual(toHex(setBit(Uint8Array.from([0b11111111]), 7, 1)), toHex(Uint8Array.from([0b11111111]))) 64 | assert.strictEqual(toHex(setBit(Uint8Array.from([0b01010101]), 1, 1)), toHex(Uint8Array.from([0b01010111]))) 65 | assert.strictEqual(toHex(setBit(Uint8Array.from([0b01010101]), 7, 1)), toHex(Uint8Array.from([0b11010101]))) 66 | assert.strictEqual(toHex(setBit(Uint8Array.from([0b11111111]), 0, 0)), toHex(Uint8Array.from([0b11111110]))) 67 | assert.strictEqual(toHex(setBit(Uint8Array.from([0b11111111]), 1, 0)), toHex(Uint8Array.from([0b11111101]))) 68 | assert.strictEqual(toHex(setBit(Uint8Array.from([0b11111111]), 7, 0)), toHex(Uint8Array.from([0b01111111]))) 69 | assert.strictEqual(toHex(setBit(Uint8Array.from([0, 0b11111111]), 8 + 0, 1)), toHex(Uint8Array.from([0, 0b11111111]))) 70 | assert.strictEqual(toHex(setBit(Uint8Array.from([0, 0b11111111]), 8 + 7, 1)), toHex(Uint8Array.from([0, 0b11111111]))) 71 | assert.strictEqual(toHex(setBit(Uint8Array.from([0, 0b01010101]), 8 + 1, 1)), toHex(Uint8Array.from([0, 0b01010111]))) 72 | assert.strictEqual(toHex(setBit(Uint8Array.from([0, 0b01010101]), 8 + 7, 1)), toHex(Uint8Array.from([0, 0b11010101]))) 73 | assert.strictEqual(toHex(setBit(Uint8Array.from([0, 0b11111111]), 8 + 0, 0)), toHex(Uint8Array.from([0, 0b11111110]))) 74 | assert.strictEqual(toHex(setBit(Uint8Array.from([0, 0b11111111]), 8 + 1, 0)), toHex(Uint8Array.from([0, 0b11111101]))) 75 | assert.strictEqual(toHex(setBit(Uint8Array.from([0, 0b11111111]), 8 + 7, 0)), toHex(Uint8Array.from([0, 0b01111111]))) 76 | assert.strictEqual(toHex(setBit(Uint8Array.from([0]), 0, 0)), toHex(Uint8Array.from([0b00000000]))) 77 | assert.strictEqual(toHex(setBit(Uint8Array.from([0]), 7, 0)), toHex(Uint8Array.from([0b00000000]))) 78 | assert.strictEqual(toHex(setBit(Uint8Array.from([0b01010101]), 0, 0)), toHex(Uint8Array.from([0b01010100]))) 79 | assert.strictEqual(toHex(setBit(Uint8Array.from([0b01010101]), 6, 0)), toHex(Uint8Array.from([0b00010101]))) 80 | assert.strictEqual(toHex(setBit(Uint8Array.from([0b11000010, 0b11010010, 0b01001010, 0b0000001]), 0, 0)), toHex(Uint8Array.from([0b11000010, 0b11010010, 0b01001010, 0b0000001]))) 81 | assert.strictEqual(toHex(setBit(Uint8Array.from([0b11000010, 0b11010010, 0b01001010, 0b0000001]), 0, 1)), toHex(Uint8Array.from([0b11000011, 0b11010010, 0b01001010, 0b0000001]))) 82 | assert.strictEqual(toHex(setBit(Uint8Array.from([0b11000010, 0b11010010, 0b01001010, 0b0000001]), 12, 0)), toHex(Uint8Array.from([0b11000010, 0b11000010, 0b01001010, 0b0000001]))) 83 | assert.strictEqual(toHex(setBit(Uint8Array.from([0b11000010, 0b11010010, 0b01001010, 0b0000001]), 12, 1)), toHex(Uint8Array.from([0b11000010, 0b11010010, 0b01001010, 0b0000001]))) 84 | assert.strictEqual(toHex(setBit(Uint8Array.from([0b11000010, 0b11010010, 0b01001010, 0b0000001]), 24, 0)), toHex(Uint8Array.from([0b11000010, 0b11010010, 0b01001010, 0b0000000]))) 85 | assert.strictEqual(toHex(setBit(Uint8Array.from([0b11000010, 0b11010010, 0b01001010, 0b0000001]), 24, 1)), toHex(Uint8Array.from([0b11000010, 0b11010010, 0b01001010, 0b0000001]))) 86 | assert.strictEqual(toHex(setBit(Uint8Array.from([0, 0, 0, 0]), 31, 1)), toHex(Uint8Array.from([0, 0, 0, -0b10000000]))) 87 | }) 88 | }) 89 | -------------------------------------------------------------------------------- /test/common.js: -------------------------------------------------------------------------------- 1 | // Copyright Rod Vagg; Licensed under the Apache License, Version 2.0, see README.md for more information 2 | 3 | // @ts-ignore 4 | import murmurhash3 from 'murmurhash3js-revisited' 5 | import { assert } from 'chai' 6 | 7 | /** 8 | * @typedef {import('./interface').TestStore} TestStore 9 | */ 10 | 11 | /** 12 | * @param {Uint8Array} key 13 | * @returns {Uint8Array} 14 | */ 15 | export function murmurHasher (key) { 16 | assert(key instanceof Uint8Array) 17 | const b = new Uint8Array(4) 18 | const view = new DataView(b.buffer) 19 | view.setUint32(0, murmurhash3.x86.hash32(key), true) 20 | return b 21 | } 22 | 23 | // probably best not to use this for real applications, unless your keys have the qualities of hashes 24 | /** 25 | * @param {Uint8Array} key 26 | * @returns {Uint8Array} 27 | */ 28 | export function identityHasher (key) { 29 | assert(key instanceof Uint8Array) 30 | return key 31 | } 32 | 33 | /** 34 | * @param {any} obj 35 | * @returns {number} 36 | */ 37 | function hash (obj) { 38 | return murmurhash3.x86.hash32(new TextEncoder().encode(JSON.stringify(obj))) 39 | } 40 | 41 | // simple util to generate stable content IDs for objects, this is not necessarily how 42 | // you'd use IAMap, ideally your backing store would generate IDs for you, such as a 43 | // CID for IPLD. 44 | 45 | /** 46 | * @returns {TestStore} 47 | */ 48 | export function memoryStore () { 49 | return { 50 | map: new Map(), 51 | saves: 0, 52 | loads: 0, 53 | async save (obj) { 54 | const id = hash(obj) 55 | this.map.set(id, obj) 56 | this.saves++ 57 | return id 58 | }, 59 | load (id) { // this can be async 60 | this.loads++ 61 | return this.map.get(id) 62 | }, 63 | isEqual (id1, id2) { 64 | return id1 === id2 65 | }, 66 | isLink (obj) { 67 | return typeof obj === 'number' 68 | } 69 | } 70 | } 71 | 72 | /** 73 | * @param {any} obj 74 | * @returns {Uint8Array} 75 | */ 76 | function toBytes (obj) { 77 | if (obj instanceof Uint8Array && obj.constructor.name === 'Uint8Array') { 78 | return obj 79 | } 80 | if (obj instanceof ArrayBuffer) { 81 | return new Uint8Array(obj) 82 | } 83 | if (ArrayBuffer.isView(obj)) { 84 | return new Uint8Array(obj.buffer, obj.byteOffset, obj.byteLength) 85 | } 86 | /* c8 ignore next */ 87 | throw new Error('Unknown type, must be binary type') 88 | } 89 | 90 | /** 91 | * @param {Uint8Array} d 92 | * @returns {string} 93 | */ 94 | export function toHex (d) { 95 | if (typeof d === 'string') { 96 | return d 97 | } 98 | // @ts-ignore 99 | return Array.prototype.reduce.call(toBytes(d), (p, c) => `${p}${c.toString(16).padStart(2, '0')}`, '') 100 | } 101 | 102 | /** 103 | * @param {string|Uint8Array} hex 104 | * @returns {Uint8Array} 105 | */ 106 | export function fromHex (hex) { 107 | if (hex instanceof Uint8Array) { 108 | return hex 109 | } 110 | if (!hex.length) { 111 | return new Uint8Array(0) 112 | } 113 | return new Uint8Array(hex.split('') 114 | // @ts-ignore 115 | .map((c, i, d) => i % 2 === 0 ? `0x${c}${d[i + 1]}` : '') 116 | .filter(Boolean) 117 | // @ts-ignore 118 | .map((e) => parseInt(e, 16))) 119 | } 120 | -------------------------------------------------------------------------------- /test/errors-test.js: -------------------------------------------------------------------------------- 1 | // Copyright Rod Vagg; Licensed under the Apache License, Version 2.0, see README.md for more information 2 | 3 | /* eslint-env mocha */ 4 | 5 | import * as chai from 'chai' 6 | import chaiAsPromised from '@rvagg/chai-as-promised' 7 | import * as iamap from '../iamap.js' 8 | import { identityHasher } from './common.js' 9 | 10 | /** 11 | * @typedef {import('../interface').Store} Store 12 | */ 13 | 14 | chai.use(chaiAsPromised) 15 | const { assert } = chai 16 | 17 | iamap.registerHasher(0x00 /* 'identity' */, 32, identityHasher) // not recommended 18 | 19 | // absolutely not recommended 20 | /** @type {Store} */ 21 | const devnull = { 22 | async save (_) { 23 | return 0 24 | }, 25 | async load (_) { 26 | throw new Error('unimplemented') 27 | }, 28 | isEqual () { 29 | return true 30 | }, 31 | isLink (_) { 32 | return true 33 | } 34 | } 35 | 36 | describe('Errors', () => { 37 | it('registerHasher errors', () => { 38 | // @ts-ignore 39 | assert.throws(() => { iamap.registerHasher('herp', 32, () => {}) }) 40 | // @ts-ignore 41 | assert.throws(() => { iamap.registerHasher('herp', 'derp', 32, () => {}) }) 42 | // @ts-ignore 43 | assert.throws(() => { iamap.registerHasher(0x00, 'derp', () => {}) }) 44 | // @ts-ignore 45 | assert.throws(() => { iamap.registerHasher(0x00, 32) }) 46 | }) 47 | 48 | it('constructor errors', async () => { 49 | // @ts-ignore 50 | await assert.isRejected(iamap.create({ herp: 'derp' }, { hashAlg: 0x00 /* 'identity' */, bitWidth: 4, bucketSize: 3 })) 51 | // @ts-ignore 52 | await assert.isRejected(iamap.create(devnull)) 53 | await assert.isFulfilled(iamap.create(devnull, { hashAlg: 0x00 /* 'identity' */ })) 54 | // @ts-ignore 55 | await assert.isRejected(iamap.create(devnull, { hashAlg: 'herp' })) 56 | await assert.isRejected(iamap.create(devnull, { hashAlg: 0xff /* '??' */, bitWidth: 3 })) 57 | await assert.isRejected(iamap.create(devnull, { hashAlg: 0x00 /* 'identity' */, bitWidth: -1 })) 58 | await assert.isRejected(iamap.create(devnull, { hashAlg: 0x00 /* 'identity' */, bitWidth: 0 })) 59 | await assert.isRejected(iamap.create(devnull, { hashAlg: 0x00 /* 'identity' */, bitWidth: 1 })) 60 | await assert.isRejected(iamap.create(devnull, { hashAlg: 0x00 /* 'identity' */, bitWidth: 1 })) 61 | await assert.isRejected(iamap.create(devnull, { hashAlg: 0x00 /* 'identity' */, bitWidth: 2 })) 62 | await assert.isFulfilled(iamap.create(devnull, { hashAlg: 0x00 /* 'identity' */, bitWidth: 3 })) 63 | await assert.isFulfilled(iamap.create(devnull, { hashAlg: 0x00 /* 'identity' */, bitWidth: 8 })) 64 | await assert.isFulfilled(iamap.create(devnull, { hashAlg: 0x00 /* 'identity' */, bitWidth: 16 })) 65 | await assert.isRejected(iamap.create(devnull, { hashAlg: 0x00 /* 'identity' */, bitWidth: 17 })) 66 | await assert.isRejected(iamap.create(devnull, { hashAlg: 0x00 /* 'identity' */, bitWidth: 4, bucketSize: -1 })) 67 | await assert.isRejected(iamap.create(devnull, { hashAlg: 0x00 /* 'identity' */, bitWidth: 4, bucketSize: 0 })) 68 | await assert.isRejected(iamap.create(devnull, { hashAlg: 0x00 /* 'identity' */, bitWidth: 4, bucketSize: 1 })) 69 | await assert.isFulfilled(iamap.create(devnull, { hashAlg: 0x00 /* 'identity' */, bitWidth: 4, bucketSize: 2 })) 70 | await assert.isFulfilled(iamap.create(devnull, { hashAlg: 0x00 /* 'identity' */, bitWidth: 4, bucketSize: 16 })) 71 | // @ts-ignore 72 | await assert.isRejected(iamap.create(devnull, { hashAlg: 0x00 /* 'identity' */, bitWidth: 4, bucketSize: 16 }, 'blerk')) 73 | await assert.isRejected(iamap.create(devnull, { hashAlg: 0x00 /* 'identity' */, bitWidth: 10, bucketSize: 16 }, new Uint8Array(2))) 74 | await assert.isFulfilled(iamap.create(devnull, { hashAlg: 0x00 /* 'identity' */, bitWidth: 10, bucketSize: 16 }, new Uint8Array((2 ** 10) / 8))) 75 | }) 76 | }) 77 | -------------------------------------------------------------------------------- /test/interface.ts: -------------------------------------------------------------------------------- 1 | import { Store } from '../interface' 2 | 3 | export interface TestStore extends Store { 4 | map: Map, 5 | saves: number, 6 | loads: number 7 | } -------------------------------------------------------------------------------- /test/largeish-test.js: -------------------------------------------------------------------------------- 1 | // Copyright Rod Vagg; Licensed under the Apache License, Version 2.0, see README.md for more information 2 | 3 | /* eslint-env mocha */ 4 | 5 | import { assert } from 'chai' 6 | import { murmurHasher, memoryStore } from './common.js' 7 | import * as iamap from '../iamap.js' 8 | 9 | iamap.registerHasher(0x23 /* 'murmur3-32' */, 32, murmurHasher) 10 | 11 | const PEAK = 100 // not huge but delete is super expensive 12 | 13 | const textDecoder = new TextDecoder() 14 | const store = memoryStore() // same store across tests 15 | /** @type {number} */ 16 | let loadId 17 | /** @type {string[]} */ 18 | const keys = [] 19 | 20 | describe('Large(ish)', () => { 21 | it(`fill with ${PEAK}`, async () => { 22 | let map = await iamap.create(store, { hashAlg: 0x23 /* 'murmur3-32' */ }) 23 | const expectedValues = [] 24 | const expectedEntries = [] 25 | 26 | assert.deepEqual(map.toSerializable(), { 27 | hashAlg: 0x23 /* 'murmur3-32' */, 28 | bucketSize: 5, 29 | hamt: [new Uint8Array((2 ** 8) / 8), []] 30 | }) 31 | assert.strictEqual(store.map.size, 1) 32 | assert.strictEqual(store.saves, 1) 33 | assert.strictEqual(store.loads, 0) 34 | 35 | assert.strictEqual(await map.isInvariant(), true) 36 | map = await map.set('foo', 'bar') 37 | map = await map.set('bar', 'baz') 38 | assert.strictEqual(await map.isInvariant(), true) 39 | assert.strictEqual(await map.get('foo'), 'bar') 40 | assert.strictEqual(await map.get('bar'), 'baz') 41 | assert.strictEqual(await map.get('boom'), undefined) 42 | 43 | map = await map.set('bar', 'baz') // repeat 44 | assert.strictEqual(await map.get('bar'), 'baz') 45 | map = await map.set('bar', 'booz') // replace 46 | assert.strictEqual(await map.get('bar'), 'booz') 47 | assert.strictEqual(await map.isInvariant(), true) 48 | 49 | for (let i = 0; i < PEAK; i++) { 50 | const key = `k${i}` 51 | const value = `v${i}` 52 | map = await map.set(key, value) 53 | keys.push(key) 54 | expectedValues.push(value) 55 | expectedEntries.push(JSON.stringify({ key, value })) 56 | } 57 | for (let i = PEAK - 1; i >= 0; i--) { 58 | assert.strictEqual(await map.get(`k${i}`), `v${i}`) 59 | assert.strictEqual(await map.has(`k${i}`), true) 60 | } 61 | 62 | const actualKeys = [] 63 | for await (const k of map.keys()) { 64 | actualKeys.push(textDecoder.decode(k)) 65 | } 66 | const actualValues = [] 67 | for await (const v of map.values()) { 68 | actualValues.push(v.toString()) 69 | } 70 | const actualEntries = [] 71 | for await (const e of map.entries()) { 72 | actualEntries.push(JSON.stringify({ key: textDecoder.decode(e.key), value: e.value })) 73 | } 74 | 75 | keys.sort() 76 | expectedValues.sort() 77 | expectedEntries.sort() 78 | actualKeys.sort() 79 | actualValues.sort() 80 | actualEntries.sort() 81 | assert.deepEqual(actualKeys, ['bar', 'foo'].concat(keys)) 82 | assert.deepEqual(actualValues, ['bar', 'booz'].concat(expectedValues)) 83 | assert.deepEqual(actualEntries, ['{"key":"bar","value":"booz"}', '{"key":"foo","value":"bar"}'].concat(expectedEntries)) 84 | 85 | loadId = map.id 86 | }) 87 | 88 | it(`load ${PEAK} node map and empty it`, async () => { 89 | let map = await iamap.load(store, loadId) 90 | 91 | assert.strictEqual(await map.get('foo'), 'bar') 92 | assert.strictEqual(await map.get('bar'), 'booz') 93 | for (let i = 0; i < PEAK; i++) { 94 | assert.strictEqual(await map.get(`k${i}`), `v${i}`) 95 | } 96 | assert.strictEqual(await map.isInvariant(), true) 97 | 98 | assert.strictEqual(await map.delete('whoop'), map) // nothing to delete 99 | 100 | assert.strictEqual(await map.get('foo'), 'bar') 101 | assert.strictEqual(await map.get('bar'), 'booz') 102 | map = await map.delete('foo') 103 | assert.strictEqual(await map.get('foo'), undefined) 104 | assert.strictEqual(await map.get('bar'), 'booz') 105 | map = await map.delete('bar') 106 | assert.ok(!await map.get('bar')) 107 | 108 | const shuffledKeys = keys.slice().sort(() => 0.5 - Math.random()) // randomish 109 | for (let i = 0; i < shuffledKeys.length; i++) { 110 | const key = shuffledKeys[i] 111 | map = await map.delete(key) 112 | assert.strictEqual(await map.get(key), undefined) 113 | for (let j = i + 1; j < shuffledKeys.length; j++) { // make sure the rest are still there 114 | const key = shuffledKeys[j] 115 | const value = key.replace(/^k/, 'v') 116 | assert.strictEqual(await map.get(key), value) 117 | } 118 | } 119 | }) 120 | }) 121 | -------------------------------------------------------------------------------- /test/serialization-test.js: -------------------------------------------------------------------------------- 1 | // Copyright Rod Vagg; Licensed under the Apache License, Version 2.0, see README.md for more information 2 | 3 | /* eslint-env mocha */ 4 | 5 | import * as chai from 'chai' 6 | import chaiAsPromised from '@rvagg/chai-as-promised' 7 | import { murmurHasher, identityHasher, memoryStore, toHex } from './common.js' 8 | import * as iamap from '../iamap.js' 9 | 10 | chai.use(chaiAsPromised) 11 | const { assert } = chai 12 | 13 | /** 14 | * @typedef {import('../iamap.js').SerializedRoot} SerializedRoot 15 | * @typedef {import('../iamap.js').SerializedNode} SerializedNode 16 | */ 17 | 18 | iamap.registerHasher(0x23 /* 'murmur3-32' */, 32, murmurHasher) 19 | iamap.registerHasher(0x00 /* 'identity' */, 32, identityHasher) // not recommended 20 | 21 | /** @type {iamap.IAMap} */ 22 | let Constructor 23 | 24 | describe('Serialization', () => { 25 | it('empty object', async () => { 26 | const store = memoryStore() 27 | const map = await iamap.create(store, { hashAlg: 0x23 /* 'murmur3-32' */ }) 28 | /** @type {SerializedRoot} */ 29 | const emptySerialized = { 30 | hashAlg: 0x23 /* 'murmur3-32' */, 31 | bucketSize: 5, 32 | hamt: [new Uint8Array((2 ** 8) / 8), []] 33 | } 34 | 35 | assert.deepEqual(map.toSerializable(), emptySerialized) 36 | 37 | const loadedMap = await iamap.load(store, map.id) 38 | assert.deepEqual(loadedMap, map) 39 | 40 | // @ts-ignore 41 | Constructor = map.constructor 42 | }) 43 | 44 | it('empty custom', async () => { 45 | const store = memoryStore() 46 | /** @type {SerializedRoot} */ 47 | const emptySerialized = { 48 | hashAlg: 0x00 /* 'identity' */, // identity 49 | bucketSize: 3, 50 | hamt: [new Uint8Array(2 ** 8 / 8), []] 51 | } 52 | const id = await store.save(emptySerialized) 53 | 54 | const map = await iamap.load(store, id) 55 | assert.deepEqual(map.toSerializable(), emptySerialized) 56 | assert.strictEqual(map.config.hashAlg, 0x00 /* 'identity' */) 57 | assert.strictEqual(map.config.bitWidth, 8) 58 | assert.strictEqual(map.config.bucketSize, 3) 59 | assert.strictEqual(toHex(map.map), toHex(new Uint8Array(2 ** 8 / 8))) 60 | assert.ok(Array.isArray(map.data)) 61 | assert.strictEqual(map.data.length, 0) 62 | }) 63 | 64 | it('child custom', async () => { 65 | const store = memoryStore() 66 | const dmap = new Uint8Array(2 ** 7 / 8) 67 | dmap[5] = 0b110011 68 | /** @type {SerializedNode} */ 69 | const emptySerialized = [dmap, []] 70 | const id = await store.save(emptySerialized) 71 | 72 | const map = await iamap.load(store, id, 10, { 73 | hashAlg: 0x00 /* 'identity' */, 74 | bitWidth: 7, 75 | bucketSize: 30 76 | }) 77 | 78 | assert.deepEqual(map.toSerializable(), emptySerialized) 79 | assert.strictEqual(map.depth, 10) 80 | assert.strictEqual(map.config.hashAlg, 0x00 /* 'identity' */) 81 | assert.strictEqual(map.config.bitWidth, 7) 82 | assert.strictEqual(map.config.bucketSize, 30) 83 | assert.strictEqual(toHex(map.map), toHex(dmap)) 84 | assert.ok(Array.isArray(map.data)) 85 | assert.strictEqual(map.data.length, 0) 86 | }) 87 | 88 | it('malformed', async () => { 89 | const store = memoryStore() 90 | const emptyMap = new Uint8Array(2 ** 8 / 8) 91 | /** @type {SerializedRoot} */ 92 | let emptySerialized = { 93 | hashAlg: 0x12 /* 'sha2-256' */, // not registered 94 | bucketSize: 3, 95 | hamt: [emptyMap, []] 96 | } 97 | let id = await store.save(emptySerialized) 98 | await assert.isRejected(iamap.load(store, id)) 99 | 100 | emptySerialized = Object.assign({}, emptySerialized) // clone 101 | emptySerialized.hashAlg = 0x00 /* 'identity' */ 102 | // @ts-ignore 103 | emptySerialized.bucketSize = 'foo' 104 | // @ts-ignore 105 | id = await store.save(emptySerialized) 106 | await assert.isRejected(iamap.load(store, id)) 107 | 108 | emptySerialized = Object.assign({}, emptySerialized) // clone 109 | emptySerialized.bucketSize = -1 110 | // @ts-ignore 111 | id = await store.save(emptySerialized) 112 | await assert.isRejected(iamap.load(store, id)) 113 | 114 | // @ts-ignore 115 | emptySerialized = Object.assign({}, emptySerialized) // clone 116 | emptySerialized.bucketSize = 3 117 | // @ts-ignore 118 | emptySerialized.hamt[1] = { nope: 'nope' } 119 | // @ts-ignore 120 | id = await store.save(emptySerialized) 121 | await assert.isRejected(iamap.load(store, id)) 122 | 123 | emptySerialized = Object.assign({}, emptySerialized) // clone 124 | emptySerialized.hamt[1] = [] 125 | // @ts-ignore 126 | emptySerialized.hamt[0] = 'foo' 127 | // @ts-ignore 128 | id = await store.save(emptySerialized) 129 | await assert.isRejected(iamap.load(store, id)) 130 | 131 | emptySerialized = Object.assign({}, emptySerialized) // clone 132 | // @ts-ignore 133 | emptySerialized.hamt[0] = emptyMap 134 | // @ts-ignore 135 | id = await store.save(emptySerialized) 136 | // @ts-ignore 137 | await assert.isRejected(iamap.load(store, id, 'foo')) 138 | 139 | emptySerialized = Object.assign({}, emptySerialized) // clone 140 | // @ts-ignore 141 | emptySerialized.hamt[1] = [{ woot: 'nope' }] 142 | // @ts-ignore 143 | id = await store.save(emptySerialized) 144 | await assert.isRejected(iamap.load(store, id)) 145 | 146 | emptySerialized = Object.assign({}, emptySerialized) // clone 147 | // @ts-ignore 148 | emptySerialized.hamt[1] = [[{ nope: 'nope' }]] 149 | // @ts-ignore 150 | id = await store.save(emptySerialized) 151 | await assert.isRejected(iamap.load(store, id)) 152 | 153 | const mapCopy = Uint8Array.from(emptyMap) 154 | mapCopy[0] = 0b110011 155 | /** @type {SerializedNode} */ 156 | let emptyChildSerialized = [mapCopy, []] 157 | id = await store.save(emptyChildSerialized) 158 | assert.isFulfilled(iamap.load(store, id, 32, { 159 | hashAlg: 0x00 /* 'identity' */, 160 | bitWidth: 8, 161 | bucketSize: 30 162 | })) // this is OK for bitWidth of 8 and hash bytes of 32 163 | 164 | emptyChildSerialized = /** @type {SerializedNode} */ (emptyChildSerialized.slice()) // clone 165 | id = await store.save(emptyChildSerialized) 166 | await assert.isRejected(iamap.load(store, id, 33, { // this is not OK for a bitWidth of 8 and hash bytes of 32 167 | hashAlg: 0x00 /* 'identity' */, 168 | bitWidth: 8, 169 | bucketSize: 30 170 | })) 171 | 172 | assert.throws(() => { 173 | iamap.fromSerializable(store, undefined, ['nope'], { 174 | hashAlg: 0x00 /* 'identity' */, 175 | bitWidth: 5, 176 | bucketSize: 2 177 | // @ts-ignore 178 | }, 'foobar') 179 | }) 180 | 181 | assert.throws(() => { 182 | iamap.fromSerializable(store, undefined, emptyChildSerialized, { 183 | hashAlg: 0x00 /* 'identity' */, 184 | bitWidth: 5, 185 | bucketSize: 2 186 | // @ts-ignore 187 | }, 'foobar') 188 | }) 189 | 190 | // @ts-ignore 191 | assert.throws(() => new Constructor(store, { hashAlg: 0x00 /* 'identity' */, bitWidth: 8 }, new Uint8Array(2 ** 8 / 8), 0, [{ nope: 'nope' }])) 192 | }) 193 | 194 | it('fromChildSerializable', async () => { 195 | const store = memoryStore() 196 | 197 | /** @type {SerializedRoot} */ 198 | const emptySerializedRoot = { 199 | hashAlg: 0x00 /* 'identity' */, 200 | bucketSize: 3, 201 | hamt: [new Uint8Array(2 ** 8 / 8), []] 202 | } 203 | const childMap = new Uint8Array(2 ** 8 / 8) 204 | childMap[4] = 0b110011 205 | /** @type {SerializedNode} */ 206 | const emptySerializedChild = [childMap, []] 207 | 208 | assert.strictEqual(iamap.isRootSerializable(emptySerializedRoot), true) 209 | assert.strictEqual(iamap.isSerializable(emptySerializedRoot), true) 210 | assert.strictEqual(iamap.isSerializable(emptySerializedChild), true) 211 | 212 | const root = await iamap.fromSerializable(store, 'somerootid', emptySerializedRoot) 213 | 214 | assert.deepEqual(root.toSerializable(), emptySerializedRoot) 215 | assert.strictEqual(root.id, 'somerootid') 216 | 217 | let child = await root.fromChildSerializable('somechildid', emptySerializedChild, 10) 218 | 219 | assert.deepEqual(child.toSerializable(), emptySerializedChild) 220 | assert.strictEqual(child.id, 'somechildid') 221 | assert.strictEqual(child.config.hashAlg, 0x00 /* 'identity' */) 222 | assert.strictEqual(child.config.bitWidth, 8) 223 | assert.strictEqual(child.config.bucketSize, 3) 224 | assert.strictEqual(toHex(child.map), toHex(childMap)) 225 | assert.ok(Array.isArray(child.data)) 226 | assert.strictEqual(child.data.length, 0) 227 | 228 | child = await root.fromChildSerializable(undefined, emptySerializedChild, 10) 229 | 230 | assert.deepEqual(child.toSerializable(), emptySerializedChild) 231 | assert.strictEqual(child.id, null) 232 | }) 233 | 234 | it('bad loads', async () => { 235 | const store = memoryStore() 236 | const map = new Uint8Array(2 ** 8 / 8) 237 | map[4] = 0b110011 238 | 239 | const emptySerialized = [map, []] 240 | const id = await store.save(emptySerialized) 241 | 242 | // @ts-ignore 243 | await assert.isRejected(iamap.load(store, id, 32, { 244 | bitWidth: 8, 245 | bucketSize: 30 246 | })) // no hashAlg 247 | 248 | await assert.isRejected(iamap.load(store, id, 32, { 249 | // @ts-ignore 250 | hashAlg: { yoiks: true }, 251 | bitWidth: 8, 252 | bucketSize: 30 253 | })) // bad hashAlg 254 | 255 | await assert.isRejected(iamap.load(store, id, 32, { 256 | hashAlg: 0x00 /* 'identity' */, 257 | // @ts-ignore 258 | bitWidth: 'foo', 259 | bucketSize: 30 260 | })) // bad bitWidth 261 | 262 | await assert.isRejected(iamap.load(store, id, 32, { 263 | hashAlg: 0x00 /* 'identity' */, 264 | bitWidth: 8, 265 | // @ts-ignore 266 | bucketSize: true 267 | })) // bad bucketSize 268 | 269 | // @ts-ignore 270 | await assert.isRejected(iamap.load(store, id, 'foo', { 271 | hashAlg: 0x00 /* 'identity' */, 272 | bitWidth: 8, 273 | bucketSize: 8 274 | })) // bad bucketSize 275 | }) 276 | }) 277 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "checkJs": true, 5 | "forceConsistentCasingInFileNames": true, 6 | "noImplicitReturns": false, 7 | "noImplicitAny": true, 8 | "noImplicitThis": true, 9 | "noFallthroughCasesInSwitch": true, 10 | "noUnusedLocals": true, 11 | "noUnusedParameters": true, 12 | "strictFunctionTypes": false, 13 | "strictNullChecks": true, 14 | "strictPropertyInitialization": true, 15 | "strictBindCallApply": true, 16 | "strict": true, 17 | "alwaysStrict": true, 18 | "esModuleInterop": true, 19 | "target": "ES2018", 20 | "module": "ESNext", 21 | "moduleResolution": "node", 22 | "declaration": true, 23 | "declarationMap": true, 24 | "outDir": "types", 25 | "skipLibCheck": true, 26 | "stripInternal": true, 27 | "resolveJsonModule": true, 28 | "emitDeclarationOnly": true, 29 | "baseUrl": "." 30 | }, 31 | "exclude": [ 32 | "./node_modules", 33 | "./types/*", 34 | "./examples" 35 | ], 36 | "compileOnSave": false 37 | } 38 | -------------------------------------------------------------------------------- /types/bit-utils.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Uint8Array} hash 3 | * @param {number} depth 4 | * @param {number} nbits 5 | * @returns {number} 6 | */ 7 | export function mask(hash: Uint8Array, depth: number, nbits: number): number; 8 | /** 9 | * set the `position` bit in the given `bitmap` to be `set` (truthy=1, falsey=0) 10 | * @param {Uint8Array} bitmap 11 | * @param {number} position 12 | * @param {boolean|0|1} set 13 | * @returns {Uint8Array} 14 | */ 15 | export function setBit(bitmap: Uint8Array, position: number, set: boolean | 0 | 1): Uint8Array; 16 | /** 17 | * check whether `bitmap` has a `1` at the given `position` bit 18 | * @param {Uint8Array} bitmap 19 | * @param {number} [position] 20 | * @param {number} [byte] 21 | * @param {number} [offset] 22 | * @returns {boolean} 23 | */ 24 | export function bitmapHas(bitmap: Uint8Array, position?: number | undefined, byte?: number | undefined, offset?: number | undefined): boolean; 25 | /** 26 | * count how many `1` bits are in `bitmap up until `position` 27 | * tells us where in the compacted element array an element should live 28 | * TODO: optimize with a popcount on a `position` shifted bitmap? 29 | * assumes bitmapHas(bitmap, position) == true, hence the i} store - A backing store for this Map. The store should be able to save and load a serialised 14 | * form of a single node of a IAMap which is provided as a plain object representation. `store.save(node)` takes 15 | * a serialisable node and should return a content address / ID for the node. `store.load(id)` serves the inverse 16 | * purpose, taking a content address / ID as provided by a `save()` operation and returning the serialised form 17 | * of a node which can be instantiated by IAMap. In addition, two identifier handling methods are needed: 18 | * `store.isEqual(id1, id2)` is required to check the equality of the two content addresses / IDs 19 | * (which may be custom for that data type). `store.isLink(obj)` is used to determine if an object is a link type 20 | * that can be used for `load()` operations on the store. It is important that link types be different to standard 21 | * JavaScript arrays and don't share properties used by the serialized form of an IAMap (e.g. such that a 22 | * `typeof obj === 'object' && Array.isArray(obj.data)`) .This is because a node data element may either be a link to 23 | * a child node, or an inlined child node, so `isLink()` should be able to determine if an object is a link, and if not, 24 | * `Array.isArray(obj)` will determine if that data element is a bucket of elements, or the above object check be able 25 | * to determine that an inline child node exists at the data element. 26 | * The `store` object should take the following form: 27 | * `{ async save(node):id, async load(id):node, isEqual(id,id):boolean, isLink(obj):boolean }` 28 | * A `store` should throw an appropriately informative error when a node that is requested does not exist in the backing 29 | * store. 30 | * 31 | * Options: 32 | * - hashAlg (number) - A [multicodec](https://github.com/multiformats/multicodec/blob/master/table.csv) 33 | * hash function identifier, e.g. `0x23` for `murmur3-32`. Hash functions must be registered with {@link iamap.registerHasher}. 34 | * - bitWidth (number, default 8) - The number of bits to extract from the hash to form a data element index at 35 | * each level of the Map, e.g. a bitWidth of 5 will extract 5 bits to be used as the data element index, since 2^5=32, 36 | * each node will store up to 32 data elements (child nodes and/or entry buckets). The maximum depth of the Map is 37 | * determined by `floor((hashBytes * 8) / bitWidth)` where `hashBytes` is the number of bytes the hash function 38 | * produces, e.g. `hashBytes=32` and `bitWidth=5` yields a maximum depth of 51 nodes. The maximum `bitWidth` 39 | * currently allowed is `8` which will store 256 data elements in each node. 40 | * - bucketSize (number, default 5) - The maximum number of collisions acceptable at each level of the Map. A 41 | * collision in the `bitWidth` index at a given depth will result in entries stored in a bucket (array). Once the 42 | * bucket exceeds `bucketSize`, a new child node is created for that index and all entries in the bucket are 43 | * pushed 44 | * 45 | * @param {Options} options - Options for this IAMap 46 | * @param {Uint8Array} [map] - for internal use 47 | * @param {number} [depth] - for internal use 48 | * @param {Element[]} [data] - for internal use 49 | */ 50 | export function create(store: import("./interface").Store, options: Options, map?: Uint8Array | undefined, depth?: number | undefined, data?: Element[] | undefined): Promise>; 51 | /** 52 | * ```js 53 | * let map = await iamap.load(store, id) 54 | * ``` 55 | * 56 | * Create a IAMap instance loaded from a serialised form in a backing store. See {@link iamap.create}. 57 | * 58 | * @name iamap.load 59 | * @function 60 | * @async 61 | * @template T 62 | * @param {Store} store - A backing store for this Map. See {@link iamap.create}. 63 | * @param {any} id - An content address / ID understood by the backing `store`. 64 | * @param {number} [depth=0] 65 | * @param {Options} [options] 66 | */ 67 | export function load(store: import("./interface").Store, id: any, depth?: number | undefined, options?: import("./interface").Options | undefined): Promise>; 68 | /** 69 | * ```js 70 | * iamap.registerHasher(hashAlg, hashBytes, hasher) 71 | * ``` 72 | * 73 | * Register a new hash function. IAMap has no hash functions by default, at least one is required to create a new 74 | * IAMap. 75 | * 76 | * @name iamap.registerHasher 77 | * @function 78 | * @param {number} hashAlg - A [multicodec](https://github.com/multiformats/multicodec/blob/master/table.csv) hash 79 | * function identifier **number**, e.g. `0x23` for `murmur3-32`. 80 | * @param {number} hashBytes - The number of bytes to use from the result of the `hasher()` function (e.g. `32`) 81 | * @param {Hasher} hasher - A hash function that takes a `Uint8Array` derived from the `key` values used for this 82 | * Map and returns a `Uint8Array` (or a `Uint8Array`-like, such that each data element of the array contains a single byte value). The function 83 | * may or may not be asynchronous but will be called with an `await`. 84 | */ 85 | export function registerHasher(hashAlg: number, hashBytes: number, hasher: Hasher): void; 86 | /** 87 | * Determine if a serializable object is an IAMap root type, can be used to assert whether a data block is 88 | * an IAMap before trying to instantiate it. 89 | * 90 | * @name iamap.isRootSerializable 91 | * @function 92 | * @param {any} serializable An object that may be a serialisable form of an IAMap root node 93 | * @returns {boolean} An indication that the serialisable form is or is not an IAMap root node 94 | */ 95 | export function isRootSerializable(serializable: any): boolean; 96 | /** 97 | * Determine if a serializable object is an IAMap node type, can be used to assert whether a data block is 98 | * an IAMap node before trying to instantiate it. 99 | * This should pass for both root nodes as well as child nodes 100 | * 101 | * @name iamap.isSerializable 102 | * @function 103 | * @param {any} serializable An object that may be a serialisable form of an IAMap node 104 | * @returns {boolean} An indication that the serialisable form is or is not an IAMap node 105 | */ 106 | export function isSerializable(serializable: any): boolean; 107 | /** 108 | * Instantiate an IAMap from a valid serialisable form of an IAMap node. The serializable should be the same as 109 | * produced by {@link IAMap#toSerializable}. 110 | * Serialised forms of root nodes must satisfy both {@link iamap.isRootSerializable} and {@link iamap.isSerializable}. For 111 | * root nodes, the `options` parameter will be ignored and the `depth` parameter must be the default value of `0`. 112 | * Serialised forms of non-root nodes must satisfy {@link iamap.isSerializable} and have a valid `options` parameter and 113 | * a non-`0` `depth` parameter. 114 | * 115 | * @name iamap.fromSerializable 116 | * @function 117 | * @template T 118 | * @param {Store} store A backing store for this Map. See {@link iamap.create}. 119 | * @param {any} id An optional ID for the instantiated IAMap node. Unlike {@link iamap.create}, 120 | * `fromSerializable()` does not `save()` a newly created IAMap node so an ID is not generated for it. If one is 121 | * required for downstream purposes it should be provided, if the value is `null` or `undefined`, `node.id` will 122 | * be `null` but will remain writable. 123 | * @param {any} serializable The serializable form of an IAMap node to be instantiated 124 | * @param {Options} [options=null] An options object for IAMap child node instantiation. Will be ignored for root 125 | * node instantiation (where `depth` = `0`) See {@link iamap.create}. 126 | * @param {number} [depth=0] The depth of the IAMap node. Where `0` is the root node and any `>0` number is a child 127 | * node. 128 | * @returns {IAMap} 129 | */ 130 | export function fromSerializable(store: import("./interface").Store, id: any, serializable: any, options?: import("./interface").Options | undefined, depth?: number | undefined): IAMap; 131 | /** 132 | * Immutable Asynchronous Map 133 | * 134 | * The `IAMap` constructor should not be used directly. Use `iamap.create()` or `iamap.load()` to instantiate. 135 | * 136 | * @class 137 | * @template T 138 | * @property {any} id - A unique identifier for this `IAMap` instance. IDs are generated by the backing store and 139 | * are returned on `save()` operations. 140 | * @property {number} config.hashAlg - The hash function used by this `IAMap` instance. See {@link iamap.create} for more 141 | * details. 142 | * @property {number} config.bitWidth - The number of bits used at each level of this `IAMap`. See {@link iamap.create} 143 | * for more details. 144 | * @property {number} config.bucketSize - TThe maximum number of collisions acceptable at each level of the Map. 145 | * @property {Uint8Array} [map=Uint8Array] - Bitmap indicating which slots are occupied by data entries or child node links, 146 | * each data entry contains an bucket of entries. Must be the appropriate size for `config.bitWidth` 147 | * (`2 ** config.bitWith / 8` bytes). 148 | * @property {number} [depth=0] - Depth of the current node in the IAMap, `depth` is used to extract bits from the 149 | * key hashes to locate slots 150 | * @property {Array} [data=[]] - Array of data elements (an internal `Element` type), each of which contains a 151 | * bucket of entries or an ID of a child node 152 | * See {@link iamap.create} for more details. 153 | */ 154 | export class IAMap { 155 | /** 156 | * @ignore 157 | * @param {Store} store 158 | * @param {Options} [options] 159 | * @param {Uint8Array} [map] 160 | * @param {number} [depth] 161 | * @param {Element[]} [data] 162 | */ 163 | constructor(store: Store, options?: import("./interface").Options | undefined, map?: Uint8Array | undefined, depth?: number | undefined, data?: Element[] | undefined); 164 | store: import("./interface").Store; 165 | /** 166 | * @ignore 167 | * @type {any|null} 168 | */ 169 | id: any | null; 170 | config: import("./interface").Config; 171 | map: Uint8Array; 172 | depth: number; 173 | /** 174 | * @ignore 175 | * @type {ReadonlyElement} 176 | */ 177 | data: ReadonlyElement; 178 | /** 179 | * Asynchronously create a new `IAMap` instance identical to this one but with `key` set to `value`. 180 | * 181 | * @param {(string|Uint8Array)} key - A key for the `value` being set whereby that same `value` may 182 | * be retrieved with a `get()` operation with the same `key`. The type of the `key` object should either be a 183 | * `Uint8Array` or be convertable to a `Uint8Array` via `TextEncoder. 184 | * @param {any} value - Any value that can be stored in the backing store. A value could be a serialisable object 185 | * or an address or content address or other kind of link to the actual value. 186 | * @param {Uint8Array} [_cachedHash] - for internal use 187 | * @returns {Promise>} A `Promise` containing a new `IAMap` that contains the new key/value pair. 188 | * @async 189 | */ 190 | set(key: (string | Uint8Array), value: any, _cachedHash?: Uint8Array | undefined): Promise>; 191 | /** 192 | * Asynchronously find and return a value for the given `key` if it exists within this `IAMap`. 193 | * 194 | * @param {string|Uint8Array} key - A key for the value being sought. See {@link IAMap#set} for 195 | * details about acceptable `key` types. 196 | * @param {Uint8Array} [_cachedHash] - for internal use 197 | * @returns {Promise} A `Promise` that resolves to the value being sought if that value exists within this `IAMap`. If the 198 | * key is not found in this `IAMap`, the `Promise` will resolve to `undefined`. 199 | * @async 200 | */ 201 | get(key: string | Uint8Array, _cachedHash?: Uint8Array | undefined): Promise; 202 | /** 203 | * Asynchronously find and return a boolean indicating whether the given `key` exists within this `IAMap` 204 | * 205 | * @param {string|Uint8Array} key - A key to check for existence within this `IAMap`. See 206 | * {@link IAMap#set} for details about acceptable `key` types. 207 | * @returns {Promise} A `Promise` that resolves to either `true` or `false` depending on whether the `key` exists 208 | * within this `IAMap`. 209 | * @async 210 | */ 211 | has(key: string | Uint8Array): Promise; 212 | /** 213 | * Asynchronously create a new `IAMap` instance identical to this one but with `key` and its associated 214 | * value removed. If the `key` does not exist within this `IAMap`, this instance of `IAMap` is returned. 215 | * 216 | * @param {string|Uint8Array} key - A key to remove. See {@link IAMap#set} for details about 217 | * acceptable `key` types. 218 | * @param {Uint8Array} [_cachedHash] - for internal use 219 | * @returns {Promise>} A `Promise` that resolves to a new `IAMap` instance without the given `key` or the same `IAMap` 220 | * instance if `key` does not exist within it. 221 | * @async 222 | */ 223 | delete(key: string | Uint8Array, _cachedHash?: Uint8Array | undefined): Promise>; 224 | /** 225 | * Asynchronously count the number of key/value pairs contained within this `IAMap`, including its children. 226 | * 227 | * @returns {Promise} A `Promise` with a `number` indicating the number of key/value pairs within this `IAMap` instance. 228 | * @async 229 | */ 230 | size(): Promise; 231 | /** 232 | * Asynchronously emit all keys that exist within this `IAMap`, including its children. This will cause a full 233 | * traversal of all nodes. 234 | * 235 | * @returns {AsyncGenerator} An async iterator that yields keys. All keys will be in `Uint8Array` format regardless of which 236 | * format they were inserted via `set()`. 237 | * @async 238 | */ 239 | keys(): AsyncGenerator; 240 | /** 241 | * Asynchronously emit all values that exist within this `IAMap`, including its children. This will cause a full 242 | * traversal of all nodes. 243 | * 244 | * @returns {AsyncGenerator} An async iterator that yields values. 245 | * @async 246 | */ 247 | values(): AsyncGenerator; 248 | /** 249 | * Asynchronously emit all { key, value } pairs that exist within this `IAMap`, including its children. This will 250 | * cause a full traversal of all nodes. 251 | * 252 | * @returns {AsyncGenerator<{ key: Uint8Array, value: any}>} An async iterator that yields objects with the properties `key` and `value`. 253 | * @async 254 | */ 255 | entries(): AsyncGenerator<{ 256 | key: Uint8Array; 257 | value: any; 258 | }>; 259 | /** 260 | * Asynchronously emit the IDs of this `IAMap` and all of its children. 261 | * 262 | * @returns {AsyncGenerator} An async iterator that yields the ID of this `IAMap` and all of its children. The type of ID is 263 | * determined by the backing store which is responsible for generating IDs upon `save()` operations. 264 | */ 265 | ids(): AsyncGenerator; 266 | /** 267 | * Returns a serialisable form of this `IAMap` node. The internal representation of this local node is copied into a plain 268 | * JavaScript `Object` including a representation of its data array that the key/value pairs it contains as well as 269 | * the identifiers of child nodes. 270 | * Root nodes (depth==0) contain the full map configuration information, while intermediate and leaf nodes contain only 271 | * data that cannot be inferred by traversal from a root node that already has this data (hashAlg and bucketSize -- bitWidth 272 | * is inferred by the length of the `map` byte array). 273 | * The backing store can use this representation to create a suitable serialised form. When loading from the backing store, 274 | * `IAMap` expects to receive an object with the same layout from which it can instantiate a full `IAMap` object. Where 275 | * root nodes contain the full set of data and intermediate and leaf nodes contain just the required data. 276 | * For content addressable backing stores, it is expected that the same data in this serialisable form will always produce 277 | * the same identifier. 278 | * Note that the `map` property is a `Uint8Array` so will need special handling for some serialization forms (e.g. JSON). 279 | * 280 | * Root node form: 281 | * ``` 282 | * { 283 | * hashAlg: number 284 | * bucketSize: number 285 | * hamt: [Uint8Array, Array] 286 | * } 287 | * ``` 288 | * 289 | * Intermediate and leaf node form: 290 | * ``` 291 | * [Uint8Array, Array] 292 | * ``` 293 | * 294 | * The `Uint8Array` in both forms is the 'map' used to identify the presence of an element in this node. 295 | * 296 | * The second element in the tuple in both forms, `Array`, is an elements array a mix of either buckets or links: 297 | * 298 | * * A bucket is an array of two elements, the first being a `key` of type `Uint8Array` and the second a `value` 299 | * or whatever type has been provided in `set()` operations for this `IAMap`. 300 | * * A link is an object of the type that the backing store provides upon `save()` operations and can be identified 301 | * with `isLink()` calls. 302 | * 303 | * Buckets and links are differentiated by their "kind": a bucket is an array while a link is a "link" kind as dictated 304 | * by the backing store. We use `Array.isArray()` and `store.isLink()` to perform this differentiation. 305 | * 306 | * @returns {SerializedNode|SerializedRoot} An object representing the internal state of this local `IAMap` node, including its links to child nodes 307 | * if any. 308 | */ 309 | toSerializable(): import("./interface").SerializedNode | SerializedRoot; 310 | /** 311 | * Calculate the number of entries locally stored by this node. Performs a scan of local buckets and adds up 312 | * their size. 313 | * 314 | * @returns {number} A number representing the number of local entries. 315 | */ 316 | directEntryCount(): number; 317 | /** 318 | * Calculate the number of child nodes linked by this node. Performs a scan of the local entries and tallies up the 319 | * ones containing links to child nodes. 320 | * 321 | * @returns {number} A number representing the number of direct child nodes 322 | */ 323 | directNodeCount(): number; 324 | /** 325 | * Asynchronously perform a check on this node and its children that it is in canonical format for the current data. 326 | * As this uses `size()` to calculate the total number of entries in this node and its children, it performs a full 327 | * scan of nodes and therefore incurs a load and deserialisation cost for each child node. 328 | * A `false` result from this method suggests a flaw in the implemetation. 329 | * 330 | * @async 331 | * @returns {Promise} A Promise with a boolean value indicating whether this IAMap is correctly formatted. 332 | */ 333 | isInvariant(): Promise; 334 | /** 335 | * A convenience shortcut to {@link iamap.fromSerializable} that uses this IAMap node instance's backing `store` and 336 | * configuration `options`. Intended to be used to instantiate child IAMap nodes from a root IAMap node. 337 | * 338 | * @param {any} id An optional ID for the instantiated IAMap node. See {@link iamap.fromSerializable}. 339 | * @param {any} serializable The serializable form of an IAMap node to be instantiated. 340 | * @param {number} [depth=0] The depth of the IAMap node. See {@link iamap.fromSerializable}. 341 | */ 342 | fromChildSerializable(id: any, serializable: any, depth?: number | undefined): IAMap; 343 | } 344 | export namespace IAMap { 345 | /** 346 | * @template T 347 | * @param {IAMap | any} node 348 | * @returns {boolean} 349 | */ 350 | function isIAMap(node: any): boolean; 351 | } 352 | /** 353 | * 354 | */ 355 | export type Store = import('./interface').Store; 356 | export type Config = import('./interface').Config; 357 | export type Options = import('./interface').Options; 358 | export type SerializedKV = import('./interface').SerializedKV; 359 | export type SerializedElement = import('./interface').SerializedElement; 360 | export type SerializedNode = import('./interface').SerializedNode; 361 | export type SerializedRoot = import('./interface').SerializedRoot; 362 | export type Hasher = (inp: Uint8Array) => (Uint8Array | Promise); 363 | export type Registry = { 364 | hasher: Hasher; 365 | hashBytes: number; 366 | }[]; 367 | export type IsLink = (link: any) => boolean; 368 | export type ReadonlyElement = readonly Element[]; 369 | export type FoundElement = { 370 | data?: { 371 | found: boolean; 372 | elementAt: number; 373 | element: Element; 374 | bucketIndex?: number; 375 | bucketEntry?: KV; 376 | }; 377 | link?: { 378 | elementAt: number; 379 | element: Element; 380 | }; 381 | }; 382 | declare class Element { 383 | /** 384 | * @ignore 385 | * @param {KV[]} [bucket] 386 | * @param {any} [link] 387 | */ 388 | constructor(bucket?: KV[] | undefined, link?: any); 389 | bucket: KV[] | null; 390 | link: any; 391 | /** 392 | * @ignore 393 | * @returns {SerializedElement} 394 | */ 395 | toSerializable(): SerializedElement; 396 | } 397 | declare namespace Element { 398 | /** 399 | * @ignore 400 | * @param {IsLink} isLink 401 | * @param {any} obj 402 | * @returns {Element} 403 | */ 404 | function fromSerializable(isLink: IsLink, obj: any): Element; 405 | } 406 | /** 407 | * internal utility to fetch a map instance's hash function 408 | * 409 | * @ignore 410 | * @template T 411 | * @param {IAMap} map 412 | * @returns {Hasher} 413 | */ 414 | declare function hasher(map: IAMap): Hasher; 415 | /** 416 | * @ignore 417 | */ 418 | declare class KV { 419 | /** 420 | * @ignore 421 | * @param {Uint8Array} key 422 | * @param {any} value 423 | */ 424 | constructor(key: Uint8Array, value: any); 425 | key: Uint8Array; 426 | value: any; 427 | /** 428 | * @ignore 429 | * @returns {SerializedKV} 430 | */ 431 | toSerializable(): import("./interface").SerializedKV; 432 | } 433 | declare namespace KV { 434 | /** 435 | * @ignore 436 | * @param {SerializedKV} obj 437 | * @returns {KV} 438 | */ 439 | function fromSerializable(obj: import("./interface").SerializedKV): KV; 440 | } 441 | export {}; 442 | //# sourceMappingURL=iamap.d.ts.map -------------------------------------------------------------------------------- /types/iamap.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"iamap.d.ts","sourceRoot":"","sources":["../iamap.js"],"names":[],"mappings":"AA4CA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,0EALW,OAAO,6GASjB;AAED;;;;;;;;;;;;;;;GAeG;AACH,mEAJW,GAAG,sGAWb;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wCAPW,MAAM,aAEN,MAAM,UACN,MAAM,QAehB;AAo4BD;;;;;;;;GAQG;AACH,iDAHW,GAAG,GACD,OAAO,CAQnB;AAED;;;;;;;;;GASG;AACH,6CAHW,GAAG,GACD,OAAO,CAOnB;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,+EAXW,GAAG,gBAIH,GAAG,6FAgCb;AAp4BD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH;IACE;;;;;;;OAOG;IACH,mBANW,MAAM,CAAC,CAAC,+IAqDlB;IAxCC,sCAAkB;IAElB;;;OAGG;IACH,IAFU,GAAG,GAAC,IAAI,CAEJ;IACd,qCAAkC;IAWlC,gBAA2C;IAK3C,cAAuB;IAOvB;;;OAGG;IACH,sBAAqC;IAQvC;;;;;;;;;;;OAWG;IACH,SATW,CAAC,MAAM,GAAC,UAAU,CAAC,SAGnB,GAAG,yCAGD,QAAQ,MAAM,CAAC,CAAC,CAAC,CAiD7B;IAED;;;;;;;;;OASG;IACH,SAPW,MAAM,GAAC,UAAU,yCAGf,QAAQ,GAAG,CAAC,CA6CxB;IAED;;;;;;;;OAQG;IACH,SANW,MAAM,GAAC,UAAU,GAEf,QAAQ,OAAO,CAAC,CAM5B;IAED;;;;;;;;;;OAUG;IACH,YAPW,MAAM,GAAC,UAAU,yCAGf,QAAQ,MAAM,CAAC,CAAC,CAAC,CAyE7B;IAED;;;;;OAKG;IACH,QAHa,QAAQ,MAAM,CAAC,CAc3B;IAED;;;;;;;OAOG;IACH,QAJa,eAAe,UAAU,CAAC,CAiBtC;IAED;;;;;;OAMG;IACH,UAHa,eAAe,GAAG,CAAC,CAgB/B;IAED;;;;;;OAMG;IACH,WAHa,eAAe;QAAE,GAAG,EAAE,UAAU,CAAC;QAAC,KAAK,EAAE,GAAG,CAAA;KAAC,CAAC,CAgB1D;IAED;;;;;OAKG;IACH,OAHa,eAAe,GAAG,CAAC,CAW/B;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA0CG;IACH,kBAHa,uCAAe,cAAc,CAyBzC;IAED;;;;;OAKG;IACH,oBAFa,MAAM,CAMlB;IAED;;;;;OAKG;IACH,mBAFa,MAAM,CAMlB;IAED;;;;;;;;OAQG;IACH,eAFa,QAAQ,OAAO,CAAC,CAmB5B;IAED;;;;;;;MAOE;IACF,0BAJW,GAAG,gBACH,GAAG,wCAKb;CACF;;IA0YD;;;;OAIG;IACH,0CAEC;;;;;uBA3nCY,OAAO,aAAa,EAAE,KAAK,CAAC,CAAC,CAAC;qBAG9B,OAAO,aAAa,EAAE,MAAM;sBAC5B,OAAO,aAAa,EAAE,OAAO;2BAC7B,OAAO,aAAa,EAAE,YAAY;gCAClC,OAAO,aAAa,EAAE,iBAAiB;6BACvC,OAAO,aAAa,EAAE,cAAc;6BACpC,OAAO,aAAa,EAAE,cAAc;2BAC/B,UAAU,KAAG,CAAC,UAAU,GAAC,QAAQ,UAAU,CAAC,CAAC;uBAClD;IAAE,QAAQ,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,EAAE;4BACjC,GAAG,KAAG,OAAO;8BACnB,SAAS,OAAO,EAAE;2BAClB;IAAC,IAAI,CAAC,EAAE;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,EAAE,CAAA;KAAE,CAAC;IAAC,IAAI,CAAC,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAA;CAAC;AAyKrK;IACE;;;;OAIG;IACH,8CAFW,GAAG,EAMb;IAHC,oBAA4B;IAC5B,UAA4C;IAI9C;;;OAGG;IACH,kBAFa,iBAAiB,CAW7B;CACF;;IAED;;;;;OAKG;IACH,6DAOC;;AA85BD;;;;;;;GAOG;AACH,2CAFa,MAAM,CAIlB;AAr/BD;;GAEG;AACH;IACE;;;;OAIG;IACH,iBAHW,UAAU,SACV,GAAG,EAKb;IAFC,gBAAc;IACd,WAAkB;IAGpB;;;OAGG;IACH,qDAEC;CACF;;IAED;;;;OAIG;IACH,uEAIC"} -------------------------------------------------------------------------------- /types/interface.d.ts: -------------------------------------------------------------------------------- 1 | export interface Store { 2 | save(node: any): Promise; 3 | load(id: T): Promise; 4 | isLink(link: T): boolean; 5 | isEqual(link1: T, link2: T): boolean; 6 | } 7 | export interface Options { 8 | bitWidth?: number; 9 | bucketSize?: number; 10 | hashAlg: number; 11 | } 12 | export interface Config { 13 | bitWidth: number; 14 | bucketSize: number; 15 | hashAlg: number; 16 | } 17 | export type SerializedKV = [Uint8Array, any]; 18 | export type SerializedElement = SerializedKV | any; 19 | type NodeMap = Uint8Array; 20 | type NodeData = SerializedElement[]; 21 | export type SerializedNode = [NodeMap, NodeData]; 22 | export interface SerializedRoot { 23 | hashAlg: number; 24 | bucketSize: number; 25 | hamt: SerializedNode; 26 | } 27 | export {}; 28 | //# sourceMappingURL=interface.d.ts.map -------------------------------------------------------------------------------- /types/interface.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"interface.d.ts","sourceRoot":"","sources":["../interface.ts"],"names":[],"mappings":"AACA,MAAM,WAAW,KAAK,CAAC,CAAC;IACtB,IAAI,CAAC,IAAI,EAAE,GAAG,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAC5B,IAAI,CAAC,EAAE,EAAE,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;IAC1B,MAAM,CAAC,IAAI,EAAE,CAAC,GAAG,OAAO,CAAC;IACzB,OAAO,CAAC,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,OAAO,CAAC;CACtC;AAED,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,MAAM;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,MAAM,YAAY,GAAG,CAAC,UAAU,EAAE,GAAG,CAAC,CAAA;AAE5C,MAAM,MAAM,iBAAiB,GAAG,YAAY,GAAG,GAAG,CAAA;AAElD,KAAK,OAAO,GAAG,UAAU,CAAA;AACzB,KAAK,QAAQ,GAAG,iBAAiB,EAAE,CAAA;AAEnC,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAA;AAEhD,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,cAAc,CAAA;CACrB"} -------------------------------------------------------------------------------- /types/test/basic-test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | //# sourceMappingURL=basic-test.d.ts.map -------------------------------------------------------------------------------- /types/test/basic-test.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"basic-test.d.ts","sourceRoot":"","sources":["../../test/basic-test.js"],"names":[],"mappings":""} -------------------------------------------------------------------------------- /types/test/bit-utils-test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | //# sourceMappingURL=bit-utils-test.d.ts.map -------------------------------------------------------------------------------- /types/test/bit-utils-test.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"bit-utils-test.d.ts","sourceRoot":"","sources":["../../test/bit-utils-test.js"],"names":[],"mappings":""} -------------------------------------------------------------------------------- /types/test/common.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('./interface').TestStore} TestStore 3 | */ 4 | /** 5 | * @param {Uint8Array} key 6 | * @returns {Uint8Array} 7 | */ 8 | export function murmurHasher(key: Uint8Array): Uint8Array; 9 | /** 10 | * @param {Uint8Array} key 11 | * @returns {Uint8Array} 12 | */ 13 | export function identityHasher(key: Uint8Array): Uint8Array; 14 | /** 15 | * @returns {TestStore} 16 | */ 17 | export function memoryStore(): TestStore; 18 | /** 19 | * @param {Uint8Array} d 20 | * @returns {string} 21 | */ 22 | export function toHex(d: Uint8Array): string; 23 | /** 24 | * @param {string|Uint8Array} hex 25 | * @returns {Uint8Array} 26 | */ 27 | export function fromHex(hex: string | Uint8Array): Uint8Array; 28 | export type TestStore = import('./interface').TestStore; 29 | //# sourceMappingURL=common.d.ts.map -------------------------------------------------------------------------------- /types/test/common.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../test/common.js"],"names":[],"mappings":"AAMA;;GAEG;AAEH;;;GAGG;AACH,kCAHW,UAAU,GACR,UAAU,CAQtB;AAGD;;;GAGG;AACH,oCAHW,UAAU,GACR,UAAU,CAKtB;AAcD;;GAEG;AACH,+BAFa,SAAS,CAwBrB;AAoBD;;;GAGG;AACH,yBAHW,UAAU,GACR,MAAM,CAQlB;AAED;;;GAGG;AACH,6BAHW,MAAM,GAAC,UAAU,GACf,UAAU,CAetB;wBA/GY,OAAO,aAAa,EAAE,SAAS"} -------------------------------------------------------------------------------- /types/test/errors-test.d.ts: -------------------------------------------------------------------------------- 1 | export type Store = import('../interface').Store; 2 | //# sourceMappingURL=errors-test.d.ts.map -------------------------------------------------------------------------------- /types/test/errors-test.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"errors-test.d.ts","sourceRoot":"","sources":["../../test/errors-test.js"],"names":[],"mappings":"oBAUa,OAAO,cAAc,EAAE,KAAK,CAAC,MAAM,CAAC"} -------------------------------------------------------------------------------- /types/test/interface.d.ts: -------------------------------------------------------------------------------- 1 | import { Store } from '../interface'; 2 | export interface TestStore extends Store { 3 | map: Map; 4 | saves: number; 5 | loads: number; 6 | } 7 | //# sourceMappingURL=interface.d.ts.map -------------------------------------------------------------------------------- /types/test/interface.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"interface.d.ts","sourceRoot":"","sources":["../../test/interface.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,cAAc,CAAA;AAEpC,MAAM,WAAW,SAAU,SAAQ,KAAK,CAAC,MAAM,CAAC;IAC9C,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAA;CACd"} -------------------------------------------------------------------------------- /types/test/largeish-test.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | //# sourceMappingURL=largeish-test.d.ts.map -------------------------------------------------------------------------------- /types/test/largeish-test.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"largeish-test.d.ts","sourceRoot":"","sources":["../../test/largeish-test.js"],"names":[],"mappings":""} -------------------------------------------------------------------------------- /types/test/serialization-test.d.ts: -------------------------------------------------------------------------------- 1 | export type SerializedRoot = import('../iamap.js').SerializedRoot; 2 | export type SerializedNode = import('../iamap.js').SerializedNode; 3 | //# sourceMappingURL=serialization-test.d.ts.map -------------------------------------------------------------------------------- /types/test/serialization-test.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"serialization-test.d.ts","sourceRoot":"","sources":["../../test/serialization-test.js"],"names":[],"mappings":"6BAaa,OAAO,aAAa,EAAE,cAAc;6BACpC,OAAO,aAAa,EAAE,cAAc"} --------------------------------------------------------------------------------