@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.
- package/CHANGELOG.md +11 -0
- package/README.md +33 -8
- package/dist/final/neutral/browser.d.mts +60 -0
- package/dist/final/neutral/browser.mjs +1 -0
- package/dist/final/neutral/index.d.mts +360 -594
- 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 +360 -594
- 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 +18 -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 +34 -0
- package/src/default-sinks.node.ts +32 -0
- package/src/error-format.ts +23 -23
- 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 localStorage quota heuristics.
|
|
3
|
+
|
|
4
|
+
The Web Storage API exposes no way to read the localStorage 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 localStorage measures and what the sink
|
|
11
|
+
compares `serialized.length` against.
|
|
12
|
+
|
|
13
|
+
@module
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import {
|
|
@@ -19,21 +19,21 @@ import {
|
|
|
19
19
|
} from './web-storage-runtime.ts';
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
22
|
+
Measured default per-origin localStorage quotas, in UTF-16 code units, one
|
|
23
|
+
bucket per detectable runtime:
|
|
24
|
+
|
|
25
|
+
- `deno`: 10,477,569 on Deno 2.9, which lands 8 KiB short of the 10 MiB its
|
|
26
|
+
sessionStorage measures, presumably backing-store overhead; the measured
|
|
27
|
+
figure is kept as-is rather than rounded up past what actually fits.
|
|
28
|
+
- `node`: 5 MiB on Node 26 launched with `--localstorage-file`.
|
|
29
|
+
- `browser`: 5 MiB, measured on headless Chromium 149 over an
|
|
30
|
+
`http://127.0.0.1` origin. Firefox and WebKit are assumed to share the
|
|
31
|
+
bucket: both measured 5 MiB for sessionStorage under Playwright v1.61 and
|
|
32
|
+
neither was fill-probed for localStorage here.
|
|
33
|
+
|
|
34
|
+
Bun 1.3 exposes no `localStorage`, so its bucket is uncapped: its sink never
|
|
35
|
+
verifies and never reaches the cap. An unrecognized runtime is also uncapped
|
|
36
|
+
so the caller relies on reactive eviction alone.
|
|
37
37
|
*/
|
|
38
38
|
const RUNTIME_QUOTA_CHARS: Record<WebStorageRuntime, number> = {
|
|
39
39
|
browser: 5_242_880,
|
|
@@ -44,16 +44,16 @@ const RUNTIME_QUOTA_CHARS: Record<WebStorageRuntime, number> = {
|
|
|
44
44
|
};
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
47
|
+
Detects the current runtime's default localStorage quota in UTF-16 code
|
|
48
|
+
units, or `Number.POSITIVE_INFINITY` when the runtime is unrecognized so the
|
|
49
|
+
caller leaves its footprint uncapped and relies on reactive eviction alone.
|
|
50
|
+
|
|
51
|
+
@returns Total quota in code units, or `Number.POSITIVE_INFINITY` if unknown.
|
|
52
|
+
|
|
53
|
+
@example
|
|
54
|
+
```ts
|
|
55
|
+
const capChars = detectLocalStorageQuotaChars() / 2; // half the total
|
|
56
|
+
```
|
|
57
57
|
*/
|
|
58
58
|
export function detectLocalStorageQuotaChars(): number {
|
|
59
59
|
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>;
|
|
@@ -9,61 +9,61 @@ import { detectLocalStorageQuotaChars, } from './local-storage-quota.ts';
|
|
|
9
9
|
import { isQuotaExceededError, } from './web-storage-quota-error.ts';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
Radix for the run nonce so `Number.prototype.toString` yields compact
|
|
13
|
+
alphanumerics.
|
|
14
14
|
*/
|
|
15
15
|
const NONCE_RADIX = 36;
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
Length of the run nonce; four base-36 characters make a same-millisecond
|
|
19
|
+
collision between two tabs vanishingly unlikely while keeping keys short.
|
|
20
20
|
*/
|
|
21
21
|
const NONCE_LENGTH = 4;
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
One adopted prior-run entry: its parsed identity plus the value length it
|
|
25
|
+
occupies, captured once at adoption so eviction needs no re-read.
|
|
26
26
|
*/
|
|
27
27
|
type PriorEntry = ParsedLogKey & { readonly chars: number; };
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
30
|
+
Builds the persistence engine behind the localStorage sink: each `persist`
|
|
31
|
+
lands one already-serialized batch under a run-scoped counter-incremented
|
|
32
|
+
key, with proactive and reactive quota eviction. Run identity and counters
|
|
33
|
+
live in this instance's closure (no module-global state), so independent
|
|
34
|
+
sinks and tests never share keys or need a reset hook.
|
|
35
|
+
|
|
36
|
+
Unlike sessionStorage, localStorage is shared by every tab of the origin and
|
|
37
|
+
survives restarts, so this engine differs from the sessionStorage engine in
|
|
38
|
+
two ways. Keys carry a run identity (see `local-storage-key.ts`), so
|
|
39
|
+
concurrent tabs never collide on a counter. And on its first persist the
|
|
40
|
+
engine adopts every strictly-parsed entry left by other runs into its
|
|
41
|
+
footprint tally, evicting those oldest-first before its own entries;
|
|
42
|
+
without that, leftovers from dead sessions would fill the store until no
|
|
43
|
+
run could ever write again. Adoption is deferred to first persist rather
|
|
44
|
+
than construction so building the default sink set never touches
|
|
45
|
+
`globalThis.localStorage` on runtimes where the sink never verifies (plain
|
|
46
|
+
Node warns on mere access). Keys that fail the strict parse, including the
|
|
47
|
+
host application's, are never counted and never evicted.
|
|
48
|
+
|
|
49
|
+
The engine caps its own footprint (adopted entries included) at half the
|
|
50
|
+
runtime's localStorage quota, proactively dropping oldest-first, and
|
|
51
|
+
reactively drops again if the real store still overflows; see
|
|
52
|
+
{@link createLocalStorageStore.persist}.
|
|
53
|
+
|
|
54
|
+
@returns Engine exposing `persist` for one batch value per call.
|
|
55
|
+
|
|
56
|
+
@example
|
|
57
|
+
```ts
|
|
58
|
+
const store = createLocalStorageStore();
|
|
59
|
+
store.persist('{"level":"info","message":"hi","timestamp":0}');
|
|
60
|
+
```
|
|
61
61
|
*/
|
|
62
62
|
export function createLocalStorageStore(): { readonly persist: (batch: string,) => void; } {
|
|
63
63
|
/**
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
Identity of this run, embedded in every key this engine writes: the stamp
|
|
65
|
+
orders runs for cross-run eviction and the nonce keeps two tabs started
|
|
66
|
+
in the same millisecond apart.
|
|
67
67
|
*/
|
|
68
68
|
const runIdentity = {
|
|
69
69
|
stamp: Date.now(),
|
|
@@ -80,14 +80,14 @@ export function createLocalStorageStore(): { readonly persist: (batch: string,)
|
|
|
80
80
|
};
|
|
81
81
|
|
|
82
82
|
/**
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
83
|
+
Instance-local write cursor, eviction watermark, and footprint tally,
|
|
84
|
+
mirroring the sessionStorage engine: this run's own entries occupy the
|
|
85
|
+
contiguous index range `[oldestIndex, lineCounter)` and `usedChars`
|
|
86
|
+
tracks the code units the engine accounts for (adopted prior-run entries
|
|
87
|
+
included) so the half-quota cap needs no re-summing. `reportedFailure`
|
|
88
|
+
gates the give-up diagnostic to once per failure episode, re-armed by the
|
|
89
|
+
next landed write. `adoptedPrior` defers the prior-run scan to the first
|
|
90
|
+
persist, which only happens after verification.
|
|
91
91
|
*/
|
|
92
92
|
const state: {
|
|
93
93
|
lineCounter: number;
|
|
@@ -104,10 +104,10 @@ export function createLocalStorageStore(): { readonly persist: (batch: string,)
|
|
|
104
104
|
};
|
|
105
105
|
|
|
106
106
|
/**
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
107
|
+
Prior-run entries adopted at first persist, sorted oldest-first, with a
|
|
108
|
+
cursor marking how far eviction has consumed them; entries before the
|
|
109
|
+
cursor are already removed. Prior entries always evict before this run's
|
|
110
|
+
own, since they predate everything this run writes.
|
|
111
111
|
*/
|
|
112
112
|
const prior: {
|
|
113
113
|
entries: readonly PriorEntry[];
|
|
@@ -118,21 +118,21 @@ export function createLocalStorageStore(): { readonly persist: (batch: string,)
|
|
|
118
118
|
};
|
|
119
119
|
|
|
120
120
|
/**
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
121
|
+
Half the detected runtime localStorage quota, in UTF-16 code units, or
|
|
122
|
+
`Number.POSITIVE_INFINITY` on an unrecognized runtime. The engine keeps
|
|
123
|
+
its accounted footprint at or below this so the logger never claims more
|
|
124
|
+
than half the store, leaving the rest for the host application. An
|
|
125
|
+
infinite cap disables the proactive check, leaving only reactive
|
|
126
|
+
quota-error eviction.
|
|
127
127
|
*/
|
|
128
128
|
const capChars = detectLocalStorageQuotaChars() / 2;
|
|
129
129
|
|
|
130
130
|
/**
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
131
|
+
Builds this run's key for a batch slot.
|
|
132
|
+
|
|
133
|
+
@param index - Zero-based batch slot within this run.
|
|
134
|
+
|
|
135
|
+
@returns Run-scoped namespaced key.
|
|
136
136
|
*/
|
|
137
137
|
function ownKey(index: number,): string {
|
|
138
138
|
return buildLogKey({
|
|
@@ -143,32 +143,32 @@ export function createLocalStorageStore(): { readonly persist: (batch: string,)
|
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
/**
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
146
|
+
Scans localStorage once for strictly-parsed entries left by other runs,
|
|
147
|
+
sorts them oldest-first for eviction, and adds their lengths to the
|
|
148
|
+
footprint tally. Entries another tab writes after this scan are invisible
|
|
149
|
+
to the tally; the reactive quota loop covers that staleness.
|
|
150
150
|
*/
|
|
151
151
|
function adoptPriorEntries(): void {
|
|
152
152
|
/**
|
|
153
|
-
|
|
154
|
-
|
|
153
|
+
Entry count at scan time; enumeration is by index because `Storage`
|
|
154
|
+
exposes no iterator.
|
|
155
155
|
*/
|
|
156
156
|
const total = globalThis.localStorage
|
|
157
157
|
.length;
|
|
158
158
|
/**
|
|
159
|
-
|
|
159
|
+
Strictly-parsed foreign-run entries found by the scan, unsorted.
|
|
160
160
|
*/
|
|
161
161
|
const found: PriorEntry[] = [];
|
|
162
162
|
for (let slot = 0; slot < total; slot++) {
|
|
163
163
|
/**
|
|
164
|
-
|
|
164
|
+
Key at this enumeration slot; `null` past the end under concurrent removal.
|
|
165
165
|
*/
|
|
166
166
|
const key = globalThis.localStorage
|
|
167
167
|
.key(slot,);
|
|
168
168
|
if (key === null)
|
|
169
169
|
continue;
|
|
170
170
|
/**
|
|
171
|
-
|
|
171
|
+
Parsed run identity, absent for any key the engine must not touch.
|
|
172
172
|
*/
|
|
173
173
|
const { parsed, } = parseLogKey(key,);
|
|
174
174
|
if (parsed === undefined)
|
|
@@ -176,7 +176,7 @@ export function createLocalStorageStore(): { readonly persist: (batch: string,)
|
|
|
176
176
|
if ((parsed.stamp === runIdentity.stamp) && (parsed.nonce === runIdentity.nonce))
|
|
177
177
|
continue;
|
|
178
178
|
/**
|
|
179
|
-
|
|
179
|
+
Stored batch, read so its length enters the footprint tally.
|
|
180
180
|
*/
|
|
181
181
|
const value = globalThis.localStorage
|
|
182
182
|
.getItem(key,);
|
|
@@ -208,14 +208,14 @@ export function createLocalStorageStore(): { readonly persist: (batch: string,)
|
|
|
208
208
|
}
|
|
209
209
|
|
|
210
210
|
/**
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
211
|
+
Reports whether anything remains this engine may evict: an adopted
|
|
212
|
+
prior-run entry past the cursor, or one of this run's own entries.
|
|
213
|
+
|
|
214
|
+
@returns Whether an eviction call would reclaim something.
|
|
215
215
|
*/
|
|
216
216
|
function hasEvictable(): boolean {
|
|
217
217
|
/**
|
|
218
|
-
|
|
218
|
+
Count of adopted prior-run entries; those before the cursor are gone.
|
|
219
219
|
*/
|
|
220
220
|
const priorCount = prior.entries
|
|
221
221
|
.length;
|
|
@@ -224,17 +224,17 @@ export function createLocalStorageStore(): { readonly persist: (batch: string,)
|
|
|
224
224
|
}
|
|
225
225
|
|
|
226
226
|
/**
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
227
|
+
Removes the oldest not-yet-evicted adopted prior-run entry, if one
|
|
228
|
+
remains. Its length leaves the tally from the adoption snapshot: prior
|
|
229
|
+
keys are never rewritten (counters only advance), so a re-read could only
|
|
230
|
+
observe the same value or a concurrent removal, and in both cases the
|
|
231
|
+
snapshot is what the tally counted.
|
|
232
|
+
|
|
233
|
+
@returns Whether a prior-run entry was evicted.
|
|
234
234
|
*/
|
|
235
235
|
function evictOldestPrior(): boolean {
|
|
236
236
|
/**
|
|
237
|
-
|
|
237
|
+
Oldest remaining adopted entry, or `undefined` when all are consumed.
|
|
238
238
|
*/
|
|
239
239
|
const entry = prior.entries[prior.cursor];
|
|
240
240
|
if (entry === undefined)
|
|
@@ -250,18 +250,18 @@ export function createLocalStorageStore(): { readonly persist: (batch: string,)
|
|
|
250
250
|
}
|
|
251
251
|
|
|
252
252
|
/**
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
253
|
+
Removes this run's oldest still-present entry, advancing the watermark
|
|
254
|
+
and subtracting the reclaimed entry's code units from the running
|
|
255
|
+
footprint. Reading the value back before removal keeps `usedChars` honest
|
|
256
|
+
even if the entry drifted from what was written.
|
|
257
257
|
*/
|
|
258
258
|
function evictOldestOwn(): void {
|
|
259
259
|
/**
|
|
260
|
-
|
|
260
|
+
Key of the oldest owned entry, removed to reclaim its slot and its space.
|
|
261
261
|
*/
|
|
262
262
|
const key = ownKey(state.oldestIndex,);
|
|
263
263
|
/**
|
|
264
|
-
|
|
264
|
+
Value being evicted, read back so its length can leave the footprint tally.
|
|
265
265
|
*/
|
|
266
266
|
const evicted = globalThis.localStorage
|
|
267
267
|
.getItem(key,);
|
|
@@ -276,9 +276,9 @@ export function createLocalStorageStore(): { readonly persist: (batch: string,)
|
|
|
276
276
|
}
|
|
277
277
|
|
|
278
278
|
/**
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
279
|
+
Evicts the single oldest thing the engine still owns: adopted prior-run
|
|
280
|
+
entries first (they predate everything this run wrote), then this run's
|
|
281
|
+
own oldest. Callers guard with {@link hasEvictable}.
|
|
282
282
|
*/
|
|
283
283
|
function evictOldest(): void {
|
|
284
284
|
if (evictOldestPrior())
|
|
@@ -288,21 +288,21 @@ export function createLocalStorageStore(): { readonly persist: (batch: string,)
|
|
|
288
288
|
}
|
|
289
289
|
|
|
290
290
|
/**
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
291
|
+
Persists one serialized batch to localStorage under this run's next
|
|
292
|
+
counter-incremented key.
|
|
293
|
+
|
|
294
|
+
The first call adopts prior-run entries into the footprint tally. Each
|
|
295
|
+
call then proactively evicts oldest-first (prior runs before this run's
|
|
296
|
+
own) until the accounted footprint fits under half the runtime's
|
|
297
|
+
localStorage quota, writes, and on a quota overflow (the store being
|
|
298
|
+
fuller than the tally accounts for, such as another live tab writing
|
|
299
|
+
concurrently) evicts and retries until the batch fits or nothing owned
|
|
300
|
+
remains to drop. A batch larger than the whole quota therefore evicts
|
|
301
|
+
everything owned, then reports and gives up rather than looping forever.
|
|
302
|
+
A non-quota failure is reported without any eviction. The sink only
|
|
303
|
+
persists after verification, so no availability guard is needed here.
|
|
304
|
+
|
|
305
|
+
@param batch - Serialized JSONL batch to persist.
|
|
306
306
|
*/
|
|
307
307
|
function persist(batch: string,): void {
|
|
308
308
|
if (!state.adoptedPrior) {
|
|
@@ -311,7 +311,7 @@ export function createLocalStorageStore(): { readonly persist: (batch: string,)
|
|
|
311
311
|
}
|
|
312
312
|
|
|
313
313
|
/**
|
|
314
|
-
|
|
314
|
+
Code units this batch adds; the key's length is left out as a negligible near-constant.
|
|
315
315
|
*/
|
|
316
316
|
const batchChars = batch.length;
|
|
317
317
|
|
|
@@ -323,8 +323,8 @@ export function createLocalStorageStore(): { readonly persist: (batch: string,)
|
|
|
323
323
|
}
|
|
324
324
|
|
|
325
325
|
/**
|
|
326
|
-
|
|
327
|
-
|
|
326
|
+
Write-attempt bound: one try for each entry still available to evict,
|
|
327
|
+
followed by one final try after every owned entry has been removed.
|
|
328
328
|
*/
|
|
329
329
|
const maxWriteAttempts = (prior
|
|
330
330
|
.entries
|
|
@@ -9,20 +9,20 @@ import {
|
|
|
9
9
|
} from '@monochromatic-dev/module-logger';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
One mebibyte of UTF-16 code units; the node cap under test is half the
|
|
13
|
+
measured 5 MiB quota, so multi-mebibyte batches drive eviction.
|
|
14
14
|
*/
|
|
15
15
|
const MIB = 1_048_576;
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
18
|
+
Installs `fake` as `globalThis.localStorage` via the property descriptor
|
|
19
|
+
(plain assignment would call Node's phantom setter-less path on some hosts),
|
|
20
|
+
restoring the original descriptor, or removing the property when none
|
|
21
|
+
existed, when the returned guard leaves `using` scope.
|
|
22
|
+
|
|
23
|
+
@param fake - Storage stand-in to install for the duration of the scope.
|
|
24
|
+
|
|
25
|
+
@returns Disposable that restores the original `localStorage` on exit.
|
|
26
26
|
*/
|
|
27
27
|
function installFakeLocalStorage(fake: Storage,): Disposable {
|
|
28
28
|
const original = Object.getOwnPropertyDescriptor(globalThis, 'localStorage',);
|
|
@@ -41,16 +41,16 @@ function installFakeLocalStorage(fake: Storage,): Disposable {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
44
|
+
Builds an in-memory `Storage` stand-in with full enumeration support (the
|
|
45
|
+
engine's adoption scan walks `length`/`key`), rejecting a `setItem` once
|
|
46
|
+
stored value lengths would exceed `quotaChars`, throwing the same
|
|
47
|
+
`QuotaExceededError` a real backend raises. Records every `removeItem`
|
|
48
|
+
under `removed` so a test can assert exactly which keys were evicted.
|
|
49
|
+
|
|
50
|
+
@param quotaChars - Total value length the store accepts before
|
|
51
|
+
overflowing; omitted means unlimited.
|
|
52
|
+
|
|
53
|
+
@returns Storage stand-in exposing `removed` and the raw `backing` map.
|
|
54
54
|
*/
|
|
55
55
|
function createFakeStorage(
|
|
56
56
|
{ quotaChars, }: { readonly quotaChars?: number; } = {},
|
|
@@ -98,12 +98,12 @@ function createFakeStorage(
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
/**
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
101
|
+
Builds an in-memory `Storage` stand-in whose first `setItem` succeeds and
|
|
102
|
+
every later one throws a non-quota error, so a test can prove the engine
|
|
103
|
+
does not evict for failures other than a quota overflow. Enumeration is
|
|
104
|
+
supported for the adoption scan; `removeItem` calls land in `removed`.
|
|
105
|
+
|
|
106
|
+
@returns Storage stand-in exposing the evicted-key log as `removed`.
|
|
107
107
|
*/
|
|
108
108
|
function createFlakyStorage(): Storage & { readonly removed: string[]; } {
|
|
109
109
|
const backing = new Map<string, string>();
|
|
@@ -134,11 +134,11 @@ function createFlakyStorage(): Storage & { readonly removed: string[]; } {
|
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
/**
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
137
|
+
Captures `console.warn` output, restoring the real method when the returned
|
|
138
|
+
guard leaves `using` scope, so a test can count the engine's give-up
|
|
139
|
+
reports.
|
|
140
|
+
|
|
141
|
+
@returns Disposable exposing captured warn lines as `calls`.
|
|
142
142
|
*/
|
|
143
143
|
function spyConsoleWarn(): Disposable & { readonly calls: string[]; } {
|
|
144
144
|
const original = console.warn;
|
|
@@ -171,7 +171,7 @@ await describe({
|
|
|
171
171
|
store.persist('beta',);
|
|
172
172
|
|
|
173
173
|
/**
|
|
174
|
-
|
|
174
|
+
Parsed identities of every landed key; both must carry one shared run identity.
|
|
175
175
|
*/
|
|
176
176
|
const parsed = [...fake.backing.keys(),]
|
|
177
177
|
.flatMap((key,) => {
|
|
@@ -236,13 +236,13 @@ await describe({
|
|
|
236
236
|
expect(fake.removed,)
|
|
237
237
|
.toHaveLength(1,);
|
|
238
238
|
/**
|
|
239
|
-
|
|
239
|
+
Identity of the evicted key; it must be this run's slot zero.
|
|
240
240
|
*/
|
|
241
241
|
const { parsed: evicted, } = parseLogKey(fake.removed[0] ?? '',);
|
|
242
242
|
expect(evicted?.index,)
|
|
243
243
|
.toBe(0,);
|
|
244
244
|
/**
|
|
245
|
-
|
|
245
|
+
Indices still present after eviction, in insertion order.
|
|
246
246
|
*/
|
|
247
247
|
const remaining = [...fake.backing.keys(),]
|
|
248
248
|
.flatMap((key,) => {
|
|
@@ -272,7 +272,7 @@ await describe({
|
|
|
272
272
|
expect(fake.removed,)
|
|
273
273
|
.toEqual(['monochromatic.log.1000.aaaa.0',],);
|
|
274
274
|
/**
|
|
275
|
-
|
|
275
|
+
Landed batch values after the retry; only the new batch remains.
|
|
276
276
|
*/
|
|
277
277
|
const values = [...fake.backing.values(),];
|
|
278
278
|
expect(values,)
|