@monochromatic-dev/module-logger 0.2.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 (62) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +33 -8
  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 +360 -594
  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 +360 -594
  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 +18 -4
  16. package/src/artifact-platform-split.unit.test.ts +140 -0
  17. package/src/browser.ts +14 -0
  18. package/src/create-logger.ts +183 -183
  19. package/src/create-logger.unit.test.ts +112 -112
  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/logger.ts +23 -50
  24. package/src/node.ts +23 -0
  25. package/src/sink/console-control-chars.ts +64 -64
  26. package/src/sink/console-control-chars.unit.test.ts +14 -14
  27. package/src/sink/console.ts +194 -194
  28. package/src/sink/console.unit.test.ts +18 -18
  29. package/src/sink/file.ts +136 -140
  30. package/src/sink/file.unit.test.ts +19 -26
  31. package/src/sink/index.ts +4 -7
  32. package/src/sink/indexed-db-util.ts +42 -42
  33. package/src/sink/indexed-db.browser.test.ts +7 -7
  34. package/src/sink/indexed-db.ts +109 -109
  35. package/src/sink/indexed-db.unit.test.ts +5 -13
  36. package/src/sink/local-storage-key.ts +73 -73
  37. package/src/sink/local-storage-key.unit.test.ts +8 -8
  38. package/src/sink/local-storage-quota.ts +37 -37
  39. package/src/sink/local-storage-quota.unit.test.ts +8 -8
  40. package/src/sink/local-storage-store.ts +113 -113
  41. package/src/sink/local-storage-store.unit.test.ts +35 -35
  42. package/src/sink/local-storage.ts +72 -72
  43. package/src/sink/local-storage.unit.test.ts +27 -27
  44. package/src/sink/noop.ts +20 -20
  45. package/src/sink/noop.unit.test.ts +1 -1
  46. package/src/sink/opfs.browser.test.ts +7 -7
  47. package/src/sink/opfs.ts +62 -62
  48. package/src/sink/opfs.unit.test.ts +5 -13
  49. package/src/sink/record-buffer.ts +84 -84
  50. package/src/sink/record-buffer.unit.test.ts +20 -20
  51. package/src/sink/session-storage-quota.ts +34 -34
  52. package/src/sink/session-storage-quota.unit.test.ts +8 -8
  53. package/src/sink/session-storage-store.ts +72 -72
  54. package/src/sink/session-storage.ts +48 -48
  55. package/src/sink/session-storage.unit.test.ts +39 -39
  56. package/src/sink/web-storage-quota-error.ts +22 -22
  57. package/src/sink/web-storage-quota-error.unit.test.ts +2 -2
  58. package/src/sink/web-storage-runtime.ts +24 -24
  59. package/src/startup.unit.test.ts +18 -18
  60. package/src/tagged.ts +35 -35
  61. package/src/tagged.unit.test.ts +8 -8
  62. package/src/types.ts +39 -39
@@ -11,47 +11,47 @@ import type {
11
11
  } from '../types.ts';
12
12
 
13
13
  /**
14
- * Database holding this logger's batches, one per origin, shared by every tab.
14
+ Database holding this logger's batches, one per origin, shared by every tab.
15
15
  */
16
16
  const DATABASE_NAME = 'monochromatic.log';
17
17
 
18
18
  /**
19
- * Schema version; bump only with an upgrade path in `onupgradeneeded`.
19
+ Schema version; bump only with an upgrade path in `onupgradeneeded`.
20
20
  */
21
21
  const DATABASE_VERSION = 1;
22
22
 
23
23
  /**
24
- * Object store holding one newline-joined JSONL batch string per
25
- * auto-incremented key, so key order is arrival order across every tab and
26
- * retention can trim oldest-first without any run bookkeeping.
24
+ Object store holding one newline-joined JSONL batch string per
25
+ auto-incremented key, so key order is arrival order across every tab and
26
+ retention can trim oldest-first without any run bookkeeping.
27
27
  */
28
28
  const BATCH_STORE = 'batch';
29
29
 
