@aztec/kv-store 0.0.0-test.1 → 0.0.1-commit.b655e406

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 (130) hide show
  1. package/dest/config.d.ts +1 -1
  2. package/dest/config.d.ts.map +1 -1
  3. package/dest/config.js +5 -3
  4. package/dest/indexeddb/array.d.ts +2 -1
  5. package/dest/indexeddb/array.d.ts.map +1 -1
  6. package/dest/indexeddb/array.js +3 -0
  7. package/dest/indexeddb/index.js +1 -1
  8. package/dest/indexeddb/map.d.ts +11 -5
  9. package/dest/indexeddb/map.d.ts.map +1 -1
  10. package/dest/indexeddb/map.js +38 -60
  11. package/dest/indexeddb/multi_map.d.ts +12 -0
  12. package/dest/indexeddb/multi_map.d.ts.map +1 -0
  13. package/dest/indexeddb/multi_map.js +78 -0
  14. package/dest/indexeddb/singleton.d.ts +2 -1
  15. package/dest/indexeddb/singleton.d.ts.map +1 -1
  16. package/dest/indexeddb/singleton.js +3 -1
  17. package/dest/indexeddb/store.d.ts +12 -13
  18. package/dest/indexeddb/store.d.ts.map +1 -1
  19. package/dest/indexeddb/store.js +45 -42
  20. package/dest/interfaces/array.d.ts +4 -3
  21. package/dest/interfaces/array.d.ts.map +1 -1
  22. package/dest/interfaces/array.js +1 -3
  23. package/dest/interfaces/common.d.ts +10 -8
  24. package/dest/interfaces/common.d.ts.map +1 -1
  25. package/dest/interfaces/common.js +8 -3
  26. package/dest/interfaces/index.d.ts +3 -1
  27. package/dest/interfaces/index.d.ts.map +1 -1
  28. package/dest/interfaces/index.js +2 -0
  29. package/dest/interfaces/map.d.ts +20 -48
  30. package/dest/interfaces/map.d.ts.map +1 -1
  31. package/dest/interfaces/map.js +1 -1
  32. package/dest/interfaces/map_test_suite.d.ts.map +1 -1
  33. package/dest/interfaces/map_test_suite.js +135 -70
  34. package/dest/interfaces/multi_map.d.ts +35 -0
  35. package/dest/interfaces/multi_map.d.ts.map +1 -0
  36. package/dest/interfaces/multi_map.js +3 -0
  37. package/dest/interfaces/multi_map_test_suite.d.ts +3 -0
  38. package/dest/interfaces/multi_map_test_suite.d.ts.map +1 -0
  39. package/dest/interfaces/multi_map_test_suite.js +245 -0
  40. package/dest/interfaces/store.d.ts +17 -42
  41. package/dest/interfaces/store.d.ts.map +1 -1
  42. package/dest/interfaces/utils.d.ts +1 -0
  43. package/dest/interfaces/utils.d.ts.map +1 -1
  44. package/dest/interfaces/utils.js +2 -1
  45. package/dest/lmdb/array.d.ts +2 -1
  46. package/dest/lmdb/array.d.ts.map +1 -1
  47. package/dest/lmdb/index.js +2 -2
  48. package/dest/lmdb/map.d.ts +10 -22
  49. package/dest/lmdb/map.d.ts.map +1 -1
  50. package/dest/lmdb/map.js +15 -81
  51. package/dest/lmdb/multi_map.d.ts +12 -0
  52. package/dest/lmdb/multi_map.d.ts.map +1 -0
  53. package/dest/lmdb/multi_map.js +29 -0
  54. package/dest/lmdb/store.d.ts +7 -22
  55. package/dest/lmdb/store.d.ts.map +1 -1
  56. package/dest/lmdb/store.js +11 -31
  57. package/dest/lmdb-v2/array.d.ts +2 -1
  58. package/dest/lmdb-v2/array.d.ts.map +1 -1
  59. package/dest/lmdb-v2/array.js +1 -0
  60. package/dest/lmdb-v2/factory.d.ts +1 -1
  61. package/dest/lmdb-v2/factory.d.ts.map +1 -1
  62. package/dest/lmdb-v2/factory.js +16 -6
  63. package/dest/lmdb-v2/map.d.ts +10 -43
  64. package/dest/lmdb-v2/map.d.ts.map +1 -1
  65. package/dest/lmdb-v2/map.js +17 -103
  66. package/dest/lmdb-v2/message.d.ts +23 -4
  67. package/dest/lmdb-v2/message.d.ts.map +1 -1
  68. package/dest/lmdb-v2/message.js +6 -4
  69. package/dest/lmdb-v2/multi_map.d.ts +51 -0
  70. package/dest/lmdb-v2/multi_map.d.ts.map +1 -0
  71. package/dest/lmdb-v2/multi_map.js +113 -0
  72. package/dest/lmdb-v2/read_transaction.d.ts +2 -0
  73. package/dest/lmdb-v2/read_transaction.d.ts.map +1 -1
  74. package/dest/lmdb-v2/read_transaction.js +34 -0
  75. package/dest/lmdb-v2/set.d.ts +15 -0
  76. package/dest/lmdb-v2/set.d.ts.map +1 -0
  77. package/dest/lmdb-v2/set.js +23 -0
  78. package/dest/lmdb-v2/singleton.d.ts.map +1 -1
  79. package/dest/lmdb-v2/singleton.js +1 -0
  80. package/dest/lmdb-v2/store.d.ts +9 -8
  81. package/dest/lmdb-v2/store.d.ts.map +1 -1
  82. package/dest/lmdb-v2/store.js +19 -7
  83. package/dest/lmdb-v2/utils.d.ts +2 -4
  84. package/dest/lmdb-v2/utils.d.ts.map +1 -1
  85. package/dest/lmdb-v2/write_transaction.d.ts +2 -4
  86. package/dest/lmdb-v2/write_transaction.d.ts.map +1 -1
  87. package/dest/stores/index.d.ts +1 -0
  88. package/dest/stores/index.d.ts.map +1 -1
  89. package/dest/stores/index.js +1 -0
  90. package/dest/stores/l2_tips_store.d.ts +2 -1
  91. package/dest/stores/l2_tips_store.d.ts.map +1 -1
  92. package/dest/stores/l2_tips_store.js +18 -9
  93. package/package.json +18 -14
  94. package/src/config.ts +6 -4
  95. package/src/indexeddb/array.ts +5 -1
  96. package/src/indexeddb/index.ts +2 -2
  97. package/src/indexeddb/map.ts +35 -53
  98. package/src/indexeddb/multi_map.ts +79 -0
  99. package/src/indexeddb/singleton.ts +4 -1
  100. package/src/indexeddb/store.ts +66 -56
  101. package/src/interfaces/array.ts +5 -3
  102. package/src/interfaces/common.ts +20 -9
  103. package/src/interfaces/index.ts +3 -1
  104. package/src/interfaces/map.ts +19 -53
  105. package/src/interfaces/map_test_suite.ts +73 -44
  106. package/src/interfaces/multi_map.ts +38 -0
  107. package/src/interfaces/multi_map_test_suite.ts +242 -0
  108. package/src/interfaces/store.ts +18 -53
  109. package/src/interfaces/utils.ts +1 -0
  110. package/src/lmdb/array.ts +2 -1
  111. package/src/lmdb/index.ts +3 -3
  112. package/src/lmdb/map.ts +23 -94
  113. package/src/lmdb/multi_map.ts +35 -0
  114. package/src/lmdb/store.ts +23 -47
  115. package/src/lmdb-v2/array.ts +7 -2
  116. package/src/lmdb-v2/factory.ts +17 -10
  117. package/src/lmdb-v2/map.ts +29 -126
  118. package/src/lmdb-v2/message.ts +23 -0
  119. package/src/lmdb-v2/multi_map.ts +141 -0
  120. package/src/lmdb-v2/read_transaction.ts +40 -0
  121. package/src/lmdb-v2/set.ts +33 -0
  122. package/src/lmdb-v2/singleton.ts +5 -1
  123. package/src/lmdb-v2/store.ts +22 -14
  124. package/src/lmdb-v2/write_transaction.ts +2 -2
  125. package/src/stores/index.ts +2 -0
  126. package/src/stores/l2_tips_store.ts +18 -9
  127. package/dest/interfaces/store_test_suite.d.ts +0 -3
  128. package/dest/interfaces/store_test_suite.d.ts.map +0 -1
  129. package/dest/interfaces/store_test_suite.js +0 -37
  130. package/src/interfaces/store_test_suite.ts +0 -56
