@monochromatic-dev/module-logger 0.1.0 → 0.3.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 +27 -0
- package/README.md +58 -12
- package/dist/final/neutral/browser.d.mts +60 -0
- package/dist/final/neutral/browser.mjs +1 -0
- package/dist/final/neutral/index.d.mts +366 -572
- package/dist/final/neutral/index.mjs +2 -3
- package/dist/final/neutral/indexed-db-hsIfv7Cv.mjs +2 -0
- package/dist/final/neutral/types-BkkBXgY3.d.mts +76 -0
- package/dist/final/node/file-CRGb1hDK.mjs +1 -0
- package/dist/final/node/index.d.mts +366 -572
- package/dist/final/node/index.mjs +3 -3
- package/dist/final/node/node.d.mts +103 -0
- package/dist/final/node/node.mjs +1 -0
- package/dist/final/node/types-BkkBXgY3.d.mts +76 -0
- package/package.json +19 -5
- package/src/artifact-platform-split.unit.test.ts +140 -0
- package/src/browser.ts +14 -0
- package/src/create-logger.ts +249 -151
- package/src/create-logger.unit.test.ts +527 -75
- package/src/default-sinks.neutral.ts +34 -0
- package/src/default-sinks.node.ts +32 -0
- package/src/error-format.ts +23 -23
- package/src/index.ts +2 -0
- package/src/logger.ts +23 -50
- package/src/node.ts +23 -0
- package/src/sink/console-control-chars.ts +64 -64
- package/src/sink/console-control-chars.unit.test.ts +14 -14
- package/src/sink/console.ts +194 -194
- package/src/sink/console.unit.test.ts +18 -18
- package/src/sink/file.ts +136 -140
- package/src/sink/file.unit.test.ts +19 -26
- package/src/sink/index.ts +4 -7
- package/src/sink/indexed-db-util.ts +42 -42
- package/src/sink/indexed-db.browser.test.ts +7 -7
- package/src/sink/indexed-db.ts +109 -109
- package/src/sink/indexed-db.unit.test.ts +5 -13
- package/src/sink/local-storage-key.ts +73 -73
- package/src/sink/local-storage-key.unit.test.ts +8 -8
- package/src/sink/local-storage-quota.ts +37 -37
- package/src/sink/local-storage-quota.unit.test.ts +8 -8
- package/src/sink/local-storage-store.ts +113 -113
- package/src/sink/local-storage-store.unit.test.ts +35 -35
- package/src/sink/local-storage.ts +72 -72
- package/src/sink/local-storage.unit.test.ts +27 -27
- package/src/sink/noop.ts +20 -20
- package/src/sink/noop.unit.test.ts +1 -1
- package/src/sink/opfs.browser.test.ts +7 -7
- package/src/sink/opfs.ts +62 -62
- package/src/sink/opfs.unit.test.ts +5 -13
- package/src/sink/record-buffer.ts +84 -84
- package/src/sink/record-buffer.unit.test.ts +20 -20
- package/src/sink/session-storage-quota.ts +34 -34
- package/src/sink/session-storage-quota.unit.test.ts +8 -8
- package/src/sink/session-storage-store.ts +72 -72
- package/src/sink/session-storage.ts +48 -48
- package/src/sink/session-storage.unit.test.ts +39 -39
- package/src/sink/web-storage-quota-error.ts +22 -22
- package/src/sink/web-storage-quota-error.unit.test.ts +2 -2
- package/src/sink/web-storage-runtime.ts +24 -24
- package/src/startup.unit.test.ts +18 -18
- package/src/tagged.ts +35 -35
- package/src/tagged.unit.test.ts +8 -8
- package/src/types.ts +39 -39
package/src/sink/opfs.ts
CHANGED
|
@@ -7,56 +7,56 @@ import type {
|
|
|
7
7
|
} from '../types.ts';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
10
|
+
Builds an OPFS sink that buffers serialized records through the shared
|
|
11
|
+
{@link createRecordBuffer} policy and appends each newline-joined JSONL
|
|
12
|
+
batch to a per-session file in the Origin Private File System with one
|
|
13
|
+
stream write per batch. The kept-open writable stream lives in this
|
|
14
|
+
instance's closure (no module-global state), so independent loggers and
|
|
15
|
+
tests never share a handle or need a reset hook.
|
|
16
|
+
|
|
17
|
+
Flush triggers (32 KiB in-write cap, `warn`-or-worse severity, 250 ms
|
|
18
|
+
quiet-period deadline, page lifecycle, and the `flush` hook) are the
|
|
19
|
+
buffer's; see {@link createRecordBuffer}. Batch writes queue on the stream
|
|
20
|
+
in issue order, so ordering holds at the batch boundary, and the sink's
|
|
21
|
+
`flush` hook awaits every issued batch before resolving.
|
|
22
|
+
|
|
23
|
+
@returns Sink backed by OPFS.
|
|
24
|
+
|
|
25
|
+
@example
|
|
26
|
+
```ts
|
|
27
|
+
const { logger } = createLogger({ sinks: [createOpfsSink()] });
|
|
28
|
+
logger.warn('quota nearing limit');
|
|
29
|
+
```
|
|
30
30
|
*/
|
|
31
31
|
export function createOpfsSink(): Sink {
|
|
32
32
|
/**
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
Instance-local kept-open OPFS stream, opened by `verify` and reused by
|
|
34
|
+
every batch write. Absent until a successful verification.
|
|
35
35
|
*/
|
|
36
36
|
const state: { writable?: FileSystemWritableFileStream; } = {};
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
Batch writes issued to the stream and not yet settled; the `flush` hook
|
|
40
|
+
drains this so logger-level `flush()` observes every issued batch.
|
|
41
41
|
*/
|
|
42
42
|
const pendingBatchWrites = new Set<Promise<void>>();
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
Verifies OPFS is available and round-trips a probe write, then opens the
|
|
46
|
+
stream reused by subsequent writes. The logger calls this once and owns
|
|
47
|
+
the resulting availability.
|
|
48
|
+
|
|
49
|
+
@returns Whether OPFS logging is available.
|
|
50
50
|
*/
|
|
51
51
|
async function verify(): Promise<boolean> {
|
|
52
52
|
try {
|
|
53
53
|
/**
|
|
54
|
-
|
|
54
|
+
Origin Private File System directory handle that hosts every monochromatic log file.
|
|
55
55
|
*/
|
|
56
56
|
const opfsRoot = await navigator.storage
|
|
57
57
|
.getDirectory();
|
|
58
58
|
/**
|
|
59
|
-
|
|
59
|
+
ISO timestamp with colons replaced by dashes so it can be embedded in a cross-platform file name.
|
|
60
60
|
*/
|
|
61
61
|
const timestamp = new Date().toISOString()
|
|
62
62
|
.replaceAll(
|
|
@@ -64,7 +64,7 @@ export function createOpfsSink(): Sink {
|
|
|
64
64
|
'-',
|
|
65
65
|
);
|
|
66
66
|
/**
|
|
67
|
-
|
|
67
|
+
OPFS handle for the per-run log file, created on first verification and reused for subsequent writes.
|
|
68
68
|
*/
|
|
69
69
|
const fileHandle = await opfsRoot.getFileHandle(
|
|
70
70
|
`monochromatic-${timestamp}.log.jsonl`,
|
|
@@ -73,26 +73,26 @@ export function createOpfsSink(): Sink {
|
|
|
73
73
|
// Write test data and close to flush; getFile() reads stale content
|
|
74
74
|
// while a FileSystemWritableFileStream is still open.
|
|
75
75
|
/**
|
|
76
|
-
|
|
76
|
+
Throwaway writable used only to flush the probe so the next `getFile` returns persisted content.
|
|
77
77
|
*/
|
|
78
78
|
const probeWritable = await fileHandle.createWritable({ keepExistingData: true, },);
|
|
79
79
|
/**
|
|
80
|
-
|
|
80
|
+
Probe record written and read back to confirm OPFS round-trips writes.
|
|
81
81
|
*/
|
|
82
82
|
const testData = `{"test":true,"timestamp":${Date.now()}}\n`;
|
|
83
83
|
await probeWritable.write(testData,);
|
|
84
84
|
await probeWritable.close();
|
|
85
85
|
|
|
86
86
|
/**
|
|
87
|
-
|
|
87
|
+
File snapshot of the probe, taken after closing `probeWritable` so its bytes are flushed.
|
|
88
88
|
*/
|
|
89
89
|
const file = await fileHandle.getFile();
|
|
90
90
|
/**
|
|
91
|
-
|
|
91
|
+
Probe contents read back; matching the literal `"test":true` proves OPFS persisted the data.
|
|
92
92
|
*/
|
|
93
93
|
const content = await file.text();
|
|
94
94
|
/**
|
|
95
|
-
|
|
95
|
+
Whether the probe round-tripped; only then is the reused stream opened.
|
|
96
96
|
*/
|
|
97
97
|
const available = content.includes('"test":true',);
|
|
98
98
|
|
|
@@ -104,7 +104,7 @@ export function createOpfsSink(): Sink {
|
|
|
104
104
|
}
|
|
105
105
|
catch (error: unknown) {
|
|
106
106
|
/**
|
|
107
|
-
|
|
107
|
+
OPFS storage object, present only when the current platform exposes the backend this sink verifies.
|
|
108
108
|
*/
|
|
109
109
|
const opfsStorage = globalThis.navigator
|
|
110
110
|
?.storage;
|
|
@@ -118,10 +118,10 @@ export function createOpfsSink(): Sink {
|
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
/**
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
121
|
+
Writes one newline-terminated batch to the OPFS stream, swallowing and
|
|
122
|
+
reporting failures so the pending-write set always settles.
|
|
123
|
+
|
|
124
|
+
@param batch - Newline-joined JSONL batch from the buffer.
|
|
125
125
|
*/
|
|
126
126
|
async function writeBatch(batch: string,): Promise<void> {
|
|
127
127
|
if (!state.writable)
|
|
@@ -140,10 +140,10 @@ export function createOpfsSink(): Sink {
|
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
/**
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
143
|
+
Removes a tracked batch write from {@link pendingBatchWrites} once it
|
|
144
|
+
settles.
|
|
145
|
+
|
|
146
|
+
@param pending - Promise returned by {@link writeBatch}.
|
|
147
147
|
*/
|
|
148
148
|
async function removePendingWhenSettled(pending: Promise<void>,): Promise<void> {
|
|
149
149
|
await pending;
|
|
@@ -151,14 +151,14 @@ export function createOpfsSink(): Sink {
|
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
/**
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
154
|
+
Backend handoff for the buffer: issues the batch write without awaiting
|
|
155
|
+
(stream writes queue in issue order) and tracks it for the `flush` hook.
|
|
156
|
+
|
|
157
|
+
@param batch - Newline-joined JSONL batch from the buffer.
|
|
158
158
|
*/
|
|
159
159
|
function handOffBatch(batch: string,): void {
|
|
160
160
|
/**
|
|
161
|
-
|
|
161
|
+
In-flight batch write; never rejects, because {@link writeBatch} reports internally.
|
|
162
162
|
*/
|
|
163
163
|
const pending = writeBatch(batch,);
|
|
164
164
|
pendingBatchWrites.add(pending,);
|
|
@@ -166,18 +166,18 @@ export function createOpfsSink(): Sink {
|
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
/**
|
|
169
|
-
|
|
170
|
-
|
|
169
|
+
Shared buffering stage; every flush trigger issues one queued stream
|
|
170
|
+
write per joined batch.
|
|
171
171
|
*/
|
|
172
172
|
const buffer = createRecordBuffer({ onFlush: handOffBatch, },);
|
|
173
173
|
|
|
174
174
|
/**
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
175
|
+
Buffers a log record through the shared policy; see
|
|
176
|
+
{@link createRecordBuffer} for the flush triggers.
|
|
177
|
+
|
|
178
|
+
@param record - Log record to buffer and eventually append.
|
|
179
|
+
|
|
180
|
+
@mutates record - `JSON.stringify` may invoke `toJSON`, getters, or proxy traps.
|
|
181
181
|
*/
|
|
182
182
|
function write(record: {
|
|
183
183
|
level: Level;
|
|
@@ -192,13 +192,13 @@ export function createOpfsSink(): Sink {
|
|
|
192
192
|
}
|
|
193
193
|
|
|
194
194
|
/**
|
|
195
|
-
|
|
196
|
-
|
|
195
|
+
Drains the buffer onto the stream and resolves once every issued batch
|
|
196
|
+
write has settled.
|
|
197
197
|
*/
|
|
198
198
|
async function flush(): Promise<void> {
|
|
199
199
|
buffer.drain();
|
|
200
200
|
/**
|
|
201
|
-
|
|
201
|
+
Snapshot of in-flight batch writes at drain time.
|
|
202
202
|
*/
|
|
203
203
|
const writes = [...pendingBatchWrites,];
|
|
204
204
|
await Promise.all(writes,);
|
|
@@ -3,23 +3,15 @@ import {
|
|
|
3
3
|
expect,
|
|
4
4
|
it,
|
|
5
5
|
} from '@monochromatic-dev/module-test/ts';
|
|
6
|
-
import {
|
|
7
|
-
sinks,
|
|
8
|
-
} from '@monochromatic-dev/module-logger';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Sink factories under test, read from the built artifact's `sinks` namespace.
|
|
12
|
-
*/
|
|
13
|
-
const {
|
|
14
|
-
createOpfsSink,
|
|
15
|
-
} = sinks;
|
|
6
|
+
import { createOpfsSink, } from '@monochromatic-dev/module-logger/browser';
|
|
16
7
|
|
|
17
8
|
// Node/Bun has no `navigator.storage`, so this file exercises the
|
|
18
9
|
// unavailable-backend fallback that the browser test (which runs where OPFS
|
|
19
10
|
// exists) never reaches: `getDirectory` throws and is caught, and drained
|
|
20
11
|
// batches hit the unset-stream guard. The available path lives in
|
|
21
12
|
// `opfs.browser.test.ts`; the shared buffering policy is covered in
|
|
22
|
-
// `record-buffer.unit.test.ts`.
|
|
13
|
+
// `record-buffer.unit.test.ts`. The factory is reached through the
|
|
14
|
+
// `./browser` subpath because the root entry no longer exports it.
|
|
23
15
|
await describe({
|
|
24
16
|
name: 'OPFS sink (node fallback)',
|
|
25
17
|
children: [
|
|
@@ -41,7 +33,7 @@ await describe({
|
|
|
41
33
|
// flush trigger fires.
|
|
42
34
|
const sink = createOpfsSink();
|
|
43
35
|
/**
|
|
44
|
-
|
|
36
|
+
Resolved write result; the sink write contract is `Promise<void>`.
|
|
45
37
|
*/
|
|
46
38
|
const result = await sink.write({
|
|
47
39
|
level: 'info',
|
|
@@ -70,7 +62,7 @@ await describe({
|
|
|
70
62
|
timestamp: 1,
|
|
71
63
|
},);
|
|
72
64
|
/**
|
|
73
|
-
|
|
65
|
+
Resolved flush result; must settle even with no stream to write to.
|
|
74
66
|
*/
|
|
75
67
|
const result = await sink.flush?.();
|
|
76
68
|
expect(result,)
|
|
@@ -1,33 +1,33 @@
|
|
|
1
1
|
import type { Level, } from '../types.ts';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
4
|
+
Buffered code units that force a synchronous flush from inside `add`
|
|
5
|
+
itself. 32 KiB sits in the measured flat bottom of the batch-size curve on
|
|
6
|
+
Chromium 149 and Node 26 (0.15 µs to 1.7 µs per record versus 5 µs to
|
|
7
|
+
15 µs unbatched) while staying clear of the measured U-turn where flushes
|
|
8
|
+
past ~100 KiB cost more per record than not batching; see
|
|
9
|
+
`doc/troubleshooting/web-storage-sink-main-thread-cost.md`. Because this
|
|
10
|
+
flush runs synchronously inside `add`, a wedged main thread that keeps
|
|
11
|
+
logging can never hold more than one cap's worth of unpersisted records.
|
|
12
12
|
*/
|
|
13
13
|
const FLUSH_BUFFER_CAP_CHARS = 32_768;
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
Quiet-period deadline before a buffered record is flushed by timer, so
|
|
17
|
+
low-volume sessions still reach the backend without waiting for the byte
|
|
18
|
+
cap. Each deadline flush costs one backend call, so this cadence is
|
|
19
|
+
negligible while keeping the loss window for idle periods under a quarter
|
|
20
|
+
second.
|
|
21
21
|
*/
|
|
22
22
|
const FLUSH_DEADLINE_MS = 250;
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
Severities that flush the buffer synchronously from inside `add`, so every
|
|
26
|
+
record up to and including a warning or worse reaches the backend before
|
|
27
|
+
control returns to the caller. Failure forensics is why persistent sinks
|
|
28
|
+
exist; these records are rare, so paying the per-batch cost immediately
|
|
29
|
+
for them does not dent the amortization of the bulk `debug`/`trace`/`info`
|
|
30
|
+
volume.
|
|
31
31
|
*/
|
|
32
32
|
const FLUSH_IMMEDIATELY_BY_LEVEL: Record<Level, boolean> = {
|
|
33
33
|
debug: false,
|
|
@@ -39,19 +39,19 @@ const FLUSH_IMMEDIATELY_BY_LEVEL: Record<Level, boolean> = {
|
|
|
39
39
|
};
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
Timer handle exposing Node's keep-alive release. Browsers return a bare
|
|
43
|
+
number from `setTimeout` and need no release; Node returns an object whose
|
|
44
|
+
`unref` lets the process exit while the timer is pending.
|
|
45
45
|
*/
|
|
46
46
|
type UnrefableTimer = { readonly unref: () => void; };
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
49
|
+
Narrows a `setTimeout` return value to a handle exposing `unref`, so a
|
|
50
|
+
pending deadline flush never pins a server process open past its work.
|
|
51
|
+
|
|
52
|
+
@param timer - Return value of `globalThis.setTimeout`.
|
|
53
|
+
|
|
54
|
+
@returns Whether `timer` exposes a callable `unref`.
|
|
55
55
|
*/
|
|
56
56
|
function isUnrefableTimer(timer: unknown,): timer is UnrefableTimer {
|
|
57
57
|
if (((typeof timer) !== 'object') || (timer === null))
|
|
@@ -62,32 +62,32 @@ function isUnrefableTimer(timer: unknown,): timer is UnrefableTimer {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
/**
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
65
|
+
Builds the buffering stage shared by batch-persisting sinks: serialized
|
|
66
|
+
records accumulate and leave as one newline-joined JSONL batch through
|
|
67
|
+
`onFlush`. One uniform policy runs on every runtime; no per-runtime mode
|
|
68
|
+
exists.
|
|
69
|
+
|
|
70
|
+
A batch flushes synchronously from inside `add` when it reaches
|
|
71
|
+
{@link FLUSH_BUFFER_CAP_CHARS} or when the record's severity is `warn` or
|
|
72
|
+
worse, by timer after {@link FLUSH_DEADLINE_MS} of quiet, on `pagehide`
|
|
73
|
+
and on the document becoming hidden (where those events exist), and on
|
|
74
|
+
`drain`. The byte-cap and severity flushes run on the caller's stack, so
|
|
75
|
+
neither a synchronous workload nor a wedged main thread can accumulate
|
|
76
|
+
more than one cap of unhanded records. When an addition would breach the
|
|
77
|
+
cap, the existing entries flush first so an oversized record's downstream
|
|
78
|
+
failure can only ever drop that record, never its batch-mates.
|
|
79
|
+
|
|
80
|
+
@param onFlush - Backend handoff receiving each newline-joined batch;
|
|
81
|
+
called synchronously from whichever trigger fires, in record order.
|
|
82
|
+
|
|
83
|
+
@returns Buffer exposing `add` for records and `drain` for forced flushes.
|
|
84
|
+
|
|
85
|
+
@example
|
|
86
|
+
```ts
|
|
87
|
+
const buffer = createRecordBuffer({ onFlush: (batch) => store.persist(batch) });
|
|
88
|
+
buffer.add({ level: 'info', serialized: JSON.stringify(record) });
|
|
89
|
+
buffer.drain();
|
|
90
|
+
```
|
|
91
91
|
*/
|
|
92
92
|
export function createRecordBuffer(
|
|
93
93
|
{ onFlush, }: { readonly onFlush: (batch: string,) => void; },
|
|
@@ -99,16 +99,16 @@ export function createRecordBuffer(
|
|
|
99
99
|
readonly drain: () => void;
|
|
100
100
|
} {
|
|
101
101
|
/**
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
Serialized records awaiting one joined handoff; drained in add order by
|
|
103
|
+
every flush trigger.
|
|
104
104
|
*/
|
|
105
105
|
const entries: string[] = [];
|
|
106
106
|
|
|
107
107
|
/**
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
108
|
+
Instance-local buffer bookkeeping. `chars` mirrors the joined length of
|
|
109
|
+
{@link entries} (records plus one separator between neighbors) so cap
|
|
110
|
+
checks need no re-summing; `timer`, present only while armed, holds the
|
|
111
|
+
quiet-period deadline flush so idle sessions still hand off.
|
|
112
112
|
*/
|
|
113
113
|
const bufferState: {
|
|
114
114
|
chars: number;
|
|
@@ -116,30 +116,30 @@ export function createRecordBuffer(
|
|
|
116
116
|
} = { chars: 0, };
|
|
117
117
|
|
|
118
118
|
/**
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
119
|
+
Joined length the buffer would have after appending `serialized`,
|
|
120
|
+
counting the newline separator a non-empty buffer needs before it.
|
|
121
|
+
|
|
122
|
+
@param serialized - Record about to be appended.
|
|
123
|
+
|
|
124
|
+
@returns Prospective joined batch length in code units.
|
|
125
125
|
*/
|
|
126
126
|
function charsWith(serialized: string,): number {
|
|
127
127
|
/**
|
|
128
|
-
|
|
128
|
+
Newline separator the join adds before this record when the buffer already holds one.
|
|
129
129
|
*/
|
|
130
130
|
const separatorChars = (entries.length > 0) ? 1 : 0;
|
|
131
131
|
/**
|
|
132
|
-
|
|
132
|
+
Length of the buffer as currently joined, before this record.
|
|
133
133
|
*/
|
|
134
134
|
const joinedChars = bufferState.chars + separatorChars;
|
|
135
135
|
return joinedChars + serialized.length;
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
/**
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
139
|
+
Hands the buffered records to `onFlush` as one newline-joined batch and
|
|
140
|
+
disarms the deadline timer. Runs synchronously so byte-cap and severity
|
|
141
|
+
flushes complete on the caller's stack. Safe to call with an empty
|
|
142
|
+
buffer.
|
|
143
143
|
*/
|
|
144
144
|
function drain(): void {
|
|
145
145
|
if (bufferState.timer !== undefined) {
|
|
@@ -149,8 +149,8 @@ export function createRecordBuffer(
|
|
|
149
149
|
if (entries.length === 0)
|
|
150
150
|
return;
|
|
151
151
|
/**
|
|
152
|
-
|
|
153
|
-
|
|
152
|
+
Newline-joined JSONL batch; `JSON.stringify` escapes newlines inside
|
|
153
|
+
records, so the separator is unambiguous for readers splitting lines.
|
|
154
154
|
*/
|
|
155
155
|
const batch = entries.join('\n',);
|
|
156
156
|
entries.length = 0;
|
|
@@ -159,15 +159,15 @@ export function createRecordBuffer(
|
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
/**
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
162
|
+
Arms the quiet-period deadline flush if none is pending, releasing the
|
|
163
|
+
runtime's keep-alive where the handle supports it so a pending flush
|
|
164
|
+
never holds a process open.
|
|
165
165
|
*/
|
|
166
166
|
function scheduleDeadlineFlush(): void {
|
|
167
167
|
if (bufferState.timer !== undefined)
|
|
168
168
|
return;
|
|
169
169
|
/**
|
|
170
|
-
|
|
170
|
+
Freshly armed deadline handle; kept on {@link bufferState} so a cap or severity flush can disarm it.
|
|
171
171
|
*/
|
|
172
172
|
const timer = globalThis.setTimeout(
|
|
173
173
|
drain,
|
|
@@ -179,11 +179,11 @@ export function createRecordBuffer(
|
|
|
179
179
|
}
|
|
180
180
|
|
|
181
181
|
/**
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
182
|
+
Buffers one serialized record, flushing synchronously when the joined
|
|
183
|
+
batch reaches the byte cap or the record's severity is `warn` or worse.
|
|
184
|
+
|
|
185
|
+
@param entry - Serialized record plus the severity that decides an
|
|
186
|
+
immediate flush.
|
|
187
187
|
*/
|
|
188
188
|
function add(entry: {
|
|
189
189
|
readonly level: Level;
|
|
@@ -214,7 +214,7 @@ export function createRecordBuffer(
|
|
|
214
214
|
'visibilitychange',
|
|
215
215
|
function flushWhenHidden(): void {
|
|
216
216
|
/**
|
|
217
|
-
|
|
217
|
+
Current page visibility; the listener only fires where a document exists.
|
|
218
218
|
*/
|
|
219
219
|
const visibility = globalThis.document
|
|
220
220
|
?.visibilityState;
|
|
@@ -10,10 +10,10 @@ import {
|
|
|
10
10
|
} from '@monochromatic-dev/module-logger';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
Builds a capturing backend handoff so a test can assert exactly which
|
|
14
|
+
batches left the buffer and in what shape.
|
|
15
|
+
|
|
16
|
+
@returns Handoff function plus the captured batch list.
|
|
17
17
|
*/
|
|
18
18
|
function createCapturingFlush(): {
|
|
19
19
|
readonly batches: string[];
|
|
@@ -29,11 +29,11 @@ function createCapturingFlush(): {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
Installs a capturing `globalThis.addEventListener` (absent under Node, so
|
|
33
|
+
the buffer's lifecycle registration is otherwise a no-op there), restoring
|
|
34
|
+
the prior value when the returned guard leaves `using` scope.
|
|
35
|
+
|
|
36
|
+
@returns Disposable exposing captured handlers by event type.
|
|
37
37
|
*/
|
|
38
38
|
function installFakeGlobalListeners(): Disposable & {
|
|
39
39
|
readonly handlers: Map<string, () => void>;
|
|
@@ -55,12 +55,12 @@ function installFakeGlobalListeners(): Disposable & {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
58
|
+
Installs a fake `globalThis.document` (absent under Node) whose
|
|
59
|
+
`visibilityState` is controllable and whose `addEventListener` captures
|
|
60
|
+
handlers, restoring the prior value when the returned guard leaves `using`
|
|
61
|
+
scope.
|
|
62
|
+
|
|
63
|
+
@returns Disposable exposing captured handlers and mutable visibility.
|
|
64
64
|
*/
|
|
65
65
|
function installFakeDocument(): Disposable & {
|
|
66
66
|
readonly handlers: Map<string, () => void>;
|
|
@@ -90,12 +90,12 @@ function installFakeDocument(): Disposable & {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
/**
|
|
93
|
-
|
|
93
|
+
Severities the buffer must flush synchronously on `add`.
|
|
94
94
|
*/
|
|
95
95
|
const URGENT_LEVELS: readonly Level[] = ['warn', 'error', 'fatal',];
|
|
96
96
|
|
|
97
97
|
/**
|
|
98
|
-
|
|
98
|
+
Severities the buffer must keep buffered on `add`.
|
|
99
99
|
*/
|
|
100
100
|
const ROUTINE_LEVELS: readonly Level[] = ['trace', 'debug', 'info',];
|
|
101
101
|
|
|
@@ -211,7 +211,7 @@ await describe({
|
|
|
211
211
|
buffer.add({ level: 'debug', serialized: 'idle', },);
|
|
212
212
|
|
|
213
213
|
/**
|
|
214
|
-
|
|
214
|
+
Comfortably past the buffer's 250 ms quiet-period deadline.
|
|
215
215
|
*/
|
|
216
216
|
const pastDeadlineMs = 400;
|
|
217
217
|
await wait(pastDeadlineMs,);
|
|
@@ -244,7 +244,7 @@ await describe({
|
|
|
244
244
|
|
|
245
245
|
buffer.add({ level: 'info', serialized: 'leaving', },);
|
|
246
246
|
/**
|
|
247
|
-
|
|
247
|
+
Captured pagehide handler; the buffer must have registered one.
|
|
248
248
|
*/
|
|
249
249
|
const onPagehide = listeners.handlers
|
|
250
250
|
.get('pagehide',);
|
|
@@ -266,7 +266,7 @@ await describe({
|
|
|
266
266
|
|
|
267
267
|
buffer.add({ level: 'info', serialized: 'tabbed away', },);
|
|
268
268
|
/**
|
|
269
|
-
|
|
269
|
+
Captured visibilitychange handler; the buffer must have registered one.
|
|
270
270
|
*/
|
|
271
271
|
const onVisibilityChange = fakeDocument.handlers
|
|
272
272
|
.get('visibilitychange',);
|