@logtape/logtape 1.0.0-dev.246 → 1.0.0-dev.248
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/formatter.cjs +187 -18
- package/dist/formatter.d.cts.map +1 -1
- package/dist/formatter.d.ts.map +1 -1
- package/dist/formatter.js +187 -18
- package/dist/formatter.js.map +1 -1
- package/dist/logger.cjs +26 -20
- package/dist/logger.js +26 -20
- package/dist/logger.js.map +1 -1
- package/dist/sink.cjs +107 -10
- package/dist/sink.d.cts +71 -2
- package/dist/sink.d.cts.map +1 -1
- package/dist/sink.d.ts +71 -2
- package/dist/sink.d.ts.map +1 -1
- package/dist/sink.js +107 -10
- package/dist/sink.js.map +1 -1
- package/formatter.ts +273 -68
- package/logger.ts +61 -30
- package/package.json +2 -1
- package/sink.test.ts +424 -5
- package/sink.ts +245 -13
package/sink.ts
CHANGED
|
@@ -173,6 +173,42 @@ export interface StreamSinkOptions {
|
|
|
173
173
|
* The text encoder to use. Defaults to an instance of {@link TextEncoder}.
|
|
174
174
|
*/
|
|
175
175
|
encoder?: { encode(text: string): Uint8Array };
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Enable non-blocking mode with optional buffer configuration.
|
|
179
|
+
* When enabled, log records are buffered and flushed in the background.
|
|
180
|
+
*
|
|
181
|
+
* @example Simple non-blocking mode
|
|
182
|
+
* ```typescript
|
|
183
|
+
* getStreamSink(stream, { nonBlocking: true });
|
|
184
|
+
* ```
|
|
185
|
+
*
|
|
186
|
+
* @example Custom buffer configuration
|
|
187
|
+
* ```typescript
|
|
188
|
+
* getStreamSink(stream, {
|
|
189
|
+
* nonBlocking: {
|
|
190
|
+
* bufferSize: 1000,
|
|
191
|
+
* flushInterval: 50
|
|
192
|
+
* }
|
|
193
|
+
* });
|
|
194
|
+
* ```
|
|
195
|
+
*
|
|
196
|
+
* @default `false`
|
|
197
|
+
* @since 1.0.0
|
|
198
|
+
*/
|
|
199
|
+
nonBlocking?: boolean | {
|
|
200
|
+
/**
|
|
201
|
+
* Maximum number of records to buffer before flushing.
|
|
202
|
+
* @default `100`
|
|
203
|
+
*/
|
|
204
|
+
bufferSize?: number;
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Interval in milliseconds between automatic flushes.
|
|
208
|
+
* @default `100`
|
|
209
|
+
*/
|
|
210
|
+
flushInterval?: number;
|
|
211
|
+
};
|
|
176
212
|
}
|
|
177
213
|
|
|
178
214
|
/**
|
|
@@ -206,18 +242,98 @@ export function getStreamSink(
|
|
|
206
242
|
const formatter = options.formatter ?? defaultTextFormatter;
|
|
207
243
|
const encoder = options.encoder ?? new TextEncoder();
|
|
208
244
|
const writer = stream.getWriter();
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
.
|
|
214
|
-
|
|
245
|
+
|
|
246
|
+
if (!options.nonBlocking) {
|
|
247
|
+
let lastPromise = Promise.resolve();
|
|
248
|
+
const sink: Sink & AsyncDisposable = (record: LogRecord) => {
|
|
249
|
+
const bytes = encoder.encode(formatter(record));
|
|
250
|
+
lastPromise = lastPromise
|
|
251
|
+
.then(() => writer.ready)
|
|
252
|
+
.then(() => writer.write(bytes));
|
|
253
|
+
};
|
|
254
|
+
sink[Symbol.asyncDispose] = async () => {
|
|
255
|
+
await lastPromise;
|
|
256
|
+
await writer.close();
|
|
257
|
+
};
|
|
258
|
+
return sink;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Non-blocking mode implementation
|
|
262
|
+
const nonBlockingConfig = options.nonBlocking === true
|
|
263
|
+
? {}
|
|
264
|
+
: options.nonBlocking;
|
|
265
|
+
const bufferSize = nonBlockingConfig.bufferSize ?? 100;
|
|
266
|
+
const flushInterval = nonBlockingConfig.flushInterval ?? 100;
|
|
267
|
+
|
|
268
|
+
const buffer: LogRecord[] = [];
|
|
269
|
+
let flushTimer: ReturnType<typeof setInterval> | null = null;
|
|
270
|
+
let disposed = false;
|
|
271
|
+
let activeFlush: Promise<void> | null = null;
|
|
272
|
+
const maxBufferSize = bufferSize * 2; // Overflow protection
|
|
273
|
+
|
|
274
|
+
async function flush(): Promise<void> {
|
|
275
|
+
if (buffer.length === 0) return;
|
|
276
|
+
|
|
277
|
+
const records = buffer.splice(0);
|
|
278
|
+
for (const record of records) {
|
|
279
|
+
try {
|
|
280
|
+
const bytes = encoder.encode(formatter(record));
|
|
281
|
+
await writer.ready;
|
|
282
|
+
await writer.write(bytes);
|
|
283
|
+
} catch {
|
|
284
|
+
// Silently ignore errors in non-blocking mode to avoid disrupting the application
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function scheduleFlush(): void {
|
|
290
|
+
if (activeFlush) return;
|
|
291
|
+
|
|
292
|
+
activeFlush = flush().finally(() => {
|
|
293
|
+
activeFlush = null;
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function startFlushTimer(): void {
|
|
298
|
+
if (flushTimer !== null || disposed) return;
|
|
299
|
+
|
|
300
|
+
flushTimer = setInterval(() => {
|
|
301
|
+
scheduleFlush();
|
|
302
|
+
}, flushInterval);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const nonBlockingSink: Sink & AsyncDisposable = (record: LogRecord) => {
|
|
306
|
+
if (disposed) return;
|
|
307
|
+
|
|
308
|
+
// Buffer overflow protection: drop oldest records if buffer is too large
|
|
309
|
+
if (buffer.length >= maxBufferSize) {
|
|
310
|
+
buffer.shift(); // Remove oldest record
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
buffer.push(record);
|
|
314
|
+
|
|
315
|
+
if (buffer.length >= bufferSize) {
|
|
316
|
+
scheduleFlush();
|
|
317
|
+
} else if (flushTimer === null) {
|
|
318
|
+
startFlushTimer();
|
|
319
|
+
}
|
|
215
320
|
};
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
321
|
+
|
|
322
|
+
nonBlockingSink[Symbol.asyncDispose] = async () => {
|
|
323
|
+
disposed = true;
|
|
324
|
+
if (flushTimer !== null) {
|
|
325
|
+
clearInterval(flushTimer);
|
|
326
|
+
flushTimer = null;
|
|
327
|
+
}
|
|
328
|
+
await flush();
|
|
329
|
+
try {
|
|
330
|
+
await writer.close();
|
|
331
|
+
} catch {
|
|
332
|
+
// Writer might already be closed or errored
|
|
333
|
+
}
|
|
219
334
|
};
|
|
220
|
-
|
|
335
|
+
|
|
336
|
+
return nonBlockingSink;
|
|
221
337
|
}
|
|
222
338
|
|
|
223
339
|
type ConsoleMethod = "debug" | "info" | "log" | "warn" | "error";
|
|
@@ -253,15 +369,54 @@ export interface ConsoleSinkOptions {
|
|
|
253
369
|
* The console to log to. Defaults to {@link console}.
|
|
254
370
|
*/
|
|
255
371
|
console?: Console;
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Enable non-blocking mode with optional buffer configuration.
|
|
375
|
+
* When enabled, log records are buffered and flushed in the background.
|
|
376
|
+
*
|
|
377
|
+
* @example Simple non-blocking mode
|
|
378
|
+
* ```typescript
|
|
379
|
+
* getConsoleSink({ nonBlocking: true });
|
|
380
|
+
* ```
|
|
381
|
+
*
|
|
382
|
+
* @example Custom buffer configuration
|
|
383
|
+
* ```typescript
|
|
384
|
+
* getConsoleSink({
|
|
385
|
+
* nonBlocking: {
|
|
386
|
+
* bufferSize: 1000,
|
|
387
|
+
* flushInterval: 50
|
|
388
|
+
* }
|
|
389
|
+
* });
|
|
390
|
+
* ```
|
|
391
|
+
*
|
|
392
|
+
* @default `false`
|
|
393
|
+
* @since 1.0.0
|
|
394
|
+
*/
|
|
395
|
+
nonBlocking?: boolean | {
|
|
396
|
+
/**
|
|
397
|
+
* Maximum number of records to buffer before flushing.
|
|
398
|
+
* @default `100`
|
|
399
|
+
*/
|
|
400
|
+
bufferSize?: number;
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Interval in milliseconds between automatic flushes.
|
|
404
|
+
* @default `100`
|
|
405
|
+
*/
|
|
406
|
+
flushInterval?: number;
|
|
407
|
+
};
|
|
256
408
|
}
|
|
257
409
|
|
|
258
410
|
/**
|
|
259
411
|
* A console sink factory that returns a sink that logs to the console.
|
|
260
412
|
*
|
|
261
413
|
* @param options The options for the sink.
|
|
262
|
-
* @returns A sink that logs to the console.
|
|
414
|
+
* @returns A sink that logs to the console. If `nonBlocking` is enabled,
|
|
415
|
+
* returns a sink that also implements {@link Disposable}.
|
|
263
416
|
*/
|
|
264
|
-
export function getConsoleSink(
|
|
417
|
+
export function getConsoleSink(
|
|
418
|
+
options: ConsoleSinkOptions = {},
|
|
419
|
+
): Sink | (Sink & Disposable) {
|
|
265
420
|
const formatter = options.formatter ?? defaultConsoleFormatter;
|
|
266
421
|
const levelMap: Record<LogLevel, ConsoleMethod> = {
|
|
267
422
|
trace: "debug",
|
|
@@ -273,7 +428,8 @@ export function getConsoleSink(options: ConsoleSinkOptions = {}): Sink {
|
|
|
273
428
|
...(options.levelMap ?? {}),
|
|
274
429
|
};
|
|
275
430
|
const console = options.console ?? globalThis.console;
|
|
276
|
-
|
|
431
|
+
|
|
432
|
+
const baseSink = (record: LogRecord) => {
|
|
277
433
|
const args = formatter(record);
|
|
278
434
|
const method = levelMap[record.level];
|
|
279
435
|
if (method === undefined) {
|
|
@@ -286,6 +442,82 @@ export function getConsoleSink(options: ConsoleSinkOptions = {}): Sink {
|
|
|
286
442
|
console[method](...args);
|
|
287
443
|
}
|
|
288
444
|
};
|
|
445
|
+
|
|
446
|
+
if (!options.nonBlocking) {
|
|
447
|
+
return baseSink;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Non-blocking mode implementation
|
|
451
|
+
const nonBlockingConfig = options.nonBlocking === true
|
|
452
|
+
? {}
|
|
453
|
+
: options.nonBlocking;
|
|
454
|
+
const bufferSize = nonBlockingConfig.bufferSize ?? 100;
|
|
455
|
+
const flushInterval = nonBlockingConfig.flushInterval ?? 100;
|
|
456
|
+
|
|
457
|
+
const buffer: LogRecord[] = [];
|
|
458
|
+
let flushTimer: ReturnType<typeof setInterval> | null = null;
|
|
459
|
+
let disposed = false;
|
|
460
|
+
let flushScheduled = false;
|
|
461
|
+
const maxBufferSize = bufferSize * 2; // Overflow protection
|
|
462
|
+
|
|
463
|
+
function flush(): void {
|
|
464
|
+
if (buffer.length === 0) return;
|
|
465
|
+
|
|
466
|
+
const records = buffer.splice(0);
|
|
467
|
+
for (const record of records) {
|
|
468
|
+
try {
|
|
469
|
+
baseSink(record);
|
|
470
|
+
} catch {
|
|
471
|
+
// Silently ignore errors in non-blocking mode to avoid disrupting the application
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function scheduleFlush(): void {
|
|
477
|
+
if (flushScheduled) return;
|
|
478
|
+
|
|
479
|
+
flushScheduled = true;
|
|
480
|
+
setTimeout(() => {
|
|
481
|
+
flushScheduled = false;
|
|
482
|
+
flush();
|
|
483
|
+
}, 0);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function startFlushTimer(): void {
|
|
487
|
+
if (flushTimer !== null || disposed) return;
|
|
488
|
+
|
|
489
|
+
flushTimer = setInterval(() => {
|
|
490
|
+
flush();
|
|
491
|
+
}, flushInterval);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const nonBlockingSink: Sink & Disposable = (record: LogRecord) => {
|
|
495
|
+
if (disposed) return;
|
|
496
|
+
|
|
497
|
+
// Buffer overflow protection: drop oldest records if buffer is too large
|
|
498
|
+
if (buffer.length >= maxBufferSize) {
|
|
499
|
+
buffer.shift(); // Remove oldest record
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
buffer.push(record);
|
|
503
|
+
|
|
504
|
+
if (buffer.length >= bufferSize) {
|
|
505
|
+
scheduleFlush();
|
|
506
|
+
} else if (flushTimer === null) {
|
|
507
|
+
startFlushTimer();
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
nonBlockingSink[Symbol.dispose] = () => {
|
|
512
|
+
disposed = true;
|
|
513
|
+
if (flushTimer !== null) {
|
|
514
|
+
clearInterval(flushTimer);
|
|
515
|
+
flushTimer = null;
|
|
516
|
+
}
|
|
517
|
+
flush();
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
return nonBlockingSink;
|
|
289
521
|
}
|
|
290
522
|
|
|
291
523
|
/**
|