@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.
Files changed (57) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSES/GPL-3.0-or-later.txt +674 -0
  3. package/LICENSES/LGPL-3.0-or-later.txt +165 -0
  4. package/README.md +404 -0
  5. package/dist/final/neutral/index.d.mts +673 -0
  6. package/dist/final/neutral/index.mjs +3 -0
  7. package/dist/final/neutral/rolldown-runtime-5duEfhBv.mjs +1 -0
  8. package/dist/final/node/index.d.mts +673 -0
  9. package/dist/final/node/index.mjs +3 -0
  10. package/dist/final/node/rolldown-runtime-5duEfhBv.mjs +1 -0
  11. package/package.json +43 -0
  12. package/src/create-logger.ts +494 -0
  13. package/src/create-logger.unit.test.ts +752 -0
  14. package/src/error-format.ts +43 -0
  15. package/src/index.ts +35 -0
  16. package/src/logger.ts +67 -0
  17. package/src/logger.unit.test.ts +190 -0
  18. package/src/sink/console-control-chars.ts +140 -0
  19. package/src/sink/console-control-chars.unit.test.ts +206 -0
  20. package/src/sink/console.ts +531 -0
  21. package/src/sink/console.unit.test.ts +542 -0
  22. package/src/sink/file.ts +297 -0
  23. package/src/sink/file.unit.test.ts +202 -0
  24. package/src/sink/index.ts +11 -0
  25. package/src/sink/indexed-db-util.ts +96 -0
  26. package/src/sink/indexed-db.browser.test.ts +184 -0
  27. package/src/sink/indexed-db.ts +324 -0
  28. package/src/sink/indexed-db.unit.test.ts +80 -0
  29. package/src/sink/local-storage-key.ts +176 -0
  30. package/src/sink/local-storage-key.unit.test.ts +106 -0
  31. package/src/sink/local-storage-quota.ts +60 -0
  32. package/src/sink/local-storage-quota.unit.test.ts +98 -0
  33. package/src/sink/local-storage-store.ts +368 -0
  34. package/src/sink/local-storage-store.unit.test.ts +329 -0
  35. package/src/sink/local-storage.browser.test.ts +125 -0
  36. package/src/sink/local-storage.ts +182 -0
  37. package/src/sink/local-storage.unit.test.ts +218 -0
  38. package/src/sink/noop.ts +46 -0
  39. package/src/sink/noop.unit.test.ts +47 -0
  40. package/src/sink/opfs.browser.test.ts +84 -0
  41. package/src/sink/opfs.ts +212 -0
  42. package/src/sink/opfs.unit.test.ts +81 -0
  43. package/src/sink/record-buffer.ts +230 -0
  44. package/src/sink/record-buffer.unit.test.ts +288 -0
  45. package/src/sink/session-storage-quota.ts +57 -0
  46. package/src/sink/session-storage-quota.unit.test.ts +98 -0
  47. package/src/sink/session-storage-store.ts +178 -0
  48. package/src/sink/session-storage.browser.test.ts +137 -0
  49. package/src/sink/session-storage.ts +128 -0
  50. package/src/sink/session-storage.unit.test.ts +527 -0
  51. package/src/sink/web-storage-quota-error.ts +43 -0
  52. package/src/sink/web-storage-quota-error.unit.test.ts +55 -0
  53. package/src/sink/web-storage-runtime.ts +49 -0
  54. package/src/startup.unit.test.ts +232 -0
  55. package/src/tagged.ts +74 -0
  56. package/src/tagged.unit.test.ts +211 -0
  57. 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
+ }