Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 15 additions & 47 deletions yarn-project/kv-store/src/indexeddb/map.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
import type { IDBPDatabase, IDBPObjectStore } from 'idb';

import type { Key, Range } from '../interfaces/common.js';
import type { AztecAsyncMultiMap } from '../interfaces/map.js';
import type { AztecAsyncMap } from '../interfaces/map.js';
import type { AztecIDBSchema } from './store.js';

/**
* A map backed by IndexedDB.
*/
export class IndexedDBAztecMap<K extends Key, V> implements AztecAsyncMultiMap<K, V> {
export class IndexedDBAztecMap<K extends Key, V> implements AztecAsyncMap<K, V> {
protected name: string;
#container: string;
protected container: string;

#_db?: IDBPObjectStore<AztecIDBSchema, ['data'], 'data', 'readwrite'>;
#rootDB: IDBPDatabase<AztecIDBSchema>;

constructor(rootDB: IDBPDatabase<AztecIDBSchema>, mapName: string) {
this.name = mapName;
this.#container = `map:${mapName}`;
this.container = `map:${mapName}`;
this.#rootDB = rootDB;
}

Expand All @@ -29,38 +29,22 @@ export class IndexedDBAztecMap<K extends Key, V> implements AztecAsyncMultiMap<K
}

async getAsync(key: K): Promise<V | undefined> {
const data = await this.db.get(this.#slot(key));
const data = await this.db.get(this.slot(key));
return data?.value as V;
}

async *getValuesAsync(key: K): AsyncIterableIterator<V> {
const index = this.db.index('keyCount');
const rangeQuery = IDBKeyRange.bound(
[this.#container, this.#normalizeKey(key), 0],
[this.#container, this.#normalizeKey(key), Number.MAX_SAFE_INTEGER],
false,
false,
);
for await (const cursor of index.iterate(rangeQuery)) {
yield cursor.value.value as V;
}
}

async hasAsync(key: K): Promise<boolean> {
const result = (await this.getAsync(key)) !== undefined;
return result;
}

async set(key: K, val: V): Promise<void> {
const count = await this.db
.index('key')
.count(IDBKeyRange.bound([this.#container, this.#normalizeKey(key)], [this.#container, this.#normalizeKey(key)]));
await this.db.put({
value: val,
container: this.#container,
key: this.#normalizeKey(key),
keyCount: count + 1,
slot: this.#slot(key, count),
container: this.container,
key: this.normalizeKey(key),
keyCount: 1,
slot: this.slot(key),
});
}

Expand All @@ -77,30 +61,14 @@ export class IndexedDBAztecMap<K extends Key, V> implements AztecAsyncMultiMap<K
}

async delete(key: K): Promise<void> {
await this.db.delete(this.#slot(key));
}

async deleteValue(key: K, val: V): Promise<void> {
const index = this.db.index('keyCount');
const rangeQuery = IDBKeyRange.bound(
[this.#container, this.#normalizeKey(key), 0],
[this.#container, this.#normalizeKey(key), Number.MAX_SAFE_INTEGER],
false,
false,
);
for await (const cursor of index.iterate(rangeQuery)) {
if (JSON.stringify(cursor.value.value) === JSON.stringify(val)) {
await cursor.delete();
return;
}
}
await this.db.delete(this.slot(key));
}

async *entriesAsync(range: Range<K> = {}): AsyncIterableIterator<[K, V]> {
const index = this.db.index('key');
const rangeQuery = IDBKeyRange.bound(
[this.#container, range.start ?? ''],
[this.#container, range.end ?? '\uffff'],
[this.container, range.start ?? ''],
[this.container, range.end ?? '\uffff'],
!!range.reverse,
!range.reverse,
);
Expand Down Expand Up @@ -131,12 +99,12 @@ export class IndexedDBAztecMap<K extends Key, V> implements AztecAsyncMultiMap<K
return (denormalizedKey.length > 1 ? denormalizedKey : key) as K;
}

#normalizeKey(key: K): string {
protected normalizeKey(key: K): string {
const arrayKey = Array.isArray(key) ? key : [key];
return arrayKey.join(',');
}

#slot(key: K, index: number = 0): string {
return `map:${this.name}:slot:${this.#normalizeKey(key)}:${index}`;
protected slot(key: K, index: number = 0): string {
return `map:${this.name}:slot:${this.normalizeKey(key)}:${index}`;
}
}
7 changes: 7 additions & 0 deletions yarn-project/kv-store/src/indexeddb/multi_map.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { describeAztecMultiMap } from '../interfaces/multi_map_test_suite.js';
import { mockLogger } from '../interfaces/utils.js';
import { AztecIndexedDBStore } from './store.js';

describe('IndexedDBMap', () => {
describeAztecMultiMap('AztecMultiMap', async () => await AztecIndexedDBStore.open(mockLogger, undefined, true));
});
53 changes: 53 additions & 0 deletions yarn-project/kv-store/src/indexeddb/multi_map.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { Key } from '../interfaces/common.js';
import type { AztecAsyncMultiMap } from '../interfaces/multi_map.js';
import { IndexedDBAztecMap } from './map.js';

/**
* A multi map backed by IndexedDB.
*/
export class IndexedDBAztecMultiMap<K extends Key, V>
extends IndexedDBAztecMap<K, V>
implements AztecAsyncMultiMap<K, V>
Comment on lines +8 to +10

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a question whether you'd want this to use a different name as compared to the normal map?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mmm, not sure I follow? IDBMultimap extends IDBMap, implements AsyncMultiMap...LGTM even if a tongue twister 🤣

{
override async set(key: K, val: V): Promise<void> {
const count = await this.db
.index('key')
.count(IDBKeyRange.bound([this.container, this.normalizeKey(key)], [this.container, this.normalizeKey(key)]));
await this.db.put({
value: val,
container: this.container,
key: this.normalizeKey(key),
keyCount: count + 1,
slot: this.slot(key, count),
});
}

async *getValuesAsync(key: K): AsyncIterableIterator<V> {
const index = this.db.index('keyCount');
const rangeQuery = IDBKeyRange.bound(
[this.container, this.normalizeKey(key), 0],
[this.container, this.normalizeKey(key), Number.MAX_SAFE_INTEGER],
false,
false,
);
for await (const cursor of index.iterate(rangeQuery)) {
yield cursor.value.value as V;
}
}

async deleteValue(key: K, val: V): Promise<void> {
const index = this.db.index('keyCount');
const rangeQuery = IDBKeyRange.bound(
[this.container, this.normalizeKey(key), 0],
[this.container, this.normalizeKey(key), Number.MAX_SAFE_INTEGER],
false,
false,
);
for await (const cursor of index.iterate(rangeQuery)) {
if (JSON.stringify(cursor.value.value) === JSON.stringify(val)) {
await cursor.delete();
return;
}
}
}
}
12 changes: 9 additions & 3 deletions yarn-project/kv-store/src/indexeddb/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ import { type DBSchema, type IDBPDatabase, deleteDB, openDB } from 'idb';
import type { AztecAsyncArray } from '../interfaces/array.js';
import type { Key, StoreSize } from '../interfaces/common.js';
import type { AztecAsyncCounter } from '../interfaces/counter.js';
import type { AztecAsyncMap, AztecAsyncMultiMap } from '../interfaces/map.js';
import type { AztecAsyncMap } from '../interfaces/map.js';
import type { AztecAsyncMultiMap } from '../interfaces/multi_map.js';
import type { AztecAsyncSet } from '../interfaces/set.js';
import type { AztecAsyncSingleton } from '../interfaces/singleton.js';
import type { AztecAsyncKVStore } from '../interfaces/store.js';
import { IndexedDBAztecArray } from './array.js';
import { IndexedDBAztecMap } from './map.js';
import { IndexedDBAztecMultiMap } from './multi_map.js';
import { IndexedDBAztecSet } from './set.js';
import { IndexedDBAztecSingleton } from './singleton.js';

Expand All @@ -34,7 +36,11 @@ export class AztecIndexedDBStore implements AztecAsyncKVStore {
#name: string;

#containers = new Set<
IndexedDBAztecArray<any> | IndexedDBAztecMap<any, any> | IndexedDBAztecSet<any> | IndexedDBAztecSingleton<any>
| IndexedDBAztecArray<any>
| IndexedDBAztecMap<any, any>
| IndexedDBAztecMultiMap<any, any>
| IndexedDBAztecSet<any>
| IndexedDBAztecSingleton<any>
>();

constructor(rootDB: IDBPDatabase<AztecIDBSchema>, public readonly isEphemeral: boolean, log: Logger, name: string) {
Expand Down Expand Up @@ -119,7 +125,7 @@ export class AztecIndexedDBStore implements AztecAsyncKVStore {
* @returns A new AztecMultiMap
*/
openMultiMap<K extends Key, V>(name: string): AztecAsyncMultiMap<K, V> {
const multimap = new IndexedDBAztecMap<K, V>(this.#rootDB, name);
const multimap = new IndexedDBAztecMultiMap<K, V>(this.#rootDB, name);
this.#containers.add(multimap);
return multimap;
}
Expand Down
1 change: 1 addition & 0 deletions yarn-project/kv-store/src/interfaces/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export * from './counter.js';
export * from './singleton.js';
export * from './store.js';
export * from './set.js';
export * from './multi_map.js';
export type { Range, StoreSize } from './common.js';
52 changes: 0 additions & 52 deletions yarn-project/kv-store/src/interfaces/map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,40 +62,6 @@ export interface AztecMap<K extends Key, V> extends AztecBaseMap<K, V> {
clear(): Promise<void>;
}

export interface AztecMapWithSize<K extends Key, V> extends AztecMap<K, V> {
/**
* Gets the size of the map.
* @returns The size of the map
*/
size(): number;
}

/**
* A map backed by a persistent store that can have multiple values for a single key.
*/
export interface AztecMultiMap<K extends Key, V> extends AztecMap<K, V> {
/**
* Gets all the values at the given key.
* @param key - The key to get the values from
*/
getValues(key: K): IterableIterator<V>;

/**
* Deletes a specific value at the given key.
* @param key - The key to delete the value at
* @param val - The value to delete
*/
deleteValue(key: K, val: V): Promise<void>;
}

export interface AztecMultiMapWithSize<K extends Key, V> extends AztecMultiMap<K, V> {
/**
* Gets the size of the map.
* @returns The size of the map
*/
size(): number;
}

/**
* A map backed by a persistent store.
*/
Expand Down Expand Up @@ -131,21 +97,3 @@ export interface AztecAsyncMap<K extends Key, V> extends AztecBaseMap<K, V> {
*/
keysAsync(range?: Range<K>): AsyncIterableIterator<K>;
}

/**
* A map backed by a persistent store that can have multiple values for a single key.
*/
export interface AztecAsyncMultiMap<K extends Key, V> extends AztecAsyncMap<K, V> {
/**
* Gets all the values at the given key.
* @param key - The key to get the values from
*/
getValuesAsync(key: K): AsyncIterableIterator<V>;

/**
* Deletes a specific value at the given key.
* @param key - The key to delete the value at
* @param val - The value to delete
*/
deleteValue(key: K, val: V): Promise<void>;
}
51 changes: 18 additions & 33 deletions yarn-project/kv-store/src/interfaces/map_test_suite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { toArray } from '@aztec/foundation/iterable';
import { expect } from 'chai';

import type { Key, Range } from './common.js';
import type { AztecAsyncMap, AztecAsyncMultiMap, AztecMap, AztecMultiMap } from './map.js';
import type { AztecAsyncMap, AztecMap } from './map.js';
import type { AztecAsyncKVStore, AztecKVStore } from './store.js';
import { isSyncStore } from './utils.js';

Expand All @@ -14,11 +14,11 @@ export function describeAztecMap(
) {
describe(testName, () => {
let store: AztecKVStore | AztecAsyncKVStore;
let map: AztecMultiMap<Key, string> | AztecAsyncMultiMap<Key, string>;
let map: AztecMap<Key, string> | AztecAsyncMap<Key, string>;

beforeEach(async () => {
store = await getStore();
map = store.openMultiMap<string, string>('test');
map = store.openMap<string, string>('test');
});

afterEach(async () => {
Expand All @@ -27,32 +27,26 @@ export function describeAztecMap(

async function get(key: Key, sut: AztecAsyncMap<any, any> | AztecMap<any, any> = map) {
return isSyncStore(store) && !forceAsync
? (sut as AztecMultiMap<any, any>).get(key)
: await (sut as AztecAsyncMultiMap<any, any>).getAsync(key);
? (sut as AztecMap<any, any>).get(key)
: await (sut as AztecAsyncMap<any, any>).getAsync(key);
}

async function entries() {
return isSyncStore(store) && !forceAsync
? await toArray((map as AztecMultiMap<any, any>).entries())
: await toArray((map as AztecAsyncMultiMap<any, any>).entriesAsync());
? await toArray((map as AztecMap<any, any>).entries())
: await toArray((map as AztecAsyncMap<any, any>).entriesAsync());
}

async function values() {
return isSyncStore(store) && !forceAsync
? await toArray((map as AztecMultiMap<any, any>).values())
: await toArray((map as AztecAsyncMultiMap<any, any>).valuesAsync());
? await toArray((map as AztecMap<any, any>).values())
: await toArray((map as AztecAsyncMap<any, any>).valuesAsync());
}

async function keys(range?: Range<Key>, sut: AztecAsyncMap<any, any> | AztecMap<any, any> = map) {
return isSyncStore(store) && !forceAsync
? await toArray((sut as AztecMultiMap<any, any>).keys(range))
: await toArray((sut as AztecAsyncMultiMap<any, any>).keysAsync(range));
}

async function getValues(key: Key) {
return isSyncStore(store) && !forceAsync
? await toArray((map as AztecMultiMap<any, any>).getValues(key))
: await toArray((map as AztecAsyncMultiMap<any, any>).getValuesAsync(key));
? await toArray((sut as AztecMap<any, any>).keys(range))
: await toArray((sut as AztecAsyncMap<any, any>).keysAsync(range));
}

it('should be able to set and get values', async () => {
Expand All @@ -64,6 +58,13 @@ export function describeAztecMap(
expect(await get('quux')).to.equal(undefined);
});

it('should be able to overwrite values', async () => {
await map.set('foo', 'bar');
await map.set('foo', 'baz');

expect(await get('foo')).to.equal('baz');
});

it('should be able to set values if they do not exist', async () => {
expect(await map.setIfNotExists('foo', 'bar')).to.equal(true);
expect(await map.setIfNotExists('foo', 'baz')).to.equal(false);
Expand Down Expand Up @@ -109,22 +110,6 @@ export function describeAztecMap(
expect(await keys()).to.deep.equal(['baz', 'foo']);
});

it('should be able to get multiple values for a single key', async () => {
await map.set('foo', 'bar');
await map.set('foo', 'baz');

expect(await getValues('foo')).to.deep.equal(['bar', 'baz']);
});

it('should be able to delete individual values for a single key', async () => {
await map.set('foo', 'bar');
await map.set('foo', 'baz');

await map.deleteValue('foo', 'bar');

expect(await getValues('foo')).to.deep.equal(['baz']);
});

it('supports range queries', async () => {
await map.set('a', 'a');
await map.set('b', 'b');
Expand Down
Loading