@logtape/file 1.0.5 → 1.0.6
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/dist/filesink.base.d.cts +74 -0
- package/dist/dist/filesink.base.d.cts.map +1 -0
- package/dist/dist/filesink.node.d.cts +45 -0
- package/dist/dist/filesink.node.d.cts.map +1 -0
- package/dist/mod.d.cts +1 -1
- package/dist/streamfilesink.cjs +10 -3
- package/dist/streamfilesink.d.cts +3 -2
- package/dist/streamfilesink.d.cts.map +1 -1
- package/dist/streamfilesink.d.ts +3 -2
- package/dist/streamfilesink.d.ts.map +1 -1
- package/dist/streamfilesink.js +10 -3
- package/dist/streamfilesink.js.map +1 -1
- package/package.json +2 -2
- package/streamfilesink.test.ts +20 -47
- package/streamfilesink.ts +17 -6
package/deno.json
CHANGED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { StreamSinkOptions } from "@logtape/logtape";
|
|
2
|
+
|
|
3
|
+
//#region dist/filesink.base.d.ts
|
|
4
|
+
|
|
5
|
+
//#region filesink.base.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* Options for the {@link getBaseFileSink} function.
|
|
8
|
+
*/
|
|
9
|
+
type FileSinkOptions = StreamSinkOptions & {
|
|
10
|
+
/**
|
|
11
|
+
* If `true`, the file is not opened until the first write. Defaults to `false`.
|
|
12
|
+
*/
|
|
13
|
+
lazy?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* The size of the buffer to use when writing to the file. If not specified,
|
|
16
|
+
* a default buffer size will be used. If it is less or equal to 0,
|
|
17
|
+
* the file will be written directly without buffering.
|
|
18
|
+
* @default 8192
|
|
19
|
+
* @since 0.12.0
|
|
20
|
+
*/
|
|
21
|
+
bufferSize?: number;
|
|
22
|
+
/**
|
|
23
|
+
* The maximum time interval in milliseconds between flushes. If this time
|
|
24
|
+
* passes since the last flush, the buffer will be flushed regardless of size.
|
|
25
|
+
* This helps prevent log loss during unexpected process termination.
|
|
26
|
+
* @default 5000
|
|
27
|
+
* @since 0.12.0
|
|
28
|
+
*/
|
|
29
|
+
flushInterval?: number;
|
|
30
|
+
/**
|
|
31
|
+
* Enable non-blocking mode with background flushing.
|
|
32
|
+
* When enabled, flush operations are performed asynchronously to prevent
|
|
33
|
+
* blocking the main thread during file I/O operations.
|
|
34
|
+
*
|
|
35
|
+
* @default `false`
|
|
36
|
+
* @since 1.0.0
|
|
37
|
+
*/
|
|
38
|
+
nonBlocking?: boolean;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* A platform-specific file sink driver.
|
|
42
|
+
* @typeParam TFile The type of the file descriptor.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get a platform-independent file sink.
|
|
47
|
+
*
|
|
48
|
+
* @typeParam TFile The type of the file descriptor.
|
|
49
|
+
* @param path A path to the file to write to.
|
|
50
|
+
* @param options The options for the sink and the file driver.
|
|
51
|
+
* @returns A sink that writes to the file. The sink is also a disposable
|
|
52
|
+
* object that closes the file when disposed. If `nonBlocking` is enabled,
|
|
53
|
+
* returns a sink that also implements {@link AsyncDisposable}.
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Options for the {@link getBaseRotatingFileSink} function.
|
|
58
|
+
*/
|
|
59
|
+
interface RotatingFileSinkOptions extends Omit<FileSinkOptions, "lazy"> {
|
|
60
|
+
/**
|
|
61
|
+
* The maximum bytes of the file before it is rotated. 1 MiB by default.
|
|
62
|
+
*/
|
|
63
|
+
maxSize?: number;
|
|
64
|
+
/**
|
|
65
|
+
* The maximum number of files to keep. 5 by default.
|
|
66
|
+
*/
|
|
67
|
+
maxFiles?: number;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* A platform-specific rotating file sink driver.
|
|
71
|
+
*/
|
|
72
|
+
//#endregion
|
|
73
|
+
export { FileSinkOptions, RotatingFileSinkOptions };
|
|
74
|
+
//# sourceMappingURL=filesink.base.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filesink.base.d.cts","names":["Sink","StreamSinkOptions","FileSinkOptions","FileSinkDriver","TFile","Uint8Array","AsyncFileSinkDriver","Promise","RotatingFileSinkOptions","Omit","RotatingFileSinkDriver","AsyncRotatingFileSinkDriver"],"sources":["../filesink.base.d.ts"],"sourcesContent":["import { Sink, StreamSinkOptions } from \"@logtape/logtape\";\n\n//#region filesink.base.d.ts\n\n/**\n * Options for the {@link getBaseFileSink} function.\n */\ntype FileSinkOptions = StreamSinkOptions & {\n /**\n * If `true`, the file is not opened until the first write. Defaults to `false`.\n */\n lazy?: boolean;\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 * 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 * Enable non-blocking mode with background flushing.\n * When enabled, flush operations are performed asynchronously to prevent\n * blocking the main thread during file I/O operations.\n *\n * @default `false`\n * @since 1.0.0\n */\n nonBlocking?: boolean;\n};\n/**\n * A platform-specific file sink driver.\n * @typeParam TFile The type of the file descriptor.\n */\ninterface 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 * 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 * Write multiple chunks of data to the file in a single operation.\n * This is optional - if not implemented, falls back to multiple writeSync calls.\n * @param fd The file descriptor.\n * @param chunks Array of data chunks to write.\n */\n writeManySync?(fd: TFile, chunks: Uint8Array[]): void;\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 * Close the file.\n * @param fd The file descriptor.\n */\n closeSync(fd: TFile): void;\n}\n/**\n * A platform-specific async file sink driver.\n * @typeParam TFile The type of the file descriptor.\n * @since 1.0.0\n */\ninterface AsyncFileSinkDriver<TFile> extends FileSinkDriver<TFile> {\n /**\n * Asynchronously write multiple chunks of data to the file in a single operation.\n * This is optional - if not implemented, falls back to multiple writeSync calls.\n * @param fd The file descriptor.\n * @param chunks Array of data chunks to write.\n */\n writeMany?(fd: TFile, chunks: Uint8Array[]): Promise<void>;\n /**\n * Asynchronously flush the file to ensure that all data is written to the disk.\n * @param fd The file descriptor.\n */\n flush(fd: TFile): Promise<void>;\n /**\n * Asynchronously close the file.\n * @param fd The file descriptor.\n */\n close(fd: TFile): Promise<void>;\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. If `nonBlocking` is enabled,\n * returns a sink that also implements {@link AsyncDisposable}.\n */\n\n/**\n * Options for the {@link getBaseRotatingFileSink} function.\n */\ninterface 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 * The maximum number of files to keep. 5 by default.\n */\n maxFiles?: number;\n}\n/**\n * A platform-specific rotating file sink driver.\n */\ninterface 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): {\n 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 * A platform-specific async rotating file sink driver.\n * @since 1.0.0\n */\ninterface AsyncRotatingFileSinkDriver<TFile> extends AsyncFileSinkDriver<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): {\n 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 * 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. If `nonBlocking` is enabled,\n * returns a sink that also implements {@link AsyncDisposable}.\n */\n//#endregion\nexport { AsyncRotatingFileSinkDriver, FileSinkDriver, FileSinkOptions, RotatingFileSinkDriver, RotatingFileSinkOptions };\n//# sourceMappingURL=filesink.base.d.ts.map"],"mappings":";;;;;AA6E2D;;;KAtEtDE,eAAAA,GAAkBD,iBAuGmBQ,GAAAA;EAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAApCD,uBAAAA,SAAgCC,KAAKP"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { FileSinkOptions, RotatingFileSinkOptions } from "./filesink.base.cjs";
|
|
2
|
+
import { Sink } from "@logtape/logtape";
|
|
3
|
+
|
|
4
|
+
//#region dist/filesink.node.d.ts
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Get a file sink.
|
|
8
|
+
*
|
|
9
|
+
* Note that this function is unavailable in the browser.
|
|
10
|
+
*
|
|
11
|
+
* @param path A path to the file to write to.
|
|
12
|
+
* @param options The options for the sink.
|
|
13
|
+
* @returns A sink that writes to the file. The sink is also a disposable
|
|
14
|
+
* object that closes the file when disposed. If `nonBlocking` is enabled,
|
|
15
|
+
* returns a sink that also implements {@link AsyncDisposable}.
|
|
16
|
+
*/
|
|
17
|
+
declare function getFileSink(path: string, options?: FileSinkOptions): Sink & Disposable;
|
|
18
|
+
declare function getFileSink(path: string, options: FileSinkOptions & {
|
|
19
|
+
nonBlocking: true;
|
|
20
|
+
}): Sink & AsyncDisposable;
|
|
21
|
+
/**
|
|
22
|
+
* Get a rotating file sink.
|
|
23
|
+
*
|
|
24
|
+
* This sink writes log records to a file, and rotates the file when it reaches
|
|
25
|
+
* the `maxSize`. The rotated files are named with the original file name
|
|
26
|
+
* followed by a dot and a number, starting from 1. The number is incremented
|
|
27
|
+
* for each rotation, and the maximum number of files to keep is `maxFiles`.
|
|
28
|
+
*
|
|
29
|
+
* Note that this function is unavailable in the browser.
|
|
30
|
+
*
|
|
31
|
+
* @param path A path to the file to write to.
|
|
32
|
+
* @param options The options for the sink and the file driver.
|
|
33
|
+
* @returns A sink that writes to the file. The sink is also a disposable
|
|
34
|
+
* object that closes the file when disposed. If `nonBlocking` is enabled,
|
|
35
|
+
* returns a sink that also implements {@link AsyncDisposable}.
|
|
36
|
+
*/
|
|
37
|
+
declare function getRotatingFileSink(path: string, options?: RotatingFileSinkOptions): Sink & Disposable;
|
|
38
|
+
declare function getRotatingFileSink(path: string, options: RotatingFileSinkOptions & {
|
|
39
|
+
nonBlocking: true;
|
|
40
|
+
}): Sink & AsyncDisposable;
|
|
41
|
+
//# sourceMappingURL=filesink.node.d.ts.map
|
|
42
|
+
//#endregion
|
|
43
|
+
//#endregion
|
|
44
|
+
export { getFileSink, getRotatingFileSink };
|
|
45
|
+
//# sourceMappingURL=filesink.node.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filesink.node.d.cts","names":["AsyncRotatingFileSinkDriver","FileSinkOptions","RotatingFileSinkDriver","RotatingFileSinkOptions","Sink","nodeDriver","nodeAsyncDriver","getFileSink","Disposable","AsyncDisposable","getRotatingFileSink"],"sources":["../filesink.node.d.ts"],"sourcesContent":["import { AsyncRotatingFileSinkDriver, FileSinkOptions, RotatingFileSinkDriver, RotatingFileSinkOptions } from \"./filesink.base.js\";\nimport { Sink } from \"@logtape/logtape\";\n\n//#region filesink.node.d.ts\n\n/**\n * A Node.js-specific file sink driver.\n */\ndeclare const nodeDriver: RotatingFileSinkDriver<number | void>;\n/**\n * A Node.js-specific async file sink driver.\n * @since 1.0.0\n */\ndeclare const nodeAsyncDriver: AsyncRotatingFileSinkDriver<number | void>;\n/**\n * Get a file sink.\n *\n * Note that this function is unavailable in the browser.\n *\n * @param path A path to the file to write to.\n * @param options The options for the sink.\n * @returns A sink that writes to the file. The sink is also a disposable\n * object that closes the file when disposed. If `nonBlocking` is enabled,\n * returns a sink that also implements {@link AsyncDisposable}.\n */\ndeclare function getFileSink(path: string, options?: FileSinkOptions): Sink & Disposable;\ndeclare function getFileSink(path: string, options: FileSinkOptions & {\n nonBlocking: true;\n}): Sink & AsyncDisposable;\n/**\n * Get a 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 * Note that this function is unavailable in the browser.\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. If `nonBlocking` is enabled,\n * returns a sink that also implements {@link AsyncDisposable}.\n */\ndeclare function getRotatingFileSink(path: string, options?: RotatingFileSinkOptions): Sink & Disposable;\ndeclare function getRotatingFileSink(path: string, options: RotatingFileSinkOptions & {\n nonBlocking: true;\n}): Sink & AsyncDisposable;\n//# sourceMappingURL=filesink.node.d.ts.map\n//#endregion\nexport { getFileSink, getRotatingFileSink, nodeAsyncDriver, nodeDriver };\n//# sourceMappingURL=filesink.node.d.ts.map"],"mappings":";;;;;;AA4B0B;AAAA;;;;;AAiB8E;AAAA;;;iBApBvFO,WAAAA,CAuBbH,IAAAA,EAAAA,MAAAA,EAAAA,OAAAA,CAAAA,EAvBiDH,eAuBjDG,CAAAA,EAvBmEA,IAuBnEA,GAvB0EI,UAuB1EJ;iBAtBaG,WAAAA,CAsBNE,IAAAA,EAAAA,MAAAA,EAAAA,OAAAA,EAtByCR,eAsBzCQ,GAAAA;EAAe,WAAA,EAAA,IAAA;IApBtBL,OAAOK;;;;;;;;;;;;;;;;;iBAiBMC,mBAAAA,yBAA4CP,0BAA0BC,OAAOI;iBAC7EE,mBAAAA,wBAA2CP;;IAExDC,OAAOK"}
|
package/dist/mod.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { FileSinkDriver, FileSinkOptions, RotatingFileSinkDriver, RotatingFileSinkOptions } from "./filesink.base.cjs";
|
|
2
2
|
import { StreamFileSinkOptions, getStreamFileSink } from "./streamfilesink.cjs";
|
|
3
|
-
import { getFileSink, getRotatingFileSink } from "
|
|
3
|
+
import { getFileSink, getRotatingFileSink } from "./dist/filesink.node.cjs";
|
|
4
4
|
export { FileSinkDriver, FileSinkOptions, RotatingFileSinkDriver, RotatingFileSinkOptions, StreamFileSinkOptions, getFileSink, getRotatingFileSink, getStreamFileSink };
|
package/dist/streamfilesink.cjs
CHANGED
|
@@ -53,7 +53,8 @@ const node_stream = require_rolldown_runtime.__toESM(require("node:stream"));
|
|
|
53
53
|
* if it doesn't exist, or appended to if it does exist.
|
|
54
54
|
* @param options Configuration options for the stream-based sink.
|
|
55
55
|
* @returns A sink that writes formatted log records to the specified file.
|
|
56
|
-
* The returned sink implements `
|
|
56
|
+
* The returned sink implements `AsyncDisposable` for proper resource cleanup
|
|
57
|
+
* that waits for all data to be flushed to disk.
|
|
57
58
|
*
|
|
58
59
|
* @since 1.0.0
|
|
59
60
|
*/
|
|
@@ -71,11 +72,17 @@ function getStreamFileSink(path, options = {}) {
|
|
|
71
72
|
if (disposed) return;
|
|
72
73
|
passThrough.write(formatter(record));
|
|
73
74
|
};
|
|
74
|
-
sink[Symbol.
|
|
75
|
+
sink[Symbol.asyncDispose] = async () => {
|
|
75
76
|
if (disposed) return;
|
|
76
77
|
disposed = true;
|
|
77
78
|
passThrough.end();
|
|
78
|
-
|
|
79
|
+
await new Promise((resolve) => {
|
|
80
|
+
writeStream.once("finish", () => {
|
|
81
|
+
writeStream.close(() => {
|
|
82
|
+
resolve();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
});
|
|
79
86
|
};
|
|
80
87
|
return sink;
|
|
81
88
|
}
|
|
@@ -84,11 +84,12 @@ interface StreamFileSinkOptions {
|
|
|
84
84
|
* if it doesn't exist, or appended to if it does exist.
|
|
85
85
|
* @param options Configuration options for the stream-based sink.
|
|
86
86
|
* @returns A sink that writes formatted log records to the specified file.
|
|
87
|
-
* The returned sink implements `
|
|
87
|
+
* The returned sink implements `AsyncDisposable` for proper resource cleanup
|
|
88
|
+
* that waits for all data to be flushed to disk.
|
|
88
89
|
*
|
|
89
90
|
* @since 1.0.0
|
|
90
91
|
*/
|
|
91
|
-
declare function getStreamFileSink(path: string, options?: StreamFileSinkOptions): Sink &
|
|
92
|
+
declare function getStreamFileSink(path: string, options?: StreamFileSinkOptions): Sink & AsyncDisposable;
|
|
92
93
|
//# sourceMappingURL=streamfilesink.d.ts.map
|
|
93
94
|
//#endregion
|
|
94
95
|
export { StreamFileSinkOptions, getStreamFileSink };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"streamfilesink.d.cts","names":[],"sources":["../streamfilesink.ts"],"sourcesContent":[],"mappings":";;;;;;AAkBA;
|
|
1
|
+
{"version":3,"file":"streamfilesink.d.cts","names":[],"sources":["../streamfilesink.ts"],"sourcesContent":[],"mappings":";;;;;;AAkBA;AAgFA;;;;;AAGyB;UAnFR,qBAAA;;;;;;;;;;;;;;;;;;;;;;uBAuBM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAyDP,iBAAA,yBAEL,wBACR,OAAO"}
|
package/dist/streamfilesink.d.ts
CHANGED
|
@@ -84,11 +84,12 @@ interface StreamFileSinkOptions {
|
|
|
84
84
|
* if it doesn't exist, or appended to if it does exist.
|
|
85
85
|
* @param options Configuration options for the stream-based sink.
|
|
86
86
|
* @returns A sink that writes formatted log records to the specified file.
|
|
87
|
-
* The returned sink implements `
|
|
87
|
+
* The returned sink implements `AsyncDisposable` for proper resource cleanup
|
|
88
|
+
* that waits for all data to be flushed to disk.
|
|
88
89
|
*
|
|
89
90
|
* @since 1.0.0
|
|
90
91
|
*/
|
|
91
|
-
declare function getStreamFileSink(path: string, options?: StreamFileSinkOptions): Sink &
|
|
92
|
+
declare function getStreamFileSink(path: string, options?: StreamFileSinkOptions): Sink & AsyncDisposable;
|
|
92
93
|
//# sourceMappingURL=streamfilesink.d.ts.map
|
|
93
94
|
//#endregion
|
|
94
95
|
export { StreamFileSinkOptions, getStreamFileSink };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"streamfilesink.d.ts","names":[],"sources":["../streamfilesink.ts"],"sourcesContent":[],"mappings":";;;;;;AAkBA;
|
|
1
|
+
{"version":3,"file":"streamfilesink.d.ts","names":[],"sources":["../streamfilesink.ts"],"sourcesContent":[],"mappings":";;;;;;AAkBA;AAgFA;;;;;AAGyB;UAnFR,qBAAA;;;;;;;;;;;;;;;;;;;;;;uBAuBM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAyDP,iBAAA,yBAEL,wBACR,OAAO"}
|
package/dist/streamfilesink.js
CHANGED
|
@@ -52,7 +52,8 @@ import { PassThrough } from "node:stream";
|
|
|
52
52
|
* if it doesn't exist, or appended to if it does exist.
|
|
53
53
|
* @param options Configuration options for the stream-based sink.
|
|
54
54
|
* @returns A sink that writes formatted log records to the specified file.
|
|
55
|
-
* The returned sink implements `
|
|
55
|
+
* The returned sink implements `AsyncDisposable` for proper resource cleanup
|
|
56
|
+
* that waits for all data to be flushed to disk.
|
|
56
57
|
*
|
|
57
58
|
* @since 1.0.0
|
|
58
59
|
*/
|
|
@@ -70,11 +71,17 @@ function getStreamFileSink(path, options = {}) {
|
|
|
70
71
|
if (disposed) return;
|
|
71
72
|
passThrough.write(formatter(record));
|
|
72
73
|
};
|
|
73
|
-
sink[Symbol.
|
|
74
|
+
sink[Symbol.asyncDispose] = async () => {
|
|
74
75
|
if (disposed) return;
|
|
75
76
|
disposed = true;
|
|
76
77
|
passThrough.end();
|
|
77
|
-
|
|
78
|
+
await new Promise((resolve) => {
|
|
79
|
+
writeStream.once("finish", () => {
|
|
80
|
+
writeStream.close(() => {
|
|
81
|
+
resolve();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
});
|
|
78
85
|
};
|
|
79
86
|
return sink;
|
|
80
87
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"streamfilesink.js","names":["path: string","options: StreamFileSinkOptions","sink: Sink &
|
|
1
|
+
{"version":3,"file":"streamfilesink.js","names":["path: string","options: StreamFileSinkOptions","sink: Sink & AsyncDisposable","record: LogRecord"],"sources":["../streamfilesink.ts"],"sourcesContent":["import {\n defaultTextFormatter,\n type LogRecord,\n type Sink,\n type TextFormatter,\n} from \"@logtape/logtape\";\nimport { createWriteStream } from \"node:fs\";\nimport { PassThrough } from \"node:stream\";\n\n/**\n * Options for the {@link getStreamFileSink} function.\n *\n * This interface configures the high-performance stream-based file sink that\n * uses Node.js PassThrough streams for optimal I/O performance with automatic\n * backpressure management.\n *\n * @since 1.0.0\n */\nexport interface StreamFileSinkOptions {\n /**\n * High water mark for the PassThrough stream buffer in bytes.\n *\n * This controls the internal buffer size of the PassThrough stream.\n * Higher values can improve performance for high-volume logging but use\n * more memory. Lower values reduce memory usage but may impact performance.\n *\n * @default 16384\n * @since 1.0.0\n */\n readonly highWaterMark?: number;\n\n /**\n * A custom formatter for log records.\n *\n * If not specified, the default text formatter will be used, which formats\n * records in the standard LogTape format with timestamp, level, category,\n * and message.\n *\n * @default defaultTextFormatter\n * @since 1.0.0\n */\n readonly formatter?: TextFormatter;\n}\n\n/**\n * Create a high-performance stream-based file sink that writes log records to a file.\n *\n * This sink uses Node.js PassThrough streams piped to WriteStreams for optimal\n * I/O performance. It leverages the Node.js stream infrastructure to provide\n * automatic backpressure management, efficient buffering, and asynchronous writes\n * without blocking the main thread.\n *\n * ## Performance Characteristics\n *\n * - **High Performance**: Optimized for high-volume logging scenarios\n * - **Non-blocking**: Uses asynchronous I/O that doesn't block the main thread\n * - **Memory Efficient**: Automatic backpressure prevents memory buildup\n * - **Stream-based**: Leverages Node.js native stream optimizations\n *\n * ## When to Use\n *\n * Use this sink when you need:\n * - High-performance file logging for production applications\n * - Non-blocking I/O behavior for real-time applications\n * - Automatic backpressure handling for high-volume scenarios\n * - Simple file output without complex buffering configuration\n *\n * For more control over buffering behavior, consider using {@link getFileSink}\n * instead, which provides options for buffer size, flush intervals, and\n * non-blocking modes.\n *\n * ## Example\n *\n * ```typescript\n * import { configure } from \"@logtape/logtape\";\n * import { getStreamFileSink } from \"@logtape/file\";\n *\n * await configure({\n * sinks: {\n * file: getStreamFileSink(\"app.log\", {\n * highWaterMark: 32768 // 32KB buffer for high-volume logging\n * })\n * },\n * loggers: [\n * { category: [\"myapp\"], sinks: [\"file\"] }\n * ]\n * });\n * ```\n *\n * @param path The path to the file to write logs to. The file will be created\n * if it doesn't exist, or appended to if it does exist.\n * @param options Configuration options for the stream-based sink.\n * @returns A sink that writes formatted log records to the specified file.\n * The returned sink implements `AsyncDisposable` for proper resource cleanup\n * that waits for all data to be flushed to disk.\n *\n * @since 1.0.0\n */\nexport function getStreamFileSink(\n path: string,\n options: StreamFileSinkOptions = {},\n): Sink & AsyncDisposable {\n const highWaterMark = options.highWaterMark ?? 16384;\n const formatter = options.formatter ?? defaultTextFormatter;\n\n // Create PassThrough stream for optimal performance\n const passThrough = new PassThrough({\n highWaterMark,\n objectMode: false,\n });\n\n // Create WriteStream immediately (not lazy)\n const writeStream = createWriteStream(path, { flags: \"a\" });\n\n // Pipe PassThrough to WriteStream for automatic backpressure handling\n passThrough.pipe(writeStream);\n\n let disposed = false;\n\n // Stream-based sink function for high performance\n const sink: Sink & AsyncDisposable = (record: LogRecord) => {\n if (disposed) return;\n\n // Direct write to PassThrough stream\n passThrough.write(formatter(record));\n };\n\n // Async disposal that waits for streams to finish\n sink[Symbol.asyncDispose] = async () => {\n if (disposed) return;\n disposed = true;\n\n // End the PassThrough stream\n passThrough.end();\n\n // Wait for both finish (data flushed) and close (file handle closed) events\n await new Promise<void>((resolve) => {\n writeStream.once(\"finish\", () => {\n writeStream.close(() => {\n resolve();\n });\n });\n });\n };\n\n return sink;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkGA,SAAgB,kBACdA,MACAC,UAAiC,CAAE,GACX;CACxB,MAAM,gBAAgB,QAAQ,iBAAiB;CAC/C,MAAM,YAAY,QAAQ,aAAa;CAGvC,MAAM,cAAc,IAAI,YAAY;EAClC;EACA,YAAY;CACb;CAGD,MAAM,cAAc,kBAAkB,MAAM,EAAE,OAAO,IAAK,EAAC;AAG3D,aAAY,KAAK,YAAY;CAE7B,IAAI,WAAW;CAGf,MAAMC,OAA+B,CAACC,WAAsB;AAC1D,MAAI,SAAU;AAGd,cAAY,MAAM,UAAU,OAAO,CAAC;CACrC;AAGD,MAAK,OAAO,gBAAgB,YAAY;AACtC,MAAI,SAAU;AACd,aAAW;AAGX,cAAY,KAAK;AAGjB,QAAM,IAAI,QAAc,CAAC,YAAY;AACnC,eAAY,KAAK,UAAU,MAAM;AAC/B,gBAAY,MAAM,MAAM;AACtB,cAAS;IACV,EAAC;GACH,EAAC;EACH;CACF;AAED,QAAO;AACR"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@logtape/file",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "File sink and rotating file sink for LogTape",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"logging",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
},
|
|
58
58
|
"sideEffects": false,
|
|
59
59
|
"peerDependencies": {
|
|
60
|
-
"@logtape/logtape": "^1.0.
|
|
60
|
+
"@logtape/logtape": "^1.0.6"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@alinea/suite": "^0.6.3",
|
package/streamfilesink.test.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { getStreamFileSink } from "./streamfilesink.ts";
|
|
2
2
|
import { suite } from "@alinea/suite";
|
|
3
|
-
import type { LogRecord
|
|
3
|
+
import type { LogRecord } from "@logtape/logtape";
|
|
4
4
|
import { assert } from "@std/assert/assert";
|
|
5
5
|
import { assertEquals } from "@std/assert/equals";
|
|
6
6
|
import { delay } from "@std/async/delay";
|
|
7
7
|
import { join } from "@std/path/join";
|
|
8
8
|
import fs from "node:fs";
|
|
9
|
-
import {
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
10
|
import { debug, error, fatal, info, warning } from "../logtape/fixtures.ts";
|
|
11
11
|
|
|
12
12
|
const test = suite(import.meta);
|
|
@@ -17,7 +17,7 @@ function makeTempFileSync(): string {
|
|
|
17
17
|
|
|
18
18
|
test("getStreamFileSink() basic functionality", async () => {
|
|
19
19
|
const path = makeTempFileSync();
|
|
20
|
-
const sink
|
|
20
|
+
const sink = getStreamFileSink(path);
|
|
21
21
|
|
|
22
22
|
sink(debug);
|
|
23
23
|
sink(info);
|
|
@@ -25,10 +25,7 @@ test("getStreamFileSink() basic functionality", async () => {
|
|
|
25
25
|
sink(error);
|
|
26
26
|
sink(fatal);
|
|
27
27
|
|
|
28
|
-
sink[Symbol.
|
|
29
|
-
|
|
30
|
-
// Allow stream to fully flush
|
|
31
|
-
await delay(50);
|
|
28
|
+
await sink[Symbol.asyncDispose]();
|
|
32
29
|
|
|
33
30
|
const content = fs.readFileSync(path, { encoding: "utf-8" });
|
|
34
31
|
assertEquals(
|
|
@@ -47,9 +44,7 @@ test("getStreamFileSink() with custom highWaterMark", async () => {
|
|
|
47
44
|
|
|
48
45
|
sink(debug);
|
|
49
46
|
sink(info);
|
|
50
|
-
sink[Symbol.
|
|
51
|
-
|
|
52
|
-
await delay(50);
|
|
47
|
+
await sink[Symbol.asyncDispose]();
|
|
53
48
|
|
|
54
49
|
const content = fs.readFileSync(path, { encoding: "utf-8" });
|
|
55
50
|
assertEquals(
|
|
@@ -67,9 +62,7 @@ test("getStreamFileSink() with custom formatter", async () => {
|
|
|
67
62
|
|
|
68
63
|
sink(debug);
|
|
69
64
|
sink(info);
|
|
70
|
-
sink[Symbol.
|
|
71
|
-
|
|
72
|
-
await delay(50);
|
|
65
|
+
await sink[Symbol.asyncDispose]();
|
|
73
66
|
|
|
74
67
|
const content = fs.readFileSync(path, { encoding: "utf-8" });
|
|
75
68
|
assertEquals(
|
|
@@ -87,9 +80,7 @@ test("getStreamFileSink() appends to existing file", async () => {
|
|
|
87
80
|
|
|
88
81
|
const sink = getStreamFileSink(path);
|
|
89
82
|
sink(debug);
|
|
90
|
-
sink[Symbol.
|
|
91
|
-
|
|
92
|
-
await delay(50);
|
|
83
|
+
await sink[Symbol.asyncDispose]();
|
|
93
84
|
|
|
94
85
|
const content = fs.readFileSync(path, { encoding: "utf-8" });
|
|
95
86
|
assert(content.startsWith("Initial content\n"));
|
|
@@ -109,8 +100,7 @@ test("getStreamFileSink() high-volume logging", async () => {
|
|
|
109
100
|
sink(record);
|
|
110
101
|
}
|
|
111
102
|
|
|
112
|
-
sink[Symbol.
|
|
113
|
-
await delay(100); // Allow streams to finish
|
|
103
|
+
await sink[Symbol.asyncDispose]();
|
|
114
104
|
|
|
115
105
|
const content = fs.readFileSync(path, { encoding: "utf-8" });
|
|
116
106
|
const lines = content.split("\n").filter((line) => line.length > 0);
|
|
@@ -126,14 +116,12 @@ test("getStreamFileSink() disposal stops writing", async () => {
|
|
|
126
116
|
const sink = getStreamFileSink(path);
|
|
127
117
|
|
|
128
118
|
sink(debug);
|
|
129
|
-
sink[Symbol.
|
|
119
|
+
await sink[Symbol.asyncDispose]();
|
|
130
120
|
|
|
131
121
|
// Writing after disposal should be ignored
|
|
132
122
|
sink(info);
|
|
133
123
|
sink(warning);
|
|
134
124
|
|
|
135
|
-
await delay(50);
|
|
136
|
-
|
|
137
125
|
const content = fs.readFileSync(path, { encoding: "utf-8" });
|
|
138
126
|
const lines = content.split("\n").filter((line) => line.length > 0);
|
|
139
127
|
assertEquals(lines.length, 1); // Only debug record
|
|
@@ -147,10 +135,8 @@ test("getStreamFileSink() double disposal", async () => {
|
|
|
147
135
|
const sink = getStreamFileSink(path);
|
|
148
136
|
|
|
149
137
|
sink(debug);
|
|
150
|
-
sink[Symbol.
|
|
151
|
-
sink[Symbol.
|
|
152
|
-
|
|
153
|
-
await delay(50);
|
|
138
|
+
await sink[Symbol.asyncDispose]();
|
|
139
|
+
await sink[Symbol.asyncDispose](); // Should not throw
|
|
154
140
|
|
|
155
141
|
const content = fs.readFileSync(path, { encoding: "utf-8" });
|
|
156
142
|
const lines = content.split("\n").filter((line) => line.length > 0);
|
|
@@ -163,9 +149,7 @@ test("getStreamFileSink() handles rapid disposal", async () => {
|
|
|
163
149
|
|
|
164
150
|
sink(debug);
|
|
165
151
|
// Dispose immediately without waiting
|
|
166
|
-
sink[Symbol.
|
|
167
|
-
|
|
168
|
-
await delay(50);
|
|
152
|
+
await sink[Symbol.asyncDispose]();
|
|
169
153
|
|
|
170
154
|
const content = fs.readFileSync(path, { encoding: "utf-8" });
|
|
171
155
|
assert(content.includes("Hello, 123 & 456!"));
|
|
@@ -193,8 +177,7 @@ test("getStreamFileSink() concurrent writes", async () => {
|
|
|
193
177
|
}
|
|
194
178
|
|
|
195
179
|
await Promise.all(promises);
|
|
196
|
-
sink[Symbol.
|
|
197
|
-
await delay(100);
|
|
180
|
+
await sink[Symbol.asyncDispose]();
|
|
198
181
|
|
|
199
182
|
const content = fs.readFileSync(path, { encoding: "utf-8" });
|
|
200
183
|
const lines = content.split("\n").filter((line) => line.length > 0);
|
|
@@ -216,9 +199,7 @@ test("getStreamFileSink() with empty records", async () => {
|
|
|
216
199
|
};
|
|
217
200
|
|
|
218
201
|
sink(emptyRecord);
|
|
219
|
-
sink[Symbol.
|
|
220
|
-
|
|
221
|
-
await delay(50);
|
|
202
|
+
await sink[Symbol.asyncDispose]();
|
|
222
203
|
|
|
223
204
|
const content = fs.readFileSync(path, { encoding: "utf-8" });
|
|
224
205
|
assert(content.includes("[DBG]"));
|
|
@@ -237,9 +218,7 @@ test("getStreamFileSink() with large messages", async () => {
|
|
|
237
218
|
};
|
|
238
219
|
|
|
239
220
|
sink(largeRecord);
|
|
240
|
-
sink[Symbol.
|
|
241
|
-
|
|
242
|
-
await delay(100); // Give more time for large write
|
|
221
|
+
await sink[Symbol.asyncDispose]();
|
|
243
222
|
|
|
244
223
|
const content = fs.readFileSync(path, { encoding: "utf-8" });
|
|
245
224
|
assert(content.includes(largeMessage));
|
|
@@ -264,8 +243,7 @@ test("getStreamFileSink() memory efficiency", async () => {
|
|
|
264
243
|
}
|
|
265
244
|
}
|
|
266
245
|
|
|
267
|
-
sink[Symbol.
|
|
268
|
-
await delay(platform() === "win32" ? 1000 : 200);
|
|
246
|
+
await sink[Symbol.asyncDispose]();
|
|
269
247
|
|
|
270
248
|
const content = fs.readFileSync(path, { encoding: "utf-8" });
|
|
271
249
|
const lines = content.split("\n").filter((line) => line.length > 0);
|
|
@@ -283,9 +261,7 @@ test("getStreamFileSink() creates new file when it doesn't exist", async () => {
|
|
|
283
261
|
|
|
284
262
|
const sink = getStreamFileSink(path);
|
|
285
263
|
sink(debug);
|
|
286
|
-
sink[Symbol.
|
|
287
|
-
|
|
288
|
-
await delay(50);
|
|
264
|
+
await sink[Symbol.asyncDispose]();
|
|
289
265
|
|
|
290
266
|
// File should have been created
|
|
291
267
|
assert(fs.existsSync(path));
|
|
@@ -302,10 +278,8 @@ test("getStreamFileSink() multiple instances on same file", async () => {
|
|
|
302
278
|
sink1(debug);
|
|
303
279
|
sink2(info);
|
|
304
280
|
|
|
305
|
-
sink1[Symbol.
|
|
306
|
-
sink2[Symbol.
|
|
307
|
-
|
|
308
|
-
await delay(100);
|
|
281
|
+
await sink1[Symbol.asyncDispose]();
|
|
282
|
+
await sink2[Symbol.asyncDispose]();
|
|
309
283
|
|
|
310
284
|
const content = fs.readFileSync(path, { encoding: "utf-8" });
|
|
311
285
|
assert(content.includes("[DBG]"));
|
|
@@ -317,8 +291,7 @@ test("getStreamFileSink() stream error handling", async () => {
|
|
|
317
291
|
const sink = getStreamFileSink(path);
|
|
318
292
|
|
|
319
293
|
sink(debug);
|
|
320
|
-
sink[Symbol.
|
|
321
|
-
await delay(50);
|
|
294
|
+
await sink[Symbol.asyncDispose]();
|
|
322
295
|
|
|
323
296
|
// Delete the file after disposal
|
|
324
297
|
try {
|
package/streamfilesink.ts
CHANGED
|
@@ -91,14 +91,15 @@ export interface StreamFileSinkOptions {
|
|
|
91
91
|
* if it doesn't exist, or appended to if it does exist.
|
|
92
92
|
* @param options Configuration options for the stream-based sink.
|
|
93
93
|
* @returns A sink that writes formatted log records to the specified file.
|
|
94
|
-
* The returned sink implements `
|
|
94
|
+
* The returned sink implements `AsyncDisposable` for proper resource cleanup
|
|
95
|
+
* that waits for all data to be flushed to disk.
|
|
95
96
|
*
|
|
96
97
|
* @since 1.0.0
|
|
97
98
|
*/
|
|
98
99
|
export function getStreamFileSink(
|
|
99
100
|
path: string,
|
|
100
101
|
options: StreamFileSinkOptions = {},
|
|
101
|
-
): Sink &
|
|
102
|
+
): Sink & AsyncDisposable {
|
|
102
103
|
const highWaterMark = options.highWaterMark ?? 16384;
|
|
103
104
|
const formatter = options.formatter ?? defaultTextFormatter;
|
|
104
105
|
|
|
@@ -117,19 +118,29 @@ export function getStreamFileSink(
|
|
|
117
118
|
let disposed = false;
|
|
118
119
|
|
|
119
120
|
// Stream-based sink function for high performance
|
|
120
|
-
const sink: Sink &
|
|
121
|
+
const sink: Sink & AsyncDisposable = (record: LogRecord) => {
|
|
121
122
|
if (disposed) return;
|
|
122
123
|
|
|
123
124
|
// Direct write to PassThrough stream
|
|
124
125
|
passThrough.write(formatter(record));
|
|
125
126
|
};
|
|
126
127
|
|
|
127
|
-
//
|
|
128
|
-
sink[Symbol.
|
|
128
|
+
// Async disposal that waits for streams to finish
|
|
129
|
+
sink[Symbol.asyncDispose] = async () => {
|
|
129
130
|
if (disposed) return;
|
|
130
131
|
disposed = true;
|
|
132
|
+
|
|
133
|
+
// End the PassThrough stream
|
|
131
134
|
passThrough.end();
|
|
132
|
-
|
|
135
|
+
|
|
136
|
+
// Wait for both finish (data flushed) and close (file handle closed) events
|
|
137
|
+
await new Promise<void>((resolve) => {
|
|
138
|
+
writeStream.once("finish", () => {
|
|
139
|
+
writeStream.close(() => {
|
|
140
|
+
resolve();
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
});
|
|
133
144
|
};
|
|
134
145
|
|
|
135
146
|
return sink;
|