88 lines
3.0 KiB
JavaScript
88 lines
3.0 KiB
JavaScript
export class SqlCache {
|
|
#owner;
|
|
#sqls;
|
|
capacity;
|
|
constructor(owner, capacity) {
|
|
this.#owner = owner;
|
|
this.#sqls = new Lru();
|
|
this.capacity = capacity;
|
|
}
|
|
// Replaces SQL strings with cached `hrana.Sql` objects in the statements in `hranaStmts`. After this
|
|
// function returns, we guarantee that all `hranaStmts` refer to valid (not closed) `hrana.Sql` objects,
|
|
// but _we may invalidate any other `hrana.Sql` objects_ (by closing them, thus removing them from the
|
|
// server).
|
|
//
|
|
// In practice, this means that after calling this function, you can use the statements only up to the
|
|
// first `await`, because concurrent code may also use the cache and invalidate those statements.
|
|
apply(hranaStmts) {
|
|
if (this.capacity <= 0) {
|
|
return;
|
|
}
|
|
const usedSqlObjs = new Set();
|
|
for (const hranaStmt of hranaStmts) {
|
|
if (typeof hranaStmt.sql !== "string") {
|
|
continue;
|
|
}
|
|
const sqlText = hranaStmt.sql;
|
|
// Stored SQL cannot exceed 5kb.
|
|
// https://github.com/tursodatabase/libsql/blob/e9d637e051685f92b0da43849507b5ef4232fbeb/libsql-server/src/hrana/http/request.rs#L10
|
|
if (sqlText.length >= 5000) {
|
|
continue;
|
|
}
|
|
let sqlObj = this.#sqls.get(sqlText);
|
|
if (sqlObj === undefined) {
|
|
while (this.#sqls.size + 1 > this.capacity) {
|
|
const [evictSqlText, evictSqlObj] = this.#sqls.peekLru();
|
|
if (usedSqlObjs.has(evictSqlObj)) {
|
|
// The SQL object that we are trying to evict is already in use in this batch, so we
|
|
// must not evict and close it.
|
|
break;
|
|
}
|
|
evictSqlObj.close();
|
|
this.#sqls.delete(evictSqlText);
|
|
}
|
|
if (this.#sqls.size + 1 <= this.capacity) {
|
|
sqlObj = this.#owner.storeSql(sqlText);
|
|
this.#sqls.set(sqlText, sqlObj);
|
|
}
|
|
}
|
|
if (sqlObj !== undefined) {
|
|
hranaStmt.sql = sqlObj;
|
|
usedSqlObjs.add(sqlObj);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
class Lru {
|
|
// This maps keys to the cache values. The entries are ordered by their last use (entires that were used
|
|
// most recently are at the end).
|
|
#cache;
|
|
constructor() {
|
|
this.#cache = new Map();
|
|
}
|
|
get(key) {
|
|
const value = this.#cache.get(key);
|
|
if (value !== undefined) {
|
|
// move the entry to the back of the Map
|
|
this.#cache.delete(key);
|
|
this.#cache.set(key, value);
|
|
}
|
|
return value;
|
|
}
|
|
set(key, value) {
|
|
this.#cache.set(key, value);
|
|
}
|
|
peekLru() {
|
|
for (const entry of this.#cache.entries()) {
|
|
return entry;
|
|
}
|
|
return undefined;
|
|
}
|
|
delete(key) {
|
|
this.#cache.delete(key);
|
|
}
|
|
get size() {
|
|
return this.#cache.size;
|
|
}
|
|
}
|