174 lines · 4.6 KB · TypeScript
RawDownload
import fs from "node:fs";import os from "node:os";import path from "node:path"; import {  type RendererStateSetInput,  type RendererStateSnapshot,  rendererStateSetInputSchema,  rendererStateSnapshotSchema,} from "@t3tools/contracts"; interface RowShape {  stateJson: string;  updatedAt: string;} interface SqliteAdapter {  getState(): RowShape | null;  setState(stateJson: string, updatedAt: string): void;  close(): void;} interface BunSqliteDatabase {  exec(sql: string): void;  run(sql: string, ...params: unknown[]): unknown;  query(sql: string): {    get(...params: unknown[]): unknown;  };  close(): void;} interface BunSqliteModule {  Database: new (filename?: string) => BunSqliteDatabase;} function defaultStateDir(): string {  return path.join(os.homedir(), ".t3", "userdata");} function isObject(value: unknown): value is Record<string, unknown> {  return typeof value === "object" && value !== null;} function parseRowShape(row: unknown): RowShape | null {  if (!isObject(row)) return null;  const stateJson = row.stateJson;  const updatedAt = row.updatedAt;  if (typeof stateJson !== "string" || typeof updatedAt !== "string") return null;  return { stateJson, updatedAt };} async function createNodeSqliteAdapter(dbPath: string): Promise<SqliteAdapter> {  const module = await import("node:sqlite");  const db = new module.DatabaseSync(dbPath);  db.exec(`    CREATE TABLE IF NOT EXISTS renderer_state (      id INTEGER PRIMARY KEY CHECK (id = 1),      state_json TEXT NOT NULL,      updated_at TEXT NOT NULL    )  `);  const selectStatement = db.prepare(`    SELECT state_json AS stateJson, updated_at AS updatedAt    FROM renderer_state    WHERE id = 1  `);  const upsertStatement = db.prepare(`    INSERT INTO renderer_state (id, state_json, updated_at)    VALUES (1, ?, ?)    ON CONFLICT(id) DO UPDATE SET      state_json = excluded.state_json,      updated_at = excluded.updated_at  `);   return {    getState() {      return parseRowShape(selectStatement.get());    },    setState(stateJson: string, updatedAt: string) {      upsertStatement.run(stateJson, updatedAt);    },    close() {      db.close();    },  };} async function createBunSqliteAdapter(dbPath: string): Promise<SqliteAdapter> {  const loadBunSqlite = new Function(    "return import('bun:sqlite')",  ) as () => Promise<BunSqliteModule>;  const module = await loadBunSqlite();  const db = new module.Database(dbPath);  db.exec(`    CREATE TABLE IF NOT EXISTS renderer_state (      id INTEGER PRIMARY KEY CHECK (id = 1),      state_json TEXT NOT NULL,      updated_at TEXT NOT NULL    )  `);  const selectQuery = db.query(`    SELECT state_json AS stateJson, updated_at AS updatedAt    FROM renderer_state    WHERE id = 1  `);  const upsertSql = `    INSERT INTO renderer_state (id, state_json, updated_at)    VALUES (1, ?, ?)    ON CONFLICT(id) DO UPDATE SET      state_json = excluded.state_json,      updated_at = excluded.updated_at  `;   return {    getState() {      return parseRowShape(selectQuery.get());    },    setState(stateJson: string, updatedAt: string) {      db.run(upsertSql, stateJson, updatedAt);    },    close() {      db.close();    },  };} async function createSqliteAdapter(dbPath: string): Promise<SqliteAdapter> {  try {    return await createNodeSqliteAdapter(dbPath);  } catch {    return createBunSqliteAdapter(dbPath);  }} export class RendererStateStore {  private readonly stateDir: string;  private readonly dbPath: string;  private adapterPromise: Promise<SqliteAdapter> | null = null;   constructor(stateDir: string = defaultStateDir()) {    this.stateDir = stateDir;    this.dbPath = path.join(this.stateDir, "renderer-state.sqlite");    fs.mkdirSync(this.stateDir, { recursive: true });  }   async get(): Promise<RendererStateSnapshot | null> {    const adapter = await this.getAdapter();    const row = adapter.getState();    if (!row) return null;    const parsed = rendererStateSnapshotSchema.safeParse(row);    if (!parsed.success) return null;    return parsed.data;  }   async set(raw: RendererStateSetInput): Promise<void> {    const input = rendererStateSetInputSchema.parse(raw);    const adapter = await this.getAdapter();    adapter.setState(input.stateJson, new Date().toISOString());  }   async close(): Promise<void> {    if (!this.adapterPromise) return;    const adapter = await this.adapterPromise;    adapter.close();    this.adapterPromise = null;  }   private async getAdapter(): Promise<SqliteAdapter> {    if (!this.adapterPromise) {      this.adapterPromise = createSqliteAdapter(this.dbPath);    }    return this.adapterPromise;  }}