file-timestamp-stream
Version:
Writing stream with file rotating based on timestamp
156 lines (155 loc) • 5.46 kB
JavaScript
/// <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;