/* eslint-disable promise/prefer-await-to-then */ import * as NodeJSFileSystem from 'fs'; import * as NodeJSPath from 'path'; import { LogHandler } from '../handler'; import { LogRecord } from '../record'; type Encoder = (record: LogRecord) => string; function nodeRequire(name: 'path'): typeof NodeJSPath; function nodeRequire(name: 'fs'): typeof NodeJSFileSystem; function nodeRequire(name: string): unknown { // eslint-disable-next-line global-require, import/no-dynamic-require, @typescript-eslint/no-explicit-any, @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports, @typescript-eslint/no-unnecessary-type-assertion,@typescript-eslint/no-unsafe-return return require((() => name)()) as any; } export const fileHandler = ({ fs = nodeRequire('fs').promises, filename, writeOptions, format = (logRecord) => `${JSON.stringify(logRecord)}\n`, }: { fs?: Pick; filename: string | (() => string); writeOptions?: { encoding?: BufferEncoding; mode?: number; flag?: string; /** * Write in file every `flushInterval`ms */ flushInterval?: number; }; format?: Encoder; }): LogHandler => { type LogRecordBuffer = ReadonlyArray; type State = Readonly<{ logRecordBuffer: LogRecordBuffer; flushPromise: undefined | Promise; }>; const path = nodeRequire('path'); const flushInterval = writeOptions?.flushInterval ?? 500; const initialState: State = { logRecordBuffer: [], flushPromise: undefined, }; let state: State = initialState; function setState(updater: (currentState: State) => State): void { state = updater(state); } function bufferToString(buffer: LogRecordBuffer): string { return buffer.map(format).join(''); } function bufferConsume(): LogRecordBuffer { const logRecords = state.logRecordBuffer; // Reset state setState(() => initialState); return logRecords; } function bufferPush(logRecord: LogRecord): void { setState((currentState) => ({ ...currentState, logRecordBuffer: [...currentState.logRecordBuffer, logRecord], })); } async function ensureDirectory(pathToDirectory: string): Promise { try { await fs.mkdir(pathToDirectory, { recursive: true }); } catch (error: unknown) { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access if ((error as any).code === 'EEXIST') { return undefined; } throw error; } return undefined; } async function executeTransaction( filePath: string, block: (fileHandle: NodeJSFileSystem.promises.FileHandle) => void | Promise ): Promise { const file = await fs.open(filePath, 'a'); try { await block(file); } finally { await file.close(); } } async function executeFlush(): Promise { const logRecords = bufferConsume(); const resolvedFilename = typeof filename == 'string' ? filename : filename(); const dirname = path.dirname(resolvedFilename); await ensureDirectory(dirname); await executeTransaction(resolvedFilename, (fileHandle) => fs.writeFile(fileHandle, bufferToString(logRecords), writeOptions) ); } function scheduleFlush(): Promise { const stateFlushPromise = state.flushPromise; if (stateFlushPromise == null) { const flushPromise = new Promise((resolve, reject) => { setTimeout(() => { executeFlush().then(resolve).catch(reject); }, flushInterval); }); setState((currentState) => ({ ...currentState, flushPromise, })); return flushPromise; } return stateFlushPromise; } return async (logRecord) => { bufferPush(logRecord); return scheduleFlush(); }; };