| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024 |
- /**
- * Durable whole-generation publication for JSONL Session artifacts.
- *
- * Format packages transform parsed JSON values. This module owns the physical
- * encoding, exact source identity, immutable generation files, and exclusive
- * current-generation publication for both configured JSONL suffixes.
- * @module @deepseek-ai/dsh-session-persistence-jsonl/generation
- */
- import { createHash, randomBytes } from 'node:crypto'
- import {
- link as fsLink,
- lstat as fsLstat,
- open as fsOpen,
- readFile as fsReadFile,
- readdir as fsReaddir,
- rm as fsRm,
- stat as fsStat,
- type FileHandle,
- } from 'node:fs/promises'
- import { basename, dirname, join } from 'node:path'
- import { performance } from 'node:perf_hooks'
- import { pipeline, Readable } from 'node:stream'
- import { scheduler } from 'node:timers/promises'
- import { isDeepStrictEqual } from 'node:util'
- import { constants, createZstdCompress } from 'node:zlib'
- import { Session } from '@deepseek-ai/dsh-session'
- import type { SessionEvent } from '@deepseek-ai/dsh-session'
- import { BlockAssembler, expandAssistantStream } from '@deepseek-ai/dsh-llm'
- import type {
- SessionFormatArtifact,
- SessionFormatJsonValue,
- SessionFormatRestore,
- } from '@deepseek-ai/dsh-session-format'
- import { validateStoredEvents } from '@deepseek-ai/dsh-session-persistence'
- import type { JsonlCompression } from './format.ts'
- import { generationLogFilename, logSuffix, SessionLogScanner } from './format.ts'
- import { publishNewFileWin32 } from './win32.ts'
- import {
- compressZstdFrame,
- createZstdFrameDecoder,
- decompressZstdPrefix,
- scanZstdFrames,
- } from './zstd.ts'
- /** Internal scheduling bounds: preserve old decode cadence and cap each synchronous encode slice. */
- const MIGRATION_DECODE_YIELD_INTERVAL_MS = 500
- const MIGRATION_WORK_CHUNK_BYTES = 1024 * 1024
- const MIGRATION_WRITE_CHUNK_BYTES = 4 * 1024 * 1024
- const ZSTD_CHECKSUM_OPTIONS = {
- chunkSize: MIGRATION_WORK_CHUNK_BYTES,
- params: { [constants.ZSTD_c_checksumFlag]: 1 },
- }
- /** Pure adapter between backend-owned JSONL framing and the format catalog. */
- export interface JsonlGenerationFormatAdapter {
- readonly currentVersion: number
- /** Create the single-pass codec and migration state for a historical header. */
- createRestore(header: Record<string, unknown>): SessionFormatRestore
- /** Encode one current header record without materializing body rows. */
- encodeHeader(header: SessionFormatArtifact['header'], inheritedEventCount: number): SessionFormatJsonValue
- /** Encode one current event record. */
- encodeEvent(event: SessionFormatArtifact['events'][number]): SessionFormatJsonValue
- /** Classify a supported-version artifact that policy refuses to migrate. */
- isUnsupportedMigrationError?(error: unknown): error is Error
- }
- /** Inputs for preparing one historical generation and publishing its current successor later. */
- export interface PrepareJsonlMigrationOptions {
- /** Immutable generation selected by the backend resolver. */
- readonly sourcePath: string
- /** Version selected from the source filename and independently checked against its header. */
- readonly sourceVersion: number
- /** Canonical filename for `format.currentVersion` in the same Session directory. */
- readonly currentPath: string
- readonly compression: JsonlCompression
- readonly format: JsonlGenerationFormatAdapter
- /** Validate one selected historical header's identity before any migration write. */
- readonly validateHistoricalHeader?: (
- header: Readonly<Record<string, unknown>>,
- ) => void | Promise<void>
- /** Validate the staged file in an isolated worker before publication. */
- readonly verifyCurrentFile: (
- path: string,
- compression: JsonlCompression,
- expectedId: string,
- expectedEventCount: number,
- expectedPrefix?: JsonlExpectedPrefix,
- signal?: AbortSignal,
- ) => Promise<JsonlVerifiedGeneration>
- readonly signal?: AbortSignal
- }
- /** Small physical identity returned by an isolated generation verifier. */
- export interface JsonlVerifiedGeneration {
- readonly identity: JsonlPhysicalIdentity
- readonly bytes: number
- readonly digest: string
- }
- /** Physical byte prefix already proven to be a valid complete generation. */
- export interface JsonlExpectedPrefix {
- readonly bytes: number
- readonly digest: string
- }
- /** A historical source changed after its single decode and migration pass. */
- export class JsonlGenerationSourceChangedError extends Error {
- override readonly name = 'JsonlGenerationSourceChangedError'
- /** @param path - historical generation whose revision changed. */
- constructor(readonly path: string) {
- super(`historical session generation changed during migration: "${path}"`)
- }
- }
- /** Current logical state prepared independently from durable publication. */
- export interface PreparedJsonlMigration {
- readonly sourceIdentity: JsonlPhysicalIdentity
- readonly artifact: SessionFormatArtifact
- /** Encode, verify, and exclusively publish once; every call shares the same success or failure. */
- publish(): Promise<JsonlPhysicalIdentity>
- }
- /** A historical artifact is intact, but the format edge refuses its contents. */
- export class JsonlGenerationUnsupportedMigrationError extends Error {
- override readonly name = 'JsonlGenerationUnsupportedMigrationError'
- /**
- * @param fromVersion - unchanged source generation version.
- * @param reason - format-edge refusal.
- */
- constructor(
- readonly fromVersion: number,
- readonly reason: Error,
- ) {
- super(reason.message, { cause: reason })
- }
- }
- /** A current-generation filename already names different or invalid bytes. */
- export class JsonlGenerationTargetConflictError extends Error {
- override readonly name = 'JsonlGenerationTargetConflictError'
- /**
- * @param path - immutable target that prevented exclusive publication.
- * @param reason - why the existing target cannot be accepted.
- */
- constructor(
- readonly path: string,
- readonly reason: Error,
- ) {
- super(`current session generation already exists at "${path}": ${reason.message}`, { cause: reason })
- }
- }
- /** Stat identity captured together with exact generation bytes. */
- export interface JsonlPhysicalIdentity {
- readonly dev: bigint
- readonly ino: bigint
- readonly size: bigint
- readonly mtimeNs: bigint
- readonly ctimeNs: bigint
- }
- /** Exact bytes of one stable file revision together with the stat identity that proved it stable. */
- export interface StablePhysicalFile {
- readonly bytes: Buffer
- readonly identity: JsonlPhysicalIdentity
- }
- interface GenerationFileSystem {
- open(path: string, flags: string, mode?: number): Promise<FileHandle>
- readFile(path: string, signal?: AbortSignal): Promise<Buffer>
- readdir(path: string): Promise<string[]>
- stat(path: string): Promise<JsonlPhysicalIdentity>
- lstat(path: string): Promise<{ isFile(): boolean; isSymbolicLink(): boolean }>
- link(existingPath: string, newPath: string): Promise<void>
- rm(path: string): Promise<void>
- }
- type GenerationBarrierPhase =
- | 'before-source-check'
- | 'after-publication'
- interface JsonlGenerationInternals {
- readonly fs: GenerationFileSystem
- readonly randomToken: () => string
- readonly platform: NodeJS.Platform
- readonly publishNewWin32: typeof publishNewFileWin32
- readonly barrier: (phase: GenerationBarrierPhase, attempt: number) => void | Promise<void>
- }
- /** Dependency overrides for an isolated generation runtime. */
- export type JsonlGenerationRuntimeOverrides = Partial<Omit<JsonlGenerationInternals, 'fs'>> & {
- readonly fs?: Partial<GenerationFileSystem>
- }
- /** Bound generation operations used by production defaults and deterministic tests. */
- export interface JsonlGenerationRuntime {
- readStable(path: string, signal?: AbortSignal): Promise<StablePhysicalFile>
- prepare(options: PrepareJsonlMigrationOptions): Promise<PreparedJsonlMigration>
- verify(
- path: string,
- compression: JsonlCompression,
- expectedId: string,
- expectedEventCount: number,
- expectedPrefix?: JsonlExpectedPrefix,
- ): Promise<JsonlVerifiedGeneration>
- }
- const defaultFileSystem: GenerationFileSystem = {
- open: (path, flags, mode) => fsOpen(path, flags, mode),
- readFile: (path, signal) => fsReadFile(path, signal === undefined ? undefined : { signal }),
- readdir: path => fsReaddir(path),
- stat: path => fsStat(path, { bigint: true }),
- lstat: path => fsLstat(path),
- link: fsLink,
- rm: path => fsRm(path, { force: true }),
- }
- const defaultInternals: JsonlGenerationInternals = {
- fs: defaultFileSystem,
- randomToken: () => randomBytes(8).toString('hex'),
- platform: process.platform,
- publishNewWin32: publishNewFileWin32,
- barrier: () => {},
- }
- function isEEXIST(error: unknown): boolean {
- return (error as NodeJS.ErrnoException | null)?.code === 'EEXIST'
- }
- /** Whether a filesystem-owned failure should retain its original errno and path. */
- function isErrnoException(error: unknown): error is NodeJS.ErrnoException {
- return typeof (error as NodeJS.ErrnoException | null)?.code === 'string'
- }
- function identity(value: JsonlPhysicalIdentity): string {
- return [value.dev, value.ino, value.size, value.mtimeNs, value.ctimeNs].join(':')
- }
- /**
- * Read one stable revision of a JSONL file with a single retry. If an append
- * overlaps both reads, return the second read's committed pre-read prefix
- * instead of starving behind a continuous writer.
- * @param path - the generation file to read.
- * @param signal - optional cancellation for the stat/read work.
- * @returns the stable bytes (or the committed prefix) and their stat identity.
- */
- export async function readStableJsonlFile(
- path: string,
- signal?: AbortSignal,
- ): Promise<StablePhysicalFile> {
- return defaultGenerationRuntime.readStable(path, signal)
- }
- async function readStableSnapshot(
- path: string,
- signal: AbortSignal | undefined,
- fs: GenerationFileSystem,
- ): Promise<StablePhysicalFile> {
- signal?.throwIfAborted()
- let before = await fs.stat(path)
- for (let attempt = 0; ; attempt += 1) {
- const bytes = await fs.readFile(path, signal)
- signal?.throwIfAborted()
- const after = await fs.stat(path)
- if (identity(before) === identity(after)) {
- signal?.throwIfAborted()
- return { bytes, identity: after }
- }
- if (attempt === 1) {
- return { bytes: bytes.subarray(0, Number(before.size)), identity: before }
- }
- before = after
- }
- }
- /** Parse the version discriminator without validating any version-specific field. */
- function storedVersion(header: unknown): number {
- if (typeof header !== 'object' || header === null || Array.isArray(header)) {
- throw new Error('corrupt session log: first line is not a JSON object')
- }
- const version = (header as { version?: unknown }).version
- if (!Number.isSafeInteger(version) || (version as number) < 0 || Object.is(version, -0)) {
- throw new Error('corrupt session log: header version is not a non-negative safe integer')
- }
- return version as number
- }
- function parseJson(text: string, subject: string): unknown {
- try {
- return JSON.parse(text)
- } catch (error) {
- throw new Error(`corrupt session log: ${subject} is not valid JSON`, { cause: error })
- }
- }
- /** Incremental JSONL parser that retains only one cross-frame record fragment. */
- class MigratingJsonlRows {
- private fragments: Buffer[] = []
- private fragmentBytes = 0
- private rowIndex = 0
- private issue: Error | undefined
- constructor(private readonly restore: SessionFormatRestore) {}
- /** Consume plaintext bytes following the independently decoded header. */
- write(chunk: Buffer): void {
- /* jscpd:ignore-start -- migration parsing and readable-log scanning own different recovery and byte-accounting state. */
- let lineStart = 0
- for (
- let newline = chunk.indexOf(0x0A);
- newline !== -1;
- newline = chunk.indexOf(0x0A, lineStart)
- ) {
- const fragment = chunk.subarray(lineStart, newline)
- let line = fragment
- if (this.fragments.length > 0) {
- if (fragment.length > 0) this.fragments.push(fragment)
- line = Buffer.concat(this.fragments, this.fragmentBytes + fragment.length)
- this.fragments = []
- this.fragmentBytes = 0
- }
- this.consume(line)
- lineStart = newline + 1
- }
- if (lineStart < chunk.length) {
- const fragment = Buffer.from(chunk.subarray(lineStart))
- this.fragments.push(fragment)
- this.fragmentBytes += fragment.length
- }
- /* jscpd:ignore-end */
- }
- /** Refuse a record fragment left by structurally complete Zstandard frames. */
- assertCompleteFramesEndOnRecord(): void {
- if (this.fragments.length > 0) {
- throw new Error('corrupt Zstandard session log: complete frame contains a torn JSONL record')
- }
- }
- finish(): SessionFormatArtifact {
- return this.restore.finish()
- }
- private consume(line: Buffer): void {
- const index = this.rowIndex
- this.rowIndex += 1
- let row: unknown
- try {
- row = parseJson(line.toString('utf8'), `row ${index + 1}`)
- } catch (error: unknown) {
- this.issue ??= asError(error)
- return
- }
- if (this.issue !== undefined) {
- if (typeof row === 'object' && row !== null
- && (row as { type?: unknown }).type === 'turn/end') throw this.issue
- return
- }
- this.restore.decodeRow(row)
- }
- }
- interface StartedMigrationStream {
- readonly parser: MigratingJsonlRows
- }
- async function startMigrationStream(
- headerRecord: Buffer,
- sourceVersion: number,
- format: JsonlGenerationFormatAdapter,
- validateHistoricalHeader?: PrepareJsonlMigrationOptions['validateHistoricalHeader'],
- ): Promise<StartedMigrationStream> {
- const value = parseJson(headerRecord.subarray(0, -1).toString('utf8'), 'header line')
- const version = storedVersion(value)
- if (version !== sourceVersion) {
- throw new Error(`resolved JSONL source filename identifies v${sourceVersion}, but its header identifies v${version}`)
- }
- const header = value as Record<string, unknown>
- const validation = validateHistoricalHeader?.(header)
- if (validation !== undefined) await validation
- const stream = format.createRestore(header)
- return { parser: new MigratingJsonlRows(stream) }
- }
- async function consumeMigrationBytes(
- rows: MigratingJsonlRows,
- chunks: Iterable<Buffer>,
- signal?: AbortSignal,
- ): Promise<void> {
- signal?.throwIfAborted()
- let yieldDeadline = performance.now() + MIGRATION_DECODE_YIELD_INTERVAL_MS
- for (const bytes of chunks) {
- for (let offset = 0; offset < bytes.length; offset += MIGRATION_WORK_CHUNK_BYTES) {
- rows.write(bytes.subarray(offset, offset + MIGRATION_WORK_CHUNK_BYTES))
- if (performance.now() < yieldDeadline) continue
- await scheduler.yield()
- signal?.throwIfAborted()
- yieldDeadline = performance.now() + MIGRATION_DECODE_YIELD_INTERVAL_MS
- }
- }
- }
- async function decodeStreamingMigration(
- bytes: Buffer,
- compression: JsonlCompression,
- sourceVersion: number,
- format: JsonlGenerationFormatAdapter,
- validateHistoricalHeader: PrepareJsonlMigrationOptions['validateHistoricalHeader'],
- signal?: AbortSignal,
- ): Promise<SessionFormatArtifact> {
- signal?.throwIfAborted()
- if (compression === 'none') {
- const headerEnd = bytes.indexOf(0x0A)
- if (headerEnd === -1) throw new Error('empty or header-less session log')
- const stream = await startMigrationStream(
- bytes.subarray(0, headerEnd + 1),
- sourceVersion,
- format,
- validateHistoricalHeader,
- )
- signal?.throwIfAborted()
- const bodyEnd = bytes.lastIndexOf(0x0A)
- if (bodyEnd > headerEnd) {
- await consumeMigrationBytes(
- stream.parser,
- [bytes.subarray(headerEnd + 1, bodyEnd + 1)],
- signal,
- )
- }
- return stream.parser.finish()
- }
- const { frames, tornStart } = scanZstdFrames(bytes)
- if (frames.length === 0) throw new Error('empty or header-less Zstandard session log')
- const decoder = createZstdFrameDecoder()
- try {
- const decoded = decoder.decode(bytes, frames)
- const first = decoded.next()
- /* v8 ignore next -- a non-empty structural frame list yields once or throws. */
- if (first.done) throw new Error('empty or header-less Zstandard session log')
- assertIndependentHeaderFrame(first.value)
- const stream = await startMigrationStream(
- first.value,
- sourceVersion,
- format,
- validateHistoricalHeader,
- )
- signal?.throwIfAborted()
- await consumeMigrationBytes(stream.parser, decoded, signal)
- stream.parser.assertCompleteFramesEndOnRecord()
- if (tornStart !== undefined) {
- let recovered: Buffer = Buffer.alloc(0)
- try {
- recovered = await decompressZstdPrefix(bytes.subarray(tornStart))
- } catch {
- /* v8 ignore next -- decoder failure plus concurrent abort is timing-dependent. */
- if (signal?.aborted) signal.throwIfAborted()
- }
- signal?.throwIfAborted()
- const newline = recovered.lastIndexOf(0x0A)
- if (newline !== -1) {
- await consumeMigrationBytes(
- stream.parser,
- [recovered.subarray(0, newline + 1)],
- signal,
- )
- }
- }
- return stream.parser.finish()
- } finally {
- decoder.close()
- }
- }
- /**
- * Read and validate one complete current generation for an isolated verifier.
- * @param path - staged or competing current-generation path.
- * @param compression - configured physical encoding.
- * @param expectedId - Session identity expected in the header.
- * @param expectedEventCount - exact logical event count expected after decoding.
- * @param expectedPrefix - verified migration prefix; an append tail may be present and is not validated.
- * @returns stable physical identity and digest for publication comparison.
- */
- export async function verifyJsonlCurrentGeneration(
- path: string,
- compression: JsonlCompression,
- expectedId: string,
- expectedEventCount: number,
- expectedPrefix?: JsonlExpectedPrefix,
- ): Promise<JsonlVerifiedGeneration> {
- return defaultGenerationRuntime.verify(path, compression, expectedId, expectedEventCount, expectedPrefix)
- }
- async function verifyCurrentGeneration(
- path: string,
- compression: JsonlCompression,
- expectedId: string,
- expectedEventCount: number,
- fs: GenerationFileSystem,
- expectedPrefix?: JsonlExpectedPrefix,
- ): Promise<JsonlVerifiedGeneration> {
- const before = await fs.stat(path)
- const bytes = await fs.readFile(path)
- const after = await fs.stat(path)
- if (expectedPrefix !== undefined) {
- if (bytes.length < expectedPrefix.bytes) {
- throw new Error('target bytes are shorter than the migrated generation')
- }
- const digest = createHash('sha256').update(bytes.subarray(0, expectedPrefix.bytes)).digest('hex')
- if (digest !== expectedPrefix.digest) {
- throw new Error('target bytes do not begin with the migrated generation')
- }
- return { identity: after, bytes: expectedPrefix.bytes, digest }
- }
- if (identity(before) !== identity(after)) {
- throw new Error('current session generation changed during verification')
- }
- const snapshot = { bytes, identity: after }
- const generation = decodeCurrentGeneration(snapshot.bytes, compression)
- validateStoredEvents(generation.meta, generation.events, { kind: 'jsonl', path })
- if (generation.meta.id !== expectedId) {
- throw new Error(`current session generation contains id "${generation.meta.id}", expected "${expectedId}"`)
- }
- if (generation.events.length !== expectedEventCount) {
- throw new Error(
- `current session generation contains ${generation.events.length} events, expected ${expectedEventCount}`,
- )
- }
- Session.fromRestore(
- generation.meta.id,
- generation.events,
- generation.meta,
- generation.inheritedEventCount,
- 'detached',
- )
- assertCurrentAssistantStreams(generation.events)
- return {
- identity: snapshot.identity,
- bytes: snapshot.bytes.length,
- digest: createHash('sha256').update(snapshot.bytes).digest('hex'),
- }
- }
- /** Fully replay embedded streams only inside isolated current-generation verification. */
- function assertCurrentAssistantStreams(events: readonly SessionEvent[]): void {
- for (const [index, event] of events.entries()) {
- if (event.type !== 'assistant/message' && event.type !== 'assistant/attempt') continue
- const assembler = new BlockAssembler()
- let timed: ReturnType<typeof expandAssistantStream>
- try {
- timed = expandAssistantStream(event.data.stream)
- for (const member of timed) assembler.push(member.chunk)
- } catch (error: unknown) {
- throw new Error(`seed ${event.type} at index ${index} has an invalid embedded stream`, { cause: error })
- }
- if (event.type === 'assistant/attempt' || timed.length === 0) continue
- const content = event.data.interrupted === true ? assembler.interruptedBlocks() : assembler.blocks()
- if (!isDeepStrictEqual(event.data.message.content, content)) {
- throw new Error(`seed assistant/message at index ${index} content disagrees with its embedded stream`)
- }
- if (!isDeepStrictEqual(event.data.usage, assembler.usage)) {
- throw new Error(`seed assistant/message at index ${index} usage disagrees with its embedded stream`)
- }
- if (!isDeepStrictEqual(event.data.message.source.replayState, assembler.replayState)) {
- throw new Error(`seed assistant/message at index ${index} replay state disagrees with its embedded stream`)
- }
- }
- }
- function decodeCurrentGeneration(
- bytes: Buffer,
- compression: JsonlCompression,
- ): ReturnType<SessionLogScanner['finish']> {
- if (compression === 'none') {
- const headerEnd = bytes.indexOf(0x0A)
- if (headerEnd === -1) throw new Error('empty or header-less session log')
- const scanner = new SessionLogScanner(bytes.subarray(0, headerEnd + 1), 'strict')
- scanner.write(bytes.subarray(headerEnd + 1))
- return finishCurrentGenerationScan(scanner)
- }
- const { frames, tornStart } = scanZstdFrames(bytes)
- if (frames.length === 0) throw new Error('empty or header-less Zstandard session log')
- if (tornStart !== undefined) throw new Error('current session generation has a torn physical tail')
- const decoder = createZstdFrameDecoder()
- try {
- const plaintext = decoder.decode(bytes, frames)
- const header = plaintext.next()
- /* v8 ignore next -- a non-empty structural frame list yields once or throws. */
- if (header.done) throw new Error('empty or header-less Zstandard session log')
- assertIndependentHeaderFrame(header.value)
- const scanner = new SessionLogScanner(header.value, 'strict')
- for (const chunk of plaintext) scanner.write(chunk)
- return finishCurrentGenerationScan(scanner)
- } finally {
- decoder.close()
- }
- }
- function finishCurrentGenerationScan(
- scanner: SessionLogScanner,
- ): ReturnType<SessionLogScanner['finish']> {
- const inputBytes = scanner.checkpoint().inputBytes
- const decoded = scanner.finish()
- if (decoded.committedBytes !== inputBytes) throw new Error('current session generation has a torn physical tail')
- return decoded
- }
- function stringifyJson(value: unknown, subject: string): string {
- let text: unknown
- try {
- text = JSON.stringify(value)
- } catch (error) {
- throw new Error(`${subject} is not lossless JSON`, { cause: error })
- }
- if (typeof text !== 'string') throw new Error(`${subject} is not lossless JSON`)
- return text
- }
- function assertIndependentHeaderFrame(plaintext: Buffer): void {
- if (plaintext.length === 0 || plaintext.indexOf(0x0A) !== plaintext.length - 1) {
- throw new Error('corrupt Zstandard session log: first frame is not exactly one header line')
- }
- }
- function assertGenerationPaths(
- sourcePath: string,
- sourceVersion: number,
- currentPath: string,
- currentVersion: number,
- compression: JsonlCompression,
- ): string {
- const expectedSource = generationLogFilename(sourceVersion, compression)
- const expectedCurrent = generationLogFilename(currentVersion, compression)
- if (basename(sourcePath) !== expectedSource) {
- throw new Error(`resolved JSONL source path must end with "${expectedSource}": ${sourcePath}`)
- }
- if (basename(currentPath) !== expectedCurrent) {
- throw new Error(`current JSONL generation path must end with "${expectedCurrent}": ${currentPath}`)
- }
- if (dirname(sourcePath) !== dirname(currentPath)) {
- throw new Error('source and current JSONL generations must share one Session directory')
- }
- return logSuffix(compression)
- }
- async function syncDirectory(path: string, internals: JsonlGenerationInternals): Promise<void> {
- /* v8 ignore next -- Windows namespace operations request write-through directly. */
- if (internals.platform === 'win32') return
- const handle = await internals.fs.open(path, 'r')
- try {
- await handle.sync()
- } finally {
- await handle.close()
- }
- }
- interface StreamedMigrationStage {
- readonly path: string
- readonly bytes: number
- readonly digest: string
- }
- /** Produce bounded JSONL chunks while yielding between main-thread encoding slices. */
- async function* encodeMigrationRows(
- artifact: SessionFormatArtifact,
- format: JsonlGenerationFormatAdapter,
- signal?: AbortSignal,
- ): AsyncGenerator<Buffer, void, void> {
- signal?.throwIfAborted()
- let lines: string[] = []
- let bytes = 0
- for (const value of artifact.events) {
- const line = `${stringifyJson(format.encodeEvent(value), `migrated Session event ${value.seq}`)}\n`
- const lineBytes = Buffer.byteLength(line)
- if (bytes > 0 && bytes + lineBytes > MIGRATION_WORK_CHUNK_BYTES) {
- yield Buffer.from(lines.join(''))
- await scheduler.yield()
- signal?.throwIfAborted()
- lines = []
- bytes = 0
- }
- lines.push(line)
- bytes += lineBytes
- }
- yield Buffer.from(lines.join(''))
- }
- async function writeMigrationChunks(
- chunks: AsyncIterable<Buffer>,
- write: (chunk: Buffer) => Promise<void>,
- ): Promise<void> {
- let pending: Buffer[] = []
- let bytes = 0
- for await (const chunk of chunks) {
- pending.push(chunk)
- bytes += chunk.length
- if (bytes < MIGRATION_WRITE_CHUNK_BYTES) continue
- await write(pending.length === 1 ? pending[0] as Buffer : Buffer.concat(pending, bytes))
- pending = []
- bytes = 0
- }
- if (bytes > 0) await write(pending.length === 1 ? pending[0] as Buffer : Buffer.concat(pending, bytes))
- }
- /** Encode directly into one synced stage without a whole-artifact row or byte buffer. */
- async function writeSyncedTemp(
- currentPath: string,
- suffix: string,
- compression: JsonlCompression,
- artifact: SessionFormatArtifact,
- format: JsonlGenerationFormatAdapter,
- signal: AbortSignal | undefined,
- internals: JsonlGenerationInternals,
- ): Promise<StreamedMigrationStage> {
- signal?.throwIfAborted()
- let path: string
- let handle: FileHandle
- for (;;) {
- path = join(dirname(currentPath), `session.migration.${internals.randomToken()}${suffix}.tmp`)
- try {
- handle = await internals.fs.open(path, 'wx', 0o600)
- break
- } catch (error) {
- if (isEEXIST(error)) continue
- throw error
- }
- }
- const hash = createHash('sha256')
- let bytes = 0
- const write = async (chunk: Buffer): Promise<void> => {
- await handle.writeFile(chunk)
- hash.update(chunk)
- bytes += chunk.length
- }
- let failure: unknown
- try {
- const headerValue = format.encodeHeader(artifact.header, artifact.inheritedEventCount)
- const header = Buffer.from(`${stringifyJson(headerValue, 'migrated session header')}\n`)
- await write(compression === 'zstd' ? await compressZstdFrame(header) : header)
- if (artifact.events.length > 0) {
- const rows = encodeMigrationRows(artifact, format, signal)
- if (compression === 'none') {
- await writeMigrationChunks(rows, write)
- } else {
- await new Promise<void>((resolve, reject) => {
- pipeline(
- Readable.from(rows, { objectMode: false, highWaterMark: MIGRATION_WORK_CHUNK_BYTES }),
- createZstdCompress(ZSTD_CHECKSUM_OPTIONS),
- async (source) => { await writeMigrationChunks(source as AsyncIterable<Buffer>, write) },
- (error: Error | null | undefined) => {
- if (error instanceof Error) reject(error)
- else resolve()
- },
- )
- })
- }
- }
- signal?.throwIfAborted()
- await handle.sync()
- } catch (error: unknown) {
- failure = error
- }
- try {
- await handle.close()
- } catch (error: unknown) {
- failure = failure === undefined
- ? error
- : new AggregateError([failure, error], `failed to write and close migration stage "${path}"`)
- }
- if (failure !== undefined) {
- const writeError = failure instanceof Error
- ? failure
- : new Error('migration stage write failed with a non-Error rejection', { cause: failure })
- await removeTemporary(path, writeError, internals)
- throw writeError
- }
- return { path, bytes, digest: hash.digest('hex') }
- }
- /** Remove one temporary file without hiding the operation failure that made it disposable. */
- async function removeTemporary(
- path: string,
- primaryFailure: unknown,
- internals: JsonlGenerationInternals,
- ): Promise<void> {
- try {
- await internals.fs.rm(path)
- } catch (cleanupFailure: unknown) {
- throw new AggregateError(
- [primaryFailure, cleanupFailure],
- `failed to clean migration temporary "${path}" after an earlier failure`,
- )
- }
- }
- /** Remove a redundant stage after the target has been validated as committed. */
- async function removeCommittedTemporary(
- path: string,
- internals: JsonlGenerationInternals,
- ): Promise<void> {
- try {
- await internals.fs.rm(path)
- } catch {
- // The validated target owns the committed bytes; a redundant link cannot turn success into failure.
- }
- }
- async function publishCurrentExclusive(
- staged: string,
- currentPath: string,
- internals: JsonlGenerationInternals,
- ): Promise<boolean> {
- if (internals.platform === 'win32') {
- try {
- await internals.publishNewWin32(staged, currentPath)
- return true
- } catch (error) {
- /* v8 ignore else -- native helper tests own non-collision Win32 failures. */
- if (isEEXIST(error)) return false
- /* v8 ignore next -- the filesystem error is already complete. */
- throw error
- }
- }
- try {
- await internals.fs.link(staged, currentPath)
- } catch (error) {
- /* v8 ignore else -- a non-collision filesystem error propagates unchanged. */
- if (isEEXIST(error)) return false
- /* v8 ignore next -- the filesystem error is already complete. */
- throw error
- }
- await syncDirectory(dirname(currentPath), internals)
- return true
- }
- function asError(error: unknown): Error {
- return error instanceof Error ? error : new Error('current-generation validation failed with a non-Error rejection', {
- cause: error,
- })
- }
- async function inspectExpectedCurrent<T>(
- currentPath: string,
- internals: JsonlGenerationInternals,
- inspect: () => Promise<T>,
- ): Promise<T> {
- try {
- const expectedName = basename(currentPath)
- const names = await internals.fs.readdir(dirname(currentPath))
- if (!names.includes(expectedName)) {
- const noncanonical = names.find(name => name.toLowerCase() === expectedName.toLowerCase())
- if (noncanonical !== undefined) {
- throw new Error(`target resolves to noncanonical directory entry "${noncanonical}"`)
- }
- }
- const info = await internals.fs.lstat(currentPath)
- if (info.isSymbolicLink() || !info.isFile()) {
- throw new Error(`target is a ${info.isSymbolicLink() ? 'symbolic link' : 'non-regular file'}`)
- }
- return await inspect()
- } catch (error: unknown) {
- if (isErrnoException(error)) throw error
- throw new JsonlGenerationTargetConflictError(currentPath, asError(error))
- }
- }
- function withOverrides(overrides: JsonlGenerationRuntimeOverrides): JsonlGenerationInternals {
- return {
- ...defaultInternals,
- ...overrides,
- fs: { ...defaultFileSystem, ...overrides.fs },
- }
- }
- async function publishPreparedMigration(
- options: PrepareJsonlMigrationOptions,
- suffix: string,
- artifact: SessionFormatArtifact,
- sourceIdentity: JsonlPhysicalIdentity,
- internals: JsonlGenerationInternals,
- ): Promise<JsonlPhysicalIdentity> {
- await scheduler.yield()
- const { sourcePath, currentPath, compression, verifyCurrentFile } = options
- const eventCount = artifact.events.length
- let staged = await writeSyncedTemp(currentPath, suffix, compression, artifact, options.format, undefined, internals)
- try {
- const verifiedStage = await verifyCurrentFile(
- staged.path,
- compression,
- artifact.header.id,
- eventCount,
- )
- if (verifiedStage.bytes !== staged.bytes || verifiedStage.digest !== staged.digest) {
- throw new Error('staged session generation changed during verification')
- }
- await internals.barrier('before-source-check', 1)
- const beforePublish = await internals.fs.stat(sourcePath)
- if (identity(beforePublish) !== identity(sourceIdentity)) {
- throw new JsonlGenerationSourceChangedError(sourcePath)
- }
- const published = await publishCurrentExclusive(staged.path, currentPath, internals)
- if (published && internals.platform === 'win32') staged = { ...staged, path: '' }
- await internals.barrier('after-publication', 1)
- let currentIdentity: JsonlPhysicalIdentity
- if (published) {
- if (staged.path !== '') {
- await removeCommittedTemporary(staged.path, internals)
- staged = { ...staged, path: '' }
- }
- currentIdentity = await internals.fs.stat(currentPath)
- } else {
- const winner = await inspectExpectedCurrent(currentPath, internals, async () => {
- const candidate = await verifyCurrentFile(
- currentPath,
- compression,
- artifact.header.id,
- eventCount,
- staged,
- )
- if (candidate.bytes !== staged.bytes || candidate.digest !== staged.digest) {
- throw new Error('target bytes differ from the migrated generation')
- }
- return candidate
- })
- currentIdentity = winner.identity
- await removeCommittedTemporary(staged.path, internals)
- staged = { ...staged, path: '' }
- }
- return currentIdentity
- } catch (error: unknown) {
- if (staged.path !== '') await removeTemporary(staged.path, error, internals)
- throw error
- }
- }
- async function prepareMigration(
- options: PrepareJsonlMigrationOptions,
- internals: JsonlGenerationInternals,
- ): Promise<PreparedJsonlMigration> {
- const { sourcePath, sourceVersion, currentPath, compression, format, signal } = options
- const suffix = assertGenerationPaths(
- sourcePath,
- sourceVersion,
- currentPath,
- format.currentVersion,
- compression,
- )
- if (sourceVersion >= format.currentVersion) {
- throw new Error(`migration preparation requires a historical source, got v${sourceVersion}`)
- }
- const source = await readStableSnapshot(sourcePath, signal, internals.fs)
- let artifact: SessionFormatArtifact
- try {
- artifact = await decodeStreamingMigration(
- source.bytes,
- compression,
- sourceVersion,
- format,
- options.validateHistoricalHeader,
- signal,
- )
- } catch (error: unknown) {
- if (format.isUnsupportedMigrationError?.(error) === true) {
- throw new JsonlGenerationUnsupportedMigrationError(sourceVersion, error)
- }
- throw error
- }
- if (artifact.header.version !== format.currentVersion) {
- throw new Error(`format migration returned v${artifact.header.version}, expected v${format.currentVersion}`)
- }
- const sourceIdentity = source.identity
- let publication: Promise<JsonlPhysicalIdentity> | undefined
- return {
- sourceIdentity,
- artifact,
- publish() {
- if (publication === undefined) {
- publication = publishPreparedMigration(
- options,
- suffix,
- artifact,
- sourceIdentity,
- internals,
- )
- }
- return publication
- },
- }
- }
- /**
- * Decode and migrate one historical generation without writing its successor.
- * @param options - resolved source, current target, format adapter, and load cancellation.
- * @returns the current artifact and an idempotent explicit publication operation.
- */
- export function prepareJsonlMigration(
- options: PrepareJsonlMigrationOptions,
- ): Promise<PreparedJsonlMigration> {
- return defaultGenerationRuntime.prepare(options)
- }
- /**
- * Create one generation runtime with fixed filesystem and publication dependencies.
- * @param overrides - deterministic filesystem, platform, and race dependencies.
- * @returns bound generation operations.
- */
- export function createJsonlGenerationRuntime(
- overrides: JsonlGenerationRuntimeOverrides = {},
- ): JsonlGenerationRuntime {
- const internals = withOverrides(overrides)
- return {
- readStable: (path, signal) => readStableSnapshot(path, signal, internals.fs),
- prepare: options => prepareMigration(options, internals),
- verify: (path, compression, expectedId, expectedEventCount, expectedPrefix) => verifyCurrentGeneration(
- path, compression, expectedId, expectedEventCount, internals.fs, expectedPrefix,
- ),
- }
- }
- const defaultGenerationRuntime = createJsonlGenerationRuntime()
|