diff --git a/packages/fs/package.json b/packages/fs/package.json index 1713333..6a582b6 100644 --- a/packages/fs/package.json +++ b/packages/fs/package.json @@ -15,8 +15,9 @@ "src" ], "dependencies": { - "cborg": "^4.2.3", - "@ocas/core": "workspace:*" + "@ocas/core": "workspace:*", + "better-sqlite3": "^12.10.0", + "cborg": "^4.2.3" }, "repository": { "type": "git", @@ -28,6 +29,7 @@ "url": "https://github.com/shazhou-ww/ocas/issues" }, "devDependencies": { + "@types/better-sqlite3": "^7.6.13", "@types/node": "^25.9.1" } } diff --git a/packages/fs/src/index.ts b/packages/fs/src/index.ts index 349f8c8..196033d 100644 --- a/packages/fs/src/index.ts +++ b/packages/fs/src/index.ts @@ -1 +1,2 @@ export { createFsStore, openStore, prepareStore } from "./store.js"; +export { createSqliteVarStore } from "./sqlite-store.js"; diff --git a/packages/fs/src/sqlite-store.ts b/packages/fs/src/sqlite-store.ts new file mode 100644 index 0000000..51e01bc --- /dev/null +++ b/packages/fs/src/sqlite-store.ts @@ -0,0 +1,511 @@ +import { mkdirSync } from "node:fs"; +import { join } from "node:path"; +import Database from "better-sqlite3"; +import type { + CasStore, + Hash, + HistoryEntry, + ListEntry, + ListOptions, + Tag, + TagOp, + TagStore, + Variable, + VarListOptions, + VarSetOptions, + VarStore, +} from "@ocas/core"; +import { + checkTagLabelConflict, + extractSchema, + MAX_HISTORY, + SchemaMismatchError, + VariableNotFoundError, + validateName, +} from "@ocas/core"; + +const DB_FILE = "_store.db"; + +function openDb(dir: string): InstanceType { + mkdirSync(dir, { recursive: true }); + const db = new Database(join(dir, DB_FILE)); + db.pragma("journal_mode = WAL"); + db.pragma("foreign_keys = ON"); + return db; +} + +function initVarTables(db: InstanceType): void { + db.exec(` + CREATE TABLE IF NOT EXISTS vars ( + name TEXT NOT NULL, + schema TEXT NOT NULL, + value TEXT NOT NULL, + created INTEGER NOT NULL, + updated INTEGER NOT NULL, + tags TEXT NOT NULL DEFAULT '{}', + labels TEXT NOT NULL DEFAULT '[]', + PRIMARY KEY (name, schema) + ); + CREATE TABLE IF NOT EXISTS var_history ( + name TEXT NOT NULL, + schema TEXT NOT NULL, + value TEXT NOT NULL, + position INTEGER NOT NULL, + set_at INTEGER NOT NULL, + PRIMARY KEY (name, schema, position), + FOREIGN KEY (name, schema) REFERENCES vars(name, schema) ON DELETE CASCADE + ); + CREATE INDEX IF NOT EXISTS idx_vars_name ON vars(name); + CREATE INDEX IF NOT EXISTS idx_vars_created ON vars(created); + CREATE INDEX IF NOT EXISTS idx_vars_updated ON vars(updated); + `); +} + +function initTagTables(db: InstanceType): void { + db.exec(` + CREATE TABLE IF NOT EXISTS tags ( + target TEXT NOT NULL, + key TEXT NOT NULL, + value TEXT, + created INTEGER NOT NULL, + PRIMARY KEY (target, key) + ); + CREATE INDEX IF NOT EXISTS idx_tags_key ON tags(key); + `); +} + +function toVariable(row: Record): Variable { + return { + name: row["name"] as string, + schema: row["schema"] as Hash, + value: row["value"] as Hash, + created: row["created"] as number, + updated: row["updated"] as number, + tags: JSON.parse(row["tags"] as string) as Record, + labels: JSON.parse(row["labels"] as string) as string[], + }; +} + +export function createSqliteVarStore( + dir: string, + cas: CasStore, +): { var: VarStore; tag: TagStore; close: () => void } { + const db = openDb(dir); + initVarTables(db); + initTagTables(db); + + // ── Prepared statements (var) ── + const stmtGetVar = db.prepare( + "SELECT * FROM vars WHERE name = ? AND schema = ?", + ); + const stmtGetByName = db.prepare("SELECT * FROM vars WHERE name = ?"); + const stmtInsertVar = db.prepare(` + INSERT INTO vars (name, schema, value, created, updated, tags, labels) + VALUES (?, ?, ?, ?, ?, ?, ?) + `); + const stmtUpdateVar = db.prepare(` + UPDATE vars SET value = ?, updated = ?, tags = ?, labels = ? + WHERE name = ? AND schema = ? + `); + const stmtDeleteVar = db.prepare( + "DELETE FROM vars WHERE name = ? AND schema = ?", + ); + const stmtDeleteVarByName = db.prepare("DELETE FROM vars WHERE name = ?"); + const stmtInsertHistory = db.prepare(` + INSERT INTO var_history (name, schema, value, position, set_at) + VALUES (?, ?, ?, ?, ?) + `); + const stmtGetHistory = db.prepare( + "SELECT value, position, set_at FROM var_history WHERE name = ? AND schema = ? ORDER BY position DESC", + ); + const stmtMaxPosition = db.prepare( + "SELECT MAX(position) as max_pos FROM var_history WHERE name = ? AND schema = ?", + ); + const stmtTruncateHistory = db.prepare( + "DELETE FROM var_history WHERE name = ? AND schema = ? AND position < (SELECT MAX(position) - ? + 1 FROM var_history WHERE name = ? AND schema = ?)", + ); + + // ── Prepared statements (tag) ── + const stmtGetTag = db.prepare( + "SELECT * FROM tags WHERE target = ? AND key = ?", + ); + const stmtUpsertTag = db.prepare(` + INSERT INTO tags (target, key, value, created) VALUES (?, ?, ?, ?) + ON CONFLICT(target, key) DO UPDATE SET value = excluded.value + `); + const stmtDeleteTag = db.prepare( + "DELETE FROM tags WHERE target = ? AND key = ?", + ); + const stmtGetTagsByTarget = db.prepare( + "SELECT * FROM tags WHERE target = ? ORDER BY key", + ); + const stmtGetTagsByKey = db.prepare("SELECT * FROM tags WHERE key = ?"); + const stmtGetTagsByKeyValue = db.prepare( + "SELECT * FROM tags WHERE key = ? AND value = ?", + ); + + // ── VarStore implementation ── + const varStore: VarStore = { + set(name: string, hash: Hash, options?: VarSetOptions): Variable { + validateName(name); + const schema = extractSchema(cas, hash); + const existing = stmtGetVar.get(name, schema) as + | Record + | undefined; + const now = Date.now(); + + if (existing) { + const v = toVariable(existing); + const tags = options?.tags ?? v.tags; + const labels = options?.labels ?? v.labels; + if (options !== undefined) checkTagLabelConflict(tags, labels); + + // Check if value changed + if (v.value !== hash) { + const maxRow = stmtMaxPosition.get(name, schema) as { + max_pos: number | null; + }; + const nextPos = (maxRow.max_pos ?? -1) + 1; + stmtInsertHistory.run(name, schema, hash, nextPos, now); + stmtTruncateHistory.run(name, schema, MAX_HISTORY, name, schema); + stmtUpdateVar.run( + hash, + now, + JSON.stringify(options !== undefined ? tags : v.tags), + JSON.stringify(options !== undefined ? labels : v.labels), + name, + schema, + ); + } else if (options !== undefined) { + stmtUpdateVar.run( + v.value, + v.updated, + JSON.stringify(tags), + JSON.stringify(labels), + name, + schema, + ); + } + + return { + name, + schema, + value: hash, + created: v.created, + updated: v.value !== hash ? now : v.updated, + tags: options !== undefined ? { ...tags } : { ...v.tags }, + labels: options !== undefined ? [...labels] : [...v.labels], + }; + } + + // New variable + const tags = options?.tags ?? {}; + const labels = options?.labels ?? []; + checkTagLabelConflict(tags, labels); + stmtInsertVar.run( + name, + schema, + hash, + now, + now, + JSON.stringify(tags), + JSON.stringify(labels), + ); + stmtInsertHistory.run(name, schema, hash, 0, now); + return { + name, + schema, + value: hash, + created: now, + updated: now, + tags: { ...tags }, + labels: [...labels], + }; + }, + + get(name: string, schema?: Hash): Variable | null { + if (schema !== undefined) { + const row = stmtGetVar.get(name, schema) as + | Record + | undefined; + return row ? toVariable(row) : null; + } + const rows = stmtGetByName.all(name) as Record[]; + if (rows.length !== 1) return null; + return toVariable(rows[0]!); + }, + + remove(name: string, schema?: Hash): Variable[] { + if (schema !== undefined) { + const row = stmtGetVar.get(name, schema) as + | Record + | undefined; + if (!row) return []; + const v = toVariable(row); + stmtDeleteVar.run(name, schema); + return [v]; + } + const rows = stmtGetByName.all(name) as Record[]; + if (rows.length === 0) return []; + const removed = rows.map(toVariable); + stmtDeleteVarByName.run(name); + return removed; + }, + + update(name: string, hash: Hash, options?: VarSetOptions): Variable { + validateName(name); + const newSchema = extractSchema(cas, hash); + const rows = stmtGetByName.all(name) as Record[]; + if (rows.length === 0) throw new VariableNotFoundError(name, newSchema); + + const existing = stmtGetVar.get(name, newSchema) as + | Record + | undefined; + if (!existing) { + // Schema mismatch + const first = toVariable(rows[0]!); + throw new SchemaMismatchError(first.schema, newSchema); + } + + const v = toVariable(existing); + const now = Date.now(); + const tags = options?.tags ?? v.tags; + const labels = options?.labels ?? v.labels; + if (options !== undefined) checkTagLabelConflict(tags, labels); + + if (v.value !== hash) { + const maxRow = stmtMaxPosition.get(name, newSchema) as { + max_pos: number | null; + }; + const nextPos = (maxRow.max_pos ?? -1) + 1; + stmtInsertHistory.run(name, newSchema, hash, nextPos, now); + stmtTruncateHistory.run(name, newSchema, MAX_HISTORY, name, newSchema); + stmtUpdateVar.run( + hash, + now, + JSON.stringify(options !== undefined ? tags : v.tags), + JSON.stringify(options !== undefined ? labels : v.labels), + name, + newSchema, + ); + } else if (options !== undefined) { + stmtUpdateVar.run( + v.value, + v.updated, + JSON.stringify(tags), + JSON.stringify(labels), + name, + newSchema, + ); + } + + return { + name, + schema: newSchema, + value: hash, + created: v.created, + updated: v.value !== hash ? now : v.updated, + tags: options !== undefined ? { ...tags } : { ...v.tags }, + labels: options !== undefined ? [...labels] : [...v.labels], + }; + }, + + list(options?: VarListOptions): Variable[] { + if ( + options?.namePrefix !== undefined && + options?.exactName !== undefined + ) { + throw new Error( + "namePrefix and exactName are mutually exclusive - cannot specify both", + ); + } + + const limit = options?.limit; + if (limit !== undefined && limit <= 0) return []; + + // Build dynamic query + const conditions: string[] = []; + const params: unknown[] = []; + + if (options?.exactName !== undefined) { + conditions.push("name = ?"); + params.push(options.exactName); + } + if (options?.namePrefix !== undefined) { + conditions.push("name LIKE ?"); + // Escape % and _ in the prefix for LIKE + const escaped = options.namePrefix + .replace(/%/g, "\\%") + .replace(/_/g, "\\_"); + params.push(`${escaped}%`); + } + if (options?.schema !== undefined) { + conditions.push("schema = ?"); + params.push(options.schema); + } + + const sortCol = + options?.sort === "updated" ? "updated" : "created"; + const sortDir = options?.desc ? "DESC" : "ASC"; + const where = + conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : ""; + + let sql = `SELECT * FROM vars ${where} ORDER BY ${sortCol} ${sortDir}, name ASC`; + if (limit !== undefined || (options?.offset ?? 0) > 0) { + sql += ` LIMIT ${limit ?? -1} OFFSET ${options?.offset ?? 0}`; + } + + const rows = db.prepare(sql).all(...params) as Record[]; + + // Post-filter by tags and labels (stored as JSON) + const filterTags = options?.tags ?? {}; + const filterLabels = options?.labels ?? []; + const needsPostFilter = + Object.keys(filterTags).length > 0 || filterLabels.length > 0; + + if (!needsPostFilter) return rows.map(toVariable); + + const results: Variable[] = []; + for (const row of rows) { + const v = toVariable(row); + let ok = true; + for (const [tk, tv] of Object.entries(filterTags)) { + if (v.tags[tk] !== tv) { + ok = false; + break; + } + } + if (!ok) continue; + for (const lb of filterLabels) { + if (!v.labels.includes(lb)) { + ok = false; + break; + } + } + if (ok) results.push(v); + } + return results; + }, + + history(name: string, schema?: Hash): HistoryEntry[] { + if (schema !== undefined) { + const rows = stmtGetHistory.all(name, schema) as Record< + string, + unknown + >[]; + return rows.map((r) => ({ + value: r["value"] as Hash, + position: r["position"] as number, + setAt: r["set_at"] as number, + })); + } + // No schema: if exactly one variant, return its history + const vars = stmtGetByName.all(name) as Record[]; + if (vars.length !== 1) return []; + const v = vars[0]!; + const rows = stmtGetHistory.all( + v["name"] as string, + v["schema"] as string, + ) as Record[]; + return rows.map((r) => ({ + value: r["value"] as Hash, + position: r["position"] as number, + setAt: r["set_at"] as number, + })); + }, + + close(): void { + db.close(); + }, + }; + + // ── TagStore implementation ── + const tagStore: TagStore = { + tag(target: Hash, operations: TagOp[]): Tag[] { + const now = Date.now(); + for (const op of operations) { + if (op.op === "set") { + const existing = stmtGetTag.get(target, op.key) as + | Record + | undefined; + const created = (existing?.["created"] as number) ?? now; + stmtUpsertTag.run(target, op.key, op.value ?? null, created); + } else { + stmtDeleteTag.run(target, op.key); + } + } + const rows = stmtGetTagsByTarget.all(target) as Record< + string, + unknown + >[]; + return rows.map((r) => ({ + key: r["key"] as string, + value: r["value"] as string | null, + target, + created: r["created"] as number, + })); + }, + + untag(target: Hash, keys: string[]): void { + for (const k of keys) { + stmtDeleteTag.run(target, k); + } + }, + + tags(target: Hash): Tag[] { + const rows = stmtGetTagsByTarget.all(target) as Record< + string, + unknown + >[]; + return rows.map((r) => ({ + key: r["key"] as string, + value: r["value"] as string | null, + target, + created: r["created"] as number, + })); + }, + + listByTag(tag: string, options?: ListOptions): Hash[] { + let key = tag; + let value: string | null | undefined; + const eqIdx = tag.indexOf("="); + if (eqIdx >= 0) { + key = tag.slice(0, eqIdx); + value = tag.slice(eqIdx + 1); + } + + const rows = ( + value !== undefined + ? stmtGetTagsByKeyValue.all(key, value) + : stmtGetTagsByKey.all(key) + ) as Record[]; + + let entries: ListEntry[] = rows.map((r) => ({ + hash: r["target"] as Hash, + created: r["created"] as number, + updated: r["created"] as number, + })); + + // Apply sort/limit/offset from ListOptions + const sort = options?.sort ?? "created"; + const desc = options?.desc ?? false; + entries.sort((a, b) => { + const av = sort === "updated" ? a.updated : a.created; + const bv = sort === "updated" ? b.updated : b.created; + return desc ? bv - av : av - bv; + }); + const offset = options?.offset ?? 0; + if (offset > 0) entries = entries.slice(offset); + const limit = options?.limit; + if (limit !== undefined) entries = entries.slice(0, limit); + + return entries.map((e) => e.hash); + }, + }; + + return { + var: varStore, + tag: tagStore, + close: () => db.close(), + }; +} diff --git a/packages/fs/src/store.ts b/packages/fs/src/store.ts index 9d4b701..66f5ba6 100644 --- a/packages/fs/src/store.ts +++ b/packages/fs/src/store.ts @@ -27,7 +27,7 @@ import { type Store, } from "@ocas/core"; import { decode } from "cborg"; -import { createFsTagStore, createFsVarStoreFor } from "./var-store.js"; +import { createSqliteVarStore } from "./sqlite-store.js"; const INDEX_DIR = "_index"; const META_FILE = "_meta"; @@ -393,10 +393,11 @@ export async function prepareStore(dir: string): Promise { */ export async function openStore(dir: string): Promise { const cas = await prepareStore(dir); + const sqlite = createSqliteVarStore(dir, cas); const ocas: Store = { cas, - var: createFsVarStoreFor(dir, cas), - tag: createFsTagStore(dir), + var: sqlite.var, + tag: sqlite.tag, }; bootstrap(ocas); return ocas; diff --git a/packages/fs/src/tag-store.test.ts b/packages/fs/src/tag-store.test.ts index e38695a..a966d11 100644 --- a/packages/fs/src/tag-store.test.ts +++ b/packages/fs/src/tag-store.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, test } from "vitest"; -import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { existsSync, mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { openStore } from "./store.js"; @@ -16,7 +16,7 @@ describe("FsTagStore", () => { rmSync(dir, { recursive: true, force: true }); }); - test("B1. set tag with key/value round-trip + JSONL persisted", async () => { + test("B1. set tag with key/value round-trip + SQLite persisted", async () => { const store = await openStore(dir); const result = store.tag.tag(T1, [ { op: "set", key: "env", value: "prod" }, @@ -26,17 +26,8 @@ describe("FsTagStore", () => { expect(result[0]?.value).toBe("prod"); expect(store.tag.tags(T1)).toEqual(result); - const jsonl = join(dir, "_tags.jsonl"); - expect(existsSync(jsonl)).toBe(true); - const content = readFileSync(jsonl, "utf8"); - const lines = content.split("\n").filter((l) => l.length > 0); - expect(lines).toHaveLength(1); - const parsed = JSON.parse(lines[0] as string) as { - key: string; - value: string; - }; - expect(parsed.key).toBe("env"); - expect(parsed.value).toBe("prod"); + const dbFile = join(dir, "_store.db"); + expect(existsSync(dbFile)).toBe(true); }); test("B2. label tag (no value) records value: null", async () => { @@ -120,7 +111,7 @@ describe("FsTagStore", () => { expect(tags.map((t) => t.value)).toEqual(["prod", "platform"]); }); - test("B11. JSONL replay fidelity (set/delete/untag mix)", async () => { + test("B11. SQLite replay fidelity (set/delete/untag mix)", async () => { const store = await openStore(dir); store.tag.tag(T1, [{ op: "set", key: "a", value: "1" }]); store.tag.tag(T1, [{ op: "set", key: "b", value: "2" }]); diff --git a/packages/fs/src/var-store.test.ts b/packages/fs/src/var-store.test.ts index 8821e28..e259886 100644 --- a/packages/fs/src/var-store.test.ts +++ b/packages/fs/src/var-store.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, test } from "vitest"; -import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { existsSync, mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import type { Hash, Store } from "@ocas/core"; @@ -40,7 +40,7 @@ describe("FsVarStore", () => { rmSync(dir, { recursive: true, force: true }); }); - test("A1. set + get round-trip persists to JSONL", async () => { + test("A1. set + get round-trip persists to SQLite", async () => { const { store, schema, put } = await setupStore(dir); const h = put("hello"); const v = store.var.set("@app/x", h); @@ -51,17 +51,8 @@ describe("FsVarStore", () => { const got = store.var.get("@app/x", schema); expect(got?.value).toBe(h); - const jsonl = join(dir, "_vars.jsonl"); - expect(existsSync(jsonl)).toBe(true); - const content = readFileSync(jsonl, "utf8"); - expect(content.length).toBeGreaterThan(0); - const lines = content.split("\n").filter((l) => l.length > 0); - expect(lines.length).toBeGreaterThanOrEqual(1); - const matching = lines - .map((l) => JSON.parse(l) as { name?: string; value?: Hash }) - .find((r) => r.name === "@app/x"); - expect(matching).toBeDefined(); - expect(matching?.value).toBe(h); + const dbFile = join(dir, "_store.db"); + expect(existsSync(dbFile)).toBe(true); }); test("A2. name validation", async () => { diff --git a/packages/fs/src/var-store.ts b/packages/fs/src/var-store.ts deleted file mode 100644 index 770d046..0000000 --- a/packages/fs/src/var-store.ts +++ /dev/null @@ -1,418 +0,0 @@ -import { - appendFileSync, - mkdirSync, - readFileSync, - writeFileSync, -} from "node:fs"; -import { join } from "node:path"; -import type { - CasStore, - Hash, - ListEntry, - Tag, - TagStore, - Variable, - VarListOptions, - VarStore, -} from "@ocas/core"; -import { - addNameIndex, - applyListOptions, - casListEntry, - checkTagLabelConflict, - cloneVarRecord, - extractSchema, - pushHistory, - removeNameIndex, - SchemaMismatchError, - VariableNotFoundError, - type VarRecord, - validateName, - varKey, -} from "@ocas/core"; - -const VARS_FILE = "_vars.jsonl"; -const TAGS_FILE = "_tags.jsonl"; - -export function createFsVarStoreFor(dir: string, cas: CasStore): VarStore { - const records = new Map(); - const byName = new Map>(); - const path = join(dir, VARS_FILE); - - // Load existing records (last record per key wins) - try { - const content = readFileSync(path, "utf8"); - for (const line of content.split("\n")) { - if (line.length === 0) continue; - try { - const rec = JSON.parse(line) as VarRecord & { __op?: string }; - if (rec.__op === "remove") { - const k = varKey(rec.name, rec.schema); - records.delete(k); - removeNameIndex(byName, rec.name, k); - } else { - const k = varKey(rec.name, rec.schema); - records.set(k, rec); - addNameIndex(byName, rec.name, k); - } - } catch { - // skip malformed - } - } - } catch { - // file may not exist - } - - function persistFull(): void { - mkdirSync(dir, { recursive: true }); - const lines: string[] = []; - for (const rec of records.values()) { - lines.push(JSON.stringify(rec)); - } - writeFileSync(path, lines.length ? `${lines.join("\n")}\n` : "", "utf8"); - } - - function appendRecord(rec: VarRecord): void { - mkdirSync(dir, { recursive: true }); - appendFileSync(path, `${JSON.stringify(rec)}\n`, "utf8"); - } - - function appendRemoval(name: string, schema: Hash): void { - mkdirSync(dir, { recursive: true }); - appendFileSync( - path, - `${JSON.stringify({ __op: "remove", name, schema })}\n`, - "utf8", - ); - } - - return { - set(name, hash, options) { - validateName(name); - const schema = extractSchema(cas, hash); - const k = varKey(name, schema); - const existing = records.get(k); - const now = Date.now(); - if (existing) { - const tags = options?.tags ?? existing.tags; - const labels = options?.labels ?? existing.labels; - if (options !== undefined) checkTagLabelConflict(tags, labels); - const changed = pushHistory(existing, hash, now); - if (changed) { - existing.value = hash; - existing.updated = now; - } - if (options !== undefined) { - existing.tags = { ...tags }; - existing.labels = [...labels]; - } - persistFull(); - return cloneVarRecord(existing); - } - const tags = options?.tags ?? {}; - const labels = options?.labels ?? []; - checkTagLabelConflict(tags, labels); - const rec: VarRecord = { - name, - schema, - value: hash, - created: now, - updated: now, - tags: { ...tags }, - labels: [...labels], - history: [{ value: hash, position: 0, setAt: now }], - }; - records.set(k, rec); - addNameIndex(byName, name, k); - appendRecord(rec); - return cloneVarRecord(rec); - }, - - get(name, schema) { - if (schema !== undefined) { - const rec = records.get(varKey(name, schema)); - return rec ? cloneVarRecord(rec) : null; - } - const set = byName.get(name); - if (!set || set.size !== 1) return null; - const onlyKey = set.values().next().value; - if (onlyKey === undefined) return null; - const rec = records.get(onlyKey); - return rec ? cloneVarRecord(rec) : null; - }, - - remove(name, schema) { - if (schema !== undefined) { - const k = varKey(name, schema); - const rec = records.get(k); - if (!rec) return []; - records.delete(k); - removeNameIndex(byName, name, k); - appendRemoval(name, schema); - return [cloneVarRecord(rec)]; - } - const set = byName.get(name); - if (!set) return []; - const removed: Variable[] = []; - for (const k of [...set]) { - const rec = records.get(k); - if (rec) { - removed.push(cloneVarRecord(rec)); - records.delete(k); - appendRemoval(rec.name, rec.schema); - } - } - byName.delete(name); - return removed; - }, - - update(name, hash, options) { - validateName(name); - const newSchema = extractSchema(cas, hash); - const set = byName.get(name); - if (!set || set.size === 0) - throw new VariableNotFoundError(name, newSchema); - const k = varKey(name, newSchema); - const existing = records.get(k); - if (!existing) { - for (const ek of set) { - const erec = records.get(ek); - if (erec) throw new SchemaMismatchError(erec.schema, newSchema); - } - throw new VariableNotFoundError(name, newSchema); - } - const now = Date.now(); - const tags = options?.tags ?? existing.tags; - const labels = options?.labels ?? existing.labels; - if (options !== undefined) checkTagLabelConflict(tags, labels); - const changed = pushHistory(existing, hash, now); - if (changed) { - existing.value = hash; - existing.updated = now; - } - if (options !== undefined) { - existing.tags = { ...tags }; - existing.labels = [...labels]; - } - persistFull(); - return cloneVarRecord(existing); - }, - - list(options?: VarListOptions) { - if ( - options?.namePrefix !== undefined && - options?.exactName !== undefined - ) { - throw new Error( - "namePrefix and exactName are mutually exclusive - cannot specify both", - ); - } - const namePrefix = options?.namePrefix; - const exactName = options?.exactName; - const schema = options?.schema; - const filterTags = options?.tags ?? {}; - const filterLabels = options?.labels ?? []; - const sort = options?.sort ?? "created"; - const desc = options?.desc ?? false; - const limit = options?.limit; - const offset = options?.offset ?? 0; - if (limit !== undefined && limit <= 0) return []; - - let results: VarRecord[] = []; - for (const rec of records.values()) { - if (exactName !== undefined && rec.name !== exactName) continue; - if (namePrefix !== undefined && !rec.name.startsWith(namePrefix)) - continue; - if (schema !== undefined && rec.schema !== schema) continue; - let ok = true; - for (const [tk, tv] of Object.entries(filterTags)) { - if (rec.tags[tk] !== tv) { - ok = false; - break; - } - } - if (!ok) continue; - for (const lb of filterLabels) { - if (!rec.labels.includes(lb)) { - ok = false; - break; - } - } - if (!ok) continue; - results.push(rec); - } - results.sort((a, b) => { - const av = sort === "updated" ? a.updated : a.created; - const bv = sort === "updated" ? b.updated : b.created; - if (av !== bv) return desc ? bv - av : av - bv; - return a.name < b.name ? -1 : a.name > b.name ? 1 : 0; - }); - if (offset > 0) results = results.slice(offset); - if (limit !== undefined) results = results.slice(0, limit); - return results.map(cloneVarRecord); - }, - - history(name, schema) { - if (schema !== undefined) { - const rec = records.get(varKey(name, schema)); - return rec ? rec.history.map((e) => ({ ...e })) : []; - } - const set = byName.get(name); - if (!set || set.size !== 1) return []; - const onlyKey = set.values().next().value; - if (onlyKey === undefined) return []; - const rec = records.get(onlyKey); - return rec ? rec.history.map((e) => ({ ...e })) : []; - }, - - close() { - // no-op (synchronous file ops) - }, - }; -} - -type StoredTag = { - key: string; - value: string | null; - target: Hash; - created: number; -}; - -export function createFsTagStore(dir: string): TagStore { - const byTarget = new Map>(); - const byKey = new Map>(); - const path = join(dir, TAGS_FILE); - - function addKeyIndex(k: string, target: Hash): void { - let set = byKey.get(k); - if (!set) { - set = new Set(); - byKey.set(k, set); - } - set.add(target); - } - function removeKeyIndex(k: string, target: Hash): void { - const set = byKey.get(k); - if (!set) return; - const tmap = byTarget.get(target); - if (tmap?.has(k)) return; - set.delete(target); - if (set.size === 0) byKey.delete(k); - } - - // Load - try { - const content = readFileSync(path, "utf8"); - for (const line of content.split("\n")) { - if (line.length === 0) continue; - try { - const ent = JSON.parse(line) as - | (StoredTag & { __op?: "set" | "untag" }) - | { __op: "untag"; target: Hash; key: string }; - if ((ent as { __op?: string }).__op === "untag") { - const e = ent as { target: Hash; key: string }; - const tm = byTarget.get(e.target); - if (tm) { - tm.delete(e.key); - removeKeyIndex(e.key, e.target); - if (tm.size === 0) byTarget.delete(e.target); - } - } else { - const t = ent as StoredTag; - let tm = byTarget.get(t.target); - if (!tm) { - tm = new Map(); - byTarget.set(t.target, tm); - } - tm.set(t.key, { - key: t.key, - value: t.value, - target: t.target, - created: t.created, - }); - addKeyIndex(t.key, t.target); - } - } catch { - // skip - } - } - } catch { - // none - } - - function append(line: object): void { - mkdirSync(dir, { recursive: true }); - appendFileSync(path, `${JSON.stringify(line)}\n`, "utf8"); - } - - return { - tag(target, ops) { - let tm = byTarget.get(target); - if (!tm) { - tm = new Map(); - byTarget.set(target, tm); - } - const now = Date.now(); - for (const op of ops) { - if (op.op === "set") { - const existing = tm.get(op.key); - const tag: Tag = { - key: op.key, - value: op.value ?? null, - target, - created: existing?.created ?? now, - }; - tm.set(op.key, tag); - addKeyIndex(op.key, target); - append(tag); - } else { - tm.delete(op.key); - removeKeyIndex(op.key, target); - append({ __op: "untag", target, key: op.key }); - } - } - return [...tm.values()].sort((a, b) => - a.key < b.key ? -1 : a.key > b.key ? 1 : 0, - ); - }, - untag(target, keys) { - const tm = byTarget.get(target); - if (!tm) return; - for (const k of keys) { - tm.delete(k); - removeKeyIndex(k, target); - append({ __op: "untag", target, key: k }); - } - if (tm.size === 0) byTarget.delete(target); - }, - tags(target) { - const tm = byTarget.get(target); - if (!tm) return []; - return [...tm.values()].sort((a, b) => - a.key < b.key ? -1 : a.key > b.key ? 1 : 0, - ); - }, - listByTag(tag, options) { - let key = tag; - let value: string | null | undefined; - const eqIdx = tag.indexOf("="); - if (eqIdx >= 0) { - key = tag.slice(0, eqIdx); - value = tag.slice(eqIdx + 1); - } - const targets = byKey.get(key); - if (!targets) return []; - let entries: ListEntry[] = []; - for (const t of targets) { - const tm = byTarget.get(t); - if (!tm) continue; - const tagEntry = tm.get(key); - if (!tagEntry) continue; - if (value !== undefined && tagEntry.value !== value) continue; - entries.push(casListEntry(t, tagEntry.created)); - } - entries = applyListOptions(entries, options); - return entries.map((e) => e.hash); - }, - }; -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7d50b81..8b6446c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -64,10 +64,16 @@ importers: '@ocas/core': specifier: workspace:* version: link:../core + better-sqlite3: + specifier: ^12.10.0 + version: 12.10.0 cborg: specifier: ^4.2.3 version: 4.5.8 devDependencies: + '@types/better-sqlite3': + specifier: ^7.6.13 + version: 7.6.13 '@types/node': specifier: ^25.9.1 version: 25.9.1 @@ -1009,6 +1015,9 @@ packages: '@tybys/wasm-util@0.10.2': resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} + '@types/better-sqlite3@7.6.13': + resolution: {integrity: sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==} + '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -1069,9 +1078,25 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + better-sqlite3@12.10.0: + resolution: {integrity: sha512-CyzaZRQKyHkB2ZInfTTl2nvT33EbDpjkLEbE8/Zck3Ll6O0qqvuGdrJ45HgtH+HykRg88ITY3AdreBGN70aBSQ==} + engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x || 26.x} + + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + blake3-wasm@2.1.5: resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + cborg@4.5.8: resolution: {integrity: sha512-6/viltD51JklRhq4L7jC3zgy6gryuG5xfZ3kzpE+PravtyeQLeQmCYLREhQH7pWENg5pY4Yu/XCd6a7dKScVlw==} hasBin: true @@ -1080,6 +1105,9 @@ packages: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1108,6 +1136,14 @@ packages: data-uri-to-buffer@2.0.2: resolution: {integrity: sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==} + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + defu@6.1.7: resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} @@ -1115,6 +1151,9 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + es-module-lexer@2.1.0: resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} @@ -1147,6 +1186,10 @@ packages: resolution: {integrity: sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==} engines: {node: '>=6'} + expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + expect-type@1.3.0: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} @@ -1169,6 +1212,12 @@ packages: picomatch: optional: true + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1177,9 +1226,21 @@ packages: get-source@2.0.12: resolution: {integrity: sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==} + github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + glob-to-regexp@0.4.1: resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + is-arrayish@0.3.4: resolution: {integrity: sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==} @@ -1279,11 +1340,21 @@ packages: engines: {node: '>=10.0.0'} hasBin: true + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + miniflare@3.20250718.3: resolution: {integrity: sha512-JuPrDJhwLrNLEJiNLWO7ZzJrv/Vv9kZuwMYCfv0LskQDM6Eonw4OvywO3CH/wCGjgHzha/qyjUh8JQ068TjDgQ==} engines: {node: '>=16.13'} hasBin: true + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + mustache@4.2.0: resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} hasBin: true @@ -1293,12 +1364,22 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + napi-build-utils@2.0.0: + resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} + + node-abi@3.92.0: + resolution: {integrity: sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==} + engines: {node: '>=10'} + obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} ohash@2.0.11: resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} @@ -1316,9 +1397,26 @@ packages: resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} engines: {node: ^10 || ^12 || >=14} + prebuild-install@7.1.3: + resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} + engines: {node: '>=10'} + deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. + hasBin: true + printable-characters@1.0.42: resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==} + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -1343,6 +1441,9 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + semver@7.8.1: resolution: {integrity: sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==} engines: {node: '>=10'} @@ -1355,6 +1456,12 @@ packages: siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + + simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + simple-swizzle@0.2.4: resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==} @@ -1383,6 +1490,20 @@ packages: resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==} engines: {node: '>=4', npm: '>=6'} + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + + tar-fs@2.1.4: + resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -1406,6 +1527,9 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -1428,6 +1552,9 @@ packages: unenv@2.0.0-rc.14: resolution: {integrity: sha512-od496pShMen7nOy5VmVJCnq8rptd45vh6Nx/r2iPbrba6pa6p+tS2ywuIHRZ/OBvSbQZB0kWvpO9XBNVFXHD3Q==} + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + vite@5.4.21: resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} engines: {node: ^18.0.0 || >=20.0.0} @@ -1563,6 +1690,9 @@ packages: '@cloudflare/workers-types': optional: true + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.18.0: resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} engines: {node: '>=10.0.0'} @@ -2142,6 +2272,10 @@ snapshots: tslib: 2.8.1 optional: true + '@types/better-sqlite3@7.6.13': + dependencies: + '@types/node': 25.9.1 + '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -2213,12 +2347,36 @@ snapshots: assertion-error@2.0.1: {} + base64-js@1.5.1: {} + + better-sqlite3@12.10.0: + dependencies: + bindings: 1.5.0 + prebuild-install: 7.1.3 + + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + blake3-wasm@2.1.5: {} + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + cborg@4.5.8: {} chai@6.2.2: {} + chownr@1.1.4: {} + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -2247,10 +2405,20 @@ snapshots: data-uri-to-buffer@2.0.2: {} + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + + deep-extend@0.6.0: {} + defu@6.1.7: {} detect-libc@2.1.2: {} + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + es-module-lexer@2.1.0: {} esbuild@0.17.19: @@ -2343,6 +2511,8 @@ snapshots: exit-hook@2.2.1: {} + expand-template@2.0.3: {} + expect-type@1.3.0: {} exsolve@1.0.8: {} @@ -2355,6 +2525,10 @@ snapshots: optionalDependencies: picomatch: 4.0.4 + file-uri-to-path@1.0.0: {} + + fs-constants@1.0.0: {} + fsevents@2.3.3: optional: true @@ -2363,8 +2537,16 @@ snapshots: data-uri-to-buffer: 2.0.2 source-map: 0.6.1 + github-from-package@0.0.0: {} + glob-to-regexp@0.4.1: {} + ieee754@1.2.1: {} + + inherits@2.0.4: {} + + ini@1.3.8: {} + is-arrayish@0.3.4: optional: true @@ -2435,6 +2617,8 @@ snapshots: mime@3.0.0: {} + mimic-response@3.1.0: {} + miniflare@3.20250718.3: dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -2452,14 +2636,28 @@ snapshots: - bufferutil - utf-8-validate + minimist@1.2.8: {} + + mkdirp-classic@0.5.3: {} + mustache@4.2.0: {} nanoid@3.3.12: {} + napi-build-utils@2.0.0: {} + + node-abi@3.92.0: + dependencies: + semver: 7.8.1 + obug@2.1.1: {} ohash@2.0.11: {} + once@1.4.0: + dependencies: + wrappy: 1.0.2 + path-to-regexp@6.3.0: {} pathe@2.0.3: {} @@ -2474,8 +2672,41 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + prebuild-install@7.1.3: + dependencies: + detect-libc: 2.1.2 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 2.0.0 + node-abi: 3.92.0 + pump: 3.0.4 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.4 + tunnel-agent: 0.6.0 + printable-characters@1.0.42: {} + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + require-from-string@2.0.2: {} rolldown@1.0.3: @@ -2544,8 +2775,9 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.61.0 fsevents: 2.3.3 - semver@7.8.1: - optional: true + safe-buffer@5.2.1: {} + + semver@7.8.1: {} sharp@0.33.5: dependencies: @@ -2576,6 +2808,14 @@ snapshots: siginfo@2.0.0: {} + simple-concat@1.0.1: {} + + simple-get@4.0.1: + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + simple-swizzle@0.2.4: dependencies: is-arrayish: 0.3.4 @@ -2598,6 +2838,27 @@ snapshots: stoppable@1.1.0: {} + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-json-comments@2.0.1: {} + + tar-fs@2.1.4: + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.4 + tar-stream: 2.2.0 + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + tinybench@2.9.0: {} tinyexec@1.2.4: {} @@ -2618,6 +2879,10 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + typescript@5.9.3: {} ufo@1.6.4: {} @@ -2640,6 +2905,8 @@ snapshots: pathe: 2.0.3 ufo: 1.6.4 + util-deprecate@1.0.2: {} + vite@5.4.21(@types/node@25.9.1)(lightningcss@1.32.0): dependencies: esbuild: 0.21.5 @@ -2723,6 +2990,8 @@ snapshots: - bufferutil - utf-8-validate + wrappy@1.0.2: {} + ws@8.18.0: {} xxhash-wasm@1.1.0: {}