@monochromatic-dev/module-logger 0.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.
Files changed (57) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSES/GPL-3.0-or-later.txt +674 -0
  3. package/LICENSES/LGPL-3.0-or-later.txt +165 -0
  4. package/README.md +404 -0
  5. package/dist/final/neutral/index.d.mts +673 -0
  6. package/dist/final/neutral/index.mjs +3 -0
  7. package/dist/final/neutral/rolldown-runtime-5duEfhBv.mjs +1 -0
  8. package/dist/final/node/index.d.mts +673 -0
  9. package/dist/final/node/index.mjs +3 -0
  10. package/dist/final/node/rolldown-runtime-5duEfhBv.mjs +1 -0
  11. package/package.json +43 -0
  12. package/src/create-logger.ts +494 -0
  13. package/src/create-logger.unit.test.ts +752 -0
  14. package/src/error-format.ts +43 -0
  15. package/src/index.ts +35 -0
  16. package/src/logger.ts +67 -0
  17. package/src/logger.unit.test.ts +190 -0
  18. package/src/sink/console-control-chars.ts +140 -0
  19. package/src/sink/console-control-chars.unit.test.ts +206 -0
  20. package/src/sink/console.ts +531 -0
  21. package/src/sink/console.unit.test.ts +542 -0
  22. package/src/sink/file.ts +297 -0
  23. package/src/sink/file.unit.test.ts +202 -0
  24. package/src/sink/index.ts +11 -0
  25. package/src/sink/indexed-db-util.ts +96 -0
  26. package/src/sink/indexed-db.browser.test.ts +184 -0
  27. package/src/sink/indexed-db.ts +324 -0
  28. package/src/sink/indexed-db.unit.test.ts +80 -0
  29. package/src/sink/local-storage-key.ts +176 -0
  30. package/src/sink/local-storage-key.unit.test.ts +106 -0
  31. package/src/sink/local-storage-quota.ts +60 -0
  32. package/src/sink/local-storage-quota.unit.test.ts +98 -0
  33. package/src/sink/local-storage-store.ts +368 -0
  34. package/src/sink/local-storage-store.unit.test.ts +329 -0
  35. package/src/sink/local-storage.browser.test.ts +125 -0
  36. package/src/sink/local-storage.ts +182 -0
  37. package/src/sink/local-storage.unit.test.ts +218 -0
  38. package/src/sink/noop.ts +46 -0
  39. package/src/sink/noop.unit.test.ts +47 -0
  40. package/src/sink/opfs.browser.test.ts +84 -0
  41. package/src/sink/opfs.ts +212 -0
  42. package/src/sink/opfs.unit.test.ts +81 -0
  43. package/src/sink/record-buffer.ts +230 -0
  44. package/src/sink/record-buffer.unit.test.ts +288 -0
  45. package/src/sink/session-storage-quota.ts +57 -0
  46. package/src/sink/session-storage-quota.unit.test.ts +98 -0
  47. package/src/sink/session-storage-store.ts +178 -0
  48. package/src/sink/session-storage.browser.test.ts +137 -0
  49. package/src/sink/session-storage.ts +128 -0
  50. package/src/sink/session-storage.unit.test.ts +527 -0
  51. package/src/sink/web-storage-quota-error.ts +43 -0
  52. package/src/sink/web-storage-quota-error.unit.test.ts +55 -0
  53. package/src/sink/web-storage-runtime.ts +49 -0
  54. package/src/startup.unit.test.ts +232 -0
  55. package/src/tagged.ts +74 -0
  56. package/src/tagged.unit.test.ts +211 -0
  57. package/src/types.ts +78 -0
