@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,531 @@
|
|
|
1
|
+
import { reportLoggerInternalError, } from '../error-format.ts';
|
|
2
|
+
import { neutralizeControlCharacters, } from './console-control-chars.ts';
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
Level,
|
|
6
|
+
LogRecord,
|
|
7
|
+
Sink,
|
|
8
|
+
} from '../types.ts';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Sentinel for an uncomputed `verboseCache` slot. A unique `Symbol` rather
|
|
12
|
+
* than `null`: the `no-nullish-union` rule bans a nullish "absent" arm, and
|
|
13
|
+
* a real `boolean` value is the computed state this must stay distinct from.
|
|
14
|
+
*/
|
|
15
|
+
const VERBOSE_UNCOMPUTED = Symbol('logger:verbose-detection-uncomputed',);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Levels silenced by default unless verbose mode is active.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* SILENT_LEVELS.has('trace'); // true
|
|
23
|
+
* SILENT_LEVELS.has('info'); // false
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
const SILENT_LEVELS: ReadonlySet<string> = new Set([
|
|
27
|
+
'debug',
|
|
28
|
+
'trace',
|
|
29
|
+
],);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Detects verbose mode from environment variables, process arguments,
|
|
33
|
+
* and runtime environment.
|
|
34
|
+
* Checks `process.env.MONOCHROMATIC_VERBOSE`, `process.argv` for `--verbose`,
|
|
35
|
+
* and whether the runtime is a browser.
|
|
36
|
+
* Browser environments enable verbose by default because DevTools
|
|
37
|
+
* already provides its own log-level filtering, making logger-side
|
|
38
|
+
* suppression redundant. Each check is individually guarded
|
|
39
|
+
* so unavailable globals never cause throws.
|
|
40
|
+
*
|
|
41
|
+
* @returns Whether verbose output is enabled.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```ts
|
|
45
|
+
* // With MONOCHROMATIC_VERBOSE=true in environment
|
|
46
|
+
* detectVerbose(); // true
|
|
47
|
+
* ```
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```ts
|
|
51
|
+
* // In a browser environment (window is defined)
|
|
52
|
+
* detectVerbose(); // true
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
function detectVerbose(): boolean {
|
|
56
|
+
try {
|
|
57
|
+
if (((typeof process) !== 'undefined') && (process.env
|
|
58
|
+
.MONOCHROMATIC_VERBOSE
|
|
59
|
+
=== 'true'))
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
catch (error: unknown) {
|
|
63
|
+
reportLoggerInternalError({
|
|
64
|
+
context: 'MONOCHROMATIC_VERBOSE environment probe failed during verbose detection',
|
|
65
|
+
error,
|
|
66
|
+
},);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
if (((typeof process) !== 'undefined')
|
|
71
|
+
&& Array
|
|
72
|
+
.isArray(process.argv,)
|
|
73
|
+
&& process
|
|
74
|
+
.argv
|
|
75
|
+
.includes('--verbose',))
|
|
76
|
+
{
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch (error: unknown) {
|
|
81
|
+
reportLoggerInternalError({
|
|
82
|
+
context: 'process argv probe failed during verbose detection',
|
|
83
|
+
error,
|
|
84
|
+
},);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
// Browser DevTools already provides log-level filtering,
|
|
89
|
+
// so suppressing debug/trace at the logger level is redundant.
|
|
90
|
+
if ('window' in globalThis)
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
catch (error: unknown) {
|
|
94
|
+
reportLoggerInternalError({
|
|
95
|
+
context: 'window probe failed during verbose detection',
|
|
96
|
+
error,
|
|
97
|
+
},);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Detects explicit warn suppression via the `MONOCHROMATIC_WARN` environment
|
|
105
|
+
* variable.
|
|
106
|
+
*
|
|
107
|
+
* Setting `MONOCHROMATIC_WARN=false` drops `warn`-level records, for machine-protocol
|
|
108
|
+
* consumers (such as a stdin/stdout codec) whose output streams must stay clean
|
|
109
|
+
* on success. Only the exact string `'false'` suppresses; any other value, or an
|
|
110
|
+
* absent variable, leaves `warn` enabled. Read on each call (not memoized) so a
|
|
111
|
+
* host can toggle it between logs.
|
|
112
|
+
*
|
|
113
|
+
* @returns Whether `warn`-level records are suppressed.
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```ts
|
|
117
|
+
* // With MONOCHROMATIC_WARN=false in environment
|
|
118
|
+
* isWarnSuppressed(); // true
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
function isWarnSuppressed(): boolean {
|
|
122
|
+
try {
|
|
123
|
+
return ((typeof process) !== 'undefined') && (process.env
|
|
124
|
+
.MONOCHROMATIC_WARN
|
|
125
|
+
=== 'false');
|
|
126
|
+
}
|
|
127
|
+
catch (error: unknown) {
|
|
128
|
+
reportLoggerInternalError({
|
|
129
|
+
context: 'MONOCHROMATIC_WARN environment probe failed during warn suppression detection',
|
|
130
|
+
error,
|
|
131
|
+
},);
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Maps log levels to the name of the console method that handles them.
|
|
138
|
+
* Names rather than function references so tests (and other hot patches)
|
|
139
|
+
* that replace `console.info` etc. after module load still see their
|
|
140
|
+
* replacement when the sink flushes. `debug` uses this mapping only when
|
|
141
|
+
* process stderr is unavailable, preserving browser `console.debug` output.
|
|
142
|
+
*/
|
|
143
|
+
const LEVEL_TO_CONSOLE_METHOD: Record<Level,
|
|
144
|
+
'debug' | 'error' | 'info' | 'trace' | 'warn'> = {
|
|
145
|
+
debug: 'debug',
|
|
146
|
+
error: 'error',
|
|
147
|
+
fatal: 'error',
|
|
148
|
+
info: 'info',
|
|
149
|
+
trace: 'trace',
|
|
150
|
+
warn: 'warn',
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Formats a single log record into the display string used by console output.
|
|
155
|
+
* The message passes through {@link neutralizeControlCharacters} first, so a
|
|
156
|
+
* terminal never receives a control sequence smuggled inside log text.
|
|
157
|
+
*
|
|
158
|
+
* @param record - Record to format.
|
|
159
|
+
*
|
|
160
|
+
* @returns Formatted line of the shape `[level] [iso] message`.
|
|
161
|
+
*
|
|
162
|
+
* @example
|
|
163
|
+
* ```ts
|
|
164
|
+
* formatRecord({ level: 'info', message: 'hi', timestamp: 0 });
|
|
165
|
+
* // => '[info] [1970-01-01T00:00:00.000Z] hi'
|
|
166
|
+
* ```
|
|
167
|
+
*/
|
|
168
|
+
function formatRecord(record: LogRecord,): string {
|
|
169
|
+
return `[${record.level}] [${
|
|
170
|
+
new Date(record.timestamp,)
|
|
171
|
+
.toISOString()
|
|
172
|
+
}] ${neutralizeControlCharacters(record.message,)}`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Detects whether process stderr can receive debug records directly. Kept
|
|
177
|
+
* separate from writing so availability checks can still require
|
|
178
|
+
* `console.debug` when stderr is unavailable and browser fallback is needed.
|
|
179
|
+
*
|
|
180
|
+
* @returns Whether process stderr exposes a callable `write` method.
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* ```ts
|
|
184
|
+
* hasProcessStderr();
|
|
185
|
+
* // => true in Node.js and Bun processes
|
|
186
|
+
* ```
|
|
187
|
+
*/
|
|
188
|
+
function hasProcessStderr(): boolean {
|
|
189
|
+
try {
|
|
190
|
+
if ((typeof process) === 'undefined')
|
|
191
|
+
return false;
|
|
192
|
+
|
|
193
|
+
return (typeof process
|
|
194
|
+
.stderr
|
|
195
|
+
.write) === 'function';
|
|
196
|
+
}
|
|
197
|
+
catch (error: unknown) {
|
|
198
|
+
reportLoggerInternalError({
|
|
199
|
+
context: 'process stderr availability probe failed',
|
|
200
|
+
error,
|
|
201
|
+
},);
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Writes a formatted debug run to process stderr when the host exposes a
|
|
208
|
+
* process stream. Falling back to `console.debug` keeps browser and restricted
|
|
209
|
+
* runtimes working when `process` is absent or unusable.
|
|
210
|
+
*
|
|
211
|
+
* @param text - Formatted debug run text that should stay off stdout.
|
|
212
|
+
*
|
|
213
|
+
* @returns Whether process stderr accepted the debug run.
|
|
214
|
+
*
|
|
215
|
+
* @example
|
|
216
|
+
* ```ts
|
|
217
|
+
* writeDebugRunToProcessStderr('[debug] [1970-01-01T00:00:00.000Z] hi');
|
|
218
|
+
* // => true when process.stderr.write is available
|
|
219
|
+
* ```
|
|
220
|
+
*/
|
|
221
|
+
function writeDebugRunToProcessStderr(text: string,): boolean {
|
|
222
|
+
try {
|
|
223
|
+
if (!hasProcessStderr())
|
|
224
|
+
return false;
|
|
225
|
+
|
|
226
|
+
process.stderr
|
|
227
|
+
.write(`${text}\n`,);
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
catch (error: unknown) {
|
|
231
|
+
reportLoggerInternalError({
|
|
232
|
+
context: 'debug run process stderr write failed',
|
|
233
|
+
error,
|
|
234
|
+
},);
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Emits a contiguous run of same-level records as a single console call,
|
|
241
|
+
* joining formatted lines with `\n`. Debug records write to process stderr
|
|
242
|
+
* when `process.stderr.write` is available, keeping stdout clean in CLI hosts.
|
|
243
|
+
*
|
|
244
|
+
* @param records - Records that all share `level`.
|
|
245
|
+
*
|
|
246
|
+
* @param level - Shared severity level whose mapped `console.*` receives
|
|
247
|
+
* the joined text.
|
|
248
|
+
*
|
|
249
|
+
* @example
|
|
250
|
+
* ```ts
|
|
251
|
+
* emitRun({ records: [{ level: 'info', message: 'a', timestamp: 0 }], level: 'info' });
|
|
252
|
+
* // calls console.info('[info] [1970-01-01T00:00:00.000Z] a')
|
|
253
|
+
* ```
|
|
254
|
+
*/
|
|
255
|
+
function emitRun(
|
|
256
|
+
{
|
|
257
|
+
records,
|
|
258
|
+
level,
|
|
259
|
+
}: {
|
|
260
|
+
readonly records: readonly LogRecord[];
|
|
261
|
+
readonly level: Level;
|
|
262
|
+
},
|
|
263
|
+
): void {
|
|
264
|
+
/**
|
|
265
|
+
* Joined run text; one `\n`-separated string per console call so a long run becomes a single grouped entry rather than N separate ones.
|
|
266
|
+
*/
|
|
267
|
+
const text = records
|
|
268
|
+
.map(function formatOne(r,) {
|
|
269
|
+
return formatRecord(r,);
|
|
270
|
+
},)
|
|
271
|
+
.join('\n',);
|
|
272
|
+
if ((level === 'debug') && writeDebugRunToProcessStderr(text,))
|
|
273
|
+
return;
|
|
274
|
+
try {
|
|
275
|
+
/**
|
|
276
|
+
* Name (not the function reference) of the matching `console.*` method; resolved lazily so post-import hot patches still apply.
|
|
277
|
+
*/
|
|
278
|
+
const method = LEVEL_TO_CONSOLE_METHOD[level];
|
|
279
|
+
/**
|
|
280
|
+
* Resolved console method looked up by name; may be missing or non-callable in stripped runtimes, which the guard handles.
|
|
281
|
+
*/
|
|
282
|
+
const consoleFn = console[method];
|
|
283
|
+
if ((typeof consoleFn) === 'function') {
|
|
284
|
+
consoleFn.call(
|
|
285
|
+
console,
|
|
286
|
+
text,
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
catch (error: unknown) {
|
|
291
|
+
reportLoggerInternalError({
|
|
292
|
+
context: 'console method call failed while emitting log run',
|
|
293
|
+
error,
|
|
294
|
+
},);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* A contiguous slice of buffered records that share one level. Built by
|
|
300
|
+
* {@link groupRuns} and consumed by {@link flushBuffer} to emit one
|
|
301
|
+
* `console.*` call per slice.
|
|
302
|
+
*/
|
|
303
|
+
type Run = {
|
|
304
|
+
level: Level;
|
|
305
|
+
records: LogRecord[];
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Groups a record sequence into contiguous same-level runs. Each input
|
|
310
|
+
* record is appended to the trailing run when its level matches, otherwise
|
|
311
|
+
* a new run is opened. A reduce-based collapse keeps the cursor (run head
|
|
312
|
+
* and span) out of mutable function-body locals.
|
|
313
|
+
*
|
|
314
|
+
* @param records - Buffered records in arrival order.
|
|
315
|
+
*
|
|
316
|
+
* @returns Ordered list of runs covering every input record exactly once.
|
|
317
|
+
*
|
|
318
|
+
* @example
|
|
319
|
+
* ```ts
|
|
320
|
+
* groupRuns([
|
|
321
|
+
* { level: 'debug', message: 'a', timestamp: 0 },
|
|
322
|
+
* { level: 'debug', message: 'b', timestamp: 0 },
|
|
323
|
+
* { level: 'warn', message: 'c', timestamp: 0 },
|
|
324
|
+
* ]);
|
|
325
|
+
* // => [{ level: 'debug', records: [a, b] }, { level: 'warn', records: [c] }]
|
|
326
|
+
* ```
|
|
327
|
+
*/
|
|
328
|
+
function groupRuns(records: readonly LogRecord[],): Run[] {
|
|
329
|
+
return records.reduce<Run[]>(
|
|
330
|
+
function appendToRuns(
|
|
331
|
+
runs,
|
|
332
|
+
record,
|
|
333
|
+
) {
|
|
334
|
+
/**
|
|
335
|
+
* Trailing run being extended; new same-level records append onto it, otherwise a fresh run is opened.
|
|
336
|
+
*/
|
|
337
|
+
const tail = runs.at(-1,);
|
|
338
|
+
if ((tail !== undefined) && (tail.level
|
|
339
|
+
=== record
|
|
340
|
+
.level)) {
|
|
341
|
+
tail.records
|
|
342
|
+
.push(record,);
|
|
343
|
+
return runs;
|
|
344
|
+
}
|
|
345
|
+
runs.push({
|
|
346
|
+
level: record.level,
|
|
347
|
+
records: [record,],
|
|
348
|
+
},);
|
|
349
|
+
return runs;
|
|
350
|
+
},
|
|
351
|
+
[],
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Verifies console is available and microtask scheduling is supported.
|
|
357
|
+
* `queueMicrotask` is the batching primitive; without it there is no
|
|
358
|
+
* ordering guarantee that preserves "end of current sync frame" semantics,
|
|
359
|
+
* so the sink reports itself unavailable instead of falling back to an
|
|
360
|
+
* inferior scheduler. Stateless: the logger calls this once and owns the
|
|
361
|
+
* resulting availability.
|
|
362
|
+
*
|
|
363
|
+
* @returns Whether console logging is available.
|
|
364
|
+
*
|
|
365
|
+
* @example
|
|
366
|
+
* ```ts
|
|
367
|
+
* if (await verifyConsole()) {
|
|
368
|
+
* // console usable
|
|
369
|
+
* }
|
|
370
|
+
* ```
|
|
371
|
+
*/
|
|
372
|
+
function verifyConsole(): Promise<boolean> {
|
|
373
|
+
try {
|
|
374
|
+
if ((typeof console) === 'undefined')
|
|
375
|
+
return Promise.resolve(false,);
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Sample console method used only to check that debug has an output path:
|
|
379
|
+
* process runtimes use stderr, while fallback runtimes need `console.debug`.
|
|
380
|
+
*/
|
|
381
|
+
const testFn = hasProcessStderr() ? console.info : console.debug;
|
|
382
|
+
if ((typeof testFn) !== 'function')
|
|
383
|
+
return Promise.resolve(false,);
|
|
384
|
+
|
|
385
|
+
if ((typeof queueMicrotask) !== 'function')
|
|
386
|
+
return Promise.resolve(false,);
|
|
387
|
+
|
|
388
|
+
return Promise.resolve(true,);
|
|
389
|
+
}
|
|
390
|
+
catch (error: unknown) {
|
|
391
|
+
reportLoggerInternalError({
|
|
392
|
+
context: 'console sink verification failed',
|
|
393
|
+
error,
|
|
394
|
+
},);
|
|
395
|
+
return Promise.resolve(false,);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Builds a microtask-batched console sink. The pending buffer, schedule flag,
|
|
401
|
+
* and memoized verbose detection live in this instance's closure (no
|
|
402
|
+
* module-global state), so independent loggers and tests stay isolated with
|
|
403
|
+
* no reset hook. Collapses contiguous same-level runs into single `console.*`
|
|
404
|
+
* calls, sharply reducing console-panel overhead when an instrumented path
|
|
405
|
+
* emits many records per sync frame.
|
|
406
|
+
*
|
|
407
|
+
* @returns Sink that writes formatted lines to `console.*`, except
|
|
408
|
+
* process-hosted debug records write to stderr.
|
|
409
|
+
*
|
|
410
|
+
* @example
|
|
411
|
+
* ```ts
|
|
412
|
+
* const { logger } = createLogger({ sinks: [createConsoleSink()] });
|
|
413
|
+
* logger.info('server started');
|
|
414
|
+
* ```
|
|
415
|
+
*/
|
|
416
|
+
export function createConsoleSink(): Sink {
|
|
417
|
+
/**
|
|
418
|
+
* Instance-local console-sink state. `buffer` holds records awaiting the
|
|
419
|
+
* next microtask flush; `scheduled` guards against redundant
|
|
420
|
+
* `queueMicrotask` calls within one sync frame; `verboseCache` memoizes
|
|
421
|
+
* verbose detection (the sentinel means not yet computed) so a host can
|
|
422
|
+
* mutate `process.env.MONOCHROMATIC_VERBOSE` before the first log and still be seen.
|
|
423
|
+
*/
|
|
424
|
+
const state: {
|
|
425
|
+
buffer: LogRecord[];
|
|
426
|
+
scheduled: boolean;
|
|
427
|
+
verboseCache: boolean | typeof VERBOSE_UNCOMPUTED;
|
|
428
|
+
} = {
|
|
429
|
+
buffer: [],
|
|
430
|
+
scheduled: false,
|
|
431
|
+
verboseCache: VERBOSE_UNCOMPUTED,
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Reads the memoized verbose flag, evaluating via {@link detectVerbose} on
|
|
436
|
+
* first call. Lazy rather than at construction so tests (and hosts) can
|
|
437
|
+
* mutate `process.env.MONOCHROMATIC_VERBOSE` between construction and first log without a
|
|
438
|
+
* stale cache.
|
|
439
|
+
*
|
|
440
|
+
* @returns Whether verbose logging is enabled for this process.
|
|
441
|
+
*/
|
|
442
|
+
function getVerbose(): boolean {
|
|
443
|
+
/**
|
|
444
|
+
* Cached verbose flag; the sentinel means detection has not run yet.
|
|
445
|
+
*/
|
|
446
|
+
const cached = state.verboseCache;
|
|
447
|
+
if (cached !== VERBOSE_UNCOMPUTED)
|
|
448
|
+
return cached;
|
|
449
|
+
/**
|
|
450
|
+
* Computed verbose flag, stored so subsequent reads skip detection.
|
|
451
|
+
*/
|
|
452
|
+
const computed = detectVerbose();
|
|
453
|
+
state.verboseCache = computed;
|
|
454
|
+
return computed;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Drains the buffer, collapsing contiguous same-level runs (via
|
|
459
|
+
* {@link groupRuns}) into single console calls emitted by {@link emitRun}.
|
|
460
|
+
* A sequence `[debug, debug, warn, debug]` becomes three calls:
|
|
461
|
+
* `process.stderr.write` (two lines joined), `console.warn`, then
|
|
462
|
+
* `process.stderr.write` under process runtimes. Typical instrumented
|
|
463
|
+
* functions use a single level throughout, so most flushes collapse to one
|
|
464
|
+
* call.
|
|
465
|
+
*/
|
|
466
|
+
function flushBuffer(): void {
|
|
467
|
+
state.scheduled = false;
|
|
468
|
+
if (state.buffer
|
|
469
|
+
.length
|
|
470
|
+
=== 0)
|
|
471
|
+
return;
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Snapshot of buffered records drained before the loop.
|
|
475
|
+
*
|
|
476
|
+
* Using `splice(0)` empties the buffer atomically so any record enqueued
|
|
477
|
+
* during emission lands in the next flush rather than this one.
|
|
478
|
+
*/
|
|
479
|
+
const records = state.buffer
|
|
480
|
+
.splice(0,);
|
|
481
|
+
for (const run of groupRuns(records,)) {
|
|
482
|
+
emitRun({
|
|
483
|
+
level: run.level,
|
|
484
|
+
records: run.records,
|
|
485
|
+
},);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Enqueues a record for microtask-batched emission. Silently discards
|
|
491
|
+
* `debug`/`trace` unless {@link getVerbose} reports verbose mode is active
|
|
492
|
+
* (via `MONOCHROMATIC_VERBOSE=true` env var, `--verbose` argv, or browser environment), and
|
|
493
|
+
* drops `warn` when {@link isWarnSuppressed}.
|
|
494
|
+
*
|
|
495
|
+
* @param record - Log record to write.
|
|
496
|
+
*/
|
|
497
|
+
function write(record: LogRecord,): Promise<void> {
|
|
498
|
+
if ((!getVerbose()) && SILENT_LEVELS
|
|
499
|
+
.has(record.level,))
|
|
500
|
+
return Promise.resolve();
|
|
501
|
+
|
|
502
|
+
if ((record.level === 'warn') && isWarnSuppressed())
|
|
503
|
+
return Promise.resolve();
|
|
504
|
+
|
|
505
|
+
state.buffer
|
|
506
|
+
.push(record,);
|
|
507
|
+
|
|
508
|
+
if (!state.scheduled) {
|
|
509
|
+
state.scheduled = true;
|
|
510
|
+
queueMicrotask(flushBuffer,);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return Promise.resolve();
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Forces any buffered records through to the console immediately via
|
|
518
|
+
* {@link flushBuffer}. Returns an already-resolved promise so call sites
|
|
519
|
+
* await uniformly with async sinks. Safe to call when the buffer is empty.
|
|
520
|
+
*/
|
|
521
|
+
function flush(): Promise<void> {
|
|
522
|
+
flushBuffer();
|
|
523
|
+
return Promise.resolve();
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
return {
|
|
527
|
+
flush,
|
|
528
|
+
verify: verifyConsole,
|
|
529
|
+
write,
|
|
530
|
+
};
|
|
531
|
+
}
|