@@ -1,4 +1,4 @@
1
- /** Stores currently synced L2 tips and unfinalized block hashes. */ export class L2TipsStore {
1
+ /** Stores currently synced L2 tips and unfinalized block hashes. */ export class L2TipsKVStore {
2
2
  l2TipsStore;
3
3
  l2BlockHashesStore;
4
4
  constructor(store, namespace){
@@ -41,25 +41,34 @@
41
41
  async handleBlockStreamEvent(event) {
42
42
  switch(event.type){
43
43
  case 'blocks-added':
44
- for (const block of event.blocks){
45
- await this.l2BlockHashesStore.set(block.number, (await block.header.hash()).toString());
44
+ {
45
+ const blocks = event.blocks.map((b)=>b.block);
46
+ for (const block of blocks){
47
+ await this.l2BlockHashesStore.set(block.number, (await block.hash()).toString());
48
+ }
49
+ await this.l2TipsStore.set('latest', blocks.at(-1).number);
50
+ break;
46
51
  }
47
- await this.l2TipsStore.set('latest', event.blocks.at(-1).number);
48
- break;
49
52
  case 'chain-pruned':
50
- await this.l2TipsStore.set('latest', event.blockNumber);
53
+ await this.saveTag('latest', event.block);
51
54
  break;
52
55
  case 'chain-proven':
53
- await this.l2TipsStore.set('proven', event.blockNumber);
56
+ await this.saveTag('proven', event.block);
54
57
  break;
55
58
  case 'chain-finalized':
56
- await this.l2TipsStore.set('finalized', event.blockNumber);
59
+ await this.saveTag('finalized', event.block);
57
60
  for await (const key of this.l2BlockHashesStore.keysAsync({
58
- end: event.blockNumber
61
+ end: event.block.number
59
62
  })){
60
63
  await this.l2BlockHashesStore.delete(key);
61
64
  }
62
65
  break;
63
66
  }
64
67
  }
68
+ async saveTag(name, block) {
69
+ await this.l2TipsStore.set(name, block.number);
70
+ if (block.hash) {
71
+ await this.l2BlockHashesStore.set(block.number, block.hash);
72
+ }
73
+ }
65
74
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aztec/kv-store",
3
- "version": "0.0.0-test.1",
3
+ "version": "0.0.1-commit.b655e406",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./dest/interfaces/index.js",
@@ -14,46 +14,46 @@
14
14
  "build": "yarn clean && tsc -b",
15
15
  "build:dev": "tsc -b --watch",
16
16
  "clean": "rm -rf ./dest .tsbuildinfo",
17
- "formatting": "run -T prettier --check ./src && run -T eslint ./src",
18
- "formatting:fix": "run -T eslint --fix ./src && run -T prettier -w ./src",
19
17
  "test:node": "NODE_NO_WARNINGS=1 mocha --config ./.mocharc.json",
20
18
  "test:browser": "wtr --config ./web-test-runner.config.mjs",
21
- "test": "yarn test:node && yarn test:browser && true"
19
+ "test": "yarn test:node",
20
+ "test:jest": "NODE_NO_WARNINGS=1 node --experimental-vm-modules ../node_modules/.bin/jest --passWithNoTests --maxWorkers=${JEST_MAX_WORKERS:-8}"
22
21
  },
23
22
  "inherits": [
24
23
  "../package.common.json",
25
24
  "./package.local.json"
26
25
  ],
27
26
  "dependencies": {
28
- "@aztec/ethereum": "0.0.0-test.1",
29
- "@aztec/foundation": "0.0.0-test.1",
30
- "@aztec/native": "0.0.0-test.1",
31
- "@aztec/stdlib": "0.0.0-test.1",
27
+ "@aztec/ethereum": "0.0.1-commit.b655e406",
28
+ "@aztec/foundation": "0.0.1-commit.b655e406",
29
+ "@aztec/native": "0.0.1-commit.b655e406",
30
+ "@aztec/stdlib": "0.0.1-commit.b655e406",
32
31
  "idb": "^8.0.0",
33
32
  "lmdb": "^3.2.0",
34
33
  "msgpackr": "^1.11.2",
34
+ "ohash": "^2.0.11",
35
35
  "ordered-binary": "^1.5.3"
36
36
  },
37
37
  "devDependencies": {
38
- "@jest/globals": "^29.5.0",
38
+ "@jest/globals": "^30.0.0",
39
39
  "@types/chai": "^5.0.1",
40
40
  "@types/chai-as-promised": "^8.0.1",
41
- "@types/jest": "^29.5.0",
41
+ "@types/jest": "^30.0.0",
42
42
  "@types/mocha": "^10.0.10",
43
43
  "@types/mocha-each": "^2.0.4",
44
- "@types/node": "^18.7.23",
44
+ "@types/node": "^22.15.17",
45
45
  "@types/sinon": "^17.0.3",
46
46
  "@web/dev-server-esbuild": "^1.0.3",
47
47
  "@web/test-runner": "^0.19.0",
48
48
  "@web/test-runner-playwright": "^0.11.0",
49
49
  "chai": "^5.1.2",
50
50
  "chai-as-promised": "^8.0.1",
51
- "jest": "^29.5.0",
51
+ "jest": "^30.0.0",
52
52
  "mocha": "^10.8.2",
53
53
  "mocha-each": "^2.0.1",
54
54
  "sinon": "^19.0.2",
55
55
  "ts-node": "^10.9.1",
56
- "typescript": "^5.0.4"
56
+ "typescript": "^5.3.3"
57
57
  },
58
58
  "files": [
59
59
  "dest",
@@ -61,7 +61,7 @@
61
61
  "!*.test.*"
62
62
  ],
63
63
  "engines": {
64
- "node": ">=18"
64
+ "node": ">=20.10"
65
65
  },
66
66
  "jest": {
67
67
  "extensionsToTreatAsEsm": [
@@ -94,6 +94,10 @@
94
94
  "testTimeout": 120000,
95
95
  "setupFiles": [
96
96
  "../../foundation/src/jest/setup.mjs"
97
+ ],
98
+ "testEnvironment": "../../foundation/src/jest/env.mjs",
99
+ "setupFilesAfterEnv": [
100
+ "../../foundation/src/jest/setupAfterEnv.mjs"
97
101
  ]
98
102
  }
99
103
  }
package/src/config.ts CHANGED
@@ -4,7 +4,7 @@ import type { EthAddress } from '@aztec/foundation/eth-address';
4
4
 
5
5
  export type DataStoreConfig = {
6
6
  dataDirectory: string | undefined;
7
- dataStoreMapSizeKB: number;
7
+ dataStoreMapSizeKb: number;
8
8
  l1Contracts?: { rollupAddress: EthAddress };
9
9
  };
10
10
 
@@ -13,14 +13,16 @@ export const dataConfigMappings: ConfigMappingsType<DataStoreConfig> = {
13
13
  env: 'DATA_DIRECTORY',
14
14
  description: 'Optional dir to store data. If omitted will store in memory.',
15
15
  },
16
- dataStoreMapSizeKB: {
16
+ dataStoreMapSizeKb: {
17
17
  env: 'DATA_STORE_MAP_SIZE_KB',
18
- description: 'DB mapping size to be applied to all key/value stores',
18
+ description: 'The maximum possible size of a data store DB in KB. Can be overridden by component-specific options.',
19
19
  ...numberConfigHelper(128 * 1_024 * 1_024), // Defaulted to 128 GB
20
20
  },
21
21
  l1Contracts: {
22
22
  description: 'The deployed L1 contract addresses',
23
- nested: l1ContractAddressesMapping,
23
+ nested: {
24
+ rollupAddress: l1ContractAddressesMapping.rollupAddress,
25
+ },
24
26
  },
25
27
  };
26
28
 
@@ -1,12 +1,14 @@
1
1
  import type { IDBPDatabase, IDBPObjectStore } from 'idb';
2
+ import { hash } from 'ohash';
2
3
 
3
4
  import type { AztecAsyncArray } from '../interfaces/array.js';
5
+ import type { Value } from '../interfaces/common.js';
4
6
  import type { AztecIDBSchema } from './store.js';
5
7
 
6
8
  /**
7
9
  * A persistent array backed by IndexedDB.
8
10
  */
9
- export class IndexedDBAztecArray<T> implements AztecAsyncArray<T> {
11
+ export class IndexedDBAztecArray<T extends Value> implements AztecAsyncArray<T> {
10
12
  #_db?: IDBPObjectStore<AztecIDBSchema, ['data'], 'data', 'readwrite'>;
11
13
  #rootDB: IDBPDatabase<AztecIDBSchema>;
12
14
  #container: string;
@@ -39,6 +41,7 @@ export class IndexedDBAztecArray<T> implements AztecAsyncArray<T> {
39
41
  for (const val of vals) {
40
42
  await this.db.put({
41
43
  value: val,
44
+ hash: hash(val),
42
45
  container: this.#container,
43
46
  key: this.#name,
44
47
  keyCount: length + 1,
@@ -86,6 +89,7 @@ export class IndexedDBAztecArray<T> implements AztecAsyncArray<T> {
86
89
 
87
90
  await this.db.put({
88
91
  value: val,
92
+ hash: hash(val),
89
93
  container: this.#container,
90
94
  key: this.#name,
91
95
  keyCount: index + 1,
@@ -14,8 +14,8 @@ export async function createStore(name: string, config: DataStoreConfig, log: Lo
14
14
 
15
15
  log.info(
16
16
  dataDirectory
17
- ? `Creating ${name} data store at directory ${dataDirectory} with map size ${config.dataStoreMapSizeKB} KB`
18
- : `Creating ${name} ephemeral data store with map size ${config.dataStoreMapSizeKB} KB`,
17
+ ? `Creating ${name} data store at directory ${dataDirectory} with map size ${config.dataStoreMapSizeKb} KB`
18
+ : `Creating ${name} ephemeral data store with map size ${config.dataStoreMapSizeKb} KB`,
19
19
  );
20
20
  const store = await AztecIndexedDBStore.open(createLogger('kv-store:indexeddb'), dataDirectory ?? '', false);
21
21
  if (config.l1Contracts?.rollupAddress) {
@@ -1,22 +1,23 @@
1
1
  import type { IDBPDatabase, IDBPObjectStore } from 'idb';
2
+ import { hash } from 'ohash';
2
3
 
3
- import type { Key, Range } from '../interfaces/common.js';
4
- import type { AztecAsyncMultiMap } from '../interfaces/map.js';
4
+ import type { Key, Range, Value } from '../interfaces/common.js';
5
+ import type { AztecAsyncMap } from '../interfaces/map.js';
5
6
  import type { AztecIDBSchema } from './store.js';
6
7
 
7
8
  /**
8
9
  * A map backed by IndexedDB.
9
10
  */
10
- export class IndexedDBAztecMap<K extends Key, V> implements AztecAsyncMultiMap<K, V> {
11
+ export class IndexedDBAztecMap<K extends Key, V extends Value> implements AztecAsyncMap<K, V> {
11
12
  protected name: string;
12
- #container: string;
13
+ protected container: string;
13
14
 
14
15
  #_db?: IDBPObjectStore<AztecIDBSchema, ['data'], 'data', 'readwrite'>;
15
16
  #rootDB: IDBPDatabase<AztecIDBSchema>;
16
17
 
17
18
  constructor(rootDB: IDBPDatabase<AztecIDBSchema>, mapName: string) {
18
19
  this.name = mapName;
19
- this.#container = `map:${mapName}`;
20
+ this.container = `map:${mapName}`;
20
21
  this.#rootDB = rootDB;
21
22
  }
22
23
 
@@ -29,41 +30,38 @@ export class IndexedDBAztecMap<K extends Key, V> implements AztecAsyncMultiMap<K
29
30
  }
30
31
 
31
32
  async getAsync(key: K): Promise<V | undefined> {
32
- const data = await this.db.get(this.#slot(key));
33
+ const data = await this.db.get(this.slot(key));
33
34
  return data?.value as V;
34
35
  }
35
36
 
36
- async *getValuesAsync(key: K): AsyncIterableIterator<V> {
37
- const index = this.db.index('keyCount');
38
- const rangeQuery = IDBKeyRange.bound(
39
- [this.#container, this.#normalizeKey(key), 0],
40
- [this.#container, this.#normalizeKey(key), Number.MAX_SAFE_INTEGER],
41
- false,
42
- false,
43
- );
44
- for await (const cursor of index.iterate(rangeQuery)) {
45
- yield cursor.value.value as V;
46
- }
47
- }
48
-
49
37
  async hasAsync(key: K): Promise<boolean> {
50
38
  const result = (await this.getAsync(key)) !== undefined;
51
39
  return result;
52
40
  }
53
41
 
42
+ async sizeAsync(): Promise<number> {
43
+ const index = this.db.index('key');
44
+ const rangeQuery = IDBKeyRange.bound([this.container, ''], [this.container, '\uffff']);
45
+ return await index.count(rangeQuery);
46
+ }
47
+
54
48
  async set(key: K, val: V): Promise<void> {
55
- const count = await this.db
56
- .index('key')
57
- .count(IDBKeyRange.bound([this.#container, this.#normalizeKey(key)], [this.#container, this.#normalizeKey(key)]));
58
49
  await this.db.put({
59
50
  value: val,
60
- container: this.#container,
61
- key: this.#normalizeKey(key),
62
- keyCount: count + 1,
63
- slot: this.#slot(key, count),
51
+ hash: hash(val),
52
+ container: this.container,
53
+ key: this.normalizeKey(key),
54
+ keyCount: 1,
55
+ slot: this.slot(key),
64
56
  });
65
57
  }
66
58
 
59
+ async setMany(entries: { key: K; value: V }[]): Promise<void> {
60
+ for (const { key, value } of entries) {
61
+ await this.set(key, value);
62
+ }
63
+ }
64
+
67
65
  swap(_key: K, _fn: (val: V | undefined) => V): Promise<void> {
68
66
  throw new Error('Not implemented');
69
67
  }
@@ -77,30 +75,14 @@ export class IndexedDBAztecMap<K extends Key, V> implements AztecAsyncMultiMap<K
77
75
  }
78
76
 
79
77
  async delete(key: K): Promise<void> {
80
- await this.db.delete(this.#slot(key));
81
- }
82
-
83
- async deleteValue(key: K, val: V): Promise<void> {
84
- const index = this.db.index('keyCount');
85
- const rangeQuery = IDBKeyRange.bound(
86
- [this.#container, this.#normalizeKey(key), 0],
87
- [this.#container, this.#normalizeKey(key), Number.MAX_SAFE_INTEGER],
88
- false,
89
- false,
90
- );
91
- for await (const cursor of index.iterate(rangeQuery)) {
92
- if (JSON.stringify(cursor.value.value) === JSON.stringify(val)) {
93
- await cursor.delete();
94
- return;
95
- }
96
- }
78
+ await this.db.delete(this.slot(key));
97
79
  }
98
80
 
99
81
  async *entriesAsync(range: Range<K> = {}): AsyncIterableIterator<[K, V]> {
100
82
  const index = this.db.index('key');
101
83
  const rangeQuery = IDBKeyRange.bound(
102
- [this.#container, range.start ?? ''],
103
- [this.#container, range.end ?? '\uffff'],
84
+ [this.container, range.start ? this.normalizeKey(range.start) : ''],
85
+ [this.container, range.end ? this.normalizeKey(range.end) : '\uffff'],
104
86
  !!range.reverse,
105
87
  !range.reverse,
106
88
  );
@@ -109,7 +91,7 @@ export class IndexedDBAztecMap<K extends Key, V> implements AztecAsyncMultiMap<K
109
91
  if (range.limit && count >= range.limit) {
110
92
  return;
111
93
  }
112
- yield [cursor.value.key, cursor.value.value] as [K, V];
94
+ yield [this.#denormalizeKey(cursor.value.key), cursor.value.value] as [K, V];
113
95
  count++;
114
96
  }
115
97
  }
@@ -122,21 +104,21 @@ export class IndexedDBAztecMap<K extends Key, V> implements AztecAsyncMultiMap<K
122
104
 
123
105
  async *keysAsync(range: Range<K> = {}): AsyncIterableIterator<K> {
124
106
  for await (const [key, _] of this.entriesAsync(range)) {
125
- yield this.#denormalizeKey(key as string);
107
+ yield key;
126
108
  }
127
109
  }
128
110
 
129
111
  #denormalizeKey(key: string): K {
130
- const denormalizedKey = (key as string).split(',').map(part => (isNaN(parseInt(part)) ? part : parseInt(part)));
131
- return (denormalizedKey.length > 1 ? denormalizedKey : key) as K;
112
+ const denormalizedKey = key.split(',').map(part => (part.startsWith('n_') ? Number(part.slice(2)) : part));
113
+ return (denormalizedKey.length > 1 ? denormalizedKey : denormalizedKey[0]) as K;
132
114
  }
133
115
 
134
- #normalizeKey(key: K): string {
116
+ protected normalizeKey(key: K): string {
135
117
  const arrayKey = Array.isArray(key) ? key : [key];
136
- return arrayKey.join(',');
118
+ return (arrayKey as K[]).map((element: K) => (typeof element === 'number' ? `n_${element}` : element)).join(',');
137
119
  }
138
120
 
139
- #slot(key: K, index: number = 0): string {
140
- return `map:${this.name}:slot:${this.#normalizeKey(key)}:${index}`;
121
+ protected slot(key: K, index: number = 0): string {
122
+ return `map:${this.name}:slot:${this.normalizeKey(key)}:${index}`;
141
123
  }
142
124
  }
@@ -0,0 +1,79 @@
1
+ import { hash } from 'ohash';
2
+
3
+ import type { Key, Value } from '../interfaces/common.js';
4
+ import type { AztecAsyncMultiMap } from '../interfaces/multi_map.js';
5
+ import { IndexedDBAztecMap } from './map.js';
6
+
7
+ /**
8
+ * A multi map backed by IndexedDB.
9
+ */
10
+ export class IndexedDBAztecMultiMap<K extends Key, V extends Value>
11
+ extends IndexedDBAztecMap<K, V>
12
+ implements AztecAsyncMultiMap<K, V>
13
+ {
14
+ override async set(key: K, val: V): Promise<void> {
15
+ // Inserting repeated values is a no-op
16
+ const exists = !!(await this.db
17
+ .index('hash')
18
+ .get(
19
+ IDBKeyRange.bound(
20
+ [this.container, this.normalizeKey(key), hash(val)],
21
+ [this.container, this.normalizeKey(key), hash(val)],
22
+ ),
23
+ ));
24
+ if (exists) {
25
+ return;
26
+ }
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;
40
+ await this.db.put({
41
+ value: val,
42
+ hash: hash(val),
43
+ container: this.container,
44
+ key: this.normalizeKey(key),
45
+ keyCount: count + 1,
46
+ slot: this.slot(key, count),
47
+ });
48
+ }
49
+
50
+ async *getValuesAsync(key: K): AsyncIterableIterator<V> {
51
+ // Iterate over the whole range of keyCount for the given key
52
+ const index = this.db.index('keyCount');
53
+ const rangeQuery = IDBKeyRange.bound(
54
+ [this.container, this.normalizeKey(key), 0],
55
+ [this.container, this.normalizeKey(key), Number.MAX_SAFE_INTEGER],
56
+ false,
57
+ false,
58
+ );
59
+ for await (const cursor of index.iterate(rangeQuery)) {
60
+ yield cursor.value.value as V;
61
+ }
62
+ }
63
+
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
67
+ const fullKey = await this.db
68
+ .index('hash')
69
+ .getKey(
70
+ IDBKeyRange.bound(
71
+ [this.container, this.normalizeKey(key), hash(val)],
72
+ [this.container, this.normalizeKey(key), hash(val)],
73
+ ),
74
+ );
75
+ if (fullKey) {
76
+ await this.db.delete(fullKey);
77
+ }
78
+ }
79
+ }
@@ -1,12 +1,14 @@
1
1
  import type { IDBPDatabase, IDBPObjectStore } from 'idb';
2
+ import { hash } from 'ohash';
2
3
 
4
+ import type { Value } from '../interfaces/common.js';
3
5
  import type { AztecAsyncSingleton } from '../interfaces/singleton.js';
4
6
  import type { AztecIDBSchema } from './store.js';
5
7
 
6
8
  /**
7
9
  * Stores a single value in IndexedDB.
8
10
  */
9
- export class IndexedDBAztecSingleton<T> implements AztecAsyncSingleton<T> {
11
+ export class IndexedDBAztecSingleton<T extends Value> implements AztecAsyncSingleton<T> {
10
12
  #_db?: IDBPObjectStore<AztecIDBSchema, ['data'], 'data', 'readwrite'>;
11
13
  #rootDB: IDBPDatabase<AztecIDBSchema>;
12
14
  #container: string;
@@ -38,6 +40,7 @@ export class IndexedDBAztecSingleton<T> implements AztecAsyncSingleton<T> {
38
40
  key: this.#slot,
39
41
  keyCount: 1,
40
42
  value: val,
43
+ hash: hash(val),
41
44
  });
42
45
  return result !== undefined;
43
46
  }