@@ -0,0 +1,673 @@
1
+ import { stat as stat$1 } from "node:fs/promises";
2
+ import { dirname as dirname$1, join as join$1 } from "node:path";
3
+ //#region src/types.d.ts
4
+ /**
5
+ * Log severity levels ordered from least to most severe.
6
+ */
7
+ type Level = "debug" | "error" | "fatal" | "info" | "trace" | "warn";
8
+ /**
9
+ * Structured log record written to sinks.
10
+ */
11
+ type LogRecord = {
12
+ readonly level: Level;
13
+ readonly message: string;
14
+ readonly timestamp: number;
15
+ };
16
+ /**
17
+ * Optional drain hook for sinks that buffer records internally.
18
+ * Called via logger-level {@link Logger.flush} to force buffered work
19
+ * through before a process exit, critical error boundary, or assertion.
20
+ *
21
+ * Always async: sinks whose drain is synchronous return an
22
+ * already-resolved promise so callers `await` uniformly. A `void` arm is
23
+ * not used; under the `no-optional-escape` rule `T | void` is a banned
24
+ * fake-optional encoding, and there is no real synchronous value to carry.
25
+ */
26
+ type SinkFlush = () => Promise<void>;
27
+ /**
28
+ * Verification function that checks if a sink backend is available.
29
+ * May run setup side effects (resolving a log path, opening a writable
30
+ * stream) and reports whether the backend is usable. A sink whose
31
+ * verification resolves `false` (or rejects) is dropped by the logger and
32
+ * receives no further records.
33
+ *
34
+ * Always async, matching `write` and `flush`: a synchronous check returns an
35
+ * already-resolved promise (`Promise.resolve(check)`) so the logger awaits
36
+ * verification uniformly with no sync/async branch.
37
+ */
38
+ type Verify = () => Promise<boolean>;
39
+ /**
40
+ * Sink that receives log records. A sink is a self-describing adapter: it
41
+ * carries everything the logger must know to use it, namely how to
42
+ * `verify` its backend is available, how to `write` a record, and
43
+ * optionally how to `flush` buffered work. Holding `verify` on the sink
44
+ * (rather than as a sibling export the logger pairs by hand) lets the
45
+ * logger treat a registry as a plain `Sink[]` and lets a test supply one
46
+ * self-contained fake.
47
+ *
48
+ * Sinks that buffer records (e.g. microtask-batched console) may
49
+ * expose a `flush` hook so callers can force emission on demand.
50
+ *
51
+ * `write` is always async: a synchronous sink does its work eagerly and
52
+ * returns an already-resolved promise, so the logger observes a uniform
53
+ * `Promise<void>`. A rejected write is handled per sink and does not
54
+ * disable the backend; only a failed `verify` drops a sink. A `void` arm
55
+ * is not used, for the reason stated on {@link SinkFlush}.
56
+ */
57
+ type Sink = {
58
+ readonly flush?: SinkFlush;
59
+ readonly verify: Verify;
60
+ readonly write: (record: LogRecord) => Promise<void>;
61
+ };
62
+ /**
63
+ * Logger interface with 6 log levels plus `flush` for startup and sink drains.
64
+ * `flush()` resolves once startup verification has completed, tracked sink
65
+ * writes have settled, and every available sink's own {@link SinkFlush} hook
66
+ * has settled. Safe to call even when no sink buffers.
67
+ */
68
+ type Logger = {
69
+ readonly debug: (message: string) => void;
70
+ readonly error: (message: string) => void;
71
+ readonly fatal: (message: string) => void;
72
+ readonly flush: () => Promise<void>;
73
+ readonly info: (message: string) => void;
74
+ readonly trace: (message: string) => void;
75
+ readonly warn: (message: string) => void;
76
+ };
77
+ //#endregion
78
+ //#region src/create-logger.d.ts
79
+ /**
80
+ * Default `flush()` deadline in milliseconds. Measured on 2026-09-06: a
81
+ * default logger flushing 100 records through the console and file sinks
82
+ * settles in about 2 ms locally, so this leaves three orders of magnitude for
83
+ * a slow but working backend while still bounding shutdown on a wedged one.
84
+ * Override per logger through the `flushDeadlineMs` option of
85
+ * {@link createLogger}.
86
+ */
87
+ export declare const DEFAULT_FLUSH_DEADLINE_MS = 5e3;
88
+ /**
89
+ * Builds a multi-sink logger over the supplied sink adapters. All
90
+ * orchestration (per-sink availability, startup buffering and replay,
91
+ * in-flight write tracking, and flush) lives here; the exported default
92
+ * `logger` is just this factory applied to the default sink set, and tests
93
+ * apply it to fake sinks to exercise the orchestration directly.
94
+ *
95
+ * Verification runs eagerly at construction and never blocks callers:
96
+ * records emitted while an async sink is still verifying buffer internally
97
+ * and replay to that sink the moment it verifies. A sink whose `verify`
98
+ * resolves `false` or throws is dropped and receives no records. A rejected
99
+ * `write` is the sink's own concern and does not disable the backend.
100
+ *
101
+ * `flush()` always resolves: one deadline (`flushDeadlineMs`, default
102
+ * {@link DEFAULT_FLUSH_DEADLINE_MS}) wraps startup verification, the
103
+ * in-flight write drain, and every sink flush hook together. When it elapses
104
+ * the logger reports one breadcrumb, abandons the tracked writes from its
105
+ * view (the sinks expose no cancellation, so the underlying work continues),
106
+ * and resolves, so a wedged backend cannot hang a shutdown.
107
+ *
108
+ * @param sinks - Sink adapters to fan each record out to, in priority order.
109
+ *
110
+ * @param flushDeadlineMs - Milliseconds one `flush()` may take before it
111
+ * resolves anyway; raise it for slow but working backends such as network
112
+ * filesystems.
113
+ *
114
+ * @returns Logger plus its eager `initPromise`; callers need not await
115
+ * `initPromise` before logging, since startup records replay on verify.
116
+ *
117
+ * @example
118
+ * ```ts
119
+ * const { logger } = createLogger({ sinks: [createConsoleSink()] });
120
+ * logger.info('ready');
121
+ * await logger.flush();
122
+ * ```
123
+ *
124
+ * @example
125
+ * ```ts
126
+ * const { logger } = createLogger({
127
+ * sinks: [createFileSink()],
128
+ * flushDeadlineMs: 30_000,
129
+ * });
130
+ * ```
131
+ */
132
+ export declare function createLogger({ sinks, flushDeadlineMs }: {
133
+ readonly sinks: readonly Sink[];
134
+ readonly flushDeadlineMs?: number;
135
+ }): {
136
+ readonly initPromise: Promise<void>;
137
+ readonly logger: Logger;
138
+ };
139
+ //#endregion
140
+ //#region src/logger.d.ts
141
+ /**
142
+ * Eager readiness promise. Consumers do not need to await this before logging;
143
+ * {@link Logger.flush} awaits it internally, and startup records replay to
144
+ * async sinks as they become available.
145
+ */
146
+ export declare const initPromise: Promise<void>;
147
+ /**
148
+ * Multi-sink logger that writes to all available backends.
149
+ * Startup records replay to async sinks that verify after the log call.
150
+ * Log calls throw only when initialization proves no backend is available,
151
+ * which the console sink prevents in every supported runtime.
152
+ *
153
+ * @example
154
+ * ```ts
155
+ * import { logger, } from '\@monochromatic-dev/module-logger/logger';
156
+ *
157
+ * logger.error('unexpected shutdown',);
158
+ * await logger.flush();
159
+ * ```
160
+ */
161
+ export declare const logger: Logger;
162
+ //#endregion
163
+ //#region src/sink/console.d.ts
164
+ /**
165
+ * Builds a microtask-batched console sink. The pending buffer, schedule flag,
166
+ * and memoized verbose detection live in this instance's closure (no
167
+ * module-global state), so independent loggers and tests stay isolated with
168
+ * no reset hook. Collapses contiguous same-level runs into single `console.*`
169
+ * calls, sharply reducing console-panel overhead when an instrumented path
170
+ * emits many records per sync frame.
171
+ *
172
+ * @returns Sink that writes formatted lines to `console.*`, except
173
+ * process-hosted debug records write to stderr.
174
+ *
175
+ * @example
176
+ * ```ts
177
+ * const { logger } = createLogger({ sinks: [createConsoleSink()] });
178
+ * logger.info('server started');
179
+ * ```
180
+ */
181
+ declare function createConsoleSink(): Sink;
182
+ //#endregion
183
+ //#region src/error-format.d.ts
184
+ /**
185
+ * Reports a logger-internal caught value without going back through logger
186
+ * sinks, formatting it via {@link caughtValueText}.
187
+ *
188
+ * @param context - Human-readable operation that caught the value.
189
+ *
190
+ * @param error - Caught value to include in the diagnostic.
191
+ *
192
+ * @mutates error - `caughtValueText` may invoke string-conversion hooks.
193
+ *
194
+ * @example
195
+ * ```ts
196
+ * reportLoggerInternalError({
197
+ * context: 'console sink verify failed',
198
+ * error: new Error('blocked'),
199
+ * });
200
+ * ```
201
+ */
202
+ declare function reportLoggerInternalError({ context, error }: {
203
+ readonly context: string;
204
+ readonly error: unknown;
205
+ }): void;
206
+ //#endregion
207
+ //#region src/sink/file.d.ts
208
+ /**
209
+ * Sentinel returned by {@link findNodeModulesUp} when no ancestor directory
210
+ * contains a `node_modules`. A unique symbol so it never collides with a real
211
+ * path string the walk might otherwise return, keeping the result free of a
212
+ * banned `string | undefined` union.
213
+ *
214
+ * @example
215
+ * ```ts
216
+ * const dir = await findNodeModulesUp({ cwd, stat, dirname, join });
217
+ * if (dir === NO_NODE_MODULES_FOUND) {
218
+ * // no ancestor project root
219
+ * }
220
+ * ```
221
+ */
222
+ declare const NO_NODE_MODULES_FOUND: unique symbol;
223
+ /**
224
+ * Walks up from `cwd` to find the nearest ancestor directory containing a
225
+ * `node_modules` subdirectory, returning that subdirectory's absolute path.
226
+ *
227
+ * Using find-up rather than cwd-relative placement keeps log directories
228
+ * anchored to the project the caller actually belongs to. Without this,
229
+ * scripts invoked from build output (e.g. `dist/`) or other stray cwds
230
+ * would create `node_modules/.monochromatic/` inside those trees, polluting
231
+ * shipped artifacts.
232
+ *
233
+ * Exported primarily so `index.unit.test.ts` can exercise both the hit
234
+ * and miss paths directly with an injected `stat`.
235
+ *
236
+ * @param cwd - starting directory for the upward search
237
+ *
238
+ * @param stat - `node:fs/promises` stat (injected so the dynamic
239
+ * import stays in one place)
240
+ *
241
+ * @param dirname - `node:path` dirname
242
+ *
243
+ * @param join - `node:path` join
244
+ *
245
+ * @param reportError - logger fault reporter injected for deterministic tests
246
+ *
247
+ * @returns absolute path to the nearest ancestor `node_modules`, or
248
+ * {@link NO_NODE_MODULES_FOUND} when no ancestor contains one
249
+ *
250
+ * @example
251
+ * ```ts
252
+ * const dir = await findNodeModulesUp({ cwd: process.cwd(), stat, dirname, join });
253
+ * ```
254
+ */
255
+ declare function findNodeModulesUp({ cwd, stat, dirname, join, reportError }: {
256
+ readonly cwd: string;
257
+ readonly stat: typeof stat$1;
258
+ readonly dirname: typeof dirname$1;
259
+ readonly join: typeof join$1;
260
+ readonly reportError?: typeof reportLoggerInternalError;
261
+ }): Promise<string | typeof NO_NODE_MODULES_FOUND>;
262
+ /**
263
+ * Builds a file sink that appends JSONL records to the nearest ancestor
264
+ * `node_modules/.monochromatic/{timestamp}.log.jsonl` (resolved once during
265
+ * verification). The resolved path, the cached `appendFile`, and the
266
+ * verification memo live in this instance's closure (no module-global state),
267
+ * so independent loggers and tests never share a log file or need a reset
268
+ * hook. No `flush` hook: each `write` awaits `appendFile` directly, so there
269
+ * is no buffered state to drain.
270
+ *
271
+ * @returns Sink backed by `node:fs/promises`.
272
+ *
273
+ * @example
274
+ * ```ts
275
+ * const { logger } = createLogger({ sinks: [createFileSink()] });
276
+ * logger.error('unhandled rejection');
277
+ * await logger.flush();
278
+ * ```
279
+ */
280
+ declare function createFileSink(): Sink;
281
+ //#endregion
282
+ //#region src/sink/indexed-db.d.ts
283
+ /**
284
+ * Builds an IndexedDB sink that buffers serialized records through the shared
285
+ * {@link createRecordBuffer} policy and persists each newline-joined JSONL
286
+ * batch as one string value per transaction, measured at 0.15 µs of
287
+ * main-thread enqueue per record on headless Chromium 149 (one `add` per
288
+ * 32 KiB batch). The connection lives in this instance's closure (no
289
+ * module-global state), so independent loggers and tests never share a
290
+ * handle or need a reset hook.
291
+ *
292
+ * Records are readable the moment their transaction settles (DevTools
293
+ * Application tab included), survive tab close and browser restart, and
294
+ * auto-incremented keys serialize across tabs, so no run-scoped naming is
295
+ * needed. Retention trims oldest-first past {@link MAX_STORED_BATCHES}.
296
+ * Transactions use the default relaxed durability: relaxed commits reach the
297
+ * browser's storage backend promptly and survive renderer crashes, and the
298
+ * OS-crash window `durability: 'strict'` would close is the rarest failure
299
+ * class, not worth an fsync per batch.
300
+ *
301
+ * Flush triggers (32 KiB in-write cap, `warn`-or-worse severity, 250 ms
302
+ * quiet-period deadline, page lifecycle, and the `flush` hook) are the
303
+ * buffer's; see {@link createRecordBuffer}. The sink's `flush` hook awaits
304
+ * every issued batch transaction before resolving.
305
+ *
306
+ * @returns Sink backed by IndexedDB.
307
+ *
308
+ * @example
309
+ * ```ts
310
+ * const { logger } = createLogger({ sinks: [createIndexedDbSink()] });
311
+ * logger.warn('quota nearing limit');
312
+ * ```
313
+ */
314
+ declare function createIndexedDbSink(): Sink;
315
+ //#endregion
316
+ //#region src/sink/local-storage.d.ts
317
+ /**
318
+ * Builds a localStorage sink that buffers serialized records through the
319
+ * shared {@link createRecordBuffer} policy and persists each newline-joined
320
+ * JSONL batch under a run-scoped counter-incremented key through
321
+ * {@link createLocalStorageStore}. One uniform write path runs on every
322
+ * runtime; no per-runtime mode exists. Flush triggers (32 KiB in-write cap,
323
+ * `warn`-or-worse severity, 250 ms quiet-period deadline, page lifecycle,
324
+ * and the `flush` hook) are the buffer's; see {@link createRecordBuffer}.
325
+ *
326
+ * Unlike the sessionStorage sink, whose store dies with the tab, this sink's
327
+ * batches survive tab close and browser restart, bounded by oldest-first
328
+ * eviction at half the localStorage quota; that makes it the web storage sink
329
+ * whose records remain inspectable after a full crash-and-restart.
330
+ *
331
+ * @returns Sink backed by web `localStorage`.
332
+ *
333
+ * @example
334
+ * ```ts
335
+ * const { logger } = createLogger({ sinks: [createLocalStorageSink()] });
336
+ * logger.info('user signed in'); // buffered
337
+ * logger.warn('quota near'); // flushes both records in one batch
338
+ * ```
339
+ */
340
+ declare function createLocalStorageSink(): Sink;
341
+ //#endregion
342
+ //#region src/sink/noop.d.ts
343
+ /**
344
+ * Builds a noop sink that discards every record and always verifies as
345
+ * available. Stateless, so the returned adapters share the same functions;
346
+ * the factory shape merely matches the other sinks. Useful as a stand-in
347
+ * that disables logging without removing log calls.
348
+ *
349
+ * @returns Sink that discards all records and exposes no `flush` (nothing
350
+ * is buffered).
351
+ *
352
+ * @example
353
+ * ```ts
354
+ * const { logger } = createLogger({ sinks: [createNoopSink()] });
355
+ * logger.info('goes nowhere');
356
+ * ```
357
+ */
358
+ declare function createNoopSink(): Sink;
359
+ //#endregion
360
+ //#region src/sink/opfs.d.ts
361
+ /**
362
+ * Builds an OPFS sink that buffers serialized records through the shared
363
+ * {@link createRecordBuffer} policy and appends each newline-joined JSONL
364
+ * batch to a per-session file in the Origin Private File System with one
365
+ * stream write per batch. The kept-open writable stream lives in this
366
+ * instance's closure (no module-global state), so independent loggers and
367
+ * tests never share a handle or need a reset hook.
368
+ *
369
+ * Flush triggers (32 KiB in-write cap, `warn`-or-worse severity, 250 ms
370
+ * quiet-period deadline, page lifecycle, and the `flush` hook) are the
371
+ * buffer's; see {@link createRecordBuffer}. Batch writes queue on the stream
372
+ * in issue order, so ordering holds at the batch boundary, and the sink's
373
+ * `flush` hook awaits every issued batch before resolving.
374
+ *
375
+ * @returns Sink backed by OPFS.
376
+ *
377
+ * @example
378
+ * ```ts
379
+ * const { logger } = createLogger({ sinks: [createOpfsSink()] });
380
+ * logger.warn('quota nearing limit');
381
+ * ```
382
+ */
383
+ declare function createOpfsSink(): Sink;
384
+ //#endregion
385
+ //#region src/sink/session-storage.d.ts
386
+ /**
387
+ * Builds a sessionStorage sink that buffers serialized records through the
388
+ * shared {@link createRecordBuffer} policy and persists each newline-joined
389
+ * JSONL batch under a counter-incremented key through
390
+ * {@link createSessionStorageStore}. One uniform write path runs on every
391
+ * runtime; no per-runtime mode exists. Flush triggers (32 KiB in-write cap,
392
+ * `warn`-or-worse severity, 250 ms quiet-period deadline, page lifecycle,
393
+ * and the `flush` hook) are the buffer's; see {@link createRecordBuffer}.
394
+ *
395
+ * @returns Sink backed by web `sessionStorage`.
396
+ *
397
+ * @example
398
+ * ```ts
399
+ * const { logger } = createLogger({ sinks: [createSessionStorageSink()] });
400
+ * logger.info('user signed in'); // buffered
401
+ * logger.warn('quota near'); // flushes both records in one batch
402
+ * ```
403
+ */
404
+ declare function createSessionStorageSink(): Sink;
405
+ declare namespace index_d_exports {
406
+ export { NO_NODE_MODULES_FOUND, createConsoleSink, createFileSink, createIndexedDbSink, createLocalStorageSink, createNoopSink, createOpfsSink, createSessionStorageSink, findNodeModulesUp };
407
+ }
408
+ //#endregion
409
+ //#region src/tagged.d.ts
410
+ /**
411
+ * Wraps a logger so every message is prefixed with `[tag] `.
412
+ * Callers typically pass `myFn.name` as tag to keep prefixes
413
+ * in sync with refactors.
414
+ *
415
+ * @param tag - Prefix string inserted before each message
416
+ *
417
+ * @param l - Base logger to wrap; defaults to the module-level {@link logger}
418
+ * singleton
419
+ *
420
+ * @returns Logger whose methods prepend `[tag] ` to every message
421
+ *
422
+ * @example
423
+ * ```ts
424
+ * import { tagged } from '\@monochromatic-dev/module-logger/tagged';
425
+ *
426
+ * function handleRequest({ l }: { l: Logger }): void {
427
+ * l.info('received');
428
+ * }
429
+ *
430
+ * handleRequest({ l: tagged({ tag: handleRequest.name }) });
431
+ * // logs: [handleRequest] received
432
+ * ```
433
+ *
434
+ * @example
435
+ * ```ts
436
+ * // Composing tags: the outermost wrap (`l2` here) prepends to the message
437
+ * // last, so its tag ends up rightmost. The innermost wrap (`l1`) hits the
438
+ * // underlying logger first, so its tag is leftmost. The chain reads
439
+ * // root-first: outer wrap = inner tag position.
440
+ * const l1 = tagged({ tag: 'http' });
441
+ * const l2 = tagged({ tag: 'retry', l: l1 });
442
+ * l2.info('attempt 3');
443
+ * // logs: [http] [retry] attempt 3
444
+ * ```
445
+ */
446
+ export declare function tagged({ tag, l }: {
447
+ readonly l?: Logger;
448
+ readonly tag: string;
449
+ }): Logger;
450
+ //#endregion
451
+ //#region src/sink/console-control-chars.d.ts
452
+ /**
453
+ * Neutralizes terminal control characters in console-bound text. One linear
454
+ * pass over the code points: each neutralized control becomes a `\uXXXX`
455
+ * escape, everything else is copied through, and newline and tab pass
456
+ * untouched. Well-formed and malformed escape sequences get no
457
+ * special treatment because the introducer byte itself is neutralized, so a
458
+ * trailing lone ESC, an unterminated OSC, and a nested ESC all lose their
459
+ * teeth the same way.
460
+ *
461
+ * @param text - Message text destined for `console.*` or `process.stderr`.
462
+ *
463
+ * @returns Text with every neutralized control rendered as `\uXXXX`.
464
+ *
465
+ * @example
466
+ * ```ts
467
+ * neutralizeControlCharacters('title:\u001B]0;x\u0007 ok\n\tnext');
468
+ * // => 'title:\\u001B]0;x\\u0007 ok\n\tnext'
469
+ * ```
470
+ */
471
+ declare function neutralizeControlCharacters(text: string): string;
472
+ //#endregion
473
+ //#region src/sink/local-storage-key.d.ts
474
+ /**
475
+ * Parsed identity of one owned localStorage entry, used to order eviction
476
+ * across runs.
477
+ */
478
+ type ParsedLogKey = {
479
+ readonly key: string;
480
+ readonly stamp: number;
481
+ readonly nonce: string;
482
+ readonly index: number;
483
+ };
484
+ /**
485
+ * Builds the namespaced localStorage key for one batch slot of one run.
486
+ *
487
+ * @param stamp - Run creation time ordering runs oldest-first.
488
+ *
489
+ * @param nonce - Same-millisecond disambiguator between concurrent tabs.
490
+ *
491
+ * @param index - Zero-based batch slot within the run.
492
+ *
493
+ * @returns Key such as `monochromatic.log.1753000000000.a1b2.3`.
494
+ *
495
+ * @example
496
+ * ```ts
497
+ * buildLogKey({ stamp: 1753000000000, nonce: 'a1b2', index: 3 });
498
+ * ```
499
+ */
500
+ declare function buildLogKey({ stamp, nonce, index }: {
501
+ readonly stamp: number;
502
+ readonly nonce: string;
503
+ readonly index: number;
504
+ }): string;
505
+ /**
506
+ * Parses a localStorage key back into its run identity, or reports it foreign
507
+ * by leaving `parsed` absent. Parsing is strict (exact prefix, exactly the
508
+ * identity segment count, digit-shaped stamp and index, non-empty nonce)
509
+ * because eviction trusts this to never classify a host application's key, or
510
+ * the sessionStorage sink's flat `monochromatic.log.{n}` shape, as evictable.
511
+ *
512
+ * @param key - Candidate localStorage key.
513
+ *
514
+ * @returns Wrapper whose `parsed` property is present only for an owned key.
515
+ *
516
+ * @example
517
+ * ```ts
518
+ * parseLogKey('monochromatic.log.1753000000000.a1b2.3').parsed; // ParsedLogKey
519
+ * parseLogKey('monochromatic.log.5').parsed; // undefined: sessionStorage shape
520
+ * ```
521
+ */
522
+ declare function parseLogKey(key: string): {
523
+ readonly parsed?: ParsedLogKey;
524
+ };
525
+ /**
526
+ * Orders parsed keys oldest-first for eviction: by run stamp, then by nonce
527
+ * (an arbitrary but stable tiebreak between same-millisecond runs), then by
528
+ * batch index within the run.
529
+ *
530
+ * @param first - Parsed key compared first.
531
+ *
532
+ * @param second - Parsed key compared second.
533
+ *
534
+ * @returns Negative when `first` is older, positive when newer, zero on ties.
535
+ *
536
+ * @example
537
+ * ```ts
538
+ * entries.toSorted(function byOldestFirst(first, second) {
539
+ * return compareLogKeys({ first, second });
540
+ * });
541
+ * ```
542
+ */
543
+ declare function compareLogKeys({ first, second }: {
544
+ readonly first: ParsedLogKey;
545
+ readonly second: ParsedLogKey;
546
+ }): number;
547
+ //#endregion
548
+ //#region src/sink/local-storage-quota.d.ts
549
+ /**
550
+ * Detects the current runtime's default localStorage quota in UTF-16 code
551
+ * units, or `Number.POSITIVE_INFINITY` when the runtime is unrecognized so the
552
+ * caller leaves its footprint uncapped and relies on reactive eviction alone.
553
+ *
554
+ * @returns Total quota in code units, or `Number.POSITIVE_INFINITY` if unknown.
555
+ *
556
+ * @example
557
+ * ```ts
558
+ * const capChars = detectLocalStorageQuotaChars() / 2; // half the total
559
+ * ```
560
+ */
561
+ declare function detectLocalStorageQuotaChars(): number;
562
+ //#endregion
563
+ //#region src/sink/local-storage-store.d.ts
564
+ /**
565
+ * Builds the persistence engine behind the localStorage sink: each `persist`
566
+ * lands one already-serialized batch under a run-scoped counter-incremented
567
+ * key, with proactive and reactive quota eviction. Run identity and counters
568
+ * live in this instance's closure (no module-global state), so independent
569
+ * sinks and tests never share keys or need a reset hook.
570
+ *
571
+ * Unlike sessionStorage, localStorage is shared by every tab of the origin and
572
+ * survives restarts, so this engine differs from the sessionStorage engine in
573
+ * two ways. Keys carry a run identity (see `local-storage-key.ts`), so
574
+ * concurrent tabs never collide on a counter. And on its first persist the
575
+ * engine adopts every strictly-parsed entry left by other runs into its
576
+ * footprint tally, evicting those oldest-first before its own entries;
577
+ * without that, leftovers from dead sessions would fill the store until no
578
+ * run could ever write again. Adoption is deferred to first persist rather
579
+ * than construction so building the default sink set never touches
580
+ * `globalThis.localStorage` on runtimes where the sink never verifies (plain
581
+ * Node warns on mere access). Keys that fail the strict parse, including the
582
+ * host application's, are never counted and never evicted.
583
+ *
584
+ * The engine caps its own footprint (adopted entries included) at half the
585
+ * runtime's localStorage quota, proactively dropping oldest-first, and
586
+ * reactively drops again if the real store still overflows; see
587
+ * {@link createLocalStorageStore.persist}.
588
+ *
589
+ * @returns Engine exposing `persist` for one batch value per call.
590
+ *
591
+ * @example
592
+ * ```ts
593
+ * const store = createLocalStorageStore();
594
+ * store.persist('{"level":"info","message":"hi","timestamp":0}');
595
+ * ```
596
+ */
597
+ declare function createLocalStorageStore(): {
598
+ readonly persist: (batch: string) => void;
599
+ };
600
+ //#endregion
601
+ //#region src/sink/record-buffer.d.ts
602
+ /**
603
+ * Builds the buffering stage shared by batch-persisting sinks: serialized
604
+ * records accumulate and leave as one newline-joined JSONL batch through
605
+ * `onFlush`. One uniform policy runs on every runtime; no per-runtime mode
606
+ * exists.
607
+ *
608
+ * A batch flushes synchronously from inside `add` when it reaches
609
+ * {@link FLUSH_BUFFER_CAP_CHARS} or when the record's severity is `warn` or
610
+ * worse, by timer after {@link FLUSH_DEADLINE_MS} of quiet, on `pagehide`
611
+ * and on the document becoming hidden (where those events exist), and on
612
+ * `drain`. The byte-cap and severity flushes run on the caller's stack, so
613
+ * neither a synchronous workload nor a wedged main thread can accumulate
614
+ * more than one cap of unhanded records. When an addition would breach the
615
+ * cap, the existing entries flush first so an oversized record's downstream
616
+ * failure can only ever drop that record, never its batch-mates.
617
+ *
618
+ * @param onFlush - Backend handoff receiving each newline-joined batch;
619
+ * called synchronously from whichever trigger fires, in record order.
620
+ *
621
+ * @returns Buffer exposing `add` for records and `drain` for forced flushes.
622
+ *
623
+ * @example
624
+ * ```ts
625
+ * const buffer = createRecordBuffer({ onFlush: (batch) => store.persist(batch) });
626
+ * buffer.add({ level: 'info', serialized: JSON.stringify(record) });
627
+ * buffer.drain();
628
+ * ```
629
+ */
630
+ declare function createRecordBuffer({ onFlush }: {
631
+ readonly onFlush: (batch: string) => void;
632
+ }): {
633
+ readonly add: (entry: {
634
+ readonly level: Level;
635
+ readonly serialized: string;
636
+ }) => void;
637
+ readonly drain: () => void;
638
+ };
639
+ //#endregion
640
+ //#region src/sink/session-storage-quota.d.ts
641
+ /**
642
+ * Detects the current runtime's default sessionStorage quota in UTF-16 code
643
+ * units, or `Number.POSITIVE_INFINITY` when the runtime is unrecognized so the
644
+ * caller leaves its footprint uncapped and relies on reactive eviction alone.
645
+ *
646
+ * @returns Total quota in code units, or `Number.POSITIVE_INFINITY` if unknown.
647
+ *
648
+ * @example
649
+ * ```ts
650
+ * const capChars = detectSessionStorageQuotaChars() / 2; // half the total
651
+ * ```
652
+ */
653
+ declare function detectSessionStorageQuotaChars(): number;
654
+ //#endregion
655
+ //#region src/sink/web-storage-quota-error.d.ts
656
+ /**
657
+ * Reports whether a caught `setItem` value is a storage quota overflow, so
658
+ * eviction reclaims space only for a full store and never for an unrelated
659
+ * write fault such as a disabled-storage `SecurityError`.
660
+ *
661
+ * @param error - Caught value from a `setItem` failure.
662
+ *
663
+ * @returns Whether `error` names a quota overflow.
664
+ *
665
+ * @example
666
+ * ```ts
667
+ * try { sessionStorage.setItem(k, v); }
668
+ * catch (error: unknown) { if (isQuotaExceededError(error)) evictOldest(); }
669
+ * ```
670
+ */
671
+ declare function isQuotaExceededError(error: unknown): boolean;
672
+ //#endregion
673
+ export { type Level, type LogRecord, type Logger, type Sink, type SinkFlush, type Verify, buildLogKey as _buildLogKey, compareLogKeys as _compareLogKeys, createLocalStorageStore as _createLocalStorageStore, createRecordBuffer as _createRecordBuffer, detectLocalStorageQuotaChars as _detectLocalStorageQuotaChars, detectSessionStorageQuotaChars as _detectSessionStorageQuotaChars, isQuotaExceededError as _isQuotaExceededError, neutralizeControlCharacters as _neutralizeControlCharacters, parseLogKey as _parseLogKey, index_d_exports as sinks };