@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
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
2
|
+
Per-runtime default sessionStorage quota heuristics.
|
|
3
|
+
|
|
4
|
+
The Web Storage API exposes no way to read the sessionStorage quota (unlike
|
|
5
|
+
`navigator.storage.estimate()`, which reports the unrelated persistent-storage
|
|
6
|
+
budget), so the sink caps its own footprint from a table of measured
|
|
7
|
+
defaults. Each figure was fill-probed on a fresh store: values are written at
|
|
8
|
+
a growing single key until a `QuotaExceededError`, binary-searching the
|
|
9
|
+
largest that fits. Figures are UTF-16 code units (JS string length, counting
|
|
10
|
+
key plus value) because that is what sessionStorage measures and what the
|
|
11
|
+
sink compares `serialized.length` against.
|
|
12
|
+
|
|
13
|
+
@module
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import {
|
|
@@ -19,18 +19,18 @@ import {
|
|
|
19
19
|
} from './web-storage-runtime.ts';
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
22
|
+
Measured default per-origin sessionStorage quotas, in UTF-16 code units, one
|
|
23
|
+
bucket per detectable runtime:
|
|
24
|
+
|
|
25
|
+
- `deno`: 10 MiB on Deno 2.9.
|
|
26
|
+
- `node`: 5 MiB on Node 26.
|
|
27
|
+
- `browser`: 5 MiB, measured identical on Chromium, Firefox, and WebKit under
|
|
28
|
+
Playwright v1.61, so the three engines share one bucket and no fragile
|
|
29
|
+
user-agent sniffing is needed to tell them apart.
|
|
30
|
+
|
|
31
|
+
Bun 1.3 exposes no `sessionStorage`, so its bucket is uncapped: its sink
|
|
32
|
+
never verifies and never reaches the cap. An unrecognized runtime is also
|
|
33
|
+
uncapped so the caller relies on reactive eviction alone.
|
|
34
34
|
*/
|
|
35
35
|
const RUNTIME_QUOTA_CHARS: Record<WebStorageRuntime, number> = {
|
|
36
36
|
browser: 5_242_880,
|
|
@@ -41,16 +41,16 @@ const RUNTIME_QUOTA_CHARS: Record<WebStorageRuntime, number> = {
|
|
|
41
41
|
};
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
44
|
+
Detects the current runtime's default sessionStorage quota in UTF-16 code
|
|
45
|
+
units, or `Number.POSITIVE_INFINITY` when the runtime is unrecognized so the
|
|
46
|
+
caller leaves its footprint uncapped and relies on reactive eviction alone.
|
|
47
|
+
|
|
48
|
+
@returns Total quota in code units, or `Number.POSITIVE_INFINITY` if unknown.
|
|
49
|
+
|
|
50
|
+
@example
|
|
51
|
+
```ts
|
|
52
|
+
const capChars = detectSessionStorageQuotaChars() / 2; // half the total
|
|
53
|
+
```
|
|
54
54
|
*/
|
|
55
55
|
export function detectSessionStorageQuotaChars(): number {
|
|
56
56
|
return RUNTIME_QUOTA_CHARS[detectWebStorageRuntime()];
|
|
@@ -8,14 +8,14 @@ import {
|
|
|
8
8
|
} from '@monochromatic-dev/module-logger';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
11
|
+
Temporarily sets `globalThis` keys to the supplied values, restoring each to
|
|
12
|
+
its prior value (or deleting keys that were absent) when the returned guard
|
|
13
|
+
leaves `using` scope, so a runtime-detection test can impersonate Deno, Bun,
|
|
14
|
+
or a browser without leaking the fake globals into later tests.
|
|
15
|
+
|
|
16
|
+
@param overrides - Global keys to install for the duration of the scope.
|
|
17
|
+
|
|
18
|
+
@returns Disposable that restores the original globals on exit.
|
|
19
19
|
*/
|
|
20
20
|
function withGlobalOverrides(overrides: Record<string, unknown>,): Disposable {
|
|
21
21
|
const host = globalThis as unknown as Record<string, unknown>;
|
|
@@ -3,61 +3,61 @@ import { detectSessionStorageQuotaChars, } from './session-storage-quota.ts';
|
|
|
3
3
|
import { isQuotaExceededError, } from './web-storage-quota-error.ts';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
|
|
6
|
+
Prefix for sessionStorage keys to namespace log entries.
|
|
7
7
|
*/
|
|
8
8
|
const STORAGE_KEY_PREFIX = 'monochromatic.log';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
11
|
+
Builds the namespaced sessionStorage key for a log entry at `index`.
|
|
12
|
+
|
|
13
|
+
@param index - Zero-based slot number of an entry.
|
|
14
|
+
|
|
15
|
+
@returns Prefixed key such as `monochromatic.log.3`.
|
|
16
|
+
|
|
17
|
+
@example
|
|
18
|
+
```ts
|
|
19
|
+
storageKey(3); // 'monochromatic.log.3'
|
|
20
|
+
```
|
|
21
21
|
*/
|
|
22
22
|
function storageKey(index: number,): string {
|
|
23
23
|
return `${STORAGE_KEY_PREFIX}.${index}`;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
27
|
+
Builds the persistence engine behind the sessionStorage sink: each `persist`
|
|
28
|
+
lands one already-serialized batch under a counter-incremented key, with
|
|
29
|
+
proactive and reactive quota eviction. The counter lives in this instance's
|
|
30
|
+
closure (no module-global state), so independent sinks and tests never share
|
|
31
|
+
keys or need a reset hook.
|
|
32
|
+
|
|
33
|
+
The engine caps its own footprint at half the runtime's sessionStorage
|
|
34
|
+
quota, proactively dropping its oldest entries, and reactively drops them
|
|
35
|
+
again if the real store overflows; see {@link createSessionStorageStore.persist}.
|
|
36
|
+
|
|
37
|
+
@returns Engine exposing `persist` for one batch value per call.
|
|
38
|
+
|
|
39
|
+
@example
|
|
40
|
+
```ts
|
|
41
|
+
const store = createSessionStorageStore();
|
|
42
|
+
store.persist('{"level":"info","message":"hi","timestamp":0}');
|
|
43
|
+
```
|
|
44
44
|
*/
|
|
45
45
|
export function createSessionStorageStore(): { readonly persist: (batch: string,) => void; } {
|
|
46
46
|
/**
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
47
|
+
Instance-local write cursor, eviction watermark, and footprint tally.
|
|
48
|
+
`lineCounter` is the next slot to write and advances only when a `setItem`
|
|
49
|
+
actually lands, so this engine's present entries occupy the contiguous
|
|
50
|
+
range `[oldestIndex, lineCounter)`. `oldestIndex` is the lowest slot the
|
|
51
|
+
engine still owns; eviction removes that entry and climbs `oldestIndex`
|
|
52
|
+
toward `lineCounter`, so `oldestIndex < lineCounter` doubles as the "a
|
|
53
|
+
prior write succeeded and an owned entry remains" guard that keeps
|
|
54
|
+
eviction from ever touching another origin consumer's keys. `usedChars`
|
|
55
|
+
tracks the code units this engine currently occupies so the half-quota cap
|
|
56
|
+
needs no re-summing. `reportedFailure` gates the give-up diagnostic to
|
|
57
|
+
once per failure episode: a persistently full store (another writer owning
|
|
58
|
+
the space) would otherwise emit one `console.warn` per batch, so the flag
|
|
59
|
+
stays set until a write next lands, which re-arms a single report for the
|
|
60
|
+
next episode.
|
|
61
61
|
*/
|
|
62
62
|
const state: {
|
|
63
63
|
lineCounter: number;
|
|
@@ -72,27 +72,27 @@ export function createSessionStorageStore(): { readonly persist: (batch: string,
|
|
|
72
72
|
};
|
|
73
73
|
|
|
74
74
|
/**
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
75
|
+
Half the detected runtime sessionStorage quota, in UTF-16 code units, or
|
|
76
|
+
`Number.POSITIVE_INFINITY` on an unrecognized runtime. The engine keeps
|
|
77
|
+
its own footprint at or below this so the logger never claims more than
|
|
78
|
+
half the store, leaving the rest for the host application. An infinite cap
|
|
79
|
+
disables the proactive check, leaving only reactive quota-error eviction.
|
|
80
80
|
*/
|
|
81
81
|
const capChars = detectSessionStorageQuotaChars() / 2;
|
|
82
82
|
|
|
83
83
|
/**
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
84
|
+
Removes this engine's oldest still-present entry, advancing the watermark
|
|
85
|
+
and subtracting the reclaimed entry's code units from the running
|
|
86
|
+
footprint. Reading the value back before removal keeps `usedChars` honest
|
|
87
|
+
even if the entry drifted from what was written.
|
|
88
88
|
*/
|
|
89
89
|
function evictOldest(): void {
|
|
90
90
|
/**
|
|
91
|
-
|
|
91
|
+
Key of the oldest owned entry, removed to reclaim its slot and its space.
|
|
92
92
|
*/
|
|
93
93
|
const key = storageKey(state.oldestIndex,);
|
|
94
94
|
/**
|
|
95
|
-
|
|
95
|
+
Value being evicted, read back so its length can leave the footprint tally.
|
|
96
96
|
*/
|
|
97
97
|
const evicted = globalThis.sessionStorage
|
|
98
98
|
.getItem(key,);
|
|
@@ -107,26 +107,26 @@ export function createSessionStorageStore(): { readonly persist: (batch: string,
|
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
/**
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
110
|
+
Persists one serialized batch to sessionStorage under a
|
|
111
|
+
counter-incremented key.
|
|
112
|
+
|
|
113
|
+
First it proactively drops its own oldest entries so its footprint stays
|
|
114
|
+
at or below half the runtime's sessionStorage quota, leaving the rest for
|
|
115
|
+
the host application. It then writes, and on a quota overflow (the store
|
|
116
|
+
being fuller than the cap accounts for), and only while an owned entry
|
|
117
|
+
remains (so the reclaimed keys are its own, never another origin
|
|
118
|
+
consumer's), it drops its oldest still-present entry and retries until the
|
|
119
|
+
batch fits or nothing of its own remains to drop. A batch larger than the
|
|
120
|
+
whole quota therefore evicts every owned entry, then reports and gives up
|
|
121
|
+
rather than looping forever. A non-quota failure is reported without any
|
|
122
|
+
eviction. The sink only persists after verification, so no availability
|
|
123
|
+
guard is needed here.
|
|
124
|
+
|
|
125
|
+
@param batch - Serialized JSONL batch to persist.
|
|
126
126
|
*/
|
|
127
127
|
function persist(batch: string,): void {
|
|
128
128
|
/**
|
|
129
|
-
|
|
129
|
+
Code units this batch adds; the key's length is left out as a negligible near-constant.
|
|
130
130
|
*/
|
|
131
131
|
const batchChars = batch.length;
|
|
132
132
|
|
|
@@ -138,8 +138,8 @@ export function createSessionStorageStore(): { readonly persist: (batch: string,
|
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
/**
|
|
141
|
-
|
|
142
|
-
|
|
141
|
+
Write-attempt bound: one try for each entry still available to evict,
|
|
142
|
+
followed by one final try after every owned entry has been removed.
|
|
143
143
|
*/
|
|
144
144
|
const maxWriteAttempts = (state.lineCounter - state.oldestIndex) + 1;
|
|
145
145
|
for (let writeAttempt = 0; writeAttempt < maxWriteAttempts; writeAttempt++) {
|
|
@@ -8,32 +8,32 @@ import type {
|
|
|
8
8
|
} from '../types.ts';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
11
|
+
Verifies sessionStorage actually persists data. Stateless: the logger calls
|
|
12
|
+
this once per sink at startup and owns the resulting availability, so no
|
|
13
|
+
verified/available flag is kept here.
|
|
14
|
+
|
|
15
|
+
Election is by probe alone: any runtime whose `sessionStorage` round-trips
|
|
16
|
+
(browsers, Node 22+, Deno) keeps the sink, and the buffered write path
|
|
17
|
+
keeps the per-record cost acceptable everywhere rather than a runtime brand
|
|
18
|
+
check deciding who may log here.
|
|
19
|
+
|
|
20
|
+
@returns Whether sessionStorage is available and round-trips a probe write.
|
|
21
|
+
|
|
22
|
+
@example
|
|
23
|
+
```ts
|
|
24
|
+
if (await verifySessionStorage()) {
|
|
25
|
+
// sessionStorage usable
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
28
|
*/
|
|
29
29
|
function verifySessionStorage(): Promise<boolean> {
|
|
30
30
|
try {
|
|
31
31
|
/**
|
|
32
|
-
|
|
32
|
+
Sentinel key used only for the probe write/read; removed afterward to avoid polluting real log entries.
|
|
33
33
|
*/
|
|
34
34
|
const testKey = '__monochromatic_verify__';
|
|
35
35
|
/**
|
|
36
|
-
|
|
36
|
+
Timestamp-based probe value so concurrent verifications never read each other's writes.
|
|
37
37
|
*/
|
|
38
38
|
const testValue = `test-${Date.now()}`;
|
|
39
39
|
globalThis.sessionStorage
|
|
@@ -42,7 +42,7 @@ function verifySessionStorage(): Promise<boolean> {
|
|
|
42
42
|
testValue,
|
|
43
43
|
);
|
|
44
44
|
/**
|
|
45
|
-
|
|
45
|
+
Probe value read back from storage; equality with `testValue` proves writes actually persist.
|
|
46
46
|
*/
|
|
47
47
|
const readBack = globalThis.sessionStorage
|
|
48
48
|
.getItem(testKey,);
|
|
@@ -61,43 +61,43 @@ function verifySessionStorage(): Promise<boolean> {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
/**
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
64
|
+
Builds a sessionStorage sink that buffers serialized records through the
|
|
65
|
+
shared {@link createRecordBuffer} policy and persists each newline-joined
|
|
66
|
+
JSONL batch under a counter-incremented key through
|
|
67
|
+
{@link createSessionStorageStore}. One uniform write path runs on every
|
|
68
|
+
runtime; no per-runtime mode exists. Flush triggers (32 KiB in-write cap,
|
|
69
|
+
`warn`-or-worse severity, 250 ms quiet-period deadline, page lifecycle,
|
|
70
|
+
and the `flush` hook) are the buffer's; see {@link createRecordBuffer}.
|
|
71
|
+
|
|
72
|
+
@returns Sink backed by web `sessionStorage`.
|
|
73
|
+
|
|
74
|
+
@example
|
|
75
|
+
```ts
|
|
76
|
+
const { logger } = createLogger({ sinks: [createSessionStorageSink()] });
|
|
77
|
+
logger.info('user signed in'); // buffered
|
|
78
|
+
logger.warn('quota near'); // flushes both records in one batch
|
|
79
|
+
```
|
|
80
80
|
*/
|
|
81
81
|
export function createSessionStorageSink(): Sink {
|
|
82
82
|
/**
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
Persistence engine owning key allocation, footprint accounting, and quota
|
|
84
|
+
eviction; the buffer decides when a batch is handed to it.
|
|
85
85
|
*/
|
|
86
86
|
const store = createSessionStorageStore();
|
|
87
87
|
|
|
88
88
|
/**
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
Shared buffering stage; every flush trigger lands one joined batch in the
|
|
90
|
+
persistence engine synchronously.
|
|
91
91
|
*/
|
|
92
92
|
const buffer = createRecordBuffer({ onFlush: store.persist, },);
|
|
93
93
|
|
|
94
94
|
/**
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
95
|
+
Buffers a log record through the shared policy; see
|
|
96
|
+
{@link createRecordBuffer} for the flush triggers.
|
|
97
|
+
|
|
98
|
+
@param record - Log record to buffer and eventually persist.
|
|
99
|
+
|
|
100
|
+
@mutates record - `JSON.stringify` may invoke `toJSON`, getters, or proxy traps.
|
|
101
101
|
*/
|
|
102
102
|
function write(record: {
|
|
103
103
|
level: Level;
|
|
@@ -112,8 +112,8 @@ export function createSessionStorageSink(): Sink {
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
/**
|
|
115
|
-
|
|
116
|
-
|
|
115
|
+
Drains the buffer into the persistence engine; the drain is synchronous,
|
|
116
|
+
so the batch has landed by the time the resolved promise is observed.
|
|
117
117
|
*/
|
|
118
118
|
function flush(): Promise<void> {
|
|
119
119
|
buffer.drain();
|
|
@@ -11,20 +11,20 @@ import {
|
|
|
11
11
|
} from '@monochromatic-dev/module-logger';
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
|
|
14
|
+
Sink factories under test, read from the built artifact's `sinks` namespace.
|
|
15
15
|
*/
|
|
16
16
|
const {
|
|
17
17
|
createSessionStorageSink,
|
|
18
18
|
} = sinks;
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
Swaps `globalThis.sessionStorage` for `fake`, restoring the real backend when
|
|
22
|
+
the returned guard leaves `using` scope, so a fake never leaks into a later
|
|
23
|
+
test in the serial suite.
|
|
24
|
+
|
|
25
|
+
@param fake - Storage stand-in to install for the duration of the scope.
|
|
26
|
+
|
|
27
|
+
@returns Disposable that restores the original `sessionStorage` on exit.
|
|
28
28
|
*/
|
|
29
29
|
function installFakeStorage(fake: Storage,): Disposable {
|
|
30
30
|
const original = globalThis.sessionStorage;
|
|
@@ -37,14 +37,14 @@ function installFakeStorage(fake: Storage,): Disposable {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
40
|
+
Builds an in-memory `Storage` stand-in that rejects a `setItem` once stored
|
|
41
|
+
value lengths would exceed `byteBudget`, throwing the same
|
|
42
|
+
`QuotaExceededError` a real backend raises. Records every `removeItem` under
|
|
43
|
+
`removed` so a test can assert exactly which keys the sink evicted.
|
|
44
|
+
|
|
45
|
+
@param byteBudget - Total value length the store accepts before overflowing.
|
|
46
|
+
|
|
47
|
+
@returns Storage stand-in exposing the evicted-key log as `removed`.
|
|
48
48
|
*/
|
|
49
49
|
function createQuotaStorage(byteBudget: number,): Storage & { readonly removed: string[]; } {
|
|
50
50
|
const store = new Map<string, string>();
|
|
@@ -77,12 +77,12 @@ function createQuotaStorage(byteBudget: number,): Storage & { readonly removed:
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
/**
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
80
|
+
Builds an in-memory `Storage` stand-in whose first `setItem` succeeds and
|
|
81
|
+
every later one throws a non-quota error, so a test can prove the sink does
|
|
82
|
+
not evict for failures other than a quota overflow. Records `removeItem`
|
|
83
|
+
calls under `removed`.
|
|
84
|
+
|
|
85
|
+
@returns Storage stand-in exposing the evicted-key log as `removed`.
|
|
86
86
|
*/
|
|
87
87
|
function createFlakyStorage(): Storage & { readonly removed: string[]; } {
|
|
88
88
|
const store = new Map<string, string>();
|
|
@@ -107,10 +107,10 @@ function createFlakyStorage(): Storage & { readonly removed: string[]; } {
|
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
/**
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
110
|
+
Captures `console.warn` output, restoring the real method when the returned
|
|
111
|
+
guard leaves `using` scope, so a test can count the sink's give-up reports.
|
|
112
|
+
|
|
113
|
+
@returns Disposable exposing captured warn lines as `calls`.
|
|
114
114
|
*/
|
|
115
115
|
function spyConsoleWarn(): Disposable & { readonly calls: string[]; } {
|
|
116
116
|
const original = console.warn;
|
|
@@ -128,11 +128,11 @@ function spyConsoleWarn(): Disposable & { readonly calls: string[]; } {
|
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
/**
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
131
|
+
Counts captured warn lines that are the sessionStorage sink's give-up report.
|
|
132
|
+
|
|
133
|
+
@param calls - Captured `console.warn` lines from {@link spyConsoleWarn}.
|
|
134
|
+
|
|
135
|
+
@returns How many lines report a sink write failure.
|
|
136
136
|
*/
|
|
137
137
|
function sinkFailureCount(calls: readonly string[],): number {
|
|
138
138
|
return calls.filter(function isSinkFailure(line,) {
|
|
@@ -174,7 +174,7 @@ await describe({
|
|
|
174
174
|
await sink.verify();
|
|
175
175
|
|
|
176
176
|
/**
|
|
177
|
-
|
|
177
|
+
Routine record; severity below `warn` stays buffered.
|
|
178
178
|
*/
|
|
179
179
|
const record: LogRecord = {
|
|
180
180
|
level: 'info',
|
|
@@ -206,7 +206,7 @@ await describe({
|
|
|
206
206
|
await sink.verify();
|
|
207
207
|
|
|
208
208
|
/**
|
|
209
|
-
|
|
209
|
+
Routine record buffered first; must survive into the batch the warning triggers.
|
|
210
210
|
*/
|
|
211
211
|
const first: LogRecord = {
|
|
212
212
|
level: 'info',
|
|
@@ -214,7 +214,7 @@ await describe({
|
|
|
214
214
|
timestamp: 0,
|
|
215
215
|
};
|
|
216
216
|
/**
|
|
217
|
-
|
|
217
|
+
Warning record whose severity forces the synchronous flush.
|
|
218
218
|
*/
|
|
219
219
|
const second: LogRecord = {
|
|
220
220
|
level: 'warn',
|
|
@@ -244,7 +244,7 @@ await describe({
|
|
|
244
244
|
await sink.verify();
|
|
245
245
|
|
|
246
246
|
/**
|
|
247
|
-
|
|
247
|
+
Routine record left to the deadline timer.
|
|
248
248
|
*/
|
|
249
249
|
const record: LogRecord = {
|
|
250
250
|
level: 'debug',
|
|
@@ -254,7 +254,7 @@ await describe({
|
|
|
254
254
|
await sink.write(record,);
|
|
255
255
|
|
|
256
256
|
/**
|
|
257
|
-
|
|
257
|
+
Comfortably past the sink's 250 ms quiet-period deadline.
|
|
258
258
|
*/
|
|
259
259
|
const pastDeadlineMs = 400;
|
|
260
260
|
await wait(pastDeadlineMs,);
|
|
@@ -275,7 +275,7 @@ await describe({
|
|
|
275
275
|
await sink.verify();
|
|
276
276
|
|
|
277
277
|
/**
|
|
278
|
-
|
|
278
|
+
Small routine record buffered first; must not share a batch with the cap-breaching record.
|
|
279
279
|
*/
|
|
280
280
|
const small: LogRecord = {
|
|
281
281
|
level: 'info',
|
|
@@ -343,7 +343,7 @@ await describe({
|
|
|
343
343
|
name: 'caps its own footprint at half the runtime quota, proactively evicting oldest',
|
|
344
344
|
fn: async () => {
|
|
345
345
|
/**
|
|
346
|
-
|
|
346
|
+
Half the detected runtime quota: the footprint ceiling the engine enforces.
|
|
347
347
|
*/
|
|
348
348
|
const capChars = detectSessionStorageQuotaChars() / 2;
|
|
349
349
|
// A fake store far larger than the cap, so only the proactive half-quota
|
|
@@ -483,7 +483,7 @@ await describe({
|
|
|
483
483
|
using warn = spyConsoleWarn();
|
|
484
484
|
const sink = createSessionStorageSink();
|
|
485
485
|
/**
|
|
486
|
-
|
|
486
|
+
Record larger than the whole budget; unwritable even after eviction.
|
|
487
487
|
*/
|
|
488
488
|
const oversized = {
|
|
489
489
|
level: 'error' as const,
|