import type { Bindable } from '@livestore/common' import { BoundMap, BoundSet, SessionIdSymbol } from '@livestore/common' const SymbolsBase = Symbol('base') const SymbolsBrand = Symbol('brand') type Opaque = BaseType & { readonly [SymbolsBase]: BaseType readonly [SymbolsBrand]: BrandType } export type CacheKey = Opaque type TableName = string const ignore = ['begin', 'rollback', 'commit', 'savepoint', 'release'] // TODO: profile to see how big we need this cache to be. const cacheSize = 200 export default class QueryCache { #entries = new BoundMap(cacheSize) #dependencies = new Map>() getKey = (sql: string, bindValues?: Bindable): CacheKey => { if (bindValues == null) { return sql as CacheKey } const formatValue = (value: any) => (value === SessionIdSymbol ? 'SessionIdSymbol' : String(value)) if (Array.isArray(bindValues) === true) { return `${sql}\n${bindValues.map(formatValue).join('\n')}` as CacheKey } return (sql + '\n' + Object.entries(bindValues) .map(([key, value]) => `${key}:${formatValue(value)}`) .join('\n')) as CacheKey } get = (key: CacheKey) => { return this.#entries.get(key) } set = (queriedTables: Iterable, key: CacheKey, results: any) => { this.#entries.set(key, results) for (const table of queriedTables) { let keys = this.#dependencies.get(table) if (keys == null) { keys = new BoundSet(cacheSize) keys.onEvict = this.#dependencyTrackerEvicted this.#dependencies.set(table, keys) } keys.add(key) } } #dependencyTrackerEvicted = (key: CacheKey) => { this.#entries.delete(key) } ignoreQuery = (query: string) => { return ignore.some((prefix) => query.startsWith(prefix)) } // The next simplest step is to create a specific implementation for invalidating // the expensive track list queries only when constraints data in a write overlaps with read constraints. // // As well as either: // a. removeing the big view (since we'll have our cache) // b. incrementally updating the view on insert by the EventImporter // // We'll not try to tackle any generalized approach until we have a proof of concept working. invalidate = (queriedTables: Iterable) => { for (const table of queriedTables) { const keys = this.#dependencies.get(table) if (keys == null) { continue } for (const k of keys) { this.#entries.delete(k) } } } }