/** * Opt-in SQLite persistence provider. Logical sessions remain unchanged; * the physical backend packs eligible chunk runs into schema-19 rows. * @module @deepseek-ai/dsh-session-persistence-sqlite */ import { Context, Service } from '@deepseek-ai/cordis' import z from '@deepseek-ai/schemastery' import type { Session, SessionEvent, SessionHeader, SessionId, SessionPreparation, } from '@deepseek-ai/dsh-session' import { DEFAULT_PREPARED_SESSION_CACHE_SIZE, DEFAULT_WRITE_BATCH_MAX_DELAY_MS, MAX_WRITE_BATCH_DELAY_MS, type BorrowedSessionSource, PersistenceCoordinator, SessionPersistence, type SessionInspection, type SessionLocation, type SessionPersistenceSnapshot, } from '@deepseek-ai/dsh-session-persistence' import type { JournalMode } from './schema.ts' import { SqliteStore } from './store.ts' export { SCHEMA_VERSION } from './schema.ts' /** Default wait for another SQLite connection's write reservation. */ export const DEFAULT_BUSY_TIMEOUT_MS = 5_000 /** Largest busy timeout accepted by SQLite's signed millisecond interface. */ export const MAX_BUSY_TIMEOUT_MS = 2_147_483_647 /** Plugin configuration. */ export interface Config { /** SQLite database path, or `:memory:` for an in-process database. */ path: string /** Durable SQLite journal mode; defaults to `wal`. */ journalMode?: JournalMode /** Maximum wait for another SQLite connection's lock; defaults to 5,000 ms. */ busyTimeoutMs?: number /** Maximum cold Session preparations retained for history-to-resume reuse. */ preparedSessionCacheSize?: number /** Fixed live-event coalescing window; not a backend completion deadline. */ writeBatchMaxDelayMs?: number } /** * SQLite `SessionPersistence` provider with a schema-owned physical codec. */ export class SqliteSessionPersistence extends SessionPersistence { override readonly supportsRawArtifacts = false override readonly name = 'session-persistence-sqlite' static inject = ['sessions'] static Config: z = z.object({ path: z.string().required(), journalMode: z.union(['wal', 'delete', 'truncate', 'persist'] as const).default('wal'), busyTimeoutMs: z.number().step(1).min(0).max(MAX_BUSY_TIMEOUT_MS).default(DEFAULT_BUSY_TIMEOUT_MS), preparedSessionCacheSize: z.number().step(1).min(1).default(DEFAULT_PREPARED_SESSION_CACHE_SIZE), writeBatchMaxDelayMs: z.number().step(1).min(1).max(MAX_WRITE_BATCH_DELAY_MS) .default(DEFAULT_WRITE_BATCH_MAX_DELAY_MS), }) private readonly store: SqliteStore private readonly coordinator: PersistenceCoordinator constructor(ctx: Context, public config: Config) { super(ctx) const preparedSessionCacheSize = config.preparedSessionCacheSize ?? DEFAULT_PREPARED_SESSION_CACHE_SIZE const writeBatchMaxDelayMs = config.writeBatchMaxDelayMs ?? DEFAULT_WRITE_BATCH_MAX_DELAY_MS this.store = new SqliteStore({ path: config.path, journalMode: config.journalMode ?? 'wal', busyTimeoutMs: config.busyTimeoutMs ?? DEFAULT_BUSY_TIMEOUT_MS, }) this.coordinator = new PersistenceCoordinator(this.ctx, this.store, { preparedSessionCacheSize, writeBatchMaxDelayMs, }) } /** Reject self-contained path and ownership failures without loading Node SQLite. */ protected async [Service.init](): Promise { await this.store.validatePath() } /** SQLite has one database, not an independent per-session artifact. */ locate(_meta: SessionHeader): SessionLocation | undefined { return undefined } create(meta: SessionHeader): Promise { return this.coordinator.create(meta) } override ensureMaterialized(session: Session): Promise { return this.coordinator.ensureMaterialized(session) } append(id: SessionId, events: readonly SessionEvent[]): Promise { return this.coordinator.append(id, events) } override prepare(id: SessionId, signal?: AbortSignal): Promise { return this.coordinator.prepare(id, signal) } load(id: SessionId): Promise { return this.coordinator.load(id) } inspect(id: SessionId, signal?: AbortSignal): Promise { return this.coordinator.inspect(id, signal) } override borrowSession(id: SessionId, signal?: AbortSignal): Promise { return this.coordinator.borrowSession(id, signal) } readFrom( id: SessionId, fromSeq: number, signal?: AbortSignal, ): Promise<{ meta: SessionHeader; events: SessionEvent[] }> { return this.coordinator.readFrom(id, fromSeq, signal) } list(signal?: AbortSignal): Promise { return this.store.list(signal) } listSnapshots(signal?: AbortSignal): Promise { return this.store.listSnapshots(signal) } } export default SqliteSessionPersistence