import type { ScopedQueryResult, ScopedRunResult, ScopedStatement, ScopedValue } from "./types"; const SCHEMA_KEY = "scoped-schema"; /** A stable fingerprint of the DDL, so a deploy that adds tables re-applies the schema once. */ function fingerprint(schema: readonly string[]): string { let hash = 0; for (const statement of schema) { for (let index = 0; index < statement.length; index++) hash = (hash * 31 + statement.charCodeAt(index)) | 0; } return `${schema.length}:${hash}`; } /** * The SQLite inside a scoped Durable Object: applies the scope's schema (every statement is IF NOT EXISTS, so a * re-run only adds what's new) and runs statements for callers. One object per scope id, so every write here is * serialized and `transactionSync` makes a batch atomic. */ export class ScopedStore { private ready = false; constructor( private readonly storage: DurableObjectStorage, private readonly schema: readonly string[], ) {} get sql(): SqlStorage { return this.storage.sql; } /** Must run before any query; cheap after the first call in an isolate. */ async prepare(): Promise { if (this.ready) return; const print = fingerprint(this.schema); if ((await this.storage.get(SCHEMA_KEY)) !== print) { for (const statement of this.schema) this.sql.exec(statement); await this.storage.put(SCHEMA_KEY, print); } this.ready = true; } all(sql: string, params: ScopedValue[]): Record[] { return this.query(sql, params).rows; } /** Rows plus how many the statement read, for cost accounting (Durable Object SQL bills rows read). */ query(sql: string, params: ScopedValue[]): ScopedQueryResult { const cursor = this.sql.exec>(sql, ...params); const rows = cursor.toArray(); return { rows, rowsRead: cursor.rowsRead }; } run(sql: string, params: ScopedValue[]): ScopedRunResult { const cursor = this.sql.exec(sql, ...params); cursor.toArray(); return { ...this.lastResult(), rowsRead: cursor.rowsRead, rowsWritten: cursor.rowsWritten }; } batch(statements: ScopedStatement[]): ScopedRunResult[] { return this.storage.transactionSync(() => statements.map((statement) => { const cursor = this.sql.exec(statement.sql, ...statement.params); cursor.toArray(); return { ...this.lastResult(), rowsRead: cursor.rowsRead, rowsWritten: cursor.rowsWritten }; }), ); } /** Forgets everything, for when the repository or account is deleted. */ async wipe(): Promise { await this.storage.deleteAlarm(); await this.storage.deleteAll(); this.ready = false; } private lastResult(): ScopedRunResult { const row = this.sql .exec<{ changes: number; last_id: number }>("SELECT changes() AS changes, last_insert_rowid() AS last_id") .one(); return { changes: row.changes, lastRowId: row.last_id }; } }