@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/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
- let lastPromise = Promise.resolve();
210
- const sink: Sink & AsyncDisposable = (record: LogRecord) => {
211
- const bytes = encoder.encode(formatter(record));
212
- lastPromise = lastPromise
213
- .then(() => writer.ready)
214
- .then(() => writer.write(bytes));
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
- sink[Symbol.asyncDispose] = async () => {
217
- await lastPromise;
218
- await writer.close();
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
- return sink;
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(options: ConsoleSinkOptions = {}): Sink {
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
- return (record: LogRecord) => {
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
  /**