diff options
Diffstat (limited to 'packages/journal-sink/src/journal-sink.ts')
| -rw-r--r-- | packages/journal-sink/src/journal-sink.ts | 262 |
1 files changed, 131 insertions, 131 deletions
diff --git a/packages/journal-sink/src/journal-sink.ts b/packages/journal-sink/src/journal-sink.ts index 8e36fd6..072a013 100644 --- a/packages/journal-sink/src/journal-sink.ts +++ b/packages/journal-sink/src/journal-sink.ts @@ -8,32 +8,32 @@ import type { LogRecord, LogSink } from "@dispatch/kernel"; * Pure function — no I/O, no side effects. */ export function serialize(record: LogRecord): string { - return `${JSON.stringify(record)}\n`; + return `${JSON.stringify(record)}\n`; } // --- Imperative shell (fs edge) --- /** Injectable fs operations — confined to the edge. */ export interface FsOps { - readonly open: (path: string) => number; - readonly write: (fd: number, data: string) => void; - readonly close: (fd: number) => void; - readonly rename: (oldPath: string, newPath: string) => void; - readonly statSize: (path: string) => number; - readonly fsync: (fd: number) => void; + readonly open: (path: string) => number; + readonly write: (fd: number, data: string) => void; + readonly close: (fd: number) => void; + readonly rename: (oldPath: string, newPath: string) => void; + readonly statSize: (path: string) => number; + readonly fsync: (fd: number) => void; } /** Clock injection for fsync interval. */ export interface ClockOps { - readonly now: () => number; + readonly now: () => number; } export interface JournalSinkOpts { - readonly path: string; - readonly maxBytes?: number; - readonly fsync?: "periodic" | "none"; - readonly fs?: FsOps; - readonly clock?: ClockOps; + readonly path: string; + readonly maxBytes?: number; + readonly fsync?: "periodic" | "none"; + readonly fs?: FsOps; + readonly clock?: ClockOps; } const DEFAULT_MAX_BYTES = 50 * 1024 * 1024; // 50 MB @@ -45,127 +45,127 @@ const FSYNC_INTERVAL_MS = 5_000; // 5 seconds * Rotates when file exceeds maxBytes (rename → .1, reopen fresh). */ export function createJournalSink(opts: JournalSinkOpts): LogSink { - const filePath = opts.path; - const maxBytes = opts.maxBytes ?? DEFAULT_MAX_BYTES; - const syncMode = opts.fsync ?? "periodic"; - const fs = opts.fs ?? createDefaultFsOps(); - const clock = opts.clock ?? { now: () => Date.now() }; - - const NO_FD = -1; - let fd: number; - let bytesWritten: number; - try { - fd = fs.open(filePath); - bytesWritten = fs.statSize(filePath); - } catch { - fd = NO_FD; - bytesWritten = 0; - } - let lastFsyncAt = clock.now(); - - function tryOpen(): boolean { - if (fd !== NO_FD) return true; - try { - fd = fs.open(filePath); - bytesWritten = 0; - return true; - } catch { - return false; - } - } - - function rotate(): void { - if (fd !== NO_FD) { - try { - fs.close(fd); - } catch { - // Ignore close errors during rotation. - } - } - const rotatedPath = `${filePath}.1`; - try { - fs.rename(filePath, rotatedPath); - } catch { - // If rename fails, just truncate by reopening. - } - try { - fd = fs.open(filePath); - } catch { - fd = NO_FD; - } - bytesWritten = 0; - } - - function maybeFsync(): void { - if (syncMode !== "periodic" || fd === NO_FD) return; - const now = clock.now(); - if (now - lastFsyncAt >= FSYNC_INTERVAL_MS) { - try { - fs.fsync(fd); - } catch { - // Swallow — fail-safe. - } - lastFsyncAt = now; - } - } - - const sink: LogSink = { - emit(record: LogRecord): void { - try { - if (!tryOpen()) { - console.warn("[journal-sink] cannot open journal, dropping record"); - return; - } - - const line = serialize(record); - const lineBytes = Buffer.byteLength(line, "utf8"); - - if (bytesWritten + lineBytes > maxBytes) { - rotate(); - if (fd === NO_FD) { - console.warn("[journal-sink] rotation failed, dropping record"); - return; - } - } - - fs.write(fd, line); - bytesWritten += lineBytes; - maybeFsync(); - } catch (err) { - // Fail-safe: drop + warn, never throw to the caller (D3/D7). - console.warn("[journal-sink] write failed, dropping record:", err); - } - }, - }; - - return sink; + const filePath = opts.path; + const maxBytes = opts.maxBytes ?? DEFAULT_MAX_BYTES; + const syncMode = opts.fsync ?? "periodic"; + const fs = opts.fs ?? createDefaultFsOps(); + const clock = opts.clock ?? { now: () => Date.now() }; + + const NO_FD = -1; + let fd: number; + let bytesWritten: number; + try { + fd = fs.open(filePath); + bytesWritten = fs.statSize(filePath); + } catch { + fd = NO_FD; + bytesWritten = 0; + } + let lastFsyncAt = clock.now(); + + function tryOpen(): boolean { + if (fd !== NO_FD) return true; + try { + fd = fs.open(filePath); + bytesWritten = 0; + return true; + } catch { + return false; + } + } + + function rotate(): void { + if (fd !== NO_FD) { + try { + fs.close(fd); + } catch { + // Ignore close errors during rotation. + } + } + const rotatedPath = `${filePath}.1`; + try { + fs.rename(filePath, rotatedPath); + } catch { + // If rename fails, just truncate by reopening. + } + try { + fd = fs.open(filePath); + } catch { + fd = NO_FD; + } + bytesWritten = 0; + } + + function maybeFsync(): void { + if (syncMode !== "periodic" || fd === NO_FD) return; + const now = clock.now(); + if (now - lastFsyncAt >= FSYNC_INTERVAL_MS) { + try { + fs.fsync(fd); + } catch { + // Swallow — fail-safe. + } + lastFsyncAt = now; + } + } + + const sink: LogSink = { + emit(record: LogRecord): void { + try { + if (!tryOpen()) { + console.warn("[journal-sink] cannot open journal, dropping record"); + return; + } + + const line = serialize(record); + const lineBytes = Buffer.byteLength(line, "utf8"); + + if (bytesWritten + lineBytes > maxBytes) { + rotate(); + if (fd === NO_FD) { + console.warn("[journal-sink] rotation failed, dropping record"); + return; + } + } + + fs.write(fd, line); + bytesWritten += lineBytes; + maybeFsync(); + } catch (err) { + // Fail-safe: drop + warn, never throw to the caller (D3/D7). + console.warn("[journal-sink] write failed, dropping record:", err); + } + }, + }; + + return sink; } // --- Default fs ops (real Bun/Node I/O) --- function createDefaultFsOps(): FsOps { - return { - open(path: string): number { - return openSync(path, "a"); - }, - write(fd: number, data: string): void { - writeSync(fd, data); - }, - close(fd: number): void { - closeSync(fd); - }, - rename(oldPath: string, newPath: string): void { - renameSync(oldPath, newPath); - }, - statSize(path: string): number { - try { - return statSync(path).size; - } catch { - return 0; - } - }, - fsync(fd: number): void { - fsyncSync(fd); - }, - }; + return { + open(path: string): number { + return openSync(path, "a"); + }, + write(fd: number, data: string): void { + writeSync(fd, data); + }, + close(fd: number): void { + closeSync(fd); + }, + rename(oldPath: string, newPath: string): void { + renameSync(oldPath, newPath); + }, + statSize(path: string): number { + try { + return statSync(path).size; + } catch { + return 0; + } + }, + fsync(fd: number): void { + fsyncSync(fd); + }, + }; } |
