@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.
Files changed (63) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +58 -12
  3. package/dist/final/neutral/browser.d.mts +60 -0
  4. package/dist/final/neutral/browser.mjs +1 -0
  5. package/dist/final/neutral/index.d.mts +366 -572
  6. package/dist/final/neutral/index.mjs +2 -3
  7. package/dist/final/neutral/indexed-db-hsIfv7Cv.mjs +2 -0
  8. package/dist/final/neutral/types-BkkBXgY3.d.mts +76 -0
  9. package/dist/final/node/file-CRGb1hDK.mjs +1 -0
  10. package/dist/final/node/index.d.mts +366 -572
  11. package/dist/final/node/index.mjs +3 -3
  12. package/dist/final/node/node.d.mts +103 -0
  13. package/dist/final/node/node.mjs +1 -0
  14. package/dist/final/node/types-BkkBXgY3.d.mts +76 -0
  15. package/package.json +19 -5
  16. package/src/artifact-platform-split.unit.test.ts +140 -0
  17. package/src/browser.ts +14 -0
  18. package/src/create-logger.ts +249 -151
  19. package/src/create-logger.unit.test.ts +527 -75
  20. package/src/default-sinks.neutral.ts +34 -0
  21. package/src/default-sinks.node.ts +32 -0
  22. package/src/error-format.ts +23 -23
  23. package/src/index.ts +2 -0
  24. package/src/logger.ts +23 -50
  25. package/src/node.ts +23 -0
  26. package/src/sink/console-control-chars.ts +64 -64
  27. package/src/sink/console-control-chars.unit.test.ts +14 -14
  28. package/src/sink/console.ts +194 -194
  29. package/src/sink/console.unit.test.ts +18 -18
  30. package/src/sink/file.ts +136 -140
  31. package/src/sink/file.unit.test.ts +19 -26
  32. package/src/sink/index.ts +4 -7
  33. package/src/sink/indexed-db-util.ts +42 -42
  34. package/src/sink/indexed-db.browser.test.ts +7 -7
  35. package/src/sink/indexed-db.ts +109 -109
  36. package/src/sink/indexed-db.unit.test.ts +5 -13
  37. package/src/sink/local-storage-key.ts +73 -73
  38. package/src/sink/local-storage-key.unit.test.ts +8 -8
  39. package/src/sink/local-storage-quota.ts +37 -37
  40. package/src/sink/local-storage-quota.unit.test.ts +8 -8
  41. package/src/sink/local-storage-store.ts +113 -113
  42. package/src/sink/local-storage-store.unit.test.ts +35 -35
  43. package/src/sink/local-storage.ts +72 -72
  44. package/src/sink/local-storage.unit.test.ts +27 -27
  45. package/src/sink/noop.ts +20 -20
  46. package/src/sink/noop.unit.test.ts +1 -1
  47. package/src/sink/opfs.browser.test.ts +7 -7
  48. package/src/sink/opfs.ts +62 -62
  49. package/src/sink/opfs.unit.test.ts +5 -13
  50. package/src/sink/record-buffer.ts +84 -84
  51. package/src/sink/record-buffer.unit.test.ts +20 -20
  52. package/src/sink/session-storage-quota.ts +34 -34
  53. package/src/sink/session-storage-quota.unit.test.ts +8 -8
  54. package/src/sink/session-storage-store.ts +72 -72
  55. package/src/sink/session-storage.ts +48 -48
  56. package/src/sink/session-storage.unit.test.ts +39 -39
  57. package/src/sink/web-storage-quota-error.ts +22 -22
  58. package/src/sink/web-storage-quota-error.unit.test.ts +2 -2
  59. package/src/sink/web-storage-runtime.ts +24 -24
  60. package/src/startup.unit.test.ts +18 -18
  61. package/src/tagged.ts +35 -35
  62. package/src/tagged.unit.test.ts +8 -8
  63. 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
- * 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
- * ```
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
- * Instance-local kept-open OPFS stream, opened by `verify` and reused by
34
- * every batch write. Absent until a successful verification.
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
- * Batch writes issued to the stream and not yet settled; the `flush` hook
40
- * drains this so logger-level `flush()` observes every issued batch.
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
- * 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.
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
- * Origin Private File System directory handle that hosts every monochromatic log file.
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
- * ISO timestamp with colons replaced by dashes so it can be embedded in a cross-platform file name.
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
- * OPFS handle for the per-run log file, created on first verification and reused for subsequent writes.
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
- * Throwaway writable used only to flush the probe so the next `getFile` returns persisted content.
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
- * Probe record written and read back to confirm OPFS round-trips writes.
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
- * File snapshot of the probe, taken after closing `probeWritable` so its bytes are flushed.
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
- * Probe contents read back; matching the literal `"test":true` proves OPFS persisted the data.
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
- * Whether the probe round-tripped; only then is the reused stream opened.
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
- * OPFS storage object, present only when the current platform exposes the backend this sink verifies.
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
- * 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.
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
- * Removes a tracked batch write from {@link pendingBatchWrites} once it
144
- * settles.
145
- *
146
- * @param pending - Promise returned by {@link writeBatch}.
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
- * 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.
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
- * In-flight batch write; never rejects, because {@link writeBatch} reports internally.
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
- * Shared buffering stage; every flush trigger issues one queued stream
170
- * write per joined batch.
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
- * 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.
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
- * Drains the buffer onto the stream and resolves once every issued batch
196
- * write has settled.
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
- * Snapshot of in-flight batch writes at drain time.
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
- * Resolved write result; the sink write contract is `Promise<void>`.
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
- * Resolved flush result; must settle even with no stream to write to.
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
- * 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.
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
- * 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.
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
- * 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.
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
- * 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.
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
- * 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`.
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
- * 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
- * ```
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
- * Serialized records awaiting one joined handoff; drained in add order by
103
- * every flush trigger.
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
- * 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.
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
- * 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.
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
- * Newline separator the join adds before this record when the buffer already holds one.
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
- * Length of the buffer as currently joined, before this record.
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
- * 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.
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
- * Newline-joined JSONL batch; `JSON.stringify` escapes newlines inside
153
- * records, so the separator is unambiguous for readers splitting lines.
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
- * 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.
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
- * Freshly armed deadline handle; kept on {@link bufferState} so a cap or severity flush can disarm it.
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
- * 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.
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
- * Current page visibility; the listener only fires where a document exists.
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
- * 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.
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
- * 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.
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
- * 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.
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
- * Severities the buffer must flush synchronously on `add`.
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
- * Severities the buffer must keep buffered on `add`.
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
- * Comfortably past the buffer's 250 ms quiet-period deadline.
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
- * Captured pagehide handler; the buffer must have registered one.
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
- * Captured visibilitychange handler; the buffer must have registered one.
269
+ Captured visibilitychange handler; the buffer must have registered one.
270
270
  */
271
271
  const onVisibilityChange = fakeDocument.handlers
272
272
  .get('visibilitychange',);