@monochromatic-dev/module-logger 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +20 -5
- package/dist/final/neutral/index.d.mts +8 -11
- package/dist/final/neutral/index.mjs +1 -1
- package/dist/final/node/index.d.mts +8 -11
- package/dist/final/node/index.mjs +1 -1
- package/package.json +5 -4
- package/src/default-sinks.neutral.ts +22 -9
- package/src/default-sinks.node.ts +21 -10
- package/src/index.ts +1 -4
- package/src/logger.ts +75 -23
- package/src/restricted-global-scope.unit.test.ts +101 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @monochromatic-dev/module-logger
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- The default logger is built by the first log or flush call instead of at import,
|
|
8
|
+
the default sink list is created at that moment,
|
|
9
|
+
and the package declares `sideEffects: false`.
|
|
10
|
+
Importing the root entry or `tagged` now runs no sink discovery,
|
|
11
|
+
no timers,
|
|
12
|
+
and no I/O,
|
|
13
|
+
so global-scope-restricted runtimes such as Cloudflare Workers start without the four sink-verification warnings.
|
|
14
|
+
The `initPromise` root export is removed;
|
|
15
|
+
`flush()` awaits readiness internally,
|
|
16
|
+
and `createLogger` still returns its instance's `initPromise`.
|
|
17
|
+
Commit `7d52ddd`.
|
|
18
|
+
|
|
3
19
|
## 0.3.0
|
|
4
20
|
|
|
5
21
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# module-logger
|
|
2
2
|
|
|
3
3
|
Zero-config multi-sink logger with tagged composition.
|
|
4
|
-
Works immediately
|
|
5
|
-
auto-discovers available backends for the current runtime,
|
|
4
|
+
Works immediately with no setup call:
|
|
5
|
+
the first log or flush call builds the default logger and auto-discovers available backends for the current runtime,
|
|
6
6
|
and records emitted while async backend verification is still pending replay to those
|
|
7
7
|
backends as soon as they verify.
|
|
8
|
-
|
|
8
|
+
Importing the package runs no discovery,
|
|
9
|
+
no timers,
|
|
10
|
+
and no I/O.
|
|
9
11
|
|
|
10
12
|
## Usage
|
|
11
13
|
|
|
@@ -56,6 +58,17 @@ Node 24 or newer (the build calls `Error.isError`),
|
|
|
56
58
|
plus current browsers,
|
|
57
59
|
Deno,
|
|
58
60
|
and Bun for the sinks whose `verify` finds a backend there.
|
|
61
|
+
### Global-scope-restricted runtimes
|
|
62
|
+
|
|
63
|
+
Cloudflare Workers (and any runtime that forbids timers,
|
|
64
|
+
I/O,
|
|
65
|
+
and random values in global scope) can import the root entry and `tagged` freely:
|
|
66
|
+
nothing is built at import,
|
|
67
|
+
so no sink probe and no timer runs in global scope.
|
|
68
|
+
The first log or flush call inside a handler builds the default logger and verifies its sinks there.
|
|
69
|
+
A Worker that wants a logger scoped to one request can still build its own with `createLogger` over `sinks.createConsoleSink()`
|
|
70
|
+
and hand `flush()` to `ctx.waitUntil`.
|
|
71
|
+
|
|
59
72
|
The published package exposes the built artifact only.
|
|
60
73
|
The `/ts` source subpath used inside this workspace is stripped at publish time,
|
|
61
74
|
because Node refuses `.ts` files under `node_modules`.
|
|
@@ -262,8 +275,10 @@ File,
|
|
|
262
275
|
|
|
263
276
|
## Error handling
|
|
264
277
|
|
|
265
|
-
-
|
|
266
|
-
|
|
278
|
+
- The default logger is built by the first log or flush call,
|
|
279
|
+
never at import;
|
|
280
|
+
there is no readiness promise to await,
|
|
281
|
+
because `flush()` awaits verification and startup replay internally
|
|
267
282
|
- `logger.flush()` awaits startup verification,
|
|
268
283
|
pending sink writes,
|
|
269
284
|
and sink-owned flush hooks,
|
|
@@ -91,20 +91,17 @@ export declare function createLogger({ sinks, flushDeadlineMs, verifyTimeoutMs }
|
|
|
91
91
|
//#endregion
|
|
92
92
|
//#region src/logger.d.ts
|
|
93
93
|
/**
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
Startup records replay to async sinks that verify after the log call.
|
|
102
|
-
Log calls throw only when initialization proves no backend is available,
|
|
103
|
-
which the console sink prevents in every supported runtime.
|
|
94
|
+
Multi-sink logger that writes to all available backends, built lazily on
|
|
95
|
+
the first call. Startup records replay to async sinks that verify after
|
|
96
|
+
the log call. Log calls throw only when initialization proves no backend is
|
|
97
|
+
available, which the console sink prevents in every supported runtime.
|
|
98
|
+
`flush()` awaits verification internally, so no readiness promise is
|
|
99
|
+
exported: awaiting one at module top level was the mistake this design
|
|
100
|
+
removes.
|
|
104
101
|
|
|
105
102
|
@example
|
|
106
103
|
```ts
|
|
107
|
-
import { logger, } from '\@monochromatic-dev/module-logger
|
|
104
|
+
import { logger, } from '\@monochromatic-dev/module-logger';
|
|
108
105
|
|
|
109
106
|
logger.error('unexpected shutdown',);
|
|
110
107
|
await logger.flush();
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import{t as __exportAll}from"./rolldown-runtime-5duEfhBv.mjs";import{n as createRecordBuffer,r as reportLoggerInternalError,t as createIndexedDbSink}from"./indexed-db-hsIfv7Cv.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()}}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}}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 store=createLocalStorageStore(),buffer=createRecordBuffer({onFlush:store.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 store=createSessionStorageStore(),buffer=createRecordBuffer({onFlush:store.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()]}),initPromise=defaultInitPromise,logger=defaultLogger;function verify(){return Promise.resolve(!0)}function write(_record){return Promise.resolve()}function createNoopSink(){return{verify,write}}var sink_exports=__exportAll({createConsoleSink:()=>createConsoleSink,createLocalStorageSink:()=>createLocalStorageSink,createNoopSink:()=>createNoopSink,createSessionStorageSink:()=>createSessionStorageSink});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};
|
|
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}}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 store=createLocalStorageStore(),buffer=createRecordBuffer({onFlush:store.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 store=createSessionStorageStore(),buffer=createRecordBuffer({onFlush:store.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}}function createDefaultSinks(){return[createConsoleSink(),createIndexedDbSink(),createSessionStorageSink(),createLocalStorageSink()]}const memo={};function defaultInstance(){return memo.current??=createLogger({sinks:createDefaultSinks()}),memo.current}function forward(level){return function(message){defaultInstance().logger[level](message)}}async function flush(){await defaultInstance().logger.flush()}const logger={debug:forward(`debug`),error:forward(`error`),fatal:forward(`fatal`),flush,info:forward(`info`),trace:forward(`trace`),warn:forward(`warn`)};function verify(){return Promise.resolve(!0)}function write(_record){return Promise.resolve()}function createNoopSink(){return{verify,write}}var sink_exports=__exportAll({createConsoleSink:()=>createConsoleSink,createLocalStorageSink:()=>createLocalStorageSink,createNoopSink:()=>createNoopSink,createSessionStorageSink:()=>createSessionStorageSink});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,logger,sink_exports as sinks,tagged};
|
|
@@ -91,20 +91,17 @@ export declare function createLogger({ sinks, flushDeadlineMs, verifyTimeoutMs }
|
|
|
91
91
|
//#endregion
|
|
92
92
|
//#region src/logger.d.ts
|
|
93
93
|
/**
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
Startup records replay to async sinks that verify after the log call.
|
|
102
|
-
Log calls throw only when initialization proves no backend is available,
|
|
103
|
-
which the console sink prevents in every supported runtime.
|
|
94
|
+
Multi-sink logger that writes to all available backends, built lazily on
|
|
95
|
+
the first call. Startup records replay to async sinks that verify after
|
|
96
|
+
the log call. Log calls throw only when initialization proves no backend is
|
|
97
|
+
available, which the console sink prevents in every supported runtime.
|
|
98
|
+
`flush()` awaits verification internally, so no readiness promise is
|
|
99
|
+
exported: awaiting one at module top level was the mistake this design
|
|
100
|
+
removes.
|
|
104
101
|
|
|
105
102
|
@example
|
|
106
103
|
```ts
|
|
107
|
-
import { logger, } from '\@monochromatic-dev/module-logger
|
|
104
|
+
import { logger, } from '\@monochromatic-dev/module-logger';
|
|
108
105
|
|
|
109
106
|
logger.error('unexpected shutdown',);
|
|
110
107
|
await logger.flush();
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import{t as __exportAll}from"./rolldown-runtime-5duEfhBv.mjs";import{i as reportLoggerInternalError,n as createFileSink}from"./file-CRGb1hDK.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])}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
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}}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 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 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}}
|
|
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 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}}function createDefaultSinks(){return[createConsoleSink(),createSessionStorageSink(),createLocalStorageSink(),createFileSink()]}const memo={};function defaultInstance(){return memo.current??=createLogger({sinks:createDefaultSinks()}),memo.current}function forward(level){return function(message){defaultInstance().logger[level](message)}}async function flush(){await defaultInstance().logger.flush()}const logger={debug:forward(`debug`),error:forward(`error`),fatal:forward(`fatal`),flush,info:forward(`info`),trace:forward(`trace`),warn:forward(`warn`)};function verify(){return Promise.resolve(!0)}function write(_record){return Promise.resolve()}function createNoopSink(){return{verify,write}}var sink_exports=__exportAll({createConsoleSink:()=>createConsoleSink,createLocalStorageSink:()=>createLocalStorageSink,createNoopSink:()=>createNoopSink,createSessionStorageSink:()=>createSessionStorageSink});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,logger,sink_exports as sinks,tagged};
|
package/package.json
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
"name": "@monochromatic-dev/module-logger",
|
|
4
4
|
"description": "Zero-config multi-sink logger with tagged composition. Auto-discovers console, sessionStorage, and localStorage backends at import time, plus the file backend under Node and IndexedDB in browsers, with no dynamic imports in either build.",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
6
7
|
"module": "dist/final/neutral/index.mjs",
|
|
7
8
|
"exports": {
|
|
8
9
|
".": {
|
|
@@ -35,13 +36,13 @@
|
|
|
35
36
|
"@types/node": ">=26.2.0",
|
|
36
37
|
"typescript": ">=7.0.2",
|
|
37
38
|
"@monochromatic-dev/config-rolldown": "0.0.1",
|
|
38
|
-
"@monochromatic-dev/module-async-time": "0.0.1",
|
|
39
|
-
"@monochromatic-dev/module-test": "0.0.1",
|
|
40
39
|
"@monochromatic-dev/config-typescript": "0.0.5",
|
|
41
|
-
"@monochromatic-dev/module-
|
|
40
|
+
"@monochromatic-dev/module-async-time": "0.0.1",
|
|
41
|
+
"@monochromatic-dev/module-caught-value": "0.0.1",
|
|
42
|
+
"@monochromatic-dev/module-test": "0.0.1"
|
|
42
43
|
},
|
|
43
44
|
"dependencies": {},
|
|
44
|
-
"version": "0.
|
|
45
|
+
"version": "0.4.0",
|
|
45
46
|
"repository": {
|
|
46
47
|
"type": "git",
|
|
47
48
|
"url": "https://github.com/Aquaticat/Monochromatic.git",
|
|
@@ -6,10 +6,14 @@ import { createSessionStorageSink, } from './sink/session-storage.ts';
|
|
|
6
6
|
import type { Sink, } from './types.ts';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
|
|
10
|
-
order. Chosen at bundle time: `package.json` maps `#default-sinks`
|
|
11
|
-
module under the `default` condition, so every non-Node resolution
|
|
9
|
+
Builds the default sink backends the platform-neutral artifact attempts, in
|
|
10
|
+
priority order. Chosen at bundle time: `package.json` maps `#default-sinks`
|
|
11
|
+
to this module under the `default` condition, so every non-Node resolution
|
|
12
12
|
(browsers, Deno, Bun, workers) inlines this list without a runtime
|
|
13
|
+
platform probe. Called on the default logger's first use, never at import,
|
|
14
|
+
so a runtime that forbids timers and I/O in global scope (Cloudflare
|
|
15
|
+
Workers, issue #493) constructs and verifies its sinks inside the handler
|
|
16
|
+
that logs first. Each runtime keeps only the sinks whose `verify` confirms
|
|
13
17
|
platform probe. Each runtime keeps only the sinks whose `verify` confirms
|
|
14
18
|
its backend: {@link createConsoleSink} everywhere,
|
|
15
19
|
{@link createIndexedDbSink} in browsers, {@link createSessionStorageSink}
|
|
@@ -25,10 +29,19 @@ import type { Sink, } from './types.ts';
|
|
|
25
29
|
instead. The OPFS sink is exported from `./browser` but not a default:
|
|
26
30
|
its stream stages writes until a close that a crash never performs, so
|
|
27
31
|
IndexedDB holds the persistent-browser slot; see `DECISIONS.md`.
|
|
32
|
+
|
|
33
|
+
@returns Fresh default sinks in priority order.
|
|
34
|
+
|
|
35
|
+
@example
|
|
36
|
+
```ts
|
|
37
|
+
const { logger } = createLogger({ sinks: createDefaultSinks() });
|
|
38
|
+
```
|
|
28
39
|
*/
|
|
29
|
-
export
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
40
|
+
export function createDefaultSinks(): readonly Sink[] {
|
|
41
|
+
return [
|
|
42
|
+
createConsoleSink(),
|
|
43
|
+
createIndexedDbSink(),
|
|
44
|
+
createSessionStorageSink(),
|
|
45
|
+
createLocalStorageSink(),
|
|
46
|
+
];
|
|
47
|
+
}
|
|
@@ -6,10 +6,12 @@ import { createSessionStorageSink, } from './sink/session-storage.ts';
|
|
|
6
6
|
import type { Sink, } from './types.ts';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
|
|
10
|
-
Chosen at bundle time: `package.json` maps `#default-sinks` to this
|
|
11
|
-
under the `node` condition, so `logger.ts` inlines this list without
|
|
12
|
-
runtime platform probe.
|
|
9
|
+
Builds the default sink backends the Node artifact attempts, in priority
|
|
10
|
+
order. Chosen at bundle time: `package.json` maps `#default-sinks` to this
|
|
11
|
+
module under the `node` condition, so `logger.ts` inlines this list without
|
|
12
|
+
a runtime platform probe. Called on the default logger's first use, never
|
|
13
|
+
at import, so no sink is constructed in global scope (issue #493). Each
|
|
14
|
+
runtime keeps only the sinks whose `verify`
|
|
13
15
|
confirms its backend: {@link createConsoleSink} everywhere,
|
|
14
16
|
{@link createSessionStorageSink} wherever web storage round-trips (Node
|
|
15
17
|
22+, Deno), {@link createLocalStorageSink} wherever `localStorage`
|
|
@@ -23,10 +25,19 @@ import type { Sink, } from './types.ts';
|
|
|
23
25
|
because Node exposes neither `indexedDB` nor `navigator.storage`; they
|
|
24
26
|
ship through the `./browser` subpath instead, keeping their probes and
|
|
25
27
|
code out of this artifact.
|
|
28
|
+
|
|
29
|
+
@returns Fresh default sinks in priority order.
|
|
30
|
+
|
|
31
|
+
@example
|
|
32
|
+
```ts
|
|
33
|
+
const { logger } = createLogger({ sinks: createDefaultSinks() });
|
|
34
|
+
```
|
|
26
35
|
*/
|
|
27
|
-
export
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
36
|
+
export function createDefaultSinks(): readonly Sink[] {
|
|
37
|
+
return [
|
|
38
|
+
createConsoleSink(),
|
|
39
|
+
createSessionStorageSink(),
|
|
40
|
+
createLocalStorageSink(),
|
|
41
|
+
createFileSink(),
|
|
42
|
+
];
|
|
43
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -4,10 +4,7 @@ export {
|
|
|
4
4
|
DEFAULT_VERIFY_TIMEOUT_MS,
|
|
5
5
|
STARTUP_BUFFER_CAP,
|
|
6
6
|
} from './create-logger.ts';
|
|
7
|
-
export {
|
|
8
|
-
initPromise,
|
|
9
|
-
logger,
|
|
10
|
-
} from './logger.ts';
|
|
7
|
+
export { logger, } from './logger.ts';
|
|
11
8
|
export * as sinks from './sink/index.ts';
|
|
12
9
|
export { tagged, } from './tagged.ts';
|
|
13
10
|
|
package/src/logger.ts
CHANGED
|
@@ -1,40 +1,92 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createDefaultSinks, } from '#default-sinks';
|
|
2
2
|
|
|
3
3
|
import { createLogger, } from './create-logger.ts';
|
|
4
4
|
|
|
5
|
-
import type {
|
|
5
|
+
import type {
|
|
6
|
+
Level,
|
|
7
|
+
Logger,
|
|
8
|
+
} from './types.ts';
|
|
6
9
|
|
|
7
10
|
/**
|
|
8
|
-
Default
|
|
9
|
-
applying {@link createLogger} to the platform-selected `defaultSinks`
|
|
10
|
-
(`default-sinks.node.ts` under the `node` condition,
|
|
11
|
-
`default-sinks.neutral.ts` otherwise; see the `imports` map in
|
|
12
|
-
`package.json`).
|
|
11
|
+
Default logger instance and its readiness promise, built on first use.
|
|
13
12
|
*/
|
|
14
|
-
|
|
15
|
-
initPromise:
|
|
16
|
-
logger:
|
|
17
|
-
}
|
|
13
|
+
type DefaultInstance = {
|
|
14
|
+
readonly initPromise: Promise<void>;
|
|
15
|
+
readonly logger: Logger;
|
|
16
|
+
};
|
|
18
17
|
|
|
19
18
|
/**
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
Memo for the default instance. Empty until the first log or flush call, so
|
|
20
|
+
importing this module (or `tagged`, which reaches it) runs no sink
|
|
21
|
+
discovery: no timers, no I/O, no storage probes. Runtimes that forbid those
|
|
22
|
+
in global scope (Cloudflare Workers, issue #493) therefore pay nothing at
|
|
23
|
+
import and verify their sinks inside whatever handler logs first.
|
|
23
24
|
*/
|
|
24
|
-
|
|
25
|
+
const memo: { current?: DefaultInstance; } = {};
|
|
25
26
|
|
|
26
27
|
/**
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
Builds the default instance on first use and returns it afterwards.
|
|
29
|
+
|
|
30
|
+
@returns Default logger and its readiness promise.
|
|
31
|
+
|
|
32
|
+
@example
|
|
33
|
+
```ts
|
|
34
|
+
const { logger } = defaultInstance();
|
|
35
|
+
```
|
|
36
|
+
*/
|
|
37
|
+
function defaultInstance(): DefaultInstance {
|
|
38
|
+
memo.current ??= createLogger({ sinks: createDefaultSinks(), },);
|
|
39
|
+
return memo.current;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
Builds one level method that forwards to the default instance, creating it
|
|
44
|
+
on the first call.
|
|
45
|
+
|
|
46
|
+
@param level - Severity the method logs at.
|
|
47
|
+
|
|
48
|
+
@returns Forwarding level method.
|
|
49
|
+
*/
|
|
50
|
+
function forward(level: Level,): (message: string,) => void {
|
|
51
|
+
return function logAtLevel(message: string,): void {
|
|
52
|
+
defaultInstance()
|
|
53
|
+
.logger[level](message,);
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
Awaits the default instance's own `flush`, creating the instance first so a
|
|
59
|
+
flush before any log still verifies the sinks and drains them.
|
|
60
|
+
*/
|
|
61
|
+
async function flush(): Promise<void> {
|
|
62
|
+
await defaultInstance()
|
|
63
|
+
.logger
|
|
64
|
+
.flush();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
Multi-sink logger that writes to all available backends, built lazily on
|
|
69
|
+
the first call. Startup records replay to async sinks that verify after
|
|
70
|
+
the log call. Log calls throw only when initialization proves no backend is
|
|
71
|
+
available, which the console sink prevents in every supported runtime.
|
|
72
|
+
`flush()` awaits verification internally, so no readiness promise is
|
|
73
|
+
exported: awaiting one at module top level was the mistake this design
|
|
74
|
+
removes.
|
|
75
|
+
|
|
32
76
|
@example
|
|
33
77
|
```ts
|
|
34
|
-
import { logger, } from '\@monochromatic-dev/module-logger
|
|
35
|
-
|
|
78
|
+
import { logger, } from '\@monochromatic-dev/module-logger';
|
|
79
|
+
|
|
36
80
|
logger.error('unexpected shutdown',);
|
|
37
81
|
await logger.flush();
|
|
38
82
|
```
|
|
39
83
|
*/
|
|
40
|
-
export const logger: Logger =
|
|
84
|
+
export const logger: Logger = {
|
|
85
|
+
debug: forward('debug',),
|
|
86
|
+
error: forward('error',),
|
|
87
|
+
fatal: forward('fatal',),
|
|
88
|
+
flush,
|
|
89
|
+
info: forward('info',),
|
|
90
|
+
trace: forward('trace',),
|
|
91
|
+
warn: forward('warn',),
|
|
92
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
Guards issue #493: evaluating the root entry in a runtime that forbids
|
|
3
|
+
timers in global scope (Cloudflare Workers throw from `setTimeout` there)
|
|
4
|
+
must produce no `logger internal error` output, because the default logger
|
|
5
|
+
is built on first use, not at import. Once a handler runs, the first log
|
|
6
|
+
call builds it and the console sink verifies normally.
|
|
7
|
+
|
|
8
|
+
The built artifact is imported dynamically inside the test so the throwing
|
|
9
|
+
`setTimeout` is in place during module evaluation; this file therefore
|
|
10
|
+
imports nothing from the logger statically.
|
|
11
|
+
|
|
12
|
+
@module
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
describe,
|
|
17
|
+
expect,
|
|
18
|
+
it,
|
|
19
|
+
} from '@monochromatic-dev/module-test/ts';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
Message the first log call inside the "handler" sends, distinct enough to
|
|
23
|
+
find in the console stub's calls.
|
|
24
|
+
*/
|
|
25
|
+
const HANDLER_MESSAGE = 'first log inside a handler after a restricted import';
|
|
26
|
+
|
|
27
|
+
await describe({
|
|
28
|
+
name: 'default logger under a global-scope-restricted runtime',
|
|
29
|
+
// Both tests stub console methods, so they run one at a time.
|
|
30
|
+
concurrency: 1,
|
|
31
|
+
children: [
|
|
32
|
+
it({
|
|
33
|
+
name: 'importing the root entry while setTimeout throws writes no breadcrumb, and the first log inside a handler works',
|
|
34
|
+
fn: async ({ sinon, },) => {
|
|
35
|
+
const warn = sinon.stub(
|
|
36
|
+
console,
|
|
37
|
+
'warn',
|
|
38
|
+
);
|
|
39
|
+
const info = sinon.stub(
|
|
40
|
+
console,
|
|
41
|
+
'info',
|
|
42
|
+
);
|
|
43
|
+
/**
|
|
44
|
+
Timer stub that behaves like a Workers global scope: any timer is a
|
|
45
|
+
disallowed operation.
|
|
46
|
+
*/
|
|
47
|
+
const forbidTimers = sinon.stub(
|
|
48
|
+
globalThis,
|
|
49
|
+
'setTimeout',
|
|
50
|
+
)
|
|
51
|
+
.throws(new Error('Disallowed operation called within global scope',),);
|
|
52
|
+
/**
|
|
53
|
+
Root entry evaluated with timers forbidden, as a Worker isolate does.
|
|
54
|
+
*/
|
|
55
|
+
const entry = await import('@monochromatic-dev/module-logger');
|
|
56
|
+
/**
|
|
57
|
+
`tagged` reaches the singleton through its default parameter; wrapping
|
|
58
|
+
must not build it either.
|
|
59
|
+
*/
|
|
60
|
+
const l = entry.tagged({ tag: 'restricted', },);
|
|
61
|
+
expect(warn.callCount,)
|
|
62
|
+
.toBe(0,);
|
|
63
|
+
forbidTimers.restore();
|
|
64
|
+
|
|
65
|
+
// Inside a handler, timers are allowed again: the first log builds the
|
|
66
|
+
// default logger, its sinks verify, and the console sink writes.
|
|
67
|
+
l.info(HANDLER_MESSAGE,);
|
|
68
|
+
await entry.logger.flush();
|
|
69
|
+
expect(warn.callCount,)
|
|
70
|
+
.toBe(0,);
|
|
71
|
+
/**
|
|
72
|
+
Console lines that carried the handler message.
|
|
73
|
+
*/
|
|
74
|
+
const landed = info.getCalls()
|
|
75
|
+
.filter(function carriesMessage(call,) {
|
|
76
|
+
return call.args
|
|
77
|
+
.some(function mentions(argument,) {
|
|
78
|
+
return String(argument,)
|
|
79
|
+
.includes(HANDLER_MESSAGE,);
|
|
80
|
+
},);
|
|
81
|
+
},);
|
|
82
|
+
expect(landed.length,)
|
|
83
|
+
.toBe(1,);
|
|
84
|
+
},
|
|
85
|
+
},),
|
|
86
|
+
|
|
87
|
+
it({
|
|
88
|
+
name: 'flush before any log still builds the default logger and resolves',
|
|
89
|
+
fn: async ({ sinon, },) => {
|
|
90
|
+
const warn = sinon.stub(
|
|
91
|
+
console,
|
|
92
|
+
'warn',
|
|
93
|
+
);
|
|
94
|
+
const entry = await import('@monochromatic-dev/module-logger');
|
|
95
|
+
await entry.logger.flush();
|
|
96
|
+
expect(warn.callCount,)
|
|
97
|
+
.toBe(0,);
|
|
98
|
+
},
|
|
99
|
+
},),
|
|
100
|
+
],
|
|
101
|
+
},);
|