@aztec/kv-store 0.86.0-nightly.20250501 → 0.86.0-nightly.20250502

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"multi_map.d.ts","sourceRoot":"","sources":["../../src/indexeddb/multi_map.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AACrE,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAE7C;;GAEG;AACH,qBAAa,sBAAsB,CAAC,CAAC,SAAS,GAAG,EAAE,CAAC,SAAS,KAAK,CAChE,SAAQ,iBAAiB,CAAC,CAAC,EAAE,CAAC,CAC9B,YAAW,kBAAkB,CAAC,CAAC,EAAE,CAAC,CAAC;IAEpB,GAAG,CAAC,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAyB1C,cAAc,CAAC,GAAG,EAAE,CAAC,GAAG,qBAAqB,CAAC,CAAC,CAAC;IAajD,WAAW,CAAC,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;CAajD"}
1
+ {"version":3,"file":"multi_map.d.ts","sourceRoot":"","sources":["../../src/indexeddb/multi_map.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AACrE,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAE7C;;GAEG;AACH,qBAAa,sBAAsB,CAAC,CAAC,SAAS,GAAG,EAAE,CAAC,SAAS,KAAK,CAChE,SAAQ,iBAAiB,CAAC,CAAC,EAAE,CAAC,CAC9B,YAAW,kBAAkB,CAAC,CAAC,EAAE,CAAC,CAAC;IAEpB,GAAG,CAAC,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAoC1C,cAAc,CAAC,GAAG,EAAE,CAAC,GAAG,qBAAqB,CAAC,CAAC,CAAC;IAcjD,WAAW,CAAC,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;CAejD"}
@@ -4,6 +4,7 @@ import { IndexedDBAztecMap } from './map.js';
4
4
  * A multi map backed by IndexedDB.
5
5
  */ export class IndexedDBAztecMultiMap extends IndexedDBAztecMap {
6
6
  async set(key, val) {
7
+ // Inserting repeated values is a no-op
7
8
  const exists = !!await this.db.index('hash').get(IDBKeyRange.bound([
8
9
  this.container,
9
10
  this.normalizeKey(key),
@@ -16,13 +17,23 @@ import { IndexedDBAztecMap } from './map.js';
16
17
  if (exists) {
17
18
  return;
18
19
  }
19
- const count = await this.db.index('key').count(IDBKeyRange.bound([
20
- this.container,
21
- this.normalizeKey(key)
22
- ], [
20
+ // Get the maximum keyCount for the given key
21
+ // In order to support sparse multimaps, we cannot rely
22
+ // on just counting the number of entries for the key, since we would repeat slots
23
+ // if we delete an entry
24
+ // set -> container:key:0 (keyCount = 1)
25
+ // set -> container:key:1 (keyCount = 2)
26
+ // delete -> container:key:0 (keyCount = 1)
27
+ // set -> container:key:1 <--- already exists!
28
+ // Instead, we iterate in reverse order to get the last inserted entry
29
+ const index = this.db.index('keyCount');
30
+ const rangeQuery = IDBKeyRange.upperBound([
23
31
  this.container,
24
- this.normalizeKey(key)
25
- ]));
32
+ this.normalizeKey(key),
33
+ Number.MAX_SAFE_INTEGER
34
+ ]);
35
+ const maxEntry = (await index.iterate(rangeQuery, 'prevunique').next()).value;
36
+ const count = maxEntry?.value?.keyCount ?? 0;
26
37
  await this.db.put({
27
38
  value: val,
28
39
  hash: hash(val),
@@ -33,6 +44,7 @@ import { IndexedDBAztecMap } from './map.js';
33
44
  });
34
45
  }
35
46
  async *getValuesAsync(key) {
47
+ // Iterate over the whole range of keyCount for the given key
36
48
  const index = this.db.index('keyCount');
37
49
  const rangeQuery = IDBKeyRange.bound([
38
50
  this.container,
@@ -48,6 +60,8 @@ import { IndexedDBAztecMap } from './map.js';
48
60
  }
49
61
  }
50
62
  async deleteValue(key, val) {
63
+ // Since we know the value, we can hash it and directly query the "hash" index
64
+ // to avoid having to iterate over all the values
51
65
  const fullKey = await this.db.index('hash').getKey(IDBKeyRange.bound([
52
66
  this.container,
53
67
  this.normalizeKey(key),
@@ -1 +1 @@
1
- {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/indexeddb/store.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/indexeddb/store.ts"],"names":[],"mappings":"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"}
@@ -42,13 +42,17 @@ import { IndexedDBAztecSingleton } from './singleton.js';
42
42
  ], {
43
43
  unique: false
44
44
  });
45
+ // Keep count of the maximum number of keys ever inserted in the container
46
+ // This allows unique slots for repeated keys, which is useful for multi-maps
45
47
  objectStore.createIndex('keyCount', [
46
48
  'container',
47
49
  'key',
48
50
  'keyCount'
49
51
  ], {
50
- unique: false
52
+ unique: true
51
53
  });
54
+ // Keep an index on the pair key-hash for a given container, allowing us to efficiently
55
+ // delete unique values from multi-maps
52
56
  objectStore.createIndex('hash', [
53
57
  'container',
54
58
  'key',
@@ -1 +1 @@
1
- {"version":3,"file":"multi_map_test_suite.d.ts","sourceRoot":"","sources":["../../src/interfaces/multi_map_test_suite.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAGlE,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,YAAY,GAAG,OAAO,CAAC,iBAAiB,CAAC,EACzD,UAAU,GAAE,OAAe,QAyI5B"}
1
+ {"version":3,"file":"multi_map_test_suite.d.ts","sourceRoot":"","sources":["../../src/interfaces/multi_map_test_suite.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAGlE,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,YAAY,GAAG,OAAO,CAAC,iBAAiB,CAAC,EACzD,UAAU,GAAE,OAAe,QAsM5B"}
@@ -95,11 +95,75 @@ export function describeAztecMultiMap(testName, getStore, forceAsync = false) {
95
95
  ]);
96
96
  });
97
97
  it('should be able to delete individual values for a single key', async ()=>{
98
+ await multiMap.set('foo', '1');
99
+ await multiMap.set('foo', '2');
100
+ await multiMap.set('foo', '3');
101
+ await multiMap.deleteValue('foo', '2');
102
+ expect(await getValues('foo')).to.deep.equal([
103
+ '1',
104
+ '3'
105
+ ]);
106
+ });
107
+ it('should be able to delete the last and first values for a key', async ()=>{
108
+ await multiMap.set('foo', '1');
109
+ await multiMap.set('foo', '2');
110
+ await multiMap.set('foo', '3');
111
+ await multiMap.deleteValue('foo', '1');
112
+ expect(await getValues('foo')).to.deep.equal([
113
+ '2',
114
+ '3'
115
+ ]);
116
+ await multiMap.deleteValue('foo', '3');
117
+ expect(await getValues('foo')).to.deep.equal([
118
+ '2'
119
+ ]);
120
+ });
121
+ it('should be able to fully clear a key', async ()=>{
122
+ await multiMap.set('foo', '1');
123
+ await multiMap.set('foo', '2');
124
+ await multiMap.set('foo', '3');
125
+ await multiMap.deleteValue('foo', '1');
126
+ await multiMap.deleteValue('foo', '3');
127
+ await multiMap.deleteValue('foo', '2');
128
+ expect(await getValues('foo')).to.deep.equal([]);
129
+ });
130
+ it('should be able to insert after deletion', async ()=>{
131
+ await multiMap.set('foo', '1');
132
+ await multiMap.set('foo', '2');
133
+ await multiMap.set('foo', '3');
134
+ await multiMap.deleteValue('foo', '2');
98
135
  await multiMap.set('foo', 'bar');
99
- await multiMap.set('foo', 'baz');
136
+ expect(await getValues('foo')).to.deep.equal([
137
+ '1',
138
+ '3',
139
+ 'bar'
140
+ ]);
141
+ // Delete the just-added entry
100
142
  await multiMap.deleteValue('foo', 'bar');
101
143
  expect(await getValues('foo')).to.deep.equal([
102
- 'baz'
144
+ '1',
145
+ '3'
146
+ ]);
147
+ // Reinsert the initially deleted key
148
+ await multiMap.set('foo', '2');
149
+ // LMDB and IndexedDB behave differently here, the former ordering by value and the latter by insertion. This is
150
+ // fine because there is no expectation for values in a multimap to be ordered.
151
+ const values = (await getValues('foo')).sort((a, b)=>a.localeCompare(b));
152
+ expect(values).to.deep.equal([
153
+ '1',
154
+ '2',
155
+ '3'
156
+ ]);
157
+ // Fully clear the key
158
+ await multiMap.deleteValue('foo', '1');
159
+ await multiMap.deleteValue('foo', '3');
160
+ await multiMap.deleteValue('foo', '2');
161
+ // Insert some more
162
+ await multiMap.set('foo', 'baz');
163
+ await multiMap.set('foo', 'qux');
164
+ expect(await getValues('foo')).to.deep.equal([
165
+ 'baz',
166
+ 'qux'
103
167
  ]);
104
168
  });
105
169
  it('supports range queries', async ()=>{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aztec/kv-store",
3
- "version": "0.86.0-nightly.20250501",
3
+ "version": "0.86.0-nightly.20250502",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./dest/interfaces/index.js",
@@ -23,10 +23,10 @@
23
23
  "./package.local.json"
24
24
  ],
25
25
  "dependencies": {
26
- "@aztec/ethereum": "0.86.0-nightly.20250501",
27
- "@aztec/foundation": "0.86.0-nightly.20250501",
28
- "@aztec/native": "0.86.0-nightly.20250501",
29
- "@aztec/stdlib": "0.86.0-nightly.20250501",
26
+ "@aztec/ethereum": "0.86.0-nightly.20250502",
27
+ "@aztec/foundation": "0.86.0-nightly.20250502",
28
+ "@aztec/native": "0.86.0-nightly.20250502",
29
+ "@aztec/stdlib": "0.86.0-nightly.20250502",
30
30
  "idb": "^8.0.0",
31
31
  "lmdb": "^3.2.0",
32
32
  "msgpackr": "^1.11.2",
@@ -12,6 +12,7 @@ export class IndexedDBAztecMultiMap<K extends Key, V extends Value>
12
12
  implements AztecAsyncMultiMap<K, V>
13
13
  {
14
14
  override async set(key: K, val: V): Promise<void> {
15
+ // Inserting repeated values is a no-op
15
16
  const exists = !!(await this.db
16
17
  .index('hash')
17
18
  .get(
@@ -23,9 +24,19 @@ export class IndexedDBAztecMultiMap<K extends Key, V extends Value>
23
24
  if (exists) {
24
25
  return;
25
26
  }
26
- const count = await this.db
27
- .index('key')
28
- .count(IDBKeyRange.bound([this.container, this.normalizeKey(key)], [this.container, this.normalizeKey(key)]));
27
+ // Get the maximum keyCount for the given key
28
+ // In order to support sparse multimaps, we cannot rely
29
+ // on just counting the number of entries for the key, since we would repeat slots
30
+ // if we delete an entry
31
+ // set -> container:key:0 (keyCount = 1)
32
+ // set -> container:key:1 (keyCount = 2)
33
+ // delete -> container:key:0 (keyCount = 1)
34
+ // set -> container:key:1 <--- already exists!
35
+ // Instead, we iterate in reverse order to get the last inserted entry
36
+ const index = this.db.index('keyCount');
37
+ const rangeQuery = IDBKeyRange.upperBound([this.container, this.normalizeKey(key), Number.MAX_SAFE_INTEGER]);
38
+ const maxEntry = (await index.iterate(rangeQuery, 'prevunique').next()).value;
39
+ const count = maxEntry?.value?.keyCount ?? 0;
29
40
  await this.db.put({
30
41
  value: val,
31
42
  hash: hash(val),
@@ -37,6 +48,7 @@ export class IndexedDBAztecMultiMap<K extends Key, V extends Value>
37
48
  }
38
49
 
39
50
  async *getValuesAsync(key: K): AsyncIterableIterator<V> {
51
+ // Iterate over the whole range of keyCount for the given key
40
52
  const index = this.db.index('keyCount');
41
53
  const rangeQuery = IDBKeyRange.bound(
42
54
  [this.container, this.normalizeKey(key), 0],
@@ -50,6 +62,8 @@ export class IndexedDBAztecMultiMap<K extends Key, V extends Value>
50
62
  }
51
63
 
52
64
  async deleteValue(key: K, val: V): Promise<void> {
65
+ // Since we know the value, we can hash it and directly query the "hash" index
66
+ // to avoid having to iterate over all the values
53
67
  const fullKey = await this.db
54
68
  .index('hash')
55
69
  .getKey(
@@ -73,7 +73,11 @@ export class AztecIndexedDBStore implements AztecAsyncKVStore {
73
73
  const objectStore = db.createObjectStore('data', { keyPath: 'slot' });
74
74
 
75
75
  objectStore.createIndex('key', ['container', 'key'], { unique: false });
76
- objectStore.createIndex('keyCount', ['container', 'key', 'keyCount'], { unique: false });
76
+ // Keep count of the maximum number of keys ever inserted in the container
77
+ // This allows unique slots for repeated keys, which is useful for multi-maps
78
+ objectStore.createIndex('keyCount', ['container', 'key', 'keyCount'], { unique: true });
79
+ // Keep an index on the pair key-hash for a given container, allowing us to efficiently
80
+ // delete unique values from multi-maps
77
81
  objectStore.createIndex('hash', ['container', 'key', 'hash'], { unique: true });
78
82
  },
79
83
  });
@@ -124,12 +124,73 @@ export function describeAztecMultiMap(
124
124
  });
125
125
 
126
126
  it('should be able to delete individual values for a single key', async () => {
127
+ await multiMap.set('foo', '1');
128
+ await multiMap.set('foo', '2');
129
+ await multiMap.set('foo', '3');
130
+
131
+ await multiMap.deleteValue('foo', '2');
132
+
133
+ expect(await getValues('foo')).to.deep.equal(['1', '3']);
134
+ });
135
+
136
+ it('should be able to delete the last and first values for a key', async () => {
137
+ await multiMap.set('foo', '1');
138
+ await multiMap.set('foo', '2');
139
+ await multiMap.set('foo', '3');
140
+
141
+ await multiMap.deleteValue('foo', '1');
142
+
143
+ expect(await getValues('foo')).to.deep.equal(['2', '3']);
144
+
145
+ await multiMap.deleteValue('foo', '3');
146
+
147
+ expect(await getValues('foo')).to.deep.equal(['2']);
148
+ });
149
+
150
+ it('should be able to fully clear a key', async () => {
151
+ await multiMap.set('foo', '1');
152
+ await multiMap.set('foo', '2');
153
+ await multiMap.set('foo', '3');
154
+
155
+ await multiMap.deleteValue('foo', '1');
156
+ await multiMap.deleteValue('foo', '3');
157
+ await multiMap.deleteValue('foo', '2');
158
+
159
+ expect(await getValues('foo')).to.deep.equal([]);
160
+ });
161
+
162
+ it('should be able to insert after deletion', async () => {
163
+ await multiMap.set('foo', '1');
164
+ await multiMap.set('foo', '2');
165
+ await multiMap.set('foo', '3');
166
+
167
+ await multiMap.deleteValue('foo', '2');
127
168
  await multiMap.set('foo', 'bar');
128
- await multiMap.set('foo', 'baz');
129
169
 
170
+ expect(await getValues('foo')).to.deep.equal(['1', '3', 'bar']);
171
+
172
+ // Delete the just-added entry
130
173
  await multiMap.deleteValue('foo', 'bar');
131
174
 
132
- expect(await getValues('foo')).to.deep.equal(['baz']);
175
+ expect(await getValues('foo')).to.deep.equal(['1', '3']);
176
+
177
+ // Reinsert the initially deleted key
178
+ await multiMap.set('foo', '2');
179
+
180
+ // LMDB and IndexedDB behave differently here, the former ordering by value and the latter by insertion. This is
181
+ // fine because there is no expectation for values in a multimap to be ordered.
182
+ const values = (await getValues('foo')).sort((a, b) => a.localeCompare(b));
183
+ expect(values).to.deep.equal(['1', '2', '3']);
184
+
185
+ // Fully clear the key
186
+ await multiMap.deleteValue('foo', '1');
187
+ await multiMap.deleteValue('foo', '3');
188
+ await multiMap.deleteValue('foo', '2');
189
+
190
+ // Insert some more
191
+ await multiMap.set('foo', 'baz');
192
+ await multiMap.set('foo', 'qux');
193
+ expect(await getValues('foo')).to.deep.equal(['baz', 'qux']);
133
194
  });
134
195
 
135
196
  it('supports range queries', async () => {