30
30
  /**
31
- * Retention cap on stored batches, trimmed oldest-first inside each persist
32
- * transaction. At the buffer's 32 KiB flush cap this bounds the store near
33
- * 64 MiB, well under the multi-gigabyte origin quota
34
- * (`navigator.storage.estimate()` reported 10 GiB on the measuring machine)
35
- * while months of sessions still fit. A count cap instead of a byte tally
36
- * because severity-flushed batches vary in size and an exact byte budget
37
- * would need a cross-session tally re-summed at startup; the bound is
38
- * approximate by design.
31
+ Retention cap on stored batches, trimmed oldest-first inside each persist
32
+ transaction. At the buffer's 32 KiB flush cap this bounds the store near
33
+ 64 MiB, well under the multi-gigabyte origin quota
34
+ (`navigator.storage.estimate()` reported 10 GiB on the measuring machine)
35
+ while months of sessions still fit. A count cap instead of a byte tally
36
+ because severity-flushed batches vary in size and an exact byte budget
37
+ would need a cross-session tally re-summed at startup; the bound is
38
+ approximate by design.
39
39
  */
40
40
  const MAX_STORED_BATCHES = 2_048;
41
41
 
42
42
  /**
43
- * Opens (creating on first use) the logger's IndexedDB database with the
44
- * batch store ready.
45
- *
46
- * @returns Open database connection.
47
- *
48
- * @throws DOMException - When the backend refuses to open, for example in a
49
- * storage-partitioned context that denies IndexedDB.
43
+ Opens (creating on first use) the logger's IndexedDB database with the
44
+ batch store ready.
45
+
46
+ @returns Open database connection.
47
+
48
+ @throws DOMException - When the backend refuses to open, for example in a
49
+ storage-partitioned context that denies IndexedDB.
50
50
  */
