| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340 |
- /**
- * Fixed physical-record compression for SQLite. Schema-owned functions
- * encode logical events and decode tagged rows before persistence consumers
- * observe them.
- * @module @deepseek-ai/dsh-session-persistence-sqlite/compression
- */
- import { readFileSync } from 'node:fs'
- import { TextDecoder } from 'node:util'
- import { constants, zstdCompressSync, zstdDecompressSync } from 'node:zlib'
- import type { SessionEvent, SurfaceEventType } from '@deepseek-ai/dsh-session'
- import {
- decodeSerializedChunkRow,
- type ChunkRow,
- MAX_PACKED_DATA_BYTES,
- type StorageRecord,
- } from './codec.ts'
- import type { EventRow } from './schema.ts'
- /** One physical row ready for SQLite parameter binding. */
- export interface BoundRecord {
- readonly seq: number
- readonly type: string
- readonly time: number
- readonly data: string | Uint8Array
- readonly sourceEventSeqs: Uint8Array | null
- readonly surfaceOp: string | null
- readonly isPacked: 0 | 1
- }
- const UTF8_DECODER = new TextDecoder('utf-8', { fatal: true })
- const ZSTD_COMPRESSION_LEVEL = 3
- const DELTA_TAG = 0
- const RUN_TAG = 1
- const MAX_SAFE_INTEGER = BigInt(Number.MAX_SAFE_INTEGER)
- const MAX_ZIGZAG_INTEGER = MAX_SAFE_INTEGER * 2n
- /**
- * Schema-19 raw-content zstd dictionary for independently decodable data rows.
- * Its exact bytes are part of the physical format; changing the resource
- * requires a schema-version bump.
- */
- const ZSTD_DICTIONARY = readFileSync(new URL('../resources/zstd-dictionary.bin', import.meta.url))
- /** Compress options shared by every data-column frame. */
- const DATA_ZSTD_OPTIONS = {
- dictionary: ZSTD_DICTIONARY,
- params: { [constants.ZSTD_c_compressionLevel]: ZSTD_COMPRESSION_LEVEL },
- } as const
- const CHUNK_TAGS = ['text-chunks', 'reasoning-chunks', 'tool-call-chunks'] as const
- type ChunkTag = typeof CHUNK_TAGS[number]
- function isChunkTag(value: string): value is ChunkTag {
- return (CHUNK_TAGS as readonly string[]).includes(value)
- }
- /**
- * Decode one physical SQLite row into its complete logical event span.
- * @param row - detached SQLite event row.
- * @returns every logical event represented by the row.
- */
- export function decodeRow(row: EventRow): SessionEvent[] {
- if (row.is_packed === 0) return [decodeScalarRow(row)]
- if (!isChunkTag(row.type)) {
- throw new Error(`malformed ${row.type} storage row: packed discriminator requires a chunk tag`)
- }
- if (row.source_event_seqs !== null || row.surface_op !== null) {
- throw new Error(`malformed ${row.type} storage row: packed surface fields must be null`)
- }
- return decodeSerializedChunkRow(
- row.type,
- row.seq,
- row.time,
- decodeData(row.data, MAX_PACKED_DATA_BYTES),
- )
- }
- /**
- * Convert a storage record to SQLite column values.
- * @param record - scalar event or packed chunk record.
- * @returns column values for one physical insert.
- */
- export function bindRecord(record: StorageRecord): BoundRecord {
- if (isChunkRow(record)) {
- return {
- seq: record.seq0,
- type: record.type,
- time: record.time0,
- data: encodeData(JSON.stringify(record.data)),
- sourceEventSeqs: null,
- surfaceOp: null,
- isPacked: 1,
- }
- }
- const event = record
- const surface = event as SessionEvent<SurfaceEventType>
- return {
- seq: event.seq,
- type: event.type,
- time: event.time,
- data: encodeData(JSON.stringify(event.data)),
- sourceEventSeqs: surface.sourceEventSeqs === undefined
- ? null
- : encodeSourceEventSeqs(surface.sourceEventSeqs),
- surfaceOp: surface.surfaceOp === undefined ? null : JSON.stringify(surface.surfaceOp),
- isPacked: 0,
- }
- }
- function encodeData(serialized: string): string | Uint8Array {
- const bytes = Buffer.from(serialized)
- const compressed = zstdCompressSync(bytes, DATA_ZSTD_OPTIONS)
- return compressed.length < bytes.length ? compressed : serialized
- }
- function decodeData(value: string | Uint8Array, maxOutputLength?: number): string {
- if (typeof value === 'string') return value
- const decoded = maxOutputLength === undefined
- ? zstdDecompressSync(value, { dictionary: ZSTD_DICTIONARY })
- : zstdDecompressSync(value, { dictionary: ZSTD_DICTIONARY, maxOutputLength })
- return UTF8_DECODER.decode(decoded)
- }
- function encodeSourceEventSeqs(values: readonly number[]): Uint8Array {
- if (values.length === 0) return new Uint8Array()
- const deltas = [DELTA_TAG]
- let previous = 0n
- for (let index = 0; index < values.length; index += 1) {
- const value = values[index] as number
- if (!Number.isSafeInteger(value) || value < 0) {
- throw new TypeError('sourceEventSeqs must contain non-negative safe integers')
- }
- const current = BigInt(value)
- const encoded = index === 0
- ? current
- : current >= previous
- ? (current - previous) * 2n
- : ((previous - current) * 2n) - 1n
- appendVarint(deltas, encoded)
- previous = current
- }
- if (!isStrictlyIncreasing(values)) return Uint8Array.from(deltas)
- const runs = [RUN_TAG]
- let start = values[0] as number
- let end = start
- for (let index = 1; index < values.length; index += 1) {
- const value = values[index] as number
- if (value === end + 1) {
- end = value
- continue
- }
- appendVarint(runs, BigInt(start))
- appendVarint(runs, BigInt(end - start + 1))
- start = value
- end = start
- }
- appendVarint(runs, BigInt(start))
- appendVarint(runs, BigInt(end - start + 1))
- return Uint8Array.from(runs.length < deltas.length ? runs : deltas)
- }
- function isStrictlyIncreasing(values: readonly number[]): boolean {
- return values.every((value, index) => index === 0 || value > (values[index - 1] as number))
- }
- function appendVarint(bytes: number[], value: bigint): void {
- let remaining = value
- while (remaining >= 0x80n) {
- bytes.push(Number(remaining & 0x7fn) | 0x80)
- remaining >>= 7n
- }
- bytes.push(Number(remaining))
- }
- function decodeSourceEventSeqs(bytes: Uint8Array, maxEntries: number): number[] {
- if (bytes.length === 0) return []
- if (bytes.length === 1) {
- throw new Error('malformed source_event_seqs storage value: truncated tagged payload')
- }
- switch (bytes[0]) {
- case DELTA_TAG: return decodeDeltaVarints(bytes, 1)
- case RUN_TAG: return decodeRunVarints(bytes, 1, maxEntries)
- default: throw new Error('malformed source_event_seqs storage value: unknown encoding tag')
- }
- }
- function decodeDeltaVarints(bytes: Uint8Array, offset: number): number[] {
- const values: number[] = []
- let previous = 0n
- while (offset < bytes.length) {
- const first = values.length === 0
- const decoded = readVarint(bytes, offset, first ? MAX_SAFE_INTEGER : MAX_ZIGZAG_INTEGER)
- offset = decoded.offset
- const delta = first
- ? decoded.value
- : (decoded.value & 1n) === 0n
- ? decoded.value / 2n
- : -((decoded.value + 1n) / 2n)
- const value = first ? delta : previous + delta
- if (value < 0n || value > MAX_SAFE_INTEGER) {
- throw new Error('malformed source_event_seqs storage value: decoded seq is out of range')
- }
- values.push(Number(value))
- previous = value
- }
- return values
- }
- function decodeRunVarints(bytes: Uint8Array, offset: number, maxEntries: number): number[] {
- const values: number[] = []
- let previousEnd = -1
- while (offset < bytes.length) {
- const start = readVarint(bytes, offset, MAX_SAFE_INTEGER)
- const count = readVarint(bytes, start.offset, MAX_SAFE_INTEGER)
- offset = count.offset
- const first = Number(start.value)
- const length = Number(count.value)
- if (length < 1) {
- throw new Error('malformed source_event_seqs storage value: run count must be positive')
- }
- if (first <= previousEnd || !Number.isSafeInteger(first + length - 1)) {
- throw new Error('malformed source_event_seqs storage value: runs must ascend within safe integers')
- }
- if (length > maxEntries - values.length) {
- throw new Error('malformed source_event_seqs storage value: run exceeds its event sequence')
- }
- for (let index = 0; index < length; index += 1) values.push(first + index)
- previousEnd = first + length - 1
- }
- return values
- }
- function readVarint(
- bytes: Uint8Array,
- offset: number,
- limit: bigint,
- ): { readonly value: bigint; readonly offset: number } {
- let value = 0n
- let shift = 0n
- while (offset < bytes.length) {
- const byte = bytes[offset] as number
- offset += 1
- value |= BigInt(byte & 0x7f) << shift
- if ((byte & 0x80) === 0) {
- if (shift > 0n && (byte & 0x7f) === 0) {
- throw new Error('malformed source_event_seqs storage value: non-canonical varint')
- }
- if (value > limit) {
- throw new Error('malformed source_event_seqs storage value: varint is out of range')
- }
- return { value, offset }
- }
- shift += 7n
- if (shift > 56n) {
- throw new Error('malformed source_event_seqs storage value: varint is out of range')
- }
- }
- throw new Error('malformed source_event_seqs storage value: truncated varint')
- }
- function isChunkRow(record: StorageRecord): record is ChunkRow {
- return isChunkTag(record.type) && 'seq0' in record && !('seq' in record)
- }
- function decodeScalarRow(row: EventRow): SessionEvent {
- const surfaceFields = {
- ...row.source_event_seqs === null
- ? {}
- : { sourceEventSeqs: decodeSourceEventSeqs(row.source_event_seqs, row.seq) },
- ...row.surface_op === null
- ? {}
- : { surfaceOp: JSON.parse(row.surface_op) as SessionEvent<SurfaceEventType>['surfaceOp'] },
- }
- return {
- type: row.type as SessionEvent['type'],
- seq: row.seq,
- time: row.time,
- data: JSON.parse(decodeData(row.data)) as SessionEvent['data'],
- ...surfaceFields,
- } as SessionEvent
- }
- /**
- * Validate and flatten physical rows into their logical prefix. A malformed
- * row or logical gap is committed corruption when a later valid turn end
- * exists; otherwise it starts a removable physical tail.
- * @param rows - physical rows ordered by their first logical sequence.
- * @param base - logical sequence expected from the first selected row.
- * @returns the contiguous logical prefix and optional physical deletion base.
- */
- export function scanRows(
- rows: readonly EventRow[],
- base = 0,
- ): { preserved: SessionEvent[]; tornFrom?: number } {
- let lastTurnEndRow = -1
- for (let index = rows.length - 1; index >= 0; index -= 1) {
- try {
- if (decodeRow(rows[index] as EventRow).some(event => event.type === 'turn/end')) {
- lastTurnEndRow = index
- break
- }
- } catch {
- // A malformed row cannot prove that an earlier physical prefix committed.
- }
- }
- const preserved: SessionEvent[] = []
- let expected = base
- for (let rowIndex = 0; rowIndex < rows.length; rowIndex += 1) {
- const physical = rows[rowIndex] as EventRow
- let logicalEvents: SessionEvent[] | undefined
- try {
- logicalEvents = decodeRow(physical)
- } catch {
- // The committed-prefix rule below owns whether this invalid row is fatal or repairable.
- }
- if (logicalEvents === undefined) {
- if (rowIndex <= lastTurnEndRow) {
- throw new Error(`corrupt session log: invalid committed physical row at seq ${physical.seq}`)
- }
- return { preserved, tornFrom: physical.seq }
- }
- let contiguous = true
- for (const event of logicalEvents) {
- if (event.seq !== expected) {
- contiguous = false
- break
- }
- expected += 1
- }
- if (!contiguous) {
- if (rowIndex <= lastTurnEndRow) {
- throw new Error(`corrupt session log: invalid committed physical row at seq ${physical.seq}`)
- }
- return { preserved, tornFrom: physical.seq }
- }
- preserved.push(...logicalEvents)
- }
- return { preserved }
- }
|