@monochromatic-dev/module-logger 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/LICENSES/GPL-3.0-or-later.txt +674 -0
- package/LICENSES/LGPL-3.0-or-later.txt +165 -0
- package/README.md +404 -0
- package/dist/final/neutral/index.d.mts +673 -0
- package/dist/final/neutral/index.mjs +3 -0
- package/dist/final/neutral/rolldown-runtime-5duEfhBv.mjs +1 -0
- package/dist/final/node/index.d.mts +673 -0
- package/dist/final/node/index.mjs +3 -0
- package/dist/final/node/rolldown-runtime-5duEfhBv.mjs +1 -0
- package/package.json +43 -0
- package/src/create-logger.ts +494 -0
- package/src/create-logger.unit.test.ts +752 -0
- package/src/error-format.ts +43 -0
- package/src/index.ts +35 -0
- package/src/logger.ts +67 -0
- package/src/logger.unit.test.ts +190 -0
- package/src/sink/console-control-chars.ts +140 -0
- package/src/sink/console-control-chars.unit.test.ts +206 -0
- package/src/sink/console.ts +531 -0
- package/src/sink/console.unit.test.ts +542 -0
- package/src/sink/file.ts +297 -0
- package/src/sink/file.unit.test.ts +202 -0
- package/src/sink/index.ts +11 -0
- package/src/sink/indexed-db-util.ts +96 -0
- package/src/sink/indexed-db.browser.test.ts +184 -0
- package/src/sink/indexed-db.ts +324 -0
- package/src/sink/indexed-db.unit.test.ts +80 -0
- package/src/sink/local-storage-key.ts +176 -0
- package/src/sink/local-storage-key.unit.test.ts +106 -0
- package/src/sink/local-storage-quota.ts +60 -0
- package/src/sink/local-storage-quota.unit.test.ts +98 -0
- package/src/sink/local-storage-store.ts +368 -0
- package/src/sink/local-storage-store.unit.test.ts +329 -0
- package/src/sink/local-storage.browser.test.ts +125 -0
- package/src/sink/local-storage.ts +182 -0
- package/src/sink/local-storage.unit.test.ts +218 -0
- package/src/sink/noop.ts +46 -0
- package/src/sink/noop.unit.test.ts +47 -0
- package/src/sink/opfs.browser.test.ts +84 -0
- package/src/sink/opfs.ts +212 -0
- package/src/sink/opfs.unit.test.ts +81 -0
- package/src/sink/record-buffer.ts +230 -0
- package/src/sink/record-buffer.unit.test.ts +288 -0
- package/src/sink/session-storage-quota.ts +57 -0
- package/src/sink/session-storage-quota.unit.test.ts +98 -0
- package/src/sink/session-storage-store.ts +178 -0
- package/src/sink/session-storage.browser.test.ts +137 -0
- package/src/sink/session-storage.ts +128 -0
- package/src/sink/session-storage.unit.test.ts +527 -0
- package/src/sink/web-storage-quota-error.ts +43 -0
- package/src/sink/web-storage-quota-error.unit.test.ts +55 -0
- package/src/sink/web-storage-runtime.ts +49 -0
- package/src/startup.unit.test.ts +232 -0
- package/src/tagged.ts +74 -0
- package/src/tagged.unit.test.ts +211 -0
- package/src/types.ts +78 -0
|
@@ -0,0 +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`&"A_EXCEEDED_NAMES.has(error.name)}function createLocalStorageStore(){let runIdentity={stamp:Date.now(),nonce:Math.random().toString(36).slice(2,6).padEnd(4,`0`)},state={lineCounter:0,oldestIndex:0,usedChars:0,reportedFailure:!1,adoptedPrior:!1},prior={entries:[],cursor:0},capChars=detectLocalStorageQuotaChars()/2;function ownKey(index){return buildLogKey({stamp:runIdentity.stamp,nonce:runIdentity.nonce,index})}function adoptPriorEntries(){let total=globalThis.localStorage.length,found=[];for(let slot=0;slot<total;slot++){let key=globalThis.localStorage.key(slot);if(key===null)continue;let{parsed}=parseLogKey(key);if(parsed===void 0||parsed.stamp===runIdentity.stamp&&parsed.nonce===runIdentity.nonce)continue;let value=globalThis.localStorage.getItem(key);value!==null&&found.push({...parsed,chars:value.length})}prior.entries=found.toSorted(function(first,second){return compareLogKeys({first,second})}),state.usedChars+=found.reduce(function(sum,entry){return sum+entry.chars},0)}function hasEvictable(){let priorCount=prior.entries.length;return prior.cursor<priorCount||state.oldestIndex<state.lineCounter}function evictOldestPrior(){let entry=prior.entries[prior.cursor];return entry!==void 0&&(prior.cursor++,globalThis.localStorage.removeItem(entry.key),state.usedChars=Math.max(0,state.usedChars-entry.chars),!0)}function evictOldestOwn(){let key=ownKey(state.oldestIndex),evicted=globalThis.localStorage.getItem(key);globalThis.localStorage.removeItem(key),state.oldestIndex++,evicted!==null&&(state.usedChars=Math.max(0,state.usedChars-evicted.length))}function evictOldest(){evictOldestPrior()||state.oldestIndex<state.lineCounter&&evictOldestOwn()}function persist(batch){state.adoptedPrior||(state.adoptedPrior=!0,adoptPriorEntries());let batchChars=batch.length;for(;hasEvictable()&&state.usedChars+batchChars>capChars;)evictOldest();let maxWriteAttempts=prior.entries.length-prior.cursor+(state.lineCounter-state.oldestIndex)+1;for(let writeAttempt=0;writeAttempt<maxWriteAttempts;writeAttempt++)try{globalThis.localStorage.setItem(ownKey(state.lineCounter),batch),state.lineCounter++,state.usedChars+=batchChars,state.reportedFailure=!1;return}catch(error){if(isQuotaExceededError(error)&&hasEvictable()){evictOldest();continue}state.reportedFailure||=(reportLoggerInternalError({context:`localStorage sink record write failed (repeats suppressed until a write next succeeds)`,error}),!0);return}}return{persist}}const NODE_LOCALSTORAGE_FLAG=`--localstorage-file`;function nodeWithoutLocalStorageFile(){if(detectWebStorageRuntime()!==`node`||`document`in globalThis)return!1;let flaggedInExecArgv=process.execArgv.some(function(argument){return argument.startsWith(NODE_LOCALSTORAGE_FLAG)}),flaggedInNodeOptions=(process.env.NODE_OPTIONS??``).includes(NODE_LOCALSTORAGE_FLAG);return!(flaggedInExecArgv||flaggedInNodeOptions)}function verifyLocalStorage(){if(nodeWithoutLocalStorageFile())return Promise.resolve(!1);try{let testKey=`__monochromatic_verify__`,testValue=`test-${Date.now()}`;globalThis.localStorage.setItem(testKey,testValue);let readBack=globalThis.localStorage.getItem(testKey);return globalThis.localStorage.removeItem(testKey),Promise.resolve(readBack===testValue)}catch(error){return`localStorage`in globalThis&&reportLoggerInternalError({context:`localStorage sink verification failed`,error}),Promise.resolve(!1)}}function createLocalStorageSink(){let buffer=createRecordBuffer({onFlush:createLocalStorageStore().persist});function write(record){return buffer.add({level:record.level,serialized:JSON.stringify(record)}),Promise.resolve()}function flush(){return buffer.drain(),Promise.resolve()}return{flush,verify:verifyLocalStorage,write}}const RUNTIME_QUOTA_CHARS={browser:5242880,bun:1/0,deno:10485760,node:5242880,unknown:1/0};function detectSessionStorageQuotaChars(){return RUNTIME_QUOTA_CHARS[detectWebStorageRuntime()]}function storageKey(index){return`monochromatic.log.${index}`}function createSessionStorageStore(){let state={lineCounter:0,oldestIndex:0,usedChars:0,reportedFailure:!1},capChars=detectSessionStorageQuotaChars()/2;function evictOldest(){let key=storageKey(state.oldestIndex),evicted=globalThis.sessionStorage.getItem(key);globalThis.sessionStorage.removeItem(key),state.oldestIndex++,evicted!==null&&(state.usedChars=Math.max(0,state.usedChars-evicted.length))}function persist(batch){let batchChars=batch.length;for(;state.oldestIndex<state.lineCounter&&state.usedChars+batchChars>capChars;)evictOldest();let maxWriteAttempts=state.lineCounter-state.oldestIndex+1;for(let writeAttempt=0;writeAttempt<maxWriteAttempts;writeAttempt++)try{globalThis.sessionStorage.setItem(storageKey(state.lineCounter),batch),state.lineCounter++,state.usedChars+=batchChars,state.reportedFailure=!1;return}catch(error){if(isQuotaExceededError(error)&&state.oldestIndex<state.lineCounter){evictOldest();continue}state.reportedFailure||=(reportLoggerInternalError({context:`sessionStorage sink record write failed (repeats suppressed until a write next succeeds)`,error}),!0);return}}return{persist}}function verifySessionStorage(){try{let testKey=`__monochromatic_verify__`,testValue=`test-${Date.now()}`;globalThis.sessionStorage.setItem(testKey,testValue);let readBack=globalThis.sessionStorage.getItem(testKey);return globalThis.sessionStorage.removeItem(testKey),Promise.resolve(readBack===testValue)}catch(error){return`sessionStorage`in globalThis&&reportLoggerInternalError({context:`sessionStorage sink verification failed`,error}),Promise.resolve(!1)}}function createSessionStorageSink(){let buffer=createRecordBuffer({onFlush:createSessionStorageStore().persist});function write(record){return buffer.add({level:record.level,serialized:JSON.stringify(record)}),Promise.resolve()}function flush(){return buffer.drain(),Promise.resolve()}return{flush,verify:verifySessionStorage,write}}const{initPromise:defaultInitPromise,logger:defaultLogger}=createLogger({sinks:[createConsoleSink(),createIndexedDbSink(),createSessionStorageSink(),createLocalStorageSink(),createFileSink()]}),initPromise=defaultInitPromise,logger=defaultLogger;function verify(){return Promise.resolve(!0)}function write(_record){return Promise.resolve()}function createNoopSink(){return{verify,write}}function createOpfsSink(){let state={},pendingBatchWrites=new Set;async function verify(){try{let opfsRoot=await navigator.storage.getDirectory(),timestamp=new Date().toISOString().replaceAll(`:`,`-`),fileHandle=await opfsRoot.getFileHandle(`monochromatic-${timestamp}.log.jsonl`,{create:!0}),probeWritable=await fileHandle.createWritable({keepExistingData:!0}),testData=`{"test":true,"timestamp":${Date.now()}}\n`;await probeWritable.write(testData),await probeWritable.close();let available=(await(await fileHandle.getFile()).text()).includes(`"test":true`);return available&&(state.writable=await fileHandle.createWritable({keepExistingData:!0})),available}catch(error){let opfsStorage=globalThis.navigator?.storage;return opfsStorage!==void 0&&`getDirectory`in opfsStorage&&reportLoggerInternalError({context:`OPFS sink verification failed`,error}),!1}}async function writeBatch(batch){if(state.writable)try{await state.writable.write(`${batch}\n`)}catch(error){reportLoggerInternalError({context:`OPFS sink record write failed`,error})}}async function removePendingWhenSettled(pending){await pending,pendingBatchWrites.delete(pending)}function handOffBatch(batch){let pending=writeBatch(batch);pendingBatchWrites.add(pending),removePendingWhenSettled(pending)}let buffer=createRecordBuffer({onFlush:handOffBatch});function write(record){return buffer.add({level:record.level,serialized:JSON.stringify(record)}),Promise.resolve()}async function flush(){buffer.drain();let writes=[...pendingBatchWrites];await Promise.all(writes)}return{flush,verify,write}}var sink_exports=__exportAll({NO_NODE_MODULES_FOUND:()=>NO_NODE_MODULES_FOUND,createConsoleSink:()=>createConsoleSink,createFileSink:()=>createFileSink,createIndexedDbSink:()=>createIndexedDbSink,createLocalStorageSink:()=>createLocalStorageSink,createNoopSink:()=>createNoopSink,createOpfsSink:()=>createOpfsSink,createSessionStorageSink:()=>createSessionStorageSink,findNodeModulesUp:()=>findNodeModulesUp});function tagged({tag,l=logger}){let prefix=`[${tag}] `;return{debug:function(message){l.debug(`${prefix}${message}`)},error:function(message){l.error(`${prefix}${message}`)},fatal:function(message){l.fatal(`${prefix}${message}`)},flush:function(){return l.flush()},info:function(message){l.info(`${prefix}${message}`)},trace:function(message){l.trace(`${prefix}${message}`)},warn:function(message){l.warn(`${prefix}${message}`)}}}export{DEFAULT_FLUSH_DEADLINE_MS,buildLogKey as _buildLogKey,compareLogKeys as _compareLogKeys,createLocalStorageStore as _createLocalStorageStore,createRecordBuffer as _createRecordBuffer,detectLocalStorageQuotaChars as _detectLocalStorageQuotaChars,detectSessionStorageQuotaChars as _detectSessionStorageQuotaChars,isQuotaExceededError as _isQuotaExceededError,neutralizeControlCharacters as _neutralizeControlCharacters,parseLogKey as _parseLogKey,createLogger,initPromise,logger,sink_exports as sinks,tagged};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var __defProp=Object.defineProperty,__exportAll=(all,no_symbols)=>{let target={};for(var name in all)__defProp(target,name,{get:all[name],enumerable:!0});return no_symbols||__defProp(target,Symbol.toStringTag,{value:`Module`}),target};export{__exportAll as t};
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"license": "LGPL-3.0-or-later",
|
|
3
|
+
"name": "@monochromatic-dev/module-logger",
|
|
4
|
+
"description": "Zero-config multi-sink logger with tagged composition. Auto-discovers console, file, IndexedDB, sessionStorage, and localStorage backends at import time.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"module": "dist/final/neutral/index.mjs",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/final/neutral/index.d.mts",
|
|
10
|
+
"node": "./dist/final/node/index.mjs",
|
|
11
|
+
"default": "./dist/final/neutral/index.mjs"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist/final",
|
|
16
|
+
"src",
|
|
17
|
+
"LICENSES",
|
|
18
|
+
"CHANGELOG.md"
|
|
19
|
+
],
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/node": ">=26.2.0",
|
|
22
|
+
"typescript": ">=7.0.2",
|
|
23
|
+
"@monochromatic-dev/config-rolldown": "0.0.1",
|
|
24
|
+
"@monochromatic-dev/config-typescript": "0.0.5",
|
|
25
|
+
"@monochromatic-dev/module-async-time": "0.0.1",
|
|
26
|
+
"@monochromatic-dev/module-caught-value": "0.0.1",
|
|
27
|
+
"@monochromatic-dev/module-test": "0.0.1"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {},
|
|
30
|
+
"version": "0.1.0",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/Aquaticat/Monochromatic.git",
|
|
34
|
+
"directory": "package/module/logger"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=24"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public",
|
|
41
|
+
"provenance": true
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
import { withTimeout, } from '@monochromatic-dev/module-async-time/ts';
|
|
2
|
+
import { reportLoggerInternalError, } from './error-format.ts';
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
Level,
|
|
6
|
+
Logger,
|
|
7
|
+
LogRecord,
|
|
8
|
+
Sink,
|
|
9
|
+
} from './types.ts';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Default `flush()` deadline in milliseconds. Measured on 2026-09-06: a
|
|
13
|
+
* default logger flushing 100 records through the console and file sinks
|
|
14
|
+
* settles in about 2 ms locally, so this leaves three orders of magnitude for
|
|
15
|
+
* a slow but working backend while still bounding shutdown on a wedged one.
|
|
16
|
+
* Override per logger through the `flushDeadlineMs` option of
|
|
17
|
+
* {@link createLogger}.
|
|
18
|
+
*/
|
|
19
|
+
export const DEFAULT_FLUSH_DEADLINE_MS = 5_000;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Sink paired with its current availability inside one logger instance.
|
|
23
|
+
* Availability starts `false` (not-yet-verified reads as unavailable,
|
|
24
|
+
* identical to a verified failure) and flips `true` once the sink's own
|
|
25
|
+
* `verify` confirms its backend.
|
|
26
|
+
*/
|
|
27
|
+
type SinkEntry = {
|
|
28
|
+
available: boolean;
|
|
29
|
+
readonly sink: Sink;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Awaits one sink write so `flush()` can observe its settling. A rejected
|
|
34
|
+
* write is swallowed here via {@link reportLoggerInternalError} (the sink
|
|
35
|
+
* owns its own write-error handling) and does not disable the sink: one
|
|
36
|
+
* transient failure must not retire a backend for the rest of the run.
|
|
37
|
+
*
|
|
38
|
+
* @param writePromise - Promise returned by the sink write call.
|
|
39
|
+
*/
|
|
40
|
+
async function trackWrite(
|
|
41
|
+
{ writePromise, }: { readonly writePromise: Promise<void>; },
|
|
42
|
+
): Promise<void> {
|
|
43
|
+
try {
|
|
44
|
+
await writePromise;
|
|
45
|
+
}
|
|
46
|
+
catch (error: unknown) {
|
|
47
|
+
reportLoggerInternalError({
|
|
48
|
+
context: 'sink write promise rejected while being tracked',
|
|
49
|
+
error,
|
|
50
|
+
},);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Builds a multi-sink logger over the supplied sink adapters. All
|
|
56
|
+
* orchestration (per-sink availability, startup buffering and replay,
|
|
57
|
+
* in-flight write tracking, and flush) lives here; the exported default
|
|
58
|
+
* `logger` is just this factory applied to the default sink set, and tests
|
|
59
|
+
* apply it to fake sinks to exercise the orchestration directly.
|
|
60
|
+
*
|
|
61
|
+
* Verification runs eagerly at construction and never blocks callers:
|
|
62
|
+
* 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.
|
|
66
|
+
*
|
|
67
|
+
* `flush()` always resolves: one deadline (`flushDeadlineMs`, default
|
|
68
|
+
* {@link DEFAULT_FLUSH_DEADLINE_MS}) wraps startup verification, the
|
|
69
|
+
* in-flight write drain, and every sink flush hook together. When it elapses
|
|
70
|
+
* the logger reports one breadcrumb, abandons the tracked writes from its
|
|
71
|
+
* view (the sinks expose no cancellation, so the underlying work continues),
|
|
72
|
+
* and resolves, so a wedged backend cannot hang a shutdown.
|
|
73
|
+
*
|
|
74
|
+
* @param sinks - Sink adapters to fan each record out to, in priority order.
|
|
75
|
+
*
|
|
76
|
+
* @param flushDeadlineMs - Milliseconds one `flush()` may take before it
|
|
77
|
+
* resolves anyway; raise it for slow but working backends such as network
|
|
78
|
+
* filesystems.
|
|
79
|
+
*
|
|
80
|
+
* @returns Logger plus its eager `initPromise`; callers need not await
|
|
81
|
+
* `initPromise` before logging, since startup records replay on verify.
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```ts
|
|
85
|
+
* const { logger } = createLogger({ sinks: [createConsoleSink()] });
|
|
86
|
+
* logger.info('ready');
|
|
87
|
+
* await logger.flush();
|
|
88
|
+
* ```
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```ts
|
|
92
|
+
* const { logger } = createLogger({
|
|
93
|
+
* sinks: [createFileSink()],
|
|
94
|
+
* flushDeadlineMs: 30_000,
|
|
95
|
+
* });
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
export function createLogger(
|
|
99
|
+
{
|
|
100
|
+
sinks,
|
|
101
|
+
flushDeadlineMs = DEFAULT_FLUSH_DEADLINE_MS,
|
|
102
|
+
}: {
|
|
103
|
+
readonly sinks: readonly Sink[];
|
|
104
|
+
readonly flushDeadlineMs?: number;
|
|
105
|
+
},
|
|
106
|
+
): {
|
|
107
|
+
readonly initPromise: Promise<void>;
|
|
108
|
+
readonly logger: Logger;
|
|
109
|
+
} {
|
|
110
|
+
/**
|
|
111
|
+
* Per-sink availability for this logger instance, indexed by startup order.
|
|
112
|
+
*/
|
|
113
|
+
const entries: SinkEntry[] = sinks.map(function toEntry(sink,): SinkEntry {
|
|
114
|
+
return {
|
|
115
|
+
available: false,
|
|
116
|
+
sink,
|
|
117
|
+
};
|
|
118
|
+
},);
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Log records emitted before every sink has completed startup verification.
|
|
122
|
+
* Records stay here only during initialization; each sink that later
|
|
123
|
+
* verifies as available receives a replay, while already-available sinks
|
|
124
|
+
* still receive writes immediately.
|
|
125
|
+
*/
|
|
126
|
+
const startupRecords: LogRecord[] = [];
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Sink writes currently in flight. Logger-level `flush()` drains these so
|
|
130
|
+
* sinks without their own flush hook, such as file writes, still settle
|
|
131
|
+
* before the flush resolves.
|
|
132
|
+
*/
|
|
133
|
+
const pendingWrites = new Set<Promise<void>>();
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Instance-local aggregate flags. `initialized` flips true once the eager
|
|
137
|
+
* `initialize()` settles; `hasAvailableSink` reflects whether any sink
|
|
138
|
+
* survives verification and is recomputed by `recomputeAvailability`.
|
|
139
|
+
*/
|
|
140
|
+
const state: {
|
|
141
|
+
hasAvailableSink: boolean;
|
|
142
|
+
initialized: boolean;
|
|
143
|
+
} = {
|
|
144
|
+
hasAvailableSink: false,
|
|
145
|
+
initialized: false,
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Recomputes aggregate availability after a sink entry's flag flips.
|
|
150
|
+
*/
|
|
151
|
+
function recomputeAvailability(): void {
|
|
152
|
+
state.hasAvailableSink = entries.some(function isAvailable(entry,) {
|
|
153
|
+
return entry.available;
|
|
154
|
+
},);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Reads a sink entry by startup-order index.
|
|
159
|
+
*
|
|
160
|
+
* @param entryIndex - Sink entry index from the construction-time order.
|
|
161
|
+
*
|
|
162
|
+
* @returns Sink entry at that index.
|
|
163
|
+
*
|
|
164
|
+
* @throws Error when index no longer maps to a sink entry.
|
|
165
|
+
*/
|
|
166
|
+
function getSinkEntry({ entryIndex, }: { readonly entryIndex: number; },): SinkEntry {
|
|
167
|
+
/**
|
|
168
|
+
* Sink entry read from startup-order storage; undefined means the caller supplied an invalid index.
|
|
169
|
+
*/
|
|
170
|
+
const entry = entries[entryIndex];
|
|
171
|
+
if (entry === undefined)
|
|
172
|
+
throw new Error(`Missing logger sink entry at index ${entryIndex}.`,);
|
|
173
|
+
|
|
174
|
+
return entry;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Marks a sink unavailable after its verification fails or throws. Write
|
|
179
|
+
* failures never reach here: a rejected write is the sink's own concern and
|
|
180
|
+
* leaves the backend available, so one transient hiccup does not retire it.
|
|
181
|
+
*
|
|
182
|
+
* @param entryIndex - Sink entry index whose backend failed verification.
|
|
183
|
+
*/
|
|
184
|
+
function markEntryUnavailable({ entryIndex, }: { readonly entryIndex: number; },): void {
|
|
185
|
+
/**
|
|
186
|
+
* Mutable sink entry being disabled.
|
|
187
|
+
*/
|
|
188
|
+
const entry = getSinkEntry({ entryIndex, },);
|
|
189
|
+
entry.available = false;
|
|
190
|
+
recomputeAvailability();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Removes a tracked sink write from {@link pendingWrites} once it settles.
|
|
195
|
+
*
|
|
196
|
+
* @param trackedWrite - Promise returned by {@link trackWrite}.
|
|
197
|
+
*/
|
|
198
|
+
async function removePendingWriteWhenSettled(
|
|
199
|
+
{ trackedWrite, }: { readonly trackedWrite: Promise<void>; },
|
|
200
|
+
): Promise<void> {
|
|
201
|
+
await trackedWrite;
|
|
202
|
+
pendingWrites.delete(trackedWrite,);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Sends a record to one sink, recording the write so `flush()` can await it.
|
|
207
|
+
*
|
|
208
|
+
* @param entryIndex - Available sink entry index to receive the record.
|
|
209
|
+
*
|
|
210
|
+
* @param record - Log record to deliver.
|
|
211
|
+
*/
|
|
212
|
+
function writeRecordToEntry(
|
|
213
|
+
{
|
|
214
|
+
entryIndex,
|
|
215
|
+
record,
|
|
216
|
+
}: {
|
|
217
|
+
readonly entryIndex: number;
|
|
218
|
+
readonly record: LogRecord;
|
|
219
|
+
},
|
|
220
|
+
): void {
|
|
221
|
+
try {
|
|
222
|
+
/**
|
|
223
|
+
* Sink entry receiving the record.
|
|
224
|
+
*/
|
|
225
|
+
const entry = getSinkEntry({ entryIndex, },);
|
|
226
|
+
/**
|
|
227
|
+
* Monitored sink write; resolves even when the underlying write rejects, because {@link trackWrite} swallows rejection.
|
|
228
|
+
*/
|
|
229
|
+
const trackedWrite = trackWrite({
|
|
230
|
+
writePromise: entry.sink
|
|
231
|
+
.write(record,),
|
|
232
|
+
},);
|
|
233
|
+
pendingWrites.add(trackedWrite,);
|
|
234
|
+
void removePendingWriteWhenSettled({ trackedWrite, },);
|
|
235
|
+
}
|
|
236
|
+
catch (error: unknown) {
|
|
237
|
+
reportLoggerInternalError({
|
|
238
|
+
context: 'sink write threw synchronously while dispatching record',
|
|
239
|
+
error,
|
|
240
|
+
},);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Replays buffered startup records to a sink that just became available.
|
|
246
|
+
*
|
|
247
|
+
* @param entryIndex - Newly available sink entry index.
|
|
248
|
+
*/
|
|
249
|
+
function replayStartupRecordsToEntry({ entryIndex, }: { readonly entryIndex: number; },): void {
|
|
250
|
+
startupRecords.forEach(function replayStartupRecord(record,) {
|
|
251
|
+
writeRecordToEntry({
|
|
252
|
+
entryIndex,
|
|
253
|
+
record,
|
|
254
|
+
},);
|
|
255
|
+
},);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Applies a verification result to a sink and replays startup records on
|
|
260
|
+
* success.
|
|
261
|
+
*
|
|
262
|
+
* @param entryIndex - Sink entry index whose verification completed.
|
|
263
|
+
*
|
|
264
|
+
* @param available - Whether the backend verified successfully.
|
|
265
|
+
*/
|
|
266
|
+
function setEntryAvailability(
|
|
267
|
+
{
|
|
268
|
+
entryIndex,
|
|
269
|
+
available,
|
|
270
|
+
}: {
|
|
271
|
+
readonly entryIndex: number;
|
|
272
|
+
readonly available: boolean;
|
|
273
|
+
},
|
|
274
|
+
): void {
|
|
275
|
+
/**
|
|
276
|
+
* Sink entry whose availability is changing.
|
|
277
|
+
*/
|
|
278
|
+
const entry = getSinkEntry({ entryIndex, },);
|
|
279
|
+
entry.available = available;
|
|
280
|
+
recomputeAvailability();
|
|
281
|
+
if (available)
|
|
282
|
+
replayStartupRecordsToEntry({ entryIndex, },);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
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.
|
|
289
|
+
*
|
|
290
|
+
* @param entryIndex - Sink entry index to verify.
|
|
291
|
+
*/
|
|
292
|
+
async function verifyAndApply({ entryIndex, }: { readonly entryIndex: number; },): Promise<void> {
|
|
293
|
+
try {
|
|
294
|
+
/**
|
|
295
|
+
* Sink entry whose verifier is about to run.
|
|
296
|
+
*/
|
|
297
|
+
const entry = getSinkEntry({ entryIndex, },);
|
|
298
|
+
setEntryAvailability({
|
|
299
|
+
available: await entry.sink
|
|
300
|
+
.verify(),
|
|
301
|
+
entryIndex,
|
|
302
|
+
},);
|
|
303
|
+
}
|
|
304
|
+
catch (error: unknown) {
|
|
305
|
+
reportLoggerInternalError({
|
|
306
|
+
context: `sink verification failed for entry ${entryIndex}`,
|
|
307
|
+
error,
|
|
308
|
+
},);
|
|
309
|
+
markEntryUnavailable({ entryIndex, },);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* 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.
|
|
319
|
+
*/
|
|
320
|
+
async function initialize(): Promise<void> {
|
|
321
|
+
if (state.initialized)
|
|
322
|
+
return;
|
|
323
|
+
|
|
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, },);
|
|
327
|
+
|
|
328
|
+
state.initialized = true;
|
|
329
|
+
startupRecords.length = 0;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Eager readiness promise. Consumers do not need to await this before
|
|
334
|
+
* logging; `flush()` awaits it internally, and startup records replay to
|
|
335
|
+
* async sinks as they become available.
|
|
336
|
+
*/
|
|
337
|
+
const initPromise: Promise<void> = initialize();
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Drains every currently tracked sink write.
|
|
341
|
+
*/
|
|
342
|
+
async function drainPendingWrites(): Promise<void> {
|
|
343
|
+
/**
|
|
344
|
+
* Snapshot of tracked writes at flush time.
|
|
345
|
+
*/
|
|
346
|
+
const writes = [...pendingWrites,];
|
|
347
|
+
await Promise.all(writes,);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Creates a logging method for the specified severity level.
|
|
352
|
+
*
|
|
353
|
+
* @param level - Log severity level for messages from this method.
|
|
354
|
+
*
|
|
355
|
+
* @returns Logging function for the given level.
|
|
356
|
+
*/
|
|
357
|
+
function createMethod(level: Level,): (message: string,) => void {
|
|
358
|
+
return function logAtLevel(message: string,): void {
|
|
359
|
+
if ((!state.hasAvailableSink) && state
|
|
360
|
+
.initialized)
|
|
361
|
+
throw new Error('No logging backends available',);
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Shared LogRecord forwarded to every available sink; built once per call so all sinks see the same timestamp.
|
|
365
|
+
*/
|
|
366
|
+
const record: LogRecord = {
|
|
367
|
+
level,
|
|
368
|
+
message,
|
|
369
|
+
timestamp: Date.now(),
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
if (!state.initialized)
|
|
373
|
+
startupRecords.push(record,);
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Indices of sinks that survived verification; recomputed per call so a sink dropped at verify time is excluded next time.
|
|
377
|
+
*/
|
|
378
|
+
const availableIndices = entries
|
|
379
|
+
.map(function indexEntry(
|
|
380
|
+
_entry,
|
|
381
|
+
entryIndex,
|
|
382
|
+
) {
|
|
383
|
+
return entryIndex;
|
|
384
|
+
},)
|
|
385
|
+
.filter(function isAvailable(entryIndex,) {
|
|
386
|
+
return getSinkEntry({ entryIndex, },)
|
|
387
|
+
.available;
|
|
388
|
+
},);
|
|
389
|
+
|
|
390
|
+
availableIndices.forEach(function writeToSink(entryIndex,) {
|
|
391
|
+
writeRecordToEntry({
|
|
392
|
+
entryIndex,
|
|
393
|
+
record,
|
|
394
|
+
},);
|
|
395
|
+
},);
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Runs every available sink's own `flush` hook. A rejecting hook marks that
|
|
401
|
+
* sink unavailable and does not fail the aggregate.
|
|
402
|
+
*/
|
|
403
|
+
async function runSinkFlushHooks(): Promise<void> {
|
|
404
|
+
await Promise.all(
|
|
405
|
+
entries.map(async function runFlush(
|
|
406
|
+
entry,
|
|
407
|
+
entryIndex,
|
|
408
|
+
) {
|
|
409
|
+
/**
|
|
410
|
+
* Optional sink-supplied flush hook; absent when the sink writes synchronously and needs no draining.
|
|
411
|
+
*/
|
|
412
|
+
const sinkFlush = entry.sink
|
|
413
|
+
.flush;
|
|
414
|
+
if ((!entry.available) || ((typeof sinkFlush) !== 'function'))
|
|
415
|
+
return;
|
|
416
|
+
try {
|
|
417
|
+
await sinkFlush();
|
|
418
|
+
}
|
|
419
|
+
catch (error: unknown) {
|
|
420
|
+
reportLoggerInternalError({
|
|
421
|
+
context: `sink flush failed for entry ${entryIndex}`,
|
|
422
|
+
error,
|
|
423
|
+
},);
|
|
424
|
+
markEntryUnavailable({ entryIndex, },);
|
|
425
|
+
}
|
|
426
|
+
},),
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Drains startup verification, in-flight sink writes (via
|
|
432
|
+
* {@link drainPendingWrites}), and every sink flush hook (via
|
|
433
|
+
* {@link runSinkFlushHooks}) in order. Never rejects on its own: every
|
|
434
|
+
* inner failure is already reported and swallowed, so the only way this
|
|
435
|
+
* stays pending is a verify, write, or hook that never settles.
|
|
436
|
+
*/
|
|
437
|
+
async function drainEverything(): Promise<void> {
|
|
438
|
+
await initPromise;
|
|
439
|
+
await drainPendingWrites();
|
|
440
|
+
await runSinkFlushHooks();
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Drops every tracked write from the logger's view after the flush deadline
|
|
445
|
+
* elapsed. The tracked wrappers keep their own rejection handling, so a
|
|
446
|
+
* late settlement can neither surface as an unhandled rejection nor stall
|
|
447
|
+
* the next `flush()`. The underlying sink work is not cancelled: sinks
|
|
448
|
+
* expose no cancellation signal.
|
|
449
|
+
*/
|
|
450
|
+
function abandonPendingWrites(): void {
|
|
451
|
+
pendingWrites.clear();
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Runs {@link drainEverything} under the flush deadline. Resolves once all
|
|
456
|
+
* tracked writes and hooks have settled, or once `flushDeadlineMs` elapses,
|
|
457
|
+
* whichever comes first; a deadline hit reports one breadcrumb and abandons
|
|
458
|
+
* the tracked writes so shutdown proceeds.
|
|
459
|
+
*/
|
|
460
|
+
async function flushAll(): Promise<void> {
|
|
461
|
+
try {
|
|
462
|
+
await withTimeout({
|
|
463
|
+
label: 'logger flush',
|
|
464
|
+
ms: flushDeadlineMs,
|
|
465
|
+
promise: drainEverything(),
|
|
466
|
+
},);
|
|
467
|
+
}
|
|
468
|
+
catch (error: unknown) {
|
|
469
|
+
reportLoggerInternalError({
|
|
470
|
+
context: `flush deadline of ${flushDeadlineMs}ms elapsed; abandoning in-flight sink work`,
|
|
471
|
+
error,
|
|
472
|
+
},);
|
|
473
|
+
abandonPendingWrites();
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Multi-sink logger that writes to all available backends.
|
|
479
|
+
*/
|
|
480
|
+
const logger: Logger = {
|
|
481
|
+
debug: createMethod('debug',),
|
|
482
|
+
error: createMethod('error',),
|
|
483
|
+
fatal: createMethod('fatal',),
|
|
484
|
+
flush: flushAll,
|
|
485
|
+
info: createMethod('info',),
|
|
486
|
+
trace: createMethod('trace',),
|
|
487
|
+
warn: createMethod('warn',),
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
return {
|
|
491
|
+
initPromise,
|
|
492
|
+
logger,
|
|
493
|
+
};
|
|
494
|
+
}
|