@logtape/file 1.3.0 → 1.3.2
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/dist/mod.d.cts +1 -1
- package/package.json +5 -2
- package/deno.json +0 -37
- package/dist/dist/filesink.base.d.cts +0 -74
- package/dist/dist/filesink.base.d.cts.map +0 -1
- package/dist/dist/filesink.node.d.cts +0 -45
- package/dist/dist/filesink.node.d.cts.map +0 -1
- package/src/filesink.base.ts +0 -815
- package/src/filesink.deno.ts +0 -122
- package/src/filesink.jsr.ts +0 -77
- package/src/filesink.node.ts +0 -118
- package/src/filesink.test.ts +0 -820
- package/src/mod.ts +0 -9
- package/src/streamfilesink.test.ts +0 -340
- package/src/streamfilesink.ts +0 -157
- package/tsdown.config.ts +0 -22
package/src/mod.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
export type {
|
|
2
|
-
FileSinkDriver,
|
|
3
|
-
FileSinkOptions,
|
|
4
|
-
RotatingFileSinkDriver,
|
|
5
|
-
RotatingFileSinkOptions,
|
|
6
|
-
} from "./filesink.base.ts";
|
|
7
|
-
export type { StreamFileSinkOptions } from "./streamfilesink.ts";
|
|
8
|
-
export { getFileSink, getRotatingFileSink } from "#filesink";
|
|
9
|
-
export { getStreamFileSink } from "./streamfilesink.ts";
|
|
@@ -1,340 +0,0 @@
|
|
|
1
|
-
import { getStreamFileSink } from "./streamfilesink.ts";
|
|
2
|
-
import { suite } from "@alinea/suite";
|
|
3
|
-
import type { LogRecord } from "@logtape/logtape";
|
|
4
|
-
import { assert } from "@std/assert/assert";
|
|
5
|
-
import { assertEquals } from "@std/assert/equals";
|
|
6
|
-
import { delay } from "@std/async/delay";
|
|
7
|
-
import { join } from "@std/path/join";
|
|
8
|
-
import fs from "node:fs";
|
|
9
|
-
import { tmpdir } from "node:os";
|
|
10
|
-
import {
|
|
11
|
-
debug,
|
|
12
|
-
error,
|
|
13
|
-
fatal,
|
|
14
|
-
info,
|
|
15
|
-
warning,
|
|
16
|
-
} from "../../logtape/src/fixtures.ts";
|
|
17
|
-
|
|
18
|
-
const test = suite(import.meta);
|
|
19
|
-
|
|
20
|
-
function makeTempFileSync(): string {
|
|
21
|
-
return join(fs.mkdtempSync(join(tmpdir(), "logtape-")), "logtape.txt");
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
test("getStreamFileSink() basic functionality", async () => {
|
|
25
|
-
const path = makeTempFileSync();
|
|
26
|
-
const sink = getStreamFileSink(path);
|
|
27
|
-
|
|
28
|
-
sink(debug);
|
|
29
|
-
sink(info);
|
|
30
|
-
sink(warning);
|
|
31
|
-
sink(error);
|
|
32
|
-
sink(fatal);
|
|
33
|
-
|
|
34
|
-
await sink[Symbol.asyncDispose]();
|
|
35
|
-
|
|
36
|
-
// Allow stream to fully flush
|
|
37
|
-
await delay(50);
|
|
38
|
-
|
|
39
|
-
const content = fs.readFileSync(path, { encoding: "utf-8" });
|
|
40
|
-
assertEquals(
|
|
41
|
-
content,
|
|
42
|
-
"2023-11-14 22:13:20.000 +00:00 [DBG] my-app·junk: Hello, 123 & 456!\n" +
|
|
43
|
-
"2023-11-14 22:13:20.000 +00:00 [INF] my-app·junk: Hello, 123 & 456!\n" +
|
|
44
|
-
"2023-11-14 22:13:20.000 +00:00 [WRN] my-app·junk: Hello, 123 & 456!\n" +
|
|
45
|
-
"2023-11-14 22:13:20.000 +00:00 [ERR] my-app·junk: Hello, 123 & 456!\n" +
|
|
46
|
-
"2023-11-14 22:13:20.000 +00:00 [FTL] my-app·junk: Hello, 123 & 456!\n",
|
|
47
|
-
);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
test("getStreamFileSink() with custom highWaterMark", async () => {
|
|
51
|
-
const path = makeTempFileSync();
|
|
52
|
-
const sink = getStreamFileSink(path, { highWaterMark: 1024 });
|
|
53
|
-
|
|
54
|
-
sink(debug);
|
|
55
|
-
sink(info);
|
|
56
|
-
await sink[Symbol.asyncDispose]();
|
|
57
|
-
|
|
58
|
-
await delay(50);
|
|
59
|
-
|
|
60
|
-
const content = fs.readFileSync(path, { encoding: "utf-8" });
|
|
61
|
-
assertEquals(
|
|
62
|
-
content,
|
|
63
|
-
"2023-11-14 22:13:20.000 +00:00 [DBG] my-app·junk: Hello, 123 & 456!\n" +
|
|
64
|
-
"2023-11-14 22:13:20.000 +00:00 [INF] my-app·junk: Hello, 123 & 456!\n",
|
|
65
|
-
);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
test("getStreamFileSink() with custom formatter", async () => {
|
|
69
|
-
const path = makeTempFileSync();
|
|
70
|
-
const customFormatter = (record: LogRecord) =>
|
|
71
|
-
`CUSTOM: ${record.message.join("")}\n`;
|
|
72
|
-
const sink = getStreamFileSink(path, { formatter: customFormatter });
|
|
73
|
-
|
|
74
|
-
sink(debug);
|
|
75
|
-
sink(info);
|
|
76
|
-
await sink[Symbol.asyncDispose]();
|
|
77
|
-
|
|
78
|
-
await delay(50);
|
|
79
|
-
|
|
80
|
-
const content = fs.readFileSync(path, { encoding: "utf-8" });
|
|
81
|
-
assertEquals(
|
|
82
|
-
content,
|
|
83
|
-
"CUSTOM: Hello, 123 & 456!\n" +
|
|
84
|
-
"CUSTOM: Hello, 123 & 456!\n",
|
|
85
|
-
);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
test("getStreamFileSink() appends to existing file", async () => {
|
|
89
|
-
const path = makeTempFileSync();
|
|
90
|
-
|
|
91
|
-
// Write initial content
|
|
92
|
-
fs.writeFileSync(path, "Initial content\n");
|
|
93
|
-
|
|
94
|
-
const sink = getStreamFileSink(path);
|
|
95
|
-
sink(debug);
|
|
96
|
-
await sink[Symbol.asyncDispose]();
|
|
97
|
-
|
|
98
|
-
await delay(50);
|
|
99
|
-
|
|
100
|
-
const content = fs.readFileSync(path, { encoding: "utf-8" });
|
|
101
|
-
assert(content.startsWith("Initial content\n"));
|
|
102
|
-
assert(content.includes("Hello, 123 & 456!"));
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
test("getStreamFileSink() high-volume logging", async () => {
|
|
106
|
-
const path = makeTempFileSync();
|
|
107
|
-
const sink = getStreamFileSink(path, { highWaterMark: 1024 });
|
|
108
|
-
|
|
109
|
-
// Write many records quickly to test stream backpressure
|
|
110
|
-
for (let i = 0; i < 100; i++) {
|
|
111
|
-
const record: LogRecord = {
|
|
112
|
-
...debug,
|
|
113
|
-
message: [`Log entry ${i}`],
|
|
114
|
-
};
|
|
115
|
-
sink(record);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
await sink[Symbol.asyncDispose]();
|
|
119
|
-
await delay(100); // Allow streams to finish
|
|
120
|
-
|
|
121
|
-
const content = fs.readFileSync(path, { encoding: "utf-8" });
|
|
122
|
-
const lines = content.split("\n").filter((line) => line.length > 0);
|
|
123
|
-
assertEquals(lines.length, 100);
|
|
124
|
-
|
|
125
|
-
// Verify first and last entries
|
|
126
|
-
assert(lines[0].includes("Log entry 0"));
|
|
127
|
-
assert(lines[99].includes("Log entry 99"));
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
test("getStreamFileSink() disposal stops writing", async () => {
|
|
131
|
-
const path = makeTempFileSync();
|
|
132
|
-
const sink = getStreamFileSink(path);
|
|
133
|
-
|
|
134
|
-
sink(debug);
|
|
135
|
-
await sink[Symbol.asyncDispose]();
|
|
136
|
-
|
|
137
|
-
// Writing after disposal should be ignored
|
|
138
|
-
sink(info);
|
|
139
|
-
sink(warning);
|
|
140
|
-
|
|
141
|
-
const content = fs.readFileSync(path, { encoding: "utf-8" });
|
|
142
|
-
const lines = content.split("\n").filter((line) => line.length > 0);
|
|
143
|
-
assertEquals(lines.length, 1); // Only debug record
|
|
144
|
-
assert(content.includes("[DBG]"));
|
|
145
|
-
assert(!content.includes("[INF]"));
|
|
146
|
-
assert(!content.includes("[WRN]"));
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
test("getStreamFileSink() double disposal", async () => {
|
|
150
|
-
const path = makeTempFileSync();
|
|
151
|
-
const sink = getStreamFileSink(path);
|
|
152
|
-
|
|
153
|
-
sink(debug);
|
|
154
|
-
await sink[Symbol.asyncDispose]();
|
|
155
|
-
await sink[Symbol.asyncDispose](); // Should not throw
|
|
156
|
-
|
|
157
|
-
await delay(50);
|
|
158
|
-
|
|
159
|
-
const content = fs.readFileSync(path, { encoding: "utf-8" });
|
|
160
|
-
const lines = content.split("\n").filter((line) => line.length > 0);
|
|
161
|
-
assertEquals(lines.length, 1);
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
test("getStreamFileSink() handles rapid disposal", async () => {
|
|
165
|
-
const path = makeTempFileSync();
|
|
166
|
-
const sink = getStreamFileSink(path);
|
|
167
|
-
|
|
168
|
-
sink(debug);
|
|
169
|
-
// Dispose immediately without waiting
|
|
170
|
-
await sink[Symbol.asyncDispose]();
|
|
171
|
-
|
|
172
|
-
await delay(50);
|
|
173
|
-
|
|
174
|
-
const content = fs.readFileSync(path, { encoding: "utf-8" });
|
|
175
|
-
assert(content.includes("Hello, 123 & 456!"));
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
test("getStreamFileSink() concurrent writes", async () => {
|
|
179
|
-
const path = makeTempFileSync();
|
|
180
|
-
const sink = getStreamFileSink(path);
|
|
181
|
-
|
|
182
|
-
// Simulate concurrent logging from different parts of application
|
|
183
|
-
const promises = [];
|
|
184
|
-
for (let i = 0; i < 10; i++) {
|
|
185
|
-
promises.push(
|
|
186
|
-
new Promise<void>((resolve) => {
|
|
187
|
-
setTimeout(() => {
|
|
188
|
-
const record: LogRecord = {
|
|
189
|
-
...debug,
|
|
190
|
-
message: [`Concurrent log ${i}`],
|
|
191
|
-
};
|
|
192
|
-
sink(record);
|
|
193
|
-
resolve();
|
|
194
|
-
}, Math.random() * 10);
|
|
195
|
-
}),
|
|
196
|
-
);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
await Promise.all(promises);
|
|
200
|
-
await sink[Symbol.asyncDispose]();
|
|
201
|
-
await delay(100);
|
|
202
|
-
|
|
203
|
-
const content = fs.readFileSync(path, { encoding: "utf-8" });
|
|
204
|
-
const lines = content.split("\n").filter((line) => line.length > 0);
|
|
205
|
-
assertEquals(lines.length, 10);
|
|
206
|
-
|
|
207
|
-
// All concurrent logs should be present
|
|
208
|
-
for (let i = 0; i < 10; i++) {
|
|
209
|
-
assert(content.includes(`Concurrent log ${i}`));
|
|
210
|
-
}
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
test("getStreamFileSink() with empty records", async () => {
|
|
214
|
-
const path = makeTempFileSync();
|
|
215
|
-
const sink = getStreamFileSink(path);
|
|
216
|
-
|
|
217
|
-
const emptyRecord: LogRecord = {
|
|
218
|
-
...debug,
|
|
219
|
-
message: [""],
|
|
220
|
-
};
|
|
221
|
-
|
|
222
|
-
sink(emptyRecord);
|
|
223
|
-
await sink[Symbol.asyncDispose]();
|
|
224
|
-
|
|
225
|
-
await delay(50);
|
|
226
|
-
|
|
227
|
-
const content = fs.readFileSync(path, { encoding: "utf-8" });
|
|
228
|
-
assert(content.includes("[DBG]"));
|
|
229
|
-
// Should still write the timestamp and level even with empty message
|
|
230
|
-
assert(content.includes("2023-11-14 22:13:20.000 +00:00"));
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
test("getStreamFileSink() with large messages", async () => {
|
|
234
|
-
const path = makeTempFileSync();
|
|
235
|
-
const sink = getStreamFileSink(path);
|
|
236
|
-
|
|
237
|
-
const largeMessage = "x".repeat(10000);
|
|
238
|
-
const largeRecord: LogRecord = {
|
|
239
|
-
...debug,
|
|
240
|
-
message: [largeMessage],
|
|
241
|
-
};
|
|
242
|
-
|
|
243
|
-
sink(largeRecord);
|
|
244
|
-
await sink[Symbol.asyncDispose]();
|
|
245
|
-
|
|
246
|
-
await delay(100); // Give more time for large write
|
|
247
|
-
|
|
248
|
-
const content = fs.readFileSync(path, { encoding: "utf-8" });
|
|
249
|
-
assert(content.includes(largeMessage));
|
|
250
|
-
assert(content.includes("[DBG]"));
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
test("getStreamFileSink() memory efficiency", async () => {
|
|
254
|
-
const path = makeTempFileSync();
|
|
255
|
-
const sink = getStreamFileSink(path);
|
|
256
|
-
|
|
257
|
-
// Create many small records to test memory usage
|
|
258
|
-
for (let i = 0; i < 1000; i++) {
|
|
259
|
-
const record: LogRecord = {
|
|
260
|
-
...debug,
|
|
261
|
-
message: [`Memory test ${i}`],
|
|
262
|
-
};
|
|
263
|
-
sink(record);
|
|
264
|
-
|
|
265
|
-
// Occasionally allow event loop to process
|
|
266
|
-
if (i % 100 === 0) {
|
|
267
|
-
await delay(1);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// Use async disposal to ensure all streams are properly flushed
|
|
272
|
-
await sink[Symbol.asyncDispose]();
|
|
273
|
-
|
|
274
|
-
const content = fs.readFileSync(path, { encoding: "utf-8" });
|
|
275
|
-
const lines = content.split("\n").filter((line) => line.length > 0);
|
|
276
|
-
assertEquals(lines.length, 1000);
|
|
277
|
-
|
|
278
|
-
// Verify first and last entries
|
|
279
|
-
assert(lines[0].includes("Memory test 0"));
|
|
280
|
-
assert(lines[999].includes("Memory test 999"));
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
test("getStreamFileSink() creates new file when it doesn't exist", async () => {
|
|
284
|
-
// Use a file that doesn't exist yet
|
|
285
|
-
const tempDir = fs.mkdtempSync(join(tmpdir(), "logtape-"));
|
|
286
|
-
const path = join(tempDir, "new-file.log");
|
|
287
|
-
|
|
288
|
-
const sink = getStreamFileSink(path);
|
|
289
|
-
sink(debug);
|
|
290
|
-
await sink[Symbol.asyncDispose]();
|
|
291
|
-
|
|
292
|
-
await delay(50);
|
|
293
|
-
|
|
294
|
-
// File should have been created
|
|
295
|
-
assert(fs.existsSync(path));
|
|
296
|
-
const content = fs.readFileSync(path, { encoding: "utf-8" });
|
|
297
|
-
assert(content.includes("Hello, 123 & 456!"));
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
test("getStreamFileSink() multiple instances on same file", async () => {
|
|
301
|
-
const path = makeTempFileSync();
|
|
302
|
-
|
|
303
|
-
const sink1 = getStreamFileSink(path);
|
|
304
|
-
const sink2 = getStreamFileSink(path);
|
|
305
|
-
|
|
306
|
-
sink1(debug);
|
|
307
|
-
sink2(info);
|
|
308
|
-
|
|
309
|
-
await sink1[Symbol.asyncDispose]();
|
|
310
|
-
await sink2[Symbol.asyncDispose]();
|
|
311
|
-
|
|
312
|
-
await delay(100);
|
|
313
|
-
|
|
314
|
-
const content = fs.readFileSync(path, { encoding: "utf-8" });
|
|
315
|
-
assert(content.includes("[DBG]"));
|
|
316
|
-
assert(content.includes("[INF]"));
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
test("getStreamFileSink() stream error handling", async () => {
|
|
320
|
-
const path = makeTempFileSync();
|
|
321
|
-
const sink = getStreamFileSink(path);
|
|
322
|
-
|
|
323
|
-
sink(debug);
|
|
324
|
-
await sink[Symbol.asyncDispose]();
|
|
325
|
-
await delay(50);
|
|
326
|
-
|
|
327
|
-
// Delete the file after disposal
|
|
328
|
-
try {
|
|
329
|
-
fs.unlinkSync(path);
|
|
330
|
-
} catch {
|
|
331
|
-
// Ignore if file doesn't exist
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// These writes after disposal should be ignored
|
|
335
|
-
sink(info);
|
|
336
|
-
sink(warning);
|
|
337
|
-
|
|
338
|
-
// Test should complete without throwing
|
|
339
|
-
assert(true);
|
|
340
|
-
});
|
package/src/streamfilesink.ts
DELETED
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
defaultTextFormatter,
|
|
3
|
-
type LogRecord,
|
|
4
|
-
type Sink,
|
|
5
|
-
type TextFormatter,
|
|
6
|
-
} from "@logtape/logtape";
|
|
7
|
-
import { createWriteStream } from "node:fs";
|
|
8
|
-
import { PassThrough } from "node:stream";
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Options for the {@link getStreamFileSink} function.
|
|
12
|
-
*
|
|
13
|
-
* This interface configures the high-performance stream-based file sink that
|
|
14
|
-
* uses Node.js PassThrough streams for optimal I/O performance with automatic
|
|
15
|
-
* backpressure management.
|
|
16
|
-
*
|
|
17
|
-
* @since 1.0.0
|
|
18
|
-
*/
|
|
19
|
-
export interface StreamFileSinkOptions {
|
|
20
|
-
/**
|
|
21
|
-
* High water mark for the PassThrough stream buffer in bytes.
|
|
22
|
-
*
|
|
23
|
-
* This controls the internal buffer size of the PassThrough stream.
|
|
24
|
-
* Higher values can improve performance for high-volume logging but use
|
|
25
|
-
* more memory. Lower values reduce memory usage but may impact performance.
|
|
26
|
-
*
|
|
27
|
-
* @default 16384
|
|
28
|
-
* @since 1.0.0
|
|
29
|
-
*/
|
|
30
|
-
readonly highWaterMark?: number;
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* A custom formatter for log records.
|
|
34
|
-
*
|
|
35
|
-
* If not specified, the default text formatter will be used, which formats
|
|
36
|
-
* records in the standard LogTape format with timestamp, level, category,
|
|
37
|
-
* and message.
|
|
38
|
-
*
|
|
39
|
-
* @default defaultTextFormatter
|
|
40
|
-
* @since 1.0.0
|
|
41
|
-
*/
|
|
42
|
-
readonly formatter?: TextFormatter;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Create a high-performance stream-based file sink that writes log records to a file.
|
|
47
|
-
*
|
|
48
|
-
* This sink uses Node.js PassThrough streams piped to WriteStreams for optimal
|
|
49
|
-
* I/O performance. It leverages the Node.js stream infrastructure to provide
|
|
50
|
-
* automatic backpressure management, efficient buffering, and asynchronous writes
|
|
51
|
-
* without blocking the main thread.
|
|
52
|
-
*
|
|
53
|
-
* ## Performance Characteristics
|
|
54
|
-
*
|
|
55
|
-
* - **High Performance**: Optimized for high-volume logging scenarios
|
|
56
|
-
* - **Non-blocking**: Uses asynchronous I/O that doesn't block the main thread
|
|
57
|
-
* - **Memory Efficient**: Automatic backpressure prevents memory buildup
|
|
58
|
-
* - **Stream-based**: Leverages Node.js native stream optimizations
|
|
59
|
-
*
|
|
60
|
-
* ## When to Use
|
|
61
|
-
*
|
|
62
|
-
* Use this sink when you need:
|
|
63
|
-
* - High-performance file logging for production applications
|
|
64
|
-
* - Non-blocking I/O behavior for real-time applications
|
|
65
|
-
* - Automatic backpressure handling for high-volume scenarios
|
|
66
|
-
* - Simple file output without complex buffering configuration
|
|
67
|
-
*
|
|
68
|
-
* For more control over buffering behavior, consider using {@link getFileSink}
|
|
69
|
-
* instead, which provides options for buffer size, flush intervals, and
|
|
70
|
-
* non-blocking modes.
|
|
71
|
-
*
|
|
72
|
-
* ## Example
|
|
73
|
-
*
|
|
74
|
-
* ```typescript
|
|
75
|
-
* import { configure } from "@logtape/logtape";
|
|
76
|
-
* import { getStreamFileSink } from "@logtape/file";
|
|
77
|
-
*
|
|
78
|
-
* await configure({
|
|
79
|
-
* sinks: {
|
|
80
|
-
* file: getStreamFileSink("app.log", {
|
|
81
|
-
* highWaterMark: 32768 // 32KB buffer for high-volume logging
|
|
82
|
-
* })
|
|
83
|
-
* },
|
|
84
|
-
* loggers: [
|
|
85
|
-
* { category: ["myapp"], sinks: ["file"] }
|
|
86
|
-
* ]
|
|
87
|
-
* });
|
|
88
|
-
* ```
|
|
89
|
-
*
|
|
90
|
-
* @param path The path to the file to write logs to. The file will be created
|
|
91
|
-
* if it doesn't exist, or appended to if it does exist.
|
|
92
|
-
* @param options Configuration options for the stream-based sink.
|
|
93
|
-
* @returns A sink that writes formatted log records to the specified file.
|
|
94
|
-
* The returned sink implements `AsyncDisposable` for proper resource cleanup
|
|
95
|
-
* that waits for all data to be flushed to disk.
|
|
96
|
-
*
|
|
97
|
-
* @since 1.0.0
|
|
98
|
-
*/
|
|
99
|
-
export function getStreamFileSink(
|
|
100
|
-
path: string,
|
|
101
|
-
options: StreamFileSinkOptions = {},
|
|
102
|
-
): Sink & AsyncDisposable {
|
|
103
|
-
const highWaterMark = options.highWaterMark ?? 16384;
|
|
104
|
-
const formatter = options.formatter ?? defaultTextFormatter;
|
|
105
|
-
|
|
106
|
-
// Create PassThrough stream for optimal performance
|
|
107
|
-
const passThrough = new PassThrough({
|
|
108
|
-
highWaterMark,
|
|
109
|
-
objectMode: false,
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
// Create WriteStream immediately (not lazy)
|
|
113
|
-
const writeStream = createWriteStream(path, { flags: "a" });
|
|
114
|
-
|
|
115
|
-
// Pipe PassThrough to WriteStream for automatic backpressure handling
|
|
116
|
-
passThrough.pipe(writeStream);
|
|
117
|
-
|
|
118
|
-
let disposed = false;
|
|
119
|
-
|
|
120
|
-
// Stream-based sink function for high performance
|
|
121
|
-
const sink: Sink & AsyncDisposable = (record: LogRecord) => {
|
|
122
|
-
if (disposed) return;
|
|
123
|
-
|
|
124
|
-
// Direct write to PassThrough stream
|
|
125
|
-
passThrough.write(formatter(record));
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
// Asynchronous disposal with sequential stream closure
|
|
129
|
-
sink[Symbol.asyncDispose] = async () => {
|
|
130
|
-
if (disposed) return;
|
|
131
|
-
disposed = true;
|
|
132
|
-
|
|
133
|
-
// Wait for PassThrough to drain if needed
|
|
134
|
-
if (passThrough.writableNeedDrain) {
|
|
135
|
-
await new Promise<void>((resolve) => {
|
|
136
|
-
passThrough.once("drain", resolve);
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// End the PassThrough stream first and wait for it to finish
|
|
141
|
-
await new Promise<void>((resolve) => {
|
|
142
|
-
passThrough.once("finish", resolve);
|
|
143
|
-
passThrough.end();
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
// Wait for WriteStream to finish and close (piped streams auto-close)
|
|
147
|
-
await new Promise<void>((resolve) => {
|
|
148
|
-
if (writeStream.closed || writeStream.destroyed) {
|
|
149
|
-
resolve();
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
writeStream.once("close", resolve);
|
|
153
|
-
});
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
return sink;
|
|
157
|
-
}
|
package/tsdown.config.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { defineConfig } from "tsdown";
|
|
2
|
-
|
|
3
|
-
export default defineConfig({
|
|
4
|
-
entry: ["src/mod.ts", "src/filesink.deno.ts", "src/filesink.node.ts"],
|
|
5
|
-
dts: {
|
|
6
|
-
sourcemap: true,
|
|
7
|
-
},
|
|
8
|
-
format: ["esm", "cjs"],
|
|
9
|
-
platform: "node",
|
|
10
|
-
unbundle: true,
|
|
11
|
-
inputOptions: {
|
|
12
|
-
onLog(level, log, defaultHandler) {
|
|
13
|
-
if (
|
|
14
|
-
level === "warn" && log.code === "UNRESOLVED_IMPORT" &&
|
|
15
|
-
log.exporter === "#filesink"
|
|
16
|
-
) {
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
|
-
defaultHandler(level, log);
|
|
20
|
-
},
|
|
21
|
-
},
|
|
22
|
-
});
|