@aztec/kv-store 0.67.1 → 0.68.0

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.
Files changed (35) hide show
  1. package/dest/indexeddb/store.d.ts.map +1 -1
  2. package/dest/indexeddb/store.js +4 -3
  3. package/dest/interfaces/array_test_suite.d.ts.map +1 -1
  4. package/dest/interfaces/array_test_suite.js +4 -1
  5. package/dest/interfaces/map.d.ts +18 -0
  6. package/dest/interfaces/map.d.ts.map +1 -1
  7. package/dest/interfaces/map_test_suite.d.ts.map +1 -1
  8. package/dest/interfaces/map_test_suite.js +4 -1
  9. package/dest/interfaces/set_test_suite.d.ts.map +1 -1
  10. package/dest/interfaces/set_test_suite.js +4 -1
  11. package/dest/interfaces/singleton_test_suite.d.ts.map +1 -1
  12. package/dest/interfaces/singleton_test_suite.js +4 -1
  13. package/dest/interfaces/store.d.ts +13 -1
  14. package/dest/interfaces/store.d.ts.map +1 -1
  15. package/dest/interfaces/store_test_suite.d.ts.map +1 -1
  16. package/dest/interfaces/store_test_suite.js +5 -1
  17. package/dest/lmdb/map.d.ts +18 -2
  18. package/dest/lmdb/map.d.ts.map +1 -1
  19. package/dest/lmdb/map.js +86 -26
  20. package/dest/lmdb/store.d.ts +15 -3
  21. package/dest/lmdb/store.d.ts.map +1 -1
  22. package/dest/lmdb/store.js +26 -10
  23. package/dest/stores/l2_tips_store.js +2 -2
  24. package/package.json +6 -6
  25. package/src/indexeddb/store.ts +3 -2
  26. package/src/interfaces/array_test_suite.ts +4 -0
  27. package/src/interfaces/map.ts +21 -0
  28. package/src/interfaces/map_test_suite.ts +4 -0
  29. package/src/interfaces/set_test_suite.ts +4 -0
  30. package/src/interfaces/singleton_test_suite.ts +4 -0
  31. package/src/interfaces/store.ts +22 -1
  32. package/src/interfaces/store_test_suite.ts +4 -0
  33. package/src/lmdb/map.ts +97 -22
  34. package/src/lmdb/store.ts +35 -12
  35. package/src/stores/l2_tips_store.ts +1 -1
package/src/lmdb/map.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { type Database, type RangeOptions } from 'lmdb';
2
2
 
3
3
  import { type Key, type Range } from '../interfaces/common.js';
4
- import { type AztecAsyncMultiMap, type AztecMultiMap } from '../interfaces/map.js';
4
+ import { type AztecAsyncMultiMap, type AztecMapWithSize, type AztecMultiMap } from '../interfaces/map.js';
5
5
 
6
6
  /** The slot where a key-value entry would be stored */
7
7
  type MapValueSlot<K extends Key | Buffer> = ['map', string, 'slot', K];
@@ -13,8 +13,8 @@ export class LmdbAztecMap<K extends Key, V> implements AztecMultiMap<K, V>, Azte
13
13
  protected db: Database<[K, V], MapValueSlot<K>>;
14
14
  protected name: string;
15
15
 
16
- #startSentinel: MapValueSlot<Buffer>;
17
- #endSentinel: MapValueSlot<Buffer>;
16
+ protected startSentinel: MapValueSlot<Buffer>;
17
+ protected endSentinel: MapValueSlot<Buffer>;
18
18
 
19
19
  constructor(rootDb: Database, mapName: string) {
20
20
  this.name = mapName;
@@ -23,8 +23,8 @@ export class LmdbAztecMap<K extends Key, V> implements AztecMultiMap<K, V>, Azte
23
23
  // sentinels are used to define the start and end of the map
24
24
  // with LMDB's key encoding, no _primitive value_ can be "less than" an empty buffer or greater than Byte 255
25
25
  // these will be used later to answer range queries
26
- this.#startSentinel = ['map', this.name, 'slot', Buffer.from([])];
27
- this.#endSentinel = ['map', this.name, 'slot', Buffer.from([255])];
26
+ this.startSentinel = ['map', this.name, 'slot', Buffer.from([])];
27
+ this.endSentinel = ['map', this.name, 'slot', Buffer.from([255])];
28
28
  }
29
29
 
30
30
  close(): Promise<void> {
@@ -32,7 +32,7 @@ export class LmdbAztecMap<K extends Key, V> implements AztecMultiMap<K, V>, Azte
32
32
  }
