@logtape/file 0.12.0-dev.195 → 0.12.0-dev.196
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/deno.json +1 -1
- package/dist/filesink.base.cjs +27 -12
- package/dist/filesink.base.d.cts +8 -0
- package/dist/filesink.base.d.cts.map +1 -1
- package/dist/filesink.base.d.ts +8 -0
- package/dist/filesink.base.d.ts.map +1 -1
- package/dist/filesink.base.js +27 -12
- package/dist/filesink.base.js.map +1 -1
- package/filesink.base.ts +51 -12
- package/filesink.test.ts +141 -0
- package/package.json +2 -2
package/deno.json
CHANGED
package/dist/filesink.base.cjs
CHANGED
|
@@ -15,21 +15,29 @@ function getBaseFileSink(path, options) {
|
|
|
15
15
|
const formatter = options.formatter ?? __logtape_logtape.defaultTextFormatter;
|
|
16
16
|
const encoder = options.encoder ?? new TextEncoder();
|
|
17
17
|
const bufferSize = options.bufferSize ?? 1024 * 8;
|
|
18
|
+
const flushInterval = options.flushInterval ?? 5e3;
|
|
18
19
|
let fd = options.lazy ? null : options.openSync(path);
|
|
19
20
|
let buffer = "";
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if (buffer.length
|
|
21
|
+
let lastFlushTimestamp = Date.now();
|
|
22
|
+
function flushBuffer() {
|
|
23
|
+
if (fd == null) return;
|
|
24
|
+
if (buffer.length > 0) {
|
|
24
25
|
options.writeSync(fd, encoder.encode(buffer));
|
|
25
26
|
buffer = "";
|
|
26
27
|
options.flushSync(fd);
|
|
28
|
+
lastFlushTimestamp = Date.now();
|
|
27
29
|
}
|
|
30
|
+
}
|
|
31
|
+
const sink = (record) => {
|
|
32
|
+
if (fd == null) fd = options.openSync(path);
|
|
33
|
+
buffer += formatter(record);
|
|
34
|
+
const shouldFlushBySize = buffer.length >= bufferSize;
|
|
35
|
+
const shouldFlushByTime = flushInterval > 0 && record.timestamp - lastFlushTimestamp >= flushInterval;
|
|
36
|
+
if (shouldFlushBySize || shouldFlushByTime) flushBuffer();
|
|
28
37
|
};
|
|
29
38
|
sink[Symbol.dispose] = () => {
|
|
30
39
|
if (fd !== null) {
|
|
31
|
-
|
|
32
|
-
options.flushSync(fd);
|
|
40
|
+
flushBuffer();
|
|
33
41
|
options.closeSync(fd);
|
|
34
42
|
}
|
|
35
43
|
};
|
|
@@ -54,12 +62,14 @@ function getBaseRotatingFileSink(path, options) {
|
|
|
54
62
|
const maxSize = options.maxSize ?? 1024 * 1024;
|
|
55
63
|
const maxFiles = options.maxFiles ?? 5;
|
|
56
64
|
const bufferSize = options.bufferSize ?? 1024 * 8;
|
|
65
|
+
const flushInterval = options.flushInterval ?? 5e3;
|
|
57
66
|
let offset = 0;
|
|
58
67
|
try {
|
|
59
68
|
const stat = options.statSync(path);
|
|
60
69
|
offset = stat.size;
|
|
61
70
|
} catch {}
|
|
62
71
|
let fd = options.openSync(path);
|
|
72
|
+
let lastFlushTimestamp = Date.now();
|
|
63
73
|
function shouldRollover(bytes) {
|
|
64
74
|
return offset + bytes.length > maxSize;
|
|
65
75
|
}
|
|
@@ -76,21 +86,26 @@ function getBaseRotatingFileSink(path, options) {
|
|
|
76
86
|
offset = 0;
|
|
77
87
|
fd = options.openSync(path);
|
|
78
88
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
buffer += formatter(record);
|
|
82
|
-
if (buffer.length >= bufferSize) {
|
|
89
|
+
function flushBuffer() {
|
|
90
|
+
if (buffer.length > 0) {
|
|
83
91
|
const bytes = encoder.encode(buffer);
|
|
84
92
|
buffer = "";
|
|
85
93
|
if (shouldRollover(bytes)) performRollover();
|
|
86
94
|
options.writeSync(fd, bytes);
|
|
87
95
|
options.flushSync(fd);
|
|
88
96
|
offset += bytes.length;
|
|
97
|
+
lastFlushTimestamp = Date.now();
|
|
89
98
|
}
|
|
99
|
+
}
|
|
100
|
+
let buffer = "";
|
|
101
|
+
const sink = (record) => {
|
|
102
|
+
buffer += formatter(record);
|
|
103
|
+
const shouldFlushBySize = buffer.length >= bufferSize;
|
|
104
|
+
const shouldFlushByTime = flushInterval > 0 && record.timestamp - lastFlushTimestamp >= flushInterval;
|
|
105
|
+
if (shouldFlushBySize || shouldFlushByTime) flushBuffer();
|
|
90
106
|
};
|
|
91
107
|
sink[Symbol.dispose] = () => {
|
|
92
|
-
|
|
93
|
-
options.flushSync(fd);
|
|
108
|
+
flushBuffer();
|
|
94
109
|
options.closeSync(fd);
|
|
95
110
|
};
|
|
96
111
|
return sink;
|
package/dist/filesink.base.d.cts
CHANGED
|
@@ -18,6 +18,14 @@ type FileSinkOptions = StreamSinkOptions & {
|
|
|
18
18
|
* @since 0.12.0
|
|
19
19
|
*/
|
|
20
20
|
bufferSize?: number;
|
|
21
|
+
/**
|
|
22
|
+
* The maximum time interval in milliseconds between flushes. If this time
|
|
23
|
+
* passes since the last flush, the buffer will be flushed regardless of size.
|
|
24
|
+
* This helps prevent log loss during unexpected process termination.
|
|
25
|
+
* @default 5000
|
|
26
|
+
* @since 0.12.0
|
|
27
|
+
*/
|
|
28
|
+
flushInterval?: number;
|
|
21
29
|
};
|
|
22
30
|
/**
|
|
23
31
|
* A platform-specific file sink driver.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"filesink.base.d.cts","names":[],"sources":["../filesink.base.ts"],"sourcesContent":[],"mappings":";;;;;;AAUA;
|
|
1
|
+
{"version":3,"file":"filesink.base.d.cts","names":[],"sources":["../filesink.base.ts"],"sourcesContent":[],"mappings":";;;;;;AAUA;AA6BiB,KA7BL,eAAA,GAAkB,iBA6BC,GAAA;EAAA;;;EAYV,IAAS,CAAA,EAAA,OAAA;EAAU;;AAYnB;AA0DrB;;;;EAAqD,UAAA,CAAA,EAAA,MAAA;EAepC;;;;AAAoD;;;;;;;;;UAjGpD;;;;;0BAKS;;;;;;gBAOV,cAAc;;;;;gBAMd;;;;;gBAMA;;;;;;;;;;;;;;;UA0DC,uBAAA,SAAgC,KAAK;;;;;;;;;;;;;UAerC,sCAAsC,eAAe"}
|
package/dist/filesink.base.d.ts
CHANGED
|
@@ -18,6 +18,14 @@ type FileSinkOptions = StreamSinkOptions & {
|
|
|
18
18
|
* @since 0.12.0
|
|
19
19
|
*/
|
|
20
20
|
bufferSize?: number;
|
|
21
|
+
/**
|
|
22
|
+
* The maximum time interval in milliseconds between flushes. If this time
|
|
23
|
+
* passes since the last flush, the buffer will be flushed regardless of size.
|
|
24
|
+
* This helps prevent log loss during unexpected process termination.
|
|
25
|
+
* @default 5000
|
|
26
|
+
* @since 0.12.0
|
|
27
|
+
*/
|
|
28
|
+
flushInterval?: number;
|
|
21
29
|
};
|
|
22
30
|
/**
|
|
23
31
|
* A platform-specific file sink driver.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"filesink.base.d.ts","names":[],"sources":["../filesink.base.ts"],"sourcesContent":[],"mappings":";;;;;;AAUA;
|
|
1
|
+
{"version":3,"file":"filesink.base.d.ts","names":[],"sources":["../filesink.base.ts"],"sourcesContent":[],"mappings":";;;;;;AAUA;AA6BiB,KA7BL,eAAA,GAAkB,iBA6BC,GAAA;EAAA;;;EAYV,IAAS,CAAA,EAAA,OAAA;EAAU;;AAYnB;AA0DrB;;;;EAAqD,UAAA,CAAA,EAAA,MAAA;EAepC;;;;AAAoD;;;;;;;;;UAjGpD;;;;;0BAKS;;;;;;gBAOV,cAAc;;;;;gBAMd;;;;;gBAMA;;;;;;;;;;;;;;;UA0DC,uBAAA,SAAgC,KAAK;;;;;;;;;;;;;UAerC,sCAAsC,eAAe"}
|
package/dist/filesink.base.js
CHANGED
|
@@ -14,21 +14,29 @@ function getBaseFileSink(path, options) {
|
|
|
14
14
|
const formatter = options.formatter ?? defaultTextFormatter;
|
|
15
15
|
const encoder = options.encoder ?? new TextEncoder();
|
|
16
16
|
const bufferSize = options.bufferSize ?? 1024 * 8;
|
|
17
|
+
const flushInterval = options.flushInterval ?? 5e3;
|
|
17
18
|
let fd = options.lazy ? null : options.openSync(path);
|
|
18
19
|
let buffer = "";
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (buffer.length
|
|
20
|
+
let lastFlushTimestamp = Date.now();
|
|
21
|
+
function flushBuffer() {
|
|
22
|
+
if (fd == null) return;
|
|
23
|
+
if (buffer.length > 0) {
|
|
23
24
|
options.writeSync(fd, encoder.encode(buffer));
|
|
24
25
|
buffer = "";
|
|
25
26
|
options.flushSync(fd);
|
|
27
|
+
lastFlushTimestamp = Date.now();
|
|
26
28
|
}
|
|
29
|
+
}
|
|
30
|
+
const sink = (record) => {
|
|
31
|
+
if (fd == null) fd = options.openSync(path);
|
|
32
|
+
buffer += formatter(record);
|
|
33
|
+
const shouldFlushBySize = buffer.length >= bufferSize;
|
|
34
|
+
const shouldFlushByTime = flushInterval > 0 && record.timestamp - lastFlushTimestamp >= flushInterval;
|
|
35
|
+
if (shouldFlushBySize || shouldFlushByTime) flushBuffer();
|
|
27
36
|
};
|
|
28
37
|
sink[Symbol.dispose] = () => {
|
|
29
38
|
if (fd !== null) {
|
|
30
|
-
|
|
31
|
-
options.flushSync(fd);
|
|
39
|
+
flushBuffer();
|
|
32
40
|
options.closeSync(fd);
|
|
33
41
|
}
|
|
34
42
|
};
|
|
@@ -53,12 +61,14 @@ function getBaseRotatingFileSink(path, options) {
|
|
|
53
61
|
const maxSize = options.maxSize ?? 1024 * 1024;
|
|
54
62
|
const maxFiles = options.maxFiles ?? 5;
|
|
55
63
|
const bufferSize = options.bufferSize ?? 1024 * 8;
|
|
64
|
+
const flushInterval = options.flushInterval ?? 5e3;
|
|
56
65
|
let offset = 0;
|
|
57
66
|
try {
|
|
58
67
|
const stat = options.statSync(path);
|
|
59
68
|
offset = stat.size;
|
|
60
69
|
} catch {}
|
|
61
70
|
let fd = options.openSync(path);
|
|
71
|
+
let lastFlushTimestamp = Date.now();
|
|
62
72
|
function shouldRollover(bytes) {
|
|
63
73
|
return offset + bytes.length > maxSize;
|
|
64
74
|
}
|
|
@@ -75,21 +85,26 @@ function getBaseRotatingFileSink(path, options) {
|
|
|
75
85
|
offset = 0;
|
|
76
86
|
fd = options.openSync(path);
|
|
77
87
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
buffer += formatter(record);
|
|
81
|
-
if (buffer.length >= bufferSize) {
|
|
88
|
+
function flushBuffer() {
|
|
89
|
+
if (buffer.length > 0) {
|
|
82
90
|
const bytes = encoder.encode(buffer);
|
|
83
91
|
buffer = "";
|
|
84
92
|
if (shouldRollover(bytes)) performRollover();
|
|
85
93
|
options.writeSync(fd, bytes);
|
|
86
94
|
options.flushSync(fd);
|
|
87
95
|
offset += bytes.length;
|
|
96
|
+
lastFlushTimestamp = Date.now();
|
|
88
97
|
}
|
|
98
|
+
}
|
|
99
|
+
let buffer = "";
|
|
100
|
+
const sink = (record) => {
|
|
101
|
+
buffer += formatter(record);
|
|
102
|
+
const shouldFlushBySize = buffer.length >= bufferSize;
|
|
103
|
+
const shouldFlushByTime = flushInterval > 0 && record.timestamp - lastFlushTimestamp >= flushInterval;
|
|
104
|
+
if (shouldFlushBySize || shouldFlushByTime) flushBuffer();
|
|
89
105
|
};
|
|
90
106
|
sink[Symbol.dispose] = () => {
|
|
91
|
-
|
|
92
|
-
options.flushSync(fd);
|
|
107
|
+
flushBuffer();
|
|
93
108
|
options.closeSync(fd);
|
|
94
109
|
};
|
|
95
110
|
return sink;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"filesink.base.js","names":["path: string","options: FileSinkOptions & FileSinkDriver<TFile>","buffer: string","sink: Sink & Disposable","record: LogRecord","options: RotatingFileSinkOptions & RotatingFileSinkDriver<TFile>","offset: number","bytes: Uint8Array"],"sources":["../filesink.base.ts"],"sourcesContent":["import {\n defaultTextFormatter,\n type LogRecord,\n type Sink,\n type StreamSinkOptions,\n} from \"@logtape/logtape\";\n\n/**\n * Options for the {@link getBaseFileSink} function.\n */\nexport type FileSinkOptions = StreamSinkOptions & {\n /**\n * If `true`, the file is not opened until the first write. Defaults to `false`.\n */\n lazy?: boolean;\n\n /**\n * The size of the buffer to use when writing to the file. If not specified,\n * a default buffer size will be used. If it is less or equal to 0,\n * the file will be written directly without buffering.\n * @default 8192\n * @since 0.12.0\n */\n bufferSize?: number;\n};\n\n/**\n * A platform-specific file sink driver.\n * @typeParam TFile The type of the file descriptor.\n */\nexport interface FileSinkDriver<TFile> {\n /**\n * Open a file for appending and return a file descriptor.\n * @param path A path to the file to open.\n */\n openSync(path: string): TFile;\n\n /**\n * Write a chunk of data to the file.\n * @param fd The file descriptor.\n * @param chunk The data to write.\n */\n writeSync(fd: TFile, chunk: Uint8Array): void;\n\n /**\n * Flush the file to ensure that all data is written to the disk.\n * @param fd The file descriptor.\n */\n flushSync(fd: TFile): void;\n\n /**\n * Close the file.\n * @param fd The file descriptor.\n */\n closeSync(fd: TFile): void;\n}\n\n/**\n * Get a platform-independent file sink.\n *\n * @typeParam TFile The type of the file descriptor.\n * @param path A path to the file to write to.\n * @param options The options for the sink and the file driver.\n * @returns A sink that writes to the file. The sink is also a disposable\n * object that closes the file when disposed.\n */\nexport function getBaseFileSink<TFile>(\n path: string,\n options: FileSinkOptions & FileSinkDriver<TFile>,\n): Sink & Disposable {\n const formatter = options.formatter ?? defaultTextFormatter;\n const encoder = options.encoder ?? new TextEncoder();\n const bufferSize = options.bufferSize ?? 1024 * 8; // Default buffer size of 8192 chars\n let fd = options.lazy ? null : options.openSync(path);\n let buffer: string = \"\";\n const sink: Sink & Disposable = (record: LogRecord) => {\n if (fd == null) fd = options.openSync(path);\n buffer += formatter(record);\n
|
|
1
|
+
{"version":3,"file":"filesink.base.js","names":["path: string","options: FileSinkOptions & FileSinkDriver<TFile>","buffer: string","lastFlushTimestamp: number","sink: Sink & Disposable","record: LogRecord","options: RotatingFileSinkOptions & RotatingFileSinkDriver<TFile>","offset: number","bytes: Uint8Array"],"sources":["../filesink.base.ts"],"sourcesContent":["import {\n defaultTextFormatter,\n type LogRecord,\n type Sink,\n type StreamSinkOptions,\n} from \"@logtape/logtape\";\n\n/**\n * Options for the {@link getBaseFileSink} function.\n */\nexport type FileSinkOptions = StreamSinkOptions & {\n /**\n * If `true`, the file is not opened until the first write. Defaults to `false`.\n */\n lazy?: boolean;\n\n /**\n * The size of the buffer to use when writing to the file. If not specified,\n * a default buffer size will be used. If it is less or equal to 0,\n * the file will be written directly without buffering.\n * @default 8192\n * @since 0.12.0\n */\n bufferSize?: number;\n\n /**\n * The maximum time interval in milliseconds between flushes. If this time\n * passes since the last flush, the buffer will be flushed regardless of size.\n * This helps prevent log loss during unexpected process termination.\n * @default 5000\n * @since 0.12.0\n */\n flushInterval?: number;\n};\n\n/**\n * A platform-specific file sink driver.\n * @typeParam TFile The type of the file descriptor.\n */\nexport interface FileSinkDriver<TFile> {\n /**\n * Open a file for appending and return a file descriptor.\n * @param path A path to the file to open.\n */\n openSync(path: string): TFile;\n\n /**\n * Write a chunk of data to the file.\n * @param fd The file descriptor.\n * @param chunk The data to write.\n */\n writeSync(fd: TFile, chunk: Uint8Array): void;\n\n /**\n * Flush the file to ensure that all data is written to the disk.\n * @param fd The file descriptor.\n */\n flushSync(fd: TFile): void;\n\n /**\n * Close the file.\n * @param fd The file descriptor.\n */\n closeSync(fd: TFile): void;\n}\n\n/**\n * Get a platform-independent file sink.\n *\n * @typeParam TFile The type of the file descriptor.\n * @param path A path to the file to write to.\n * @param options The options for the sink and the file driver.\n * @returns A sink that writes to the file. The sink is also a disposable\n * object that closes the file when disposed.\n */\nexport function getBaseFileSink<TFile>(\n path: string,\n options: FileSinkOptions & FileSinkDriver<TFile>,\n): Sink & Disposable {\n const formatter = options.formatter ?? defaultTextFormatter;\n const encoder = options.encoder ?? new TextEncoder();\n const bufferSize = options.bufferSize ?? 1024 * 8; // Default buffer size of 8192 chars\n const flushInterval = options.flushInterval ?? 5000; // Default flush interval of 5 seconds\n let fd = options.lazy ? null : options.openSync(path);\n let buffer: string = \"\";\n let lastFlushTimestamp: number = Date.now();\n\n function flushBuffer(): void {\n if (fd == null) return;\n if (buffer.length > 0) {\n options.writeSync(fd, encoder.encode(buffer));\n buffer = \"\";\n options.flushSync(fd);\n lastFlushTimestamp = Date.now();\n }\n }\n\n const sink: Sink & Disposable = (record: LogRecord) => {\n if (fd == null) fd = options.openSync(path);\n buffer += formatter(record);\n\n const shouldFlushBySize = buffer.length >= bufferSize;\n const shouldFlushByTime = flushInterval > 0 &&\n (record.timestamp - lastFlushTimestamp) >= flushInterval;\n\n if (shouldFlushBySize || shouldFlushByTime) {\n flushBuffer();\n }\n };\n sink[Symbol.dispose] = () => {\n if (fd !== null) {\n flushBuffer();\n options.closeSync(fd);\n }\n };\n return sink;\n}\n\n/**\n * Options for the {@link getBaseRotatingFileSink} function.\n */\nexport interface RotatingFileSinkOptions extends Omit<FileSinkOptions, \"lazy\"> {\n /**\n * The maximum bytes of the file before it is rotated. 1 MiB by default.\n */\n maxSize?: number;\n\n /**\n * The maximum number of files to keep. 5 by default.\n */\n maxFiles?: number;\n}\n\n/**\n * A platform-specific rotating file sink driver.\n */\nexport interface RotatingFileSinkDriver<TFile> extends FileSinkDriver<TFile> {\n /**\n * Get the size of the file.\n * @param path A path to the file.\n * @returns The `size` of the file in bytes, in an object.\n */\n statSync(path: string): { size: number };\n\n /**\n * Rename a file.\n * @param oldPath A path to the file to rename.\n * @param newPath A path to be renamed to.\n */\n renameSync(oldPath: string, newPath: string): void;\n}\n\n/**\n * Get a platform-independent rotating file sink.\n *\n * This sink writes log records to a file, and rotates the file when it reaches\n * the `maxSize`. The rotated files are named with the original file name\n * followed by a dot and a number, starting from 1. The number is incremented\n * for each rotation, and the maximum number of files to keep is `maxFiles`.\n *\n * @param path A path to the file to write to.\n * @param options The options for the sink and the file driver.\n * @returns A sink that writes to the file. The sink is also a disposable\n * object that closes the file when disposed.\n */\nexport function getBaseRotatingFileSink<TFile>(\n path: string,\n options: RotatingFileSinkOptions & RotatingFileSinkDriver<TFile>,\n): Sink & Disposable {\n const formatter = options.formatter ?? defaultTextFormatter;\n const encoder = options.encoder ?? new TextEncoder();\n const maxSize = options.maxSize ?? 1024 * 1024;\n const maxFiles = options.maxFiles ?? 5;\n const bufferSize = options.bufferSize ?? 1024 * 8; // Default buffer size of 8192 chars\n const flushInterval = options.flushInterval ?? 5000; // Default flush interval of 5 seconds\n let offset: number = 0;\n try {\n const stat = options.statSync(path);\n offset = stat.size;\n } catch {\n // Continue as the offset is already 0.\n }\n let fd = options.openSync(path);\n let lastFlushTimestamp: number = Date.now();\n\n function shouldRollover(bytes: Uint8Array): boolean {\n return offset + bytes.length > maxSize;\n }\n function performRollover(): void {\n options.closeSync(fd);\n for (let i = maxFiles - 1; i > 0; i--) {\n const oldPath = `${path}.${i}`;\n const newPath = `${path}.${i + 1}`;\n try {\n options.renameSync(oldPath, newPath);\n } catch (_) {\n // Continue if the file does not exist.\n }\n }\n options.renameSync(path, `${path}.1`);\n offset = 0;\n fd = options.openSync(path);\n }\n\n function flushBuffer(): void {\n if (buffer.length > 0) {\n const bytes = encoder.encode(buffer);\n buffer = \"\";\n if (shouldRollover(bytes)) performRollover();\n options.writeSync(fd, bytes);\n options.flushSync(fd);\n offset += bytes.length;\n lastFlushTimestamp = Date.now();\n }\n }\n\n let buffer: string = \"\";\n const sink: Sink & Disposable = (record: LogRecord) => {\n buffer += formatter(record);\n\n const shouldFlushBySize = buffer.length >= bufferSize;\n const shouldFlushByTime = flushInterval > 0 &&\n (record.timestamp - lastFlushTimestamp) >= flushInterval;\n\n if (shouldFlushBySize || shouldFlushByTime) {\n flushBuffer();\n }\n };\n sink[Symbol.dispose] = () => {\n flushBuffer();\n options.closeSync(fd);\n };\n return sink;\n}\n"],"mappings":";;;;;;;;;;;;AA2EA,SAAgB,gBACdA,MACAC,SACmB;CACnB,MAAM,YAAY,QAAQ,aAAa;CACvC,MAAM,UAAU,QAAQ,WAAW,IAAI;CACvC,MAAM,aAAa,QAAQ,cAAc,OAAO;CAChD,MAAM,gBAAgB,QAAQ,iBAAiB;CAC/C,IAAI,KAAK,QAAQ,OAAO,OAAO,QAAQ,SAAS,KAAK;CACrD,IAAIC,SAAiB;CACrB,IAAIC,qBAA6B,KAAK,KAAK;CAE3C,SAAS,cAAoB;AAC3B,MAAI,MAAM,KAAM;AAChB,MAAI,OAAO,SAAS,GAAG;AACrB,WAAQ,UAAU,IAAI,QAAQ,OAAO,OAAO,CAAC;AAC7C,YAAS;AACT,WAAQ,UAAU,GAAG;AACrB,wBAAqB,KAAK,KAAK;EAChC;CACF;CAED,MAAMC,OAA0B,CAACC,WAAsB;AACrD,MAAI,MAAM,KAAM,MAAK,QAAQ,SAAS,KAAK;AAC3C,YAAU,UAAU,OAAO;EAE3B,MAAM,oBAAoB,OAAO,UAAU;EAC3C,MAAM,oBAAoB,gBAAgB,KACvC,OAAO,YAAY,sBAAuB;AAE7C,MAAI,qBAAqB,kBACvB,cAAa;CAEhB;AACD,MAAK,OAAO,WAAW,MAAM;AAC3B,MAAI,OAAO,MAAM;AACf,gBAAa;AACb,WAAQ,UAAU,GAAG;EACtB;CACF;AACD,QAAO;AACR;;;;;;;;;;;;;;AAiDD,SAAgB,wBACdL,MACAM,SACmB;CACnB,MAAM,YAAY,QAAQ,aAAa;CACvC,MAAM,UAAU,QAAQ,WAAW,IAAI;CACvC,MAAM,UAAU,QAAQ,WAAW,OAAO;CAC1C,MAAM,WAAW,QAAQ,YAAY;CACrC,MAAM,aAAa,QAAQ,cAAc,OAAO;CAChD,MAAM,gBAAgB,QAAQ,iBAAiB;CAC/C,IAAIC,SAAiB;AACrB,KAAI;EACF,MAAM,OAAO,QAAQ,SAAS,KAAK;AACnC,WAAS,KAAK;CACf,QAAO,CAEP;CACD,IAAI,KAAK,QAAQ,SAAS,KAAK;CAC/B,IAAIJ,qBAA6B,KAAK,KAAK;CAE3C,SAAS,eAAeK,OAA4B;AAClD,SAAO,SAAS,MAAM,SAAS;CAChC;CACD,SAAS,kBAAwB;AAC/B,UAAQ,UAAU,GAAG;AACrB,OAAK,IAAI,IAAI,WAAW,GAAG,IAAI,GAAG,KAAK;GACrC,MAAM,WAAW,EAAE,KAAK,GAAG,EAAE;GAC7B,MAAM,WAAW,EAAE,KAAK,GAAG,IAAI,EAAE;AACjC,OAAI;AACF,YAAQ,WAAW,SAAS,QAAQ;GACrC,SAAQ,GAAG,CAEX;EACF;AACD,UAAQ,WAAW,OAAO,EAAE,KAAK,IAAI;AACrC,WAAS;AACT,OAAK,QAAQ,SAAS,KAAK;CAC5B;CAED,SAAS,cAAoB;AAC3B,MAAI,OAAO,SAAS,GAAG;GACrB,MAAM,QAAQ,QAAQ,OAAO,OAAO;AACpC,YAAS;AACT,OAAI,eAAe,MAAM,CAAE,kBAAiB;AAC5C,WAAQ,UAAU,IAAI,MAAM;AAC5B,WAAQ,UAAU,GAAG;AACrB,aAAU,MAAM;AAChB,wBAAqB,KAAK,KAAK;EAChC;CACF;CAED,IAAIN,SAAiB;CACrB,MAAME,OAA0B,CAACC,WAAsB;AACrD,YAAU,UAAU,OAAO;EAE3B,MAAM,oBAAoB,OAAO,UAAU;EAC3C,MAAM,oBAAoB,gBAAgB,KACvC,OAAO,YAAY,sBAAuB;AAE7C,MAAI,qBAAqB,kBACvB,cAAa;CAEhB;AACD,MAAK,OAAO,WAAW,MAAM;AAC3B,eAAa;AACb,UAAQ,UAAU,GAAG;CACtB;AACD,QAAO;AACR"}
|
package/filesink.base.ts
CHANGED
|
@@ -22,6 +22,15 @@ export type FileSinkOptions = StreamSinkOptions & {
|
|
|
22
22
|
* @since 0.12.0
|
|
23
23
|
*/
|
|
24
24
|
bufferSize?: number;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The maximum time interval in milliseconds between flushes. If this time
|
|
28
|
+
* passes since the last flush, the buffer will be flushed regardless of size.
|
|
29
|
+
* This helps prevent log loss during unexpected process termination.
|
|
30
|
+
* @default 5000
|
|
31
|
+
* @since 0.12.0
|
|
32
|
+
*/
|
|
33
|
+
flushInterval?: number;
|
|
25
34
|
};
|
|
26
35
|
|
|
27
36
|
/**
|
|
@@ -71,21 +80,36 @@ export function getBaseFileSink<TFile>(
|
|
|
71
80
|
const formatter = options.formatter ?? defaultTextFormatter;
|
|
72
81
|
const encoder = options.encoder ?? new TextEncoder();
|
|
73
82
|
const bufferSize = options.bufferSize ?? 1024 * 8; // Default buffer size of 8192 chars
|
|
83
|
+
const flushInterval = options.flushInterval ?? 5000; // Default flush interval of 5 seconds
|
|
74
84
|
let fd = options.lazy ? null : options.openSync(path);
|
|
75
85
|
let buffer: string = "";
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (
|
|
86
|
+
let lastFlushTimestamp: number = Date.now();
|
|
87
|
+
|
|
88
|
+
function flushBuffer(): void {
|
|
89
|
+
if (fd == null) return;
|
|
90
|
+
if (buffer.length > 0) {
|
|
80
91
|
options.writeSync(fd, encoder.encode(buffer));
|
|
81
92
|
buffer = "";
|
|
82
93
|
options.flushSync(fd);
|
|
94
|
+
lastFlushTimestamp = Date.now();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const sink: Sink & Disposable = (record: LogRecord) => {
|
|
99
|
+
if (fd == null) fd = options.openSync(path);
|
|
100
|
+
buffer += formatter(record);
|
|
101
|
+
|
|
102
|
+
const shouldFlushBySize = buffer.length >= bufferSize;
|
|
103
|
+
const shouldFlushByTime = flushInterval > 0 &&
|
|
104
|
+
(record.timestamp - lastFlushTimestamp) >= flushInterval;
|
|
105
|
+
|
|
106
|
+
if (shouldFlushBySize || shouldFlushByTime) {
|
|
107
|
+
flushBuffer();
|
|
83
108
|
}
|
|
84
109
|
};
|
|
85
110
|
sink[Symbol.dispose] = () => {
|
|
86
111
|
if (fd !== null) {
|
|
87
|
-
|
|
88
|
-
options.flushSync(fd);
|
|
112
|
+
flushBuffer();
|
|
89
113
|
options.closeSync(fd);
|
|
90
114
|
}
|
|
91
115
|
};
|
|
@@ -148,6 +172,7 @@ export function getBaseRotatingFileSink<TFile>(
|
|
|
148
172
|
const maxSize = options.maxSize ?? 1024 * 1024;
|
|
149
173
|
const maxFiles = options.maxFiles ?? 5;
|
|
150
174
|
const bufferSize = options.bufferSize ?? 1024 * 8; // Default buffer size of 8192 chars
|
|
175
|
+
const flushInterval = options.flushInterval ?? 5000; // Default flush interval of 5 seconds
|
|
151
176
|
let offset: number = 0;
|
|
152
177
|
try {
|
|
153
178
|
const stat = options.statSync(path);
|
|
@@ -156,6 +181,8 @@ export function getBaseRotatingFileSink<TFile>(
|
|
|
156
181
|
// Continue as the offset is already 0.
|
|
157
182
|
}
|
|
158
183
|
let fd = options.openSync(path);
|
|
184
|
+
let lastFlushTimestamp: number = Date.now();
|
|
185
|
+
|
|
159
186
|
function shouldRollover(bytes: Uint8Array): boolean {
|
|
160
187
|
return offset + bytes.length > maxSize;
|
|
161
188
|
}
|
|
@@ -174,21 +201,33 @@ export function getBaseRotatingFileSink<TFile>(
|
|
|
174
201
|
offset = 0;
|
|
175
202
|
fd = options.openSync(path);
|
|
176
203
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
buffer
|
|
180
|
-
if (buffer.length >= bufferSize) {
|
|
204
|
+
|
|
205
|
+
function flushBuffer(): void {
|
|
206
|
+
if (buffer.length > 0) {
|
|
181
207
|
const bytes = encoder.encode(buffer);
|
|
182
208
|
buffer = "";
|
|
183
209
|
if (shouldRollover(bytes)) performRollover();
|
|
184
210
|
options.writeSync(fd, bytes);
|
|
185
211
|
options.flushSync(fd);
|
|
186
212
|
offset += bytes.length;
|
|
213
|
+
lastFlushTimestamp = Date.now();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
let buffer: string = "";
|
|
218
|
+
const sink: Sink & Disposable = (record: LogRecord) => {
|
|
219
|
+
buffer += formatter(record);
|
|
220
|
+
|
|
221
|
+
const shouldFlushBySize = buffer.length >= bufferSize;
|
|
222
|
+
const shouldFlushByTime = flushInterval > 0 &&
|
|
223
|
+
(record.timestamp - lastFlushTimestamp) >= flushInterval;
|
|
224
|
+
|
|
225
|
+
if (shouldFlushBySize || shouldFlushByTime) {
|
|
226
|
+
flushBuffer();
|
|
187
227
|
}
|
|
188
228
|
};
|
|
189
229
|
sink[Symbol.dispose] = () => {
|
|
190
|
-
|
|
191
|
-
options.flushSync(fd);
|
|
230
|
+
flushBuffer();
|
|
192
231
|
options.closeSync(fd);
|
|
193
232
|
};
|
|
194
233
|
return sink;
|
package/filesink.test.ts
CHANGED
|
@@ -550,4 +550,145 @@ test("getBaseFileSink() with buffer edge cases", () => {
|
|
|
550
550
|
);
|
|
551
551
|
});
|
|
552
552
|
|
|
553
|
+
test("getBaseFileSink() with time-based flushing", async () => {
|
|
554
|
+
const path = makeTempFileSync();
|
|
555
|
+
let sink: Sink & Disposable;
|
|
556
|
+
if (isDeno) {
|
|
557
|
+
const driver: FileSinkDriver<Deno.FsFile> = {
|
|
558
|
+
openSync(path: string) {
|
|
559
|
+
return Deno.openSync(path, { create: true, append: true });
|
|
560
|
+
},
|
|
561
|
+
writeSync(fd, chunk) {
|
|
562
|
+
fd.writeSync(chunk);
|
|
563
|
+
},
|
|
564
|
+
flushSync(fd) {
|
|
565
|
+
fd.syncSync();
|
|
566
|
+
},
|
|
567
|
+
closeSync(fd) {
|
|
568
|
+
fd.close();
|
|
569
|
+
},
|
|
570
|
+
};
|
|
571
|
+
sink = getBaseFileSink(path, {
|
|
572
|
+
...driver,
|
|
573
|
+
bufferSize: 1000, // Large buffer to prevent size-based flushing
|
|
574
|
+
flushInterval: 100, // 100ms flush interval for testing
|
|
575
|
+
});
|
|
576
|
+
} else {
|
|
577
|
+
const driver: FileSinkDriver<number> = {
|
|
578
|
+
openSync(path: string) {
|
|
579
|
+
return fs.openSync(path, "a");
|
|
580
|
+
},
|
|
581
|
+
writeSync: fs.writeSync,
|
|
582
|
+
flushSync: fs.fsyncSync,
|
|
583
|
+
closeSync: fs.closeSync,
|
|
584
|
+
};
|
|
585
|
+
sink = getBaseFileSink(path, {
|
|
586
|
+
...driver,
|
|
587
|
+
bufferSize: 1000, // Large buffer to prevent size-based flushing
|
|
588
|
+
flushInterval: 100, // 100ms flush interval for testing
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Create a log record with current timestamp
|
|
593
|
+
const record1 = { ...debug, timestamp: Date.now() };
|
|
594
|
+
sink(record1);
|
|
595
|
+
|
|
596
|
+
// Should be buffered (file empty initially)
|
|
597
|
+
assertEquals(fs.readFileSync(path, { encoding: "utf-8" }), "");
|
|
598
|
+
|
|
599
|
+
// Wait for flush interval to pass and write another record
|
|
600
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
601
|
+
const record2 = { ...info, timestamp: Date.now() };
|
|
602
|
+
sink(record2);
|
|
603
|
+
|
|
604
|
+
// First record should now be flushed due to time interval
|
|
605
|
+
const content = fs.readFileSync(path, { encoding: "utf-8" });
|
|
606
|
+
assertEquals(content.includes("Hello, 123 & 456!"), true);
|
|
607
|
+
|
|
608
|
+
sink[Symbol.dispose]();
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
test("getRotatingFileSink() with time-based flushing", async () => {
|
|
612
|
+
const path = makeTempFileSync();
|
|
613
|
+
const sink: Sink & Disposable = getRotatingFileSink(path, {
|
|
614
|
+
maxSize: 1024 * 1024, // Large maxSize to prevent rotation
|
|
615
|
+
bufferSize: 1000, // Large buffer to prevent size-based flushing
|
|
616
|
+
flushInterval: 100, // 100ms flush interval for testing
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
// Create a log record with current timestamp
|
|
620
|
+
const record1 = { ...debug, timestamp: Date.now() };
|
|
621
|
+
sink(record1);
|
|
622
|
+
|
|
623
|
+
// Should be buffered (file empty initially)
|
|
624
|
+
assertEquals(fs.readFileSync(path, { encoding: "utf-8" }), "");
|
|
625
|
+
|
|
626
|
+
// Wait for flush interval to pass and write another record
|
|
627
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
628
|
+
const record2 = { ...info, timestamp: Date.now() };
|
|
629
|
+
sink(record2);
|
|
630
|
+
|
|
631
|
+
// First record should now be flushed due to time interval
|
|
632
|
+
const content = fs.readFileSync(path, { encoding: "utf-8" });
|
|
633
|
+
assertEquals(content.includes("Hello, 123 & 456!"), true);
|
|
634
|
+
|
|
635
|
+
sink[Symbol.dispose]();
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
test("getBaseFileSink() with flushInterval disabled", () => {
|
|
639
|
+
const path = makeTempFileSync();
|
|
640
|
+
let sink: Sink & Disposable;
|
|
641
|
+
if (isDeno) {
|
|
642
|
+
const driver: FileSinkDriver<Deno.FsFile> = {
|
|
643
|
+
openSync(path: string) {
|
|
644
|
+
return Deno.openSync(path, { create: true, append: true });
|
|
645
|
+
},
|
|
646
|
+
writeSync(fd, chunk) {
|
|
647
|
+
fd.writeSync(chunk);
|
|
648
|
+
},
|
|
649
|
+
flushSync(fd) {
|
|
650
|
+
fd.syncSync();
|
|
651
|
+
},
|
|
652
|
+
closeSync(fd) {
|
|
653
|
+
fd.close();
|
|
654
|
+
},
|
|
655
|
+
};
|
|
656
|
+
sink = getBaseFileSink(path, {
|
|
657
|
+
...driver,
|
|
658
|
+
bufferSize: 1000, // Large buffer to prevent size-based flushing
|
|
659
|
+
flushInterval: 0, // Disable time-based flushing
|
|
660
|
+
});
|
|
661
|
+
} else {
|
|
662
|
+
const driver: FileSinkDriver<number> = {
|
|
663
|
+
openSync(path: string) {
|
|
664
|
+
return fs.openSync(path, "a");
|
|
665
|
+
},
|
|
666
|
+
writeSync: fs.writeSync,
|
|
667
|
+
flushSync: fs.fsyncSync,
|
|
668
|
+
closeSync: fs.closeSync,
|
|
669
|
+
};
|
|
670
|
+
sink = getBaseFileSink(path, {
|
|
671
|
+
...driver,
|
|
672
|
+
bufferSize: 1000, // Large buffer to prevent size-based flushing
|
|
673
|
+
flushInterval: 0, // Disable time-based flushing
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Create log records with simulated time gap
|
|
678
|
+
const now = Date.now();
|
|
679
|
+
const record1 = { ...debug, timestamp: now };
|
|
680
|
+
const record2 = { ...info, timestamp: now + 10000 }; // 10 seconds later
|
|
681
|
+
|
|
682
|
+
sink(record1);
|
|
683
|
+
sink(record2);
|
|
684
|
+
|
|
685
|
+
// Should still be buffered since time-based flushing is disabled
|
|
686
|
+
assertEquals(fs.readFileSync(path, { encoding: "utf-8" }), "");
|
|
687
|
+
|
|
688
|
+
// Only disposal should flush
|
|
689
|
+
sink[Symbol.dispose]();
|
|
690
|
+
const content = fs.readFileSync(path, { encoding: "utf-8" });
|
|
691
|
+
assertEquals(content.includes("Hello, 123 & 456!"), true);
|
|
692
|
+
});
|
|
693
|
+
|
|
553
694
|
// cSpell: ignore filesink
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@logtape/file",
|
|
3
|
-
"version": "0.12.0-dev.
|
|
3
|
+
"version": "0.12.0-dev.196+8684bab0",
|
|
4
4
|
"description": "File sink and rotating file sink for LogTape",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"logging",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
}
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
|
53
|
-
"@logtape/logtape": "0.12.0-dev.
|
|
53
|
+
"@logtape/logtape": "0.12.0-dev.196+8684bab0"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@david/which-runtime": "npm:@jsr/david__which-runtime@^0.2.1",
|