Skip to content

Commit

Permalink
feat: in memory kv store.
Browse files Browse the repository at this point in the history
  • Loading branch information
charlielye committed Mar 30, 2024
1 parent 5f1ff04 commit eb35c20
Show file tree
Hide file tree
Showing 20 changed files with 857 additions and 168 deletions.
1 change: 1 addition & 0 deletions yarn-project/kv-store/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"exports": {
".": "./dest/interfaces/index.js",
"./lmdb": "./dest/lmdb/index.js",
"./mem": "./dest/mem/index.js",
"./utils": "./dest/utils.js"
},
"scripts": {
Expand Down
81 changes: 2 additions & 79 deletions yarn-project/kv-store/src/lmdb/array.test.ts
Original file line number Diff line number Diff line change
@@ -1,91 +1,14 @@
import { Database, open } from 'lmdb';

import { addArrayTests } from '../tests/aztec_array_tests.js';
import { LmdbAztecArray } from './array.js';

describe('LmdbAztecArray', () => {
let db: Database;
let arr: LmdbAztecArray<number>;

beforeEach(() => {
db = open({} as any);
arr = new LmdbAztecArray(db, 'test');
});

it('should be able to push and pop values', async () => {
await arr.push(1);
await arr.push(2);
await arr.push(3);

expect(arr.length).toEqual(3);
expect(await arr.pop()).toEqual(3);
expect(await arr.pop()).toEqual(2);
expect(await arr.pop()).toEqual(1);
expect(await arr.pop()).toEqual(undefined);
});

it('should be able to get values by index', async () => {
await arr.push(1);
await arr.push(2);
await arr.push(3);

expect(arr.at(0)).toEqual(1);
expect(arr.at(1)).toEqual(2);
expect(arr.at(2)).toEqual(3);
expect(arr.at(3)).toEqual(undefined);
expect(arr.at(-1)).toEqual(3);
expect(arr.at(-2)).toEqual(2);
expect(arr.at(-3)).toEqual(1);
expect(arr.at(-4)).toEqual(undefined);
});

it('should be able to set values by index', async () => {
await arr.push(1);
await arr.push(2);
await arr.push(3);

expect(await arr.setAt(0, 4)).toEqual(true);
expect(await arr.setAt(1, 5)).toEqual(true);
expect(await arr.setAt(2, 6)).toEqual(true);

expect(await arr.setAt(3, 7)).toEqual(false);

expect(arr.at(0)).toEqual(4);
expect(arr.at(1)).toEqual(5);
expect(arr.at(2)).toEqual(6);
expect(arr.at(3)).toEqual(undefined);

expect(await arr.setAt(-1, 8)).toEqual(true);
expect(await arr.setAt(-2, 9)).toEqual(true);
expect(await arr.setAt(-3, 10)).toEqual(true);

expect(await arr.setAt(-4, 11)).toEqual(false);

expect(arr.at(-1)).toEqual(8);
expect(arr.at(-2)).toEqual(9);
expect(arr.at(-3)).toEqual(10);
expect(arr.at(-4)).toEqual(undefined);
});

it('should be able to iterate over values', async () => {
await arr.push(1);
await arr.push(2);
await arr.push(3);

expect([...arr.values()]).toEqual([1, 2, 3]);
expect([...arr.entries()]).toEqual([
[0, 1],
[1, 2],
[2, 3],
]);
});

it('should be able to restore state', async () => {
await arr.push(1);
await arr.push(2);
await arr.push(3);

const arr2 = new LmdbAztecArray(db, 'test');
expect(arr2.length).toEqual(3);
expect([...arr2.values()]).toEqual([...arr.values()]);
});
addArrayTests(() => new LmdbAztecArray(db, 'test'));
});
89 changes: 2 additions & 87 deletions yarn-project/kv-store/src/lmdb/map.test.ts
Original file line number Diff line number Diff line change
@@ -1,99 +1,14 @@
import { Database, open } from 'lmdb';

import { addMapTests } from '../tests/aztec_map_tests.js';
import { LmdbAztecMap } from './map.js';

describe('LmdbAztecMap', () => {
let db: Database;
let map: LmdbAztecMap<string, string>;

beforeEach(() => {
db = open({ dupSort: true } as any);
map = new LmdbAztecMap(db, 'test');
});

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

expect(map.get('foo')).toEqual('bar');
expect(map.get('baz')).toEqual('qux');
expect(map.get('quux')).toEqual(undefined);
});

it('should be able to set values if they do not exist', async () => {
expect(await map.setIfNotExists('foo', 'bar')).toEqual(true);
expect(await map.setIfNotExists('foo', 'baz')).toEqual(false);

expect(map.get('foo')).toEqual('bar');
});

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

expect(await map.delete('foo')).toEqual(true);

expect(map.get('foo')).toEqual(undefined);
expect(map.get('baz')).toEqual('qux');
});

it('should be able to iterate over entries', async () => {
await map.set('foo', 'bar');
await map.set('baz', 'qux');

expect([...map.entries()]).toEqual([
['baz', 'qux'],
['foo', 'bar'],
]);
});

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

expect([...map.values()]).toEqual(['quux', 'bar']);
});

