@logtape/logtape 1.1.0-dev.338 → 1.1.0

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logtape/logtape",
3
- "version": "1.1.0-dev.338+557a49e7",
3
+ "version": "1.1.0",
4
4
  "license": "MIT",
5
5
  "exports": "./src/mod.ts",
6
6
  "imports": {
@@ -1 +1 @@
1
- {"version":3,"file":"logger.d.ts","names":[],"sources":["../src/logger.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;;;;;;AAyQgB,UArPC,MAAA,CAqPD;EAAoB;;;EA4DX,SAcR,QAAA,EAAA,SAAA,MAAA,EAAA;EAAW;;;;EAwEH,SAcR,MAAA,EA3YE,MA2YF,GAAA,IAAA;EAAW;;;;;;;;;;;;;;;;;AAoST;EASP,QAAA,CAAA,WAAW,EAAA,MAAY,GAAA,SAAA,CAAiB,MAAA,CAAA,GAAA,SAAA,CAAA,MAAA,EAAA,GAAA,MAAA,EAAA,CAAA,CAAA,EAlqB/C,MAkqB+C;EASxC;AASZ;;;;;;;AAqCwB;AAexB;;;;;;;;;;;;;;;;;mBA5sBmB,0BAA0B;;;;;;;;;;;;iBAa5B;;;;;;;;;;;;;;;;;;;;;;;;;;sCA6BA,iCAAiC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;oBAgC9B;;;;;;;;;;;;;kBAcF;;;;;;;;;;;iBAYD;;;;;;;;;;;;;;;;;;;;;;;;;sCA4BA,iCAAiC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;oBAgC9B;;;;;;;;;;;;kBAaF;;;;;;;;;;;gBAYF;;;;;;;;;;;;;;;;;;;;;;;;;qCA4BC,iCAAiC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mBAgC/B;;;;;;;;;;;;;iBAcF;;;;;;;;;;;gBAYD;;;;;;;;;;;;;;;;;;;;;;;;;qCA4BC,iCAAiC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mBAgC/B;;;;;;;;;;;;;iBAcF;;;;;;;;;;;;mBAaE;;;;;;;;;;;;;;;;;;;;;;;;;;wCA6BF,iCAAiC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;sBAgC5B;;;;;;;;;;;;;;oBAeF;;;;;;;;;;;iBAYH;;;;;;;;;;;;;;;;;;;;;;;;;sCA4BA,iCAAiC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;oBAgC9B;;;;;;;;;;;;;kBAcF;;;;;;;;;;;iBAYD;;;;;;;;;;;;;;;;;;;;;;;;;sCA4BA,iCAAiC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;oBAgC9B;;;;;;;;;;;;;kBAcF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;eA+BH,KAAK;;;;;;;;KASR,WAAA,YAAuB;;;;;;;;KASvB,iBAAA,aACD;;;;;UAQM,SAAA;;;;;;YAOJ;;;;;;;;;;iCAeI,iCAAiC;;;;;;eAQnC;;;;;;aAOF;;;;;;;;;;;;;;iBAeG,SAAA,yCAAsD"}
1
+ {"version":3,"file":"logger.d.ts","names":[],"sources":["../src/logger.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;;;;;;AAyQgB,UArPC,MAAA,CAqPD;EAAoB;;;EA4DX,SAcR,QAAA,EAAA,SAAA,MAAA,EAAA;EAAW;;;;EAwEH,SAcR,MAAA,EA3YE,MA2YF,GAAA,IAAA;EAAW;;;;;;;;;;;;;;;;;AAoST;EASP,QAAA,CAAA,WAAW,EAAA,MAAY,GAAA,SAAA,CAAA,MAAiB,CAAA,GAAA,SAAA,CAAA,MAAA,EAAA,GAAA,MAAA,EAAA,CAAA,CAAA,EAlqB/C,MAkqB+C;EASxC;AASZ;;;;;;;AAqCwB;AAexB;;;;;;;;;;;;;;;;;mBA5sBmB,0BAA0B;;;;;;;;;;;;iBAa5B;;;;;;;;;;;;;;;;;;;;;;;;;;sCA6BA,iCAAiC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;oBAgC9B;;;;;;;;;;;;;kBAcF;;;;;;;;;;;iBAYD;;;;;;;;;;;;;;;;;;;;;;;;;sCA4BA,iCAAiC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;oBAgC9B;;;;;;;;;;;;kBAaF;;;;;;;;;;;gBAYF;;;;;;;;;;;;;;;;;;;;;;;;;qCA4BC,iCAAiC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mBAgC/B;;;;;;;;;;;;;iBAcF;;;;;;;;;;;gBAYD;;;;;;;;;;;;;;;;;;;;;;;;;qCA4BC,iCAAiC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mBAgC/B;;;;;;;;;;;;;iBAcF;;;;;;;;;;;;mBAaE;;;;;;;;;;;;;;;;;;;;;;;;;;wCA6BF,iCAAiC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;sBAgC5B;;;;;;;;;;;;;;oBAeF;;;;;;;;;;;iBAYH;;;;;;;;;;;;;;;;;;;;;;;;;sCA4BA,iCAAiC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;oBAgC9B;;;;;;;;;;;;;kBAcF;;;;;;;;;;;iBAYD;;;;;;;;;;;;;;;;;;;;;;;;;sCA4BA,iCAAiC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;oBAgC9B;;;;;;;;;;;;;kBAcF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;eA+BH,KAAK;;;;;;;;KASR,WAAA,YAAuB;;;;;;;;KASvB,iBAAA,aACD;;;;;UAQM,SAAA;;;;;;YAOJ;;;;;;;;;;iCAeI,iCAAiC;;;;;;eAQnC;;;;;;aAOF;;;;;;;;;;;;;;iBAeG,SAAA,yCAAsD"}
package/dist/mod.cjs CHANGED
@@ -15,6 +15,7 @@ exports.defaultConsoleFormatter = require_formatter.defaultConsoleFormatter;
15
15
  exports.defaultTextFormatter = require_formatter.defaultTextFormatter;
16
16
  exports.dispose = require_config.dispose;
17
17
  exports.disposeSync = require_config.disposeSync;
18
+ exports.fingersCrossed = require_sink.fingersCrossed;
18
19
  exports.fromAsyncSink = require_sink.fromAsyncSink;
19
20
  exports.getAnsiColorFormatter = require_formatter.getAnsiColorFormatter;
20
21
  exports.getConfig = require_config.getConfig;
package/dist/mod.d.cts CHANGED
@@ -3,7 +3,7 @@ import { LogLevel, compareLogLevel, getLogLevels, isLogLevel, parseLogLevel } fr
3
3
  import { LogRecord } from "./record.cjs";
4
4
  import { Filter, FilterLike, getLevelFilter, toFilter } from "./filter.cjs";
5
5
  import { AnsiColor, AnsiColorFormatterOptions, AnsiStyle, ConsoleFormatter, FormattedValues, JsonLinesFormatterOptions, TextFormatter, TextFormatterOptions, ansiColorFormatter, defaultConsoleFormatter, defaultTextFormatter, getAnsiColorFormatter, getJsonLinesFormatter, getTextFormatter, jsonLinesFormatter } from "./formatter.cjs";
6
- import { AsyncSink, ConsoleSinkOptions, Sink, StreamSinkOptions, fromAsyncSink, getConsoleSink, getStreamSink, withFilter } from "./sink.cjs";
6
+ import { AsyncSink, ConsoleSinkOptions, FingersCrossedOptions, Sink, StreamSinkOptions, fingersCrossed, fromAsyncSink, getConsoleSink, getStreamSink, withFilter } from "./sink.cjs";
7
7
  import { Config, ConfigError, LoggerConfig, configure, configureSync, dispose, disposeSync, getConfig, reset, resetSync } from "./config.cjs";
8
8
  import { LogMethod, Logger, getLogger } from "./logger.cjs";
9
- export { AnsiColor, AnsiColorFormatterOptions, AnsiStyle, AsyncSink, Config, ConfigError, ConsoleFormatter, ConsoleSinkOptions, ContextLocalStorage, Filter, FilterLike, FormattedValues, JsonLinesFormatterOptions, LogLevel, LogMethod, LogRecord, Logger, LoggerConfig, Sink, StreamSinkOptions, TextFormatter, TextFormatterOptions, ansiColorFormatter, compareLogLevel, configure, configureSync, defaultConsoleFormatter, defaultTextFormatter, dispose, disposeSync, fromAsyncSink, getAnsiColorFormatter, getConfig, getConsoleSink, getJsonLinesFormatter, getLevelFilter, getLogLevels, getLogger, getStreamSink, getTextFormatter, isLogLevel, jsonLinesFormatter, parseLogLevel, reset, resetSync, toFilter, withContext, withFilter };
9
+ export { AnsiColor, AnsiColorFormatterOptions, AnsiStyle, AsyncSink, Config, ConfigError, ConsoleFormatter, ConsoleSinkOptions, ContextLocalStorage, Filter, FilterLike, FingersCrossedOptions, FormattedValues, JsonLinesFormatterOptions, LogLevel, LogMethod, LogRecord, Logger, LoggerConfig, Sink, StreamSinkOptions, TextFormatter, TextFormatterOptions, ansiColorFormatter, compareLogLevel, configure, configureSync, defaultConsoleFormatter, defaultTextFormatter, dispose, disposeSync, fingersCrossed, fromAsyncSink, getAnsiColorFormatter, getConfig, getConsoleSink, getJsonLinesFormatter, getLevelFilter, getLogLevels, getLogger, getStreamSink, getTextFormatter, isLogLevel, jsonLinesFormatter, parseLogLevel, reset, resetSync, toFilter, withContext, withFilter };
package/dist/mod.d.ts CHANGED
@@ -3,7 +3,7 @@ import { LogLevel, compareLogLevel, getLogLevels, isLogLevel, parseLogLevel } fr
3
3
  import { LogRecord } from "./record.js";
4
4
  import { Filter, FilterLike, getLevelFilter, toFilter } from "./filter.js";
5
5
  import { AnsiColor, AnsiColorFormatterOptions, AnsiStyle, ConsoleFormatter, FormattedValues, JsonLinesFormatterOptions, TextFormatter, TextFormatterOptions, ansiColorFormatter, defaultConsoleFormatter, defaultTextFormatter, getAnsiColorFormatter, getJsonLinesFormatter, getTextFormatter, jsonLinesFormatter } from "./formatter.js";
6
- import { AsyncSink, ConsoleSinkOptions, Sink, StreamSinkOptions, fromAsyncSink, getConsoleSink, getStreamSink, withFilter } from "./sink.js";
6
+ import { AsyncSink, ConsoleSinkOptions, FingersCrossedOptions, Sink, StreamSinkOptions, fingersCrossed, fromAsyncSink, getConsoleSink, getStreamSink, withFilter } from "./sink.js";
7
7
  import { Config, ConfigError, LoggerConfig, configure, configureSync, dispose, disposeSync, getConfig, reset, resetSync } from "./config.js";
8
8
  import { LogMethod, Logger, getLogger } from "./logger.js";
9
- export { AnsiColor, AnsiColorFormatterOptions, AnsiStyle, AsyncSink, Config, ConfigError, ConsoleFormatter, ConsoleSinkOptions, ContextLocalStorage, Filter, FilterLike, FormattedValues, JsonLinesFormatterOptions, LogLevel, LogMethod, LogRecord, Logger, LoggerConfig, Sink, StreamSinkOptions, TextFormatter, TextFormatterOptions, ansiColorFormatter, compareLogLevel, configure, configureSync, defaultConsoleFormatter, defaultTextFormatter, dispose, disposeSync, fromAsyncSink, getAnsiColorFormatter, getConfig, getConsoleSink, getJsonLinesFormatter, getLevelFilter, getLogLevels, getLogger, getStreamSink, getTextFormatter, isLogLevel, jsonLinesFormatter, parseLogLevel, reset, resetSync, toFilter, withContext, withFilter };
9
+ export { AnsiColor, AnsiColorFormatterOptions, AnsiStyle, AsyncSink, Config, ConfigError, ConsoleFormatter, ConsoleSinkOptions, ContextLocalStorage, Filter, FilterLike, FingersCrossedOptions, FormattedValues, JsonLinesFormatterOptions, LogLevel, LogMethod, LogRecord, Logger, LoggerConfig, Sink, StreamSinkOptions, TextFormatter, TextFormatterOptions, ansiColorFormatter, compareLogLevel, configure, configureSync, defaultConsoleFormatter, defaultTextFormatter, dispose, disposeSync, fingersCrossed, fromAsyncSink, getAnsiColorFormatter, getConfig, getConsoleSink, getJsonLinesFormatter, getLevelFilter, getLogLevels, getLogger, getStreamSink, getTextFormatter, isLogLevel, jsonLinesFormatter, parseLogLevel, reset, resetSync, toFilter, withContext, withFilter };
package/dist/mod.js CHANGED
@@ -2,8 +2,8 @@ import { getLevelFilter, toFilter } from "./filter.js";
2
2
  import { compareLogLevel, getLogLevels, isLogLevel, parseLogLevel } from "./level.js";
3
3
  import { getLogger } from "./logger.js";
4
4
  import { ansiColorFormatter, defaultConsoleFormatter, defaultTextFormatter, getAnsiColorFormatter, getJsonLinesFormatter, getTextFormatter, jsonLinesFormatter } from "./formatter.js";
5
- import { fromAsyncSink, getConsoleSink, getStreamSink, withFilter } from "./sink.js";
5
+ import { fingersCrossed, fromAsyncSink, getConsoleSink, getStreamSink, withFilter } from "./sink.js";
6
6
  import { ConfigError, configure, configureSync, dispose, disposeSync, getConfig, reset, resetSync } from "./config.js";
7
7
  import { withContext } from "./context.js";
8
8
 
9
- export { ConfigError, ansiColorFormatter, compareLogLevel, configure, configureSync, defaultConsoleFormatter, defaultTextFormatter, dispose, disposeSync, fromAsyncSink, getAnsiColorFormatter, getConfig, getConsoleSink, getJsonLinesFormatter, getLevelFilter, getLogLevels, getLogger, getStreamSink, getTextFormatter, isLogLevel, jsonLinesFormatter, parseLogLevel, reset, resetSync, toFilter, withContext, withFilter };
9
+ export { ConfigError, ansiColorFormatter, compareLogLevel, configure, configureSync, defaultConsoleFormatter, defaultTextFormatter, dispose, disposeSync, fingersCrossed, fromAsyncSink, getAnsiColorFormatter, getConfig, getConsoleSink, getJsonLinesFormatter, getLevelFilter, getLogLevels, getLogger, getStreamSink, getTextFormatter, isLogLevel, jsonLinesFormatter, parseLogLevel, reset, resetSync, toFilter, withContext, withFilter };
package/dist/sink.cjs CHANGED
@@ -1,4 +1,5 @@
1
1
  const require_filter = require('./filter.cjs');
2
+ const require_level = require('./level.cjs');
2
3
  const require_formatter = require('./formatter.cjs');
3
4
 
4
5
  //#region src/sink.ts
@@ -216,8 +217,143 @@ function fromAsyncSink(asyncSink) {
216
217
  };
217
218
  return sink;
218
219
  }
220
+ /**
221
+ * Creates a sink that buffers log records until a trigger level is reached.
222
+ * This pattern, known as "fingers crossed" logging, keeps detailed debug logs
223
+ * in memory and only outputs them when an error or other significant event occurs.
224
+ *
225
+ * @example Basic usage with default settings
226
+ * ```typescript
227
+ * const sink = fingersCrossed(getConsoleSink());
228
+ * // Debug and info logs are buffered
229
+ * // When an error occurs, all buffered logs + the error are output
230
+ * ```
231
+ *
232
+ * @example Custom trigger level and buffer size
233
+ * ```typescript
234
+ * const sink = fingersCrossed(getConsoleSink(), {
235
+ * triggerLevel: "warning", // Trigger on warning or higher
236
+ * maxBufferSize: 500 // Keep last 500 records
237
+ * });
238
+ * ```
239
+ *
240
+ * @example Category isolation
241
+ * ```typescript
242
+ * const sink = fingersCrossed(getConsoleSink(), {
243
+ * isolateByCategory: "descendant" // Separate buffers per category
244
+ * });
245
+ * // Error in ["app"] triggers flush of ["app"] and ["app", "module"] buffers
246
+ * // But not ["other"] buffer
247
+ * ```
248
+ *
249
+ * @param sink The sink to wrap. Buffered records are sent to this sink when
250
+ * triggered.
251
+ * @param options Configuration options for the fingers crossed behavior.
252
+ * @returns A sink that buffers records until the trigger level is reached.
253
+ * @since 1.1.0
254
+ */
255
+ function fingersCrossed(sink, options = {}) {
256
+ const triggerLevel = options.triggerLevel ?? "error";
257
+ const maxBufferSize = Math.max(0, options.maxBufferSize ?? 1e3);
258
+ const isolateByCategory = options.isolateByCategory;
259
+ try {
260
+ require_level.compareLogLevel("trace", triggerLevel);
261
+ } catch (error) {
262
+ throw new TypeError(`Invalid triggerLevel: ${JSON.stringify(triggerLevel)}. ${error instanceof Error ? error.message : String(error)}`);
263
+ }
264
+ function isDescendant(parent, child) {
265
+ if (parent.length === 0 || child.length === 0) return false;
266
+ if (parent.length > child.length) return false;
267
+ return parent.every((p, i) => p === child[i]);
268
+ }
269
+ function isAncestor(child, parent) {
270
+ if (child.length === 0 || parent.length === 0) return false;
271
+ if (child.length < parent.length) return false;
272
+ return parent.every((p, i) => p === child[i]);
273
+ }
274
+ let shouldFlushBuffer = null;
275
+ if (isolateByCategory) if (typeof isolateByCategory === "function") shouldFlushBuffer = isolateByCategory;
276
+ else switch (isolateByCategory) {
277
+ case "descendant":
278
+ shouldFlushBuffer = (trigger, buffered) => isDescendant(trigger, buffered);
279
+ break;
280
+ case "ancestor":
281
+ shouldFlushBuffer = (trigger, buffered) => isAncestor(trigger, buffered);
282
+ break;
283
+ case "both":
284
+ shouldFlushBuffer = (trigger, buffered) => isDescendant(trigger, buffered) || isAncestor(trigger, buffered);
285
+ break;
286
+ }
287
+ function getCategoryKey(category) {
288
+ return JSON.stringify(category);
289
+ }
290
+ function parseCategoryKey(key) {
291
+ return JSON.parse(key);
292
+ }
293
+ if (!isolateByCategory) {
294
+ const buffer = [];
295
+ let triggered = false;
296
+ return (record) => {
297
+ if (triggered) {
298
+ sink(record);
299
+ return;
300
+ }
301
+ if (require_level.compareLogLevel(record.level, triggerLevel) >= 0) {
302
+ triggered = true;
303
+ for (const bufferedRecord of buffer) sink(bufferedRecord);
304
+ buffer.length = 0;
305
+ sink(record);
306
+ } else {
307
+ buffer.push(record);
308
+ while (buffer.length > maxBufferSize) buffer.shift();
309
+ }
310
+ };
311
+ } else {
312
+ const buffers = /* @__PURE__ */ new Map();
313
+ const triggered = /* @__PURE__ */ new Set();
314
+ return (record) => {
315
+ const categoryKey = getCategoryKey(record.category);
316
+ if (triggered.has(categoryKey)) {
317
+ sink(record);
318
+ return;
319
+ }
320
+ if (require_level.compareLogLevel(record.level, triggerLevel) >= 0) {
321
+ const keysToFlush = /* @__PURE__ */ new Set();
322
+ for (const [bufferedKey] of buffers) if (bufferedKey === categoryKey) keysToFlush.add(bufferedKey);
323
+ else if (shouldFlushBuffer) {
324
+ const bufferedCategory = parseCategoryKey(bufferedKey);
325
+ try {
326
+ if (shouldFlushBuffer(record.category, bufferedCategory)) keysToFlush.add(bufferedKey);
327
+ } catch {}
328
+ }
329
+ const allRecordsToFlush = [];
330
+ for (const key of keysToFlush) {
331
+ const buffer = buffers.get(key);
332
+ if (buffer) {
333
+ allRecordsToFlush.push(...buffer);
334
+ buffers.delete(key);
335
+ triggered.add(key);
336
+ }
337
+ }
338
+ allRecordsToFlush.sort((a, b) => a.timestamp - b.timestamp);
339
+ for (const bufferedRecord of allRecordsToFlush) sink(bufferedRecord);
340
+ triggered.add(categoryKey);
341
+ sink(record);
342
+ } else {
343
+ let buffer = buffers.get(categoryKey);
344
+ if (!buffer) {
345
+ buffer = [];
346
+ buffers.set(categoryKey, buffer);
347
+ }
348
+ buffer.push(record);
349
+ while (buffer.length > maxBufferSize) buffer.shift();
350
+ }
351
+ };
352
+ }
353
+ }
219
354
 
220
355
  //#endregion
356
+ exports.fingersCrossed = fingersCrossed;
221
357
  exports.fromAsyncSink = fromAsyncSink;
222
358
  exports.getConsoleSink = getConsoleSink;
223
359
  exports.getStreamSink = getStreamSink;
package/dist/sink.d.cts CHANGED
@@ -208,6 +208,79 @@ declare function getConsoleSink(options?: ConsoleSinkOptions): Sink | (Sink & Di
208
208
  * @since 1.0.0
209
209
  */
210
210
  declare function fromAsyncSink(asyncSink: AsyncSink): Sink & AsyncDisposable;
211
+ /**
212
+ * Options for the {@link fingersCrossed} function.
213
+ * @since 1.1.0
214
+ */
215
+ interface FingersCrossedOptions {
216
+ /**
217
+ * Minimum log level that triggers buffer flush.
218
+ * When a log record at or above this level is received, all buffered
219
+ * records are flushed to the wrapped sink.
220
+ * @default `"error"`
221
+ */
222
+ readonly triggerLevel?: LogLevel;
223
+ /**
224
+ * Maximum buffer size before oldest records are dropped.
225
+ * When the buffer exceeds this size, the oldest records are removed
226
+ * to prevent unbounded memory growth.
227
+ * @default `1000`
228
+ */
229
+ readonly maxBufferSize?: number;
230
+ /**
231
+ * Category isolation mode or custom matcher function.
232
+ *
233
+ * When `undefined` (default), all log records share a single buffer.
234
+ *
235
+ * When set to a mode string:
236
+ *
237
+ * - `"descendant"`: Flush child category buffers when parent triggers
238
+ * - `"ancestor"`: Flush parent category buffers when child triggers
239
+ * - `"both"`: Flush both parent and child category buffers
240
+ *
241
+ * When set to a function, it receives the trigger category and buffered
242
+ * category and should return true if the buffered category should be flushed.
243
+ *
244
+ * @default `undefined` (no isolation, single global buffer)
245
+ */
246
+ readonly isolateByCategory?: "descendant" | "ancestor" | "both" | ((triggerCategory: readonly string[], bufferedCategory: readonly string[]) => boolean);
247
+ }
248
+ /**
249
+ * Creates a sink that buffers log records until a trigger level is reached.
250
+ * This pattern, known as "fingers crossed" logging, keeps detailed debug logs
251
+ * in memory and only outputs them when an error or other significant event occurs.
252
+ *
253
+ * @example Basic usage with default settings
254
+ * ```typescript
255
+ * const sink = fingersCrossed(getConsoleSink());
256
+ * // Debug and info logs are buffered
257
+ * // When an error occurs, all buffered logs + the error are output
258
+ * ```
259
+ *
260
+ * @example Custom trigger level and buffer size
261
+ * ```typescript
262
+ * const sink = fingersCrossed(getConsoleSink(), {
263
+ * triggerLevel: "warning", // Trigger on warning or higher
264
+ * maxBufferSize: 500 // Keep last 500 records
265
+ * });
266
+ * ```
267
+ *
268
+ * @example Category isolation
269
+ * ```typescript
270
+ * const sink = fingersCrossed(getConsoleSink(), {
271
+ * isolateByCategory: "descendant" // Separate buffers per category
272
+ * });
273
+ * // Error in ["app"] triggers flush of ["app"] and ["app", "module"] buffers
274
+ * // But not ["other"] buffer
275
+ * ```
276
+ *
277
+ * @param sink The sink to wrap. Buffered records are sent to this sink when
278
+ * triggered.
279
+ * @param options Configuration options for the fingers crossed behavior.
280
+ * @returns A sink that buffers records until the trigger level is reached.
281
+ * @since 1.1.0
282
+ */
283
+ declare function fingersCrossed(sink: Sink, options?: FingersCrossedOptions): Sink;
211
284
  //#endregion
212
- export { AsyncSink, ConsoleSinkOptions, Sink, StreamSinkOptions, fromAsyncSink, getConsoleSink, getStreamSink, withFilter };
285
+ export { AsyncSink, ConsoleSinkOptions, FingersCrossedOptions, Sink, StreamSinkOptions, fingersCrossed, fromAsyncSink, getConsoleSink, getStreamSink, withFilter };
213
286
  //# sourceMappingURL=sink.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sink.d.cts","names":[],"sources":["../src/sink.ts"],"sourcesContent":[],"mappings":";;;;;;;;;AAmBA;AAWA;;;;AAAsD;AAgBtD;AAA0B,KA3Bd,IAAA,GA2Bc,CAAA,MAAA,EA3BE,SA2BF,EAAA,GAAA,IAAA;;;;AAAsC;AAUhE;;;;AAS8C;AA+D9B,KAlGJ,SAAA,GAkGiB,CAAA,MAAA,EAlGI,SAkGJ,EAAA,GAlGkB,OAkGlB,CAAA,IAAA,CAAA;;;;;;AAGJ;AAgGxB;AAOD;;;;;;;AA2BY,iBAvNI,UAAA,CAuNJ,IAAA,EAvNqB,IAuNrB,EAAA,MAAA,EAvNmC,UAuNnC,CAAA,EAvNgD,IAuNhD;AAAO;AA8CnB;;AACW,UA5PM,iBAAA,CA4PN;EAAuB;;;EACN,SAAA,CAAA,EAzPd,aAyPc;EA4HZ;;;EAAkC,OAAG,CAAA,EAAA;IAAO,MAAA,CAAA,IAAA,EAAA,MAAA,CAAA,EAhXxB,UAgXwB;EAAe,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAjT3D,aAAA,SACN,0BACC,oBACR,OAAO;KAkGL,aAAA;;;;UAKY,kBAAA;;;;;cAKH,mBAAmB;;;;;;;;;;;;;;;;aAiBpB,OAAO,UAAU;;;;YAKlB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA8CI,cAAA,WACL,qBACR,QAAQ,OAAO;;;;;;;;;;;;;;;;;;;;;iBA4HF,aAAA,YAAyB,YAAY,OAAO"}
1
+ {"version":3,"file":"sink.d.cts","names":[],"sources":["../src/sink.ts"],"sourcesContent":[],"mappings":";;;;;;;;;AAmBA;AAWA;;;;AAAsD;AAgBtD;AAA0B,KA3Bd,IAAA,GA2Bc,CAAA,MAAA,EA3BE,SA2BF,EAAA,GAAA,IAAA;;;;AAAsC;AAUhE;;;;AAS8C;AA+D9B,KAlGJ,SAAA,GAkGiB,CAAA,MAAA,EAlGI,SAkGJ,EAAA,GAlGkB,OAkGlB,CAAA,IAAA,CAAA;;;;;;AAGJ;AAgGxB;AAOD;;;;;;;AA2BY,iBAvNI,UAAA,CAuNJ,IAAA,EAvNqB,IAuNrB,EAAA,MAAA,EAvNmC,UAuNnC,CAAA,EAvNgD,IAuNhD;AAAO;AA8CnB;;AACW,UA5PM,iBAAA,CA4PN;EAAuB;;;EACN,SAAA,CAAA,EAzPd,aAyPc;EA4HZ;;;EAAkC,OAAG,CAAA,EAAA;IAAO,MAAA,CAAA,IAAA,EAAA,MAAA,CAAA,EAhXxB,UAgXwB;EAAe,CAAA;EAmB1D;AA8EjB;;;;;AAGO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBArZS,aAAA,SACN,0BACC,oBACR,OAAO;KAkGL,aAAA;;;;UAKY,kBAAA;;;;;cAKH,mBAAmB;;;;;;;;;;;;;;;;aAiBpB,OAAO,UAAU;;;;YAKlB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA8CI,cAAA,WACL,qBACR,QAAQ,OAAO;;;;;;;;;;;;;;;;;;;;;iBA4HF,aAAA,YAAyB,YAAY,OAAO;;;;;UAmB3C,qBAAA;;;;;;;0BAOS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAuEV,cAAA,OACR,gBACG,wBACR"}
package/dist/sink.d.ts CHANGED
@@ -208,6 +208,79 @@ declare function getConsoleSink(options?: ConsoleSinkOptions): Sink | (Sink & Di
208
208
  * @since 1.0.0
209
209
  */
210
210
  declare function fromAsyncSink(asyncSink: AsyncSink): Sink & AsyncDisposable;
211
+ /**
212
+ * Options for the {@link fingersCrossed} function.
213
+ * @since 1.1.0
214
+ */
215
+ interface FingersCrossedOptions {
216
+ /**
217
+ * Minimum log level that triggers buffer flush.
218
+ * When a log record at or above this level is received, all buffered
219
+ * records are flushed to the wrapped sink.
220
+ * @default `"error"`
221
+ */
222
+ readonly triggerLevel?: LogLevel;
223
+ /**
224
+ * Maximum buffer size before oldest records are dropped.
225
+ * When the buffer exceeds this size, the oldest records are removed
226
+ * to prevent unbounded memory growth.
227
+ * @default `1000`
228
+ */
229
+ readonly maxBufferSize?: number;
230
+ /**
231
+ * Category isolation mode or custom matcher function.
232
+ *
233
+ * When `undefined` (default), all log records share a single buffer.
234
+ *
235
+ * When set to a mode string:
236
+ *
237
+ * - `"descendant"`: Flush child category buffers when parent triggers
238
+ * - `"ancestor"`: Flush parent category buffers when child triggers
239
+ * - `"both"`: Flush both parent and child category buffers
240
+ *
241
+ * When set to a function, it receives the trigger category and buffered
242
+ * category and should return true if the buffered category should be flushed.
243
+ *
244
+ * @default `undefined` (no isolation, single global buffer)
245
+ */
246
+ readonly isolateByCategory?: "descendant" | "ancestor" | "both" | ((triggerCategory: readonly string[], bufferedCategory: readonly string[]) => boolean);
247
+ }
248
+ /**
249
+ * Creates a sink that buffers log records until a trigger level is reached.
250
+ * This pattern, known as "fingers crossed" logging, keeps detailed debug logs
251
+ * in memory and only outputs them when an error or other significant event occurs.
252
+ *
253
+ * @example Basic usage with default settings
254
+ * ```typescript
255
+ * const sink = fingersCrossed(getConsoleSink());
256
+ * // Debug and info logs are buffered
257
+ * // When an error occurs, all buffered logs + the error are output
258
+ * ```
259
+ *
260
+ * @example Custom trigger level and buffer size
261
+ * ```typescript
262
+ * const sink = fingersCrossed(getConsoleSink(), {
263
+ * triggerLevel: "warning", // Trigger on warning or higher
264
+ * maxBufferSize: 500 // Keep last 500 records
265
+ * });
266
+ * ```
267
+ *
268
+ * @example Category isolation
269
+ * ```typescript
270
+ * const sink = fingersCrossed(getConsoleSink(), {
271
+ * isolateByCategory: "descendant" // Separate buffers per category
272
+ * });
273
+ * // Error in ["app"] triggers flush of ["app"] and ["app", "module"] buffers
274
+ * // But not ["other"] buffer
275
+ * ```
276
+ *
277
+ * @param sink The sink to wrap. Buffered records are sent to this sink when
278
+ * triggered.
279
+ * @param options Configuration options for the fingers crossed behavior.
280
+ * @returns A sink that buffers records until the trigger level is reached.
281
+ * @since 1.1.0
282
+ */
283
+ declare function fingersCrossed(sink: Sink, options?: FingersCrossedOptions): Sink;
211
284
  //#endregion
212
- export { AsyncSink, ConsoleSinkOptions, Sink, StreamSinkOptions, fromAsyncSink, getConsoleSink, getStreamSink, withFilter };
285
+ export { AsyncSink, ConsoleSinkOptions, FingersCrossedOptions, Sink, StreamSinkOptions, fingersCrossed, fromAsyncSink, getConsoleSink, getStreamSink, withFilter };
213
286
  //# sourceMappingURL=sink.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sink.d.ts","names":[],"sources":["../src/sink.ts"],"sourcesContent":[],"mappings":";;;;;;;;;AAmBA;AAWA;;;;AAAsD;AAgBtD;AAA0B,KA3Bd,IAAA,GA2Bc,CAAA,MAAA,EA3BE,SA2BF,EAAA,GAAA,IAAA;;;;AAAsC;AAUhE;;;;AAS8C;AA+D9B,KAlGJ,SAAA,GAkGiB,CAAA,MAAA,EAlGI,SAkGJ,EAAA,GAlGkB,OAkGlB,CAAA,IAAA,CAAA;;;;;;AAGJ;AAgGxB;AAOD;;;;;;;AA2BY,iBAvNI,UAAA,CAuNJ,IAAA,EAvNqB,IAuNrB,EAAA,MAAA,EAvNmC,UAuNnC,CAAA,EAvNgD,IAuNhD;AAAO;AA8CnB;;AACW,UA5PM,iBAAA,CA4PN;EAAuB;;;EACN,SAAA,CAAA,EAzPd,aAyPc;EA4HZ;;;EAAkC,OAAG,CAAA,EAAA;IAAO,MAAA,CAAA,IAAA,EAAA,MAAA,CAAA,EAhXxB,UAgXwB;EAAe,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAjT3D,aAAA,SACN,0BACC,oBACR,OAAO;KAkGL,aAAA;;;;UAKY,kBAAA;;;;;cAKH,mBAAmB;;;;;;;;;;;;;;;;aAiBpB,OAAO,UAAU;;;;YAKlB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA8CI,cAAA,WACL,qBACR,QAAQ,OAAO;;;;;;;;;;;;;;;;;;;;;iBA4HF,aAAA,YAAyB,YAAY,OAAO"}
1
+ {"version":3,"file":"sink.d.ts","names":[],"sources":["../src/sink.ts"],"sourcesContent":[],"mappings":";;;;;;;;;AAmBA;AAWA;;;;AAAsD;AAgBtD;AAA0B,KA3Bd,IAAA,GA2Bc,CAAA,MAAA,EA3BE,SA2BF,EAAA,GAAA,IAAA;;;;AAAsC;AAUhE;;;;AAS8C;AA+D9B,KAlGJ,SAAA,GAkGiB,CAAA,MAAA,EAlGI,SAkGJ,EAAA,GAlGkB,OAkGlB,CAAA,IAAA,CAAA;;;;;;AAGJ;AAgGxB;AAOD;;;;;;;AA2BY,iBAvNI,UAAA,CAuNJ,IAAA,EAvNqB,IAuNrB,EAAA,MAAA,EAvNmC,UAuNnC,CAAA,EAvNgD,IAuNhD;AAAO;AA8CnB;;AACW,UA5PM,iBAAA,CA4PN;EAAuB;;;EACN,SAAA,CAAA,EAzPd,aAyPc;EA4HZ;;;EAAkC,OAAG,CAAA,EAAA;IAAO,MAAA,CAAA,IAAA,EAAA,MAAA,CAAA,EAhXxB,UAgXwB;EAAe,CAAA;EAmB1D;AA8EjB;;;;;AAGO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBArZS,aAAA,SACN,0BACC,oBACR,OAAO;KAkGL,aAAA;;;;UAKY,kBAAA;;;;;cAKH,mBAAmB;;;;;;;;;;;;;;;;aAiBpB,OAAO,UAAU;;;;YAKlB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA8CI,cAAA,WACL,qBACR,QAAQ,OAAO;;;;;;;;;;;;;;;;;;;;;iBA4HF,aAAA,YAAyB,YAAY,OAAO;;;;;UAmB3C,qBAAA;;;;;;;0BAOS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAuEV,cAAA,OACR,gBACG,wBACR"}
package/dist/sink.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { toFilter } from "./filter.js";
2
+ import { compareLogLevel } from "./level.js";
2
3
  import { defaultConsoleFormatter, defaultTextFormatter } from "./formatter.js";
3
4
 
4
5
  //#region src/sink.ts
@@ -216,7 +217,141 @@ function fromAsyncSink(asyncSink) {
216
217
  };
217
218
  return sink;
218
219
  }
220
+ /**
221
+ * Creates a sink that buffers log records until a trigger level is reached.
222
+ * This pattern, known as "fingers crossed" logging, keeps detailed debug logs
223
+ * in memory and only outputs them when an error or other significant event occurs.
224
+ *
225
+ * @example Basic usage with default settings
226
+ * ```typescript
227
+ * const sink = fingersCrossed(getConsoleSink());
228
+ * // Debug and info logs are buffered
229
+ * // When an error occurs, all buffered logs + the error are output
230
+ * ```
231
+ *
232
+ * @example Custom trigger level and buffer size
233
+ * ```typescript
234
+ * const sink = fingersCrossed(getConsoleSink(), {
235
+ * triggerLevel: "warning", // Trigger on warning or higher
236
+ * maxBufferSize: 500 // Keep last 500 records
237
+ * });
238
+ * ```
239
+ *
240
+ * @example Category isolation
241
+ * ```typescript
242
+ * const sink = fingersCrossed(getConsoleSink(), {
243
+ * isolateByCategory: "descendant" // Separate buffers per category
244
+ * });
245
+ * // Error in ["app"] triggers flush of ["app"] and ["app", "module"] buffers
246
+ * // But not ["other"] buffer
247
+ * ```
248
+ *
249
+ * @param sink The sink to wrap. Buffered records are sent to this sink when
250
+ * triggered.
251
+ * @param options Configuration options for the fingers crossed behavior.
252
+ * @returns A sink that buffers records until the trigger level is reached.
253
+ * @since 1.1.0
254
+ */
255
+ function fingersCrossed(sink, options = {}) {
256
+ const triggerLevel = options.triggerLevel ?? "error";
257
+ const maxBufferSize = Math.max(0, options.maxBufferSize ?? 1e3);
258
+ const isolateByCategory = options.isolateByCategory;
259
+ try {
260
+ compareLogLevel("trace", triggerLevel);
261
+ } catch (error) {
262
+ throw new TypeError(`Invalid triggerLevel: ${JSON.stringify(triggerLevel)}. ${error instanceof Error ? error.message : String(error)}`);
263
+ }
264
+ function isDescendant(parent, child) {
265
+ if (parent.length === 0 || child.length === 0) return false;
266
+ if (parent.length > child.length) return false;
267
+ return parent.every((p, i) => p === child[i]);
268
+ }
269
+ function isAncestor(child, parent) {
270
+ if (child.length === 0 || parent.length === 0) return false;
271
+ if (child.length < parent.length) return false;
272
+ return parent.every((p, i) => p === child[i]);
273
+ }
274
+ let shouldFlushBuffer = null;
275
+ if (isolateByCategory) if (typeof isolateByCategory === "function") shouldFlushBuffer = isolateByCategory;
276
+ else switch (isolateByCategory) {
277
+ case "descendant":
278
+ shouldFlushBuffer = (trigger, buffered) => isDescendant(trigger, buffered);
279
+ break;
280
+ case "ancestor":
281
+ shouldFlushBuffer = (trigger, buffered) => isAncestor(trigger, buffered);
282
+ break;
283
+ case "both":
284
+ shouldFlushBuffer = (trigger, buffered) => isDescendant(trigger, buffered) || isAncestor(trigger, buffered);
285
+ break;
286
+ }
287
+ function getCategoryKey(category) {
288
+ return JSON.stringify(category);
289
+ }
290
+ function parseCategoryKey(key) {
291
+ return JSON.parse(key);
292
+ }
293
+ if (!isolateByCategory) {
294
+ const buffer = [];
295
+ let triggered = false;
296
+ return (record) => {
297
+ if (triggered) {
298
+ sink(record);
299
+ return;
300
+ }
301
+ if (compareLogLevel(record.level, triggerLevel) >= 0) {
302
+ triggered = true;
303
+ for (const bufferedRecord of buffer) sink(bufferedRecord);
304
+ buffer.length = 0;
305
+ sink(record);
306
+ } else {
307
+ buffer.push(record);
308
+ while (buffer.length > maxBufferSize) buffer.shift();
309
+ }
310
+ };
311
+ } else {
312
+ const buffers = /* @__PURE__ */ new Map();
313
+ const triggered = /* @__PURE__ */ new Set();
314
+ return (record) => {
315
+ const categoryKey = getCategoryKey(record.category);
316
+ if (triggered.has(categoryKey)) {
317
+ sink(record);
318
+ return;
319
+ }
320
+ if (compareLogLevel(record.level, triggerLevel) >= 0) {
321
+ const keysToFlush = /* @__PURE__ */ new Set();
322
+ for (const [bufferedKey] of buffers) if (bufferedKey === categoryKey) keysToFlush.add(bufferedKey);
323
+ else if (shouldFlushBuffer) {
324
+ const bufferedCategory = parseCategoryKey(bufferedKey);
325
+ try {
326
+ if (shouldFlushBuffer(record.category, bufferedCategory)) keysToFlush.add(bufferedKey);
327
+ } catch {}
328
+ }
329
+ const allRecordsToFlush = [];
330
+ for (const key of keysToFlush) {
331
+ const buffer = buffers.get(key);
332
+ if (buffer) {
333
+ allRecordsToFlush.push(...buffer);
334
+ buffers.delete(key);
335
+ triggered.add(key);
336
+ }
337
+ }
338
+ allRecordsToFlush.sort((a, b) => a.timestamp - b.timestamp);
339
+ for (const bufferedRecord of allRecordsToFlush) sink(bufferedRecord);
340
+ triggered.add(categoryKey);
341
+ sink(record);
342
+ } else {
343
+ let buffer = buffers.get(categoryKey);
344
+ if (!buffer) {
345
+ buffer = [];
346
+ buffers.set(categoryKey, buffer);
347
+ }
348
+ buffer.push(record);
349
+ while (buffer.length > maxBufferSize) buffer.shift();
350
+ }
351
+ };
352
+ }
353
+ }
219
354
 
220
355
  //#endregion
221
- export { fromAsyncSink, getConsoleSink, getStreamSink, withFilter };
356
+ export { fingersCrossed, fromAsyncSink, getConsoleSink, getStreamSink, withFilter };
222
357
  //# sourceMappingURL=sink.js.map
package/dist/sink.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"sink.js","names":["sink: Sink","filter: FilterLike","record: LogRecord","stream: WritableStream","options: StreamSinkOptions","sink: Sink & AsyncDisposable","buffer: LogRecord[]","flushTimer: ReturnType<typeof setInterval> | null","activeFlush: Promise<void> | null","nonBlockingSink: Sink & AsyncDisposable","options: ConsoleSinkOptions","levelMap: Record<LogLevel, ConsoleMethod>","nonBlockingSink: Sink & Disposable","asyncSink: AsyncSink"],"sources":["../src/sink.ts"],"sourcesContent":["import { type FilterLike, toFilter } from \"./filter.ts\";\nimport {\n type ConsoleFormatter,\n defaultConsoleFormatter,\n defaultTextFormatter,\n type TextFormatter,\n} from \"./formatter.ts\";\nimport type { LogLevel } from \"./level.ts\";\nimport type { LogRecord } from \"./record.ts\";\n\n/**\n * A sink is a function that accepts a log record and prints it somewhere.\n * Thrown exceptions will be suppressed and then logged to the meta logger,\n * a {@link Logger} with the category `[\"logtape\", \"meta\"]`. (In that case,\n * the meta log record will not be passed to the sink to avoid infinite\n * recursion.)\n *\n * @param record The log record to sink.\n */\nexport type Sink = (record: LogRecord) => void;\n\n/**\n * An async sink is a function that accepts a log record and asynchronously\n * processes it. This type is used with {@link fromAsyncSink} to create\n * a regular sink that properly handles asynchronous operations.\n *\n * @param record The log record to process asynchronously.\n * @returns A promise that resolves when the record has been processed.\n * @since 1.0.0\n */\nexport type AsyncSink = (record: LogRecord) => Promise<void>;\n\n/**\n * Turns a sink into a filtered sink. The returned sink only logs records that\n * pass the filter.\n *\n * @example Filter a console sink to only log records with the info level\n * ```typescript\n * const sink = withFilter(getConsoleSink(), \"info\");\n * ```\n *\n * @param sink A sink to be filtered.\n * @param filter A filter to apply to the sink. It can be either a filter\n * function or a {@link LogLevel} string.\n * @returns A sink that only logs records that pass the filter.\n */\nexport function withFilter(sink: Sink, filter: FilterLike): Sink {\n const filterFunc = toFilter(filter);\n return (record: LogRecord) => {\n if (filterFunc(record)) sink(record);\n };\n}\n\n/**\n * Options for the {@link getStreamSink} function.\n */\nexport interface StreamSinkOptions {\n /**\n * The text formatter to use. Defaults to {@link defaultTextFormatter}.\n */\n formatter?: TextFormatter;\n\n /**\n * The text encoder to use. Defaults to an instance of {@link TextEncoder}.\n */\n encoder?: { encode(text: string): Uint8Array };\n\n /**\n * Enable non-blocking mode with optional buffer configuration.\n * When enabled, log records are buffered and flushed in the background.\n *\n * @example Simple non-blocking mode\n * ```typescript\n * getStreamSink(stream, { nonBlocking: true });\n * ```\n *\n * @example Custom buffer configuration\n * ```typescript\n * getStreamSink(stream, {\n * nonBlocking: {\n * bufferSize: 1000,\n * flushInterval: 50\n * }\n * });\n * ```\n *\n * @default `false`\n * @since 1.0.0\n */\n nonBlocking?: boolean | {\n /**\n * Maximum number of records to buffer before flushing.\n * @default `100`\n */\n bufferSize?: number;\n\n /**\n * Interval in milliseconds between automatic flushes.\n * @default `100`\n */\n flushInterval?: number;\n };\n}\n\n/**\n * A factory that returns a sink that writes to a {@link WritableStream}.\n *\n * Note that the `stream` is of Web Streams API, which is different from\n * Node.js streams. You can convert a Node.js stream to a Web Streams API\n * stream using [`stream.Writable.toWeb()`] method.\n *\n * [`stream.Writable.toWeb()`]: https://nodejs.org/api/stream.html#streamwritabletowebstreamwritable\n *\n * @example Sink to the standard error in Deno\n * ```typescript\n * const stderrSink = getStreamSink(Deno.stderr.writable);\n * ```\n *\n * @example Sink to the standard error in Node.js\n * ```typescript\n * import stream from \"node:stream\";\n * const stderrSink = getStreamSink(stream.Writable.toWeb(process.stderr));\n * ```\n *\n * @param stream The stream to write to.\n * @param options The options for the sink.\n * @returns A sink that writes to the stream.\n */\nexport function getStreamSink(\n stream: WritableStream,\n options: StreamSinkOptions = {},\n): Sink & AsyncDisposable {\n const formatter = options.formatter ?? defaultTextFormatter;\n const encoder = options.encoder ?? new TextEncoder();\n const writer = stream.getWriter();\n\n if (!options.nonBlocking) {\n let lastPromise = Promise.resolve();\n const sink: Sink & AsyncDisposable = (record: LogRecord) => {\n const bytes = encoder.encode(formatter(record));\n lastPromise = lastPromise\n .then(() => writer.ready)\n .then(() => writer.write(bytes));\n };\n sink[Symbol.asyncDispose] = async () => {\n await lastPromise;\n await writer.close();\n };\n return sink;\n }\n\n // Non-blocking mode implementation\n const nonBlockingConfig = options.nonBlocking === true\n ? {}\n : options.nonBlocking;\n const bufferSize = nonBlockingConfig.bufferSize ?? 100;\n const flushInterval = nonBlockingConfig.flushInterval ?? 100;\n\n const buffer: LogRecord[] = [];\n let flushTimer: ReturnType<typeof setInterval> | null = null;\n let disposed = false;\n let activeFlush: Promise<void> | null = null;\n const maxBufferSize = bufferSize * 2; // Overflow protection\n\n async function flush(): Promise<void> {\n if (buffer.length === 0) return;\n\n const records = buffer.splice(0);\n for (const record of records) {\n try {\n const bytes = encoder.encode(formatter(record));\n await writer.ready;\n await writer.write(bytes);\n } catch {\n // Silently ignore errors in non-blocking mode to avoid disrupting the application\n }\n }\n }\n\n function scheduleFlush(): void {\n if (activeFlush) return;\n\n activeFlush = flush().finally(() => {\n activeFlush = null;\n });\n }\n\n function startFlushTimer(): void {\n if (flushTimer !== null || disposed) return;\n\n flushTimer = setInterval(() => {\n scheduleFlush();\n }, flushInterval);\n }\n\n const nonBlockingSink: Sink & AsyncDisposable = (record: LogRecord) => {\n if (disposed) return;\n\n // Buffer overflow protection: drop oldest records if buffer is too large\n if (buffer.length >= maxBufferSize) {\n buffer.shift(); // Remove oldest record\n }\n\n buffer.push(record);\n\n if (buffer.length >= bufferSize) {\n scheduleFlush();\n } else if (flushTimer === null) {\n startFlushTimer();\n }\n };\n\n nonBlockingSink[Symbol.asyncDispose] = async () => {\n disposed = true;\n if (flushTimer !== null) {\n clearInterval(flushTimer);\n flushTimer = null;\n }\n await flush();\n try {\n await writer.close();\n } catch {\n // Writer might already be closed or errored\n }\n };\n\n return nonBlockingSink;\n}\n\ntype ConsoleMethod = \"debug\" | \"info\" | \"log\" | \"warn\" | \"error\";\n\n/**\n * Options for the {@link getConsoleSink} function.\n */\nexport interface ConsoleSinkOptions {\n /**\n * The console formatter or text formatter to use.\n * Defaults to {@link defaultConsoleFormatter}.\n */\n formatter?: ConsoleFormatter | TextFormatter;\n\n /**\n * The mapping from log levels to console methods. Defaults to:\n *\n * ```typescript\n * {\n * trace: \"trace\",\n * debug: \"debug\",\n * info: \"info\",\n * warning: \"warn\",\n * error: \"error\",\n * fatal: \"error\",\n * }\n * ```\n * @since 0.9.0\n */\n levelMap?: Record<LogLevel, ConsoleMethod>;\n\n /**\n * The console to log to. Defaults to {@link console}.\n */\n console?: Console;\n\n /**\n * Enable non-blocking mode with optional buffer configuration.\n * When enabled, log records are buffered and flushed in the background.\n *\n * @example Simple non-blocking mode\n * ```typescript\n * getConsoleSink({ nonBlocking: true });\n * ```\n *\n * @example Custom buffer configuration\n * ```typescript\n * getConsoleSink({\n * nonBlocking: {\n * bufferSize: 1000,\n * flushInterval: 50\n * }\n * });\n * ```\n *\n * @default `false`\n * @since 1.0.0\n */\n nonBlocking?: boolean | {\n /**\n * Maximum number of records to buffer before flushing.\n * @default `100`\n */\n bufferSize?: number;\n\n /**\n * Interval in milliseconds between automatic flushes.\n * @default `100`\n */\n flushInterval?: number;\n };\n}\n\n/**\n * A console sink factory that returns a sink that logs to the console.\n *\n * @param options The options for the sink.\n * @returns A sink that logs to the console. If `nonBlocking` is enabled,\n * returns a sink that also implements {@link Disposable}.\n */\nexport function getConsoleSink(\n options: ConsoleSinkOptions = {},\n): Sink | (Sink & Disposable) {\n const formatter = options.formatter ?? defaultConsoleFormatter;\n const levelMap: Record<LogLevel, ConsoleMethod> = {\n trace: \"debug\",\n debug: \"debug\",\n info: \"info\",\n warning: \"warn\",\n error: \"error\",\n fatal: \"error\",\n ...(options.levelMap ?? {}),\n };\n const console = options.console ?? globalThis.console;\n\n const baseSink = (record: LogRecord) => {\n const args = formatter(record);\n const method = levelMap[record.level];\n if (method === undefined) {\n throw new TypeError(`Invalid log level: ${record.level}.`);\n }\n if (typeof args === \"string\") {\n const msg = args.replace(/\\r?\\n$/, \"\");\n console[method](msg);\n } else {\n console[method](...args);\n }\n };\n\n if (!options.nonBlocking) {\n return baseSink;\n }\n\n // Non-blocking mode implementation\n const nonBlockingConfig = options.nonBlocking === true\n ? {}\n : options.nonBlocking;\n const bufferSize = nonBlockingConfig.bufferSize ?? 100;\n const flushInterval = nonBlockingConfig.flushInterval ?? 100;\n\n const buffer: LogRecord[] = [];\n let flushTimer: ReturnType<typeof setInterval> | null = null;\n let disposed = false;\n let flushScheduled = false;\n const maxBufferSize = bufferSize * 2; // Overflow protection\n\n function flush(): void {\n if (buffer.length === 0) return;\n\n const records = buffer.splice(0);\n for (const record of records) {\n try {\n baseSink(record);\n } catch {\n // Silently ignore errors in non-blocking mode to avoid disrupting the application\n }\n }\n }\n\n function scheduleFlush(): void {\n if (flushScheduled) return;\n\n flushScheduled = true;\n setTimeout(() => {\n flushScheduled = false;\n flush();\n }, 0);\n }\n\n function startFlushTimer(): void {\n if (flushTimer !== null || disposed) return;\n\n flushTimer = setInterval(() => {\n flush();\n }, flushInterval);\n }\n\n const nonBlockingSink: Sink & Disposable = (record: LogRecord) => {\n if (disposed) return;\n\n // Buffer overflow protection: drop oldest records if buffer is too large\n if (buffer.length >= maxBufferSize) {\n buffer.shift(); // Remove oldest record\n }\n\n buffer.push(record);\n\n if (buffer.length >= bufferSize) {\n scheduleFlush();\n } else if (flushTimer === null) {\n startFlushTimer();\n }\n };\n\n nonBlockingSink[Symbol.dispose] = () => {\n disposed = true;\n if (flushTimer !== null) {\n clearInterval(flushTimer);\n flushTimer = null;\n }\n flush();\n };\n\n return nonBlockingSink;\n}\n\n/**\n * Converts an async sink into a regular sink with proper async handling.\n * The returned sink chains async operations to ensure proper ordering and\n * implements AsyncDisposable to wait for all pending operations on disposal.\n *\n * @example Create a sink that asynchronously posts to a webhook\n * ```typescript\n * const asyncSink: AsyncSink = async (record) => {\n * await fetch(\"https://example.com/logs\", {\n * method: \"POST\",\n * body: JSON.stringify(record),\n * });\n * };\n * const sink = fromAsyncSink(asyncSink);\n * ```\n *\n * @param asyncSink The async sink function to convert.\n * @returns A sink that properly handles async operations and disposal.\n * @since 1.0.0\n */\nexport function fromAsyncSink(asyncSink: AsyncSink): Sink & AsyncDisposable {\n let lastPromise = Promise.resolve();\n const sink: Sink & AsyncDisposable = (record: LogRecord) => {\n lastPromise = lastPromise\n .then(() => asyncSink(record))\n .catch(() => {\n // Errors are handled by the sink infrastructure\n });\n };\n sink[Symbol.asyncDispose] = async () => {\n await lastPromise;\n };\n return sink;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"sink.js","names":["sink: Sink","filter: FilterLike","record: LogRecord","stream: WritableStream","options: StreamSinkOptions","sink: Sink & AsyncDisposable","buffer: LogRecord[]","flushTimer: ReturnType<typeof setInterval> | null","activeFlush: Promise<void> | null","nonBlockingSink: Sink & AsyncDisposable","options: ConsoleSinkOptions","levelMap: Record<LogLevel, ConsoleMethod>","nonBlockingSink: Sink & Disposable","asyncSink: AsyncSink","options: FingersCrossedOptions","parent: readonly string[]","child: readonly string[]","shouldFlushBuffer:\n | ((\n triggerCategory: readonly string[],\n bufferedCategory: readonly string[],\n ) => boolean)\n | null","category: readonly string[]","key: string","allRecordsToFlush: LogRecord[]"],"sources":["../src/sink.ts"],"sourcesContent":["import { type FilterLike, toFilter } from \"./filter.ts\";\nimport {\n type ConsoleFormatter,\n defaultConsoleFormatter,\n defaultTextFormatter,\n type TextFormatter,\n} from \"./formatter.ts\";\nimport { compareLogLevel, type LogLevel } from \"./level.ts\";\nimport type { LogRecord } from \"./record.ts\";\n\n/**\n * A sink is a function that accepts a log record and prints it somewhere.\n * Thrown exceptions will be suppressed and then logged to the meta logger,\n * a {@link Logger} with the category `[\"logtape\", \"meta\"]`. (In that case,\n * the meta log record will not be passed to the sink to avoid infinite\n * recursion.)\n *\n * @param record The log record to sink.\n */\nexport type Sink = (record: LogRecord) => void;\n\n/**\n * An async sink is a function that accepts a log record and asynchronously\n * processes it. This type is used with {@link fromAsyncSink} to create\n * a regular sink that properly handles asynchronous operations.\n *\n * @param record The log record to process asynchronously.\n * @returns A promise that resolves when the record has been processed.\n * @since 1.0.0\n */\nexport type AsyncSink = (record: LogRecord) => Promise<void>;\n\n/**\n * Turns a sink into a filtered sink. The returned sink only logs records that\n * pass the filter.\n *\n * @example Filter a console sink to only log records with the info level\n * ```typescript\n * const sink = withFilter(getConsoleSink(), \"info\");\n * ```\n *\n * @param sink A sink to be filtered.\n * @param filter A filter to apply to the sink. It can be either a filter\n * function or a {@link LogLevel} string.\n * @returns A sink that only logs records that pass the filter.\n */\nexport function withFilter(sink: Sink, filter: FilterLike): Sink {\n const filterFunc = toFilter(filter);\n return (record: LogRecord) => {\n if (filterFunc(record)) sink(record);\n };\n}\n\n/**\n * Options for the {@link getStreamSink} function.\n */\nexport interface StreamSinkOptions {\n /**\n * The text formatter to use. Defaults to {@link defaultTextFormatter}.\n */\n formatter?: TextFormatter;\n\n /**\n * The text encoder to use. Defaults to an instance of {@link TextEncoder}.\n */\n encoder?: { encode(text: string): Uint8Array };\n\n /**\n * Enable non-blocking mode with optional buffer configuration.\n * When enabled, log records are buffered and flushed in the background.\n *\n * @example Simple non-blocking mode\n * ```typescript\n * getStreamSink(stream, { nonBlocking: true });\n * ```\n *\n * @example Custom buffer configuration\n * ```typescript\n * getStreamSink(stream, {\n * nonBlocking: {\n * bufferSize: 1000,\n * flushInterval: 50\n * }\n * });\n * ```\n *\n * @default `false`\n * @since 1.0.0\n */\n nonBlocking?: boolean | {\n /**\n * Maximum number of records to buffer before flushing.\n * @default `100`\n */\n bufferSize?: number;\n\n /**\n * Interval in milliseconds between automatic flushes.\n * @default `100`\n */\n flushInterval?: number;\n };\n}\n\n/**\n * A factory that returns a sink that writes to a {@link WritableStream}.\n *\n * Note that the `stream` is of Web Streams API, which is different from\n * Node.js streams. You can convert a Node.js stream to a Web Streams API\n * stream using [`stream.Writable.toWeb()`] method.\n *\n * [`stream.Writable.toWeb()`]: https://nodejs.org/api/stream.html#streamwritabletowebstreamwritable\n *\n * @example Sink to the standard error in Deno\n * ```typescript\n * const stderrSink = getStreamSink(Deno.stderr.writable);\n * ```\n *\n * @example Sink to the standard error in Node.js\n * ```typescript\n * import stream from \"node:stream\";\n * const stderrSink = getStreamSink(stream.Writable.toWeb(process.stderr));\n * ```\n *\n * @param stream The stream to write to.\n * @param options The options for the sink.\n * @returns A sink that writes to the stream.\n */\nexport function getStreamSink(\n stream: WritableStream,\n options: StreamSinkOptions = {},\n): Sink & AsyncDisposable {\n const formatter = options.formatter ?? defaultTextFormatter;\n const encoder = options.encoder ?? new TextEncoder();\n const writer = stream.getWriter();\n\n if (!options.nonBlocking) {\n let lastPromise = Promise.resolve();\n const sink: Sink & AsyncDisposable = (record: LogRecord) => {\n const bytes = encoder.encode(formatter(record));\n lastPromise = lastPromise\n .then(() => writer.ready)\n .then(() => writer.write(bytes));\n };\n sink[Symbol.asyncDispose] = async () => {\n await lastPromise;\n await writer.close();\n };\n return sink;\n }\n\n // Non-blocking mode implementation\n const nonBlockingConfig = options.nonBlocking === true\n ? {}\n : options.nonBlocking;\n const bufferSize = nonBlockingConfig.bufferSize ?? 100;\n const flushInterval = nonBlockingConfig.flushInterval ?? 100;\n\n const buffer: LogRecord[] = [];\n let flushTimer: ReturnType<typeof setInterval> | null = null;\n let disposed = false;\n let activeFlush: Promise<void> | null = null;\n const maxBufferSize = bufferSize * 2; // Overflow protection\n\n async function flush(): Promise<void> {\n if (buffer.length === 0) return;\n\n const records = buffer.splice(0);\n for (const record of records) {\n try {\n const bytes = encoder.encode(formatter(record));\n await writer.ready;\n await writer.write(bytes);\n } catch {\n // Silently ignore errors in non-blocking mode to avoid disrupting the application\n }\n }\n }\n\n function scheduleFlush(): void {\n if (activeFlush) return;\n\n activeFlush = flush().finally(() => {\n activeFlush = null;\n });\n }\n\n function startFlushTimer(): void {\n if (flushTimer !== null || disposed) return;\n\n flushTimer = setInterval(() => {\n scheduleFlush();\n }, flushInterval);\n }\n\n const nonBlockingSink: Sink & AsyncDisposable = (record: LogRecord) => {\n if (disposed) return;\n\n // Buffer overflow protection: drop oldest records if buffer is too large\n if (buffer.length >= maxBufferSize) {\n buffer.shift(); // Remove oldest record\n }\n\n buffer.push(record);\n\n if (buffer.length >= bufferSize) {\n scheduleFlush();\n } else if (flushTimer === null) {\n startFlushTimer();\n }\n };\n\n nonBlockingSink[Symbol.asyncDispose] = async () => {\n disposed = true;\n if (flushTimer !== null) {\n clearInterval(flushTimer);\n flushTimer = null;\n }\n await flush();\n try {\n await writer.close();\n } catch {\n // Writer might already be closed or errored\n }\n };\n\n return nonBlockingSink;\n}\n\ntype ConsoleMethod = \"debug\" | \"info\" | \"log\" | \"warn\" | \"error\";\n\n/**\n * Options for the {@link getConsoleSink} function.\n */\nexport interface ConsoleSinkOptions {\n /**\n * The console formatter or text formatter to use.\n * Defaults to {@link defaultConsoleFormatter}.\n */\n formatter?: ConsoleFormatter | TextFormatter;\n\n /**\n * The mapping from log levels to console methods. Defaults to:\n *\n * ```typescript\n * {\n * trace: \"trace\",\n * debug: \"debug\",\n * info: \"info\",\n * warning: \"warn\",\n * error: \"error\",\n * fatal: \"error\",\n * }\n * ```\n * @since 0.9.0\n */\n levelMap?: Record<LogLevel, ConsoleMethod>;\n\n /**\n * The console to log to. Defaults to {@link console}.\n */\n console?: Console;\n\n /**\n * Enable non-blocking mode with optional buffer configuration.\n * When enabled, log records are buffered and flushed in the background.\n *\n * @example Simple non-blocking mode\n * ```typescript\n * getConsoleSink({ nonBlocking: true });\n * ```\n *\n * @example Custom buffer configuration\n * ```typescript\n * getConsoleSink({\n * nonBlocking: {\n * bufferSize: 1000,\n * flushInterval: 50\n * }\n * });\n * ```\n *\n * @default `false`\n * @since 1.0.0\n */\n nonBlocking?: boolean | {\n /**\n * Maximum number of records to buffer before flushing.\n * @default `100`\n */\n bufferSize?: number;\n\n /**\n * Interval in milliseconds between automatic flushes.\n * @default `100`\n */\n flushInterval?: number;\n };\n}\n\n/**\n * A console sink factory that returns a sink that logs to the console.\n *\n * @param options The options for the sink.\n * @returns A sink that logs to the console. If `nonBlocking` is enabled,\n * returns a sink that also implements {@link Disposable}.\n */\nexport function getConsoleSink(\n options: ConsoleSinkOptions = {},\n): Sink | (Sink & Disposable) {\n const formatter = options.formatter ?? defaultConsoleFormatter;\n const levelMap: Record<LogLevel, ConsoleMethod> = {\n trace: \"debug\",\n debug: \"debug\",\n info: \"info\",\n warning: \"warn\",\n error: \"error\",\n fatal: \"error\",\n ...(options.levelMap ?? {}),\n };\n const console = options.console ?? globalThis.console;\n\n const baseSink = (record: LogRecord) => {\n const args = formatter(record);\n const method = levelMap[record.level];\n if (method === undefined) {\n throw new TypeError(`Invalid log level: ${record.level}.`);\n }\n if (typeof args === \"string\") {\n const msg = args.replace(/\\r?\\n$/, \"\");\n console[method](msg);\n } else {\n console[method](...args);\n }\n };\n\n if (!options.nonBlocking) {\n return baseSink;\n }\n\n // Non-blocking mode implementation\n const nonBlockingConfig = options.nonBlocking === true\n ? {}\n : options.nonBlocking;\n const bufferSize = nonBlockingConfig.bufferSize ?? 100;\n const flushInterval = nonBlockingConfig.flushInterval ?? 100;\n\n const buffer: LogRecord[] = [];\n let flushTimer: ReturnType<typeof setInterval> | null = null;\n let disposed = false;\n let flushScheduled = false;\n const maxBufferSize = bufferSize * 2; // Overflow protection\n\n function flush(): void {\n if (buffer.length === 0) return;\n\n const records = buffer.splice(0);\n for (const record of records) {\n try {\n baseSink(record);\n } catch {\n // Silently ignore errors in non-blocking mode to avoid disrupting the application\n }\n }\n }\n\n function scheduleFlush(): void {\n if (flushScheduled) return;\n\n flushScheduled = true;\n setTimeout(() => {\n flushScheduled = false;\n flush();\n }, 0);\n }\n\n function startFlushTimer(): void {\n if (flushTimer !== null || disposed) return;\n\n flushTimer = setInterval(() => {\n flush();\n }, flushInterval);\n }\n\n const nonBlockingSink: Sink & Disposable = (record: LogRecord) => {\n if (disposed) return;\n\n // Buffer overflow protection: drop oldest records if buffer is too large\n if (buffer.length >= maxBufferSize) {\n buffer.shift(); // Remove oldest record\n }\n\n buffer.push(record);\n\n if (buffer.length >= bufferSize) {\n scheduleFlush();\n } else if (flushTimer === null) {\n startFlushTimer();\n }\n };\n\n nonBlockingSink[Symbol.dispose] = () => {\n disposed = true;\n if (flushTimer !== null) {\n clearInterval(flushTimer);\n flushTimer = null;\n }\n flush();\n };\n\n return nonBlockingSink;\n}\n\n/**\n * Converts an async sink into a regular sink with proper async handling.\n * The returned sink chains async operations to ensure proper ordering and\n * implements AsyncDisposable to wait for all pending operations on disposal.\n *\n * @example Create a sink that asynchronously posts to a webhook\n * ```typescript\n * const asyncSink: AsyncSink = async (record) => {\n * await fetch(\"https://example.com/logs\", {\n * method: \"POST\",\n * body: JSON.stringify(record),\n * });\n * };\n * const sink = fromAsyncSink(asyncSink);\n * ```\n *\n * @param asyncSink The async sink function to convert.\n * @returns A sink that properly handles async operations and disposal.\n * @since 1.0.0\n */\nexport function fromAsyncSink(asyncSink: AsyncSink): Sink & AsyncDisposable {\n let lastPromise = Promise.resolve();\n const sink: Sink & AsyncDisposable = (record: LogRecord) => {\n lastPromise = lastPromise\n .then(() => asyncSink(record))\n .catch(() => {\n // Errors are handled by the sink infrastructure\n });\n };\n sink[Symbol.asyncDispose] = async () => {\n await lastPromise;\n };\n return sink;\n}\n\n/**\n * Options for the {@link fingersCrossed} function.\n * @since 1.1.0\n */\nexport interface FingersCrossedOptions {\n /**\n * Minimum log level that triggers buffer flush.\n * When a log record at or above this level is received, all buffered\n * records are flushed to the wrapped sink.\n * @default `\"error\"`\n */\n readonly triggerLevel?: LogLevel;\n\n /**\n * Maximum buffer size before oldest records are dropped.\n * When the buffer exceeds this size, the oldest records are removed\n * to prevent unbounded memory growth.\n * @default `1000`\n */\n readonly maxBufferSize?: number;\n\n /**\n * Category isolation mode or custom matcher function.\n *\n * When `undefined` (default), all log records share a single buffer.\n *\n * When set to a mode string:\n *\n * - `\"descendant\"`: Flush child category buffers when parent triggers\n * - `\"ancestor\"`: Flush parent category buffers when child triggers\n * - `\"both\"`: Flush both parent and child category buffers\n *\n * When set to a function, it receives the trigger category and buffered\n * category and should return true if the buffered category should be flushed.\n *\n * @default `undefined` (no isolation, single global buffer)\n */\n readonly isolateByCategory?:\n | \"descendant\"\n | \"ancestor\"\n | \"both\"\n | ((\n triggerCategory: readonly string[],\n bufferedCategory: readonly string[],\n ) => boolean);\n}\n\n/**\n * Creates a sink that buffers log records until a trigger level is reached.\n * This pattern, known as \"fingers crossed\" logging, keeps detailed debug logs\n * in memory and only outputs them when an error or other significant event occurs.\n *\n * @example Basic usage with default settings\n * ```typescript\n * const sink = fingersCrossed(getConsoleSink());\n * // Debug and info logs are buffered\n * // When an error occurs, all buffered logs + the error are output\n * ```\n *\n * @example Custom trigger level and buffer size\n * ```typescript\n * const sink = fingersCrossed(getConsoleSink(), {\n * triggerLevel: \"warning\", // Trigger on warning or higher\n * maxBufferSize: 500 // Keep last 500 records\n * });\n * ```\n *\n * @example Category isolation\n * ```typescript\n * const sink = fingersCrossed(getConsoleSink(), {\n * isolateByCategory: \"descendant\" // Separate buffers per category\n * });\n * // Error in [\"app\"] triggers flush of [\"app\"] and [\"app\", \"module\"] buffers\n * // But not [\"other\"] buffer\n * ```\n *\n * @param sink The sink to wrap. Buffered records are sent to this sink when\n * triggered.\n * @param options Configuration options for the fingers crossed behavior.\n * @returns A sink that buffers records until the trigger level is reached.\n * @since 1.1.0\n */\nexport function fingersCrossed(\n sink: Sink,\n options: FingersCrossedOptions = {},\n): Sink {\n const triggerLevel = options.triggerLevel ?? \"error\";\n const maxBufferSize = Math.max(0, options.maxBufferSize ?? 1000);\n const isolateByCategory = options.isolateByCategory;\n\n // Validate trigger level early\n try {\n compareLogLevel(\"trace\", triggerLevel); // Test with any valid level\n } catch (error) {\n throw new TypeError(\n `Invalid triggerLevel: ${JSON.stringify(triggerLevel)}. ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n }\n\n // Helper functions for category matching\n function isDescendant(\n parent: readonly string[],\n child: readonly string[],\n ): boolean {\n if (parent.length === 0 || child.length === 0) return false; // Empty categories are isolated\n if (parent.length > child.length) return false;\n return parent.every((p, i) => p === child[i]);\n }\n\n function isAncestor(\n child: readonly string[],\n parent: readonly string[],\n ): boolean {\n if (child.length === 0 || parent.length === 0) return false; // Empty categories are isolated\n if (child.length < parent.length) return false;\n return parent.every((p, i) => p === child[i]);\n }\n\n // Determine matcher function based on isolation mode\n let shouldFlushBuffer:\n | ((\n triggerCategory: readonly string[],\n bufferedCategory: readonly string[],\n ) => boolean)\n | null = null;\n\n if (isolateByCategory) {\n if (typeof isolateByCategory === \"function\") {\n shouldFlushBuffer = isolateByCategory;\n } else {\n switch (isolateByCategory) {\n case \"descendant\":\n shouldFlushBuffer = (trigger, buffered) =>\n isDescendant(trigger, buffered);\n break;\n case \"ancestor\":\n shouldFlushBuffer = (trigger, buffered) =>\n isAncestor(trigger, buffered);\n break;\n case \"both\":\n shouldFlushBuffer = (trigger, buffered) =>\n isDescendant(trigger, buffered) || isAncestor(trigger, buffered);\n break;\n }\n }\n }\n\n // Helper functions for category serialization\n function getCategoryKey(category: readonly string[]): string {\n return JSON.stringify(category);\n }\n\n function parseCategoryKey(key: string): string[] {\n return JSON.parse(key);\n }\n\n // Buffer management\n if (!isolateByCategory) {\n // Single global buffer\n const buffer: LogRecord[] = [];\n let triggered = false;\n\n return (record: LogRecord) => {\n if (triggered) {\n // Already triggered, pass through directly\n sink(record);\n return;\n }\n\n // Check if this record triggers flush\n if (compareLogLevel(record.level, triggerLevel) >= 0) {\n triggered = true;\n\n // Flush buffer\n for (const bufferedRecord of buffer) {\n sink(bufferedRecord);\n }\n buffer.length = 0;\n\n // Send trigger record\n sink(record);\n } else {\n // Buffer the record\n buffer.push(record);\n\n // Enforce max buffer size\n while (buffer.length > maxBufferSize) {\n buffer.shift();\n }\n }\n };\n } else {\n // Category-isolated buffers\n const buffers = new Map<string, LogRecord[]>();\n const triggered = new Set<string>();\n\n return (record: LogRecord) => {\n const categoryKey = getCategoryKey(record.category);\n\n // Check if this category is already triggered\n if (triggered.has(categoryKey)) {\n sink(record);\n return;\n }\n\n // Check if this record triggers flush\n if (compareLogLevel(record.level, triggerLevel) >= 0) {\n // Find all buffers that should be flushed\n const keysToFlush = new Set<string>();\n\n for (const [bufferedKey] of buffers) {\n if (bufferedKey === categoryKey) {\n keysToFlush.add(bufferedKey);\n } else if (shouldFlushBuffer) {\n const bufferedCategory = parseCategoryKey(bufferedKey);\n try {\n if (shouldFlushBuffer(record.category, bufferedCategory)) {\n keysToFlush.add(bufferedKey);\n }\n } catch {\n // Ignore errors from custom matcher\n }\n }\n }\n\n // Flush matching buffers\n const allRecordsToFlush: LogRecord[] = [];\n for (const key of keysToFlush) {\n const buffer = buffers.get(key);\n if (buffer) {\n allRecordsToFlush.push(...buffer);\n buffers.delete(key);\n triggered.add(key);\n }\n }\n\n // Sort by timestamp to maintain chronological order\n allRecordsToFlush.sort((a, b) => a.timestamp - b.timestamp);\n\n // Flush all records\n for (const bufferedRecord of allRecordsToFlush) {\n sink(bufferedRecord);\n }\n\n // Mark trigger category as triggered and send trigger record\n triggered.add(categoryKey);\n sink(record);\n } else {\n // Buffer the record\n let buffer = buffers.get(categoryKey);\n if (!buffer) {\n buffer = [];\n buffers.set(categoryKey, buffer);\n }\n\n buffer.push(record);\n\n // Enforce max buffer size per category\n while (buffer.length > maxBufferSize) {\n buffer.shift();\n }\n }\n };\n }\n}\n"],"mappings":"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"}