@monochromatic-dev/module-logger 0.1.0 → 0.3.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 +58 -12
- package/dist/final/neutral/browser.d.mts +60 -0
- package/dist/final/neutral/browser.mjs +1 -0
- package/dist/final/neutral/index.d.mts +366 -572
- 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 +366 -572
- 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 -5
- package/src/artifact-platform-split.unit.test.ts +140 -0
- package/src/browser.ts +14 -0
- package/src/create-logger.ts +249 -151
- package/src/create-logger.unit.test.ts +527 -75
- package/src/default-sinks.neutral.ts +34 -0
- package/src/default-sinks.node.ts +32 -0
- package/src/error-format.ts +23 -23
- package/src/index.ts +2 -0
- package/src/logger.ts +23 -50
- package/src/node.ts +23 -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
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
import {
|
|
8
8
|
createLogger,
|
|
9
9
|
DEFAULT_FLUSH_DEADLINE_MS,
|
|
10
|
+
DEFAULT_VERIFY_TIMEOUT_MS,
|
|
11
|
+
STARTUP_BUFFER_CAP,
|
|
10
12
|
type LogRecord,
|
|
11
13
|
type Sink,
|
|
12
14
|
type SinkFlush,
|
|
@@ -14,55 +16,55 @@ import {
|
|
|
14
16
|
} from '@monochromatic-dev/module-logger';
|
|
15
17
|
|
|
16
18
|
/**
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
Milliseconds a slow write parks before recording, long enough that the
|
|
20
|
+
record is provably still pending when a synchronous assertion runs but the
|
|
21
|
+
draining `flush()` must wait for it.
|
|
20
22
|
*/
|
|
21
23
|
const SLOW_WRITE_MS = 25;
|
|
22
24
|
|
|
23
25
|
/**
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
Flush deadline the deadline tests inject: short enough to keep the suite
|
|
27
|
+
fast, long enough that timer granularity cannot fire it early.
|
|
26
28
|
*/
|
|
27
29
|
const SHORT_DEADLINE_MS = 60;
|
|
28
30
|
|
|
29
31
|
/**
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
Timer slack subtracted from the deadline when asserting a flush waited it
|
|
33
|
+
out, covering setTimeout clamping and scheduler jitter.
|
|
32
34
|
*/
|
|
33
35
|
const DEADLINE_TOLERANCE_MS = 15;
|
|
34
36
|
|
|
35
37
|
/**
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
Upper bound on a flush that must not wait out the deadline again; well
|
|
39
|
+
under `SHORT_DEADLINE_MS` so a regression that re-waits is caught.
|
|
38
40
|
*/
|
|
39
41
|
const FAST_FLUSH_MS = 40;
|
|
40
42
|
|
|
41
43
|
/**
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
Harness timeout for the deadline tests: a regression that hangs forever
|
|
45
|
+
fails here instead of stalling the suite.
|
|
44
46
|
*/
|
|
45
47
|
const DEADLINE_TEST_TIMEOUT_MS = 2_000;
|
|
46
48
|
|
|
47
49
|
/**
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
Promise that never settles, standing in for a wedged sink operation.
|
|
51
|
+
|
|
52
|
+
@returns Pending promise whose resolver is unreachable.
|
|
51
53
|
*/
|
|
52
54
|
function neverSettles(): Promise<never> {
|
|
53
55
|
return Promise.withResolvers<never>().promise;
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
/**
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
Times one `flush()` call.
|
|
60
|
+
|
|
61
|
+
@param flush - Flush function to time.
|
|
62
|
+
|
|
63
|
+
@returns Elapsed milliseconds.
|
|
62
64
|
*/
|
|
63
65
|
async function timeFlush({ flush, }: { readonly flush: () => Promise<void>; },): Promise<number> {
|
|
64
66
|
/**
|
|
65
|
-
|
|
67
|
+
Start timestamp.
|
|
66
68
|
*/
|
|
67
69
|
const start = performance.now();
|
|
68
70
|
await flush();
|
|
@@ -70,48 +72,100 @@ async function timeFlush({ flush, }: { readonly flush: () => Promise<void>; },):
|
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
/**
|
|
73
|
-
|
|
74
|
-
|
|
75
|
+
Structural view of a sinon stub: only the recorded calls matter here, and
|
|
76
|
+
naming the shape keeps the test free of a direct sinon type import.
|
|
75
77
|
*/
|
|
76
78
|
type RecordedCalls = {
|
|
77
79
|
readonly getCalls: () => readonly { readonly args: readonly unknown[]; }[];
|
|
78
80
|
};
|
|
79
81
|
|
|
80
82
|
/**
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
83
|
+
Collects the console.warn messages containing `needle`. Sibling tests in
|
|
84
|
+
this file run concurrently and emit their own internal-error reports
|
|
85
|
+
through the same console, so a raw call count would be noise.
|
|
86
|
+
|
|
87
|
+
@param warn - Stubbed console.warn.
|
|
88
|
+
|
|
89
|
+
@param needle - Substring identifying the breadcrumb family.
|
|
90
|
+
|
|
91
|
+
@returns Matching messages, in call order.
|
|
89
92
|
*/
|
|
90
|
-
function
|
|
93
|
+
function breadcrumbMessages(
|
|
94
|
+
{
|
|
95
|
+
warn,
|
|
96
|
+
needle,
|
|
97
|
+
}: {
|
|
98
|
+
readonly warn: RecordedCalls;
|
|
99
|
+
readonly needle: string;
|
|
100
|
+
},
|
|
101
|
+
): string[] {
|
|
91
102
|
return warn.getCalls()
|
|
92
103
|
.map(function toMessage(call,) {
|
|
93
104
|
return String(call.args[0],);
|
|
94
105
|
},)
|
|
95
|
-
.filter(function
|
|
96
|
-
return message.includes(
|
|
106
|
+
.filter(function matchesNeedle(message,) {
|
|
107
|
+
return message.includes(needle,);
|
|
97
108
|
},);
|
|
98
109
|
}
|
|
99
110
|
|
|
100
111
|
/**
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
112
|
+
Collects the flush-deadline breadcrumbs, see {@link breadcrumbMessages}.
|
|
113
|
+
|
|
114
|
+
@param warn - Stubbed console.warn.
|
|
115
|
+
|
|
116
|
+
@returns Flush-deadline breadcrumb messages observed, in call order.
|
|
117
|
+
*/
|
|
118
|
+
function deadlineBreadcrumbMessages({ warn, }: { readonly warn: RecordedCalls; },): string[] {
|
|
119
|
+
return breadcrumbMessages({
|
|
120
|
+
needle: 'flush deadline',
|
|
121
|
+
warn,
|
|
122
|
+
},);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
Counts the sink-verification breadcrumbs (a verify that rejected, threw, or
|
|
127
|
+
ran past the verify time limit).
|
|
128
|
+
|
|
129
|
+
@param warn - Stubbed console.warn.
|
|
130
|
+
|
|
131
|
+
@returns Number of verification breadcrumbs observed.
|
|
132
|
+
*/
|
|
133
|
+
function verifyBreadcrumbs({ warn, }: { readonly warn: RecordedCalls; },): number {
|
|
134
|
+
return breadcrumbMessages({
|
|
135
|
+
needle: 'sink verification failed',
|
|
136
|
+
warn,
|
|
137
|
+
},).length;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
Builds a verifier that answers `true` after a delay.
|
|
142
|
+
|
|
143
|
+
@param delayMs - Milliseconds before the verifier resolves.
|
|
144
|
+
|
|
145
|
+
@returns Verify function resolving `true` after the delay.
|
|
146
|
+
*/
|
|
147
|
+
function verifyTrueAfter({ delayMs, }: { readonly delayMs: number; },): Verify {
|
|
148
|
+
return async function verifyLater(): Promise<boolean> {
|
|
149
|
+
await wait(delayMs,);
|
|
150
|
+
return true;
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
Counts the flush-deadline breadcrumbs, see {@link deadlineBreadcrumbMessages}.
|
|
156
|
+
|
|
157
|
+
@param warn - Stubbed console.warn.
|
|
158
|
+
|
|
159
|
+
@returns Number of flush-deadline breadcrumbs observed.
|
|
106
160
|
*/
|
|
107
161
|
function deadlineBreadcrumbs({ warn, }: { readonly warn: RecordedCalls; },): number {
|
|
108
162
|
return deadlineBreadcrumbMessages({ warn, },).length;
|
|
109
163
|
}
|
|
110
164
|
|
|
111
165
|
/**
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
166
|
+
Builds a verified sink whose every write never settles.
|
|
167
|
+
|
|
168
|
+
@returns Sink standing in for a wedged backend.
|
|
115
169
|
*/
|
|
116
170
|
function wedgedWriteSink(): Sink {
|
|
117
171
|
return {
|
|
@@ -125,8 +179,8 @@ function wedgedWriteSink(): Sink {
|
|
|
125
179
|
}
|
|
126
180
|
|
|
127
181
|
/**
|
|
128
|
-
|
|
129
|
-
|
|
182
|
+
Recording sink plus the array it appends every written record to, so a test
|
|
183
|
+
can assert exactly which records crossed the seam.
|
|
130
184
|
*/
|
|
131
185
|
type RecordingSink = {
|
|
132
186
|
readonly records: LogRecord[];
|
|
@@ -134,18 +188,18 @@ type RecordingSink = {
|
|
|
134
188
|
};
|
|
135
189
|
|
|
136
190
|
/**
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
191
|
+
Builds a fake sink that records every record it receives. The seam under
|
|
192
|
+
test is `Sink`, so the whole orchestration (verify, replay, fan-out, flush)
|
|
193
|
+
is exercised through one self-contained adapter with no globals to reset.
|
|
194
|
+
|
|
195
|
+
@param verify - Backend availability check; defaults to synchronously available.
|
|
196
|
+
|
|
197
|
+
@param flush - Optional flush hook the logger should drain.
|
|
198
|
+
|
|
199
|
+
@param writeDelayMs - Milliseconds each write parks before recording, to
|
|
200
|
+
keep a record pending across a `flush()`.
|
|
201
|
+
|
|
202
|
+
@returns Sink adapter paired with its recorded-record array.
|
|
149
203
|
*/
|
|
150
204
|
function recordingSink(
|
|
151
205
|
{
|
|
@@ -161,14 +215,14 @@ function recordingSink(
|
|
|
161
215
|
} = {},
|
|
162
216
|
): RecordingSink {
|
|
163
217
|
/**
|
|
164
|
-
|
|
218
|
+
Records this sink has received, in arrival order.
|
|
165
219
|
*/
|
|
166
220
|
const records: LogRecord[] = [];
|
|
167
221
|
|
|
168
222
|
/**
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
223
|
+
Records every received record after the optional delay.
|
|
224
|
+
|
|
225
|
+
@param record - Record handed to the sink.
|
|
172
226
|
*/
|
|
173
227
|
async function write(record: LogRecord,): Promise<void> {
|
|
174
228
|
if (writeDelayMs > 0)
|
|
@@ -190,11 +244,11 @@ function recordingSink(
|
|
|
190
244
|
}
|
|
191
245
|
|
|
192
246
|
/**
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
247
|
+
Maps recorded records down to their messages for concise assertions.
|
|
248
|
+
|
|
249
|
+
@param recording - Recording sink whose messages to read.
|
|
250
|
+
|
|
251
|
+
@returns Messages in arrival order.
|
|
198
252
|
*/
|
|
199
253
|
function messages({ recording, }: { readonly recording: RecordingSink; },): string[] {
|
|
200
254
|
return recording.records
|
|
@@ -203,6 +257,100 @@ function messages({ recording, }: { readonly recording: RecordingSink; },): stri
|
|
|
203
257
|
},);
|
|
204
258
|
}
|
|
205
259
|
|
|
260
|
+
/**
|
|
261
|
+
Recording sink whose first write parks on a promise the test settles, so a
|
|
262
|
+
`flush()` can abandon that write at the deadline and the test can then
|
|
263
|
+
settle it late, after the logger has stopped tracking it.
|
|
264
|
+
*/
|
|
265
|
+
type ParkedWriteSink = RecordingSink & {
|
|
266
|
+
readonly settleParked: PromiseWithResolvers<void>;
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
Builds a verified sink whose first write waits on `settleParked` and whose
|
|
271
|
+
later writes record immediately.
|
|
272
|
+
|
|
273
|
+
@returns Sink, its record array, and the resolvers for the parked write.
|
|
274
|
+
*/
|
|
275
|
+
function parkedFirstWriteSink(): ParkedWriteSink {
|
|
276
|
+
/**
|
|
277
|
+
Records this sink has received, in arrival order.
|
|
278
|
+
*/
|
|
279
|
+
const records: LogRecord[] = [];
|
|
280
|
+
/**
|
|
281
|
+
Resolvers the test uses to settle the parked write.
|
|
282
|
+
*/
|
|
283
|
+
const settleParked = Promise.withResolvers<void>();
|
|
284
|
+
/**
|
|
285
|
+
Whether the parked write has been handed out; only the first write parks.
|
|
286
|
+
*/
|
|
287
|
+
const handedOut = { parked: false, };
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
Parks the first write on `settleParked`, records every later one.
|
|
291
|
+
|
|
292
|
+
@param record - Record handed to the sink.
|
|
293
|
+
*/
|
|
294
|
+
async function write(record: LogRecord,): Promise<void> {
|
|
295
|
+
if (!handedOut.parked) {
|
|
296
|
+
handedOut.parked = true;
|
|
297
|
+
await settleParked.promise;
|
|
298
|
+
}
|
|
299
|
+
records.push(record,);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
records,
|
|
304
|
+
settleParked,
|
|
305
|
+
sink: {
|
|
306
|
+
verify: function verifyAvailable(): Promise<boolean> {
|
|
307
|
+
return Promise.resolve(true,);
|
|
308
|
+
},
|
|
309
|
+
write,
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
Unhandled-rejection reasons observed while the capture is alive, plus the
|
|
316
|
+
disposer that detaches the listener.
|
|
317
|
+
*/
|
|
318
|
+
type UnhandledCapture = {
|
|
319
|
+
readonly reasons: readonly unknown[];
|
|
320
|
+
readonly [Symbol.dispose]: () => void;
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
Listens for `unhandledRejection` on the process until disposed. A listener
|
|
325
|
+
also stops Node from treating the rejection as fatal, so the assertion on
|
|
326
|
+
`reasons` is what enforces the property.
|
|
327
|
+
|
|
328
|
+
@returns Capture whose `reasons` grows with every unhandled rejection.
|
|
329
|
+
*/
|
|
330
|
+
function captureUnhandledRejections(): UnhandledCapture {
|
|
331
|
+
/**
|
|
332
|
+
Reasons observed so far.
|
|
333
|
+
*/
|
|
334
|
+
const reasons: unknown[] = [];
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
Listener appended to the process.
|
|
338
|
+
|
|
339
|
+
@param reason - Rejection reason Node reports.
|
|
340
|
+
*/
|
|
341
|
+
function record(reason: unknown,): void {
|
|
342
|
+
reasons.push(reason,);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
process.on('unhandledRejection', record,);
|
|
346
|
+
return {
|
|
347
|
+
reasons,
|
|
348
|
+
[Symbol.dispose]: function detach(): void {
|
|
349
|
+
process.off('unhandledRejection', record,);
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
206
354
|
await describe({
|
|
207
355
|
name: 'createLogger orchestration',
|
|
208
356
|
children: [
|
|
@@ -369,8 +517,8 @@ await describe({
|
|
|
369
517
|
name: 'a rejecting write does not retire the sink',
|
|
370
518
|
fn: async () => {
|
|
371
519
|
/**
|
|
372
|
-
|
|
373
|
-
|
|
520
|
+
Write-attempt counter; a retired sink would stop receiving writes,
|
|
521
|
+
so a second attempt proves the rejection left the backend available.
|
|
374
522
|
*/
|
|
375
523
|
const counters: { attempts: number; } = { attempts: 0, };
|
|
376
524
|
const flaky: Sink = {
|
|
@@ -424,7 +572,7 @@ await describe({
|
|
|
424
572
|
name: 'flush runs every available sink flush hook',
|
|
425
573
|
fn: async () => {
|
|
426
574
|
/**
|
|
427
|
-
|
|
575
|
+
Hook-invocation counter proving `flush()` reached the sink's own hook.
|
|
428
576
|
*/
|
|
429
577
|
const counters: { flushes: number; } = { flushes: 0, };
|
|
430
578
|
const hooked = recordingSink({
|
|
@@ -513,8 +661,8 @@ await describe({
|
|
|
513
661
|
name: 'a synchronously-throwing write does not retire the sink',
|
|
514
662
|
fn: async () => {
|
|
515
663
|
/**
|
|
516
|
-
|
|
517
|
-
|
|
664
|
+
Write-attempt counter; a retired sink would stop receiving writes, so
|
|
665
|
+
a second attempt proves the synchronous throw left it available.
|
|
518
666
|
*/
|
|
519
667
|
const counters: { attempts: number; } = { attempts: 0, };
|
|
520
668
|
const flaky: Sink = {
|
|
@@ -554,8 +702,8 @@ await describe({
|
|
|
554
702
|
name: 'does not run the flush hook of a sink that failed verification',
|
|
555
703
|
fn: async () => {
|
|
556
704
|
/**
|
|
557
|
-
|
|
558
|
-
|
|
705
|
+
Flush-hook counter; stays zero because an unavailable sink's hook
|
|
706
|
+
must be skipped by `flushAll`.
|
|
559
707
|
*/
|
|
560
708
|
const counters: { flushes: number; } = { flushes: 0, };
|
|
561
709
|
const off = recordingSink({
|
|
@@ -595,13 +743,16 @@ await describe({
|
|
|
595
743
|
},
|
|
596
744
|
},),
|
|
597
745
|
|
|
598
|
-
//region
|
|
746
|
+
//region Breadcrumb suites (stub the shared console.warn, so sequential)
|
|
599
747
|
|
|
600
748
|
describe({
|
|
601
|
-
name: '
|
|
602
|
-
// One test at a time: each stubs
|
|
749
|
+
name: 'breadcrumb suites',
|
|
750
|
+
// One test at a time across both nested suites: each stubs console.warn.
|
|
603
751
|
concurrency: 1,
|
|
604
752
|
children: [
|
|
753
|
+
describe({
|
|
754
|
+
name: 'flush deadline',
|
|
755
|
+
children: [
|
|
605
756
|
it({
|
|
606
757
|
name: 'exports a positive default flush deadline',
|
|
607
758
|
fn: async () => {
|
|
@@ -747,6 +898,307 @@ await describe({
|
|
|
747
898
|
],
|
|
748
899
|
},),
|
|
749
900
|
|
|
750
|
-
|
|
901
|
+
describe({
|
|
902
|
+
name: 'verify liveness',
|
|
903
|
+
children: [
|
|
904
|
+
it({
|
|
905
|
+
name: 'exports a positive default verify timeout',
|
|
906
|
+
fn: async () => {
|
|
907
|
+
expect(DEFAULT_VERIFY_TIMEOUT_MS,)
|
|
908
|
+
.toBeGreaterThan(0,);
|
|
909
|
+
},
|
|
910
|
+
},),
|
|
911
|
+
|
|
912
|
+
it({
|
|
913
|
+
name: 'a verify that never settles no longer starves the sinks after it',
|
|
914
|
+
timeout: DEADLINE_TEST_TIMEOUT_MS,
|
|
915
|
+
fn: async ({ sinon, },) => {
|
|
916
|
+
const warn = sinon.stub(
|
|
917
|
+
console,
|
|
918
|
+
'warn',
|
|
919
|
+
);
|
|
920
|
+
const wedged = recordingSink({
|
|
921
|
+
verify: function verifyForever(): Promise<boolean> {
|
|
922
|
+
return neverSettles();
|
|
923
|
+
},
|
|
924
|
+
},);
|
|
925
|
+
const later = recordingSink();
|
|
926
|
+
const {
|
|
927
|
+
logger,
|
|
928
|
+
initPromise,
|
|
929
|
+
} = createLogger({
|
|
930
|
+
sinks: [
|
|
931
|
+
wedged.sink,
|
|
932
|
+
later.sink,
|
|
933
|
+
],
|
|
934
|
+
verifyTimeoutMs: SHORT_DEADLINE_MS,
|
|
935
|
+
},);
|
|
936
|
+
await initPromise;
|
|
937
|
+
logger.info('after init',);
|
|
938
|
+
await logger.flush();
|
|
939
|
+
|
|
940
|
+
expect(messages({ recording: later, },),)
|
|
941
|
+
.toEqual(['after init',],);
|
|
942
|
+
expect(messages({ recording: wedged, },),)
|
|
943
|
+
.toEqual([],);
|
|
944
|
+
expect(verifyBreadcrumbs({ warn, },),)
|
|
945
|
+
.toBe(1,);
|
|
946
|
+
},
|
|
947
|
+
},),
|
|
948
|
+
|
|
949
|
+
it({
|
|
950
|
+
name: 'a verify that answers after the time limit stays unavailable',
|
|
951
|
+
timeout: DEADLINE_TEST_TIMEOUT_MS,
|
|
952
|
+
fn: async ({ sinon, },) => {
|
|
953
|
+
sinon.stub(
|
|
954
|
+
console,
|
|
955
|
+
'warn',
|
|
956
|
+
);
|
|
957
|
+
const slow = recordingSink({
|
|
958
|
+
verify: verifyTrueAfter({ delayMs: SHORT_DEADLINE_MS * 3, },),
|
|
959
|
+
},);
|
|
960
|
+
const {
|
|
961
|
+
logger,
|
|
962
|
+
initPromise,
|
|
963
|
+
} = createLogger({
|
|
964
|
+
sinks: [slow.sink,],
|
|
965
|
+
verifyTimeoutMs: SHORT_DEADLINE_MS,
|
|
966
|
+
},);
|
|
967
|
+
await initPromise;
|
|
968
|
+
// Let the late answer arrive, then log and drain.
|
|
969
|
+
await wait(SHORT_DEADLINE_MS * 4,);
|
|
970
|
+
expect(function logAfterLateAnswer() {
|
|
971
|
+
logger.info('late',);
|
|
972
|
+
},)
|
|
973
|
+
.toThrow('No logging backends available',);
|
|
974
|
+
await logger.flush();
|
|
975
|
+
|
|
976
|
+
expect(messages({ recording: slow, },),)
|
|
977
|
+
.toEqual([],);
|
|
978
|
+
},
|
|
979
|
+
},),
|
|
980
|
+
|
|
981
|
+
it({
|
|
982
|
+
name: 'sinks verify concurrently rather than one after another',
|
|
983
|
+
timeout: DEADLINE_TEST_TIMEOUT_MS,
|
|
984
|
+
fn: async () => {
|
|
985
|
+
const first = recordingSink({ verify: verifyTrueAfter({ delayMs: SLOW_WRITE_MS, },), },);
|
|
986
|
+
const second = recordingSink({ verify: verifyTrueAfter({ delayMs: SLOW_WRITE_MS, },), },);
|
|
987
|
+
const started = performance.now();
|
|
988
|
+
const { initPromise, } = createLogger({
|
|
989
|
+
sinks: [
|
|
990
|
+
first.sink,
|
|
991
|
+
second.sink,
|
|
992
|
+
],
|
|
993
|
+
},);
|
|
994
|
+
await initPromise;
|
|
995
|
+
const elapsed = performance.now() - started;
|
|
996
|
+
|
|
997
|
+
// Sequential verification would take at least twice the delay.
|
|
998
|
+
expect(elapsed,)
|
|
999
|
+
.toBeLessThan(SLOW_WRITE_MS * 2,);
|
|
1000
|
+
},
|
|
1001
|
+
},),
|
|
1002
|
+
|
|
1003
|
+
it({
|
|
1004
|
+
name: 'a record logged while sinks verify at different speeds reaches each exactly once',
|
|
1005
|
+
timeout: DEADLINE_TEST_TIMEOUT_MS,
|
|
1006
|
+
fn: async () => {
|
|
1007
|
+
const quick = recordingSink({ verify: verifyTrueAfter({ delayMs: 1, },), },);
|
|
1008
|
+
const slow = recordingSink({ verify: verifyTrueAfter({ delayMs: SLOW_WRITE_MS, },), },);
|
|
1009
|
+
const {
|
|
1010
|
+
logger,
|
|
1011
|
+
initPromise,
|
|
1012
|
+
} = createLogger({
|
|
1013
|
+
sinks: [
|
|
1014
|
+
quick.sink,
|
|
1015
|
+
slow.sink,
|
|
1016
|
+
],
|
|
1017
|
+
},);
|
|
1018
|
+
logger.info('early',);
|
|
1019
|
+
await initPromise;
|
|
1020
|
+
logger.info('late',);
|
|
1021
|
+
await logger.flush();
|
|
1022
|
+
|
|
1023
|
+
expect(messages({ recording: quick, },),)
|
|
1024
|
+
.toEqual([
|
|
1025
|
+
'early',
|
|
1026
|
+
'late',
|
|
1027
|
+
],);
|
|
1028
|
+
expect(messages({ recording: slow, },),)
|
|
1029
|
+
.toEqual([
|
|
1030
|
+
'early',
|
|
1031
|
+
'late',
|
|
1032
|
+
],);
|
|
1033
|
+
},
|
|
1034
|
+
},),
|
|
1035
|
+
],
|
|
1036
|
+
},),
|
|
1037
|
+
|
|
1038
|
+
describe({
|
|
1039
|
+
name: 'abandoned writes',
|
|
1040
|
+
children: [
|
|
1041
|
+
it({
|
|
1042
|
+
name: 'a write rejecting after the deadline abandoned it is reported once, never left unhandled, and does not retire the sink',
|
|
1043
|
+
timeout: DEADLINE_TEST_TIMEOUT_MS,
|
|
1044
|
+
fn: async ({ sinon, },) => {
|
|
1045
|
+
const warn = sinon.stub(
|
|
1046
|
+
console,
|
|
1047
|
+
'warn',
|
|
1048
|
+
);
|
|
1049
|
+
using unhandled = captureUnhandledRejections();
|
|
1050
|
+
const parked = parkedFirstWriteSink();
|
|
1051
|
+
const {
|
|
1052
|
+
logger,
|
|
1053
|
+
initPromise,
|
|
1054
|
+
} = createLogger({
|
|
1055
|
+
flushDeadlineMs: SHORT_DEADLINE_MS,
|
|
1056
|
+
sinks: [parked.sink,],
|
|
1057
|
+
},);
|
|
1058
|
+
await initPromise;
|
|
1059
|
+
logger.info('parked',);
|
|
1060
|
+
await logger.flush();
|
|
1061
|
+
expect(deadlineBreadcrumbs({ warn, },),)
|
|
1062
|
+
.toBe(1,);
|
|
1063
|
+
|
|
1064
|
+
parked.settleParked.reject(new Error('late failure',),);
|
|
1065
|
+
await wait(1,);
|
|
1066
|
+
logger.info('after',);
|
|
1067
|
+
|
|
1068
|
+
const elapsed = await timeFlush({ flush: logger.flush, },);
|
|
1069
|
+
expect(elapsed,)
|
|
1070
|
+
.toBeLessThan(FAST_FLUSH_MS,);
|
|
1071
|
+
expect(messages({ recording: parked, },),)
|
|
1072
|
+
.toEqual(['after',],);
|
|
1073
|
+
expect(unhandled.reasons,)
|
|
1074
|
+
.toHaveLength(0,);
|
|
1075
|
+
expect(
|
|
1076
|
+
breadcrumbMessages({
|
|
1077
|
+
warn,
|
|
1078
|
+
needle: 'sink write promise rejected while being tracked',
|
|
1079
|
+
},),
|
|
1080
|
+
)
|
|
1081
|
+
.toHaveLength(1,);
|
|
1082
|
+
expect(deadlineBreadcrumbs({ warn, },),)
|
|
1083
|
+
.toBe(1,);
|
|
1084
|
+
},
|
|
1085
|
+
},),
|
|
1086
|
+
|
|
1087
|
+
it({
|
|
1088
|
+
name: 'a write resolving after the deadline abandoned it still lands and the sink keeps working',
|
|
1089
|
+
timeout: DEADLINE_TEST_TIMEOUT_MS,
|
|
1090
|
+
fn: async ({ sinon, },) => {
|
|
1091
|
+
const warn = sinon.stub(
|
|
1092
|
+
console,
|
|
1093
|
+
'warn',
|
|
1094
|
+
);
|
|
1095
|
+
using unhandled = captureUnhandledRejections();
|
|
1096
|
+
const parked = parkedFirstWriteSink();
|
|
1097
|
+
const {
|
|
1098
|
+
logger,
|
|
1099
|
+
initPromise,
|
|
1100
|
+
} = createLogger({
|
|
1101
|
+
flushDeadlineMs: SHORT_DEADLINE_MS,
|
|
1102
|
+
sinks: [parked.sink,],
|
|
1103
|
+
},);
|
|
1104
|
+
await initPromise;
|
|
1105
|
+
logger.info('parked',);
|
|
1106
|
+
await logger.flush();
|
|
1107
|
+
|
|
1108
|
+
parked.settleParked.resolve();
|
|
1109
|
+
await wait(1,);
|
|
1110
|
+
logger.info('after',);
|
|
1111
|
+
await logger.flush();
|
|
1112
|
+
|
|
1113
|
+
expect(messages({ recording: parked, },),)
|
|
1114
|
+
.toEqual([
|
|
1115
|
+
'parked',
|
|
1116
|
+
'after',
|
|
1117
|
+
],);
|
|
1118
|
+
expect(unhandled.reasons,)
|
|
1119
|
+
.toHaveLength(0,);
|
|
1120
|
+
expect(
|
|
1121
|
+
breadcrumbMessages({
|
|
1122
|
+
warn,
|
|
1123
|
+
needle: 'sink write promise rejected',
|
|
1124
|
+
},),
|
|
1125
|
+
)
|
|
1126
|
+
.toHaveLength(0,);
|
|
1127
|
+
expect(deadlineBreadcrumbs({ warn, },),)
|
|
1128
|
+
.toBe(1,);
|
|
1129
|
+
},
|
|
1130
|
+
},),
|
|
1131
|
+
],
|
|
1132
|
+
},),
|
|
1133
|
+
],
|
|
1134
|
+
},),
|
|
1135
|
+
|
|
1136
|
+
//endregion Breadcrumb suites
|
|
1137
|
+
|
|
1138
|
+
//region Startup buffer bound
|
|
1139
|
+
|
|
1140
|
+
it({
|
|
1141
|
+
name: 'exports a positive startup buffer cap',
|
|
1142
|
+
fn: async () => {
|
|
1143
|
+
expect(STARTUP_BUFFER_CAP,)
|
|
1144
|
+
.toBeGreaterThan(0,);
|
|
1145
|
+
},
|
|
1146
|
+
},),
|
|
1147
|
+
|
|
1148
|
+
it({
|
|
1149
|
+
name: 'a startup burst beyond the cap keeps the newest records and reports the loss once',
|
|
1150
|
+
timeout: DEADLINE_TEST_TIMEOUT_MS,
|
|
1151
|
+
fn: async () => {
|
|
1152
|
+
/**
|
|
1153
|
+
Records logged before the sink verifies: the cap plus a few extra
|
|
1154
|
+
that must push the oldest ones out.
|
|
1155
|
+
*/
|
|
1156
|
+
const extra = 3;
|
|
1157
|
+
/**
|
|
1158
|
+
Total records in the burst; the last one is index `burstSize - 1`.
|
|
1159
|
+
*/
|
|
1160
|
+
const burstSize = STARTUP_BUFFER_CAP + extra;
|
|
1161
|
+
const late = recordingSink({ verify: verifyTrueAfter({ delayMs: SLOW_WRITE_MS, },), },);
|
|
1162
|
+
const {
|
|
1163
|
+
logger,
|
|
1164
|
+
initPromise,
|
|
1165
|
+
} = createLogger({ sinks: [late.sink,], },);
|
|
1166
|
+
for (let index = 0; index < burstSize; index += 1)
|
|
1167
|
+
logger.info(`burst ${index}`,);
|
|
1168
|
+
await initPromise;
|
|
1169
|
+
await logger.flush();
|
|
1170
|
+
|
|
1171
|
+
const received = messages({ recording: late, },);
|
|
1172
|
+
expect(received,)
|
|
1173
|
+
.toHaveLength(STARTUP_BUFFER_CAP + 1,);
|
|
1174
|
+
expect(received[0],)
|
|
1175
|
+
.toBe(`burst ${extra}`,);
|
|
1176
|
+
expect(received[STARTUP_BUFFER_CAP - 1],)
|
|
1177
|
+
.toBe(`burst ${burstSize - 1}`,);
|
|
1178
|
+
expect(received[STARTUP_BUFFER_CAP],)
|
|
1179
|
+
.toBe(`${extra} startup records dropped before a backend verified (buffer cap ${STARTUP_BUFFER_CAP})`,);
|
|
1180
|
+
expect(late.records[STARTUP_BUFFER_CAP]?.level,)
|
|
1181
|
+
.toBe('warn',);
|
|
1182
|
+
},
|
|
1183
|
+
},),
|
|
1184
|
+
|
|
1185
|
+
it({
|
|
1186
|
+
name: 'no marker record is written when the startup buffer never overflowed',
|
|
1187
|
+
fn: async () => {
|
|
1188
|
+
const late = recordingSink({ verify: verifyTrueAfter({ delayMs: 1, },), },);
|
|
1189
|
+
const {
|
|
1190
|
+
logger,
|
|
1191
|
+
initPromise,
|
|
1192
|
+
} = createLogger({ sinks: [late.sink,], },);
|
|
1193
|
+
logger.info('one',);
|
|
1194
|
+
await initPromise;
|
|
1195
|
+
await logger.flush();
|
|
1196
|
+
|
|
1197
|
+
expect(messages({ recording: late, },),)
|
|
1198
|
+
.toEqual(['one',],);
|
|
1199
|
+
},
|
|
1200
|
+
},),
|
|
1201
|
+
|
|
1202
|
+
//endregion Startup buffer bound
|
|
751
1203
|
],
|
|
752
1204
|
},);
|