33
33
 
34
34
  get(key: K): V | undefined {
35
- return this.db.get(this.#slot(key))?.[1];
35
+ return this.db.get(this.slot(key))?.[1];
36
36
  }
37
37
 
38
38
  getAsync(key: K): Promise<V | undefined> {
@@ -40,7 +40,7 @@ export class LmdbAztecMap<K extends Key, V> implements AztecMultiMap<K, V>, Azte
40
40
  }
41
41
 
42
42
  *getValues(key: K): IterableIterator<V> {
43
- const values = this.db.getValues(this.#slot(key));
43
+ const values = this.db.getValues(this.slot(key));
44
44
  for (const value of values) {
45
45
  yield value?.[1];
46
46
  }
@@ -53,7 +53,7 @@ export class LmdbAztecMap<K extends Key, V> implements AztecMultiMap<K, V>, Azte
53
53
  }
54
54
 
55
55
  has(key: K): boolean {
56
- return this.db.doesExist(this.#slot(key));
56
+ return this.db.doesExist(this.slot(key));
57
57
  }
58
58
 
59
59
  hasAsync(key: K): Promise<boolean> {
@@ -61,30 +61,30 @@ export class LmdbAztecMap<K extends Key, V> implements AztecMultiMap<K, V>, Azte
61
61
  }
62
62
 
63
63
  async set(key: K, val: V): Promise<void> {
64
- await this.db.put(this.#slot(key), [key, val]);
64
+ await this.db.put(this.slot(key), [key, val]);
65
65
  }
66
66
 
67
67
  swap(key: K, fn: (val: V | undefined) => V): Promise<void> {
68
68
  return this.db.childTransaction(() => {
69
- const slot = this.#slot(key);
69
+ const slot = this.slot(key);
70
70
  const entry = this.db.get(slot);
71
71
  void this.db.put(slot, [key, fn(entry?.[1])]);
72
72
  });
73
73
  }
74
74
 
75
75
  setIfNotExists(key: K, val: V): Promise<boolean> {
76
- const slot = this.#slot(key);
76
+ const slot = this.slot(key);
77
77
  return this.db.ifNoExists(slot, () => {
78
78
  void this.db.put(slot, [key, val]);
79
79
  });
80
80
  }
81
81
 
82
82
  async delete(key: K): Promise<void> {
83
- await this.db.remove(this.#slot(key));
83
+ await this.db.remove(this.slot(key));
84
84
  }
85
85
 
86
86
  async deleteValue(key: K, val: V): Promise<void> {
87
- await this.db.remove(this.#slot(key), [key, val]);
87
+ await this.db.remove(this.slot(key), [key, val]);
88
88
  }
89
89
 
90
90
  *entries(range: Range<K> = {}): IterableIterator<[K, V]> {
@@ -93,19 +93,19 @@ export class LmdbAztecMap<K extends Key, V> implements AztecMultiMap<K, V>, Azte
93
93
  // in that case, we need to swap the start and end sentinels
94
94
  const start = reverse
95
95
  ? range.end
96
- ? this.#slot(range.end)
97
- : this.#endSentinel
96
+ ? this.slot(range.end)
97
+ : this.endSentinel
98
98
  : range.start
99
- ? this.#slot(range.start)
100
- : this.#startSentinel;
99
+ ? this.slot(range.start)
100
+ : this.startSentinel;
101
101
 
102
102
  const end = reverse
103
103
  ? range.start
104
- ? this.#slot(range.start)
105
- : this.#startSentinel
104
+ ? this.slot(range.start)
105
+ : this.startSentinel
106
106
  : range.end
107
- ? this.#slot(range.end)
108
- : this.#endSentinel;
107
+ ? this.slot(range.end)
108
+ : this.endSentinel;
109
109
 
110
110
  const lmdbRange: RangeOptions = {
111
111
  start,
@@ -153,7 +153,82 @@ export class LmdbAztecMap<K extends Key, V> implements AztecMultiMap<K, V>, Azte
153
153
  }
154
154
  }
155
155
 
156
- #slot(key: K): MapValueSlot<K> {
156
+ protected slot(key: K): MapValueSlot<K> {
157
157
  return ['map', this.name, 'slot', key];
158
158
  }
159
+
160
+ async clear(): Promise<void> {
161
+ const lmdbRange: RangeOptions = {
162
+ start: this.startSentinel,
163
+ end: this.endSentinel,
164
+ };
165
+
166
+ const iterator = this.db.getRange(lmdbRange);
167
+
168
+ for (const { key } of iterator) {
169
+ await this.db.remove(key);
170
+ }
171
+ }
172
+ }
173
+
174
+ export class LmdbAztecMapWithSize<K extends Key, V>
175
+ extends LmdbAztecMap<K, V>
176
+ implements AztecMapWithSize<K, V>, AztecAsyncMultiMap<K, V>
177
+ {
178
+ #sizeCache?: number;
179
+
180
+ constructor(rootDb: Database, mapName: string) {
181
+ super(rootDb, mapName);
182
+ }
183
+
184
+ override async set(key: K, val: V): Promise<void> {
185
+ await this.db.childTransaction(() => {
186
+ const exists = this.db.doesExist(this.slot(key));
187
+ this.db.putSync(this.slot(key), [key, val], {
188
+ appendDup: true,
189
+ });
190
+ if (!exists) {
191
+ this.#sizeCache = undefined; // Invalidate cache
192
+ }
193
+ });
194
+ }
195
+
196
+ override async delete(key: K): Promise<void> {
197
+ await this.db.childTransaction(async () => {
198
+ const exists = this.db.doesExist(this.slot(key));
199
+ if (exists) {
200
+ await this.db.remove(this.slot(key));
201
+ this.#sizeCache = undefined; // Invalidate cache
202
+ }
203
+ });
204
+ }
205
+
206
+ override async deleteValue(key: K, val: V): Promise<void> {
207
+ await this.db.childTransaction(async () => {
208
+ const exists = this.db.doesExist(this.slot(key));
209
+ if (exists) {
210
+ await this.db.remove(this.slot(key), [key, val]);
211
+ this.#sizeCache = undefined; // Invalidate cache
212
+ }
213
+ });
214
+ }
215
+
216
+ /**
217
+ * Gets the size of the map by counting entries.
218
+ * @returns The number of entries in the map
219
+ */
220
+ size(): number {
221
+ if (this.#sizeCache === undefined) {
222
+ this.#sizeCache = this.db.getCount({
223
+ start: this.startSentinel,
224
+ end: this.endSentinel,
225
+ });
226
+ }
227
+ return this.#sizeCache;
228
+ }
229
+
230
+ // Reset cache on clear/drop operations
231
+ clearCache() {
232
+ this.#sizeCache = undefined;
233
+ }
159
234
  }
package/src/lmdb/store.ts CHANGED
@@ -1,20 +1,28 @@
1
+ import { randomBytes } from '@aztec/foundation/crypto';
1
2
  import { createLogger } from '@aztec/foundation/log';
2
3
 
3
4
  import { promises as fs, mkdirSync } from 'fs';
4
5
  import { type Database, type RootDatabase, open } from 'lmdb';
5
6
  import { tmpdir } from 'os';
6
- import { dirname, join } from 'path';
7
+ import { join } from 'path';
7
8
 
8
9
  import { type AztecArray, type AztecAsyncArray } from '../interfaces/array.js';
9
10
  import { type Key } from '../interfaces/common.js';
10
11
  import { type AztecAsyncCounter, type AztecCounter } from '../interfaces/counter.js';
11
- import { type AztecAsyncMap, type AztecAsyncMultiMap, type AztecMap, type AztecMultiMap } from '../interfaces/map.js';
12
+ import {
13
+ type AztecAsyncMap,
14
+ type AztecAsyncMultiMap,
15
+ type AztecMap,
16
+ type AztecMapWithSize,
17
+ type AztecMultiMap,
18
+ type AztecMultiMapWithSize,
19
+ } from '../interfaces/map.js';
12
20
  import { type AztecAsyncSet, type AztecSet } from '../interfaces/set.js';
13
21
  import { type AztecAsyncSingleton, type AztecSingleton } from '../interfaces/singleton.js';
14
22
  import { type AztecAsyncKVStore, type AztecKVStore } from '../interfaces/store.js';
15
23
  import { LmdbAztecArray } from './array.js';
16
24
  import { LmdbAztecCounter } from './counter.js';
17
- import { LmdbAztecMap } from './map.js';
25
+ import { LmdbAztecMap, LmdbAztecMapWithSize } from './map.js';
18
26
  import { LmdbAztecSet } from './set.js';
19
27
  import { LmdbAztecSingleton } from './singleton.js';
20
28
 
@@ -29,7 +37,7 @@ export class AztecLmdbStore implements AztecKVStore, AztecAsyncKVStore {
29
37
  #multiMapData: Database<unknown, Key>;
30
38
  #log = createLogger('kv-store:lmdb');
31
39
 
32
- constructor(rootDb: RootDatabase, public readonly isEphemeral: boolean, private path?: string) {
40
+ constructor(rootDb: RootDatabase, public readonly isEphemeral: boolean, private path: string) {
33
41
  this.#rootDb = rootDb;
34
42
 
35
43
  // big bucket to store all the data
@@ -64,13 +72,12 @@ export class AztecLmdbStore implements AztecKVStore, AztecAsyncKVStore {
64
72
  ephemeral: boolean = false,
65
73
  log = createLogger('kv-store:lmdb'),
66
74
  ): AztecLmdbStore {
67
- if (path) {
68
- mkdirSync(path, { recursive: true });
69
- }
75
+ const dbPath = path ?? join(tmpdir(), randomBytes(8).toString('hex'));
76
+ mkdirSync(dbPath, { recursive: true });
70
77
  const mapSize = 1024 * mapSizeKb;
71
78
  log.debug(`Opening LMDB database at ${path || 'temporary location'} with map size ${mapSize}`);
72
- const rootDb = open({ path, noSync: ephemeral, mapSize });
73
- return new AztecLmdbStore(rootDb, ephemeral, path);
79
+ const rootDb = open({ path: dbPath, noSync: ephemeral, mapSize });
80
+ return new AztecLmdbStore(rootDb, ephemeral, dbPath);
74
81
  }
75
82
 
76
83
  /**
@@ -78,10 +85,9 @@ export class AztecLmdbStore implements AztecKVStore, AztecAsyncKVStore {
78
85
  * @returns A new AztecLmdbStore.
79
86
  */
80
87
  async fork() {
81
- const baseDir = this.path ? dirname(this.path) : tmpdir();
88
+ const baseDir = this.path;
82
89
  this.#log.debug(`Forking store with basedir ${baseDir}`);
83
- const forkPath =
84
- (await fs.mkdtemp(join(baseDir, 'aztec-store-fork-'))) + (this.isEphemeral || !this.path ? '/data.mdb' : '');
90
+ const forkPath = await fs.mkdtemp(join(baseDir, 'aztec-store-fork-'));
85
91
  this.#log.verbose(`Forking store to ${forkPath}`);
86
92
  await this.#rootDb.backup(forkPath, false);
87
93
  const forkDb = open(forkPath, { noSync: this.isEphemeral });
@@ -119,6 +125,23 @@ export class AztecLmdbStore implements AztecKVStore, AztecAsyncKVStore {
119
125
  openCounter<K extends Key>(name: string): AztecCounter<K> & AztecAsyncCounter<K> {
120
126
  return new LmdbAztecCounter(this.#data, name);
121
127
  }
128
+ /**
129
+ * Creates a new AztecMultiMapWithSize in the store. A multi-map with size stores multiple values for a single key automatically.
130
+ * @param name - Name of the map
131
+ * @returns A new AztecMultiMapWithSize
132
+ */
133
+ openMultiMapWithSize<K extends Key, V>(name: string): AztecMultiMapWithSize<K, V> {
134
+ return new LmdbAztecMapWithSize(this.#multiMapData, name);
135
+ }
136
+
137
+ /**
138
+ * Creates a new AztecMapWithSize in the store.
139
+ * @param name - Name of the map
140
+ * @returns A new AztecMapWithSize
141
+ */
142
+ openMapWithSize<K extends Key, V>(name: string): AztecMapWithSize<K, V> {
143
+ return new LmdbAztecMapWithSize(this.#data, name);
144
+ }
122
145
 
123
146
  /**
124
147
  * Creates a new AztecArray in the store.
@@ -47,10 +47,10 @@ export class L2TipsStore implements L2BlockStreamEventHandler, L2BlockStreamLoca
47
47
  public async handleBlockStreamEvent(event: L2BlockStreamEvent): Promise<void> {
48
48
  switch (event.type) {
49
49
  case 'blocks-added':
50
- await this.l2TipsStore.set('latest', event.blocks.at(-1)!.number);
51
50
  for (const block of event.blocks) {
52
51
  await this.l2BlockHashesStore.set(block.number, block.header.hash().toString());
53
52
  }
53
+ await this.l2TipsStore.set('latest', event.blocks.at(-1)!.number);
54
54
  break;
55
55
  case 'chain-pruned':
56
56
  await this.l2TipsStore.set('latest', event.blockNumber);