51
51
  async function openLogDatabase(): Promise<IDBDatabase> {
52
52
  /**
53
- * Open request; the upgrade handler runs only when the database is new or
54
- * below {@link DATABASE_VERSION}.
53
+ Open request; the upgrade handler runs only when the database is new or
54
+ below {@link DATABASE_VERSION}.
55
55
  */
56
56
  const request = globalThis.indexedDB
57
57
  .open(
@@ -69,20 +69,20 @@ async function openLogDatabase(): Promise<IDBDatabase> {
69
69
  }
70
70
 
71
71
  /**
72
- * Persists one batch and trims the store back under the retention cap, all
73
- * inside one readwrite transaction so a crash between the steps cannot leave
74
- * the trim half-applied.
75
- *
76
- * @param database - Open connection from {@link openLogDatabase}.
77
- *
78
- * @param batch - Newline-joined JSONL batch string to persist.
79
- *
80
- * @throws DOMException - When the transaction errors or aborts, for example
81
- * under an origin-quota overflow.
82
- *
83
- * @mutates database - `database.transaction` opens a readwrite transaction,
84
- * registering live state on the host-owned connection, and the queued add
85
- * and trim change the store that connection controls.
72
+ Persists one batch and trims the store back under the retention cap, all
73
+ inside one readwrite transaction so a crash between the steps cannot leave
74
+ the trim half-applied.
75
+
76
+ @param database - Open connection from {@link openLogDatabase}.
77
+
78
+ @param batch - Newline-joined JSONL batch string to persist.
79
+
80
+ @throws DOMException - When the transaction errors or aborts, for example
81
+ under an origin-quota overflow.
82
+
83
+ @mutates database - `database.transaction` opens a readwrite transaction,
84
+ registering live state on the host-owned connection, and the queued add
85
+ and trim change the store that connection controls.
86
86
  */
87
87
  async function persistBatch(
88
88
  {
@@ -94,32 +94,32 @@ async function persistBatch(
94
94
  },
95
95
  ): Promise<void> {
96
96
  /**
97
- * Single transaction carrying the add, the count, and any trim.
97
+ Single transaction carrying the add, the count, and any trim.
98
98
  */
99
99
  const transaction = database.transaction(
100
100
  BATCH_STORE,
101
101
  'readwrite',
102
102
  );
103
103
  /**
104
- * Batch store within this transaction.
104
+ Batch store within this transaction.
105
105
  */
106
106
  const store = transaction.objectStore(BATCH_STORE,);
107
107
  store.add(batch,);
108
108
  /**
109
- * Stored batch count including the add queued in this transaction.
109
+ Stored batch count including the add queued in this transaction.
110
110
  */
111
111
  const count = await awaitRequest(store.count(),);
112
112
  if (count > MAX_STORED_BATCHES) {
113
113
  /**
114
- * Oldest keys past the cap; `getAllKeys` returns keys in ascending order,
115
- * which for an auto-incremented store is arrival order.
114
+ Oldest keys past the cap; `getAllKeys` returns keys in ascending order,
115
+ which for an auto-incremented store is arrival order.
116
116
  */
117
117
  const staleKeys = await awaitRequest(store.getAllKeys(
118
118
  null,
119
119
  count - MAX_STORED_BATCHES,
120
120
  ),);
121
121
  /**
122
- * Newest key still to be trimmed; everything at or below it goes.
122
+ Newest key still to be trimmed; everything at or below it goes.
123
123
  */
124
124
  const newestStale = staleKeys.at(-1,);
125
125
  if (newestStale !== undefined)
@@ -129,85 +129,85 @@ async function persistBatch(
129
129
  }
130
130
 
131
131
  /**
132
- * Builds an IndexedDB sink that buffers serialized records through the shared
133
- * {@link createRecordBuffer} policy and persists each newline-joined JSONL
134
- * batch as one string value per transaction, measured at 0.15 µs of
135
- * main-thread enqueue per record on headless Chromium 149 (one `add` per
136
- * 32 KiB batch). The connection lives in this instance's closure (no
137
- * module-global state), so independent loggers and tests never share a
138
- * handle or need a reset hook.
139
- *
140
- * Records are readable the moment their transaction settles (DevTools
141
- * Application tab included), survive tab close and browser restart, and
142
- * auto-incremented keys serialize across tabs, so no run-scoped naming is
143
- * needed. Retention trims oldest-first past {@link MAX_STORED_BATCHES}.
144
- * Transactions use the default relaxed durability: relaxed commits reach the
145
- * browser's storage backend promptly and survive renderer crashes, and the
146
- * OS-crash window `durability: 'strict'` would close is the rarest failure
147
- * class, not worth an fsync per batch.
148
- *
149
- * Flush triggers (32 KiB in-write cap, `warn`-or-worse severity, 250 ms
150
- * quiet-period deadline, page lifecycle, and the `flush` hook) are the
151
- * buffer's; see {@link createRecordBuffer}. The sink's `flush` hook awaits
152
- * every issued batch transaction before resolving.
153
- *
154
- * @returns Sink backed by IndexedDB.
155
- *
156
- * @example
157
- * ```ts
158
- * const { logger } = createLogger({ sinks: [createIndexedDbSink()] });
159
- * logger.warn('quota nearing limit');
160
- * ```
132
+ Builds an IndexedDB sink that buffers serialized records through the shared
133
+ {@link createRecordBuffer} policy and persists each newline-joined JSONL
134
+ batch as one string value per transaction, measured at 0.15 µs of
135
+ main-thread enqueue per record on headless Chromium 149 (one `add` per
136
+ 32 KiB batch). The connection lives in this instance's closure (no
137
+ module-global state), so independent loggers and tests never share a
138
+ handle or need a reset hook.
139
+
140
+ Records are readable the moment their transaction settles (DevTools
141
+ Application tab included), survive tab close and browser restart, and
142
+ auto-incremented keys serialize across tabs, so no run-scoped naming is
143
+ needed. Retention trims oldest-first past {@link MAX_STORED_BATCHES}.
144
+ Transactions use the default relaxed durability: relaxed commits reach the
145
+ browser's storage backend promptly and survive renderer crashes, and the
146
+ OS-crash window `durability: 'strict'` would close is the rarest failure
147
+ class, not worth an fsync per batch.
148
+
149
+ Flush triggers (32 KiB in-write cap, `warn`-or-worse severity, 250 ms
150
+ quiet-period deadline, page lifecycle, and the `flush` hook) are the
151
+ buffer's; see {@link createRecordBuffer}. The sink's `flush` hook awaits
152
+ every issued batch transaction before resolving.
153
+
154
+ @returns Sink backed by IndexedDB.
155
+
156
+ @example
157
+ ```ts
158
+ const { logger } = createLogger({ sinks: [createIndexedDbSink()] });
159
+ logger.warn('quota nearing limit');
160
+ ```
161
161
  */
162
162
  export function createIndexedDbSink(): Sink {
163
163
  /**
164
- * Instance-local open connection, set by `verify` and reused by every batch
165
- * write. Absent until a successful verification.
164
+ Instance-local open connection, set by `verify` and reused by every batch
165
+ write. Absent until a successful verification.
166
166
  */
167
167
  const state: { database?: IDBDatabase; } = {};
168
168
 
169
169
  /**
170
- * Batch transactions issued and not yet settled; the `flush` hook drains
171
- * this so logger-level `flush()` observes every issued batch.
170
+ Batch transactions issued and not yet settled; the `flush` hook drains
171
+ this so logger-level `flush()` observes every issued batch.
172
172
  */
173
173
  const pendingBatchWrites = new Set<Promise<void>>();
174
174
 
175
175
  /**
176
- * Verifies IndexedDB is available and round-trips a probe value, keeping
177
- * the opened connection for subsequent writes. The logger calls this once
178
- * and owns the resulting availability.
179
- *
180
- * @returns Whether IndexedDB logging is available.
176
+ Verifies IndexedDB is available and round-trips a probe value, keeping
177
+ the opened connection for subsequent writes. The logger calls this once
178
+ and owns the resulting availability.
179
+
180
+ @returns Whether IndexedDB logging is available.
181
181
  */
182
182
  async function verify(): Promise<boolean> {
183
183
  try {
184
184
  if ((typeof globalThis.indexedDB) === 'undefined')
185
185
  return false;
186
186
  /**
187
- * Connection kept for the sink's lifetime once the probe passes.
187
+ Connection kept for the sink's lifetime once the probe passes.
188
188
  */
189
189
  const database = await openLogDatabase();
190
190
  /**
191
- * Probe transaction: add, read back, and remove one sentinel value.
191
+ Probe transaction: add, read back, and remove one sentinel value.
192
192
  */
193
193
  const transaction = database.transaction(
194
194
  BATCH_STORE,
195
195
  'readwrite',
196
196
  );
197
197
  /**
198
- * Batch store within the probe transaction.
198
+ Batch store within the probe transaction.
199
199
  */
200
200
  const store = transaction.objectStore(BATCH_STORE,);
201
201
  /**
202
- * Timestamp-based probe value so concurrent verifications never read each other's writes.
202
+ Timestamp-based probe value so concurrent verifications never read each other's writes.
203
203
  */
204
204
  const probeValue = `probe-${Date.now()}`;
205
205
  /**
206
- * Key the store assigned to the probe, used to read it back and remove it.
206
+ Key the store assigned to the probe, used to read it back and remove it.
207
207
  */
208
208
  const probeKey = await awaitRequest(store.add(probeValue,),);
209
209
  /**
210
- * Probe value read back; equality proves the backend round-trips writes.
210
+ Probe value read back; equality proves the backend round-trips writes.
211
211
  */
212
212
  const readBack = await awaitRequest(store.get(probeKey,) as IDBRequest<unknown>,);
213
213
  store.delete(probeKey,);
@@ -229,10 +229,10 @@ export function createIndexedDbSink(): Sink {
229
229
  }
230
230
 
231
231
  /**
232
- * Writes one batch through {@link persistBatch}, swallowing and reporting
233
- * failures so the pending-write set always settles.
234
- *
235
- * @param batch - Newline-joined JSONL batch from the buffer.
232
+ Writes one batch through {@link persistBatch}, swallowing and reporting
233
+ failures so the pending-write set always settles.
234
+
235
+ @param batch - Newline-joined JSONL batch from the buffer.
236
236
  */
237
237
  async function writeBatch(batch: string,): Promise<void> {
238
238
  if (!state.database)
@@ -253,10 +253,10 @@ export function createIndexedDbSink(): Sink {
253
253
  }
254
254
 
255
255
  /**
256
- * Removes a tracked batch write from {@link pendingBatchWrites} once it
257
- * settles.
258
- *
259
- * @param pending - Promise returned by {@link writeBatch}.
256
+ Removes a tracked batch write from {@link pendingBatchWrites} once it
257
+ settles.
258
+
259
+ @param pending - Promise returned by {@link writeBatch}.
260
260
  */
261
261
  async function removePendingWhenSettled(pending: Promise<void>,): Promise<void> {
262
262
  await pending;
@@ -264,14 +264,14 @@ export function createIndexedDbSink(): Sink {
264
264
  }
265
265
 
266
266
  /**
267
- * Backend handoff for the buffer: issues the batch transaction without
268
- * awaiting and tracks it for the `flush` hook.
269
- *
270
- * @param batch - Newline-joined JSONL batch from the buffer.
267
+ Backend handoff for the buffer: issues the batch transaction without
268
+ awaiting and tracks it for the `flush` hook.
269
+
270
+ @param batch - Newline-joined JSONL batch from the buffer.
271
271
  */
272
272
  function handOffBatch(batch: string,): void {
273
273
  /**
274
- * In-flight batch write; never rejects, because {@link writeBatch} reports internally.
274
+ In-flight batch write; never rejects, because {@link writeBatch} reports internally.
275
275
  */
276
276
  const pending = writeBatch(batch,);
277
277
  pendingBatchWrites.add(pending,);
@@ -279,17 +279,17 @@ export function createIndexedDbSink(): Sink {
279
279
  }
280
280
 
281
281
  /**
282
- * Shared buffering stage; every flush trigger issues one batch transaction.
282
+ Shared buffering stage; every flush trigger issues one batch transaction.
283
283
  */
284
284
  const buffer = createRecordBuffer({ onFlush: handOffBatch, },);
285
285
 
286
286
  /**
287
- * Buffers a log record through the shared policy; see
288
- * {@link createRecordBuffer} for the flush triggers.
289
- *
290
- * @param record - Log record to buffer and eventually persist.
291
- *
292
- * @mutates record - `JSON.stringify` may invoke `toJSON`, getters, or proxy traps.
287
+ Buffers a log record through the shared policy; see
288
+ {@link createRecordBuffer} for the flush triggers.
289
+
290
+ @param record - Log record to buffer and eventually persist.
291
+
292
+ @mutates record - `JSON.stringify` may invoke `toJSON`, getters, or proxy traps.
293
293
  */
294
294
  function write(record: {
295
295
  level: Level;
@@ -304,13 +304,13 @@ export function createIndexedDbSink(): Sink {
304
304
  }
305
305
 
306
306
  /**
307
- * Drains the buffer into the store and resolves once every issued batch
308
- * transaction has settled.
307
+ Drains the buffer into the store and resolves once every issued batch
308
+ transaction has settled.
309
309
  */
310
310
  async function flush(): Promise<void> {
311
311
  buffer.drain();
312
312
  /**
313
- * Snapshot of in-flight batch writes at drain time.
313
+ Snapshot of in-flight batch writes at drain time.
314
314
  */
315
315
  const writes = [...pendingBatchWrites,];
316
316
  await Promise.all(writes,);
@@ -3,16 +3,7 @@ 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
- createIndexedDbSink,
15
- } = sinks;
6
+ import { createIndexedDbSink, } from '@monochromatic-dev/module-logger/browser';
16
7
 
17
8
  // Node, Deno, and Bun expose no `indexedDB` (probed on Node 26, Deno 2.9,
18
9
  // Bun 1.3), so this file exercises the unavailable-backend fallback that the
@@ -20,7 +11,8 @@ const {
20
11
  // short-circuits on the missing global, and drained batches hit the
21
12
  // unset-connection guard. The available path lives in
22
13
  // `indexed-db.browser.test.ts`; the shared buffering policy is covered in
23
- // `record-buffer.unit.test.ts`.
14
+ // `record-buffer.unit.test.ts`. The factory is reached through the
15
+ // `./browser` subpath because the root entry no longer exports it.
24
16
  await describe({
25
17
  name: 'IndexedDB sink (node fallback)',
26
18
  children: [
@@ -40,7 +32,7 @@ await describe({
40
32
  // flush trigger fires.
41
33
  const sink = createIndexedDbSink();
42
34
  /**
43
- * Resolved write result; the sink write contract is `Promise<void>`.
35
+ Resolved write result; the sink write contract is `Promise<void>`.
44
36
  */
45
37
  const result = await sink.write({
46
38
  level: 'info',
@@ -69,7 +61,7 @@ await describe({
69
61
  timestamp: 1,
70
62
  },);
71
63
  /**
72
- * Resolved flush result; must settle even with no connection to write to.
64
+ Resolved flush result; must settle even with no connection to write to.
73
65
  */
74
66
  const result = await sink.flush?.();
75
67
  expect(result,)
@@ -1,30 +1,30 @@
1
1
  /**
2
- * Key naming for the localStorage persistence engine.
3
- *
4
- * localStorage is shared by every tab of the origin and survives restarts, so
5
- * unlike the per-tab sessionStorage keys (`monochromatic.log.{n}`), these keys
6
- * carry a run identity: `monochromatic.log.{stamp}.{nonce}.{index}`. The stamp
7
- * orders runs oldest-first for cross-run eviction, the nonce keeps two tabs
8
- * started in the same millisecond from colliding, and the index orders batches
9
- * within a run.
10
- *
11
- * @module
2
+ Key naming for the localStorage persistence engine.
3
+
4
+ localStorage is shared by every tab of the origin and survives restarts, so
5
+ unlike the per-tab sessionStorage keys (`monochromatic.log.{n}`), these keys
6
+ carry a run identity: `monochromatic.log.{stamp}.{nonce}.{index}`. The stamp
7
+ orders runs oldest-first for cross-run eviction, the nonce keeps two tabs
8
+ started in the same millisecond from colliding, and the index orders batches
9
+ within a run.
10
+
11
+ @module
12
12
  */
13
13
 
14
14
  /**
15
- * Prefix namespacing this logger's localStorage entries away from host
16
- * application keys.
15
+ Prefix namespacing this logger's localStorage entries away from host
16
+ application keys.
17
17
  */
18
18
  export const LOCAL_STORAGE_KEY_PREFIX = 'monochromatic.log';
19
19
 
20
20
  /**
21
- * Identity segments after the prefix: stamp, nonce, and index.
21
+ Identity segments after the prefix: stamp, nonce, and index.
22
22
  */
23
23
  const RUN_KEY_SEGMENTS = 3;
24
24
 
25
25
  /**
26
- * Parsed identity of one owned localStorage entry, used to order eviction
27
- * across runs.
26
+ Parsed identity of one owned localStorage entry, used to order eviction
27
+ across runs.
28
28
  */
29
29
  export type ParsedLogKey = {
30
30
  readonly key: string;
@@ -34,13 +34,13 @@ export type ParsedLogKey = {
34
34
  };
35
35
 
36
36
  /**
37
- * Reports whether `text` is one or more ASCII digits, so key parsing accepts
38
- * only counter-shaped segments and never claims a host application's key. A
39
- * linear scan instead of a regex: the rule is a plain character-range check.
40
- *
41
- * @param text - Candidate key segment.
42
- *
43
- * @returns Whether every character is an ASCII digit and one exists.
37
+ Reports whether `text` is one or more ASCII digits, so key parsing accepts
38
+ only counter-shaped segments and never claims a host application's key. A
39
+ linear scan instead of a regex: the rule is a plain character-range check.
40
+
41
+ @param text - Candidate key segment.
42
+
43
+ @returns Whether every character is an ASCII digit and one exists.
44
44
  */
45
45
  function isDigits(text: string,): boolean {
46
46
  if (text.length === 0)
@@ -53,20 +53,20 @@ function isDigits(text: string,): boolean {
53
53
  }
54
54
 
55
55
  /**
56
- * Builds the namespaced localStorage key for one batch slot of one run.
57
- *
58
- * @param stamp - Run creation time ordering runs oldest-first.
59
- *
60
- * @param nonce - Same-millisecond disambiguator between concurrent tabs.
61
- *
62
- * @param index - Zero-based batch slot within the run.
63
- *
64
- * @returns Key such as `monochromatic.log.1753000000000.a1b2.3`.
65
- *
66
- * @example
67
- * ```ts
68
- * buildLogKey({ stamp: 1753000000000, nonce: 'a1b2', index: 3 });
69
- * ```
56
+ Builds the namespaced localStorage key for one batch slot of one run.
57
+
58
+ @param stamp - Run creation time ordering runs oldest-first.
59
+
60
+ @param nonce - Same-millisecond disambiguator between concurrent tabs.
61
+
62
+ @param index - Zero-based batch slot within the run.
63
+
64
+ @returns Key such as `monochromatic.log.1753000000000.a1b2.3`.
65
+
66
+ @example
67
+ ```ts
68
+ buildLogKey({ stamp: 1753000000000, nonce: 'a1b2', index: 3 });
69
+ ```
70
70
  */
71
71
  export function buildLogKey(
72
72
  {
@@ -83,40 +83,40 @@ export function buildLogKey(
83
83
  }
84
84
 
85
85
  /**
86
- * Parses a localStorage key back into its run identity, or reports it foreign
87
- * by leaving `parsed` absent. Parsing is strict (exact prefix, exactly the
88
- * identity segment count, digit-shaped stamp and index, non-empty nonce)
89
- * because eviction trusts this to never classify a host application's key, or
90
- * the sessionStorage sink's flat `monochromatic.log.{n}` shape, as evictable.
91
- *
92
- * @param key - Candidate localStorage key.
93
- *
94
- * @returns Wrapper whose `parsed` property is present only for an owned key.
95
- *
96
- * @example
97
- * ```ts
98
- * parseLogKey('monochromatic.log.1753000000000.a1b2.3').parsed; // ParsedLogKey
99
- * parseLogKey('monochromatic.log.5').parsed; // undefined: sessionStorage shape
100
- * ```
86
+ Parses a localStorage key back into its run identity, or reports it foreign
87
+ by leaving `parsed` absent. Parsing is strict (exact prefix, exactly the
88
+ identity segment count, digit-shaped stamp and index, non-empty nonce)
89
+ because eviction trusts this to never classify a host application's key, or
90
+ the sessionStorage sink's flat `monochromatic.log.{n}` shape, as evictable.
91
+
92
+ @param key - Candidate localStorage key.
93
+
94
+ @returns Wrapper whose `parsed` property is present only for an owned key.
95
+
96
+ @example
97
+ ```ts
98
+ parseLogKey('monochromatic.log.1753000000000.a1b2.3').parsed; // ParsedLogKey
99
+ parseLogKey('monochromatic.log.5').parsed; // undefined: sessionStorage shape
100
+ ```
101
101
  */
102
102
  export function parseLogKey(key: string,): { readonly parsed?: ParsedLogKey; } {
103
103
  if (!key.startsWith(`${LOCAL_STORAGE_KEY_PREFIX}.`,))
104
104
  return {};
105
105
  /**
106
- * Key remainder past the prefix and its trailing dot, holding the run
107
- * identity segments.
106
+ Key remainder past the prefix and its trailing dot, holding the run
107
+ identity segments.
108
108
  */
109
109
  const rest = key.slice(LOCAL_STORAGE_KEY_PREFIX.length + 1,);
110
110
  /**
111
- * Dot-separated identity segments; exactly stamp, nonce, index for an owned
112
- * key.
111
+ Dot-separated identity segments; exactly stamp, nonce, index for an owned
112
+ key.
113
113
  */
114
114
  const segments = rest.split('.',);
115
115
  if (segments.length !== RUN_KEY_SEGMENTS)
116
116
  return {};
117
117
  /**
118
- * Identity segments in declaration order; any missing or malformed one
119
- * makes the key foreign.
118
+ Identity segments in declaration order; any missing or malformed one
119
+ makes the key foreign.
120
120
  */
121
121
  const [stampText, nonce, indexText,] = segments;
122
122
  if (
@@ -142,22 +142,22 @@ export function parseLogKey(key: string,): { readonly parsed?: ParsedLogKey; } {
142
142
  }
143
143
 
144
144
  /**
145
- * Orders parsed keys oldest-first for eviction: by run stamp, then by nonce
146
- * (an arbitrary but stable tiebreak between same-millisecond runs), then by
147
- * batch index within the run.
148
- *
149
- * @param first - Parsed key compared first.
150
- *
151
- * @param second - Parsed key compared second.
152
- *
153
- * @returns Negative when `first` is older, positive when newer, zero on ties.
154
- *
155
- * @example
156
- * ```ts
157
- * entries.toSorted(function byOldestFirst(first, second) {
158
- * return compareLogKeys({ first, second });
159
- * });
160
- * ```
145
+ Orders parsed keys oldest-first for eviction: by run stamp, then by nonce
146
+ (an arbitrary but stable tiebreak between same-millisecond runs), then by
147
+ batch index within the run.
148
+
149
+ @param first - Parsed key compared first.
150
+
151
+ @param second - Parsed key compared second.
152
+
153
+ @returns Negative when `first` is older, positive when newer, zero on ties.
154
+
155
+ @example
156
+ ```ts
157
+ entries.toSorted(function byOldestFirst(first, second) {
158
+ return compareLogKeys({ first, second });
159
+ });
160
+ ```
161
161
  */
162
162
  export function compareLogKeys(
163
163
  {
@@ -10,9 +10,9 @@ import {
10
10
  } from '@monochromatic-dev/module-logger';
11
11
 
12
12
  /**
13
- * Keys the strict parser must reject: host application keys, the
14
- * sessionStorage sink's flat shape, and malformed run identities. Eviction
15
- * safety rests on every one of these staying foreign.
13
+ Keys the strict parser must reject: host application keys, the
14
+ sessionStorage sink's flat shape, and malformed run identities. Eviction
15
+ safety rests on every one of these staying foreign.
16
16
  */
17
17
  const FOREIGN_KEYS: readonly string[] = [
18
18
  'other.key',
@@ -33,7 +33,7 @@ await describe({
33
33
  name: 'round-trips a built key back to its identity',
34
34
  fn: async () => {
35
35
  /**
36
- * Identity pushed through build-then-parse; equality proves the pair inverse.
36
+ Identity pushed through build-then-parse; equality proves the pair inverse.
37
37
  */
38
38
  const identity = {
39
39
  stamp: 1_753_000_000_000,
@@ -41,11 +41,11 @@ await describe({
41
41
  index: 7,
42
42
  };
43
43
  /**
44
- * Key the builder produced for the identity.
44
+ Key the builder produced for the identity.
45
45
  */
46
46
  const built = buildLogKey(identity,);
47
47
  /**
48
- * Identity parsed back out of the built key.
48
+ Identity parsed back out of the built key.
49
49
  */
50
50
  const { parsed, } = parseLogKey(built,);
51
51
  expect(parsed,)
@@ -70,7 +70,7 @@ await describe({
70
70
  name: 'compareLogKeys orders by stamp, then nonce, then index',
71
71
  fn: async () => {
72
72
  /**
73
- * Keys deliberately shuffled across all three ordering dimensions.
73
+ Keys deliberately shuffled across all three ordering dimensions.
74
74
  */
75
75
  const shuffled = [
76
76
  'monochromatic.log.2000.aaaa.0',
@@ -83,7 +83,7 @@ await describe({
83
83
  return (parsed === undefined) ? [] : [parsed,];
84
84
  },);
85
85
  /**
86
- * Oldest-first ordering the eviction queue relies on.
86
+ Oldest-first ordering the eviction queue relies on.
87
87
  */
88
88
  const sorted = shuffled.toSorted(function byOldestFirst(first, second,) {
89
89
  return compareLogKeys({