@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.
@@ -1,820 +0,0 @@
1
- import { getFileSink, getRotatingFileSink } from "#filesink";
2
- import { suite } from "@alinea/suite";
3
- import { isDeno } from "@david/which-runtime";
4
- import type { Sink } from "@logtape/logtape";
5
- import { assert } from "@std/assert/assert";
6
- import { assertEquals } from "@std/assert/equals";
7
- import { assertThrows } from "@std/assert/throws";
8
- import { delay } from "@std/async/delay";
9
- import { join } from "@std/path/join";
10
- import fs from "node:fs";
11
- import { tmpdir } from "node:os";
12
- import {
13
- debug,
14
- error,
15
- fatal,
16
- info,
17
- warning,
18
- } from "../../logtape/src/fixtures.ts";
19
- import { type FileSinkDriver, getBaseFileSink } from "./filesink.base.ts";
20
-
21
- const test = suite(import.meta);
22
-
23
- function makeTempFileSync(): string {
24
- return join(fs.mkdtempSync(join(tmpdir(), "logtape-")), "logtape.txt");
25
- }
26
-
27
- test("getBaseFileSink()", () => {
28
- const path = makeTempFileSync();
29
- let sink: Sink & Disposable;
30
- if (isDeno) {
31
- const driver: FileSinkDriver<Deno.FsFile> = {
32
- openSync(path: string) {
33
- return Deno.openSync(path, { create: true, append: true });
34
- },
35
- writeSync(fd, chunk) {
36
- fd.writeSync(chunk);
37
- },
38
- flushSync(fd) {
39
- fd.syncSync();
40
- },
41
- closeSync(fd) {
42
- fd.close();
43
- },
44
- };
45
- sink = getBaseFileSink(path, driver);
46
- } else {
47
- const driver: FileSinkDriver<number> = {
48
- openSync(path: string) {
49
- return fs.openSync(path, "a");
50
- },
51
- writeSync: fs.writeSync,
52
- flushSync: fs.fsyncSync,
53
- closeSync: fs.closeSync,
54
- };
55
- sink = getBaseFileSink(path, driver);
56
- }
57
- sink(debug);
58
- sink(info);
59
- sink(warning);
60
- sink(error);
61
- sink(fatal);
62
- sink[Symbol.dispose]();
63
- assertEquals(
64
- fs.readFileSync(path, { encoding: "utf-8" }),
65
- `\
66
- 2023-11-14 22:13:20.000 +00:00 [DBG] my-app·junk: Hello, 123 & 456!
67
- 2023-11-14 22:13:20.000 +00:00 [INF] my-app·junk: Hello, 123 & 456!
68
- 2023-11-14 22:13:20.000 +00:00 [WRN] my-app·junk: Hello, 123 & 456!
69
- 2023-11-14 22:13:20.000 +00:00 [ERR] my-app·junk: Hello, 123 & 456!
70
- 2023-11-14 22:13:20.000 +00:00 [FTL] my-app·junk: Hello, 123 & 456!
71
- `,
72
- );
73
- });
74
-
75
- test("getBaseFileSink() with lazy option", () => {
76
- const pathDir = fs.mkdtempSync(join(tmpdir(), "logtape-"));
77
- const path = join(pathDir, "test.log");
78
- let sink: Sink & Disposable;
79
- if (isDeno) {
80
- const driver: FileSinkDriver<Deno.FsFile> = {
81
- openSync(path: string) {
82
- return Deno.openSync(path, { create: true, append: true });
83
- },
84
- writeSync(fd, chunk) {
85
- fd.writeSync(chunk);
86
- },
87
- flushSync(fd) {
88
- fd.syncSync();
89
- },
90
- closeSync(fd) {
91
- fd.close();
92
- },
93
- };
94
- sink = getBaseFileSink(path, { ...driver, lazy: true });
95
- } else {
96
- const driver: FileSinkDriver<number> = {
97
- openSync(path: string) {
98
- return fs.openSync(path, "a");
99
- },
100
- writeSync: fs.writeSync,
101
- flushSync: fs.fsyncSync,
102
- closeSync: fs.closeSync,
103
- };
104
- sink = getBaseFileSink(path, { ...driver, lazy: true });
105
- }
106
- if (isDeno) {
107
- assertThrows(
108
- () => Deno.lstatSync(path),
109
- Deno.errors.NotFound,
110
- );
111
- } else {
112
- assertEquals(fs.existsSync(path), false);
113
- }
114
- sink(debug);
115
- sink(info);
116
- sink(warning);
117
- sink(error);
118
- sink(fatal);
119
- sink[Symbol.dispose]();
120
- assertEquals(
121
- fs.readFileSync(path, { encoding: "utf-8" }),
122
- `\
123
- 2023-11-14 22:13:20.000 +00:00 [DBG] my-app·junk: Hello, 123 & 456!
124
- 2023-11-14 22:13:20.000 +00:00 [INF] my-app·junk: Hello, 123 & 456!
125
- 2023-11-14 22:13:20.000 +00:00 [WRN] my-app·junk: Hello, 123 & 456!
126
- 2023-11-14 22:13:20.000 +00:00 [ERR] my-app·junk: Hello, 123 & 456!
127
- 2023-11-14 22:13:20.000 +00:00 [FTL] my-app·junk: Hello, 123 & 456!
128
- `,
129
- );
130
- });
131
-
132
- test("getFileSink()", () => {
133
- const path = makeTempFileSync();
134
- const sink: Sink & Disposable = getFileSink(path);
135
- sink(debug);
136
- sink(info);
137
- sink(warning);
138
- sink(error);
139
- sink(fatal);
140
- sink[Symbol.dispose]();
141
- assertEquals(
142
- fs.readFileSync(path, { encoding: "utf-8" }),
143
- `\
144
- 2023-11-14 22:13:20.000 +00:00 [DBG] my-app·junk: Hello, 123 & 456!
145
- 2023-11-14 22:13:20.000 +00:00 [INF] my-app·junk: Hello, 123 & 456!
146
- 2023-11-14 22:13:20.000 +00:00 [WRN] my-app·junk: Hello, 123 & 456!
147
- 2023-11-14 22:13:20.000 +00:00 [ERR] my-app·junk: Hello, 123 & 456!
148
- 2023-11-14 22:13:20.000 +00:00 [FTL] my-app·junk: Hello, 123 & 456!
149
- `,
150
- );
151
- });
152
-
153
- test("getFileSink() with bufferSize: 0 (no buffering)", () => {
154
- const path = makeTempFileSync();
155
- const sink: Sink & Disposable = getFileSink(path, { bufferSize: 0 });
156
-
157
- // Write first log entry
158
- sink(debug);
159
- // With no buffering, content should be immediately written to file
160
- assertEquals(
161
- fs.readFileSync(path, { encoding: "utf-8" }),
162
- "2023-11-14 22:13:20.000 +00:00 [DBG] my-app·junk: Hello, 123 & 456!\n",
163
- );
164
-
165
- // Write second log entry
166
- sink(info);
167
- assertEquals(
168
- fs.readFileSync(path, { encoding: "utf-8" }),
169
- `\
170
- 2023-11-14 22:13:20.000 +00:00 [DBG] my-app·junk: Hello, 123 & 456!
171
- 2023-11-14 22:13:20.000 +00:00 [INF] my-app·junk: Hello, 123 & 456!
172
- `,
173
- );
174
-
175
- // Write remaining entries
176
- sink(warning);
177
- sink(error);
178
- sink(fatal);
179
- sink[Symbol.dispose]();
180
-
181
- // Final verification
182
- assertEquals(
183
- fs.readFileSync(path, { encoding: "utf-8" }),
184
- `\
185
- 2023-11-14 22:13:20.000 +00:00 [DBG] my-app·junk: Hello, 123 & 456!
186
- 2023-11-14 22:13:20.000 +00:00 [INF] my-app·junk: Hello, 123 & 456!
187
- 2023-11-14 22:13:20.000 +00:00 [WRN] my-app·junk: Hello, 123 & 456!
188
- 2023-11-14 22:13:20.000 +00:00 [ERR] my-app·junk: Hello, 123 & 456!
189
- 2023-11-14 22:13:20.000 +00:00 [FTL] my-app·junk: Hello, 123 & 456!
190
- `,
191
- );
192
- });
193
-
194
- test("getFileSink() with small buffer size", () => {
195
- const path = makeTempFileSync();
196
- // Use a small buffer size (100 characters) to test buffering behavior
197
- const sink: Sink & Disposable = getFileSink(path, { bufferSize: 100 });
198
-
199
- // Write first log entry (about 65 characters)
200
- sink(debug);
201
- // Should be buffered, not yet written to file
202
- assertEquals(fs.readFileSync(path, { encoding: "utf-8" }), "");
203
-
204
- // Write second log entry - this should exceed buffer size and trigger flush
205
- sink(info);
206
- // Both entries should now be written to file
207
- assertEquals(
208
- fs.readFileSync(path, { encoding: "utf-8" }),
209
- `\
210
- 2023-11-14 22:13:20.000 +00:00 [DBG] my-app·junk: Hello, 123 & 456!
211
- 2023-11-14 22:13:20.000 +00:00 [INF] my-app·junk: Hello, 123 & 456!
212
- `,
213
- );
214
-
215
- // Write third log entry - should be buffered again
216
- sink(warning);
217
- // Should still only have the first two entries
218
- assertEquals(
219
- fs.readFileSync(path, { encoding: "utf-8" }),
220
- `\
221
- 2023-11-14 22:13:20.000 +00:00 [DBG] my-app·junk: Hello, 123 & 456!
222
- 2023-11-14 22:13:20.000 +00:00 [INF] my-app·junk: Hello, 123 & 456!
223
- `,
224
- );
225
-
226
- // Dispose should flush remaining buffer content
227
- sink[Symbol.dispose]();
228
- assertEquals(
229
- fs.readFileSync(path, { encoding: "utf-8" }),
230
- `\
231
- 2023-11-14 22:13:20.000 +00:00 [DBG] my-app·junk: Hello, 123 & 456!
232
- 2023-11-14 22:13:20.000 +00:00 [INF] my-app·junk: Hello, 123 & 456!
233
- 2023-11-14 22:13:20.000 +00:00 [WRN] my-app·junk: Hello, 123 & 456!
234
- `,
235
- );
236
- });
237
-
238
- test("getRotatingFileSink() with bufferSize: 0 (no buffering)", () => {
239
- const path = makeTempFileSync();
240
- const sink: Sink & Disposable = getRotatingFileSink(path, {
241
- maxSize: 150,
242
- bufferSize: 0, // No buffering - immediate writes
243
- });
244
-
245
- // Write first log entry - should be immediately written
246
- sink(debug);
247
- assertEquals(
248
- fs.readFileSync(path, { encoding: "utf-8" }),
249
- "2023-11-14 22:13:20.000 +00:00 [DBG] my-app·junk: Hello, 123 & 456!\n",
250
- );
251
-
252
- // Write second log entry - should be immediately written
253
- sink(info);
254
- assertEquals(
255
- fs.readFileSync(path, { encoding: "utf-8" }),
256
- `\
257
- 2023-11-14 22:13:20.000 +00:00 [DBG] my-app·junk: Hello, 123 & 456!
258
- 2023-11-14 22:13:20.000 +00:00 [INF] my-app·junk: Hello, 123 & 456!
259
- `,
260
- );
261
-
262
- // Write third log entry - should trigger rotation
263
- sink(warning);
264
- assertEquals(
265
- fs.readFileSync(path, { encoding: "utf-8" }),
266
- "2023-11-14 22:13:20.000 +00:00 [WRN] my-app·junk: Hello, 123 & 456!\n",
267
- );
268
- // Check that rotation occurred
269
- assertEquals(
270
- fs.readFileSync(`${path}.1`, { encoding: "utf-8" }),
271
- `\
272
- 2023-11-14 22:13:20.000 +00:00 [DBG] my-app·junk: Hello, 123 & 456!
273
- 2023-11-14 22:13:20.000 +00:00 [INF] my-app·junk: Hello, 123 & 456!
274
- `,
275
- );
276
-
277
- sink[Symbol.dispose]();
278
- });
279
-
280
- test("getRotatingFileSink() with small buffer size", () => {
281
- const path = makeTempFileSync();
282
- const sink: Sink & Disposable = getRotatingFileSink(path, {
283
- maxSize: 200, // Larger maxSize to allow for buffering tests
284
- bufferSize: 100, // Small buffer to test interaction with rotation
285
- });
286
-
287
- // Write first log entry - should be buffered
288
- sink(debug);
289
- assertEquals(fs.readFileSync(path, { encoding: "utf-8" }), "");
290
-
291
- // Write second log entry - should trigger buffer flush
292
- sink(info);
293
- assertEquals(
294
- fs.readFileSync(path, { encoding: "utf-8" }),
295
- `\
296
- 2023-11-14 22:13:20.000 +00:00 [DBG] my-app·junk: Hello, 123 & 456!
297
- 2023-11-14 22:13:20.000 +00:00 [INF] my-app·junk: Hello, 123 & 456!
298
- `,
299
- );
300
-
301
- // Write third log entry - should be buffered again
302
- sink(warning);
303
- assertEquals(
304
- fs.readFileSync(path, { encoding: "utf-8" }),
305
- `\
306
- 2023-11-14 22:13:20.000 +00:00 [DBG] my-app·junk: Hello, 123 & 456!
307
- 2023-11-14 22:13:20.000 +00:00 [INF] my-app·junk: Hello, 123 & 456!
308
- `,
309
- );
310
-
311
- // Write fourth log entry - should flush buffer and likely trigger rotation
312
- sink(error);
313
-
314
- // Dispose should flush any remaining buffer content
315
- sink[Symbol.dispose]();
316
-
317
- // Verify final state - all entries should be written somewhere
318
- const mainContent = fs.readFileSync(path, { encoding: "utf-8" });
319
- let rotatedContent = "";
320
- try {
321
- rotatedContent = fs.readFileSync(`${path}.1`, { encoding: "utf-8" });
322
- } catch {
323
- // No rotation occurred
324
- }
325
-
326
- const allContent = mainContent + rotatedContent;
327
- // All four log entries should be present exactly once in either main or rotated file
328
- const expectedEntries = [
329
- "2023-11-14 22:13:20.000 +00:00 [DBG] my-app·junk: Hello, 123 & 456!\n",
330
- "2023-11-14 22:13:20.000 +00:00 [INF] my-app·junk: Hello, 123 & 456!\n",
331
- "2023-11-14 22:13:20.000 +00:00 [WRN] my-app·junk: Hello, 123 & 456!\n",
332
- "2023-11-14 22:13:20.000 +00:00 [ERR] my-app·junk: Hello, 123 & 456!\n",
333
- ];
334
-
335
- for (const entry of expectedEntries) {
336
- assertEquals(
337
- allContent.includes(entry),
338
- true,
339
- `Missing log entry: ${entry.trim()}`,
340
- );
341
- }
342
-
343
- // Verify each entry appears exactly once
344
- for (const entry of expectedEntries) {
345
- const firstIndex = allContent.indexOf(entry);
346
- const lastIndex = allContent.lastIndexOf(entry);
347
- assertEquals(firstIndex, lastIndex, `Duplicate log entry: ${entry.trim()}`);
348
- }
349
- });
350
-
351
- test("getRotatingFileSink()", () => {
352
- const path = makeTempFileSync();
353
- const sink: Sink & Disposable = getRotatingFileSink(path, {
354
- maxSize: 150,
355
- bufferSize: 0, // Disable buffering for this test to maintain existing behavior
356
- });
357
- sink(debug);
358
- assertEquals(
359
- fs.readFileSync(path, { encoding: "utf-8" }),
360
- "2023-11-14 22:13:20.000 +00:00 [DBG] my-app·junk: Hello, 123 & 456!\n",
361
- );
362
- sink(info);
363
- assertEquals(
364
- fs.readFileSync(path, { encoding: "utf-8" }),
365
- `\
366
- 2023-11-14 22:13:20.000 +00:00 [DBG] my-app·junk: Hello, 123 & 456!
367
- 2023-11-14 22:13:20.000 +00:00 [INF] my-app·junk: Hello, 123 & 456!
368
- `,
369
- );
370
- sink(warning);
371
- assertEquals(
372
- fs.readFileSync(path, { encoding: "utf-8" }),
373
- "2023-11-14 22:13:20.000 +00:00 [WRN] my-app·junk: Hello, 123 & 456!\n",
374
- );
375
- assertEquals(
376
- fs.readFileSync(`${path}.1`, { encoding: "utf-8" }),
377
- `\
378
- 2023-11-14 22:13:20.000 +00:00 [DBG] my-app·junk: Hello, 123 & 456!
379
- 2023-11-14 22:13:20.000 +00:00 [INF] my-app·junk: Hello, 123 & 456!
380
- `,
381
- );
382
- sink(error);
383
- assertEquals(
384
- fs.readFileSync(path, { encoding: "utf-8" }),
385
- `\
386
- 2023-11-14 22:13:20.000 +00:00 [WRN] my-app·junk: Hello, 123 & 456!
387
- 2023-11-14 22:13:20.000 +00:00 [ERR] my-app·junk: Hello, 123 & 456!
388
- `,
389
- );
390
- assertEquals(
391
- fs.readFileSync(`${path}.1`, { encoding: "utf-8" }),
392
- `\
393
- 2023-11-14 22:13:20.000 +00:00 [DBG] my-app·junk: Hello, 123 & 456!
394
- 2023-11-14 22:13:20.000 +00:00 [INF] my-app·junk: Hello, 123 & 456!
395
- `,
396
- );
397
- sink(fatal);
398
- sink[Symbol.dispose]();
399
- assertEquals(
400
- fs.readFileSync(path, { encoding: "utf-8" }),
401
- "2023-11-14 22:13:20.000 +00:00 [FTL] my-app·junk: Hello, 123 & 456!\n",
402
- );
403
- assertEquals(
404
- fs.readFileSync(`${path}.1`, { encoding: "utf-8" }),
405
- `\
406
- 2023-11-14 22:13:20.000 +00:00 [WRN] my-app·junk: Hello, 123 & 456!
407
- 2023-11-14 22:13:20.000 +00:00 [ERR] my-app·junk: Hello, 123 & 456!
408
- `,
409
- );
410
- assertEquals(
411
- fs.readFileSync(`${path}.2`, { encoding: "utf-8" }),
412
- `\
413
- 2023-11-14 22:13:20.000 +00:00 [DBG] my-app·junk: Hello, 123 & 456!
414
- 2023-11-14 22:13:20.000 +00:00 [INF] my-app·junk: Hello, 123 & 456!
415
- `,
416
- );
417
-
418
- const dirPath = fs.mkdtempSync(join(tmpdir(), "logtape-"));
419
- const path2 = join(dirPath, "log");
420
- const sink2: Sink & Disposable = getRotatingFileSink(path2, {
421
- maxSize: 150,
422
- bufferSize: 0, // Disable buffering for this test to maintain existing behavior
423
- });
424
- sink2(debug);
425
- assertEquals(
426
- fs.readFileSync(path2, { encoding: "utf-8" }),
427
- "2023-11-14 22:13:20.000 +00:00 [DBG] my-app·junk: Hello, 123 & 456!\n",
428
- );
429
- sink2[Symbol.dispose]();
430
- });
431
-
432
- test("getBaseFileSink() with buffer edge cases", () => {
433
- // Test negative bufferSize (should behave like bufferSize: 0)
434
- const path1 = makeTempFileSync();
435
- let sink1: Sink & Disposable;
436
- if (isDeno) {
437
- const driver: FileSinkDriver<Deno.FsFile> = {
438
- openSync(path: string) {
439
- return Deno.openSync(path, { create: true, append: true });
440
- },
441
- writeSync(fd, chunk) {
442
- fd.writeSync(chunk);
443
- },
444
- flushSync(fd) {
445
- fd.syncSync();
446
- },
447
- closeSync(fd) {
448
- fd.close();
449
- },
450
- };
451
- sink1 = getBaseFileSink(path1, { ...driver, bufferSize: -10 });
452
- } else {
453
- const driver: FileSinkDriver<number> = {
454
- openSync(path: string) {
455
- return fs.openSync(path, "a");
456
- },
457
- writeSync: fs.writeSync,
458
- flushSync: fs.fsyncSync,
459
- closeSync: fs.closeSync,
460
- };
461
- sink1 = getBaseFileSink(path1, { ...driver, bufferSize: -10 });
462
- }
463
-
464
- sink1(debug);
465
- // With negative bufferSize, should write immediately
466
- assertEquals(
467
- fs.readFileSync(path1, { encoding: "utf-8" }),
468
- "2023-11-14 22:13:20.000 +00:00 [DBG] my-app·junk: Hello, 123 & 456!\n",
469
- );
470
- sink1[Symbol.dispose]();
471
-
472
- // Test bufferSize of 1 (very small)
473
- const path2 = makeTempFileSync();
474
- let sink2: Sink & Disposable;
475
- if (isDeno) {
476
- const driver: FileSinkDriver<Deno.FsFile> = {
477
- openSync(path: string) {
478
- return Deno.openSync(path, { create: true, append: true });
479
- },
480
- writeSync(fd, chunk) {
481
- fd.writeSync(chunk);
482
- },
483
- flushSync(fd) {
484
- fd.syncSync();
485
- },
486
- closeSync(fd) {
487
- fd.close();
488
- },
489
- };
490
- sink2 = getBaseFileSink(path2, { ...driver, bufferSize: 1 });
491
- } else {
492
- const driver: FileSinkDriver<number> = {
493
- openSync(path: string) {
494
- return fs.openSync(path, "a");
495
- },
496
- writeSync: fs.writeSync,
497
- flushSync: fs.fsyncSync,
498
- closeSync: fs.closeSync,
499
- };
500
- sink2 = getBaseFileSink(path2, { ...driver, bufferSize: 1 });
501
- }
502
-
503
- sink2(debug);
504
- // With bufferSize of 1, should write immediately since log entry > 1 char
505
- assertEquals(
506
- fs.readFileSync(path2, { encoding: "utf-8" }),
507
- "2023-11-14 22:13:20.000 +00:00 [DBG] my-app·junk: Hello, 123 & 456!\n",
508
- );
509
- sink2[Symbol.dispose]();
510
-
511
- // Test very large bufferSize
512
- const path3 = makeTempFileSync();
513
- let sink3: Sink & Disposable;
514
- if (isDeno) {
515
- const driver: FileSinkDriver<Deno.FsFile> = {
516
- openSync(path: string) {
517
- return Deno.openSync(path, { create: true, append: true });
518
- },
519
- writeSync(fd, chunk) {
520
- fd.writeSync(chunk);
521
- },
522
- flushSync(fd) {
523
- fd.syncSync();
524
- },
525
- closeSync(fd) {
526
- fd.close();
527
- },
528
- };
529
- sink3 = getBaseFileSink(path3, { ...driver, bufferSize: 10000 });
530
- } else {
531
- const driver: FileSinkDriver<number> = {
532
- openSync(path: string) {
533
- return fs.openSync(path, "a");
534
- },
535
- writeSync: fs.writeSync,
536
- flushSync: fs.fsyncSync,
537
- closeSync: fs.closeSync,
538
- };
539
- sink3 = getBaseFileSink(path3, { ...driver, bufferSize: 10000 });
540
- }
541
-
542
- // Write multiple entries that shouldn't exceed the large buffer
543
- sink3(debug);
544
- sink3(info);
545
- sink3(warning);
546
- // Should still be buffered (file empty)
547
- assertEquals(fs.readFileSync(path3, { encoding: "utf-8" }), "");
548
-
549
- // Dispose should flush all buffered content
550
- sink3[Symbol.dispose]();
551
- assertEquals(
552
- fs.readFileSync(path3, { encoding: "utf-8" }),
553
- `\
554
- 2023-11-14 22:13:20.000 +00:00 [DBG] my-app·junk: Hello, 123 & 456!
555
- 2023-11-14 22:13:20.000 +00:00 [INF] my-app·junk: Hello, 123 & 456!
556
- 2023-11-14 22:13:20.000 +00:00 [WRN] my-app·junk: Hello, 123 & 456!
557
- `,
558
- );
559
- });
560
-
561
- test("getBaseFileSink() with time-based flushing", async () => {
562
- const path = makeTempFileSync();
563
- let sink: Sink & Disposable;
564
- if (isDeno) {
565
- const driver: FileSinkDriver<Deno.FsFile> = {
566
- openSync(path: string) {
567
- return Deno.openSync(path, { create: true, append: true });
568
- },
569
- writeSync(fd, chunk) {
570
- fd.writeSync(chunk);
571
- },
572
- flushSync(fd) {
573
- fd.syncSync();
574
- },
575
- closeSync(fd) {
576
- fd.close();
577
- },
578
- };
579
- sink = getBaseFileSink(path, {
580
- ...driver,
581
- bufferSize: 1000, // Large buffer to prevent size-based flushing
582
- flushInterval: 100, // 100ms flush interval for testing
583
- });
584
- } else {
585
- const driver: FileSinkDriver<number> = {
586
- openSync(path: string) {
587
- return fs.openSync(path, "a");
588
- },
589
- writeSync: fs.writeSync,
590
- flushSync: fs.fsyncSync,
591
- closeSync: fs.closeSync,
592
- };
593
- sink = getBaseFileSink(path, {
594
- ...driver,
595
- bufferSize: 1000, // Large buffer to prevent size-based flushing
596
- flushInterval: 100, // 100ms flush interval for testing
597
- });
598
- }
599
-
600
- // Create a log record with current timestamp
601
- const record1 = { ...debug, timestamp: Date.now() };
602
- sink(record1);
603
-
604
- // Should be buffered (file empty initially)
605
- assertEquals(fs.readFileSync(path, { encoding: "utf-8" }), "");
606
-
607
- // Wait for flush interval to pass and write another record
608
- await new Promise((resolve) => setTimeout(resolve, 150));
609
- const record2 = { ...info, timestamp: Date.now() };
610
- sink(record2);
611
-
612
- // First record should now be flushed due to time interval
613
- const content = fs.readFileSync(path, { encoding: "utf-8" });
614
- assertEquals(content.includes("Hello, 123 & 456!"), true);
615
-
616
- sink[Symbol.dispose]();
617
- });
618
-
619
- test("getRotatingFileSink() with time-based flushing", async () => {
620
- const path = makeTempFileSync();
621
- const sink: Sink & Disposable = getRotatingFileSink(path, {
622
- maxSize: 1024 * 1024, // Large maxSize to prevent rotation
623
- bufferSize: 1000, // Large buffer to prevent size-based flushing
624
- flushInterval: 100, // 100ms flush interval for testing
625
- });
626
-
627
- // Create a log record with current timestamp
628
- const record1 = { ...debug, timestamp: Date.now() };
629
- sink(record1);
630
-
631
- // Should be buffered (file empty initially)
632
- assertEquals(fs.readFileSync(path, { encoding: "utf-8" }), "");
633
-
634
- // Wait for flush interval to pass and write another record
635
- await new Promise((resolve) => setTimeout(resolve, 150));
636
- const record2 = { ...info, timestamp: Date.now() };
637
- sink(record2);
638
-
639
- // First record should now be flushed due to time interval
640
- const content = fs.readFileSync(path, { encoding: "utf-8" });
641
- assertEquals(content.includes("Hello, 123 & 456!"), true);
642
-
643
- sink[Symbol.dispose]();
644
- });
645
-
646
- test("getBaseFileSink() with flushInterval disabled", () => {
647
- const path = makeTempFileSync();
648
- let sink: Sink & Disposable;
649
- if (isDeno) {
650
- const driver: FileSinkDriver<Deno.FsFile> = {
651
- openSync(path: string) {
652
- return Deno.openSync(path, { create: true, append: true });
653
- },
654
- writeSync(fd, chunk) {
655
- fd.writeSync(chunk);
656
- },
657
- flushSync(fd) {
658
- fd.syncSync();
659
- },
660
- closeSync(fd) {
661
- fd.close();
662
- },
663
- };
664
- sink = getBaseFileSink(path, {
665
- ...driver,
666
- bufferSize: 1000, // Large buffer to prevent size-based flushing
667
- flushInterval: 0, // Disable time-based flushing
668
- });
669
- } else {
670
- const driver: FileSinkDriver<number> = {
671
- openSync(path: string) {
672
- return fs.openSync(path, "a");
673
- },
674
- writeSync: fs.writeSync,
675
- flushSync: fs.fsyncSync,
676
- closeSync: fs.closeSync,
677
- };
678
- sink = getBaseFileSink(path, {
679
- ...driver,
680
- bufferSize: 1000, // Large buffer to prevent size-based flushing
681
- flushInterval: 0, // Disable time-based flushing
682
- });
683
- }
684
-
685
- // Create log records with simulated time gap
686
- const now = Date.now();
687
- const record1 = { ...debug, timestamp: now };
688
- const record2 = { ...info, timestamp: now + 10000 }; // 10 seconds later
689
-
690
- sink(record1);
691
- sink(record2);
692
-
693
- // Should still be buffered since time-based flushing is disabled
694
- assertEquals(fs.readFileSync(path, { encoding: "utf-8" }), "");
695
-
696
- // Only disposal should flush
697
- sink[Symbol.dispose]();
698
- const content = fs.readFileSync(path, { encoding: "utf-8" });
699
- assertEquals(content.includes("Hello, 123 & 456!"), true);
700
- });
701
-
702
- test("getFileSink() with nonBlocking mode", async () => {
703
- const path = makeTempFileSync();
704
- const sink = getFileSink(path, {
705
- nonBlocking: true,
706
- bufferSize: 50, // Small buffer to trigger flush by size
707
- });
708
-
709
- // Check that it returns AsyncDisposable
710
- assert(typeof sink === "function");
711
- assert(Symbol.asyncDispose in sink);
712
-
713
- // Add enough records to trigger buffer flush
714
- sink(debug);
715
- sink(info);
716
-
717
- // Wait for async flush to complete
718
- await delay(50);
719
- const content = fs.readFileSync(path, { encoding: "utf-8" });
720
- assert(content.includes("Hello, 123 & 456!"));
721
-
722
- await (sink as Sink & AsyncDisposable)[Symbol.asyncDispose]();
723
- });
724
-
725
- test("getRotatingFileSink() with nonBlocking mode", async () => {
726
- const path = makeTempFileSync();
727
- const sink = getRotatingFileSink(path, {
728
- maxSize: 200,
729
- nonBlocking: true,
730
- bufferSize: 1000, // Large buffer to prevent immediate flush
731
- flushInterval: 50, // Short interval for testing
732
- });
733
-
734
- // Check that it returns AsyncDisposable
735
- assert(typeof sink === "function");
736
- assert(Symbol.asyncDispose in sink);
737
-
738
- // Add records with current timestamp
739
- const record1 = { ...debug, timestamp: Date.now() };
740
- const record2 = { ...info, timestamp: Date.now() };
741
- sink(record1);
742
- sink(record2);
743
- assertEquals(fs.readFileSync(path, { encoding: "utf-8" }), ""); // Not written yet
744
-
745
- // Wait for flush interval to pass
746
- await delay(100);
747
- const content = fs.readFileSync(path, { encoding: "utf-8" });
748
- assert(content.includes("Hello, 123 & 456!"));
749
-
750
- await (sink as Sink & AsyncDisposable)[Symbol.asyncDispose]();
751
- });
752
-
753
- test("getFileSink() with nonBlocking high-volume logging", async () => {
754
- const path = makeTempFileSync();
755
- const sink = getFileSink(path, {
756
- nonBlocking: true,
757
- bufferSize: 50, // Small buffer to trigger flush
758
- flushInterval: 0, // Disable time-based flushing for this test
759
- }) as unknown as Sink & AsyncDisposable;
760
-
761
- // Add enough records to trigger buffer flush (50 chars per record roughly)
762
- let totalChars = 0;
763
- let recordCount = 0;
764
- while (totalChars < 100) { // Exceed buffer size
765
- sink(debug);
766
- totalChars += 67; // Approximate length of each debug record
767
- recordCount++;
768
- }
769
-
770
- // Wait for async flush to complete
771
- await delay(50);
772
- const content = fs.readFileSync(path, { encoding: "utf-8" });
773
-
774
- // Should have some records written by now
775
- const writtenCount = (content.match(/Hello, 123 & 456!/g) || []).length;
776
- assert(
777
- writtenCount > 0,
778
- `Expected some records to be written, but got ${writtenCount}`,
779
- );
780
-
781
- await sink[Symbol.asyncDispose]();
782
- });
783
-
784
- test("getRotatingFileSink() with nonBlocking rotation", async () => {
785
- const path = makeTempFileSync();
786
- const sink = getRotatingFileSink(path, {
787
- maxSize: 150, // Small size to trigger rotation
788
- nonBlocking: true,
789
- bufferSize: 100,
790
- flushInterval: 10,
791
- }) as unknown as Sink & AsyncDisposable;
792
-
793
- // Add enough records to trigger rotation
794
- sink(debug);
795
- sink(info);
796
- sink(warning);
797
- sink(error);
798
-
799
- // Wait for all flushes and rotation to complete
800
- await delay(200);
801
-
802
- // Check that rotation occurred
803
- const mainContent = fs.readFileSync(path, { encoding: "utf-8" });
804
- let rotatedContent = "";
805
- try {
806
- rotatedContent = fs.readFileSync(`${path}.1`, { encoding: "utf-8" });
807
- } catch {
808
- // No rotation occurred
809
- }
810
-
811
- const allContent = mainContent + rotatedContent;
812
-
813
- // Should have all 4 records somewhere
814
- const recordCount = (allContent.match(/Hello, 123 & 456!/g) || []).length;
815
- assertEquals(recordCount, 4);
816
-
817
- await sink[Symbol.asyncDispose]();
818
- });
819
-
820
- // cSpell: ignore filesink