@logtape/file 1.0.5 → 1.0.6-dev.351

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logtape/file",
3
- "version": "1.0.5",
3
+ "version": "1.0.6-dev.351+8407c5e9",
4
4
  "license": "MIT",
5
5
  "exports": "./mod.ts",
6
6
  "imports": {
@@ -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 `Disposable` for proper resource cleanup.
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.dispose] = () => {
75
+ sink[Symbol.asyncDispose] = async () => {
75
76
  if (disposed) return;
76
77
  disposed = true;
77
78
  passThrough.end();
78
- writeStream.end();
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 `Disposable` for proper resource cleanup.
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 & Disposable;
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;AA+EA;;;;;AAGoB;UAlFH,qBAAA;;;;;;;;;;;;;;;;;;;;;;uBAuBM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAwDP,iBAAA,yBAEL,wBACR,OAAO"}
1
+ {"version":3,"file":"streamfilesink.d.cts","names":[],"sources":["../streamfilesink.ts"],"sourcesContent":[],"mappings":";;;;;;AAkBA;AAgFA;;;;;AAGyB;UAnFR,qBAAA;;;;;;;;;;;;;;;;;;;;;;uBAuBM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAyDP,iBAAA,yBAEL,wBACR,OAAO"}
@@ -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 `Disposable` for proper resource cleanup.
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 & Disposable;
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;AA+EA;;;;;AAGoB;UAlFH,qBAAA;;;;;;;;;;;;;;;;;;;;;;uBAuBM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAwDP,iBAAA,yBAEL,wBACR,OAAO"}
1
+ {"version":3,"file":"streamfilesink.d.ts","names":[],"sources":["../streamfilesink.ts"],"sourcesContent":[],"mappings":";;;;;;AAkBA;AAgFA;;;;;AAGyB;UAnFR,qBAAA;;;;;;;;;;;;;;;;;;;;;;uBAuBM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAyDP,iBAAA,yBAEL,wBACR,OAAO"}
@@ -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 `Disposable` for proper resource cleanup.
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.dispose] = () => {
74
+ sink[Symbol.asyncDispose] = async () => {
74
75
  if (disposed) return;
75
76
  disposed = true;
76
77
  passThrough.end();
77
- writeStream.end();
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 & Disposable","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 `Disposable` for proper resource cleanup.\n *\n * @since 1.0.0\n */\nexport function getStreamFileSink(\n path: string,\n options: StreamFileSinkOptions = {},\n): Sink & Disposable {\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 & Disposable = (record: LogRecord) => {\n if (disposed) return;\n\n // Direct write to PassThrough stream\n passThrough.write(formatter(record));\n };\n\n // Minimal disposal\n sink[Symbol.dispose] = () => {\n if (disposed) return;\n disposed = true;\n passThrough.end();\n writeStream.end();\n };\n\n return sink;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiGA,SAAgB,kBACdA,MACAC,UAAiC,CAAE,GAChB;CACnB,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,OAA0B,CAACC,WAAsB;AACrD,MAAI,SAAU;AAGd,cAAY,MAAM,UAAU,OAAO,CAAC;CACrC;AAGD,MAAK,OAAO,WAAW,MAAM;AAC3B,MAAI,SAAU;AACd,aAAW;AACX,cAAY,KAAK;AACjB,cAAY,KAAK;CAClB;AAED,QAAO;AACR"}
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.5",
3
+ "version": "1.0.6-dev.351+8407c5e9",
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.5"
60
+ "@logtape/logtape": "^1.0.6-dev.351+8407c5e9"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@alinea/suite": "^0.6.3",
@@ -1,12 +1,12 @@
1
1
  import { getStreamFileSink } from "./streamfilesink.ts";
2
2
  import { suite } from "@alinea/suite";
3
- import type { LogRecord, Sink } from "@logtape/logtape";
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 { platform, tmpdir } from "node:os";
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: Sink & Disposable = getStreamFileSink(path);
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.dispose]();
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.dispose]();
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.dispose]();
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.dispose]();
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.dispose]();
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.dispose]();
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.dispose]();
151
- sink[Symbol.dispose](); // Should not throw
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.dispose]();
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.dispose]();
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.dispose]();
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.dispose]();
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.dispose]();
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.dispose]();
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.dispose]();
306
- sink2[Symbol.dispose]();
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.dispose]();
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 `Disposable` for proper resource cleanup.
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 & Disposable {
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 & Disposable = (record: LogRecord) => {
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
- // Minimal disposal
128
- sink[Symbol.dispose] = () => {
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
- writeStream.end();
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;