@monochromatic-dev/module-logger 0.2.0 → 0.4.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 +53 -13
- package/dist/final/neutral/browser.d.mts +60 -0
- package/dist/final/neutral/browser.mjs +1 -0
- package/dist/final/neutral/index.d.mts +359 -596
- 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 +359 -596
- 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 -4
- package/src/artifact-platform-split.unit.test.ts +140 -0
- package/src/browser.ts +14 -0
- package/src/create-logger.ts +183 -183
- package/src/create-logger.unit.test.ts +112 -112
- package/src/default-sinks.neutral.ts +47 -0
- package/src/default-sinks.node.ts +43 -0
- package/src/error-format.ts +23 -23
- package/src/index.ts +1 -4
- package/src/logger.ts +76 -51
- package/src/node.ts +23 -0
- package/src/restricted-global-scope.unit.test.ts +101 -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/indexed-db.ts
CHANGED
|
@@ -11,47 +11,47 @@ import type {
|
|
|
11
11
|
} from '../types.ts';
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
|
|
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
|
-
|
|
19
|
+
Schema version; bump only with an upgrade path in `onupgradeneeded`.
|
|
20
20
|
*/
|
|
21
21
|
const DATABASE_VERSION = 1;
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
54
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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
|
-
|
|
104
|
+
Batch store within this transaction.
|
|
105
105
|
*/
|
|
106
106
|
const store = transaction.objectStore(BATCH_STORE,);
|
|
107
107
|
store.add(batch,);
|
|
108
108
|
/**
|
|
109
|
-
|
|
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
|
-
|
|
115
|
-
|
|
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
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
165
|
-
|
|
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
|
-
|
|
171
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
187
|
+
Connection kept for the sink's lifetime once the probe passes.
|
|
188
188
|
*/
|
|
189
189
|
const database = await openLogDatabase();
|
|
190
190
|
/**
|
|
191
|
-
|
|
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
|
-
|
|
198
|
+
Batch store within the probe transaction.
|
|
199
199
|
*/
|
|
200
200
|
const store = transaction.objectStore(BATCH_STORE,);
|
|
201
201
|
/**
|
|
202
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
308
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
16
|
-
|
|
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
|
-
|
|
21
|
+
Identity segments after the prefix: stamp, nonce, and index.
|
|
22
22
|
*/
|
|
23
23
|
const RUN_KEY_SEGMENTS = 3;
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
107
|
-
|
|
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
|
-
|
|
112
|
-
|
|
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
|
-
|
|
119
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
44
|
+
Key the builder produced for the identity.
|
|
45
45
|
*/
|
|
46
46
|
const built = buildLogKey(identity,);
|
|
47
47
|
/**
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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({
|