Reference source · 0.1.0-dev.11
host-storage.ts
src/host-storage.ts · Download original file
import { DatabaseSync,backup,type SQLInputValue,type SQLOutputValue } from 'node:sqlite';
import { chmodSync,existsSync,mkdtempSync,createReadStream,linkSync,rmSync } from 'node:fs';
import { dirname,join } from 'node:path';
import { createHash } from 'node:crypto';
import { requireThat } from './core.ts';
export interface HostStatement {
get(...values:SQLInputValue[]):Record<string,SQLOutputValue>|undefined;
all(...values:SQLInputValue[]):Record<string,SQLOutputValue>[];
run(...values:SQLInputValue[]):{changes:number|bigint;lastInsertRowid:number|bigint};
}
// The reference engine requires synchronous transactions and SQLite SQL semantics.
// This is a replaceable implementation boundary, not a protocol storage requirement.
export interface HostDatabase {exec(sql:string):void;prepare(sql:string):HostStatement;close():void;readonly isOpen:boolean}
export interface HostStorage {readonly kind:string;readonly adoptLegacy?:boolean;open():HostDatabase}
export class SQLiteHostStorage implements HostStorage {
readonly kind='sqlite';readonly path:string;readonly adoptLegacy:boolean;private database?:DatabaseSync;
constructor(path:string,options:{adoptLegacy?:boolean}={}){this.path=path;this.adoptLegacy=options.adoptLegacy??false;}
open():DatabaseSync {requireThat(!this.database?.isOpen,'storage_already_open');this.database=new DatabaseSync(this.path);if(this.path!==':memory:')chmodSync(this.path,0o600);this.database.exec('PRAGMA busy_timeout=5000');return this.database;}
async snapshot(destination:string):Promise<{sha256:string;bytes:number}>{requireThat(this.database?.isOpen,'storage_closed');requireThat(!existsSync(destination),'backup_exists');const stage=mkdtempSync(join(dirname(destination),'.oprp-backup-'));try{const path=join(stage,'host.sqlite');await backup(this.database!,path);chmodSync(path,0o600);const check=new DatabaseSync(path,{readOnly:true});try{requireThat((check.prepare('PRAGMA quick_check').get() as any).quick_check==='ok','backup_integrity_failed');}finally{check.close();}let bytes=0;const hash=createHash('sha256');for await(const chunk of createReadStream(path)){hash.update(chunk);bytes+=chunk.length;}const report={sha256:hash.digest('hex'),bytes};try{linkSync(path,destination);}catch(error){if(error&&typeof error==='object'&&'code' in error&&error.code==='EEXIST')requireThat(false,'backup_exists');throw error;}return report;}finally{rmSync(stage,{recursive:true,force:true});}}
}