import type { IDBPDatabase, IDBPObjectStore } from 'idb'; import { hash } from 'ohash'; import type { AztecAsyncArray } from '../interfaces/array.js'; import type { Value } from '../interfaces/common.js'; import type { AztecIDBSchema } from './store.js'; /** * A persistent array backed by IndexedDB. */ export class IndexedDBAztecArray implements AztecAsyncArray { #_db?: IDBPObjectStore; #rootDB: IDBPDatabase; #container: string; #name: string; constructor(rootDB: IDBPDatabase, name: string) { this.#rootDB = rootDB; this.#name = name; this.#container = `array:${this.#name}`; } set db(db: IDBPObjectStore | undefined) { this.#_db = db; } get db(): IDBPObjectStore { return this.#_db ? this.#_db : this.#rootDB.transaction('data', 'readwrite').store; } async lengthAsync(): Promise { return ( (await this.db .index('key') .count(IDBKeyRange.bound([this.#container, [this.#name]], [this.#container, [this.#name]]))) ?? 0 ); } async push(...vals: T[]): Promise { let length = await this.lengthAsync(); for (const val of vals) { await this.db.put({ value: val, hash: hash(val), container: this.#container, key: [this.#name], keyCount: length + 1, slot: this.#slot(length), }); length += 1; } return length; } async pop(): Promise { const length = await this.lengthAsync(); if (length === 0) { return undefined; } const slot = this.#slot(length - 1); const data = await this.db.get(slot); await this.db.delete(slot); return data?.value; } async atAsync(index: number): Promise { const length = await this.lengthAsync(); if (index < 0) { index = length + index; } const data = await this.db.get(this.#slot(index)); return data?.value; } async setAt(index: number, val: T): Promise { const length = await this.lengthAsync(); if (index < 0) { index = length + index; } if (index < 0 || index >= length) { return Promise.resolve(false); } await this.db.put({ value: val, hash: hash(val), container: this.#container, key: [this.#name], keyCount: index + 1, slot: this.#slot(index), }); return true; } async *entriesAsync(): AsyncIterableIterator<[number, T]> { const index = this.db.index('key'); const rangeQuery = IDBKeyRange.bound([this.#container, [this.#name]], [this.#container, [this.#name]]); for await (const cursor of index.iterate(rangeQuery)) { yield [cursor.value.keyCount - 1, cursor.value.value] as [number, T]; } } async *valuesAsync(): AsyncIterableIterator { for await (const [_, value] of this.entriesAsync()) { yield value; } } [Symbol.asyncIterator](): AsyncIterableIterator { return this.valuesAsync(); } #slot(index: number): string { return `array:${this.#name}:slot:${index}`; } }