@logtape/file 1.0.0-dev.241 → 1.0.0-dev.246
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/deno.json +1 -1
- package/dist/filesink.base.cjs +52 -347
- package/dist/filesink.base.d.cts +3 -65
- package/dist/filesink.base.d.cts.map +1 -1
- package/dist/filesink.base.d.ts +3 -65
- package/dist/filesink.base.d.ts.map +1 -1
- package/dist/filesink.base.js +52 -347
- package/dist/filesink.base.js.map +1 -1
- package/dist/filesink.deno.cjs +23 -26
- package/dist/filesink.deno.d.cts +4 -17
- package/dist/filesink.deno.d.cts.map +1 -1
- package/dist/filesink.deno.d.ts +4 -17
- package/dist/filesink.deno.d.ts.map +1 -1
- package/dist/filesink.deno.js +24 -26
- package/dist/filesink.deno.js.map +1 -1
- package/dist/filesink.node.cjs +23 -33
- package/dist/filesink.node.d.cts +4 -17
- package/dist/filesink.node.d.cts.map +1 -1
- package/dist/filesink.node.d.ts +4 -17
- package/dist/filesink.node.d.ts.map +1 -1
- package/dist/filesink.node.js +24 -33
- package/dist/filesink.node.js.map +1 -1
- package/dist/mod.cjs +1 -3
- package/dist/mod.d.cts +1 -2
- package/dist/mod.d.ts +1 -2
- package/dist/mod.js +1 -2
- package/filesink.base.ts +37 -618
- package/filesink.deno.ts +4 -57
- package/filesink.jsr.ts +7 -32
- package/filesink.node.ts +4 -58
- package/filesink.test.ts +0 -120
- package/mod.ts +0 -2
- package/package.json +12 -7
- package/tsdown.config.ts +2 -2
- package/dist/streamfilesink.cjs +0 -84
- package/dist/streamfilesink.d.cts +0 -95
- package/dist/streamfilesink.d.cts.map +0 -1
- package/dist/streamfilesink.d.ts +0 -95
- package/dist/streamfilesink.d.ts.map +0 -1
- package/dist/streamfilesink.js +0 -84
- package/dist/streamfilesink.js.map +0 -1
- package/streamfilesink.test.ts +0 -336
- package/streamfilesink.ts +0 -136
package/filesink.base.ts
CHANGED
|
@@ -5,257 +5,6 @@ import {
|
|
|
5
5
|
type StreamSinkOptions,
|
|
6
6
|
} from "@logtape/logtape";
|
|
7
7
|
|
|
8
|
-
/**
|
|
9
|
-
* Adaptive flush strategy that dynamically adjusts buffer thresholds
|
|
10
|
-
* based on recent flush patterns for optimal performance.
|
|
11
|
-
*/
|
|
12
|
-
class AdaptiveFlushStrategy {
|
|
13
|
-
private recentFlushSizes: number[] = [];
|
|
14
|
-
private recentFlushTimes: number[] = [];
|
|
15
|
-
private avgFlushSize: number;
|
|
16
|
-
private avgFlushInterval: number;
|
|
17
|
-
private readonly maxHistorySize = 10;
|
|
18
|
-
private readonly baseThreshold: number;
|
|
19
|
-
|
|
20
|
-
constructor(baseThreshold: number, baseInterval: number) {
|
|
21
|
-
this.baseThreshold = baseThreshold;
|
|
22
|
-
this.avgFlushSize = baseThreshold;
|
|
23
|
-
this.avgFlushInterval = baseInterval;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Record a flush event for pattern analysis.
|
|
28
|
-
* @param size The size of data flushed in bytes.
|
|
29
|
-
* @param timeSinceLastFlush Time since last flush in milliseconds.
|
|
30
|
-
*/
|
|
31
|
-
recordFlush(size: number, timeSinceLastFlush: number): void {
|
|
32
|
-
this.recentFlushSizes.push(size);
|
|
33
|
-
this.recentFlushTimes.push(timeSinceLastFlush);
|
|
34
|
-
|
|
35
|
-
// Keep only recent history
|
|
36
|
-
if (this.recentFlushSizes.length > this.maxHistorySize) {
|
|
37
|
-
this.recentFlushSizes.shift();
|
|
38
|
-
this.recentFlushTimes.shift();
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Update averages
|
|
42
|
-
this.updateAverages();
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Determine if buffer should be flushed based on adaptive strategy.
|
|
47
|
-
* @param currentSize Current buffer size in bytes.
|
|
48
|
-
* @param timeSinceLastFlush Time since last flush in milliseconds.
|
|
49
|
-
* @returns True if buffer should be flushed.
|
|
50
|
-
*/
|
|
51
|
-
shouldFlush(currentSize: number, timeSinceLastFlush: number): boolean {
|
|
52
|
-
const adaptiveThreshold = this.calculateAdaptiveThreshold();
|
|
53
|
-
const adaptiveInterval = this.calculateAdaptiveInterval();
|
|
54
|
-
|
|
55
|
-
return currentSize >= adaptiveThreshold ||
|
|
56
|
-
(adaptiveInterval > 0 && timeSinceLastFlush >= adaptiveInterval);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
private updateAverages(): void {
|
|
60
|
-
if (this.recentFlushSizes.length === 0) return;
|
|
61
|
-
|
|
62
|
-
this.avgFlushSize =
|
|
63
|
-
this.recentFlushSizes.reduce((sum, size) => sum + size, 0) /
|
|
64
|
-
this.recentFlushSizes.length;
|
|
65
|
-
|
|
66
|
-
this.avgFlushInterval =
|
|
67
|
-
this.recentFlushTimes.reduce((sum, time) => sum + time, 0) /
|
|
68
|
-
this.recentFlushTimes.length;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
private calculateAdaptiveThreshold(): number {
|
|
72
|
-
// Adjust threshold based on recent patterns
|
|
73
|
-
// Higher average flush sizes suggest larger batches are beneficial
|
|
74
|
-
const adaptiveFactor = Math.min(
|
|
75
|
-
2.0,
|
|
76
|
-
Math.max(0.5, this.avgFlushSize / this.baseThreshold),
|
|
77
|
-
);
|
|
78
|
-
|
|
79
|
-
return Math.max(
|
|
80
|
-
Math.min(4096, this.baseThreshold / 2),
|
|
81
|
-
Math.min(64 * 1024, this.baseThreshold * adaptiveFactor),
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
private calculateAdaptiveInterval(): number {
|
|
86
|
-
// If base interval is 0, time-based flushing is disabled
|
|
87
|
-
if (this.avgFlushInterval <= 0) return 0;
|
|
88
|
-
|
|
89
|
-
// Adjust interval based on recent flush frequency
|
|
90
|
-
// More frequent flushes suggest lower latency is preferred
|
|
91
|
-
if (this.recentFlushTimes.length < 3) return this.avgFlushInterval;
|
|
92
|
-
|
|
93
|
-
const variance = this.calculateVariance(this.recentFlushTimes);
|
|
94
|
-
const stabilityFactor = Math.min(2.0, Math.max(0.5, 1000 / variance));
|
|
95
|
-
|
|
96
|
-
return Math.max(
|
|
97
|
-
1000,
|
|
98
|
-
Math.min(10000, this.avgFlushInterval * stabilityFactor),
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
private calculateVariance(values: number[]): number {
|
|
103
|
-
if (values.length < 2) return 1000; // Default variance
|
|
104
|
-
|
|
105
|
-
const mean = values.reduce((sum, val) => sum + val, 0) / values.length;
|
|
106
|
-
const squaredDiffs = values.map((val) => Math.pow(val - mean, 2));
|
|
107
|
-
return squaredDiffs.reduce((sum, diff) => sum + diff, 0) / values.length;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Memory pool for reusing Uint8Array buffers to minimize GC pressure.
|
|
113
|
-
* Maintains a pool of pre-allocated buffers for efficient reuse.
|
|
114
|
-
*/
|
|
115
|
-
class BufferPool {
|
|
116
|
-
private pool: Uint8Array[] = [];
|
|
117
|
-
private readonly maxPoolSize = 50; // Keep a reasonable pool size
|
|
118
|
-
private readonly maxBufferSize = 64 * 1024; // Don't pool very large buffers
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Acquire a buffer from the pool or create a new one.
|
|
122
|
-
* @param size The minimum size needed for the buffer.
|
|
123
|
-
* @returns A Uint8Array that can be used for encoding.
|
|
124
|
-
*/
|
|
125
|
-
acquire(size: number): Uint8Array {
|
|
126
|
-
// Don't pool very large buffers to avoid memory waste
|
|
127
|
-
if (size > this.maxBufferSize) {
|
|
128
|
-
return new Uint8Array(size);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Try to find a suitable buffer from the pool
|
|
132
|
-
for (let i = this.pool.length - 1; i >= 0; i--) {
|
|
133
|
-
const buffer = this.pool[i];
|
|
134
|
-
if (buffer.length >= size) {
|
|
135
|
-
// Remove from pool and return
|
|
136
|
-
this.pool.splice(i, 1);
|
|
137
|
-
return buffer.subarray(0, size);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// No suitable buffer found, create a new one
|
|
142
|
-
// Create slightly larger buffer to improve reuse chances
|
|
143
|
-
const actualSize = Math.max(size, 1024); // Minimum 1KB
|
|
144
|
-
return new Uint8Array(actualSize);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Return a buffer to the pool for future reuse.
|
|
149
|
-
* @param buffer The buffer to return to the pool.
|
|
150
|
-
*/
|
|
151
|
-
release(buffer: Uint8Array): void {
|
|
152
|
-
// Don't pool if we're at capacity or buffer is too large
|
|
153
|
-
if (
|
|
154
|
-
this.pool.length >= this.maxPoolSize || buffer.length > this.maxBufferSize
|
|
155
|
-
) {
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Don't pool very small buffers as they're cheap to allocate
|
|
160
|
-
if (buffer.length < 256) {
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Add to pool for reuse
|
|
165
|
-
this.pool.push(buffer);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Clear the pool to free memory. Useful for cleanup.
|
|
170
|
-
*/
|
|
171
|
-
clear(): void {
|
|
172
|
-
this.pool.length = 0;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Get current pool statistics for monitoring.
|
|
177
|
-
* @returns Object with pool size and buffer count.
|
|
178
|
-
*/
|
|
179
|
-
getStats(): { poolSize: number; totalBuffers: number } {
|
|
180
|
-
return {
|
|
181
|
-
poolSize: this.pool.reduce((sum, buf) => sum + buf.length, 0),
|
|
182
|
-
totalBuffers: this.pool.length,
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* High-performance byte buffer for batching log records.
|
|
189
|
-
* Eliminates string concatenation overhead by storing pre-encoded bytes.
|
|
190
|
-
* Uses memory pooling to reduce GC pressure.
|
|
191
|
-
*/
|
|
192
|
-
class ByteRingBuffer {
|
|
193
|
-
private buffers: Uint8Array[] = [];
|
|
194
|
-
private totalSize: number = 0;
|
|
195
|
-
private bufferPool: BufferPool;
|
|
196
|
-
|
|
197
|
-
constructor(bufferPool: BufferPool) {
|
|
198
|
-
this.bufferPool = bufferPool;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Append pre-encoded log record bytes to the buffer.
|
|
203
|
-
* @param data The encoded log record as bytes.
|
|
204
|
-
*/
|
|
205
|
-
append(data: Uint8Array): void {
|
|
206
|
-
this.buffers.push(data);
|
|
207
|
-
this.totalSize += data.length;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Get the current total size of buffered data in bytes.
|
|
212
|
-
* @returns The total size in bytes.
|
|
213
|
-
*/
|
|
214
|
-
size(): number {
|
|
215
|
-
return this.totalSize;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Get the number of buffered records.
|
|
220
|
-
* @returns The number of records in the buffer.
|
|
221
|
-
*/
|
|
222
|
-
count(): number {
|
|
223
|
-
return this.buffers.length;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Flush all buffered data and return it as an array of byte arrays.
|
|
228
|
-
* This clears the internal buffer and returns used buffers to the pool.
|
|
229
|
-
* @returns Array of buffered byte arrays ready for writev() operations.
|
|
230
|
-
*/
|
|
231
|
-
flush(): Uint8Array[] {
|
|
232
|
-
const result = [...this.buffers];
|
|
233
|
-
this.clear();
|
|
234
|
-
return result;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Clear the buffer without returning data.
|
|
239
|
-
* Returns buffers to the pool for reuse.
|
|
240
|
-
*/
|
|
241
|
-
clear(): void {
|
|
242
|
-
// Return buffers to pool for reuse
|
|
243
|
-
for (const buffer of this.buffers) {
|
|
244
|
-
this.bufferPool.release(buffer);
|
|
245
|
-
}
|
|
246
|
-
this.buffers.length = 0;
|
|
247
|
-
this.totalSize = 0;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* Check if the buffer is empty.
|
|
252
|
-
* @returns True if the buffer contains no data.
|
|
253
|
-
*/
|
|
254
|
-
isEmpty(): boolean {
|
|
255
|
-
return this.buffers.length === 0;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
8
|
/**
|
|
260
9
|
* Options for the {@link getBaseFileSink} function.
|
|
261
10
|
*/
|
|
@@ -282,16 +31,6 @@ export type FileSinkOptions = StreamSinkOptions & {
|
|
|
282
31
|
* @since 0.12.0
|
|
283
32
|
*/
|
|
284
33
|
flushInterval?: number;
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* Enable non-blocking mode with background flushing.
|
|
288
|
-
* When enabled, flush operations are performed asynchronously to prevent
|
|
289
|
-
* blocking the main thread during file I/O operations.
|
|
290
|
-
*
|
|
291
|
-
* @default `false`
|
|
292
|
-
* @since 1.0.0
|
|
293
|
-
*/
|
|
294
|
-
nonBlocking?: boolean;
|
|
295
34
|
};
|
|
296
35
|
|
|
297
36
|
/**
|
|
@@ -312,14 +51,6 @@ export interface FileSinkDriver<TFile> {
|
|
|
312
51
|
*/
|
|
313
52
|
writeSync(fd: TFile, chunk: Uint8Array): void;
|
|
314
53
|
|
|
315
|
-
/**
|
|
316
|
-
* Write multiple chunks of data to the file in a single operation.
|
|
317
|
-
* This is optional - if not implemented, falls back to multiple writeSync calls.
|
|
318
|
-
* @param fd The file descriptor.
|
|
319
|
-
* @param chunks Array of data chunks to write.
|
|
320
|
-
*/
|
|
321
|
-
writeManySync?(fd: TFile, chunks: Uint8Array[]): void;
|
|
322
|
-
|
|
323
54
|
/**
|
|
324
55
|
* Flush the file to ensure that all data is written to the disk.
|
|
325
56
|
* @param fd The file descriptor.
|
|
@@ -333,33 +64,6 @@ export interface FileSinkDriver<TFile> {
|
|
|
333
64
|
closeSync(fd: TFile): void;
|
|
334
65
|
}
|
|
335
66
|
|
|
336
|
-
/**
|
|
337
|
-
* A platform-specific async file sink driver.
|
|
338
|
-
* @typeParam TFile The type of the file descriptor.
|
|
339
|
-
* @since 1.0.0
|
|
340
|
-
*/
|
|
341
|
-
export interface AsyncFileSinkDriver<TFile> extends FileSinkDriver<TFile> {
|
|
342
|
-
/**
|
|
343
|
-
* Asynchronously write multiple chunks of data to the file in a single operation.
|
|
344
|
-
* This is optional - if not implemented, falls back to multiple writeSync calls.
|
|
345
|
-
* @param fd The file descriptor.
|
|
346
|
-
* @param chunks Array of data chunks to write.
|
|
347
|
-
*/
|
|
348
|
-
writeMany?(fd: TFile, chunks: Uint8Array[]): Promise<void>;
|
|
349
|
-
|
|
350
|
-
/**
|
|
351
|
-
* Asynchronously flush the file to ensure that all data is written to the disk.
|
|
352
|
-
* @param fd The file descriptor.
|
|
353
|
-
*/
|
|
354
|
-
flush(fd: TFile): Promise<void>;
|
|
355
|
-
|
|
356
|
-
/**
|
|
357
|
-
* Asynchronously close the file.
|
|
358
|
-
* @param fd The file descriptor.
|
|
359
|
-
*/
|
|
360
|
-
close(fd: TFile): Promise<void>;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
67
|
/**
|
|
364
68
|
* Get a platform-independent file sink.
|
|
365
69
|
*
|
|
@@ -367,228 +71,49 @@ export interface AsyncFileSinkDriver<TFile> extends FileSinkDriver<TFile> {
|
|
|
367
71
|
* @param path A path to the file to write to.
|
|
368
72
|
* @param options The options for the sink and the file driver.
|
|
369
73
|
* @returns A sink that writes to the file. The sink is also a disposable
|
|
370
|
-
* object that closes the file when disposed.
|
|
371
|
-
* returns a sink that also implements {@link AsyncDisposable}.
|
|
74
|
+
* object that closes the file when disposed.
|
|
372
75
|
*/
|
|
373
76
|
export function getBaseFileSink<TFile>(
|
|
374
77
|
path: string,
|
|
375
78
|
options: FileSinkOptions & FileSinkDriver<TFile>,
|
|
376
|
-
): Sink & Disposable
|
|
377
|
-
export function getBaseFileSink<TFile>(
|
|
378
|
-
path: string,
|
|
379
|
-
options: FileSinkOptions & AsyncFileSinkDriver<TFile>,
|
|
380
|
-
): Sink & AsyncDisposable;
|
|
381
|
-
export function getBaseFileSink<TFile>(
|
|
382
|
-
path: string,
|
|
383
|
-
options:
|
|
384
|
-
& FileSinkOptions
|
|
385
|
-
& (FileSinkDriver<TFile> | AsyncFileSinkDriver<TFile>),
|
|
386
|
-
): Sink & (Disposable | AsyncDisposable) {
|
|
79
|
+
): Sink & Disposable {
|
|
387
80
|
const formatter = options.formatter ?? defaultTextFormatter;
|
|
388
81
|
const encoder = options.encoder ?? new TextEncoder();
|
|
389
|
-
const bufferSize = options.bufferSize ?? 1024 * 8; // Default buffer size of 8192
|
|
82
|
+
const bufferSize = options.bufferSize ?? 1024 * 8; // Default buffer size of 8192 chars
|
|
390
83
|
const flushInterval = options.flushInterval ?? 5000; // Default flush interval of 5 seconds
|
|
391
84
|
let fd = options.lazy ? null : options.openSync(path);
|
|
392
|
-
|
|
393
|
-
// Initialize memory pool and buffer systems
|
|
394
|
-
const bufferPool = new BufferPool();
|
|
395
|
-
const byteBuffer = new ByteRingBuffer(bufferPool);
|
|
396
|
-
const adaptiveStrategy = new AdaptiveFlushStrategy(bufferSize, flushInterval);
|
|
85
|
+
let buffer: string = "";
|
|
397
86
|
let lastFlushTimestamp: number = Date.now();
|
|
398
87
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
const flushSize = byteBuffer.size();
|
|
406
|
-
const currentTime = Date.now();
|
|
407
|
-
const timeSinceLastFlush = currentTime - lastFlushTimestamp;
|
|
408
|
-
|
|
409
|
-
const chunks = byteBuffer.flush();
|
|
410
|
-
if (options.writeManySync && chunks.length > 1) {
|
|
411
|
-
// Use batch write if available
|
|
412
|
-
options.writeManySync(fd, chunks);
|
|
413
|
-
} else {
|
|
414
|
-
// Fallback to individual writes
|
|
415
|
-
for (const chunk of chunks) {
|
|
416
|
-
options.writeSync(fd, chunk);
|
|
417
|
-
}
|
|
418
|
-
}
|
|
88
|
+
function flushBuffer(): void {
|
|
89
|
+
if (fd == null) return;
|
|
90
|
+
if (buffer.length > 0) {
|
|
91
|
+
options.writeSync(fd, encoder.encode(buffer));
|
|
92
|
+
buffer = "";
|
|
419
93
|
options.flushSync(fd);
|
|
420
|
-
|
|
421
|
-
// Record flush for adaptive strategy
|
|
422
|
-
adaptiveStrategy.recordFlush(flushSize, timeSinceLastFlush);
|
|
423
|
-
lastFlushTimestamp = currentTime;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
const sink: Sink & Disposable = (record: LogRecord) => {
|
|
427
|
-
if (fd == null) fd = options.openSync(path);
|
|
428
|
-
|
|
429
|
-
// ULTRA FAST PATH: Direct write when buffer is empty and using default buffer settings
|
|
430
|
-
if (byteBuffer.isEmpty() && bufferSize === 8192) {
|
|
431
|
-
// Inline everything for maximum speed - avoid all function calls
|
|
432
|
-
const formattedRecord = formatter(record);
|
|
433
|
-
const encodedRecord = encoder.encode(formattedRecord);
|
|
434
|
-
|
|
435
|
-
// Only use fast path for typical log sizes to avoid breaking edge cases
|
|
436
|
-
if (encodedRecord.length < 200) {
|
|
437
|
-
// Write directly for small logs - no complex buffering logic
|
|
438
|
-
options.writeSync(fd, encodedRecord);
|
|
439
|
-
options.flushSync(fd);
|
|
440
|
-
lastFlushTimestamp = Date.now();
|
|
441
|
-
return;
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
// STANDARD PATH: Complex logic for edge cases
|
|
446
|
-
const formattedRecord = formatter(record);
|
|
447
|
-
const encodedRecord = encoder.encode(formattedRecord);
|
|
448
|
-
byteBuffer.append(encodedRecord);
|
|
449
|
-
|
|
450
|
-
// Check for immediate flush conditions
|
|
451
|
-
if (bufferSize <= 0) {
|
|
452
|
-
// No buffering - flush immediately
|
|
453
|
-
flushBuffer();
|
|
454
|
-
} else {
|
|
455
|
-
// Use adaptive strategy for intelligent flushing
|
|
456
|
-
const timeSinceLastFlush = record.timestamp - lastFlushTimestamp;
|
|
457
|
-
const shouldFlush = adaptiveStrategy.shouldFlush(
|
|
458
|
-
byteBuffer.size(),
|
|
459
|
-
timeSinceLastFlush,
|
|
460
|
-
);
|
|
461
|
-
|
|
462
|
-
if (shouldFlush) {
|
|
463
|
-
flushBuffer();
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
};
|
|
467
|
-
sink[Symbol.dispose] = () => {
|
|
468
|
-
if (fd !== null) {
|
|
469
|
-
flushBuffer();
|
|
470
|
-
options.closeSync(fd);
|
|
471
|
-
}
|
|
472
|
-
// Clean up buffer pool
|
|
473
|
-
bufferPool.clear();
|
|
474
|
-
};
|
|
475
|
-
return sink;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
// Non-blocking mode implementation
|
|
479
|
-
const asyncOptions = options as AsyncFileSinkDriver<TFile>;
|
|
480
|
-
let disposed = false;
|
|
481
|
-
let activeFlush: Promise<void> | null = null;
|
|
482
|
-
let flushTimer: ReturnType<typeof setInterval> | null = null;
|
|
483
|
-
|
|
484
|
-
async function flushBuffer(): Promise<void> {
|
|
485
|
-
if (fd == null || byteBuffer.isEmpty()) return;
|
|
486
|
-
|
|
487
|
-
const flushSize = byteBuffer.size();
|
|
488
|
-
const currentTime = Date.now();
|
|
489
|
-
const timeSinceLastFlush = currentTime - lastFlushTimestamp;
|
|
490
|
-
|
|
491
|
-
const chunks = byteBuffer.flush();
|
|
492
|
-
try {
|
|
493
|
-
if (asyncOptions.writeMany && chunks.length > 1) {
|
|
494
|
-
// Use async batch write if available
|
|
495
|
-
await asyncOptions.writeMany(fd, chunks);
|
|
496
|
-
} else {
|
|
497
|
-
// Fallback to individual writes
|
|
498
|
-
for (const chunk of chunks) {
|
|
499
|
-
asyncOptions.writeSync(fd, chunk);
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
await asyncOptions.flush(fd);
|
|
503
|
-
|
|
504
|
-
// Record flush for adaptive strategy
|
|
505
|
-
adaptiveStrategy.recordFlush(flushSize, timeSinceLastFlush);
|
|
506
|
-
lastFlushTimestamp = currentTime;
|
|
507
|
-
} catch {
|
|
508
|
-
// Silently ignore errors in non-blocking mode
|
|
94
|
+
lastFlushTimestamp = Date.now();
|
|
509
95
|
}
|
|
510
96
|
}
|
|
511
97
|
|
|
512
|
-
|
|
513
|
-
if (
|
|
514
|
-
|
|
515
|
-
activeFlush = flushBuffer().finally(() => {
|
|
516
|
-
activeFlush = null;
|
|
517
|
-
});
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
function startFlushTimer(): void {
|
|
521
|
-
if (flushTimer !== null || disposed) return;
|
|
522
|
-
|
|
523
|
-
flushTimer = setInterval(() => {
|
|
524
|
-
scheduleFlush();
|
|
525
|
-
}, flushInterval);
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
const nonBlockingSink: Sink & AsyncDisposable = (record: LogRecord) => {
|
|
529
|
-
if (disposed) return;
|
|
530
|
-
if (fd == null) fd = asyncOptions.openSync(path);
|
|
531
|
-
|
|
532
|
-
// ULTRA FAST PATH: Direct write when buffer is empty and using default buffer settings
|
|
533
|
-
if (byteBuffer.isEmpty() && !activeFlush && bufferSize === 8192) {
|
|
534
|
-
// Inline everything for maximum speed - avoid all function calls
|
|
535
|
-
const formattedRecord = formatter(record);
|
|
536
|
-
const encodedRecord = encoder.encode(formattedRecord);
|
|
537
|
-
|
|
538
|
-
// Only use fast path for typical log sizes to avoid breaking edge cases
|
|
539
|
-
if (encodedRecord.length < 200) {
|
|
540
|
-
// Write directly for small logs - no complex buffering logic
|
|
541
|
-
asyncOptions.writeSync(fd, encodedRecord);
|
|
542
|
-
scheduleFlush(); // Async flush
|
|
543
|
-
lastFlushTimestamp = Date.now();
|
|
544
|
-
return;
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
// STANDARD PATH: Complex logic for edge cases
|
|
549
|
-
const formattedRecord = formatter(record);
|
|
550
|
-
const encodedRecord = encoder.encode(formattedRecord);
|
|
551
|
-
byteBuffer.append(encodedRecord);
|
|
98
|
+
const sink: Sink & Disposable = (record: LogRecord) => {
|
|
99
|
+
if (fd == null) fd = options.openSync(path);
|
|
100
|
+
buffer += formatter(record);
|
|
552
101
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
scheduleFlush();
|
|
557
|
-
} else {
|
|
558
|
-
// Use adaptive strategy for intelligent flushing
|
|
559
|
-
const timeSinceLastFlush = record.timestamp - lastFlushTimestamp;
|
|
560
|
-
const shouldFlush = adaptiveStrategy.shouldFlush(
|
|
561
|
-
byteBuffer.size(),
|
|
562
|
-
timeSinceLastFlush,
|
|
563
|
-
);
|
|
102
|
+
const shouldFlushBySize = buffer.length >= bufferSize;
|
|
103
|
+
const shouldFlushByTime = flushInterval > 0 &&
|
|
104
|
+
(record.timestamp - lastFlushTimestamp) >= flushInterval;
|
|
564
105
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
} else if (flushTimer === null && flushInterval > 0) {
|
|
568
|
-
startFlushTimer();
|
|
569
|
-
}
|
|
106
|
+
if (shouldFlushBySize || shouldFlushByTime) {
|
|
107
|
+
flushBuffer();
|
|
570
108
|
}
|
|
571
109
|
};
|
|
572
|
-
|
|
573
|
-
nonBlockingSink[Symbol.asyncDispose] = async () => {
|
|
574
|
-
disposed = true;
|
|
575
|
-
if (flushTimer !== null) {
|
|
576
|
-
clearInterval(flushTimer);
|
|
577
|
-
flushTimer = null;
|
|
578
|
-
}
|
|
579
|
-
await flushBuffer();
|
|
110
|
+
sink[Symbol.dispose] = () => {
|
|
580
111
|
if (fd !== null) {
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
} catch {
|
|
584
|
-
// Writer might already be closed or errored
|
|
585
|
-
}
|
|
112
|
+
flushBuffer();
|
|
113
|
+
options.closeSync(fd);
|
|
586
114
|
}
|
|
587
|
-
// Clean up buffer pool
|
|
588
|
-
bufferPool.clear();
|
|
589
115
|
};
|
|
590
|
-
|
|
591
|
-
return nonBlockingSink;
|
|
116
|
+
return sink;
|
|
592
117
|
}
|
|
593
118
|
|
|
594
119
|
/**
|
|
@@ -625,27 +150,6 @@ export interface RotatingFileSinkDriver<TFile> extends FileSinkDriver<TFile> {
|
|
|
625
150
|
renameSync(oldPath: string, newPath: string): void;
|
|
626
151
|
}
|
|
627
152
|
|
|
628
|
-
/**
|
|
629
|
-
* A platform-specific async rotating file sink driver.
|
|
630
|
-
* @since 1.0.0
|
|
631
|
-
*/
|
|
632
|
-
export interface AsyncRotatingFileSinkDriver<TFile>
|
|
633
|
-
extends AsyncFileSinkDriver<TFile> {
|
|
634
|
-
/**
|
|
635
|
-
* Get the size of the file.
|
|
636
|
-
* @param path A path to the file.
|
|
637
|
-
* @returns The `size` of the file in bytes, in an object.
|
|
638
|
-
*/
|
|
639
|
-
statSync(path: string): { size: number };
|
|
640
|
-
|
|
641
|
-
/**
|
|
642
|
-
* Rename a file.
|
|
643
|
-
* @param oldPath A path to the file to rename.
|
|
644
|
-
* @param newPath A path to be renamed to.
|
|
645
|
-
*/
|
|
646
|
-
renameSync(oldPath: string, newPath: string): void;
|
|
647
|
-
}
|
|
648
|
-
|
|
649
153
|
/**
|
|
650
154
|
* Get a platform-independent rotating file sink.
|
|
651
155
|
*
|
|
@@ -657,23 +161,12 @@ export interface AsyncRotatingFileSinkDriver<TFile>
|
|
|
657
161
|
* @param path A path to the file to write to.
|
|
658
162
|
* @param options The options for the sink and the file driver.
|
|
659
163
|
* @returns A sink that writes to the file. The sink is also a disposable
|
|
660
|
-
* object that closes the file when disposed.
|
|
661
|
-
* returns a sink that also implements {@link AsyncDisposable}.
|
|
164
|
+
* object that closes the file when disposed.
|
|
662
165
|
*/
|
|
663
166
|
export function getBaseRotatingFileSink<TFile>(
|
|
664
167
|
path: string,
|
|
665
168
|
options: RotatingFileSinkOptions & RotatingFileSinkDriver<TFile>,
|
|
666
|
-
): Sink & Disposable
|
|
667
|
-
export function getBaseRotatingFileSink<TFile>(
|
|
668
|
-
path: string,
|
|
669
|
-
options: RotatingFileSinkOptions & AsyncRotatingFileSinkDriver<TFile>,
|
|
670
|
-
): Sink & AsyncDisposable;
|
|
671
|
-
export function getBaseRotatingFileSink<TFile>(
|
|
672
|
-
path: string,
|
|
673
|
-
options:
|
|
674
|
-
& RotatingFileSinkOptions
|
|
675
|
-
& (RotatingFileSinkDriver<TFile> | AsyncRotatingFileSinkDriver<TFile>),
|
|
676
|
-
): Sink & (Disposable | AsyncDisposable) {
|
|
169
|
+
): Sink & Disposable {
|
|
677
170
|
const formatter = options.formatter ?? defaultTextFormatter;
|
|
678
171
|
const encoder = options.encoder ?? new TextEncoder();
|
|
679
172
|
const maxSize = options.maxSize ?? 1024 * 1024;
|
|
@@ -689,7 +182,6 @@ export function getBaseRotatingFileSink<TFile>(
|
|
|
689
182
|
}
|
|
690
183
|
let fd = options.openSync(path);
|
|
691
184
|
let lastFlushTimestamp: number = Date.now();
|
|
692
|
-
let buffer: string = "";
|
|
693
185
|
|
|
694
186
|
function shouldRollover(bytes: Uint8Array): boolean {
|
|
695
187
|
return offset + bytes.length > maxSize;
|
|
@@ -710,80 +202,20 @@ export function getBaseRotatingFileSink<TFile>(
|
|
|
710
202
|
fd = options.openSync(path);
|
|
711
203
|
}
|
|
712
204
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
if (buffer.length > 0) {
|
|
718
|
-
const bytes = encoder.encode(buffer);
|
|
719
|
-
buffer = "";
|
|
720
|
-
if (shouldRollover(bytes)) performRollover();
|
|
721
|
-
options.writeSync(fd, bytes);
|
|
722
|
-
options.flushSync(fd);
|
|
723
|
-
offset += bytes.length;
|
|
724
|
-
lastFlushTimestamp = Date.now();
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
const sink: Sink & Disposable = (record: LogRecord) => {
|
|
729
|
-
buffer += formatter(record);
|
|
730
|
-
|
|
731
|
-
const shouldFlushBySize = buffer.length >= bufferSize;
|
|
732
|
-
const shouldFlushByTime = flushInterval > 0 &&
|
|
733
|
-
(record.timestamp - lastFlushTimestamp) >= flushInterval;
|
|
734
|
-
|
|
735
|
-
if (shouldFlushBySize || shouldFlushByTime) {
|
|
736
|
-
flushBuffer();
|
|
737
|
-
}
|
|
738
|
-
};
|
|
739
|
-
sink[Symbol.dispose] = () => {
|
|
740
|
-
flushBuffer();
|
|
741
|
-
options.closeSync(fd);
|
|
742
|
-
};
|
|
743
|
-
return sink;
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
// Non-blocking mode implementation
|
|
747
|
-
const asyncOptions = options as AsyncRotatingFileSinkDriver<TFile>;
|
|
748
|
-
let disposed = false;
|
|
749
|
-
let activeFlush: Promise<void> | null = null;
|
|
750
|
-
let flushTimer: ReturnType<typeof setInterval> | null = null;
|
|
751
|
-
|
|
752
|
-
async function flushBuffer(): Promise<void> {
|
|
753
|
-
if (buffer.length === 0) return;
|
|
754
|
-
|
|
755
|
-
const data = buffer;
|
|
756
|
-
buffer = "";
|
|
757
|
-
try {
|
|
758
|
-
const bytes = encoder.encode(data);
|
|
205
|
+
function flushBuffer(): void {
|
|
206
|
+
if (buffer.length > 0) {
|
|
207
|
+
const bytes = encoder.encode(buffer);
|
|
208
|
+
buffer = "";
|
|
759
209
|
if (shouldRollover(bytes)) performRollover();
|
|
760
|
-
|
|
761
|
-
|
|
210
|
+
options.writeSync(fd, bytes);
|
|
211
|
+
options.flushSync(fd);
|
|
762
212
|
offset += bytes.length;
|
|
763
213
|
lastFlushTimestamp = Date.now();
|
|
764
|
-
} catch {
|
|
765
|
-
// Silently ignore errors in non-blocking mode
|
|
766
214
|
}
|
|
767
215
|
}
|
|
768
216
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
activeFlush = flushBuffer().finally(() => {
|
|
773
|
-
activeFlush = null;
|
|
774
|
-
});
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
function startFlushTimer(): void {
|
|
778
|
-
if (flushTimer !== null || disposed) return;
|
|
779
|
-
|
|
780
|
-
flushTimer = setInterval(() => {
|
|
781
|
-
scheduleFlush();
|
|
782
|
-
}, flushInterval);
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
const nonBlockingSink: Sink & AsyncDisposable = (record: LogRecord) => {
|
|
786
|
-
if (disposed) return;
|
|
217
|
+
let buffer: string = "";
|
|
218
|
+
const sink: Sink & Disposable = (record: LogRecord) => {
|
|
787
219
|
buffer += formatter(record);
|
|
788
220
|
|
|
789
221
|
const shouldFlushBySize = buffer.length >= bufferSize;
|
|
@@ -791,25 +223,12 @@ export function getBaseRotatingFileSink<TFile>(
|
|
|
791
223
|
(record.timestamp - lastFlushTimestamp) >= flushInterval;
|
|
792
224
|
|
|
793
225
|
if (shouldFlushBySize || shouldFlushByTime) {
|
|
794
|
-
|
|
795
|
-
} else if (flushTimer === null && flushInterval > 0) {
|
|
796
|
-
startFlushTimer();
|
|
226
|
+
flushBuffer();
|
|
797
227
|
}
|
|
798
228
|
};
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
if (flushTimer !== null) {
|
|
803
|
-
clearInterval(flushTimer);
|
|
804
|
-
flushTimer = null;
|
|
805
|
-
}
|
|
806
|
-
await flushBuffer();
|
|
807
|
-
try {
|
|
808
|
-
await asyncOptions.close(fd);
|
|
809
|
-
} catch {
|
|
810
|
-
// Writer might already be closed or errored
|
|
811
|
-
}
|
|
229
|
+
sink[Symbol.dispose] = () => {
|
|
230
|
+
flushBuffer();
|
|
231
|
+
options.closeSync(fd);
|
|
812
232
|
};
|
|
813
|
-
|
|
814
|
-
return nonBlockingSink;
|
|
233
|
+
return sink;
|
|
815
234
|
}
|