@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,137 @@
|
|
|
1
|
+
// oxlint-disable typescript/no-unsafe-assignment, typescript/no-unsafe-member-access, typescript/strict-boolean-expressions -- browser evaluate callbacks lose type info across page boundary
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
expect,
|
|
5
|
+
test,
|
|
6
|
+
} from '@playwright/test';
|
|
7
|
+
|
|
8
|
+
declare global {
|
|
9
|
+
// oxlint-disable-next-line typescript/consistent-type-imports -- typeof import() cannot use import type syntax
|
|
10
|
+
var moduleLogger: typeof import('@monochromatic-dev/module-logger');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
test.describe('sessionStorage sink', () => {
|
|
14
|
+
test.beforeEach(async ({ page, },) => {
|
|
15
|
+
await page.goto('/',);
|
|
16
|
+
await page.waitForFunction(() => globalThis.moduleLogger !== undefined);
|
|
17
|
+
},);
|
|
18
|
+
|
|
19
|
+
test('createSessionStorageSink exposes a callable verify', async ({ page, },) => {
|
|
20
|
+
const typeofVerify = await page.evaluate(() => {
|
|
21
|
+
const { createSessionStorageSink, } = globalThis.moduleLogger.sinks;
|
|
22
|
+
return typeof createSessionStorageSink().verify;
|
|
23
|
+
},);
|
|
24
|
+
expect(typeofVerify,).toBe('function',);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('verify resolves a boolean', async ({ page, },) => {
|
|
28
|
+
const resultType = await page.evaluate(async () => {
|
|
29
|
+
const { createSessionStorageSink, } = globalThis.moduleLogger.sinks;
|
|
30
|
+
return typeof (await createSessionStorageSink().verify());
|
|
31
|
+
},);
|
|
32
|
+
expect(resultType,).toBe('boolean',);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('verify detects availability', async ({ page, },) => {
|
|
36
|
+
const result = await page.evaluate(async () => {
|
|
37
|
+
const { createSessionStorageSink, } = globalThis.moduleLogger.sinks;
|
|
38
|
+
return createSessionStorageSink().verify();
|
|
39
|
+
},);
|
|
40
|
+
expect(result,).toBe(true,);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('sink write method exists', async ({ page, },) => {
|
|
44
|
+
const typeofSink = await page.evaluate(() => {
|
|
45
|
+
const { createSessionStorageSink, } = globalThis.moduleLogger.sinks;
|
|
46
|
+
return typeof createSessionStorageSink().write;
|
|
47
|
+
},);
|
|
48
|
+
expect(typeofSink,).toBe('function',);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('a verified sink writes records across levels and message shapes', async ({ page, },) => {
|
|
52
|
+
const allSucceeded = await page.evaluate(async () => {
|
|
53
|
+
const { createSessionStorageSink, } = globalThis.moduleLogger.sinks;
|
|
54
|
+
const sink = createSessionStorageSink();
|
|
55
|
+
await sink.verify();
|
|
56
|
+
const levels = ['trace', 'debug', 'info', 'warn', 'error', 'fatal',] as const;
|
|
57
|
+
const messages = [
|
|
58
|
+
'test message',
|
|
59
|
+
'Hello 世界 🌍',
|
|
60
|
+
'',
|
|
61
|
+
'{"key": "value", "nested": {"a": 1}}',
|
|
62
|
+
];
|
|
63
|
+
for (const level of levels) {
|
|
64
|
+
for (const message of messages) {
|
|
65
|
+
try {
|
|
66
|
+
void sink.write({
|
|
67
|
+
level,
|
|
68
|
+
message,
|
|
69
|
+
timestamp: Date.now(),
|
|
70
|
+
},);
|
|
71
|
+
}
|
|
72
|
+
catch (error: unknown) {
|
|
73
|
+
console.warn('sessionStorage sink browser test write failed', error,);
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return true;
|
|
79
|
+
},);
|
|
80
|
+
expect(allSucceeded,).toBe(true,);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('written records can be retrieved from sessionStorage', async ({ page, },) => {
|
|
84
|
+
const result = await page.evaluate(async () => {
|
|
85
|
+
const { createSessionStorageSink, } = globalThis.moduleLogger.sinks;
|
|
86
|
+
|
|
87
|
+
// Clear any existing logs first.
|
|
88
|
+
const keysToRemove: string[] = [];
|
|
89
|
+
const storageLength = globalThis.sessionStorage.length;
|
|
90
|
+
for (let storageIndex = 0; storageIndex < storageLength; storageIndex++) {
|
|
91
|
+
const key = globalThis.sessionStorage.key(storageIndex,);
|
|
92
|
+
if (key?.startsWith('monochromatic.log',))
|
|
93
|
+
keysToRemove.push(key,);
|
|
94
|
+
}
|
|
95
|
+
keysToRemove.forEach(key => {
|
|
96
|
+
globalThis.sessionStorage.removeItem(key,);
|
|
97
|
+
},);
|
|
98
|
+
|
|
99
|
+
const sink = createSessionStorageSink();
|
|
100
|
+
await sink.verify();
|
|
101
|
+
|
|
102
|
+
const testMessage = `unique-test-${Date.now()}`;
|
|
103
|
+
await sink.write({
|
|
104
|
+
level: 'info' as const,
|
|
105
|
+
message: testMessage,
|
|
106
|
+
timestamp: Date.now(),
|
|
107
|
+
},);
|
|
108
|
+
// Routine severity buffers; the flush hook forces the batch out so the
|
|
109
|
+
// read-back below observes it deterministically.
|
|
110
|
+
await sink.flush?.();
|
|
111
|
+
|
|
112
|
+
// Find the written record.
|
|
113
|
+
const currentStorageLength = globalThis.sessionStorage.length;
|
|
114
|
+
for (let storageIndex = 0; storageIndex < currentStorageLength; storageIndex++) {
|
|
115
|
+
const key = globalThis.sessionStorage.key(storageIndex,);
|
|
116
|
+
if (key?.startsWith('monochromatic.log',)) {
|
|
117
|
+
const value = globalThis.sessionStorage.getItem(key,);
|
|
118
|
+
if (value?.includes(testMessage,)) {
|
|
119
|
+
const parsed = JSON.parse(value,);
|
|
120
|
+
return {
|
|
121
|
+
found: true,
|
|
122
|
+
message: parsed.message,
|
|
123
|
+
level: parsed.level,
|
|
124
|
+
testMessage,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return { found: false, message: null, level: null, testMessage, };
|
|
131
|
+
},);
|
|
132
|
+
|
|
133
|
+
expect(result.found,).toBe(true,);
|
|
134
|
+
expect(result.message,).toBe(result.testMessage,);
|
|
135
|
+
expect(result.level,).toBe('info',);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { reportLoggerInternalError, } from '../error-format.ts';
|
|
2
|
+
import { createRecordBuffer, } from './record-buffer.ts';
|
|
3
|
+
import { createSessionStorageStore, } from './session-storage-store.ts';
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
Level,
|
|
7
|
+
Sink,
|
|
8
|
+
} from '../types.ts';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Verifies sessionStorage actually persists data. Stateless: the logger calls
|
|
12
|
+
* this once per sink at startup and owns the resulting availability, so no
|
|
13
|
+
* verified/available flag is kept here.
|
|
14
|
+
*
|
|
15
|
+
* Election is by probe alone: any runtime whose `sessionStorage` round-trips
|
|
16
|
+
* (browsers, Node 22+, Deno) keeps the sink, and the buffered write path
|
|
17
|
+
* keeps the per-record cost acceptable everywhere rather than a runtime brand
|
|
18
|
+
* check deciding who may log here.
|
|
19
|
+
*
|
|
20
|
+
* @returns Whether sessionStorage is available and round-trips a probe write.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* if (await verifySessionStorage()) {
|
|
25
|
+
* // sessionStorage usable
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
function verifySessionStorage(): Promise<boolean> {
|
|
30
|
+
try {
|
|
31
|
+
/**
|
|
32
|
+
* Sentinel key used only for the probe write/read; removed afterward to avoid polluting real log entries.
|
|
33
|
+
*/
|
|
34
|
+
const testKey = '__monochromatic_verify__';
|
|
35
|
+
/**
|
|
36
|
+
* Timestamp-based probe value so concurrent verifications never read each other's writes.
|
|
37
|
+
*/
|
|
38
|
+
const testValue = `test-${Date.now()}`;
|
|
39
|
+
globalThis.sessionStorage
|
|
40
|
+
.setItem(
|
|
41
|
+
testKey,
|
|
42
|
+
testValue,
|
|
43
|
+
);
|
|
44
|
+
/**
|
|
45
|
+
* Probe value read back from storage; equality with `testValue` proves writes actually persist.
|
|
46
|
+
*/
|
|
47
|
+
const readBack = globalThis.sessionStorage
|
|
48
|
+
.getItem(testKey,);
|
|
49
|
+
globalThis.sessionStorage
|
|
50
|
+
.removeItem(testKey,);
|
|
51
|
+
return Promise.resolve(readBack === testValue,);
|
|
52
|
+
}
|
|
53
|
+
catch (error: unknown) {
|
|
54
|
+
if ('sessionStorage' in globalThis)
|
|
55
|
+
reportLoggerInternalError({
|
|
56
|
+
context: 'sessionStorage sink verification failed',
|
|
57
|
+
error,
|
|
58
|
+
},);
|
|
59
|
+
return Promise.resolve(false,);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Builds a sessionStorage sink that buffers serialized records through the
|
|
65
|
+
* shared {@link createRecordBuffer} policy and persists each newline-joined
|
|
66
|
+
* JSONL batch under a counter-incremented key through
|
|
67
|
+
* {@link createSessionStorageStore}. One uniform write path runs on every
|
|
68
|
+
* runtime; no per-runtime mode exists. Flush triggers (32 KiB in-write cap,
|
|
69
|
+
* `warn`-or-worse severity, 250 ms quiet-period deadline, page lifecycle,
|
|
70
|
+
* and the `flush` hook) are the buffer's; see {@link createRecordBuffer}.
|
|
71
|
+
*
|
|
72
|
+
* @returns Sink backed by web `sessionStorage`.
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```ts
|
|
76
|
+
* const { logger } = createLogger({ sinks: [createSessionStorageSink()] });
|
|
77
|
+
* logger.info('user signed in'); // buffered
|
|
78
|
+
* logger.warn('quota near'); // flushes both records in one batch
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
export function createSessionStorageSink(): Sink {
|
|
82
|
+
/**
|
|
83
|
+
* Persistence engine owning key allocation, footprint accounting, and quota
|
|
84
|
+
* eviction; the buffer decides when a batch is handed to it.
|
|
85
|
+
*/
|
|
86
|
+
const store = createSessionStorageStore();
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Shared buffering stage; every flush trigger lands one joined batch in the
|
|
90
|
+
* persistence engine synchronously.
|
|
91
|
+
*/
|
|
92
|
+
const buffer = createRecordBuffer({ onFlush: store.persist, },);
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Buffers a log record through the shared policy; see
|
|
96
|
+
* {@link createRecordBuffer} for the flush triggers.
|
|
97
|
+
*
|
|
98
|
+
* @param record - Log record to buffer and eventually persist.
|
|
99
|
+
*
|
|
100
|
+
* @mutates record - `JSON.stringify` may invoke `toJSON`, getters, or proxy traps.
|
|
101
|
+
*/
|
|
102
|
+
function write(record: {
|
|
103
|
+
level: Level;
|
|
104
|
+
message: string;
|
|
105
|
+
timestamp: number;
|
|
106
|
+
},): Promise<void> {
|
|
107
|
+
buffer.add({
|
|
108
|
+
level: record.level,
|
|
109
|
+
serialized: JSON.stringify(record,),
|
|
110
|
+
},);
|
|
111
|
+
return Promise.resolve();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Drains the buffer into the persistence engine; the drain is synchronous,
|
|
116
|
+
* so the batch has landed by the time the resolved promise is observed.
|
|
117
|
+
*/
|
|
118
|
+
function flush(): Promise<void> {
|
|
119
|
+
buffer.drain();
|
|
120
|
+
return Promise.resolve();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
flush,
|
|
125
|
+
verify: verifySessionStorage,
|
|
126
|
+
write,
|
|
127
|
+
};
|
|
128
|
+
}
|