@monochromatic-dev/module-logger 0.2.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 +27 -0
- package/README.md +53 -13
- package/dist/final/neutral/browser.d.mts +60 -0
- package/dist/final/neutral/browser.mjs +1 -0
- package/dist/final/neutral/index.d.mts +359 -596
- package/dist/final/neutral/index.mjs +2 -3
- package/dist/final/neutral/indexed-db-hsIfv7Cv.mjs +2 -0
- package/dist/final/neutral/types-BkkBXgY3.d.mts +76 -0
- package/dist/final/node/file-CRGb1hDK.mjs +1 -0
- package/dist/final/node/index.d.mts +359 -596
- package/dist/final/node/index.mjs +3 -3
- package/dist/final/node/node.d.mts +103 -0
- package/dist/final/node/node.mjs +1 -0
- package/dist/final/node/types-BkkBXgY3.d.mts +76 -0
- package/package.json +19 -4
- package/src/artifact-platform-split.unit.test.ts +140 -0
- package/src/browser.ts +14 -0
- package/src/create-logger.ts +183 -183
- package/src/create-logger.unit.test.ts +112 -112
- package/src/default-sinks.neutral.ts +47 -0
- package/src/default-sinks.node.ts +43 -0
- package/src/error-format.ts +23 -23
- package/src/index.ts +1 -4
- package/src/logger.ts +76 -51
- package/src/node.ts +23 -0
- package/src/restricted-global-scope.unit.test.ts +101 -0
- package/src/sink/console-control-chars.ts +64 -64
- package/src/sink/console-control-chars.unit.test.ts +14 -14
- package/src/sink/console.ts +194 -194
- package/src/sink/console.unit.test.ts +18 -18
- package/src/sink/file.ts +136 -140
- package/src/sink/file.unit.test.ts +19 -26
- package/src/sink/index.ts +4 -7
- package/src/sink/indexed-db-util.ts +42 -42
- package/src/sink/indexed-db.browser.test.ts +7 -7
- package/src/sink/indexed-db.ts +109 -109
- package/src/sink/indexed-db.unit.test.ts +5 -13
- package/src/sink/local-storage-key.ts +73 -73
- package/src/sink/local-storage-key.unit.test.ts +8 -8
- package/src/sink/local-storage-quota.ts +37 -37
- package/src/sink/local-storage-quota.unit.test.ts +8 -8
- package/src/sink/local-storage-store.ts +113 -113
- package/src/sink/local-storage-store.unit.test.ts +35 -35
- package/src/sink/local-storage.ts +72 -72
- package/src/sink/local-storage.unit.test.ts +27 -27
- package/src/sink/noop.ts +20 -20
- package/src/sink/noop.unit.test.ts +1 -1
- package/src/sink/opfs.browser.test.ts +7 -7
- package/src/sink/opfs.ts +62 -62
- package/src/sink/opfs.unit.test.ts +5 -13
- package/src/sink/record-buffer.ts +84 -84
- package/src/sink/record-buffer.unit.test.ts +20 -20
- package/src/sink/session-storage-quota.ts +34 -34
- package/src/sink/session-storage-quota.unit.test.ts +8 -8
- package/src/sink/session-storage-store.ts +72 -72
- package/src/sink/session-storage.ts +48 -48
- package/src/sink/session-storage.unit.test.ts +39 -39
- package/src/sink/web-storage-quota-error.ts +22 -22
- package/src/sink/web-storage-quota-error.unit.test.ts +2 -2
- package/src/sink/web-storage-runtime.ts +24 -24
- package/src/startup.unit.test.ts +18 -18
- package/src/tagged.ts +35 -35
- package/src/tagged.unit.test.ts +8 -8
- package/src/types.ts +39 -39
package/src/sink/file.ts
CHANGED
|
@@ -1,88 +1,97 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import {
|
|
2
|
+
appendFile,
|
|
3
|
+
mkdir,
|
|
4
|
+
readFile,
|
|
5
|
+
stat,
|
|
6
|
+
} from 'node:fs/promises';
|
|
7
|
+
import {
|
|
8
|
+
dirname,
|
|
9
|
+
join,
|
|
5
10
|
} from 'node:path';
|
|
6
11
|
|
|
7
12
|
import { reportLoggerInternalError, } from '../error-format.ts';
|
|
8
13
|
|
|
9
14
|
import type { Sink, } from '../types.ts';
|
|
10
15
|
|
|
16
|
+
//region Ancestor search
|
|
17
|
+
// Locates the project the process belongs to so log files land beside its
|
|
18
|
+
// `node_modules` rather than at whatever stray cwd launched the script.
|
|
19
|
+
|
|
11
20
|
/**
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
Sentinel returned by {@link findNodeModulesUp} when no ancestor directory
|
|
22
|
+
contains a `node_modules`. A unique symbol so it never collides with a real
|
|
23
|
+
path string the walk might otherwise return, keeping the result free of a
|
|
24
|
+
banned `string | undefined` union.
|
|
25
|
+
|
|
26
|
+
@example
|
|
27
|
+
```ts
|
|
28
|
+
const dir = await findNodeModulesUp({ cwd, stat, dirname, join });
|
|
29
|
+
if (dir === NO_NODE_MODULES_FOUND) {
|
|
30
|
+
// no ancestor project root
|
|
31
|
+
}
|
|
32
|
+
```
|
|
24
33
|
*/
|
|
25
34
|
export const NO_NODE_MODULES_FOUND: unique symbol = Symbol('logger:no-node-modules-found',);
|
|
26
35
|
|
|
27
36
|
/**
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
37
|
+
Walks up from `cwd` to find the nearest ancestor directory containing a
|
|
38
|
+
`node_modules` subdirectory, returning that subdirectory's absolute path.
|
|
39
|
+
|
|
40
|
+
Using find-up rather than cwd-relative placement keeps log directories
|
|
41
|
+
anchored to the project the caller actually belongs to. Without this,
|
|
42
|
+
scripts invoked from build output (e.g. `dist/`) or other stray cwds
|
|
43
|
+
would create `node_modules/.monochromatic/` inside those trees, polluting
|
|
44
|
+
shipped artifacts.
|
|
45
|
+
|
|
46
|
+
Exported through the `./node` subpath primarily so `file.unit.test.ts`
|
|
47
|
+
can exercise both the hit and miss paths directly with an injected `stat`.
|
|
48
|
+
|
|
49
|
+
@param cwd - starting directory for the upward search
|
|
50
|
+
|
|
51
|
+
@param stat - `node:fs/promises` stat, injected so tests drive hit,
|
|
52
|
+
miss, and fault paths deterministically without touching a real tree
|
|
53
|
+
|
|
54
|
+
@param dirname - `node:path` dirname
|
|
55
|
+
|
|
56
|
+
@param join - `node:path` join
|
|
57
|
+
|
|
58
|
+
@param reportError - logger fault reporter injected for deterministic tests
|
|
59
|
+
|
|
60
|
+
@returns absolute path to the nearest ancestor `node_modules`, or
|
|
61
|
+
{@link NO_NODE_MODULES_FOUND} when no ancestor contains one
|
|
62
|
+
|
|
63
|
+
@example
|
|
64
|
+
```ts
|
|
65
|
+
const dir = await findNodeModulesUp({ cwd: process.cwd(), stat, dirname, join });
|
|
66
|
+
```
|
|
58
67
|
*/
|
|
59
68
|
export async function findNodeModulesUp(
|
|
60
69
|
{
|
|
61
70
|
cwd,
|
|
62
|
-
stat,
|
|
63
|
-
dirname,
|
|
64
|
-
join,
|
|
71
|
+
stat: statPath,
|
|
72
|
+
dirname: parentOf,
|
|
73
|
+
join: joinPath,
|
|
65
74
|
reportError = reportLoggerInternalError,
|
|
66
75
|
}: {
|
|
67
76
|
readonly cwd: string;
|
|
68
|
-
readonly stat: typeof
|
|
69
|
-
readonly dirname: typeof
|
|
70
|
-
readonly join: typeof
|
|
77
|
+
readonly stat: typeof stat;
|
|
78
|
+
readonly dirname: typeof dirname;
|
|
79
|
+
readonly join: typeof join;
|
|
71
80
|
readonly reportError?: typeof reportLoggerInternalError;
|
|
72
81
|
},
|
|
73
82
|
): Promise<string | typeof NO_NODE_MODULES_FOUND> {
|
|
74
83
|
/**
|
|
75
|
-
|
|
84
|
+
Directory being tested in this iteration; either resolves to a node_modules or triggers the walk to the parent.
|
|
76
85
|
*/
|
|
77
|
-
const candidate =
|
|
86
|
+
const candidate = joinPath(
|
|
78
87
|
cwd,
|
|
79
88
|
'node_modules',
|
|
80
89
|
);
|
|
81
90
|
try {
|
|
82
91
|
/**
|
|
83
|
-
|
|
92
|
+
Stat result for `candidate`; only directories count as a hit, guarding against a sibling file also named `node_modules`.
|
|
84
93
|
*/
|
|
85
|
-
const entry = await
|
|
94
|
+
const entry = await statPath(candidate,);
|
|
86
95
|
if (entry.isDirectory())
|
|
87
96
|
return candidate;
|
|
88
97
|
}
|
|
@@ -98,94 +107,73 @@ export async function findNodeModulesUp(
|
|
|
98
107
|
}
|
|
99
108
|
}
|
|
100
109
|
/**
|
|
101
|
-
|
|
110
|
+
Parent directory used by the next recursive step; equal to `cwd` only at the filesystem root, which terminates the walk.
|
|
102
111
|
*/
|
|
103
|
-
const parent =
|
|
112
|
+
const parent = parentOf(cwd,);
|
|
104
113
|
if (parent === cwd)
|
|
105
114
|
return NO_NODE_MODULES_FOUND;
|
|
106
115
|
return await findNodeModulesUp({
|
|
107
116
|
cwd: parent,
|
|
108
|
-
stat,
|
|
109
|
-
dirname,
|
|
110
|
-
join,
|
|
117
|
+
stat: statPath,
|
|
118
|
+
dirname: parentOf,
|
|
119
|
+
join: joinPath,
|
|
111
120
|
reportError,
|
|
112
121
|
},);
|
|
113
122
|
}
|
|
123
|
+
//endregion Ancestor search
|
|
124
|
+
|
|
125
|
+
//region File sink
|
|
126
|
+
// Append-only JSONL sink. Imports `node:fs/promises` statically: this module
|
|
127
|
+
// is reachable only from the Node artifact (the `./node` subpath and the
|
|
128
|
+
// `#default-sinks` `node` condition), so the `node` export condition already
|
|
129
|
+
// asserts a Node runtime and no `process.versions.node` probe is needed.
|
|
114
130
|
|
|
115
131
|
/**
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
+
Builds a file sink that appends JSONL records to the nearest ancestor
|
|
133
|
+
`node_modules/.monochromatic/{timestamp}.log.jsonl` (resolved once during
|
|
134
|
+
verification). The resolved path and the verification memo live in this
|
|
135
|
+
instance's closure (no module-global state), so independent loggers and
|
|
136
|
+
tests never share a log file or need a reset hook. No `flush` hook: each
|
|
137
|
+
`write` awaits `appendFile` directly, so there is no buffered state to
|
|
138
|
+
drain.
|
|
139
|
+
|
|
140
|
+
@returns Sink backed by `node:fs/promises`.
|
|
141
|
+
|
|
142
|
+
@example
|
|
143
|
+
```ts
|
|
144
|
+
const { logger } = createLogger({ sinks: [createFileSink()] });
|
|
145
|
+
logger.error('unhandled rejection');
|
|
146
|
+
await logger.flush();
|
|
147
|
+
```
|
|
132
148
|
*/
|
|
133
149
|
export function createFileSink(): Sink {
|
|
134
150
|
/**
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
151
|
+
Instance-local file-sink resources. `filePath` is populated during
|
|
152
|
+
verification and read by `write`; `verifyPromise` memoizes concurrent
|
|
153
|
+
verification so a caller arriving mid-flight shares the same async work
|
|
154
|
+
rather than starting a second probe.
|
|
139
155
|
*/
|
|
140
156
|
const state: {
|
|
141
|
-
// oxlint-disable-next-line typescript/consistent-type-imports -- typeof import() cannot use import type syntax
|
|
142
|
-
appendFile?: typeof import('node:fs/promises').appendFile;
|
|
143
157
|
filePath?: string;
|
|
144
158
|
verifyPromise?: Promise<boolean>;
|
|
145
159
|
} = {};
|
|
146
160
|
|
|
147
161
|
/**
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
* @returns Whether file system logging is available.
|
|
162
|
+
Actual verification work, invoked exactly once via the memoized
|
|
163
|
+
`verifyPromise`. Resolves the log path, marking the sink unavailable
|
|
164
|
+
when the upward search yields {@link NO_NODE_MODULES_FOUND}. The logger
|
|
165
|
+
owns the resulting availability, so no flag is kept here.
|
|
166
|
+
|
|
167
|
+
@returns Whether file system logging is available.
|
|
155
168
|
*/
|
|
156
169
|
async function runVerify(): Promise<boolean> {
|
|
157
|
-
// Guard: skip dynamic import entirely outside Node.js to avoid
|
|
158
|
-
// browser console errors from attempting to fetch node: URLs.
|
|
159
|
-
if ((globalThis.process
|
|
160
|
-
=== undefined)
|
|
161
|
-
|| (globalThis.process
|
|
162
|
-
.versions
|
|
163
|
-
?.node
|
|
164
|
-
=== undefined))
|
|
165
|
-
return false;
|
|
166
|
-
|
|
167
170
|
try {
|
|
168
|
-
// Dynamic import for Node.js modules: cache appendFile for use in write.
|
|
169
|
-
/**
|
|
170
|
-
* Dynamically imported `node:fs/promises`; held in this scope so its members are reused without re-importing.
|
|
171
|
-
*/
|
|
172
|
-
const fs = await import('node:fs/promises');
|
|
173
|
-
/**
|
|
174
|
-
* Path utilities dynamically imported alongside `fs`; needed by the upward search for the closest node_modules.
|
|
175
|
-
*/
|
|
176
|
-
const {
|
|
177
|
-
dirname,
|
|
178
|
-
join,
|
|
179
|
-
} = await import('node:path');
|
|
180
|
-
|
|
181
|
-
state.appendFile = fs.appendFile;
|
|
182
|
-
|
|
183
171
|
/**
|
|
184
|
-
|
|
172
|
+
Resolved absolute path of the closest ancestor `node_modules`, or the sentinel when none exists (e.g. a stray cwd).
|
|
185
173
|
*/
|
|
186
174
|
const nodeModulesDir = await findNodeModulesUp({
|
|
187
175
|
cwd: process.cwd(),
|
|
188
|
-
stat
|
|
176
|
+
stat,
|
|
189
177
|
dirname,
|
|
190
178
|
join,
|
|
191
179
|
},);
|
|
@@ -198,44 +186,48 @@ export function createFileSink(): Sink {
|
|
|
198
186
|
return false;
|
|
199
187
|
|
|
200
188
|
/**
|
|
201
|
-
|
|
189
|
+
Directory under the chosen `node_modules` where every monochromatic log file lands.
|
|
202
190
|
*/
|
|
203
191
|
const LOG_DIR = join(
|
|
204
192
|
nodeModulesDir,
|
|
205
193
|
'.monochromatic',
|
|
206
194
|
);
|
|
207
|
-
await
|
|
195
|
+
await mkdir(
|
|
208
196
|
LOG_DIR,
|
|
209
197
|
{ recursive: true, },
|
|
210
198
|
);
|
|
211
199
|
|
|
212
200
|
/**
|
|
213
|
-
|
|
201
|
+
ISO timestamp with colons replaced by dashes so it can be embedded in a cross-platform file name.
|
|
214
202
|
*/
|
|
215
203
|
const timestamp = new Date().toISOString()
|
|
216
204
|
.replaceAll(
|
|
217
205
|
':',
|
|
218
206
|
'-',
|
|
219
207
|
);
|
|
220
|
-
|
|
208
|
+
/**
|
|
209
|
+
Log file chosen for this sink instance; kept in a local so the probe below reads a narrowed `string`.
|
|
210
|
+
*/
|
|
211
|
+
const filePath = join(
|
|
221
212
|
LOG_DIR,
|
|
222
213
|
`${timestamp}.log.jsonl`,
|
|
223
214
|
);
|
|
215
|
+
state.filePath = filePath;
|
|
224
216
|
|
|
225
217
|
// Verify by writing and reading test data.
|
|
226
218
|
/**
|
|
227
|
-
|
|
219
|
+
Probe record written and read back to confirm the chosen file path round-trips.
|
|
228
220
|
*/
|
|
229
221
|
const testData = `{"test":true,"timestamp":${Date.now()}}\n`;
|
|
230
|
-
await
|
|
231
|
-
|
|
222
|
+
await appendFile(
|
|
223
|
+
filePath,
|
|
232
224
|
testData,
|
|
233
225
|
);
|
|
234
226
|
/**
|
|
235
|
-
|
|
227
|
+
Probe contents read back; matching the literal `"test":true` proves the append + read path works end-to-end.
|
|
236
228
|
*/
|
|
237
|
-
const content = await
|
|
238
|
-
|
|
229
|
+
const content = await readFile(
|
|
230
|
+
filePath,
|
|
239
231
|
'utf8',
|
|
240
232
|
);
|
|
241
233
|
return content.includes('"test":true',);
|
|
@@ -250,10 +242,10 @@ export function createFileSink(): Sink {
|
|
|
250
242
|
}
|
|
251
243
|
|
|
252
244
|
/**
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
245
|
+
Verifies file system availability via {@link runVerify}, memoizing
|
|
246
|
+
concurrent calls so a second caller never starts a duplicate probe.
|
|
247
|
+
|
|
248
|
+
@returns Whether file system logging is available.
|
|
257
249
|
*/
|
|
258
250
|
function verify(): Promise<boolean> {
|
|
259
251
|
if (state.verifyPromise
|
|
@@ -265,20 +257,23 @@ export function createFileSink(): Sink {
|
|
|
265
257
|
}
|
|
266
258
|
|
|
267
259
|
/**
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
260
|
+
Writes a single record as a JSONL line to the resolved log file.
|
|
261
|
+
|
|
262
|
+
@param record - Log record to write.
|
|
263
|
+
|
|
264
|
+
@mutates record - `JSON.stringify` may invoke `toJSON`, getters, or proxy traps.
|
|
273
265
|
*/
|
|
274
266
|
async function write(record: object,): Promise<void> {
|
|
275
|
-
|
|
276
|
-
|
|
267
|
+
/**
|
|
268
|
+
Log file resolved by verification; unset before (or after a failed) verify, in which case the record is dropped silently.
|
|
269
|
+
*/
|
|
270
|
+
const { filePath, } = state;
|
|
271
|
+
if (filePath === undefined)
|
|
277
272
|
return;
|
|
278
273
|
|
|
279
274
|
try {
|
|
280
|
-
await
|
|
281
|
-
|
|
275
|
+
await appendFile(
|
|
276
|
+
filePath,
|
|
282
277
|
`${JSON.stringify(record,)}\n`,
|
|
283
278
|
);
|
|
284
279
|
}
|
|
@@ -295,3 +290,4 @@ export function createFileSink(): Sink {
|
|
|
295
290
|
write,
|
|
296
291
|
};
|
|
297
292
|
}
|
|
293
|
+
//endregion File sink
|
|
@@ -9,25 +9,18 @@ import {
|
|
|
9
9
|
expect,
|
|
10
10
|
it,
|
|
11
11
|
} from '@monochromatic-dev/module-test/ts';
|
|
12
|
+
import type { LogRecord, } from '@monochromatic-dev/module-logger';
|
|
12
13
|
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
} from '@monochromatic-dev/module-logger';
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Sink factories under test, read from the built artifact's `sinks` namespace.
|
|
19
|
-
*/
|
|
20
|
-
const {
|
|
14
|
+
_findNodeModulesUp as findNodeModulesUp,
|
|
15
|
+
_NO_NODE_MODULES_FOUND as NO_NODE_MODULES_FOUND,
|
|
21
16
|
createFileSink,
|
|
22
|
-
|
|
23
|
-
NO_NODE_MODULES_FOUND,
|
|
24
|
-
} = sinks;
|
|
17
|
+
} from '@monochromatic-dev/module-logger/node';
|
|
25
18
|
|
|
26
19
|
/**
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
20
|
+
Mock `stat` that always throws an ENOENT-like error, so `findNodeModulesUp`
|
|
21
|
+
walks the whole tree and exhausts without matching.
|
|
22
|
+
|
|
23
|
+
@returns Never; always throws.
|
|
31
24
|
*/
|
|
32
25
|
function statAlwaysMissing(): never {
|
|
33
26
|
const error: NodeJS.ErrnoException = Object.assign(
|
|
@@ -38,9 +31,9 @@ function statAlwaysMissing(): never {
|
|
|
38
31
|
}
|
|
39
32
|
|
|
40
33
|
/**
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
34
|
+
Mock `stat` that always throws an unexpected permission error.
|
|
35
|
+
|
|
36
|
+
@returns Never; always throws.
|
|
44
37
|
*/
|
|
45
38
|
function statAlwaysDenied(): never {
|
|
46
39
|
const error: NodeJS.ErrnoException = Object.assign(
|
|
@@ -51,11 +44,11 @@ function statAlwaysDenied(): never {
|
|
|
51
44
|
}
|
|
52
45
|
|
|
53
46
|
/**
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
47
|
+
Builds a LogRecord for write-path tests.
|
|
48
|
+
|
|
49
|
+
@param message - Message body.
|
|
50
|
+
|
|
51
|
+
@returns Record at a fixed timestamp.
|
|
59
52
|
*/
|
|
60
53
|
function record({ message, }: { readonly message: string; },): LogRecord {
|
|
61
54
|
return {
|
|
@@ -168,9 +161,9 @@ await describe({
|
|
|
168
161
|
it({
|
|
169
162
|
name: 'write before verify resolves without touching the filesystem',
|
|
170
163
|
fn: async () => {
|
|
171
|
-
// Verification resolves the log path
|
|
172
|
-
//
|
|
173
|
-
//
|
|
164
|
+
// Verification resolves the log path; without it the path stays
|
|
165
|
+
// unset, so write takes the unset-guard early return, resolving as a
|
|
166
|
+
// silent no-op rather than throwing or writing.
|
|
174
167
|
const sink = createFileSink();
|
|
175
168
|
await expect(
|
|
176
169
|
sink.write(record({ message: 'before verify', },),),
|
package/src/sink/index.ts
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
|
+
// Cross-platform factories only. The file sink lives behind the `./node`
|
|
2
|
+
// subpath (`src/node.ts`) and the IndexedDB and OPFS sinks behind `./browser`
|
|
3
|
+
// (`src/browser.ts`), so the root entry never pulls `node:fs` into the
|
|
4
|
+
// neutral artifact nor browser-only storage code into the Node artifact.
|
|
1
5
|
export { createConsoleSink, } from './console.ts';
|
|
2
|
-
export {
|
|
3
|
-
createFileSink,
|
|
4
|
-
findNodeModulesUp,
|
|
5
|
-
NO_NODE_MODULES_FOUND,
|
|
6
|
-
} from './file.ts';
|
|
7
|
-
export { createIndexedDbSink, } from './indexed-db.ts';
|
|
8
6
|
export { createLocalStorageSink, } from './local-storage.ts';
|
|
9
7
|
export { createNoopSink, } from './noop.ts';
|
|
10
|
-
export { createOpfsSink, } from './opfs.ts';
|
|
11
8
|
export { createSessionStorageSink, } from './session-storage.ts';
|
|
@@ -1,32 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
2
|
+
Promise bridges for the event-based IndexedDB API.
|
|
3
|
+
|
|
4
|
+
IndexedDB predates promises and signals completion only through `success`
|
|
5
|
+
and `error` events, so awaiting it requires wrapping each request and
|
|
6
|
+
transaction in a promise once, here, instead of scattering listener wiring
|
|
7
|
+
through the sink.
|
|
8
|
+
|
|
9
|
+
@module
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
13
|
+
Resolves with an IndexedDB request's result once it succeeds, rejecting
|
|
14
|
+
with the request's error when it fails.
|
|
15
|
+
|
|
16
|
+
@param request - Pending IndexedDB request.
|
|
17
|
+
|
|
18
|
+
@returns Result the request produces.
|
|
19
|
+
|
|
20
|
+
@throws DOMException - Whatever the backend attaches to a failed request.
|
|
21
|
+
|
|
22
|
+
@mutates request - `request.addEventListener` registers success and error
|
|
23
|
+
listeners on the host-owned request, and `resolve` hands `request.result`,
|
|
24
|
+
a host-owned value, out to the awaiting caller.
|
|
25
|
+
|
|
26
|
+
@example
|
|
27
|
+
```ts
|
|
28
|
+
const key = await awaitRequest(store.add(batch));
|
|
29
|
+
```
|
|
30
30
|
*/
|
|
31
31
|
export function awaitRequest<T,>(request: IDBRequest<T>,): Promise<T> {
|
|
32
32
|
// oxlint-disable-next-line promise/avoid-new -- IndexedDB is event-based and exposes no promise API; a constructed promise is the only bridge to await it.
|
|
@@ -50,23 +50,23 @@ export function awaitRequest<T,>(request: IDBRequest<T>,): Promise<T> {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
53
|
+
Resolves once an IndexedDB transaction commits, rejecting when it errors or
|
|
54
|
+
aborts, so a caller can await durability of everything queued on it.
|
|
55
|
+
|
|
56
|
+
@param transaction - Transaction whose settlement is awaited.
|
|
57
|
+
|
|
58
|
+
@throws DOMException - Whatever the backend attaches to a failed or aborted
|
|
59
|
+
transaction.
|
|
60
|
+
|
|
61
|
+
@mutates transaction - `transaction.addEventListener` registers complete,
|
|
62
|
+
error, and abort listeners on the host-owned transaction.
|
|
63
|
+
|
|
64
|
+
@example
|
|
65
|
+
```ts
|
|
66
|
+
const transaction = database.transaction('batch', 'readwrite');
|
|
67
|
+
transaction.objectStore('batch').add(batch);
|
|
68
|
+
await awaitTransaction(transaction);
|
|
69
|
+
```
|
|
70
70
|
*/
|
|
71
71
|
export function awaitTransaction(transaction: IDBTransaction,): Promise<void> {
|
|
72
72
|
// oxlint-disable-next-line promise/avoid-new -- IndexedDB is event-based and exposes no promise API; a constructed promise is the only bridge to await it.
|
|
@@ -7,18 +7,18 @@ import {
|
|
|
7
7
|
|
|
8
8
|
declare global {
|
|
9
9
|
// oxlint-disable-next-line typescript/consistent-type-imports -- typeof import() cannot use import type syntax
|
|
10
|
-
var
|
|
10
|
+
var moduleLoggerBrowser: typeof import('@monochromatic-dev/module-logger/browser');
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
test.describe('IndexedDB sink', () => {
|
|
14
14
|
test.beforeEach(async ({ page, },) => {
|
|
15
15
|
await page.goto('/',);
|
|
16
|
-
await page.waitForFunction(() => globalThis.
|
|
16
|
+
await page.waitForFunction(() => globalThis.moduleLoggerBrowser !== undefined);
|
|
17
17
|
},);
|
|
18
18
|
|
|
19
19
|
test('createIndexedDbSink exposes a callable verify', async ({ page, },) => {
|
|
20
20
|
const typeofVerify = await page.evaluate(() => {
|
|
21
|
-
const { createIndexedDbSink, } = globalThis.
|
|
21
|
+
const { createIndexedDbSink, } = globalThis.moduleLoggerBrowser;
|
|
22
22
|
return typeof createIndexedDbSink().verify;
|
|
23
23
|
},);
|
|
24
24
|
expect(typeofVerify,).toBe('function',);
|
|
@@ -26,7 +26,7 @@ test.describe('IndexedDB sink', () => {
|
|
|
26
26
|
|
|
27
27
|
test('verify detects availability', async ({ page, },) => {
|
|
28
28
|
const result = await page.evaluate(async () => {
|
|
29
|
-
const { createIndexedDbSink, } = globalThis.
|
|
29
|
+
const { createIndexedDbSink, } = globalThis.moduleLoggerBrowser;
|
|
30
30
|
return createIndexedDbSink().verify();
|
|
31
31
|
},);
|
|
32
32
|
expect(result,).toBe(true,);
|
|
@@ -34,7 +34,7 @@ test.describe('IndexedDB sink', () => {
|
|
|
34
34
|
|
|
35
35
|
test('a verified sink writes records across levels and message shapes', async ({ page, },) => {
|
|
36
36
|
const allSucceeded = await page.evaluate(async () => {
|
|
37
|
-
const { createIndexedDbSink, } = globalThis.
|
|
37
|
+
const { createIndexedDbSink, } = globalThis.moduleLoggerBrowser;
|
|
38
38
|
const sink = createIndexedDbSink();
|
|
39
39
|
await sink.verify();
|
|
40
40
|
const levels = ['trace', 'debug', 'info', 'warn', 'error', 'fatal',] as const;
|
|
@@ -67,7 +67,7 @@ test.describe('IndexedDB sink', () => {
|
|
|
67
67
|
|
|
68
68
|
test('a flushed batch is readable back out of the database as JSONL', async ({ page, },) => {
|
|
69
69
|
const result = await page.evaluate(async () => {
|
|
70
|
-
const { createIndexedDbSink, } = globalThis.
|
|
70
|
+
const { createIndexedDbSink, } = globalThis.moduleLoggerBrowser;
|
|
71
71
|
const sink = createIndexedDbSink();
|
|
72
72
|
await sink.verify();
|
|
73
73
|
|
|
@@ -126,7 +126,7 @@ test.describe('IndexedDB sink', () => {
|
|
|
126
126
|
test('retention trims the store back to the cap, oldest first', async ({ page, },) => {
|
|
127
127
|
test.setTimeout(120_000,);
|
|
128
128
|
const result = await page.evaluate(async () => {
|
|
129
|
-
const { createIndexedDbSink, } = globalThis.
|
|
129
|
+
const { createIndexedDbSink, } = globalThis.moduleLoggerBrowser;
|
|
130
130
|
const sink = createIndexedDbSink();
|
|
131
131
|
await sink.verify();
|
|
132
132
|
|