@monochromatic-dev/module-logger 0.1.0 → 0.2.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 +16 -0
- package/README.md +25 -4
- package/dist/final/neutral/index.d.mts +32 -4
- package/dist/final/neutral/index.mjs +3 -3
- package/dist/final/node/index.d.mts +32 -4
- package/dist/final/node/index.mjs +3 -3
- package/package.json +3 -3
- package/src/create-logger.ts +115 -17
- package/src/create-logger.unit.test.ts +464 -12
- package/src/index.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @monochromatic-dev/module-logger
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Records logged before every sink has answered its verify now buffer under `STARTUP_BUFFER_CAP` (10000,
|
|
8
|
+
exported).
|
|
9
|
+
On overflow the oldest buffered record is dropped,
|
|
10
|
+
and once initialization completes one `warn` record naming the dropped count is written to every available sink.
|
|
11
|
+
Commit `fee2427`.
|
|
12
|
+
- Sinks now verify concurrently,
|
|
13
|
+
each under a time limit.
|
|
14
|
+
A backend probe that never answers no longer starves the sinks after it or keeps the logger from initializing.
|
|
15
|
+
The limit is the new `createLogger` option `verifyTimeoutMs` (default `DEFAULT_VERIFY_TIMEOUT_MS`,
|
|
16
|
+
5000 ms).
|
|
17
|
+
Commit `ee58222`.
|
|
18
|
+
|
|
3
19
|
## 0.1.0
|
|
4
20
|
|
|
5
21
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -89,9 +89,19 @@ already provides its own log-level filtering.
|
|
|
89
89
|
## Sinks
|
|
90
90
|
|
|
91
91
|
The default logger writes to **all** available sinks simultaneously.
|
|
92
|
-
Availability is verified at module load
|
|
93
|
-
|
|
92
|
+
Availability is verified at module load,
|
|
93
|
+
every sink concurrently,
|
|
94
|
+
each under its own time limit (`verifyTimeoutMs`,
|
|
95
|
+
default `DEFAULT_VERIFY_TIMEOUT_MS`,
|
|
96
|
+
5000 ms);
|
|
97
|
+
a sink that does not answer in time counts as unavailable,
|
|
98
|
+
so one hung backend probe cannot starve the others.
|
|
99
|
+
Records emitted while an async sink is
|
|
94
100
|
still being verified are replayed to that sink when it becomes available.
|
|
101
|
+
That startup buffer holds at most `STARTUP_BUFFER_CAP` records (10000,
|
|
102
|
+
exported);
|
|
103
|
+
past the cap the oldest buffered record is dropped,
|
|
104
|
+
and once every sink has answered one `warn` record naming the dropped count is written to every available sink.
|
|
95
105
|
|
|
96
106
|
- **console**:
|
|
97
107
|
formats as `[level] [ISO timestamp] message`;
|
|
@@ -256,9 +266,15 @@ File,
|
|
|
256
266
|
- Throws at log time once initialization has completed with no available backend.
|
|
257
267
|
The console sink verifies wherever `console` and `queueMicrotask` exist,
|
|
258
268
|
so this is reachable only through `createLogger` with sinks that all fail verification
|
|
259
|
-
- A sink is dropped when its `verify` reports unavailable
|
|
260
|
-
|
|
269
|
+
- A sink is dropped when its `verify` reports unavailable,
|
|
270
|
+
runs past `verifyTimeoutMs`,
|
|
271
|
+
or its flush hook rejects;
|
|
272
|
+
remaining sinks continue,
|
|
273
|
+
and a late verify answer after the limit is ignored
|
|
261
274
|
- Individual `write` failures are handled per sink and do not disable the backend
|
|
275
|
+
- Records logged before every sink has answered buffer under `STARTUP_BUFFER_CAP`;
|
|
276
|
+
on overflow the oldest is dropped and the count is reported as one `warn` record after initialization,
|
|
277
|
+
never silently
|
|
262
278
|
|
|
263
279
|
## Custom loggers
|
|
264
280
|
|
|
@@ -286,6 +302,7 @@ import { createLogger, sinks, } from '@monochromatic-dev/module-logger';
|
|
|
286
302
|
const { logger, } = createLogger({
|
|
287
303
|
sinks: [sinks.createFileSink(),],
|
|
288
304
|
flushDeadlineMs: 30_000,
|
|
305
|
+
verifyTimeoutMs: 30_000,
|
|
289
306
|
},);
|
|
290
307
|
```
|
|
291
308
|
|
|
@@ -332,6 +349,10 @@ See [DECISIONS.md](DECISIONS.md) for rationale on:
|
|
|
332
349
|
- localStorage and IndexedDB sink designs and measurements
|
|
333
350
|
- Console output neutralizes control characters
|
|
334
351
|
- `flush()` has a deadline
|
|
352
|
+
- Sinks verify concurrently under a time limit
|
|
353
|
+
- The startup buffer is bounded and overflow is reported
|
|
354
|
+
- Zero-config at import,
|
|
355
|
+
no configure step (the logtape migration observations)
|
|
335
356
|
|
|
336
357
|
## Source files
|
|
337
358
|
|
|
@@ -85,6 +85,25 @@ type Logger = {
|
|
|
85
85
|
* {@link createLogger}.
|
|
86
86
|
*/
|
|
87
87
|
export declare const DEFAULT_FLUSH_DEADLINE_MS = 5e3;
|
|
88
|
+
/**
|
|
89
|
+
* Default per-sink `verify()` time limit in milliseconds. Measured on
|
|
90
|
+
* 2026-09-06: the default logger's five shipped verifies complete together in
|
|
91
|
+
* about 2.4 ms locally, so this leaves three orders of magnitude for a slow
|
|
92
|
+
* but working backend probe (a network filesystem, a busy IndexedDB) while a
|
|
93
|
+
* verify that never answers (a hung mount, an IndexedDB open blocked by
|
|
94
|
+
* another tab) can no longer stall startup. Override per logger through the
|
|
95
|
+
* `verifyTimeoutMs` option of {@link createLogger}.
|
|
96
|
+
*/
|
|
97
|
+
export declare const DEFAULT_VERIFY_TIMEOUT_MS = 5e3;
|
|
98
|
+
/**
|
|
99
|
+
* Most records the logger buffers before its sinks have verified. Startup
|
|
100
|
+
* lasts at most {@link DEFAULT_VERIFY_TIMEOUT_MS}, so this bounds the memory a
|
|
101
|
+
* burst during that window can claim; on overflow the oldest buffered record
|
|
102
|
+
* is dropped so the newest (usually most diagnostic) context survives, and
|
|
103
|
+
* one synthetic `warn` record naming the dropped count is written to every
|
|
104
|
+
* available sink once initialization completes.
|
|
105
|
+
*/
|
|
106
|
+
export declare const STARTUP_BUFFER_CAP = 1e4;
|
|
88
107
|
/**
|
|
89
108
|
* Builds a multi-sink logger over the supplied sink adapters. All
|
|
90
109
|
* orchestration (per-sink availability, startup buffering and replay,
|
|
@@ -94,9 +113,14 @@ export declare const DEFAULT_FLUSH_DEADLINE_MS = 5e3;
|
|
|
94
113
|
*
|
|
95
114
|
* Verification runs eagerly at construction and never blocks callers:
|
|
96
115
|
* records emitted while an async sink is still verifying buffer internally
|
|
97
|
-
* and replay to that sink the moment it verifies.
|
|
98
|
-
*
|
|
99
|
-
*
|
|
116
|
+
* and replay to that sink the moment it verifies. Every sink verifies
|
|
117
|
+
* concurrently under its own time limit (`verifyTimeoutMs`, default
|
|
118
|
+
* {@link DEFAULT_VERIFY_TIMEOUT_MS}), so one backend that never answers
|
|
119
|
+
* cannot starve the others or keep the logger from initializing. A sink
|
|
120
|
+
* whose `verify` resolves `false`, throws, or runs past the limit is dropped
|
|
121
|
+
* and receives no records; an answer that arrives after the limit is
|
|
122
|
+
* ignored. A rejected `write` is the sink's own concern and does not disable
|
|
123
|
+
* the backend.
|
|
100
124
|
*
|
|
101
125
|
* `flush()` always resolves: one deadline (`flushDeadlineMs`, default
|
|
102
126
|
* {@link DEFAULT_FLUSH_DEADLINE_MS}) wraps startup verification, the
|
|
@@ -111,6 +135,9 @@ export declare const DEFAULT_FLUSH_DEADLINE_MS = 5e3;
|
|
|
111
135
|
* resolves anyway; raise it for slow but working backends such as network
|
|
112
136
|
* filesystems.
|
|
113
137
|
*
|
|
138
|
+
* @param verifyTimeoutMs - Milliseconds one sink's `verify()` may take before
|
|
139
|
+
* the sink counts as unavailable; raise it for a slow but working probe.
|
|
140
|
+
*
|
|
114
141
|
* @returns Logger plus its eager `initPromise`; callers need not await
|
|
115
142
|
* `initPromise` before logging, since startup records replay on verify.
|
|
116
143
|
*
|
|
@@ -129,9 +156,10 @@ export declare const DEFAULT_FLUSH_DEADLINE_MS = 5e3;
|
|
|
129
156
|
* });
|
|
130
157
|
* ```
|
|
131
158
|
*/
|
|
132
|
-
export declare function createLogger({ sinks, flushDeadlineMs }: {
|
|
159
|
+
export declare function createLogger({ sinks, flushDeadlineMs, verifyTimeoutMs }: {
|
|
133
160
|
readonly sinks: readonly Sink[];
|
|
134
161
|
readonly flushDeadlineMs?: number;
|
|
162
|
+
readonly verifyTimeoutMs?: number;
|
|
135
163
|
}): {
|
|
136
164
|
readonly initPromise: Promise<void>;
|
|
137
165
|
readonly logger: Logger;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{t as __exportAll}from"./rolldown-runtime-5duEfhBv.mjs";function _usingCtx(){var r=typeof SuppressedError==`function`?SuppressedError:function(r,e){var n=Error();return n.name=`SuppressedError`,n.error=r,n.suppressed=e,n},e={},n=[];function using(r,e){if(e!=null){if(Object(e)!==e)throw TypeError(`using declarations can only be used with objects, functions, null, or undefined.`);if(r)var o=e[Symbol.asyncDispose||Symbol.for(`Symbol.asyncDispose`)];if(o===void 0&&(o=e[Symbol.dispose||Symbol.for(`Symbol.dispose`)],r))var t=o;if(typeof o!=`function`)throw TypeError(`Object is not disposable.`);t&&(o=function(){try{t.call(e)}catch(r){return Promise.reject(r)}}),n.push({v:e,d:o,a:r})}else r&&n.push({d:e,a:r});return e}return{e,u:using.bind(null,!1),a:using.bind(null,!0),d:function(){var o,t=this.e,s=0;function next(){for(;o=n.pop();)try{if(!o.a&&s===1)return s=0,n.push(o),Promise.resolve().then(next);if(o.d){var r=o.d.call(o.v);if(o.a)return s|=2,Promise.resolve(r).then(next,err)}else s|=1}catch(r){return err(r)}if(s===1)return t===e?Promise.resolve():Promise.reject(t);if(t!==e)throw t}function err(n){return t=t===e?n:new r(n,t),next()}return next()}}}async function withTimeout({promise,ms,label}){try{var _usingCtx$1=_usingCtx();let{promise:timeoutPromise,reject}=Promise.withResolvers(),timer=setTimeout(function(){reject(Error(`Timed out after ${String(ms)}ms: ${label}`))},ms);return _usingCtx$1.u({[Symbol.dispose](){clearTimeout(timer)}}),await Promise.race([promise,timeoutPromise])}catch(_){_usingCtx$1.e=_}finally{_usingCtx$1.d()}}function caughtValueText(value){return Error.isError(value)?value.message:String(value)}function reportLoggerInternalError({context,error}){console.warn(`logger internal error: ${context}: ${caughtValueText(error)}`)}const DEFAULT_FLUSH_DEADLINE_MS=5e3;async function trackWrite({writePromise}){try{await writePromise}catch(error){reportLoggerInternalError({context:`sink write promise rejected while being tracked`,error})}}function createLogger({sinks,flushDeadlineMs=DEFAULT_FLUSH_DEADLINE_MS}){let entries=sinks.map(function(sink){return{available:!1,sink}}),startupRecords=[],pendingWrites=new Set,state={hasAvailableSink:!1,initialized:!1};function recomputeAvailability(){state.hasAvailableSink=entries.some(function(entry){return entry.available})}function getSinkEntry({entryIndex}){let entry=entries[entryIndex];if(entry===void 0)throw Error(`Missing logger sink entry at index ${entryIndex}.`);return entry}function markEntryUnavailable({entryIndex}){let entry=getSinkEntry({entryIndex});entry.available=!1,recomputeAvailability()}async function removePendingWriteWhenSettled({trackedWrite}){await trackedWrite,pendingWrites.delete(trackedWrite)}function writeRecordToEntry({entryIndex,record}){try{let trackedWrite=trackWrite({writePromise:getSinkEntry({entryIndex}).sink.write(record)});pendingWrites.add(trackedWrite),removePendingWriteWhenSettled({trackedWrite})}catch(error){reportLoggerInternalError({context:`sink write threw synchronously while dispatching record`,error})}}function replayStartupRecordsToEntry({entryIndex}){startupRecords.forEach(function(record){writeRecordToEntry({entryIndex,record})})}function setEntryAvailability({entryIndex,available}){let entry=getSinkEntry({entryIndex});entry.available=available,recomputeAvailability(),available&&replayStartupRecordsToEntry({entryIndex})}async function verifyAndApply({entryIndex}){try{setEntryAvailability({available:await
|
|
2
|
-
`);if(!(level===`debug`&&writeDebugRunToProcessStderr(text)))try{let method=LEVEL_TO_CONSOLE_METHOD[level],consoleFn=console[method];typeof consoleFn==`function`&&consoleFn.call(console,text)}catch(error){reportLoggerInternalError({context:`console method call failed while emitting log run`,error})}}function groupRuns(records){return records.reduce(function(runs,record){let tail=runs.at(-1);return tail!==void 0&&tail.level===record.level?(tail.records.push(record),runs):(runs.push({level:record.level,records:[record]}),runs)},[])}function verifyConsole(){try{return typeof console>`u`||typeof(hasProcessStderr()?console.info:console.debug)!=`function`||typeof queueMicrotask!=`function`?Promise.resolve(!1):Promise.resolve(!0)}catch(error){return reportLoggerInternalError({context:`console sink verification failed`,error}),Promise.resolve(!1)}}function createConsoleSink(){let state={buffer:[],scheduled:!1,verboseCache:VERBOSE_UNCOMPUTED};function getVerbose(){let cached=state.verboseCache;if(cached!==VERBOSE_UNCOMPUTED)return cached;let computed=detectVerbose();return state.verboseCache=computed,computed}function flushBuffer(){if(state.scheduled=!1,state.buffer.length===0)return;let records=state.buffer.splice(0);for(let run of groupRuns(records))emitRun({level:run.level,records:run.records})}function write(record){return!getVerbose()&&SILENT_LEVELS.has(record.level)||record.level===`warn`&&isWarnSuppressed()?Promise.resolve():(state.buffer.push(record),state.scheduled||(state.scheduled=!0,queueMicrotask(flushBuffer)),Promise.resolve())}function flush(){return flushBuffer(),Promise.resolve()}return{flush,verify:verifyConsole,write}}const NO_NODE_MODULES_FOUND=Symbol(`logger:no-node-modules-found`);async function findNodeModulesUp({cwd,stat,dirname,join,reportError=reportLoggerInternalError}){let candidate=join(cwd,`node_modules`);try{if((await stat(candidate)).isDirectory())return candidate}catch(error){Error.isError(error)&&`code`in error&&error.code===`ENOENT`||reportError({context:`node_modules candidate ${candidate} unavailable during file sink search`,error})}let parent=dirname(cwd);return parent===cwd?NO_NODE_MODULES_FOUND:await findNodeModulesUp({cwd:parent,stat,dirname,join,reportError})}function createFileSink(){let state={};async function runVerify(){if(globalThis.process===void 0||globalThis.process.versions?.node===void 0)return!1;try{let fs=await import(`node:fs/promises`),{dirname,join}=await import(`node:path`);state.appendFile=fs.appendFile;let nodeModulesDir=await findNodeModulesUp({cwd:process.cwd(),stat:fs.stat,dirname,join});if(nodeModulesDir===NO_NODE_MODULES_FOUND)return!1;let LOG_DIR=join(nodeModulesDir,`.monochromatic`);await fs.mkdir(LOG_DIR,{recursive:!0});let timestamp=new Date().toISOString().replaceAll(`:`,`-`);state.filePath=join(LOG_DIR,`${timestamp}.log.jsonl`);let testData=`{"test":true,"timestamp":${Date.now()}}\n`;return await state.appendFile(state.filePath,testData),(await fs.readFile(state.filePath,`utf8`)).includes(`"test":true`)}catch(error){return reportLoggerInternalError({context:`file sink verification failed`,error}),!1}}function verify(){return state.verifyPromise===void 0&&(state.verifyPromise=runVerify()),state.verifyPromise}async function write(record){if(
|
|
3
|
-
`);entries.length=0,bufferState.chars=0,onFlush(batch)}function scheduleDeadlineFlush(){if(bufferState.timer!==void 0)return;let timer=globalThis.setTimeout(drain,250);isUnrefableTimer(timer)&&timer.unref(),bufferState.timer=timer}function add(entry){entries.length>0&&charsWith(entry.serialized)>FLUSH_BUFFER_CAP_CHARS&&drain(),bufferState.chars=charsWith(entry.serialized),entries.push(entry.serialized),FLUSH_IMMEDIATELY_BY_LEVEL[entry.level]||bufferState.chars>=FLUSH_BUFFER_CAP_CHARS?drain():scheduleDeadlineFlush()}return globalThis.addEventListener?.(`pagehide`,drain),globalThis.document?.addEventListener(`visibilitychange`,function(){globalThis.document?.visibilityState===`hidden`&&drain()}),{add,drain}}const BATCH_STORE=`batch`,MAX_STORED_BATCHES=2048;async function openLogDatabase(){let request=globalThis.indexedDB.open(`monochromatic.log`,1);return request.onupgradeneeded=function(){request.result.createObjectStore(BATCH_STORE,{autoIncrement:!0})},await awaitRequest(request)}async function persistBatch({database,batch}){let transaction=database.transaction(BATCH_STORE,`readwrite`),store=transaction.objectStore(BATCH_STORE);store.add(batch);let count=await awaitRequest(store.count());if(count>MAX_STORED_BATCHES){let newestStale=(await awaitRequest(store.getAllKeys(null,count-MAX_STORED_BATCHES))).at(-1);newestStale!==void 0&&store.delete(IDBKeyRange.upperBound(newestStale))}await awaitTransaction(transaction)}function createIndexedDbSink(){let state={},pendingBatchWrites=new Set;async function verify(){try{if(globalThis.indexedDB===void 0)return!1;let database=await openLogDatabase(),transaction=database.transaction(BATCH_STORE,`readwrite`),store=transaction.objectStore(BATCH_STORE),probeValue=`probe-${Date.now()}`,probeKey=await awaitRequest(store.add(probeValue)),readBack=await awaitRequest(store.get(probeKey));return store.delete(probeKey),await awaitTransaction(transaction),readBack===probeValue&&(state.database=database,!0)}catch(error){return`indexedDB`in globalThis&&reportLoggerInternalError({context:`IndexedDB sink verification failed`,error}),!1}}async function writeBatch(batch){if(state.database)try{await persistBatch({database:state.database,batch})}catch(error){reportLoggerInternalError({context:`IndexedDB sink record write failed`,error})}}async function removePendingWhenSettled(pending){await pending,pendingBatchWrites.delete(pending)}function handOffBatch(batch){let pending=writeBatch(batch);pendingBatchWrites.add(pending),removePendingWhenSettled(pending)}let buffer=createRecordBuffer({onFlush:handOffBatch});function write(record){return buffer.add({level:record.level,serialized:JSON.stringify(record)}),Promise.resolve()}async function flush(){buffer.drain();let writes=[...pendingBatchWrites];await Promise.all(writes)}return{flush,verify,write}}function isDigits(text){if(text.length===0)return!1;for(let character of text)if(character<`0`||character>`9`)return!1;return!0}function buildLogKey({stamp,nonce,index}){return`monochromatic.log.${stamp}.${nonce}.${index}`}function parseLogKey(key){if(!key.startsWith(`monochromatic.log.`))return{};let segments=key.slice(18).split(`.`);if(segments.length!==3)return{};let[stampText,nonce,indexText]=segments;return stampText===void 0||nonce===void 0||indexText===void 0||!isDigits(stampText)||nonce.length===0||!isDigits(indexText)?{}:{parsed:{key,stamp:Math.trunc(Number(stampText)),nonce,index:Math.trunc(Number(indexText))}}}function compareLogKeys({first,second}){return first.stamp===second.stamp?first.nonce===second.nonce?first.index-second.index:first.nonce<second.nonce?-1:1:first.stamp-second.stamp}function detectWebStorageRuntime(){return`Deno`in globalThis?`deno`:`Bun`in globalThis?`bun`:typeof process<`u`&&typeof process.versions.node==`string`?`node`:`document`in globalThis?`browser`:`unknown`}const RUNTIME_QUOTA_CHARS$1={browser:5242880,bun:1/0,deno:10477569,node:5242880,unknown:1/0};function detectLocalStorageQuotaChars(){return RUNTIME_QUOTA_CHARS$1[detectWebStorageRuntime()]}const QUOTA_EXCEEDED_NAMES=new Set([`QuotaExceededError`,`NS_ERROR_DOM_QUOTA_REACHED`]);function isQuotaExceededError(error){return typeof error==`object`&&!!error&&`name`in error&&typeof error.name==`string`&"A_EXCEEDED_NAMES.has(error.name)}function createLocalStorageStore(){let runIdentity={stamp:Date.now(),nonce:Math.random().toString(36).slice(2,6).padEnd(4,`0`)},state={lineCounter:0,oldestIndex:0,usedChars:0,reportedFailure:!1,adoptedPrior:!1},prior={entries:[],cursor:0},capChars=detectLocalStorageQuotaChars()/2;function ownKey(index){return buildLogKey({stamp:runIdentity.stamp,nonce:runIdentity.nonce,index})}function adoptPriorEntries(){let total=globalThis.localStorage.length,found=[];for(let slot=0;slot<total;slot++){let key=globalThis.localStorage.key(slot);if(key===null)continue;let{parsed}=parseLogKey(key);if(parsed===void 0||parsed.stamp===runIdentity.stamp&&parsed.nonce===runIdentity.nonce)continue;let value=globalThis.localStorage.getItem(key);value!==null&&found.push({...parsed,chars:value.length})}prior.entries=found.toSorted(function(first,second){return compareLogKeys({first,second})}),state.usedChars+=found.reduce(function(sum,entry){return sum+entry.chars},0)}function hasEvictable(){let priorCount=prior.entries.length;return prior.cursor<priorCount||state.oldestIndex<state.lineCounter}function evictOldestPrior(){let entry=prior.entries[prior.cursor];return entry!==void 0&&(prior.cursor++,globalThis.localStorage.removeItem(entry.key),state.usedChars=Math.max(0,state.usedChars-entry.chars),!0)}function evictOldestOwn(){let key=ownKey(state.oldestIndex),evicted=globalThis.localStorage.getItem(key);globalThis.localStorage.removeItem(key),state.oldestIndex++,evicted!==null&&(state.usedChars=Math.max(0,state.usedChars-evicted.length))}function evictOldest(){evictOldestPrior()||state.oldestIndex<state.lineCounter&&evictOldestOwn()}function persist(batch){state.adoptedPrior||(state.adoptedPrior=!0,adoptPriorEntries());let batchChars=batch.length;for(;hasEvictable()&&state.usedChars+batchChars>capChars;)evictOldest();let maxWriteAttempts=prior.entries.length-prior.cursor+(state.lineCounter-state.oldestIndex)+1;for(let writeAttempt=0;writeAttempt<maxWriteAttempts;writeAttempt++)try{globalThis.localStorage.setItem(ownKey(state.lineCounter),batch),state.lineCounter++,state.usedChars+=batchChars,state.reportedFailure=!1;return}catch(error){if(isQuotaExceededError(error)&&hasEvictable()){evictOldest();continue}state.reportedFailure||=(reportLoggerInternalError({context:`localStorage sink record write failed (repeats suppressed until a write next succeeds)`,error}),!0);return}}return{persist}}const NODE_LOCALSTORAGE_FLAG=`--localstorage-file`;function nodeWithoutLocalStorageFile(){if(detectWebStorageRuntime()!==`node`||`document`in globalThis)return!1;let flaggedInExecArgv=process.execArgv.some(function(argument){return argument.startsWith(NODE_LOCALSTORAGE_FLAG)}),flaggedInNodeOptions=(process.env.NODE_OPTIONS??``).includes(NODE_LOCALSTORAGE_FLAG);return!(flaggedInExecArgv||flaggedInNodeOptions)}function verifyLocalStorage(){if(nodeWithoutLocalStorageFile())return Promise.resolve(!1);try{let testKey=`__monochromatic_verify__`,testValue=`test-${Date.now()}`;globalThis.localStorage.setItem(testKey,testValue);let readBack=globalThis.localStorage.getItem(testKey);return globalThis.localStorage.removeItem(testKey),Promise.resolve(readBack===testValue)}catch(error){return`localStorage`in globalThis&&reportLoggerInternalError({context:`localStorage sink verification failed`,error}),Promise.resolve(!1)}}function createLocalStorageSink(){let buffer=createRecordBuffer({onFlush:createLocalStorageStore().persist});function write(record){return buffer.add({level:record.level,serialized:JSON.stringify(record)}),Promise.resolve()}function flush(){return buffer.drain(),Promise.resolve()}return{flush,verify:verifyLocalStorage,write}}const RUNTIME_QUOTA_CHARS={browser:5242880,bun:1/0,deno:10485760,node:5242880,unknown:1/0};function detectSessionStorageQuotaChars(){return RUNTIME_QUOTA_CHARS[detectWebStorageRuntime()]}function storageKey(index){return`monochromatic.log.${index}`}function createSessionStorageStore(){let state={lineCounter:0,oldestIndex:0,usedChars:0,reportedFailure:!1},capChars=detectSessionStorageQuotaChars()/2;function evictOldest(){let key=storageKey(state.oldestIndex),evicted=globalThis.sessionStorage.getItem(key);globalThis.sessionStorage.removeItem(key),state.oldestIndex++,evicted!==null&&(state.usedChars=Math.max(0,state.usedChars-evicted.length))}function persist(batch){let batchChars=batch.length;for(;state.oldestIndex<state.lineCounter&&state.usedChars+batchChars>capChars;)evictOldest();let maxWriteAttempts=state.lineCounter-state.oldestIndex+1;for(let writeAttempt=0;writeAttempt<maxWriteAttempts;writeAttempt++)try{globalThis.sessionStorage.setItem(storageKey(state.lineCounter),batch),state.lineCounter++,state.usedChars+=batchChars,state.reportedFailure=!1;return}catch(error){if(isQuotaExceededError(error)&&state.oldestIndex<state.lineCounter){evictOldest();continue}state.reportedFailure||=(reportLoggerInternalError({context:`sessionStorage sink record write failed (repeats suppressed until a write next succeeds)`,error}),!0);return}}return{persist}}function verifySessionStorage(){try{let testKey=`__monochromatic_verify__`,testValue=`test-${Date.now()}`;globalThis.sessionStorage.setItem(testKey,testValue);let readBack=globalThis.sessionStorage.getItem(testKey);return globalThis.sessionStorage.removeItem(testKey),Promise.resolve(readBack===testValue)}catch(error){return`sessionStorage`in globalThis&&reportLoggerInternalError({context:`sessionStorage sink verification failed`,error}),Promise.resolve(!1)}}function createSessionStorageSink(){let buffer=createRecordBuffer({onFlush:createSessionStorageStore().persist});function write(record){return buffer.add({level:record.level,serialized:JSON.stringify(record)}),Promise.resolve()}function flush(){return buffer.drain(),Promise.resolve()}return{flush,verify:verifySessionStorage,write}}const{initPromise:defaultInitPromise,logger:defaultLogger}=createLogger({sinks:[createConsoleSink(),createIndexedDbSink(),createSessionStorageSink(),createLocalStorageSink(),createFileSink()]}),initPromise=defaultInitPromise,logger=defaultLogger;function verify(){return Promise.resolve(!0)}function write(_record){return Promise.resolve()}function createNoopSink(){return{verify,write}}function createOpfsSink(){let state={},pendingBatchWrites=new Set;async function verify(){try{let opfsRoot=await navigator.storage.getDirectory(),timestamp=new Date().toISOString().replaceAll(`:`,`-`),fileHandle=await opfsRoot.getFileHandle(`monochromatic-${timestamp}.log.jsonl`,{create:!0}),probeWritable=await fileHandle.createWritable({keepExistingData:!0}),testData=`{"test":true,"timestamp":${Date.now()}}\n`;await probeWritable.write(testData),await probeWritable.close();let available=(await(await fileHandle.getFile()).text()).includes(`"test":true`);return available&&(state.writable=await fileHandle.createWritable({keepExistingData:!0})),available}catch(error){let opfsStorage=globalThis.navigator?.storage;return opfsStorage!==void 0&&`getDirectory`in opfsStorage&&reportLoggerInternalError({context:`OPFS sink verification failed`,error}),!1}}async function writeBatch(batch){if(state.writable)try{await state.writable.write(`${batch}\n`)}catch(error){reportLoggerInternalError({context:`OPFS sink record write failed`,error})}}async function removePendingWhenSettled(pending){await pending,pendingBatchWrites.delete(pending)}function handOffBatch(batch){let pending=writeBatch(batch);pendingBatchWrites.add(pending),removePendingWhenSettled(pending)}let buffer=createRecordBuffer({onFlush:handOffBatch});function write(record){return buffer.add({level:record.level,serialized:JSON.stringify(record)}),Promise.resolve()}async function flush(){buffer.drain();let writes=[...pendingBatchWrites];await Promise.all(writes)}return{flush,verify,write}}var sink_exports=__exportAll({NO_NODE_MODULES_FOUND:()=>NO_NODE_MODULES_FOUND,createConsoleSink:()=>createConsoleSink,createFileSink:()=>createFileSink,createIndexedDbSink:()=>createIndexedDbSink,createLocalStorageSink:()=>createLocalStorageSink,createNoopSink:()=>createNoopSink,createOpfsSink:()=>createOpfsSink,createSessionStorageSink:()=>createSessionStorageSink,findNodeModulesUp:()=>findNodeModulesUp});function tagged({tag,l=logger}){let prefix=`[${tag}] `;return{debug:function(message){l.debug(`${prefix}${message}`)},error:function(message){l.error(`${prefix}${message}`)},fatal:function(message){l.fatal(`${prefix}${message}`)},flush:function(){return l.flush()},info:function(message){l.info(`${prefix}${message}`)},trace:function(message){l.trace(`${prefix}${message}`)},warn:function(message){l.warn(`${prefix}${message}`)}}}export{DEFAULT_FLUSH_DEADLINE_MS,buildLogKey as _buildLogKey,compareLogKeys as _compareLogKeys,createLocalStorageStore as _createLocalStorageStore,createRecordBuffer as _createRecordBuffer,detectLocalStorageQuotaChars as _detectLocalStorageQuotaChars,detectSessionStorageQuotaChars as _detectSessionStorageQuotaChars,isQuotaExceededError as _isQuotaExceededError,neutralizeControlCharacters as _neutralizeControlCharacters,parseLogKey as _parseLogKey,createLogger,initPromise,logger,sink_exports as sinks,tagged};
|
|
1
|
+
import{t as __exportAll}from"./rolldown-runtime-5duEfhBv.mjs";function _usingCtx(){var r=typeof SuppressedError==`function`?SuppressedError:function(r,e){var n=Error();return n.name=`SuppressedError`,n.error=r,n.suppressed=e,n},e={},n=[];function using(r,e){if(e!=null){if(Object(e)!==e)throw TypeError(`using declarations can only be used with objects, functions, null, or undefined.`);if(r)var o=e[Symbol.asyncDispose||Symbol.for(`Symbol.asyncDispose`)];if(o===void 0&&(o=e[Symbol.dispose||Symbol.for(`Symbol.dispose`)],r))var t=o;if(typeof o!=`function`)throw TypeError(`Object is not disposable.`);t&&(o=function(){try{t.call(e)}catch(r){return Promise.reject(r)}}),n.push({v:e,d:o,a:r})}else r&&n.push({d:e,a:r});return e}return{e,u:using.bind(null,!1),a:using.bind(null,!0),d:function(){var o,t=this.e,s=0;function next(){for(;o=n.pop();)try{if(!o.a&&s===1)return s=0,n.push(o),Promise.resolve().then(next);if(o.d){var r=o.d.call(o.v);if(o.a)return s|=2,Promise.resolve(r).then(next,err)}else s|=1}catch(r){return err(r)}if(s===1)return t===e?Promise.resolve():Promise.reject(t);if(t!==e)throw t}function err(n){return t=t===e?n:new r(n,t),next()}return next()}}}async function withTimeout({promise,ms,label}){try{var _usingCtx$1=_usingCtx();let{promise:timeoutPromise,reject}=Promise.withResolvers(),timer=setTimeout(function(){reject(Error(`Timed out after ${String(ms)}ms: ${label}`))},ms);return _usingCtx$1.u({[Symbol.dispose](){clearTimeout(timer)}}),await Promise.race([promise,timeoutPromise])}catch(_){_usingCtx$1.e=_}finally{_usingCtx$1.d()}}function caughtValueText(value){return Error.isError(value)?value.message:String(value)}function reportLoggerInternalError({context,error}){console.warn(`logger internal error: ${context}: ${caughtValueText(error)}`)}const DEFAULT_FLUSH_DEADLINE_MS=5e3,DEFAULT_VERIFY_TIMEOUT_MS=5e3,STARTUP_BUFFER_CAP=1e4;async function trackWrite({writePromise}){try{await writePromise}catch(error){reportLoggerInternalError({context:`sink write promise rejected while being tracked`,error})}}function createLogger({sinks,flushDeadlineMs=DEFAULT_FLUSH_DEADLINE_MS,verifyTimeoutMs=DEFAULT_VERIFY_TIMEOUT_MS}){let entries=sinks.map(function(sink){return{available:!1,sink}}),startupRecords=[],pendingWrites=new Set,state={droppedStartupRecords:0,hasAvailableSink:!1,initialized:!1};function bufferStartupRecord({record}){startupRecords.length>=1e4&&(startupRecords.shift(),state.droppedStartupRecords+=1),startupRecords.push(record)}function recomputeAvailability(){state.hasAvailableSink=entries.some(function(entry){return entry.available})}function getSinkEntry({entryIndex}){let entry=entries[entryIndex];if(entry===void 0)throw Error(`Missing logger sink entry at index ${entryIndex}.`);return entry}function markEntryUnavailable({entryIndex}){let entry=getSinkEntry({entryIndex});entry.available=!1,recomputeAvailability()}async function removePendingWriteWhenSettled({trackedWrite}){await trackedWrite,pendingWrites.delete(trackedWrite)}function writeRecordToEntry({entryIndex,record}){try{let trackedWrite=trackWrite({writePromise:getSinkEntry({entryIndex}).sink.write(record)});pendingWrites.add(trackedWrite),removePendingWriteWhenSettled({trackedWrite})}catch(error){reportLoggerInternalError({context:`sink write threw synchronously while dispatching record`,error})}}function replayStartupRecordsToEntry({entryIndex}){startupRecords.forEach(function(record){writeRecordToEntry({entryIndex,record})})}function setEntryAvailability({entryIndex,available}){let entry=getSinkEntry({entryIndex});entry.available=available,recomputeAvailability(),available&&replayStartupRecordsToEntry({entryIndex})}async function verifyAndApply({entryIndex}){try{let entry=getSinkEntry({entryIndex});setEntryAvailability({available:await withTimeout({label:`sink ${entryIndex} verify`,ms:verifyTimeoutMs,promise:entry.sink.verify()}),entryIndex})}catch(error){reportLoggerInternalError({context:`sink verification failed for entry ${entryIndex}`,error}),markEntryUnavailable({entryIndex})}}function emitDroppedStartupMarker(){let dropped=state.droppedStartupRecords;if(dropped===0)return;let marker={level:`warn`,message:`${dropped} startup record${dropped===1?``:`s`} dropped before a backend verified (buffer cap ${STARTUP_BUFFER_CAP})`,timestamp:Date.now()};entries.forEach(function(entry,entryIndex){entry.available&&writeRecordToEntry({entryIndex,record:marker})})}async function initialize(){state.initialized||(await Promise.all(entries.map(function(_entry,entryIndex){return verifyAndApply({entryIndex})})),state.initialized=!0,startupRecords.length=0,emitDroppedStartupMarker())}let initPromise=initialize();async function drainPendingWrites(){let writes=[...pendingWrites];await Promise.all(writes)}function createMethod(level){return function(message){if(!state.hasAvailableSink&&state.initialized)throw Error(`No logging backends available`);let record={level,message,timestamp:Date.now()};state.initialized||bufferStartupRecord({record}),entries.map(function(_entry,entryIndex){return entryIndex}).filter(function(entryIndex){return getSinkEntry({entryIndex}).available}).forEach(function(entryIndex){writeRecordToEntry({entryIndex,record})})}}async function runSinkFlushHooks(){await Promise.all(entries.map(async function(entry,entryIndex){let sinkFlush=entry.sink.flush;if(entry.available&&typeof sinkFlush==`function`)try{await sinkFlush()}catch(error){reportLoggerInternalError({context:`sink flush failed for entry ${entryIndex}`,error}),markEntryUnavailable({entryIndex})}}))}async function drainEverything(){await initPromise,await drainPendingWrites(),await runSinkFlushHooks()}function abandonPendingWrites(){pendingWrites.clear()}async function flushAll(){try{await withTimeout({label:`logger flush`,ms:flushDeadlineMs,promise:drainEverything()})}catch(error){reportLoggerInternalError({context:`flush deadline of ${flushDeadlineMs}ms elapsed; abandoning in-flight sink work`,error}),abandonPendingWrites()}}return{initPromise,logger:{debug:createMethod(`debug`),error:createMethod(`error`),fatal:createMethod(`fatal`),flush:flushAll,info:createMethod(`info`),trace:createMethod(`trace`),warn:createMethod(`warn`)}}}function isNeutralizedControl(codeUnit){return codeUnit<32?codeUnit!==10&&codeUnit!==9:codeUnit===127||codeUnit>=128&&codeUnit<=159}function escapeCodeUnit(codeUnit){return`\\u${codeUnit.toString(16).toUpperCase().padStart(4,`0`)}`}function neutralizeControlCharacters(text){let pieces=[];for(let character of text){let codeUnit=character.charCodeAt(0);pieces.push(isNeutralizedControl(codeUnit)?escapeCodeUnit(codeUnit):character)}return pieces.join(``)}const VERBOSE_UNCOMPUTED=Symbol(`logger:verbose-detection-uncomputed`),SILENT_LEVELS=new Set([`debug`,`trace`]);function detectVerbose(){try{if(typeof process<`u`&&process.env.MONOCHROMATIC_VERBOSE===`true`)return!0}catch(error){reportLoggerInternalError({context:`MONOCHROMATIC_VERBOSE environment probe failed during verbose detection`,error})}try{if(typeof process<`u`&&Array.isArray(process.argv)&&process.argv.includes(`--verbose`))return!0}catch(error){reportLoggerInternalError({context:`process argv probe failed during verbose detection`,error})}try{if(`window`in globalThis)return!0}catch(error){reportLoggerInternalError({context:`window probe failed during verbose detection`,error})}return!1}function isWarnSuppressed(){try{return typeof process<`u`&&process.env.MONOCHROMATIC_WARN===`false`}catch(error){return reportLoggerInternalError({context:`MONOCHROMATIC_WARN environment probe failed during warn suppression detection`,error}),!1}}const LEVEL_TO_CONSOLE_METHOD={debug:`debug`,error:`error`,fatal:`error`,info:`info`,trace:`trace`,warn:`warn`};function formatRecord(record){return`[${record.level}] [${new Date(record.timestamp).toISOString()}] ${neutralizeControlCharacters(record.message)}`}function hasProcessStderr(){try{return typeof process>`u`?!1:typeof process.stderr.write==`function`}catch(error){return reportLoggerInternalError({context:`process stderr availability probe failed`,error}),!1}}function writeDebugRunToProcessStderr(text){try{return hasProcessStderr()?(process.stderr.write(`${text}\n`),!0):!1}catch(error){return reportLoggerInternalError({context:`debug run process stderr write failed`,error}),!1}}function emitRun({records,level}){let text=records.map(function(r){return formatRecord(r)}).join(`
|
|
2
|
+
`);if(!(level===`debug`&&writeDebugRunToProcessStderr(text)))try{let method=LEVEL_TO_CONSOLE_METHOD[level],consoleFn=console[method];typeof consoleFn==`function`&&consoleFn.call(console,text)}catch(error){reportLoggerInternalError({context:`console method call failed while emitting log run`,error})}}function groupRuns(records){return records.reduce(function(runs,record){let tail=runs.at(-1);return tail!==void 0&&tail.level===record.level?(tail.records.push(record),runs):(runs.push({level:record.level,records:[record]}),runs)},[])}function verifyConsole(){try{return typeof console>`u`||typeof(hasProcessStderr()?console.info:console.debug)!=`function`||typeof queueMicrotask!=`function`?Promise.resolve(!1):Promise.resolve(!0)}catch(error){return reportLoggerInternalError({context:`console sink verification failed`,error}),Promise.resolve(!1)}}function createConsoleSink(){let state={buffer:[],scheduled:!1,verboseCache:VERBOSE_UNCOMPUTED};function getVerbose(){let cached=state.verboseCache;if(cached!==VERBOSE_UNCOMPUTED)return cached;let computed=detectVerbose();return state.verboseCache=computed,computed}function flushBuffer(){if(state.scheduled=!1,state.buffer.length===0)return;let records=state.buffer.splice(0);for(let run of groupRuns(records))emitRun({level:run.level,records:run.records})}function write(record){return!getVerbose()&&SILENT_LEVELS.has(record.level)||record.level===`warn`&&isWarnSuppressed()?Promise.resolve():(state.buffer.push(record),state.scheduled||(state.scheduled=!0,queueMicrotask(flushBuffer)),Promise.resolve())}function flush(){return flushBuffer(),Promise.resolve()}return{flush,verify:verifyConsole,write}}const NO_NODE_MODULES_FOUND=Symbol(`logger:no-node-modules-found`);async function findNodeModulesUp({cwd,stat,dirname,join,reportError=reportLoggerInternalError}){let candidate=join(cwd,`node_modules`);try{if((await stat(candidate)).isDirectory())return candidate}catch(error){Error.isError(error)&&`code`in error&&error.code===`ENOENT`||reportError({context:`node_modules candidate ${candidate} unavailable during file sink search`,error})}let parent=dirname(cwd);return parent===cwd?NO_NODE_MODULES_FOUND:await findNodeModulesUp({cwd:parent,stat,dirname,join,reportError})}function createFileSink(){let state={};async function runVerify(){if(globalThis.process===void 0||globalThis.process.versions?.node===void 0)return!1;try{let fs=await import(`node:fs/promises`),{dirname,join}=await import(`node:path`);state.appendFile=fs.appendFile;let nodeModulesDir=await findNodeModulesUp({cwd:process.cwd(),stat:fs.stat,dirname,join});if(nodeModulesDir===NO_NODE_MODULES_FOUND)return!1;let LOG_DIR=join(nodeModulesDir,`.monochromatic`);await fs.mkdir(LOG_DIR,{recursive:!0});let timestamp=new Date().toISOString().replaceAll(`:`,`-`);state.filePath=join(LOG_DIR,`${timestamp}.log.jsonl`);let testData=`{"test":true,"timestamp":${Date.now()}}\n`;return await state.appendFile(state.filePath,testData),(await fs.readFile(state.filePath,`utf8`)).includes(`"test":true`)}catch(error){return reportLoggerInternalError({context:`file sink verification failed`,error}),!1}}function verify(){return state.verifyPromise===void 0&&(state.verifyPromise=runVerify()),state.verifyPromise}async function write(record){if(state.filePath&&state.appendFile)try{await state.appendFile(state.filePath,`${JSON.stringify(record)}\n`)}catch(error){reportLoggerInternalError({context:`file sink record append failed`,error})}}return{verify,write}}function awaitRequest(request){return new Promise(function(resolve,reject){request.addEventListener(`success`,function(){resolve(request.result)}),request.addEventListener(`error`,function(){reject(request.error??Error(`IndexedDB request failed without an error object`))})})}function awaitTransaction(transaction){return new Promise(function(resolve,reject){transaction.addEventListener(`complete`,function(){resolve()}),transaction.addEventListener(`error`,function(){reject(transaction.error??Error(`IndexedDB transaction failed without an error object`))}),transaction.addEventListener(`abort`,function(){reject(transaction.error??Error(`IndexedDB transaction aborted without an error object`))})})}const FLUSH_BUFFER_CAP_CHARS=32768,FLUSH_IMMEDIATELY_BY_LEVEL={debug:!1,error:!0,fatal:!0,info:!1,trace:!1,warn:!0};function isUnrefableTimer(timer){return typeof timer!=`object`||!timer||!(`unref`in timer)?!1:typeof timer.unref==`function`}function createRecordBuffer({onFlush}){let entries=[],bufferState={chars:0};function charsWith(serialized){let separatorChars=+(entries.length>0);return bufferState.chars+separatorChars+serialized.length}function drain(){if(bufferState.timer!==void 0&&(globalThis.clearTimeout(bufferState.timer),delete bufferState.timer),entries.length===0)return;let batch=entries.join(`
|
|
3
|
+
`);entries.length=0,bufferState.chars=0,onFlush(batch)}function scheduleDeadlineFlush(){if(bufferState.timer!==void 0)return;let timer=globalThis.setTimeout(drain,250);isUnrefableTimer(timer)&&timer.unref(),bufferState.timer=timer}function add(entry){entries.length>0&&charsWith(entry.serialized)>FLUSH_BUFFER_CAP_CHARS&&drain(),bufferState.chars=charsWith(entry.serialized),entries.push(entry.serialized),FLUSH_IMMEDIATELY_BY_LEVEL[entry.level]||bufferState.chars>=FLUSH_BUFFER_CAP_CHARS?drain():scheduleDeadlineFlush()}return globalThis.addEventListener?.(`pagehide`,drain),globalThis.document?.addEventListener(`visibilitychange`,function(){globalThis.document?.visibilityState===`hidden`&&drain()}),{add,drain}}const BATCH_STORE=`batch`,MAX_STORED_BATCHES=2048;async function openLogDatabase(){let request=globalThis.indexedDB.open(`monochromatic.log`,1);return request.onupgradeneeded=function(){request.result.createObjectStore(BATCH_STORE,{autoIncrement:!0})},await awaitRequest(request)}async function persistBatch({database,batch}){let transaction=database.transaction(BATCH_STORE,`readwrite`),store=transaction.objectStore(BATCH_STORE);store.add(batch);let count=await awaitRequest(store.count());if(count>MAX_STORED_BATCHES){let newestStale=(await awaitRequest(store.getAllKeys(null,count-MAX_STORED_BATCHES))).at(-1);newestStale!==void 0&&store.delete(IDBKeyRange.upperBound(newestStale))}await awaitTransaction(transaction)}function createIndexedDbSink(){let state={},pendingBatchWrites=new Set;async function verify(){try{if(globalThis.indexedDB===void 0)return!1;let database=await openLogDatabase(),transaction=database.transaction(BATCH_STORE,`readwrite`),store=transaction.objectStore(BATCH_STORE),probeValue=`probe-${Date.now()}`,probeKey=await awaitRequest(store.add(probeValue)),readBack=await awaitRequest(store.get(probeKey));return store.delete(probeKey),await awaitTransaction(transaction),readBack===probeValue&&(state.database=database,!0)}catch(error){return`indexedDB`in globalThis&&reportLoggerInternalError({context:`IndexedDB sink verification failed`,error}),!1}}async function writeBatch(batch){if(state.database)try{await persistBatch({database:state.database,batch})}catch(error){reportLoggerInternalError({context:`IndexedDB sink record write failed`,error})}}async function removePendingWhenSettled(pending){await pending,pendingBatchWrites.delete(pending)}function handOffBatch(batch){let pending=writeBatch(batch);pendingBatchWrites.add(pending),removePendingWhenSettled(pending)}let buffer=createRecordBuffer({onFlush:handOffBatch});function write(record){return buffer.add({level:record.level,serialized:JSON.stringify(record)}),Promise.resolve()}async function flush(){buffer.drain();let writes=[...pendingBatchWrites];await Promise.all(writes)}return{flush,verify,write}}function isDigits(text){if(text.length===0)return!1;for(let character of text)if(character<`0`||character>`9`)return!1;return!0}function buildLogKey({stamp,nonce,index}){return`monochromatic.log.${stamp}.${nonce}.${index}`}function parseLogKey(key){if(!key.startsWith(`monochromatic.log.`))return{};let segments=key.slice(18).split(`.`);if(segments.length!==3)return{};let[stampText,nonce,indexText]=segments;return stampText===void 0||nonce===void 0||indexText===void 0||!isDigits(stampText)||nonce.length===0||!isDigits(indexText)?{}:{parsed:{key,stamp:Math.trunc(Number(stampText)),nonce,index:Math.trunc(Number(indexText))}}}function compareLogKeys({first,second}){return first.stamp===second.stamp?first.nonce===second.nonce?first.index-second.index:first.nonce<second.nonce?-1:1:first.stamp-second.stamp}function detectWebStorageRuntime(){return`Deno`in globalThis?`deno`:`Bun`in globalThis?`bun`:typeof process<`u`&&typeof process.versions.node==`string`?`node`:`document`in globalThis?`browser`:`unknown`}const RUNTIME_QUOTA_CHARS$1={browser:5242880,bun:1/0,deno:10477569,node:5242880,unknown:1/0};function detectLocalStorageQuotaChars(){return RUNTIME_QUOTA_CHARS$1[detectWebStorageRuntime()]}const QUOTA_EXCEEDED_NAMES=new Set([`QuotaExceededError`,`NS_ERROR_DOM_QUOTA_REACHED`]);function isQuotaExceededError(error){return typeof error==`object`&&!!error&&`name`in error&&typeof error.name==`string`&"A_EXCEEDED_NAMES.has(error.name)}function createLocalStorageStore(){let runIdentity={stamp:Date.now(),nonce:Math.random().toString(36).slice(2,6).padEnd(4,`0`)},state={lineCounter:0,oldestIndex:0,usedChars:0,reportedFailure:!1,adoptedPrior:!1},prior={entries:[],cursor:0},capChars=detectLocalStorageQuotaChars()/2;function ownKey(index){return buildLogKey({stamp:runIdentity.stamp,nonce:runIdentity.nonce,index})}function adoptPriorEntries(){let total=globalThis.localStorage.length,found=[];for(let slot=0;slot<total;slot++){let key=globalThis.localStorage.key(slot);if(key===null)continue;let{parsed}=parseLogKey(key);if(parsed===void 0||parsed.stamp===runIdentity.stamp&&parsed.nonce===runIdentity.nonce)continue;let value=globalThis.localStorage.getItem(key);value!==null&&found.push({...parsed,chars:value.length})}prior.entries=found.toSorted(function(first,second){return compareLogKeys({first,second})}),state.usedChars+=found.reduce(function(sum,entry){return sum+entry.chars},0)}function hasEvictable(){let priorCount=prior.entries.length;return prior.cursor<priorCount||state.oldestIndex<state.lineCounter}function evictOldestPrior(){let entry=prior.entries[prior.cursor];return entry!==void 0&&(prior.cursor++,globalThis.localStorage.removeItem(entry.key),state.usedChars=Math.max(0,state.usedChars-entry.chars),!0)}function evictOldestOwn(){let key=ownKey(state.oldestIndex),evicted=globalThis.localStorage.getItem(key);globalThis.localStorage.removeItem(key),state.oldestIndex++,evicted!==null&&(state.usedChars=Math.max(0,state.usedChars-evicted.length))}function evictOldest(){evictOldestPrior()||state.oldestIndex<state.lineCounter&&evictOldestOwn()}function persist(batch){state.adoptedPrior||(state.adoptedPrior=!0,adoptPriorEntries());let batchChars=batch.length;for(;hasEvictable()&&state.usedChars+batchChars>capChars;)evictOldest();let maxWriteAttempts=prior.entries.length-prior.cursor+(state.lineCounter-state.oldestIndex)+1;for(let writeAttempt=0;writeAttempt<maxWriteAttempts;writeAttempt++)try{globalThis.localStorage.setItem(ownKey(state.lineCounter),batch),state.lineCounter++,state.usedChars+=batchChars,state.reportedFailure=!1;return}catch(error){if(isQuotaExceededError(error)&&hasEvictable()){evictOldest();continue}state.reportedFailure||=(reportLoggerInternalError({context:`localStorage sink record write failed (repeats suppressed until a write next succeeds)`,error}),!0);return}}return{persist}}const NODE_LOCALSTORAGE_FLAG=`--localstorage-file`;function nodeWithoutLocalStorageFile(){if(detectWebStorageRuntime()!==`node`||`document`in globalThis)return!1;let flaggedInExecArgv=process.execArgv.some(function(argument){return argument.startsWith(NODE_LOCALSTORAGE_FLAG)}),flaggedInNodeOptions=(process.env.NODE_OPTIONS??``).includes(NODE_LOCALSTORAGE_FLAG);return!(flaggedInExecArgv||flaggedInNodeOptions)}function verifyLocalStorage(){if(nodeWithoutLocalStorageFile())return Promise.resolve(!1);try{let testKey=`__monochromatic_verify__`,testValue=`test-${Date.now()}`;globalThis.localStorage.setItem(testKey,testValue);let readBack=globalThis.localStorage.getItem(testKey);return globalThis.localStorage.removeItem(testKey),Promise.resolve(readBack===testValue)}catch(error){return`localStorage`in globalThis&&reportLoggerInternalError({context:`localStorage sink verification failed`,error}),Promise.resolve(!1)}}function createLocalStorageSink(){let buffer=createRecordBuffer({onFlush:createLocalStorageStore().persist});function write(record){return buffer.add({level:record.level,serialized:JSON.stringify(record)}),Promise.resolve()}function flush(){return buffer.drain(),Promise.resolve()}return{flush,verify:verifyLocalStorage,write}}const RUNTIME_QUOTA_CHARS={browser:5242880,bun:1/0,deno:10485760,node:5242880,unknown:1/0};function detectSessionStorageQuotaChars(){return RUNTIME_QUOTA_CHARS[detectWebStorageRuntime()]}function storageKey(index){return`monochromatic.log.${index}`}function createSessionStorageStore(){let state={lineCounter:0,oldestIndex:0,usedChars:0,reportedFailure:!1},capChars=detectSessionStorageQuotaChars()/2;function evictOldest(){let key=storageKey(state.oldestIndex),evicted=globalThis.sessionStorage.getItem(key);globalThis.sessionStorage.removeItem(key),state.oldestIndex++,evicted!==null&&(state.usedChars=Math.max(0,state.usedChars-evicted.length))}function persist(batch){let batchChars=batch.length;for(;state.oldestIndex<state.lineCounter&&state.usedChars+batchChars>capChars;)evictOldest();let maxWriteAttempts=state.lineCounter-state.oldestIndex+1;for(let writeAttempt=0;writeAttempt<maxWriteAttempts;writeAttempt++)try{globalThis.sessionStorage.setItem(storageKey(state.lineCounter),batch),state.lineCounter++,state.usedChars+=batchChars,state.reportedFailure=!1;return}catch(error){if(isQuotaExceededError(error)&&state.oldestIndex<state.lineCounter){evictOldest();continue}state.reportedFailure||=(reportLoggerInternalError({context:`sessionStorage sink record write failed (repeats suppressed until a write next succeeds)`,error}),!0);return}}return{persist}}function verifySessionStorage(){try{let testKey=`__monochromatic_verify__`,testValue=`test-${Date.now()}`;globalThis.sessionStorage.setItem(testKey,testValue);let readBack=globalThis.sessionStorage.getItem(testKey);return globalThis.sessionStorage.removeItem(testKey),Promise.resolve(readBack===testValue)}catch(error){return`sessionStorage`in globalThis&&reportLoggerInternalError({context:`sessionStorage sink verification failed`,error}),Promise.resolve(!1)}}function createSessionStorageSink(){let buffer=createRecordBuffer({onFlush:createSessionStorageStore().persist});function write(record){return buffer.add({level:record.level,serialized:JSON.stringify(record)}),Promise.resolve()}function flush(){return buffer.drain(),Promise.resolve()}return{flush,verify:verifySessionStorage,write}}const{initPromise:defaultInitPromise,logger:defaultLogger}=createLogger({sinks:[createConsoleSink(),createIndexedDbSink(),createSessionStorageSink(),createLocalStorageSink(),createFileSink()]}),initPromise=defaultInitPromise,logger=defaultLogger;function verify(){return Promise.resolve(!0)}function write(_record){return Promise.resolve()}function createNoopSink(){return{verify,write}}function createOpfsSink(){let state={},pendingBatchWrites=new Set;async function verify(){try{let opfsRoot=await navigator.storage.getDirectory(),timestamp=new Date().toISOString().replaceAll(`:`,`-`),fileHandle=await opfsRoot.getFileHandle(`monochromatic-${timestamp}.log.jsonl`,{create:!0}),probeWritable=await fileHandle.createWritable({keepExistingData:!0}),testData=`{"test":true,"timestamp":${Date.now()}}\n`;await probeWritable.write(testData),await probeWritable.close();let available=(await(await fileHandle.getFile()).text()).includes(`"test":true`);return available&&(state.writable=await fileHandle.createWritable({keepExistingData:!0})),available}catch(error){let opfsStorage=globalThis.navigator?.storage;return opfsStorage!==void 0&&`getDirectory`in opfsStorage&&reportLoggerInternalError({context:`OPFS sink verification failed`,error}),!1}}async function writeBatch(batch){if(state.writable)try{await state.writable.write(`${batch}\n`)}catch(error){reportLoggerInternalError({context:`OPFS sink record write failed`,error})}}async function removePendingWhenSettled(pending){await pending,pendingBatchWrites.delete(pending)}function handOffBatch(batch){let pending=writeBatch(batch);pendingBatchWrites.add(pending),removePendingWhenSettled(pending)}let buffer=createRecordBuffer({onFlush:handOffBatch});function write(record){return buffer.add({level:record.level,serialized:JSON.stringify(record)}),Promise.resolve()}async function flush(){buffer.drain();let writes=[...pendingBatchWrites];await Promise.all(writes)}return{flush,verify,write}}var sink_exports=__exportAll({NO_NODE_MODULES_FOUND:()=>NO_NODE_MODULES_FOUND,createConsoleSink:()=>createConsoleSink,createFileSink:()=>createFileSink,createIndexedDbSink:()=>createIndexedDbSink,createLocalStorageSink:()=>createLocalStorageSink,createNoopSink:()=>createNoopSink,createOpfsSink:()=>createOpfsSink,createSessionStorageSink:()=>createSessionStorageSink,findNodeModulesUp:()=>findNodeModulesUp});function tagged({tag,l=logger}){let prefix=`[${tag}] `;return{debug:function(message){l.debug(`${prefix}${message}`)},error:function(message){l.error(`${prefix}${message}`)},fatal:function(message){l.fatal(`${prefix}${message}`)},flush:function(){return l.flush()},info:function(message){l.info(`${prefix}${message}`)},trace:function(message){l.trace(`${prefix}${message}`)},warn:function(message){l.warn(`${prefix}${message}`)}}}export{DEFAULT_FLUSH_DEADLINE_MS,DEFAULT_VERIFY_TIMEOUT_MS,STARTUP_BUFFER_CAP,buildLogKey as _buildLogKey,compareLogKeys as _compareLogKeys,createLocalStorageStore as _createLocalStorageStore,createRecordBuffer as _createRecordBuffer,detectLocalStorageQuotaChars as _detectLocalStorageQuotaChars,detectSessionStorageQuotaChars as _detectSessionStorageQuotaChars,isQuotaExceededError as _isQuotaExceededError,neutralizeControlCharacters as _neutralizeControlCharacters,parseLogKey as _parseLogKey,createLogger,initPromise,logger,sink_exports as sinks,tagged};
|
|
@@ -85,6 +85,25 @@ type Logger = {
|
|
|
85
85
|
* {@link createLogger}.
|
|
86
86
|
*/
|
|
87
87
|
export declare const DEFAULT_FLUSH_DEADLINE_MS = 5e3;
|
|
88
|
+
/**
|
|
89
|
+
* Default per-sink `verify()` time limit in milliseconds. Measured on
|
|
90
|
+
* 2026-09-06: the default logger's five shipped verifies complete together in
|
|
91
|
+
* about 2.4 ms locally, so this leaves three orders of magnitude for a slow
|
|
92
|
+
* but working backend probe (a network filesystem, a busy IndexedDB) while a
|
|
93
|
+
* verify that never answers (a hung mount, an IndexedDB open blocked by
|
|
94
|
+
* another tab) can no longer stall startup. Override per logger through the
|
|
95
|
+
* `verifyTimeoutMs` option of {@link createLogger}.
|
|
96
|
+
*/
|
|
97
|
+
export declare const DEFAULT_VERIFY_TIMEOUT_MS = 5e3;
|
|
98
|
+
/**
|
|
99
|
+
* Most records the logger buffers before its sinks have verified. Startup
|
|
100
|
+
* lasts at most {@link DEFAULT_VERIFY_TIMEOUT_MS}, so this bounds the memory a
|
|
101
|
+
* burst during that window can claim; on overflow the oldest buffered record
|
|
102
|
+
* is dropped so the newest (usually most diagnostic) context survives, and
|
|
103
|
+
* one synthetic `warn` record naming the dropped count is written to every
|
|
104
|
+
* available sink once initialization completes.
|
|
105
|
+
*/
|
|
106
|
+
export declare const STARTUP_BUFFER_CAP = 1e4;
|
|
88
107
|
/**
|
|
89
108
|
* Builds a multi-sink logger over the supplied sink adapters. All
|
|
90
109
|
* orchestration (per-sink availability, startup buffering and replay,
|
|
@@ -94,9 +113,14 @@ export declare const DEFAULT_FLUSH_DEADLINE_MS = 5e3;
|
|
|
94
113
|
*
|
|
95
114
|
* Verification runs eagerly at construction and never blocks callers:
|
|
96
115
|
* records emitted while an async sink is still verifying buffer internally
|
|
97
|
-
* and replay to that sink the moment it verifies.
|
|
98
|
-
*
|
|
99
|
-
*
|
|
116
|
+
* and replay to that sink the moment it verifies. Every sink verifies
|
|
117
|
+
* concurrently under its own time limit (`verifyTimeoutMs`, default
|
|
118
|
+
* {@link DEFAULT_VERIFY_TIMEOUT_MS}), so one backend that never answers
|
|
119
|
+
* cannot starve the others or keep the logger from initializing. A sink
|
|
120
|
+
* whose `verify` resolves `false`, throws, or runs past the limit is dropped
|
|
121
|
+
* and receives no records; an answer that arrives after the limit is
|
|
122
|
+
* ignored. A rejected `write` is the sink's own concern and does not disable
|
|
123
|
+
* the backend.
|
|
100
124
|
*
|
|
101
125
|
* `flush()` always resolves: one deadline (`flushDeadlineMs`, default
|
|
102
126
|
* {@link DEFAULT_FLUSH_DEADLINE_MS}) wraps startup verification, the
|
|
@@ -111,6 +135,9 @@ export declare const DEFAULT_FLUSH_DEADLINE_MS = 5e3;
|
|
|
111
135
|
* resolves anyway; raise it for slow but working backends such as network
|
|
112
136
|
* filesystems.
|
|
113
137
|
*
|
|
138
|
+
* @param verifyTimeoutMs - Milliseconds one sink's `verify()` may take before
|
|
139
|
+
* the sink counts as unavailable; raise it for a slow but working probe.
|
|
140
|
+
*
|
|
114
141
|
* @returns Logger plus its eager `initPromise`; callers need not await
|
|
115
142
|
* `initPromise` before logging, since startup records replay on verify.
|
|
116
143
|
*
|
|
@@ -129,9 +156,10 @@ export declare const DEFAULT_FLUSH_DEADLINE_MS = 5e3;
|
|
|
129
156
|
* });
|
|
130
157
|
* ```
|
|
131
158
|
*/
|
|
132
|
-
export declare function createLogger({ sinks, flushDeadlineMs }: {
|
|
159
|
+
export declare function createLogger({ sinks, flushDeadlineMs, verifyTimeoutMs }: {
|
|
133
160
|
readonly sinks: readonly Sink[];
|
|
134
161
|
readonly flushDeadlineMs?: number;
|
|
162
|
+
readonly verifyTimeoutMs?: number;
|
|
135
163
|
}): {
|
|
136
164
|
readonly initPromise: Promise<void>;
|
|
137
165
|
readonly logger: Logger;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{t as __exportAll}from"./rolldown-runtime-5duEfhBv.mjs";async function withTimeout({promise,ms,label}){let{promise:timeoutPromise,reject}=Promise.withResolvers(),timer=setTimeout(function(){reject(Error(`Timed out after ${String(ms)}ms: ${label}`))},ms);using _cleanup={[Symbol.dispose](){clearTimeout(timer)}};return await Promise.race([promise,timeoutPromise])}function caughtValueText(value){return Error.isError(value)?value.message:String(value)}function reportLoggerInternalError({context,error}){console.warn(`logger internal error: ${context}: ${caughtValueText(error)}`)}const DEFAULT_FLUSH_DEADLINE_MS=5e3;async function trackWrite({writePromise}){try{await writePromise}catch(error){reportLoggerInternalError({context:`sink write promise rejected while being tracked`,error})}}function createLogger({sinks,flushDeadlineMs=DEFAULT_FLUSH_DEADLINE_MS}){let entries=sinks.map(function(sink){return{available:!1,sink}}),startupRecords=[],pendingWrites=new Set,state={hasAvailableSink:!1,initialized:!1};function recomputeAvailability(){state.hasAvailableSink=entries.some(function(entry){return entry.available})}function getSinkEntry({entryIndex}){let entry=entries[entryIndex];if(entry===void 0)throw Error(`Missing logger sink entry at index ${entryIndex}.`);return entry}function markEntryUnavailable({entryIndex}){let entry=getSinkEntry({entryIndex});entry.available=!1,recomputeAvailability()}async function removePendingWriteWhenSettled({trackedWrite}){await trackedWrite,pendingWrites.delete(trackedWrite)}function writeRecordToEntry({entryIndex,record}){try{let trackedWrite=trackWrite({writePromise:getSinkEntry({entryIndex}).sink.write(record)});pendingWrites.add(trackedWrite),removePendingWriteWhenSettled({trackedWrite})}catch(error){reportLoggerInternalError({context:`sink write threw synchronously while dispatching record`,error})}}function replayStartupRecordsToEntry({entryIndex}){startupRecords.forEach(function(record){writeRecordToEntry({entryIndex,record})})}function setEntryAvailability({entryIndex,available}){let entry=getSinkEntry({entryIndex});entry.available=available,recomputeAvailability(),available&&replayStartupRecordsToEntry({entryIndex})}async function verifyAndApply({entryIndex}){try{setEntryAvailability({available:await
|
|
2
|
-
`);if(!(level===`debug`&&writeDebugRunToProcessStderr(text)))try{let method=LEVEL_TO_CONSOLE_METHOD[level],consoleFn=console[method];typeof consoleFn==`function`&&consoleFn.call(console,text)}catch(error){reportLoggerInternalError({context:`console method call failed while emitting log run`,error})}}function groupRuns(records){return records.reduce(function(runs,record){let tail=runs.at(-1);return tail!==void 0&&tail.level===record.level?(tail.records.push(record),runs):(runs.push({level:record.level,records:[record]}),runs)},[])}function verifyConsole(){try{return typeof console>`u`||typeof(hasProcessStderr()?console.info:console.debug)!=`function`||typeof queueMicrotask!=`function`?Promise.resolve(!1):Promise.resolve(!0)}catch(error){return reportLoggerInternalError({context:`console sink verification failed`,error}),Promise.resolve(!1)}}function createConsoleSink(){let state={buffer:[],scheduled:!1,verboseCache:VERBOSE_UNCOMPUTED};function getVerbose(){let cached=state.verboseCache;if(cached!==VERBOSE_UNCOMPUTED)return cached;let computed=detectVerbose();return state.verboseCache=computed,computed}function flushBuffer(){if(state.scheduled=!1,state.buffer.length===0)return;let records=state.buffer.splice(0);for(let run of groupRuns(records))emitRun({level:run.level,records:run.records})}function write(record){return!getVerbose()&&SILENT_LEVELS.has(record.level)||record.level===`warn`&&isWarnSuppressed()?Promise.resolve():(state.buffer.push(record),state.scheduled||(state.scheduled=!0,queueMicrotask(flushBuffer)),Promise.resolve())}function flush(){return flushBuffer(),Promise.resolve()}return{flush,verify:verifyConsole,write}}const NO_NODE_MODULES_FOUND=Symbol(`logger:no-node-modules-found`);async function findNodeModulesUp({cwd,stat,dirname,join,reportError=reportLoggerInternalError}){let candidate=join(cwd,`node_modules`);try{if((await stat(candidate)).isDirectory())return candidate}catch(error){Error.isError(error)&&`code`in error&&error.code===`ENOENT`||reportError({context:`node_modules candidate ${candidate} unavailable during file sink search`,error})}let parent=dirname(cwd);return parent===cwd?NO_NODE_MODULES_FOUND:await findNodeModulesUp({cwd:parent,stat,dirname,join,reportError})}function createFileSink(){let state={};async function runVerify(){if(globalThis.process===void 0||globalThis.process.versions?.node===void 0)return!1;try{let fs=await import(`node:fs/promises`),{dirname,join}=await import(`node:path`);state.appendFile=fs.appendFile;let nodeModulesDir=await findNodeModulesUp({cwd:process.cwd(),stat:fs.stat,dirname,join});if(nodeModulesDir===NO_NODE_MODULES_FOUND)return!1;let LOG_DIR=join(nodeModulesDir,`.monochromatic`);await fs.mkdir(LOG_DIR,{recursive:!0});let timestamp=new Date().toISOString().replaceAll(`:`,`-`);state.filePath=join(LOG_DIR,`${timestamp}.log.jsonl`);let testData=`{"test":true,"timestamp":${Date.now()}}\n`;return await state.appendFile(state.filePath,testData),(await fs.readFile(state.filePath,`utf8`)).includes(`"test":true`)}catch(error){return reportLoggerInternalError({context:`file sink verification failed`,error}),!1}}function verify(){return state.verifyPromise===void 0&&(state.verifyPromise=runVerify()),state.verifyPromise}async function write(record){if(
|
|
3
|
-
`);entries.length=0,bufferState.chars=0,onFlush(batch)}function scheduleDeadlineFlush(){if(bufferState.timer!==void 0)return;let timer=globalThis.setTimeout(drain,250);isUnrefableTimer(timer)&&timer.unref(),bufferState.timer=timer}function add(entry){entries.length>0&&charsWith(entry.serialized)>FLUSH_BUFFER_CAP_CHARS&&drain(),bufferState.chars=charsWith(entry.serialized),entries.push(entry.serialized),FLUSH_IMMEDIATELY_BY_LEVEL[entry.level]||bufferState.chars>=FLUSH_BUFFER_CAP_CHARS?drain():scheduleDeadlineFlush()}return globalThis.addEventListener?.(`pagehide`,drain),globalThis.document?.addEventListener(`visibilitychange`,function(){globalThis.document?.visibilityState===`hidden`&&drain()}),{add,drain}}const BATCH_STORE=`batch`,MAX_STORED_BATCHES=2048;async function openLogDatabase(){let request=globalThis.indexedDB.open(`monochromatic.log`,1);return request.onupgradeneeded=function(){request.result.createObjectStore(BATCH_STORE,{autoIncrement:!0})},await awaitRequest(request)}async function persistBatch({database,batch}){let transaction=database.transaction(BATCH_STORE,`readwrite`),store=transaction.objectStore(BATCH_STORE);store.add(batch);let count=await awaitRequest(store.count());if(count>MAX_STORED_BATCHES){let newestStale=(await awaitRequest(store.getAllKeys(null,count-MAX_STORED_BATCHES))).at(-1);newestStale!==void 0&&store.delete(IDBKeyRange.upperBound(newestStale))}await awaitTransaction(transaction)}function createIndexedDbSink(){let state={},pendingBatchWrites=new Set;async function verify(){try{if(globalThis.indexedDB===void 0)return!1;let database=await openLogDatabase(),transaction=database.transaction(BATCH_STORE,`readwrite`),store=transaction.objectStore(BATCH_STORE),probeValue=`probe-${Date.now()}`,probeKey=await awaitRequest(store.add(probeValue)),readBack=await awaitRequest(store.get(probeKey));return store.delete(probeKey),await awaitTransaction(transaction),readBack===probeValue&&(state.database=database,!0)}catch(error){return`indexedDB`in globalThis&&reportLoggerInternalError({context:`IndexedDB sink verification failed`,error}),!1}}async function writeBatch(batch){if(state.database)try{await persistBatch({database:state.database,batch})}catch(error){reportLoggerInternalError({context:`IndexedDB sink record write failed`,error})}}async function removePendingWhenSettled(pending){await pending,pendingBatchWrites.delete(pending)}function handOffBatch(batch){let pending=writeBatch(batch);pendingBatchWrites.add(pending),removePendingWhenSettled(pending)}let buffer=createRecordBuffer({onFlush:handOffBatch});function write(record){return buffer.add({level:record.level,serialized:JSON.stringify(record)}),Promise.resolve()}async function flush(){buffer.drain();let writes=[...pendingBatchWrites];await Promise.all(writes)}return{flush,verify,write}}function isDigits(text){if(text.length===0)return!1;for(let character of text)if(character<`0`||character>`9`)return!1;return!0}function buildLogKey({stamp,nonce,index}){return`monochromatic.log.${stamp}.${nonce}.${index}`}function parseLogKey(key){if(!key.startsWith(`monochromatic.log.`))return{};let segments=key.slice(18).split(`.`);if(segments.length!==3)return{};let[stampText,nonce,indexText]=segments;return stampText===void 0||nonce===void 0||indexText===void 0||!isDigits(stampText)||nonce.length===0||!isDigits(indexText)?{}:{parsed:{key,stamp:Math.trunc(Number(stampText)),nonce,index:Math.trunc(Number(indexText))}}}function compareLogKeys({first,second}){return first.stamp===second.stamp?first.nonce===second.nonce?first.index-second.index:first.nonce<second.nonce?-1:1:first.stamp-second.stamp}function detectWebStorageRuntime(){return`Deno`in globalThis?`deno`:`Bun`in globalThis?`bun`:typeof process<`u`&&typeof process.versions.node==`string`?`node`:`document`in globalThis?`browser`:`unknown`}const RUNTIME_QUOTA_CHARS$1={browser:5242880,bun:1/0,deno:10477569,node:5242880,unknown:1/0};function detectLocalStorageQuotaChars(){return RUNTIME_QUOTA_CHARS$1[detectWebStorageRuntime()]}const QUOTA_EXCEEDED_NAMES=new Set([`QuotaExceededError`,`NS_ERROR_DOM_QUOTA_REACHED`]);function isQuotaExceededError(error){return typeof error==`object`&&!!error&&`name`in error&&typeof error.name==`string`&"A_EXCEEDED_NAMES.has(error.name)}function createLocalStorageStore(){let runIdentity={stamp:Date.now(),nonce:Math.random().toString(36).slice(2,6).padEnd(4,`0`)},state={lineCounter:0,oldestIndex:0,usedChars:0,reportedFailure:!1,adoptedPrior:!1},prior={entries:[],cursor:0},capChars=detectLocalStorageQuotaChars()/2;function ownKey(index){return buildLogKey({stamp:runIdentity.stamp,nonce:runIdentity.nonce,index})}function adoptPriorEntries(){let total=globalThis.localStorage.length,found=[];for(let slot=0;slot<total;slot++){let key=globalThis.localStorage.key(slot);if(key===null)continue;let{parsed}=parseLogKey(key);if(parsed===void 0||parsed.stamp===runIdentity.stamp&&parsed.nonce===runIdentity.nonce)continue;let value=globalThis.localStorage.getItem(key);value!==null&&found.push({...parsed,chars:value.length})}prior.entries=found.toSorted(function(first,second){return compareLogKeys({first,second})}),state.usedChars+=found.reduce(function(sum,entry){return sum+entry.chars},0)}function hasEvictable(){let priorCount=prior.entries.length;return prior.cursor<priorCount||state.oldestIndex<state.lineCounter}function evictOldestPrior(){let entry=prior.entries[prior.cursor];return entry!==void 0&&(prior.cursor++,globalThis.localStorage.removeItem(entry.key),state.usedChars=Math.max(0,state.usedChars-entry.chars),!0)}function evictOldestOwn(){let key=ownKey(state.oldestIndex),evicted=globalThis.localStorage.getItem(key);globalThis.localStorage.removeItem(key),state.oldestIndex++,evicted!==null&&(state.usedChars=Math.max(0,state.usedChars-evicted.length))}function evictOldest(){evictOldestPrior()||state.oldestIndex<state.lineCounter&&evictOldestOwn()}function persist(batch){state.adoptedPrior||(state.adoptedPrior=!0,adoptPriorEntries());let batchChars=batch.length;for(;hasEvictable()&&state.usedChars+batchChars>capChars;)evictOldest();let maxWriteAttempts=prior.entries.length-prior.cursor+(state.lineCounter-state.oldestIndex)+1;for(let writeAttempt=0;writeAttempt<maxWriteAttempts;writeAttempt++)try{globalThis.localStorage.setItem(ownKey(state.lineCounter),batch),state.lineCounter++,state.usedChars+=batchChars,state.reportedFailure=!1;return}catch(error){if(isQuotaExceededError(error)&&hasEvictable()){evictOldest();continue}state.reportedFailure||=(reportLoggerInternalError({context:`localStorage sink record write failed (repeats suppressed until a write next succeeds)`,error}),!0);return}}return{persist}}const NODE_LOCALSTORAGE_FLAG=`--localstorage-file`;function nodeWithoutLocalStorageFile(){if(detectWebStorageRuntime()!==`node`||`document`in globalThis)return!1;let flaggedInExecArgv=process.execArgv.some(function(argument){return argument.startsWith(NODE_LOCALSTORAGE_FLAG)}),flaggedInNodeOptions=(process.env.NODE_OPTIONS??``).includes(NODE_LOCALSTORAGE_FLAG);return!(flaggedInExecArgv||flaggedInNodeOptions)}function verifyLocalStorage(){if(nodeWithoutLocalStorageFile())return Promise.resolve(!1);try{let testKey=`__monochromatic_verify__`,testValue=`test-${Date.now()}`;globalThis.localStorage.setItem(testKey,testValue);let readBack=globalThis.localStorage.getItem(testKey);return globalThis.localStorage.removeItem(testKey),Promise.resolve(readBack===testValue)}catch(error){return`localStorage`in globalThis&&reportLoggerInternalError({context:`localStorage sink verification failed`,error}),Promise.resolve(!1)}}function createLocalStorageSink(){let buffer=createRecordBuffer({onFlush:createLocalStorageStore().persist});function write(record){return buffer.add({level:record.level,serialized:JSON.stringify(record)}),Promise.resolve()}function flush(){return buffer.drain(),Promise.resolve()}return{flush,verify:verifyLocalStorage,write}}const RUNTIME_QUOTA_CHARS={browser:5242880,bun:1/0,deno:10485760,node:5242880,unknown:1/0};function detectSessionStorageQuotaChars(){return RUNTIME_QUOTA_CHARS[detectWebStorageRuntime()]}function storageKey(index){return`monochromatic.log.${index}`}function createSessionStorageStore(){let state={lineCounter:0,oldestIndex:0,usedChars:0,reportedFailure:!1},capChars=detectSessionStorageQuotaChars()/2;function evictOldest(){let key=storageKey(state.oldestIndex),evicted=globalThis.sessionStorage.getItem(key);globalThis.sessionStorage.removeItem(key),state.oldestIndex++,evicted!==null&&(state.usedChars=Math.max(0,state.usedChars-evicted.length))}function persist(batch){let batchChars=batch.length;for(;state.oldestIndex<state.lineCounter&&state.usedChars+batchChars>capChars;)evictOldest();let maxWriteAttempts=state.lineCounter-state.oldestIndex+1;for(let writeAttempt=0;writeAttempt<maxWriteAttempts;writeAttempt++)try{globalThis.sessionStorage.setItem(storageKey(state.lineCounter),batch),state.lineCounter++,state.usedChars+=batchChars,state.reportedFailure=!1;return}catch(error){if(isQuotaExceededError(error)&&state.oldestIndex<state.lineCounter){evictOldest();continue}state.reportedFailure||=(reportLoggerInternalError({context:`sessionStorage sink record write failed (repeats suppressed until a write next succeeds)`,error}),!0);return}}return{persist}}function verifySessionStorage(){try{let testKey=`__monochromatic_verify__`,testValue=`test-${Date.now()}`;globalThis.sessionStorage.setItem(testKey,testValue);let readBack=globalThis.sessionStorage.getItem(testKey);return globalThis.sessionStorage.removeItem(testKey),Promise.resolve(readBack===testValue)}catch(error){return`sessionStorage`in globalThis&&reportLoggerInternalError({context:`sessionStorage sink verification failed`,error}),Promise.resolve(!1)}}function createSessionStorageSink(){let buffer=createRecordBuffer({onFlush:createSessionStorageStore().persist});function write(record){return buffer.add({level:record.level,serialized:JSON.stringify(record)}),Promise.resolve()}function flush(){return buffer.drain(),Promise.resolve()}return{flush,verify:verifySessionStorage,write}}const{initPromise:defaultInitPromise,logger:defaultLogger}=createLogger({sinks:[createConsoleSink(),createIndexedDbSink(),createSessionStorageSink(),createLocalStorageSink(),createFileSink()]}),initPromise=defaultInitPromise,logger=defaultLogger;function verify(){return Promise.resolve(!0)}function write(_record){return Promise.resolve()}function createNoopSink(){return{verify,write}}function createOpfsSink(){let state={},pendingBatchWrites=new Set;async function verify(){try{let opfsRoot=await navigator.storage.getDirectory(),timestamp=new Date().toISOString().replaceAll(`:`,`-`),fileHandle=await opfsRoot.getFileHandle(`monochromatic-${timestamp}.log.jsonl`,{create:!0}),probeWritable=await fileHandle.createWritable({keepExistingData:!0}),testData=`{"test":true,"timestamp":${Date.now()}}\n`;await probeWritable.write(testData),await probeWritable.close();let available=(await(await fileHandle.getFile()).text()).includes(`"test":true`);return available&&(state.writable=await fileHandle.createWritable({keepExistingData:!0})),available}catch(error){let opfsStorage=globalThis.navigator?.storage;return opfsStorage!==void 0&&`getDirectory`in opfsStorage&&reportLoggerInternalError({context:`OPFS sink verification failed`,error}),!1}}async function writeBatch(batch){if(state.writable)try{await state.writable.write(`${batch}\n`)}catch(error){reportLoggerInternalError({context:`OPFS sink record write failed`,error})}}async function removePendingWhenSettled(pending){await pending,pendingBatchWrites.delete(pending)}function handOffBatch(batch){let pending=writeBatch(batch);pendingBatchWrites.add(pending),removePendingWhenSettled(pending)}let buffer=createRecordBuffer({onFlush:handOffBatch});function write(record){return buffer.add({level:record.level,serialized:JSON.stringify(record)}),Promise.resolve()}async function flush(){buffer.drain();let writes=[...pendingBatchWrites];await Promise.all(writes)}return{flush,verify,write}}var sink_exports=__exportAll({NO_NODE_MODULES_FOUND:()=>NO_NODE_MODULES_FOUND,createConsoleSink:()=>createConsoleSink,createFileSink:()=>createFileSink,createIndexedDbSink:()=>createIndexedDbSink,createLocalStorageSink:()=>createLocalStorageSink,createNoopSink:()=>createNoopSink,createOpfsSink:()=>createOpfsSink,createSessionStorageSink:()=>createSessionStorageSink,findNodeModulesUp:()=>findNodeModulesUp});function tagged({tag,l=logger}){let prefix=`[${tag}] `;return{debug:function(message){l.debug(`${prefix}${message}`)},error:function(message){l.error(`${prefix}${message}`)},fatal:function(message){l.fatal(`${prefix}${message}`)},flush:function(){return l.flush()},info:function(message){l.info(`${prefix}${message}`)},trace:function(message){l.trace(`${prefix}${message}`)},warn:function(message){l.warn(`${prefix}${message}`)}}}export{DEFAULT_FLUSH_DEADLINE_MS,buildLogKey as _buildLogKey,compareLogKeys as _compareLogKeys,createLocalStorageStore as _createLocalStorageStore,createRecordBuffer as _createRecordBuffer,detectLocalStorageQuotaChars as _detectLocalStorageQuotaChars,detectSessionStorageQuotaChars as _detectSessionStorageQuotaChars,isQuotaExceededError as _isQuotaExceededError,neutralizeControlCharacters as _neutralizeControlCharacters,parseLogKey as _parseLogKey,createLogger,initPromise,logger,sink_exports as sinks,tagged};
|
|
1
|
+
import{t as __exportAll}from"./rolldown-runtime-5duEfhBv.mjs";async function withTimeout({promise,ms,label}){let{promise:timeoutPromise,reject}=Promise.withResolvers(),timer=setTimeout(function(){reject(Error(`Timed out after ${String(ms)}ms: ${label}`))},ms);using _cleanup={[Symbol.dispose](){clearTimeout(timer)}};return await Promise.race([promise,timeoutPromise])}function caughtValueText(value){return Error.isError(value)?value.message:String(value)}function reportLoggerInternalError({context,error}){console.warn(`logger internal error: ${context}: ${caughtValueText(error)}`)}const DEFAULT_FLUSH_DEADLINE_MS=5e3,DEFAULT_VERIFY_TIMEOUT_MS=5e3,STARTUP_BUFFER_CAP=1e4;async function trackWrite({writePromise}){try{await writePromise}catch(error){reportLoggerInternalError({context:`sink write promise rejected while being tracked`,error})}}function createLogger({sinks,flushDeadlineMs=DEFAULT_FLUSH_DEADLINE_MS,verifyTimeoutMs=DEFAULT_VERIFY_TIMEOUT_MS}){let entries=sinks.map(function(sink){return{available:!1,sink}}),startupRecords=[],pendingWrites=new Set,state={droppedStartupRecords:0,hasAvailableSink:!1,initialized:!1};function bufferStartupRecord({record}){startupRecords.length>=1e4&&(startupRecords.shift(),state.droppedStartupRecords+=1),startupRecords.push(record)}function recomputeAvailability(){state.hasAvailableSink=entries.some(function(entry){return entry.available})}function getSinkEntry({entryIndex}){let entry=entries[entryIndex];if(entry===void 0)throw Error(`Missing logger sink entry at index ${entryIndex}.`);return entry}function markEntryUnavailable({entryIndex}){let entry=getSinkEntry({entryIndex});entry.available=!1,recomputeAvailability()}async function removePendingWriteWhenSettled({trackedWrite}){await trackedWrite,pendingWrites.delete(trackedWrite)}function writeRecordToEntry({entryIndex,record}){try{let trackedWrite=trackWrite({writePromise:getSinkEntry({entryIndex}).sink.write(record)});pendingWrites.add(trackedWrite),removePendingWriteWhenSettled({trackedWrite})}catch(error){reportLoggerInternalError({context:`sink write threw synchronously while dispatching record`,error})}}function replayStartupRecordsToEntry({entryIndex}){startupRecords.forEach(function(record){writeRecordToEntry({entryIndex,record})})}function setEntryAvailability({entryIndex,available}){let entry=getSinkEntry({entryIndex});entry.available=available,recomputeAvailability(),available&&replayStartupRecordsToEntry({entryIndex})}async function verifyAndApply({entryIndex}){try{let entry=getSinkEntry({entryIndex});setEntryAvailability({available:await withTimeout({label:`sink ${entryIndex} verify`,ms:verifyTimeoutMs,promise:entry.sink.verify()}),entryIndex})}catch(error){reportLoggerInternalError({context:`sink verification failed for entry ${entryIndex}`,error}),markEntryUnavailable({entryIndex})}}function emitDroppedStartupMarker(){let dropped=state.droppedStartupRecords;if(dropped===0)return;let marker={level:`warn`,message:`${dropped} startup record${dropped===1?``:`s`} dropped before a backend verified (buffer cap ${STARTUP_BUFFER_CAP})`,timestamp:Date.now()};entries.forEach(function(entry,entryIndex){entry.available&&writeRecordToEntry({entryIndex,record:marker})})}async function initialize(){state.initialized||(await Promise.all(entries.map(function(_entry,entryIndex){return verifyAndApply({entryIndex})})),state.initialized=!0,startupRecords.length=0,emitDroppedStartupMarker())}let initPromise=initialize();async function drainPendingWrites(){let writes=[...pendingWrites];await Promise.all(writes)}function createMethod(level){return function(message){if(!state.hasAvailableSink&&state.initialized)throw Error(`No logging backends available`);let record={level,message,timestamp:Date.now()};state.initialized||bufferStartupRecord({record}),entries.map(function(_entry,entryIndex){return entryIndex}).filter(function(entryIndex){return getSinkEntry({entryIndex}).available}).forEach(function(entryIndex){writeRecordToEntry({entryIndex,record})})}}async function runSinkFlushHooks(){await Promise.all(entries.map(async function(entry,entryIndex){let sinkFlush=entry.sink.flush;if(entry.available&&typeof sinkFlush==`function`)try{await sinkFlush()}catch(error){reportLoggerInternalError({context:`sink flush failed for entry ${entryIndex}`,error}),markEntryUnavailable({entryIndex})}}))}async function drainEverything(){await initPromise,await drainPendingWrites(),await runSinkFlushHooks()}function abandonPendingWrites(){pendingWrites.clear()}async function flushAll(){try{await withTimeout({label:`logger flush`,ms:flushDeadlineMs,promise:drainEverything()})}catch(error){reportLoggerInternalError({context:`flush deadline of ${flushDeadlineMs}ms elapsed; abandoning in-flight sink work`,error}),abandonPendingWrites()}}return{initPromise,logger:{debug:createMethod(`debug`),error:createMethod(`error`),fatal:createMethod(`fatal`),flush:flushAll,info:createMethod(`info`),trace:createMethod(`trace`),warn:createMethod(`warn`)}}}function isNeutralizedControl(codeUnit){return codeUnit<32?codeUnit!==10&&codeUnit!==9:codeUnit===127||codeUnit>=128&&codeUnit<=159}function escapeCodeUnit(codeUnit){return`\\u${codeUnit.toString(16).toUpperCase().padStart(4,`0`)}`}function neutralizeControlCharacters(text){let pieces=[];for(let character of text){let codeUnit=character.charCodeAt(0);pieces.push(isNeutralizedControl(codeUnit)?escapeCodeUnit(codeUnit):character)}return pieces.join(``)}const VERBOSE_UNCOMPUTED=Symbol(`logger:verbose-detection-uncomputed`),SILENT_LEVELS=new Set([`debug`,`trace`]);function detectVerbose(){try{if(typeof process<`u`&&process.env.MONOCHROMATIC_VERBOSE===`true`)return!0}catch(error){reportLoggerInternalError({context:`MONOCHROMATIC_VERBOSE environment probe failed during verbose detection`,error})}try{if(typeof process<`u`&&Array.isArray(process.argv)&&process.argv.includes(`--verbose`))return!0}catch(error){reportLoggerInternalError({context:`process argv probe failed during verbose detection`,error})}try{if(`window`in globalThis)return!0}catch(error){reportLoggerInternalError({context:`window probe failed during verbose detection`,error})}return!1}function isWarnSuppressed(){try{return typeof process<`u`&&process.env.MONOCHROMATIC_WARN===`false`}catch(error){return reportLoggerInternalError({context:`MONOCHROMATIC_WARN environment probe failed during warn suppression detection`,error}),!1}}const LEVEL_TO_CONSOLE_METHOD={debug:`debug`,error:`error`,fatal:`error`,info:`info`,trace:`trace`,warn:`warn`};function formatRecord(record){return`[${record.level}] [${new Date(record.timestamp).toISOString()}] ${neutralizeControlCharacters(record.message)}`}function hasProcessStderr(){try{return typeof process>`u`?!1:typeof process.stderr.write==`function`}catch(error){return reportLoggerInternalError({context:`process stderr availability probe failed`,error}),!1}}function writeDebugRunToProcessStderr(text){try{return hasProcessStderr()?(process.stderr.write(`${text}\n`),!0):!1}catch(error){return reportLoggerInternalError({context:`debug run process stderr write failed`,error}),!1}}function emitRun({records,level}){let text=records.map(function(r){return formatRecord(r)}).join(`
|
|
2
|
+
`);if(!(level===`debug`&&writeDebugRunToProcessStderr(text)))try{let method=LEVEL_TO_CONSOLE_METHOD[level],consoleFn=console[method];typeof consoleFn==`function`&&consoleFn.call(console,text)}catch(error){reportLoggerInternalError({context:`console method call failed while emitting log run`,error})}}function groupRuns(records){return records.reduce(function(runs,record){let tail=runs.at(-1);return tail!==void 0&&tail.level===record.level?(tail.records.push(record),runs):(runs.push({level:record.level,records:[record]}),runs)},[])}function verifyConsole(){try{return typeof console>`u`||typeof(hasProcessStderr()?console.info:console.debug)!=`function`||typeof queueMicrotask!=`function`?Promise.resolve(!1):Promise.resolve(!0)}catch(error){return reportLoggerInternalError({context:`console sink verification failed`,error}),Promise.resolve(!1)}}function createConsoleSink(){let state={buffer:[],scheduled:!1,verboseCache:VERBOSE_UNCOMPUTED};function getVerbose(){let cached=state.verboseCache;if(cached!==VERBOSE_UNCOMPUTED)return cached;let computed=detectVerbose();return state.verboseCache=computed,computed}function flushBuffer(){if(state.scheduled=!1,state.buffer.length===0)return;let records=state.buffer.splice(0);for(let run of groupRuns(records))emitRun({level:run.level,records:run.records})}function write(record){return!getVerbose()&&SILENT_LEVELS.has(record.level)||record.level===`warn`&&isWarnSuppressed()?Promise.resolve():(state.buffer.push(record),state.scheduled||(state.scheduled=!0,queueMicrotask(flushBuffer)),Promise.resolve())}function flush(){return flushBuffer(),Promise.resolve()}return{flush,verify:verifyConsole,write}}const NO_NODE_MODULES_FOUND=Symbol(`logger:no-node-modules-found`);async function findNodeModulesUp({cwd,stat,dirname,join,reportError=reportLoggerInternalError}){let candidate=join(cwd,`node_modules`);try{if((await stat(candidate)).isDirectory())return candidate}catch(error){Error.isError(error)&&`code`in error&&error.code===`ENOENT`||reportError({context:`node_modules candidate ${candidate} unavailable during file sink search`,error})}let parent=dirname(cwd);return parent===cwd?NO_NODE_MODULES_FOUND:await findNodeModulesUp({cwd:parent,stat,dirname,join,reportError})}function createFileSink(){let state={};async function runVerify(){if(globalThis.process===void 0||globalThis.process.versions?.node===void 0)return!1;try{let fs=await import(`node:fs/promises`),{dirname,join}=await import(`node:path`);state.appendFile=fs.appendFile;let nodeModulesDir=await findNodeModulesUp({cwd:process.cwd(),stat:fs.stat,dirname,join});if(nodeModulesDir===NO_NODE_MODULES_FOUND)return!1;let LOG_DIR=join(nodeModulesDir,`.monochromatic`);await fs.mkdir(LOG_DIR,{recursive:!0});let timestamp=new Date().toISOString().replaceAll(`:`,`-`);state.filePath=join(LOG_DIR,`${timestamp}.log.jsonl`);let testData=`{"test":true,"timestamp":${Date.now()}}\n`;return await state.appendFile(state.filePath,testData),(await fs.readFile(state.filePath,`utf8`)).includes(`"test":true`)}catch(error){return reportLoggerInternalError({context:`file sink verification failed`,error}),!1}}function verify(){return state.verifyPromise===void 0&&(state.verifyPromise=runVerify()),state.verifyPromise}async function write(record){if(state.filePath&&state.appendFile)try{await state.appendFile(state.filePath,`${JSON.stringify(record)}\n`)}catch(error){reportLoggerInternalError({context:`file sink record append failed`,error})}}return{verify,write}}function awaitRequest(request){return new Promise(function(resolve,reject){request.addEventListener(`success`,function(){resolve(request.result)}),request.addEventListener(`error`,function(){reject(request.error??Error(`IndexedDB request failed without an error object`))})})}function awaitTransaction(transaction){return new Promise(function(resolve,reject){transaction.addEventListener(`complete`,function(){resolve()}),transaction.addEventListener(`error`,function(){reject(transaction.error??Error(`IndexedDB transaction failed without an error object`))}),transaction.addEventListener(`abort`,function(){reject(transaction.error??Error(`IndexedDB transaction aborted without an error object`))})})}const FLUSH_BUFFER_CAP_CHARS=32768,FLUSH_IMMEDIATELY_BY_LEVEL={debug:!1,error:!0,fatal:!0,info:!1,trace:!1,warn:!0};function isUnrefableTimer(timer){return typeof timer!=`object`||!timer||!(`unref`in timer)?!1:typeof timer.unref==`function`}function createRecordBuffer({onFlush}){let entries=[],bufferState={chars:0};function charsWith(serialized){let separatorChars=+(entries.length>0);return bufferState.chars+separatorChars+serialized.length}function drain(){if(bufferState.timer!==void 0&&(globalThis.clearTimeout(bufferState.timer),delete bufferState.timer),entries.length===0)return;let batch=entries.join(`
|
|
3
|
+
`);entries.length=0,bufferState.chars=0,onFlush(batch)}function scheduleDeadlineFlush(){if(bufferState.timer!==void 0)return;let timer=globalThis.setTimeout(drain,250);isUnrefableTimer(timer)&&timer.unref(),bufferState.timer=timer}function add(entry){entries.length>0&&charsWith(entry.serialized)>FLUSH_BUFFER_CAP_CHARS&&drain(),bufferState.chars=charsWith(entry.serialized),entries.push(entry.serialized),FLUSH_IMMEDIATELY_BY_LEVEL[entry.level]||bufferState.chars>=FLUSH_BUFFER_CAP_CHARS?drain():scheduleDeadlineFlush()}return globalThis.addEventListener?.(`pagehide`,drain),globalThis.document?.addEventListener(`visibilitychange`,function(){globalThis.document?.visibilityState===`hidden`&&drain()}),{add,drain}}const BATCH_STORE=`batch`,MAX_STORED_BATCHES=2048;async function openLogDatabase(){let request=globalThis.indexedDB.open(`monochromatic.log`,1);return request.onupgradeneeded=function(){request.result.createObjectStore(BATCH_STORE,{autoIncrement:!0})},await awaitRequest(request)}async function persistBatch({database,batch}){let transaction=database.transaction(BATCH_STORE,`readwrite`),store=transaction.objectStore(BATCH_STORE);store.add(batch);let count=await awaitRequest(store.count());if(count>MAX_STORED_BATCHES){let newestStale=(await awaitRequest(store.getAllKeys(null,count-MAX_STORED_BATCHES))).at(-1);newestStale!==void 0&&store.delete(IDBKeyRange.upperBound(newestStale))}await awaitTransaction(transaction)}function createIndexedDbSink(){let state={},pendingBatchWrites=new Set;async function verify(){try{if(globalThis.indexedDB===void 0)return!1;let database=await openLogDatabase(),transaction=database.transaction(BATCH_STORE,`readwrite`),store=transaction.objectStore(BATCH_STORE),probeValue=`probe-${Date.now()}`,probeKey=await awaitRequest(store.add(probeValue)),readBack=await awaitRequest(store.get(probeKey));return store.delete(probeKey),await awaitTransaction(transaction),readBack===probeValue&&(state.database=database,!0)}catch(error){return`indexedDB`in globalThis&&reportLoggerInternalError({context:`IndexedDB sink verification failed`,error}),!1}}async function writeBatch(batch){if(state.database)try{await persistBatch({database:state.database,batch})}catch(error){reportLoggerInternalError({context:`IndexedDB sink record write failed`,error})}}async function removePendingWhenSettled(pending){await pending,pendingBatchWrites.delete(pending)}function handOffBatch(batch){let pending=writeBatch(batch);pendingBatchWrites.add(pending),removePendingWhenSettled(pending)}let buffer=createRecordBuffer({onFlush:handOffBatch});function write(record){return buffer.add({level:record.level,serialized:JSON.stringify(record)}),Promise.resolve()}async function flush(){buffer.drain();let writes=[...pendingBatchWrites];await Promise.all(writes)}return{flush,verify,write}}function isDigits(text){if(text.length===0)return!1;for(let character of text)if(character<`0`||character>`9`)return!1;return!0}function buildLogKey({stamp,nonce,index}){return`monochromatic.log.${stamp}.${nonce}.${index}`}function parseLogKey(key){if(!key.startsWith(`monochromatic.log.`))return{};let segments=key.slice(18).split(`.`);if(segments.length!==3)return{};let[stampText,nonce,indexText]=segments;return stampText===void 0||nonce===void 0||indexText===void 0||!isDigits(stampText)||nonce.length===0||!isDigits(indexText)?{}:{parsed:{key,stamp:Math.trunc(Number(stampText)),nonce,index:Math.trunc(Number(indexText))}}}function compareLogKeys({first,second}){return first.stamp===second.stamp?first.nonce===second.nonce?first.index-second.index:first.nonce<second.nonce?-1:1:first.stamp-second.stamp}function detectWebStorageRuntime(){return`Deno`in globalThis?`deno`:`Bun`in globalThis?`bun`:typeof process<`u`&&typeof process.versions.node==`string`?`node`:`document`in globalThis?`browser`:`unknown`}const RUNTIME_QUOTA_CHARS$1={browser:5242880,bun:1/0,deno:10477569,node:5242880,unknown:1/0};function detectLocalStorageQuotaChars(){return RUNTIME_QUOTA_CHARS$1[detectWebStorageRuntime()]}const QUOTA_EXCEEDED_NAMES=new Set([`QuotaExceededError`,`NS_ERROR_DOM_QUOTA_REACHED`]);function isQuotaExceededError(error){return typeof error==`object`&&!!error&&`name`in error&&typeof error.name==`string`&"A_EXCEEDED_NAMES.has(error.name)}function createLocalStorageStore(){let runIdentity={stamp:Date.now(),nonce:Math.random().toString(36).slice(2,6).padEnd(4,`0`)},state={lineCounter:0,oldestIndex:0,usedChars:0,reportedFailure:!1,adoptedPrior:!1},prior={entries:[],cursor:0},capChars=detectLocalStorageQuotaChars()/2;function ownKey(index){return buildLogKey({stamp:runIdentity.stamp,nonce:runIdentity.nonce,index})}function adoptPriorEntries(){let total=globalThis.localStorage.length,found=[];for(let slot=0;slot<total;slot++){let key=globalThis.localStorage.key(slot);if(key===null)continue;let{parsed}=parseLogKey(key);if(parsed===void 0||parsed.stamp===runIdentity.stamp&&parsed.nonce===runIdentity.nonce)continue;let value=globalThis.localStorage.getItem(key);value!==null&&found.push({...parsed,chars:value.length})}prior.entries=found.toSorted(function(first,second){return compareLogKeys({first,second})}),state.usedChars+=found.reduce(function(sum,entry){return sum+entry.chars},0)}function hasEvictable(){let priorCount=prior.entries.length;return prior.cursor<priorCount||state.oldestIndex<state.lineCounter}function evictOldestPrior(){let entry=prior.entries[prior.cursor];return entry!==void 0&&(prior.cursor++,globalThis.localStorage.removeItem(entry.key),state.usedChars=Math.max(0,state.usedChars-entry.chars),!0)}function evictOldestOwn(){let key=ownKey(state.oldestIndex),evicted=globalThis.localStorage.getItem(key);globalThis.localStorage.removeItem(key),state.oldestIndex++,evicted!==null&&(state.usedChars=Math.max(0,state.usedChars-evicted.length))}function evictOldest(){evictOldestPrior()||state.oldestIndex<state.lineCounter&&evictOldestOwn()}function persist(batch){state.adoptedPrior||(state.adoptedPrior=!0,adoptPriorEntries());let batchChars=batch.length;for(;hasEvictable()&&state.usedChars+batchChars>capChars;)evictOldest();let maxWriteAttempts=prior.entries.length-prior.cursor+(state.lineCounter-state.oldestIndex)+1;for(let writeAttempt=0;writeAttempt<maxWriteAttempts;writeAttempt++)try{globalThis.localStorage.setItem(ownKey(state.lineCounter),batch),state.lineCounter++,state.usedChars+=batchChars,state.reportedFailure=!1;return}catch(error){if(isQuotaExceededError(error)&&hasEvictable()){evictOldest();continue}state.reportedFailure||=(reportLoggerInternalError({context:`localStorage sink record write failed (repeats suppressed until a write next succeeds)`,error}),!0);return}}return{persist}}const NODE_LOCALSTORAGE_FLAG=`--localstorage-file`;function nodeWithoutLocalStorageFile(){if(detectWebStorageRuntime()!==`node`||`document`in globalThis)return!1;let flaggedInExecArgv=process.execArgv.some(function(argument){return argument.startsWith(NODE_LOCALSTORAGE_FLAG)}),flaggedInNodeOptions=(process.env.NODE_OPTIONS??``).includes(NODE_LOCALSTORAGE_FLAG);return!(flaggedInExecArgv||flaggedInNodeOptions)}function verifyLocalStorage(){if(nodeWithoutLocalStorageFile())return Promise.resolve(!1);try{let testKey=`__monochromatic_verify__`,testValue=`test-${Date.now()}`;globalThis.localStorage.setItem(testKey,testValue);let readBack=globalThis.localStorage.getItem(testKey);return globalThis.localStorage.removeItem(testKey),Promise.resolve(readBack===testValue)}catch(error){return`localStorage`in globalThis&&reportLoggerInternalError({context:`localStorage sink verification failed`,error}),Promise.resolve(!1)}}function createLocalStorageSink(){let buffer=createRecordBuffer({onFlush:createLocalStorageStore().persist});function write(record){return buffer.add({level:record.level,serialized:JSON.stringify(record)}),Promise.resolve()}function flush(){return buffer.drain(),Promise.resolve()}return{flush,verify:verifyLocalStorage,write}}const RUNTIME_QUOTA_CHARS={browser:5242880,bun:1/0,deno:10485760,node:5242880,unknown:1/0};function detectSessionStorageQuotaChars(){return RUNTIME_QUOTA_CHARS[detectWebStorageRuntime()]}function storageKey(index){return`monochromatic.log.${index}`}function createSessionStorageStore(){let state={lineCounter:0,oldestIndex:0,usedChars:0,reportedFailure:!1},capChars=detectSessionStorageQuotaChars()/2;function evictOldest(){let key=storageKey(state.oldestIndex),evicted=globalThis.sessionStorage.getItem(key);globalThis.sessionStorage.removeItem(key),state.oldestIndex++,evicted!==null&&(state.usedChars=Math.max(0,state.usedChars-evicted.length))}function persist(batch){let batchChars=batch.length;for(;state.oldestIndex<state.lineCounter&&state.usedChars+batchChars>capChars;)evictOldest();let maxWriteAttempts=state.lineCounter-state.oldestIndex+1;for(let writeAttempt=0;writeAttempt<maxWriteAttempts;writeAttempt++)try{globalThis.sessionStorage.setItem(storageKey(state.lineCounter),batch),state.lineCounter++,state.usedChars+=batchChars,state.reportedFailure=!1;return}catch(error){if(isQuotaExceededError(error)&&state.oldestIndex<state.lineCounter){evictOldest();continue}state.reportedFailure||=(reportLoggerInternalError({context:`sessionStorage sink record write failed (repeats suppressed until a write next succeeds)`,error}),!0);return}}return{persist}}function verifySessionStorage(){try{let testKey=`__monochromatic_verify__`,testValue=`test-${Date.now()}`;globalThis.sessionStorage.setItem(testKey,testValue);let readBack=globalThis.sessionStorage.getItem(testKey);return globalThis.sessionStorage.removeItem(testKey),Promise.resolve(readBack===testValue)}catch(error){return`sessionStorage`in globalThis&&reportLoggerInternalError({context:`sessionStorage sink verification failed`,error}),Promise.resolve(!1)}}function createSessionStorageSink(){let buffer=createRecordBuffer({onFlush:createSessionStorageStore().persist});function write(record){return buffer.add({level:record.level,serialized:JSON.stringify(record)}),Promise.resolve()}function flush(){return buffer.drain(),Promise.resolve()}return{flush,verify:verifySessionStorage,write}}const{initPromise:defaultInitPromise,logger:defaultLogger}=createLogger({sinks:[createConsoleSink(),createIndexedDbSink(),createSessionStorageSink(),createLocalStorageSink(),createFileSink()]}),initPromise=defaultInitPromise,logger=defaultLogger;function verify(){return Promise.resolve(!0)}function write(_record){return Promise.resolve()}function createNoopSink(){return{verify,write}}function createOpfsSink(){let state={},pendingBatchWrites=new Set;async function verify(){try{let opfsRoot=await navigator.storage.getDirectory(),timestamp=new Date().toISOString().replaceAll(`:`,`-`),fileHandle=await opfsRoot.getFileHandle(`monochromatic-${timestamp}.log.jsonl`,{create:!0}),probeWritable=await fileHandle.createWritable({keepExistingData:!0}),testData=`{"test":true,"timestamp":${Date.now()}}\n`;await probeWritable.write(testData),await probeWritable.close();let available=(await(await fileHandle.getFile()).text()).includes(`"test":true`);return available&&(state.writable=await fileHandle.createWritable({keepExistingData:!0})),available}catch(error){let opfsStorage=globalThis.navigator?.storage;return opfsStorage!==void 0&&`getDirectory`in opfsStorage&&reportLoggerInternalError({context:`OPFS sink verification failed`,error}),!1}}async function writeBatch(batch){if(state.writable)try{await state.writable.write(`${batch}\n`)}catch(error){reportLoggerInternalError({context:`OPFS sink record write failed`,error})}}async function removePendingWhenSettled(pending){await pending,pendingBatchWrites.delete(pending)}function handOffBatch(batch){let pending=writeBatch(batch);pendingBatchWrites.add(pending),removePendingWhenSettled(pending)}let buffer=createRecordBuffer({onFlush:handOffBatch});function write(record){return buffer.add({level:record.level,serialized:JSON.stringify(record)}),Promise.resolve()}async function flush(){buffer.drain();let writes=[...pendingBatchWrites];await Promise.all(writes)}return{flush,verify,write}}var sink_exports=__exportAll({NO_NODE_MODULES_FOUND:()=>NO_NODE_MODULES_FOUND,createConsoleSink:()=>createConsoleSink,createFileSink:()=>createFileSink,createIndexedDbSink:()=>createIndexedDbSink,createLocalStorageSink:()=>createLocalStorageSink,createNoopSink:()=>createNoopSink,createOpfsSink:()=>createOpfsSink,createSessionStorageSink:()=>createSessionStorageSink,findNodeModulesUp:()=>findNodeModulesUp});function tagged({tag,l=logger}){let prefix=`[${tag}] `;return{debug:function(message){l.debug(`${prefix}${message}`)},error:function(message){l.error(`${prefix}${message}`)},fatal:function(message){l.fatal(`${prefix}${message}`)},flush:function(){return l.flush()},info:function(message){l.info(`${prefix}${message}`)},trace:function(message){l.trace(`${prefix}${message}`)},warn:function(message){l.warn(`${prefix}${message}`)}}}export{DEFAULT_FLUSH_DEADLINE_MS,DEFAULT_VERIFY_TIMEOUT_MS,STARTUP_BUFFER_CAP,buildLogKey as _buildLogKey,compareLogKeys as _compareLogKeys,createLocalStorageStore as _createLocalStorageStore,createRecordBuffer as _createRecordBuffer,detectLocalStorageQuotaChars as _detectLocalStorageQuotaChars,detectSessionStorageQuotaChars as _detectSessionStorageQuotaChars,isQuotaExceededError as _isQuotaExceededError,neutralizeControlCharacters as _neutralizeControlCharacters,parseLogKey as _parseLogKey,createLogger,initPromise,logger,sink_exports as sinks,tagged};
|
package/package.json
CHANGED
|
@@ -21,13 +21,13 @@
|
|
|
21
21
|
"@types/node": ">=26.2.0",
|
|
22
22
|
"typescript": ">=7.0.2",
|
|
23
23
|
"@monochromatic-dev/config-rolldown": "0.0.1",
|
|
24
|
-
"@monochromatic-dev/config-typescript": "0.0.5",
|
|
25
24
|
"@monochromatic-dev/module-async-time": "0.0.1",
|
|
25
|
+
"@monochromatic-dev/module-test": "0.0.1",
|
|
26
26
|
"@monochromatic-dev/module-caught-value": "0.0.1",
|
|
27
|
-
"@monochromatic-dev/
|
|
27
|
+
"@monochromatic-dev/config-typescript": "0.0.5"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {},
|
|
30
|
-
"version": "0.
|
|
30
|
+
"version": "0.2.0",
|
|
31
31
|
"repository": {
|
|
32
32
|
"type": "git",
|
|
33
33
|
"url": "https://github.com/Aquaticat/Monochromatic.git",
|
package/src/create-logger.ts
CHANGED
|
@@ -18,6 +18,27 @@ import type {
|
|
|
18
18
|
*/
|
|
19
19
|
export const DEFAULT_FLUSH_DEADLINE_MS = 5_000;
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Default per-sink `verify()` time limit in milliseconds. Measured on
|
|
23
|
+
* 2026-09-06: the default logger's five shipped verifies complete together in
|
|
24
|
+
* about 2.4 ms locally, so this leaves three orders of magnitude for a slow
|
|
25
|
+
* but working backend probe (a network filesystem, a busy IndexedDB) while a
|
|
26
|
+
* verify that never answers (a hung mount, an IndexedDB open blocked by
|
|
27
|
+
* another tab) can no longer stall startup. Override per logger through the
|
|
28
|
+
* `verifyTimeoutMs` option of {@link createLogger}.
|
|
29
|
+
*/
|
|
30
|
+
export const DEFAULT_VERIFY_TIMEOUT_MS = 5_000;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Most records the logger buffers before its sinks have verified. Startup
|
|
34
|
+
* lasts at most {@link DEFAULT_VERIFY_TIMEOUT_MS}, so this bounds the memory a
|
|
35
|
+
* burst during that window can claim; on overflow the oldest buffered record
|
|
36
|
+
* is dropped so the newest (usually most diagnostic) context survives, and
|
|
37
|
+
* one synthetic `warn` record naming the dropped count is written to every
|
|
38
|
+
* available sink once initialization completes.
|
|
39
|
+
*/
|
|
40
|
+
export const STARTUP_BUFFER_CAP = 10_000;
|
|
41
|
+
|
|
21
42
|
/**
|
|
22
43
|
* Sink paired with its current availability inside one logger instance.
|
|
23
44
|
* Availability starts `false` (not-yet-verified reads as unavailable,
|
|
@@ -60,9 +81,14 @@ async function trackWrite(
|
|
|
60
81
|
*
|
|
61
82
|
* Verification runs eagerly at construction and never blocks callers:
|
|
62
83
|
* records emitted while an async sink is still verifying buffer internally
|
|
63
|
-
* and replay to that sink the moment it verifies.
|
|
64
|
-
*
|
|
65
|
-
*
|
|
84
|
+
* and replay to that sink the moment it verifies. Every sink verifies
|
|
85
|
+
* concurrently under its own time limit (`verifyTimeoutMs`, default
|
|
86
|
+
* {@link DEFAULT_VERIFY_TIMEOUT_MS}), so one backend that never answers
|
|
87
|
+
* cannot starve the others or keep the logger from initializing. A sink
|
|
88
|
+
* whose `verify` resolves `false`, throws, or runs past the limit is dropped
|
|
89
|
+
* and receives no records; an answer that arrives after the limit is
|
|
90
|
+
* ignored. A rejected `write` is the sink's own concern and does not disable
|
|
91
|
+
* the backend.
|
|
66
92
|
*
|
|
67
93
|
* `flush()` always resolves: one deadline (`flushDeadlineMs`, default
|
|
68
94
|
* {@link DEFAULT_FLUSH_DEADLINE_MS}) wraps startup verification, the
|
|
@@ -77,6 +103,9 @@ async function trackWrite(
|
|
|
77
103
|
* resolves anyway; raise it for slow but working backends such as network
|
|
78
104
|
* filesystems.
|
|
79
105
|
*
|
|
106
|
+
* @param verifyTimeoutMs - Milliseconds one sink's `verify()` may take before
|
|
107
|
+
* the sink counts as unavailable; raise it for a slow but working probe.
|
|
108
|
+
*
|
|
80
109
|
* @returns Logger plus its eager `initPromise`; callers need not await
|
|
81
110
|
* `initPromise` before logging, since startup records replay on verify.
|
|
82
111
|
*
|
|
@@ -99,9 +128,11 @@ export function createLogger(
|
|
|
99
128
|
{
|
|
100
129
|
sinks,
|
|
101
130
|
flushDeadlineMs = DEFAULT_FLUSH_DEADLINE_MS,
|
|
131
|
+
verifyTimeoutMs = DEFAULT_VERIFY_TIMEOUT_MS,
|
|
102
132
|
}: {
|
|
103
133
|
readonly sinks: readonly Sink[];
|
|
104
134
|
readonly flushDeadlineMs?: number;
|
|
135
|
+
readonly verifyTimeoutMs?: number;
|
|
105
136
|
},
|
|
106
137
|
): {
|
|
107
138
|
readonly initPromise: Promise<void>;
|
|
@@ -135,16 +166,35 @@ export function createLogger(
|
|
|
135
166
|
/**
|
|
136
167
|
* Instance-local aggregate flags. `initialized` flips true once the eager
|
|
137
168
|
* `initialize()` settles; `hasAvailableSink` reflects whether any sink
|
|
138
|
-
* survives verification and is recomputed by `recomputeAvailability
|
|
169
|
+
* survives verification and is recomputed by `recomputeAvailability`;
|
|
170
|
+
* `droppedStartupRecords` counts startup-buffer overflow drops for the
|
|
171
|
+
* post-initialization marker record.
|
|
139
172
|
*/
|
|
140
173
|
const state: {
|
|
174
|
+
droppedStartupRecords: number;
|
|
141
175
|
hasAvailableSink: boolean;
|
|
142
176
|
initialized: boolean;
|
|
143
177
|
} = {
|
|
178
|
+
droppedStartupRecords: 0,
|
|
144
179
|
hasAvailableSink: false,
|
|
145
180
|
initialized: false,
|
|
146
181
|
};
|
|
147
182
|
|
|
183
|
+
/**
|
|
184
|
+
* Buffers a pre-initialization record under {@link STARTUP_BUFFER_CAP},
|
|
185
|
+
* dropping the oldest buffered record (and counting the drop) when the cap
|
|
186
|
+
* is reached.
|
|
187
|
+
*
|
|
188
|
+
* @param record - Record logged before every sink has verified.
|
|
189
|
+
*/
|
|
190
|
+
function bufferStartupRecord({ record, }: { readonly record: LogRecord; },): void {
|
|
191
|
+
if (startupRecords.length >= STARTUP_BUFFER_CAP) {
|
|
192
|
+
startupRecords.shift();
|
|
193
|
+
state.droppedStartupRecords += 1;
|
|
194
|
+
}
|
|
195
|
+
startupRecords.push(record,);
|
|
196
|
+
}
|
|
197
|
+
|
|
148
198
|
/**
|
|
149
199
|
* Recomputes aggregate availability after a sink entry's flag flips.
|
|
150
200
|
*/
|
|
@@ -283,9 +333,11 @@ export function createLogger(
|
|
|
283
333
|
}
|
|
284
334
|
|
|
285
335
|
/**
|
|
286
|
-
* Runs one sink's verification and records the
|
|
287
|
-
* startup records to it on success. A rejected
|
|
288
|
-
* synchronous throw from the verifier
|
|
336
|
+
* Runs one sink's verification under the verify time limit and records the
|
|
337
|
+
* result, replaying buffered startup records to it on success. A rejected
|
|
338
|
+
* verification, a synchronous throw from the verifier, or a verify that
|
|
339
|
+
* runs past `verifyTimeoutMs` drops the sink; a late answer after the limit
|
|
340
|
+
* is never observed, so it cannot flip availability afterwards.
|
|
289
341
|
*
|
|
290
342
|
* @param entryIndex - Sink entry index to verify.
|
|
291
343
|
*/
|
|
@@ -296,8 +348,12 @@ export function createLogger(
|
|
|
296
348
|
*/
|
|
297
349
|
const entry = getSinkEntry({ entryIndex, },);
|
|
298
350
|
setEntryAvailability({
|
|
299
|
-
available: await
|
|
300
|
-
|
|
351
|
+
available: await withTimeout({
|
|
352
|
+
label: `sink ${entryIndex} verify`,
|
|
353
|
+
ms: verifyTimeoutMs,
|
|
354
|
+
promise: entry.sink
|
|
355
|
+
.verify(),
|
|
356
|
+
},),
|
|
301
357
|
entryIndex,
|
|
302
358
|
},);
|
|
303
359
|
}
|
|
@@ -310,23 +366,65 @@ export function createLogger(
|
|
|
310
366
|
}
|
|
311
367
|
}
|
|
312
368
|
|
|
369
|
+
/**
|
|
370
|
+
* Writes one synthetic `warn` record to every available sink when the
|
|
371
|
+
* startup buffer overflowed, so the loss is never silent. Runs once, after
|
|
372
|
+
* initialization, when the dropped count is known and final.
|
|
373
|
+
*/
|
|
374
|
+
function emitDroppedStartupMarker(): void {
|
|
375
|
+
/**
|
|
376
|
+
* Records dropped from the startup buffer; zero means nothing to report.
|
|
377
|
+
*/
|
|
378
|
+
const dropped = state.droppedStartupRecords;
|
|
379
|
+
if (dropped === 0)
|
|
380
|
+
return;
|
|
381
|
+
/**
|
|
382
|
+
* Marker record naming the loss; the noun agrees with the count.
|
|
383
|
+
*/
|
|
384
|
+
const marker: LogRecord = {
|
|
385
|
+
level: 'warn',
|
|
386
|
+
message: `${dropped} startup record${(dropped === 1) ? '' : 's'} dropped before a backend verified (buffer cap ${STARTUP_BUFFER_CAP})`,
|
|
387
|
+
timestamp: Date.now(),
|
|
388
|
+
};
|
|
389
|
+
entries.forEach(function writeMarker(
|
|
390
|
+
entry,
|
|
391
|
+
entryIndex,
|
|
392
|
+
) {
|
|
393
|
+
if (entry.available) {
|
|
394
|
+
writeRecordToEntry({
|
|
395
|
+
entryIndex,
|
|
396
|
+
record: marker,
|
|
397
|
+
},);
|
|
398
|
+
}
|
|
399
|
+
},);
|
|
400
|
+
}
|
|
401
|
+
|
|
313
402
|
/**
|
|
314
403
|
* Initializes all sink backends by verifying their availability. Runs once
|
|
315
|
-
* at construction.
|
|
316
|
-
*
|
|
317
|
-
*
|
|
318
|
-
* a
|
|
404
|
+
* at construction. Every verifier runs concurrently, each under the verify
|
|
405
|
+
* time limit, so one backend that never answers cannot starve the rest or
|
|
406
|
+
* keep `initialized` from flipping. Completion order does not affect
|
|
407
|
+
* correctness: a record's immediate-write set (sinks already available when
|
|
408
|
+
* it was logged) and its replay set (sinks that become available later) are
|
|
409
|
+
* disjoint, so each available sink receives each record exactly once
|
|
410
|
+
* whichever verify settles first.
|
|
319
411
|
*/
|
|
320
412
|
async function initialize(): Promise<void> {
|
|
321
413
|
if (state.initialized)
|
|
322
414
|
return;
|
|
323
415
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
416
|
+
await Promise.all(
|
|
417
|
+
entries.map(function verifyEntry(
|
|
418
|
+
_entry,
|
|
419
|
+
entryIndex,
|
|
420
|
+
) {
|
|
421
|
+
return verifyAndApply({ entryIndex, },);
|
|
422
|
+
},),
|
|
423
|
+
);
|
|
327
424
|
|
|
328
425
|
state.initialized = true;
|
|
329
426
|
startupRecords.length = 0;
|
|
427
|
+
emitDroppedStartupMarker();
|
|
330
428
|
}
|
|
331
429
|
|
|
332
430
|
/**
|
|
@@ -370,7 +468,7 @@ export function createLogger(
|
|
|
370
468
|
};
|
|
371
469
|
|
|
372
470
|
if (!state.initialized)
|
|
373
|
-
|
|
471
|
+
bufferStartupRecord({ record, },);
|
|
374
472
|
|
|
375
473
|
/**
|
|
376
474
|
* Indices of sinks that survived verification; recomputed per call so a sink dropped at verify time is excluded next time.
|
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
import {
|
|
8
8
|
createLogger,
|
|
9
9
|
DEFAULT_FLUSH_DEADLINE_MS,
|
|
10
|
+
DEFAULT_VERIFY_TIMEOUT_MS,
|
|
11
|
+
STARTUP_BUFFER_CAP,
|
|
10
12
|
type LogRecord,
|
|
11
13
|
type Sink,
|
|
12
14
|
type SinkFlush,
|
|
@@ -78,25 +80,77 @@ type RecordedCalls = {
|
|
|
78
80
|
};
|
|
79
81
|
|
|
80
82
|
/**
|
|
81
|
-
* Collects the console.warn messages
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* would be noise.
|
|
83
|
+
* Collects the console.warn messages containing `needle`. Sibling tests in
|
|
84
|
+
* this file run concurrently and emit their own internal-error reports
|
|
85
|
+
* through the same console, so a raw call count would be noise.
|
|
85
86
|
*
|
|
86
87
|
* @param warn - Stubbed console.warn.
|
|
87
88
|
*
|
|
88
|
-
* @
|
|
89
|
+
* @param needle - Substring identifying the breadcrumb family.
|
|
90
|
+
*
|
|
91
|
+
* @returns Matching messages, in call order.
|
|
89
92
|
*/
|
|
90
|
-
function
|
|
93
|
+
function breadcrumbMessages(
|
|
94
|
+
{
|
|
95
|
+
warn,
|
|
96
|
+
needle,
|
|
97
|
+
}: {
|
|
98
|
+
readonly warn: RecordedCalls;
|
|
99
|
+
readonly needle: string;
|
|
100
|
+
},
|
|
101
|
+
): string[] {
|
|
91
102
|
return warn.getCalls()
|
|
92
103
|
.map(function toMessage(call,) {
|
|
93
104
|
return String(call.args[0],);
|
|
94
105
|
},)
|
|
95
|
-
.filter(function
|
|
96
|
-
return message.includes(
|
|
106
|
+
.filter(function matchesNeedle(message,) {
|
|
107
|
+
return message.includes(needle,);
|
|
97
108
|
},);
|
|
98
109
|
}
|
|
99
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Collects the flush-deadline breadcrumbs, see {@link breadcrumbMessages}.
|
|
113
|
+
*
|
|
114
|
+
* @param warn - Stubbed console.warn.
|
|
115
|
+
*
|
|
116
|
+
* @returns Flush-deadline breadcrumb messages observed, in call order.
|
|
117
|
+
*/
|
|
118
|
+
function deadlineBreadcrumbMessages({ warn, }: { readonly warn: RecordedCalls; },): string[] {
|
|
119
|
+
return breadcrumbMessages({
|
|
120
|
+
needle: 'flush deadline',
|
|
121
|
+
warn,
|
|
122
|
+
},);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Counts the sink-verification breadcrumbs (a verify that rejected, threw, or
|
|
127
|
+
* ran past the verify time limit).
|
|
128
|
+
*
|
|
129
|
+
* @param warn - Stubbed console.warn.
|
|
130
|
+
*
|
|
131
|
+
* @returns Number of verification breadcrumbs observed.
|
|
132
|
+
*/
|
|
133
|
+
function verifyBreadcrumbs({ warn, }: { readonly warn: RecordedCalls; },): number {
|
|
134
|
+
return breadcrumbMessages({
|
|
135
|
+
needle: 'sink verification failed',
|
|
136
|
+
warn,
|
|
137
|
+
},).length;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Builds a verifier that answers `true` after a delay.
|
|
142
|
+
*
|
|
143
|
+
* @param delayMs - Milliseconds before the verifier resolves.
|
|
144
|
+
*
|
|
145
|
+
* @returns Verify function resolving `true` after the delay.
|
|
146
|
+
*/
|
|
147
|
+
function verifyTrueAfter({ delayMs, }: { readonly delayMs: number; },): Verify {
|
|
148
|
+
return async function verifyLater(): Promise<boolean> {
|
|
149
|
+
await wait(delayMs,);
|
|
150
|
+
return true;
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
100
154
|
/**
|
|
101
155
|
* Counts the flush-deadline breadcrumbs, see {@link deadlineBreadcrumbMessages}.
|
|
102
156
|
*
|
|
@@ -203,6 +257,100 @@ function messages({ recording, }: { readonly recording: RecordingSink; },): stri
|
|
|
203
257
|
},);
|
|
204
258
|
}
|
|
205
259
|
|
|
260
|
+
/**
|
|
261
|
+
* Recording sink whose first write parks on a promise the test settles, so a
|
|
262
|
+
* `flush()` can abandon that write at the deadline and the test can then
|
|
263
|
+
* settle it late, after the logger has stopped tracking it.
|
|
264
|
+
*/
|
|
265
|
+
type ParkedWriteSink = RecordingSink & {
|
|
266
|
+
readonly settleParked: PromiseWithResolvers<void>;
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Builds a verified sink whose first write waits on `settleParked` and whose
|
|
271
|
+
* later writes record immediately.
|
|
272
|
+
*
|
|
273
|
+
* @returns Sink, its record array, and the resolvers for the parked write.
|
|
274
|
+
*/
|
|
275
|
+
function parkedFirstWriteSink(): ParkedWriteSink {
|
|
276
|
+
/**
|
|
277
|
+
* Records this sink has received, in arrival order.
|
|
278
|
+
*/
|
|
279
|
+
const records: LogRecord[] = [];
|
|
280
|
+
/**
|
|
281
|
+
* Resolvers the test uses to settle the parked write.
|
|
282
|
+
*/
|
|
283
|
+
const settleParked = Promise.withResolvers<void>();
|
|
284
|
+
/**
|
|
285
|
+
* Whether the parked write has been handed out; only the first write parks.
|
|
286
|
+
*/
|
|
287
|
+
const handedOut = { parked: false, };
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Parks the first write on `settleParked`, records every later one.
|
|
291
|
+
*
|
|
292
|
+
* @param record - Record handed to the sink.
|
|
293
|
+
*/
|
|
294
|
+
async function write(record: LogRecord,): Promise<void> {
|
|
295
|
+
if (!handedOut.parked) {
|
|
296
|
+
handedOut.parked = true;
|
|
297
|
+
await settleParked.promise;
|
|
298
|
+
}
|
|
299
|
+
records.push(record,);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
records,
|
|
304
|
+
settleParked,
|
|
305
|
+
sink: {
|
|
306
|
+
verify: function verifyAvailable(): Promise<boolean> {
|
|
307
|
+
return Promise.resolve(true,);
|
|
308
|
+
},
|
|
309
|
+
write,
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Unhandled-rejection reasons observed while the capture is alive, plus the
|
|
316
|
+
* disposer that detaches the listener.
|
|
317
|
+
*/
|
|
318
|
+
type UnhandledCapture = {
|
|
319
|
+
readonly reasons: readonly unknown[];
|
|
320
|
+
readonly [Symbol.dispose]: () => void;
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Listens for `unhandledRejection` on the process until disposed. A listener
|
|
325
|
+
* also stops Node from treating the rejection as fatal, so the assertion on
|
|
326
|
+
* `reasons` is what enforces the property.
|
|
327
|
+
*
|
|
328
|
+
* @returns Capture whose `reasons` grows with every unhandled rejection.
|
|
329
|
+
*/
|
|
330
|
+
function captureUnhandledRejections(): UnhandledCapture {
|
|
331
|
+
/**
|
|
332
|
+
* Reasons observed so far.
|
|
333
|
+
*/
|
|
334
|
+
const reasons: unknown[] = [];
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Listener appended to the process.
|
|
338
|
+
*
|
|
339
|
+
* @param reason - Rejection reason Node reports.
|
|
340
|
+
*/
|
|
341
|
+
function record(reason: unknown,): void {
|
|
342
|
+
reasons.push(reason,);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
process.on('unhandledRejection', record,);
|
|
346
|
+
return {
|
|
347
|
+
reasons,
|
|
348
|
+
[Symbol.dispose]: function detach(): void {
|
|
349
|
+
process.off('unhandledRejection', record,);
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
206
354
|
await describe({
|
|
207
355
|
name: 'createLogger orchestration',
|
|
208
356
|
children: [
|
|
@@ -595,13 +743,16 @@ await describe({
|
|
|
595
743
|
},
|
|
596
744
|
},),
|
|
597
745
|
|
|
598
|
-
//region
|
|
746
|
+
//region Breadcrumb suites (stub the shared console.warn, so sequential)
|
|
599
747
|
|
|
600
748
|
describe({
|
|
601
|
-
name: '
|
|
602
|
-
// One test at a time: each stubs
|
|
749
|
+
name: 'breadcrumb suites',
|
|
750
|
+
// One test at a time across both nested suites: each stubs console.warn.
|
|
603
751
|
concurrency: 1,
|
|
604
752
|
children: [
|
|
753
|
+
describe({
|
|
754
|
+
name: 'flush deadline',
|
|
755
|
+
children: [
|
|
605
756
|
it({
|
|
606
757
|
name: 'exports a positive default flush deadline',
|
|
607
758
|
fn: async () => {
|
|
@@ -747,6 +898,307 @@ await describe({
|
|
|
747
898
|
],
|
|
748
899
|
},),
|
|
749
900
|
|
|
750
|
-
|
|
901
|
+
describe({
|
|
902
|
+
name: 'verify liveness',
|
|
903
|
+
children: [
|
|
904
|
+
it({
|
|
905
|
+
name: 'exports a positive default verify timeout',
|
|
906
|
+
fn: async () => {
|
|
907
|
+
expect(DEFAULT_VERIFY_TIMEOUT_MS,)
|
|
908
|
+
.toBeGreaterThan(0,);
|
|
909
|
+
},
|
|
910
|
+
},),
|
|
911
|
+
|
|
912
|
+
it({
|
|
913
|
+
name: 'a verify that never settles no longer starves the sinks after it',
|
|
914
|
+
timeout: DEADLINE_TEST_TIMEOUT_MS,
|
|
915
|
+
fn: async ({ sinon, },) => {
|
|
916
|
+
const warn = sinon.stub(
|
|
917
|
+
console,
|
|
918
|
+
'warn',
|
|
919
|
+
);
|
|
920
|
+
const wedged = recordingSink({
|
|
921
|
+
verify: function verifyForever(): Promise<boolean> {
|
|
922
|
+
return neverSettles();
|
|
923
|
+
},
|
|
924
|
+
},);
|
|
925
|
+
const later = recordingSink();
|
|
926
|
+
const {
|
|
927
|
+
logger,
|
|
928
|
+
initPromise,
|
|
929
|
+
} = createLogger({
|
|
930
|
+
sinks: [
|
|
931
|
+
wedged.sink,
|
|
932
|
+
later.sink,
|
|
933
|
+
],
|
|
934
|
+
verifyTimeoutMs: SHORT_DEADLINE_MS,
|
|
935
|
+
},);
|
|
936
|
+
await initPromise;
|
|
937
|
+
logger.info('after init',);
|
|
938
|
+
await logger.flush();
|
|
939
|
+
|
|
940
|
+
expect(messages({ recording: later, },),)
|
|
941
|
+
.toEqual(['after init',],);
|
|
942
|
+
expect(messages({ recording: wedged, },),)
|
|
943
|
+
.toEqual([],);
|
|
944
|
+
expect(verifyBreadcrumbs({ warn, },),)
|
|
945
|
+
.toBe(1,);
|
|
946
|
+
},
|
|
947
|
+
},),
|
|
948
|
+
|
|
949
|
+
it({
|
|
950
|
+
name: 'a verify that answers after the time limit stays unavailable',
|
|
951
|
+
timeout: DEADLINE_TEST_TIMEOUT_MS,
|
|
952
|
+
fn: async ({ sinon, },) => {
|
|
953
|
+
sinon.stub(
|
|
954
|
+
console,
|
|
955
|
+
'warn',
|
|
956
|
+
);
|
|
957
|
+
const slow = recordingSink({
|
|
958
|
+
verify: verifyTrueAfter({ delayMs: SHORT_DEADLINE_MS * 3, },),
|
|
959
|
+
},);
|
|
960
|
+
const {
|
|
961
|
+
logger,
|
|
962
|
+
initPromise,
|
|
963
|
+
} = createLogger({
|
|
964
|
+
sinks: [slow.sink,],
|
|
965
|
+
verifyTimeoutMs: SHORT_DEADLINE_MS,
|
|
966
|
+
},);
|
|
967
|
+
await initPromise;
|
|
968
|
+
// Let the late answer arrive, then log and drain.
|
|
969
|
+
await wait(SHORT_DEADLINE_MS * 4,);
|
|
970
|
+
expect(function logAfterLateAnswer() {
|
|
971
|
+
logger.info('late',);
|
|
972
|
+
},)
|
|
973
|
+
.toThrow('No logging backends available',);
|
|
974
|
+
await logger.flush();
|
|
975
|
+
|
|
976
|
+
expect(messages({ recording: slow, },),)
|
|
977
|
+
.toEqual([],);
|
|
978
|
+
},
|
|
979
|
+
},),
|
|
980
|
+
|
|
981
|
+
it({
|
|
982
|
+
name: 'sinks verify concurrently rather than one after another',
|
|
983
|
+
timeout: DEADLINE_TEST_TIMEOUT_MS,
|
|
984
|
+
fn: async () => {
|
|
985
|
+
const first = recordingSink({ verify: verifyTrueAfter({ delayMs: SLOW_WRITE_MS, },), },);
|
|
986
|
+
const second = recordingSink({ verify: verifyTrueAfter({ delayMs: SLOW_WRITE_MS, },), },);
|
|
987
|
+
const started = performance.now();
|
|
988
|
+
const { initPromise, } = createLogger({
|
|
989
|
+
sinks: [
|
|
990
|
+
first.sink,
|
|
991
|
+
second.sink,
|
|
992
|
+
],
|
|
993
|
+
},);
|
|
994
|
+
await initPromise;
|
|
995
|
+
const elapsed = performance.now() - started;
|
|
996
|
+
|
|
997
|
+
// Sequential verification would take at least twice the delay.
|
|
998
|
+
expect(elapsed,)
|
|
999
|
+
.toBeLessThan(SLOW_WRITE_MS * 2,);
|
|
1000
|
+
},
|
|
1001
|
+
},),
|
|
1002
|
+
|
|
1003
|
+
it({
|
|
1004
|
+
name: 'a record logged while sinks verify at different speeds reaches each exactly once',
|
|
1005
|
+
timeout: DEADLINE_TEST_TIMEOUT_MS,
|
|
1006
|
+
fn: async () => {
|
|
1007
|
+
const quick = recordingSink({ verify: verifyTrueAfter({ delayMs: 1, },), },);
|
|
1008
|
+
const slow = recordingSink({ verify: verifyTrueAfter({ delayMs: SLOW_WRITE_MS, },), },);
|
|
1009
|
+
const {
|
|
1010
|
+
logger,
|
|
1011
|
+
initPromise,
|
|
1012
|
+
} = createLogger({
|
|
1013
|
+
sinks: [
|
|
1014
|
+
quick.sink,
|
|
1015
|
+
slow.sink,
|
|
1016
|
+
],
|
|
1017
|
+
},);
|
|
1018
|
+
logger.info('early',);
|
|
1019
|
+
await initPromise;
|
|
1020
|
+
logger.info('late',);
|
|
1021
|
+
await logger.flush();
|
|
1022
|
+
|
|
1023
|
+
expect(messages({ recording: quick, },),)
|
|
1024
|
+
.toEqual([
|
|
1025
|
+
'early',
|
|
1026
|
+
'late',
|
|
1027
|
+
],);
|
|
1028
|
+
expect(messages({ recording: slow, },),)
|
|
1029
|
+
.toEqual([
|
|
1030
|
+
'early',
|
|
1031
|
+
'late',
|
|
1032
|
+
],);
|
|
1033
|
+
},
|
|
1034
|
+
},),
|
|
1035
|
+
],
|
|
1036
|
+
},),
|
|
1037
|
+
|
|
1038
|
+
describe({
|
|
1039
|
+
name: 'abandoned writes',
|
|
1040
|
+
children: [
|
|
1041
|
+
it({
|
|
1042
|
+
name: 'a write rejecting after the deadline abandoned it is reported once, never left unhandled, and does not retire the sink',
|
|
1043
|
+
timeout: DEADLINE_TEST_TIMEOUT_MS,
|
|
1044
|
+
fn: async ({ sinon, },) => {
|
|
1045
|
+
const warn = sinon.stub(
|
|
1046
|
+
console,
|
|
1047
|
+
'warn',
|
|
1048
|
+
);
|
|
1049
|
+
using unhandled = captureUnhandledRejections();
|
|
1050
|
+
const parked = parkedFirstWriteSink();
|
|
1051
|
+
const {
|
|
1052
|
+
logger,
|
|
1053
|
+
initPromise,
|
|
1054
|
+
} = createLogger({
|
|
1055
|
+
flushDeadlineMs: SHORT_DEADLINE_MS,
|
|
1056
|
+
sinks: [parked.sink,],
|
|
1057
|
+
},);
|
|
1058
|
+
await initPromise;
|
|
1059
|
+
logger.info('parked',);
|
|
1060
|
+
await logger.flush();
|
|
1061
|
+
expect(deadlineBreadcrumbs({ warn, },),)
|
|
1062
|
+
.toBe(1,);
|
|
1063
|
+
|
|
1064
|
+
parked.settleParked.reject(new Error('late failure',),);
|
|
1065
|
+
await wait(1,);
|
|
1066
|
+
logger.info('after',);
|
|
1067
|
+
|
|
1068
|
+
const elapsed = await timeFlush({ flush: logger.flush, },);
|
|
1069
|
+
expect(elapsed,)
|
|
1070
|
+
.toBeLessThan(FAST_FLUSH_MS,);
|
|
1071
|
+
expect(messages({ recording: parked, },),)
|
|
1072
|
+
.toEqual(['after',],);
|
|
1073
|
+
expect(unhandled.reasons,)
|
|
1074
|
+
.toHaveLength(0,);
|
|
1075
|
+
expect(
|
|
1076
|
+
breadcrumbMessages({
|
|
1077
|
+
warn,
|
|
1078
|
+
needle: 'sink write promise rejected while being tracked',
|
|
1079
|
+
},),
|
|
1080
|
+
)
|
|
1081
|
+
.toHaveLength(1,);
|
|
1082
|
+
expect(deadlineBreadcrumbs({ warn, },),)
|
|
1083
|
+
.toBe(1,);
|
|
1084
|
+
},
|
|
1085
|
+
},),
|
|
1086
|
+
|
|
1087
|
+
it({
|
|
1088
|
+
name: 'a write resolving after the deadline abandoned it still lands and the sink keeps working',
|
|
1089
|
+
timeout: DEADLINE_TEST_TIMEOUT_MS,
|
|
1090
|
+
fn: async ({ sinon, },) => {
|
|
1091
|
+
const warn = sinon.stub(
|
|
1092
|
+
console,
|
|
1093
|
+
'warn',
|
|
1094
|
+
);
|
|
1095
|
+
using unhandled = captureUnhandledRejections();
|
|
1096
|
+
const parked = parkedFirstWriteSink();
|
|
1097
|
+
const {
|
|
1098
|
+
logger,
|
|
1099
|
+
initPromise,
|
|
1100
|
+
} = createLogger({
|
|
1101
|
+
flushDeadlineMs: SHORT_DEADLINE_MS,
|
|
1102
|
+
sinks: [parked.sink,],
|
|
1103
|
+
},);
|
|
1104
|
+
await initPromise;
|
|
1105
|
+
logger.info('parked',);
|
|
1106
|
+
await logger.flush();
|
|
1107
|
+
|
|
1108
|
+
parked.settleParked.resolve();
|
|
1109
|
+
await wait(1,);
|
|
1110
|
+
logger.info('after',);
|
|
1111
|
+
await logger.flush();
|
|
1112
|
+
|
|
1113
|
+
expect(messages({ recording: parked, },),)
|
|
1114
|
+
.toEqual([
|
|
1115
|
+
'parked',
|
|
1116
|
+
'after',
|
|
1117
|
+
],);
|
|
1118
|
+
expect(unhandled.reasons,)
|
|
1119
|
+
.toHaveLength(0,);
|
|
1120
|
+
expect(
|
|
1121
|
+
breadcrumbMessages({
|
|
1122
|
+
warn,
|
|
1123
|
+
needle: 'sink write promise rejected',
|
|
1124
|
+
},),
|
|
1125
|
+
)
|
|
1126
|
+
.toHaveLength(0,);
|
|
1127
|
+
expect(deadlineBreadcrumbs({ warn, },),)
|
|
1128
|
+
.toBe(1,);
|
|
1129
|
+
},
|
|
1130
|
+
},),
|
|
1131
|
+
],
|
|
1132
|
+
},),
|
|
1133
|
+
],
|
|
1134
|
+
},),
|
|
1135
|
+
|
|
1136
|
+
//endregion Breadcrumb suites
|
|
1137
|
+
|
|
1138
|
+
//region Startup buffer bound
|
|
1139
|
+
|
|
1140
|
+
it({
|
|
1141
|
+
name: 'exports a positive startup buffer cap',
|
|
1142
|
+
fn: async () => {
|
|
1143
|
+
expect(STARTUP_BUFFER_CAP,)
|
|
1144
|
+
.toBeGreaterThan(0,);
|
|
1145
|
+
},
|
|
1146
|
+
},),
|
|
1147
|
+
|
|
1148
|
+
it({
|
|
1149
|
+
name: 'a startup burst beyond the cap keeps the newest records and reports the loss once',
|
|
1150
|
+
timeout: DEADLINE_TEST_TIMEOUT_MS,
|
|
1151
|
+
fn: async () => {
|
|
1152
|
+
/**
|
|
1153
|
+
* Records logged before the sink verifies: the cap plus a few extra
|
|
1154
|
+
* that must push the oldest ones out.
|
|
1155
|
+
*/
|
|
1156
|
+
const extra = 3;
|
|
1157
|
+
/**
|
|
1158
|
+
* Total records in the burst; the last one is index `burstSize - 1`.
|
|
1159
|
+
*/
|
|
1160
|
+
const burstSize = STARTUP_BUFFER_CAP + extra;
|
|
1161
|
+
const late = recordingSink({ verify: verifyTrueAfter({ delayMs: SLOW_WRITE_MS, },), },);
|
|
1162
|
+
const {
|
|
1163
|
+
logger,
|
|
1164
|
+
initPromise,
|
|
1165
|
+
} = createLogger({ sinks: [late.sink,], },);
|
|
1166
|
+
for (let index = 0; index < burstSize; index += 1)
|
|
1167
|
+
logger.info(`burst ${index}`,);
|
|
1168
|
+
await initPromise;
|
|
1169
|
+
await logger.flush();
|
|
1170
|
+
|
|
1171
|
+
const received = messages({ recording: late, },);
|
|
1172
|
+
expect(received,)
|
|
1173
|
+
.toHaveLength(STARTUP_BUFFER_CAP + 1,);
|
|
1174
|
+
expect(received[0],)
|
|
1175
|
+
.toBe(`burst ${extra}`,);
|
|
1176
|
+
expect(received[STARTUP_BUFFER_CAP - 1],)
|
|
1177
|
+
.toBe(`burst ${burstSize - 1}`,);
|
|
1178
|
+
expect(received[STARTUP_BUFFER_CAP],)
|
|
1179
|
+
.toBe(`${extra} startup records dropped before a backend verified (buffer cap ${STARTUP_BUFFER_CAP})`,);
|
|
1180
|
+
expect(late.records[STARTUP_BUFFER_CAP]?.level,)
|
|
1181
|
+
.toBe('warn',);
|
|
1182
|
+
},
|
|
1183
|
+
},),
|
|
1184
|
+
|
|
1185
|
+
it({
|
|
1186
|
+
name: 'no marker record is written when the startup buffer never overflowed',
|
|
1187
|
+
fn: async () => {
|
|
1188
|
+
const late = recordingSink({ verify: verifyTrueAfter({ delayMs: 1, },), },);
|
|
1189
|
+
const {
|
|
1190
|
+
logger,
|
|
1191
|
+
initPromise,
|
|
1192
|
+
} = createLogger({ sinks: [late.sink,], },);
|
|
1193
|
+
logger.info('one',);
|
|
1194
|
+
await initPromise;
|
|
1195
|
+
await logger.flush();
|
|
1196
|
+
|
|
1197
|
+
expect(messages({ recording: late, },),)
|
|
1198
|
+
.toEqual(['one',],);
|
|
1199
|
+
},
|
|
1200
|
+
},),
|
|
1201
|
+
|
|
1202
|
+
//endregion Startup buffer bound
|
|
751
1203
|
],
|
|
752
1204
|
},);
|