@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.
- package/CHANGELOG.md +11 -0
- package/LICENSES/GPL-3.0-or-later.txt +674 -0
- package/LICENSES/LGPL-3.0-or-later.txt +165 -0
- package/README.md +404 -0
- package/dist/final/neutral/index.d.mts +673 -0
- package/dist/final/neutral/index.mjs +3 -0
- package/dist/final/neutral/rolldown-runtime-5duEfhBv.mjs +1 -0
- package/dist/final/node/index.d.mts +673 -0
- package/dist/final/node/index.mjs +3 -0
- package/dist/final/node/rolldown-runtime-5duEfhBv.mjs +1 -0
- package/package.json +43 -0
- package/src/create-logger.ts +494 -0
- package/src/create-logger.unit.test.ts +752 -0
- package/src/error-format.ts +43 -0
- package/src/index.ts +35 -0
- package/src/logger.ts +67 -0
- package/src/logger.unit.test.ts +190 -0
- package/src/sink/console-control-chars.ts +140 -0
- package/src/sink/console-control-chars.unit.test.ts +206 -0
- package/src/sink/console.ts +531 -0
- package/src/sink/console.unit.test.ts +542 -0
- package/src/sink/file.ts +297 -0
- package/src/sink/file.unit.test.ts +202 -0
- package/src/sink/index.ts +11 -0
- package/src/sink/indexed-db-util.ts +96 -0
- package/src/sink/indexed-db.browser.test.ts +184 -0
- package/src/sink/indexed-db.ts +324 -0
- package/src/sink/indexed-db.unit.test.ts +80 -0
- package/src/sink/local-storage-key.ts +176 -0
- package/src/sink/local-storage-key.unit.test.ts +106 -0
- package/src/sink/local-storage-quota.ts +60 -0
- package/src/sink/local-storage-quota.unit.test.ts +98 -0
- package/src/sink/local-storage-store.ts +368 -0
- package/src/sink/local-storage-store.unit.test.ts +329 -0
- package/src/sink/local-storage.browser.test.ts +125 -0
- package/src/sink/local-storage.ts +182 -0
- package/src/sink/local-storage.unit.test.ts +218 -0
- package/src/sink/noop.ts +46 -0
- package/src/sink/noop.unit.test.ts +47 -0
- package/src/sink/opfs.browser.test.ts +84 -0
- package/src/sink/opfs.ts +212 -0
- package/src/sink/opfs.unit.test.ts +81 -0
- package/src/sink/record-buffer.ts +230 -0
- package/src/sink/record-buffer.unit.test.ts +288 -0
- package/src/sink/session-storage-quota.ts +57 -0
- package/src/sink/session-storage-quota.unit.test.ts +98 -0
- package/src/sink/session-storage-store.ts +178 -0
- package/src/sink/session-storage.browser.test.ts +137 -0
- package/src/sink/session-storage.ts +128 -0
- package/src/sink/session-storage.unit.test.ts +527 -0
- package/src/sink/web-storage-quota-error.ts +43 -0
- package/src/sink/web-storage-quota-error.unit.test.ts +55 -0
- package/src/sink/web-storage-runtime.ts +49 -0
- package/src/startup.unit.test.ts +232 -0
- package/src/tagged.ts +74 -0
- package/src/tagged.unit.test.ts +211 -0
- 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 };
|