├── .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"}
--------------------------------------------------------------------------------