83 lines · 2.9 KB
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<void> {
    if (this.ready) return;
    const print = fingerprint(this.schema);
    if ((await this.storage.get<string>(SCHEMA_KEY)) !== print) {
      for (const statement of this.schema) this.sql.exec(statement);
      await this.storage.put(SCHEMA_KEY, print);
    }
    this.ready = true;
  }