UNPKG

file-timestamp-stream

Version:

Writing stream with file rotating based on timestamp

156 lines (155 loc) 5.46 kB
/// <reference types="node" /> import * as fs from "node:fs"; import { finished, Writable } from "node:stream"; import * as timers from "timers-obj"; import strftime from "ultra-strftime"; export class FileTimestampStream extends Writable { constructor(options = {}) { super(options); this.options = options; this.flags = this.options.flags || "a"; this.fs = this.options.fs || fs; this.path = this.options.path || "out.log"; this.destroyed = false; this.streams = new Map(); this.streamCancelFinishers = new Map(); this.streamErrorHandlers = new Map(); this.closers = new Map(); } _write(chunk, encoding, callback) { if (this.destroyed) { return callback(new Error("write after destroy")); } try { this.rotate(); this.stream.write(chunk, encoding, callback); } catch (e) { callback(e); } } _writev(chunks, callback) { if (this.destroyed) { return callback(new Error("write after destroy")); } let corked = false; try { this.rotate(); corked = true; this.stream.cork(); for (const chunk of chunks) { this.stream.write(chunk.chunk, chunk.encoding); } process.nextTick(() => this.stream.uncork()); callback(); } catch (e) { if (corked) { process.nextTick(() => this.stream.uncork()); } callback(e); } } _final(callback) { if (this.stream) { this.stream.end(callback); } else { callback(); } } _destroy(error, callback) { if (this.streamErrorHandlers.size > 0) { for (const [filename, handler] of this.streamErrorHandlers) { const stream = this.streams.get(filename); if (stream) { stream.removeListener("error", handler); } } this.streamErrorHandlers.clear(); } if (this.streamCancelFinishers.size > 0) { for (const [filename, cancel] of this.streamCancelFinishers) { cancel(); this.streamCancelFinishers.delete(filename); } this.streamCancelFinishers.clear(); } if (this.streams.size > 0) { for (const stream of this.streams.values()) { if (typeof stream.destroy === "function") { stream.destroy(); } } this.streams.clear(); } if (this.closers.size > 0) { for (const closer of this.closers.values()) { closer.close(); } this.streams.clear(); } this.destroyed = true; this.stream = undefined; this.closer = undefined; callback(error); } /** * This method can be overriden in subclass * * The method generates a filename for new files. By default it returns new * filename based on path and current time. */ newFilename() { return strftime(this.path, new Date()); } rotate() { const newFilename = this.newFilename(); const { currentFilename, stream, closer } = this; if (newFilename !== currentFilename) { if (currentFilename && stream && closer) { clearInterval(closer); stream.end(); const streamErrorHandler = this.streamErrorHandlers.get(currentFilename); if (streamErrorHandler) { stream.removeListener("error", streamErrorHandler); this.streamErrorHandlers.delete(currentFilename); } } const newStream = this.fs.createWriteStream(newFilename, { flags: this.flags, }); this.stream = newStream; this.streams.set(newFilename, newStream); const newStreamErrorHandler = (err) => { this.emit("error", err); }; newStream.on("error", newStreamErrorHandler); this.streamErrorHandlers.set(newFilename, newStreamErrorHandler); const newCloser = timers .interval(FileTimestampStream.CLOSE_UNUSED_FILE_AFTER, () => { if (newFilename !== this.newFilename()) { newCloser.close(); this.closers.delete(newFilename); newStream.end(); } }) .unref(); this.closer = closer; this.closers.set(newFilename, newCloser); const newStreamCancelFinisher = finished(newStream, () => { newCloser.close(); this.closers.delete(newFilename); if (typeof newStream.destroy === "function") { newStream.destroy(); } this.streamCancelFinishers.delete(newFilename); this.streams.delete(newFilename); }); this.streamCancelFinishers.set(newFilename, newStreamCancelFinisher); this.currentFilename = newFilename; } } } FileTimestampStream.CLOSE_UNUSED_FILE_AFTER = 1000; export default FileTimestampStream;