it('should be able to iterate over keys', async () => {
await map.set('foo', 'bar');
await map.set('baz', 'qux');

expect([...map.keys()]).toEqual(['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([...map.getValues('foo')]).toEqual(['bar', 'baz']);
});

it('supports tuple keys', async () => {
const map = new LmdbAztecMap<[number, string], string>(db, 'test');

await map.set([5, 'bar'], 'val');
await map.set([0, 'foo'], 'val');

expect([...map.keys()]).toEqual([
[0, 'foo'],
[5, 'bar'],
]);

expect(map.get([5, 'bar'])).toEqual('val');
});

it('supports range queries', async () => {
await map.set('a', 'a');
await map.set('b', 'b');
await map.set('c', 'c');
await map.set('d', 'd');

expect([...map.keys({ start: 'b', end: 'c' })]).toEqual(['b']);
expect([...map.keys({ start: 'b' })]).toEqual(['b', 'c', 'd']);
expect([...map.keys({ end: 'c' })]).toEqual(['a', 'b']);
expect([...map.keys({ start: 'b', end: 'c', reverse: true })]).toEqual(['c']);
expect([...map.keys({ start: 'b', limit: 1 })]).toEqual(['b']);
expect([...map.keys({ start: 'b', reverse: true })]).toEqual(['d', 'c']);
expect([...map.keys({ end: 'b', reverse: true })]).toEqual(['b', 'a']);
});
addMapTests(() => new LmdbAztecMap(db, 'test'));
});
7 changes: 7 additions & 0 deletions yarn-project/kv-store/src/mem/array.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { addArrayTests } from '../tests/aztec_array_tests.js';
import { MemAztecArray } from './array.js';
import { MemDb } from './mem_db.js';

describe('MemAztecArray', () => {
addArrayTests(() => new MemAztecArray('test', new MemDb()));
});
70 changes: 70 additions & 0 deletions yarn-project/kv-store/src/mem/array.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { type AztecArray } from '../interfaces/array.js';
import type { MemDb } from './mem_db.js';

/**
* An persistent array backed by mem.
*/
export class MemAztecArray<T> implements AztecArray<T> {
private slot: string;

constructor(private name: string, private db: MemDb) {
this.slot = JSON.stringify(['array', this.name]);
}

get length(): number {
return this.db.get(this.slot)?.length || 0;
}

push(...vals: T[]): Promise<number> {
const arr = this.db.get(this.slot);
if (arr) {
this.db.set(this.slot, [...arr, ...vals]);
} else {
this.db.set(this.slot, [...vals]);
}
return Promise.resolve(this.length);
}

pop(): Promise<T | undefined> {
const arr = [...this.db.get(this.slot)];
const result = arr.pop();
this.db.set(this.slot, arr);
return Promise.resolve(result);
}

at(index: number): T | undefined {
const arr = this.db.get(this.slot) || [];
if (index < 0) {
return arr[arr.length + index];
} else {
return arr[index];
}
}

setAt(index: number, val: T): Promise<boolean> {
if (index < 0) {
index = this.length + index;
}

if (index < 0 || index >= this.length) {
return Promise.resolve(false);
}

const arr = [...this.db.get(this.slot)];
arr[index] = val;
this.db.set(this.slot, arr);
return Promise.resolve(true);
}

entries(): IterableIterator<[number, T]> {
return this.db.get(this.slot)?.entries() || [];
}

values(): IterableIterator<T> {
return this.db.get(this.slot)?.values() || [];
}

[Symbol.iterator](): IterableIterator<T> {
return this.values();
}
}
7 changes: 7 additions & 0 deletions yarn-project/kv-store/src/mem/counter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { addCounterTests } from '../tests/aztec_counter_tests.js';
import { MemAztecCounter } from './counter.js';
import { MemDb } from './mem_db.js';

describe('MemAztecCounter', () => {
addCounterTests(() => new MemAztecCounter('test', new MemDb()));
});
50 changes: 50 additions & 0 deletions yarn-project/kv-store/src/mem/counter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { Key, Range } from '../interfaces/common.js';
import type { AztecCounter } from '../interfaces/counter.js';
import { MemAztecMap } from './map.js';
import type { MemDb } from './mem_db.js';

export class MemAztecCounter implements AztecCounter<Key> {
private map: MemAztecMap<number>;

constructor(name: string, db: MemDb) {
this.map = new MemAztecMap(name, db);
}

async set(key: Key, value: number): Promise<boolean> {
if (value) {
return this.map.set(key, value);
} else {
await this.map.delete(key);
return true;
}
}

async update(key: Key, delta = 1): Promise<boolean> {
const current = this.map.get(key) ?? 0;
const next = current + delta;

if (next < 0) {
throw new Error(`Cannot update ${key} in counter below zero`);
}

await this.map.delete(key);

if (next > 0) {
await this.map.set(key, next);
}

return true;
}

get(key: Key): number {
return this.map.get(key) ?? 0;
}

entries(range: Range<Key> = {}): IterableIterator<[Key, number]> {
return this.map.entries(range);
}

keys(range: Range<Key> = {}): IterableIterator<Key> {
return this.map.keys(range);
}
}
1 change: 1 addition & 0 deletions yarn-project/kv-store/src/mem/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { AztecMemStore } from './store.js';
7 changes: 7 additions & 0 deletions yarn-project/kv-store/src/mem/map.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { addMapTests } from '../tests/aztec_map_tests.js';
import { MemAztecMap } from './map.js';
import { MemDb } from './mem_db.js';

describe('MemAztecMap', () => {
addMapTests(() => new MemAztecMap('test', new MemDb()));
});
Loading

0 comments on commit eb35c20

Please sign in to comment.