@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 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
- records emitted while an async sink is
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 (or its flush hook rejects);
260
- remaining sinks continue
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. A sink whose `verify`
98
- * resolves `false` or throws is dropped and receives no records. A rejected
99
- * `write` is the sink's own concern and does not disable the backend.
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 getSinkEntry({entryIndex}).sink.verify(),entryIndex})}catch(error){reportLoggerInternalError({context:`sink verification failed for entry ${entryIndex}`,error}),markEntryUnavailable({entryIndex})}}async function initialize(){if(!state.initialized){for(let[entryIndex]of entries.entries())await verifyAndApply({entryIndex});state.initialized=!0,startupRecords.length=0}}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||startupRecords.push(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`&&QUOTA_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`&&QUOTA_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. A sink whose `verify`
98
- * resolves `false` or throws is dropped and receives no records. A rejected
99
- * `write` is the sink's own concern and does not disable the backend.
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 getSinkEntry({entryIndex}).sink.verify(),entryIndex})}catch(error){reportLoggerInternalError({context:`sink verification failed for entry ${entryIndex}`,error}),markEntryUnavailable({entryIndex})}}async function initialize(){if(!state.initialized){for(let[entryIndex]of entries.entries())await verifyAndApply({entryIndex});state.initialized=!0,startupRecords.length=0}}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||startupRecords.push(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`&&QUOTA_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`&&QUOTA_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/module-test": "0.0.1"
27
+ "@monochromatic-dev/config-typescript": "0.0.5"
28
28
  },
29
29
  "dependencies": {},
30
- "version": "0.1.0",
30
+ "version": "0.2.0",
31
31
  "repository": {
32
32
  "type": "git",
33
33
  "url": "https://github.com/Aquaticat/Monochromatic.git",
@@ -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. A sink whose `verify`
64
- * resolves `false` or throws is dropped and receives no records. A rejected
65
- * `write` is the sink's own concern and does not disable the backend.
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 result, replaying buffered
287
- * startup records to it on success. A rejected verification (or a
288
- * synchronous throw from the verifier) drops the sink.
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 entry.sink
300
- .verify(),
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. Verification order does not affect correctness: every
316
- * sink that verifies available replays the full startup buffer, so no sink
317
- * depends on another verifying first. The sequential await keeps startup in
318
- * a single deterministic order rather than racing all verifiers at once.
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
- for (const [entryIndex,] of entries.entries())
325
- // oxlint-disable-next-line no-await-in-loop -- Verify sinks in priority order; order does not affect replay correctness.
326
- await verifyAndApply({ entryIndex, },);
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
- startupRecords.push(record,);
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 carrying the flush-deadline breadcrumb.
82
- * Sibling tests in this file run concurrently and emit their own
83
- * internal-error reports through the same console, so a raw call count
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
- * @returns Flush-deadline breadcrumb messages observed, in call order.
89
+ * @param needle - Substring identifying the breadcrumb family.
90
+ *
91
+ * @returns Matching messages, in call order.
89
92
  */
90
- function deadlineBreadcrumbMessages({ warn, }: { readonly warn: RecordedCalls; },): string[] {
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 isDeadlineBreadcrumb(message,) {
96
- return message.includes('flush deadline',);
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 Flush deadline
746
+ //region Breadcrumb suites (stub the shared console.warn, so sequential)
599
747
 
600
748
  describe({
601
- name: 'flush deadline',
602
- // One test at a time: each stubs the shared console.warn.
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
- //endregion Flush deadline
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
  },);
package/src/index.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  export {
2
2
  createLogger,
3
3
  DEFAULT_FLUSH_DEADLINE_MS,
4
+ DEFAULT_VERIFY_TIMEOUT_MS,
5
+ STARTUP_BUFFER_CAP,
4
6
  } from './create-logger.ts';
5
7
  export {
6
8
  initPromise,