@logtape/file 1.1.0-dev.324 → 1.1.0-dev.332

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.1.0-dev.324+017000e7",
3
+ "version": "1.1.0-dev.332+cfc70069",
4
4
  "license": "MIT",
5
5
  "exports": "./src/mod.ts",
6
6
  "imports": {
@@ -71,11 +71,23 @@ function getStreamFileSink(path, options = {}) {
71
71
  if (disposed) return;
72
72
  passThrough.write(formatter(record));
73
73
  };
74
- sink[Symbol.dispose] = () => {
74
+ sink[Symbol.asyncDispose] = async () => {
75
75
  if (disposed) return;
76
76
  disposed = true;
77
- passThrough.end();
78
- writeStream.end();
77
+ if (passThrough.writableNeedDrain) await new Promise((resolve) => {
78
+ passThrough.once("drain", resolve);
79
+ });
80
+ await new Promise((resolve) => {
81
+ passThrough.once("finish", resolve);
82
+ passThrough.end();
83
+ });
84
+ await new Promise((resolve) => {
85
+ if (writeStream.closed || writeStream.destroyed) {
86
+ resolve();
87
+ return;
88
+ }
89
+ writeStream.once("close", resolve);
90
+ });
79
91
  };
80
92
  return sink;
81
93
  }
@@ -88,7 +88,7 @@ interface StreamFileSinkOptions {
88
88
  *
89
89
  * @since 1.0.0
90
90
  */
91
- declare function getStreamFileSink(path: string, options?: StreamFileSinkOptions): Sink & Disposable;
91
+ declare function getStreamFileSink(path: string, options?: StreamFileSinkOptions): Sink & AsyncDisposable;
92
92
  //# sourceMappingURL=streamfilesink.d.ts.map
93
93
  //#endregion
94
94
  export { StreamFileSinkOptions, getStreamFileSink };
@@ -1 +1 @@
1
- {"version":3,"file":"streamfilesink.d.cts","names":[],"sources":["../src/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":["../src/streamfilesink.ts"],"sourcesContent":[],"mappings":";;;;;;AAkBA;AA+EA;;;;;AAGyB;UAlFR,qBAAA;;;;;;;;;;;;;;;;;;;;;;uBAuBM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAwDP,iBAAA,yBAEL,wBACR,OAAO"}
@@ -88,7 +88,7 @@ interface StreamFileSinkOptions {
88
88
  *
89
89
  * @since 1.0.0
90
90
  */
91
- declare function getStreamFileSink(path: string, options?: StreamFileSinkOptions): Sink & Disposable;
91
+ declare function getStreamFileSink(path: string, options?: StreamFileSinkOptions): Sink & AsyncDisposable;
92
92
  //# sourceMappingURL=streamfilesink.d.ts.map
93
93
  //#endregion
94
94
  export { StreamFileSinkOptions, getStreamFileSink };
@@ -1 +1 @@
1
- {"version":3,"file":"streamfilesink.d.ts","names":[],"sources":["../src/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":["../src/streamfilesink.ts"],"sourcesContent":[],"mappings":";;;;;;AAkBA;AA+EA;;;;;AAGyB;UAlFR,qBAAA;;;;;;;;;;;;;;;;;;;;;;uBAuBM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAwDP,iBAAA,yBAEL,wBACR,OAAO"}
@@ -70,11 +70,23 @@ function getStreamFileSink(path, options = {}) {
70
70
  if (disposed) return;
71
71
  passThrough.write(formatter(record));
72
72
  };
73
- sink[Symbol.dispose] = () => {
73
+ sink[Symbol.asyncDispose] = async () => {
74
74
  if (disposed) return;
75
75
  disposed = true;
76
- passThrough.end();
77
- writeStream.end();
76
+ if (passThrough.writableNeedDrain) await new Promise((resolve) => {
77
+ passThrough.once("drain", resolve);
78
+ });
79
+ await new Promise((resolve) => {
80
+ passThrough.once("finish", resolve);
81
+ passThrough.end();
82
+ });
83
+ await new Promise((resolve) => {
84
+ if (writeStream.closed || writeStream.destroyed) {
85
+ resolve();
86
+ return;
87
+ }
88
+ writeStream.once("close", resolve);
89
+ });
78
90
  };
79
91
  return sink;
80
92
  }
@@ -1 +1 @@
1
- {"version":3,"file":"streamfilesink.js","names":["path: string","options: StreamFileSinkOptions","sink: Sink & Disposable","record: LogRecord"],"sources":["../src/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":["../src/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 & 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 // Asynchronous disposal with sequential stream closure\n sink[Symbol.asyncDispose] = async () => {\n if (disposed) return;\n disposed = true;\n\n // Wait for PassThrough to drain if needed\n if (passThrough.writableNeedDrain) {\n await new Promise<void>((resolve) => {\n passThrough.once(\"drain\", resolve);\n });\n }\n\n // End the PassThrough stream first and wait for it to finish\n await new Promise<void>((resolve) => {\n passThrough.once(\"finish\", resolve);\n passThrough.end();\n });\n\n // Wait for WriteStream to finish and close (piped streams auto-close)\n await new Promise<void>((resolve) => {\n if (writeStream.closed || writeStream.destroyed) {\n resolve();\n return;\n }\n writeStream.once(\"close\", resolve);\n });\n };\n\n return sink;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiGA,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,MAAI,YAAY,kBACd,OAAM,IAAI,QAAc,CAAC,YAAY;AACnC,eAAY,KAAK,SAAS,QAAQ;EACnC;AAIH,QAAM,IAAI,QAAc,CAAC,YAAY;AACnC,eAAY,KAAK,UAAU,QAAQ;AACnC,eAAY,KAAK;EAClB;AAGD,QAAM,IAAI,QAAc,CAAC,YAAY;AACnC,OAAI,YAAY,UAAU,YAAY,WAAW;AAC/C,aAAS;AACT;GACD;AACD,eAAY,KAAK,SAAS,QAAQ;EACnC;CACF;AAED,QAAO;AACR"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logtape/file",
3
- "version": "1.1.0-dev.324+017000e7",
3
+ "version": "1.1.0-dev.332+cfc70069",
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.1.0-dev.324+017000e7"
60
+ "@logtape/logtape": "1.1.0-dev.332+cfc70069"
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 {
11
11
  debug,
12
12
  error,
@@ -23,7 +23,7 @@ function makeTempFileSync(): string {
23
23
 
24
24
  test("getStreamFileSink() basic functionality", async () => {
25
25
  const path = makeTempFileSync();
26
- const sink: Sink & Disposable = getStreamFileSink(path);
26
+ const sink = getStreamFileSink(path);
27
27
 
28
28
  sink(debug);
29
29
  sink(info);
@@ -31,7 +31,7 @@ test("getStreamFileSink() basic functionality", async () => {
31
31
  sink(error);
32
32
  sink(fatal);
33
33
 
34
- sink[Symbol.dispose]();
34
+ await sink[Symbol.asyncDispose]();
35
35
 
36
36
  // Allow stream to fully flush
37
37
  await delay(50);
@@ -53,7 +53,7 @@ test("getStreamFileSink() with custom highWaterMark", async () => {
53
53
 
54
54
  sink(debug);
55
55
  sink(info);
56
- sink[Symbol.dispose]();
56
+ await sink[Symbol.asyncDispose]();
57
57
 
58
58
  await delay(50);
59
59
 
@@ -73,7 +73,7 @@ test("getStreamFileSink() with custom formatter", async () => {
73
73
 
74
74
  sink(debug);
75
75
  sink(info);
76
- sink[Symbol.dispose]();
76
+ await sink[Symbol.asyncDispose]();
77
77
 
78
78
  await delay(50);
79
79
 
@@ -93,7 +93,7 @@ test("getStreamFileSink() appends to existing file", async () => {
93
93
 
94
94
  const sink = getStreamFileSink(path);
95
95
  sink(debug);
96
- sink[Symbol.dispose]();
96
+ await sink[Symbol.asyncDispose]();
97
97
 
98
98
  await delay(50);
99
99
 
@@ -115,7 +115,7 @@ test("getStreamFileSink() high-volume logging", async () => {
115
115
  sink(record);
116
116
  }
117
117
 
118
- sink[Symbol.dispose]();
118
+ await sink[Symbol.asyncDispose]();
119
119
  await delay(100); // Allow streams to finish
120
120
 
121
121
  const content = fs.readFileSync(path, { encoding: "utf-8" });
@@ -132,7 +132,7 @@ test("getStreamFileSink() disposal stops writing", async () => {
132
132
  const sink = getStreamFileSink(path);
133
133
 
134
134
  sink(debug);
135
- sink[Symbol.dispose]();
135
+ await sink[Symbol.asyncDispose]();
136
136
 
137
137
  // Writing after disposal should be ignored
138
138
  sink(info);
@@ -153,8 +153,8 @@ test("getStreamFileSink() double disposal", async () => {
153
153
  const sink = getStreamFileSink(path);
154
154
 
155
155
  sink(debug);
156
- sink[Symbol.dispose]();
157
- sink[Symbol.dispose](); // Should not throw
156
+ await sink[Symbol.asyncDispose]();
157
+ await sink[Symbol.asyncDispose](); // Should not throw
158
158
 
159
159
  await delay(50);
160
160
 
@@ -169,7 +169,7 @@ test("getStreamFileSink() handles rapid disposal", async () => {
169
169
 
170
170
  sink(debug);
171
171
  // Dispose immediately without waiting
172
- sink[Symbol.dispose]();
172
+ await sink[Symbol.asyncDispose]();
173
173
 
174
174
  await delay(50);
175
175
 
@@ -199,7 +199,7 @@ test("getStreamFileSink() concurrent writes", async () => {
199
199
  }
200
200
 
201
201
  await Promise.all(promises);
202
- sink[Symbol.dispose]();
202
+ await sink[Symbol.asyncDispose]();
203
203
  await delay(100);
204
204
 
205
205
  const content = fs.readFileSync(path, { encoding: "utf-8" });
@@ -222,7 +222,7 @@ test("getStreamFileSink() with empty records", async () => {
222
222
  };
223
223
 
224
224
  sink(emptyRecord);
225
- sink[Symbol.dispose]();
225
+ await sink[Symbol.asyncDispose]();
226
226
 
227
227
  await delay(50);
228
228
 
@@ -243,7 +243,7 @@ test("getStreamFileSink() with large messages", async () => {
243
243
  };
244
244
 
245
245
  sink(largeRecord);
246
- sink[Symbol.dispose]();
246
+ await sink[Symbol.asyncDispose]();
247
247
 
248
248
  await delay(100); // Give more time for large write
249
249
 
@@ -270,8 +270,8 @@ test("getStreamFileSink() memory efficiency", async () => {
270
270
  }
271
271
  }
272
272
 
273
- sink[Symbol.dispose]();
274
- await delay(platform() === "win32" ? 1000 : 200);
273
+ // Use async disposal to ensure all streams are properly flushed
274
+ await sink[Symbol.asyncDispose]();
275
275
 
276
276
  const content = fs.readFileSync(path, { encoding: "utf-8" });
277
277
  const lines = content.split("\n").filter((line) => line.length > 0);
@@ -289,7 +289,7 @@ test("getStreamFileSink() creates new file when it doesn't exist", async () => {
289
289
 
290
290
  const sink = getStreamFileSink(path);
291
291
  sink(debug);
292
- sink[Symbol.dispose]();
292
+ await sink[Symbol.asyncDispose]();
293
293
 
294
294
  await delay(50);
295
295
 
@@ -308,8 +308,8 @@ test("getStreamFileSink() multiple instances on same file", async () => {
308
308
  sink1(debug);
309
309
  sink2(info);
310
310
 
311
- sink1[Symbol.dispose]();
312
- sink2[Symbol.dispose]();
311
+ await sink1[Symbol.asyncDispose]();
312
+ await sink2[Symbol.asyncDispose]();
313
313
 
314
314
  await delay(100);
315
315
 
@@ -323,7 +323,7 @@ test("getStreamFileSink() stream error handling", async () => {
323
323
  const sink = getStreamFileSink(path);
324
324
 
325
325
  sink(debug);
326
- sink[Symbol.dispose]();
326
+ await sink[Symbol.asyncDispose]();
327
327
  await delay(50);
328
328
 
329
329
  // Delete the file after disposal
@@ -98,7 +98,7 @@ export interface StreamFileSinkOptions {
98
98
  export function getStreamFileSink(
99
99
  path: string,
100
100
  options: StreamFileSinkOptions = {},
101
- ): Sink & Disposable {
101
+ ): Sink & AsyncDisposable {
102
102
  const highWaterMark = options.highWaterMark ?? 16384;
103
103
  const formatter = options.formatter ?? defaultTextFormatter;
104
104
 
@@ -117,19 +117,39 @@ export function getStreamFileSink(
117
117
  let disposed = false;
118
118
 
119
119
  // Stream-based sink function for high performance
120
- const sink: Sink & Disposable = (record: LogRecord) => {
120
+ const sink: Sink & AsyncDisposable = (record: LogRecord) => {
121
121
  if (disposed) return;
122
122
 
123
123
  // Direct write to PassThrough stream
124
124
  passThrough.write(formatter(record));
125
125
  };
126
126
 
127
- // Minimal disposal
128
- sink[Symbol.dispose] = () => {
127
+ // Asynchronous disposal with sequential stream closure
128
+ sink[Symbol.asyncDispose] = async () => {
129
129
  if (disposed) return;
130
130
  disposed = true;
131
- passThrough.end();
132
- writeStream.end();
131
+
132
+ // Wait for PassThrough to drain if needed
133
+ if (passThrough.writableNeedDrain) {
134
+ await new Promise<void>((resolve) => {
135
+ passThrough.once("drain", resolve);
136
+ });
137
+ }
138
+
139
+ // End the PassThrough stream first and wait for it to finish
140
+ await new Promise<void>((resolve) => {
141
+ passThrough.once("finish", resolve);
142
+ passThrough.end();
143
+ });
144
+
145
+ // Wait for WriteStream to finish and close (piped streams auto-close)
146
+ await new Promise<void>((resolve) => {
147
+ if (writeStream.closed || writeStream.destroyed) {
148
+ resolve();
149
+ return;
150
+ }
151
+ writeStream.once("close", resolve);
152
+ });
133
153
  };
134
154
 
135
155
  return sink;