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 { 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 { 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 { const loadBunSqlite = new Function( "return import('bun:sqlite')", ) as () => Promise; 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 { try { return await createNodeSqliteAdapter(dbPath); } catch { return createBunSqliteAdapter(dbPath); } } export class RendererStateStore { private readonly stateDir: string; private readonly dbPath: string; private adapterPromise: Promise | 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 { 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 { const input = rendererStateSetInputSchema.parse(raw); const adapter = await this.getAdapter(); adapter.setState(input.stateJson, new Date().toISOString()); } async close(): Promise { if (!this.adapterPromise) return; const adapter = await this.adapterPromise; adapter.close(); this.adapterPromise = null; } private async getAdapter(): Promise { if (!this.adapterPromise) { this.adapterPromise = createSqliteAdapter(this.dbPath); } return this.adapterPromise; } }