@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,752 @@
1
+ import { wait, } from '@monochromatic-dev/module-async-time/ts';
2
+ import {
3
+ describe,
4
+ expect,
5
+ it,
6
+ } from '@monochromatic-dev/module-test/ts';
7
+ import {
8
+ createLogger,
9
+ DEFAULT_FLUSH_DEADLINE_MS,
10
+ type LogRecord,
11
+ type Sink,
12
+ type SinkFlush,
13
+ type Verify,
14
+ } from '@monochromatic-dev/module-logger';
15
+
16
+ /**
17
+ * Milliseconds a slow write parks before recording, long enough that the
18
+ * record is provably still pending when a synchronous assertion runs but the
19
+ * draining `flush()` must wait for it.
20
+ */
21
+ const SLOW_WRITE_MS = 25;
22
+
23
+ /**
24
+ * Flush deadline the deadline tests inject: short enough to keep the suite
25
+ * fast, long enough that timer granularity cannot fire it early.
26
+ */
27
+ const SHORT_DEADLINE_MS = 60;
28
+
29
+ /**
30
+ * Timer slack subtracted from the deadline when asserting a flush waited it
31
+ * out, covering setTimeout clamping and scheduler jitter.
32
+ */
33
+ const DEADLINE_TOLERANCE_MS = 15;
34
+
35
+ /**
36
+ * Upper bound on a flush that must not wait out the deadline again; well
37
+ * under `SHORT_DEADLINE_MS` so a regression that re-waits is caught.
38
+ */
39
+ const FAST_FLUSH_MS = 40;
40
+
41
+ /**
42
+ * Harness timeout for the deadline tests: a regression that hangs forever
43
+ * fails here instead of stalling the suite.
44
+ */
45
+ const DEADLINE_TEST_TIMEOUT_MS = 2_000;
46
+
47
+ /**
48
+ * Promise that never settles, standing in for a wedged sink operation.
49
+ *
50
+ * @returns Pending promise whose resolver is unreachable.
51
+ */
52
+ function neverSettles(): Promise<never> {
53
+ return Promise.withResolvers<never>().promise;
54
+ }
55
+
56
+ /**
57
+ * Times one `flush()` call.
58
+ *
59
+ * @param flush - Flush function to time.
60
+ *
61
+ * @returns Elapsed milliseconds.
62
+ */
63
+ async function timeFlush({ flush, }: { readonly flush: () => Promise<void>; },): Promise<number> {
64
+ /**
65
+ * Start timestamp.
66
+ */
67
+ const start = performance.now();
68
+ await flush();
69
+ return performance.now() - start;
70
+ }
71
+
72
+ /**
73
+ * Structural view of a sinon stub: only the recorded calls matter here, and
74
+ * naming the shape keeps the test free of a direct sinon type import.
75
+ */
76
+ type RecordedCalls = {
77
+ readonly getCalls: () => readonly { readonly args: readonly unknown[]; }[];
78
+ };
79
+
80
+ /**
81
+ * Collects the console.warn messages carrying the flush-deadline breadcrumb.
82
+ * Sibling tests in this file run concurrently and emit their own
83
+ * internal-error reports through the same console, so a raw call count
84
+ * would be noise.
85
+ *
86
+ * @param warn - Stubbed console.warn.
87
+ *
88
+ * @returns Flush-deadline breadcrumb messages observed, in call order.
89
+ */
90
+ function deadlineBreadcrumbMessages({ warn, }: { readonly warn: RecordedCalls; },): string[] {
91
+ return warn.getCalls()
92
+ .map(function toMessage(call,) {
93
+ return String(call.args[0],);
94
+ },)
95
+ .filter(function isDeadlineBreadcrumb(message,) {
96
+ return message.includes('flush deadline',);
97
+ },);
98
+ }
99
+
100
+ /**
101
+ * Counts the flush-deadline breadcrumbs, see {@link deadlineBreadcrumbMessages}.
102
+ *
103
+ * @param warn - Stubbed console.warn.
104
+ *
105
+ * @returns Number of flush-deadline breadcrumbs observed.
106
+ */
107
+ function deadlineBreadcrumbs({ warn, }: { readonly warn: RecordedCalls; },): number {
108
+ return deadlineBreadcrumbMessages({ warn, },).length;
109
+ }
110
+
111
+ /**
112
+ * Builds a verified sink whose every write never settles.
113
+ *
114
+ * @returns Sink standing in for a wedged backend.
115
+ */
116
+ function wedgedWriteSink(): Sink {
117
+ return {
118
+ verify: function verifyAvailable(): Promise<boolean> {
119
+ return Promise.resolve(true,);
120
+ },
121
+ write: function writeForever(): Promise<void> {
122
+ return neverSettles();
123
+ },
124
+ };
125
+ }
126
+
127
+ /**
128
+ * Recording sink plus the array it appends every written record to, so a test
129
+ * can assert exactly which records crossed the seam.
130
+ */
131
+ type RecordingSink = {
132
+ readonly records: LogRecord[];
133
+ readonly sink: Sink;
134
+ };
135
+
136
+ /**
137
+ * Builds a fake sink that records every record it receives. The seam under
138
+ * test is `Sink`, so the whole orchestration (verify, replay, fan-out, flush)
139
+ * is exercised through one self-contained adapter with no globals to reset.
140
+ *
141
+ * @param verify - Backend availability check; defaults to synchronously available.
142
+ *
143
+ * @param flush - Optional flush hook the logger should drain.
144
+ *
145
+ * @param writeDelayMs - Milliseconds each write parks before recording, to
146
+ * keep a record pending across a `flush()`.
147
+ *
148
+ * @returns Sink adapter paired with its recorded-record array.
149
+ */
150
+ function recordingSink(
151
+ {
152
+ verify = function verifyAvailable(): Promise<boolean> {
153
+ return Promise.resolve(true,);
154
+ },
155
+ flush,
156
+ writeDelayMs = 0,
157
+ }: {
158
+ readonly flush?: SinkFlush;
159
+ readonly verify?: Verify;
160
+ readonly writeDelayMs?: number;
161
+ } = {},
162
+ ): RecordingSink {
163
+ /**
164
+ * Records this sink has received, in arrival order.
165
+ */
166
+ const records: LogRecord[] = [];
167
+
168
+ /**
169
+ * Records every received record after the optional delay.
170
+ *
171
+ * @param record - Record handed to the sink.
172
+ */
173
+ async function write(record: LogRecord,): Promise<void> {
174
+ if (writeDelayMs > 0)
175
+ await wait(writeDelayMs,);
176
+ records.push(record,);
177
+ }
178
+
179
+ // `flush` is spread in only when present: under `exactOptionalPropertyTypes`
180
+ // an optional property cannot be assigned an explicit `undefined`.
181
+ const sink: Sink = {
182
+ ...((flush === undefined) ? {} : { flush, }),
183
+ verify,
184
+ write,
185
+ };
186
+ return {
187
+ records,
188
+ sink,
189
+ };
190
+ }
191
+
192
+ /**
193
+ * Maps recorded records down to their messages for concise assertions.
194
+ *
195
+ * @param recording - Recording sink whose messages to read.
196
+ *
197
+ * @returns Messages in arrival order.
198
+ */
199
+ function messages({ recording, }: { readonly recording: RecordingSink; },): string[] {
200
+ return recording.records
201
+ .map(function toMessage(record,) {
202
+ return record.message;
203
+ },);
204
+ }
205
+
206
+ await describe({
207
+ name: 'createLogger orchestration',
208
+ children: [
209
+ it({
210
+ name: 'fans each record out to every available sink',
211
+ fn: async () => {
212
+ const a = recordingSink();
213
+ const b = recordingSink();
214
+ const {
215
+ logger,
216
+ initPromise,
217
+ } = createLogger({ sinks: [a.sink, b.sink,], },);
218
+ await initPromise;
219
+
220
+ logger.info('hello',);
221
+ await logger.flush();
222
+
223
+ expect(messages({ recording: a, },),)
224
+ .toEqual(['hello',],);
225
+ expect(messages({ recording: b, },),)
226
+ .toEqual(['hello',],);
227
+ },
228
+ },),
229
+
230
+ it({
231
+ name: 'buffers a pre-verify record and replays it to a late-verifying sink exactly once',
232
+ fn: async () => {
233
+ const late = recordingSink({
234
+ verify: function verifyLater(): Promise<boolean> {
235
+ // Resolves on a microtask, after the synchronous log call, so the
236
+ // record must buffer and replay rather than write immediately.
237
+ return Promise.resolve(true,);
238
+ },
239
+ },);
240
+ const {
241
+ logger,
242
+ initPromise,
243
+ } = createLogger({ sinks: [late.sink,], },);
244
+
245
+ // Logged synchronously, before the async verify resolves: the record
246
+ // must buffer rather than drop, then replay once on verify.
247
+ logger.info('early',);
248
+ expect(late.records,)
249
+ .toEqual([],);
250
+
251
+ await initPromise;
252
+ await logger.flush();
253
+ expect(messages({ recording: late, },),)
254
+ .toEqual(['early',],);
255
+ },
256
+ },),
257
+
258
+ it({
259
+ name: 'delivers a startup record once to both an immediately-available and a late sink',
260
+ fn: async () => {
261
+ const eager = recordingSink();
262
+ const late = recordingSink({
263
+ verify: function verifyLater(): Promise<boolean> {
264
+ // Resolves on a microtask, after the synchronous log call, so the
265
+ // record must buffer and replay rather than write immediately.
266
+ return Promise.resolve(true,);
267
+ },
268
+ },);
269
+ const {
270
+ logger,
271
+ initPromise,
272
+ } = createLogger({ sinks: [eager.sink, late.sink,], },);
273
+
274
+ logger.info('boot',);
275
+ await initPromise;
276
+ await logger.flush();
277
+
278
+ // Exactly once each: the eager sink via the immediate write, the late
279
+ // sink via replay. No double-delivery to the eager sink.
280
+ expect(messages({ recording: eager, },),)
281
+ .toEqual(['boot',],);
282
+ expect(messages({ recording: late, },),)
283
+ .toEqual(['boot',],);
284
+ },
285
+ },),
286
+
287
+ it({
288
+ name: 'drops a sink whose verify resolves false',
289
+ fn: async () => {
290
+ const off = recordingSink({
291
+ verify: function verifyUnavailable(): Promise<boolean> {
292
+ return Promise.resolve(false,);
293
+ },
294
+ },);
295
+ const on = recordingSink();
296
+ const {
297
+ logger,
298
+ initPromise,
299
+ } = createLogger({ sinks: [off.sink, on.sink,], },);
300
+ await initPromise;
301
+
302
+ logger.info('x',);
303
+ await logger.flush();
304
+ expect(off.records,)
305
+ .toEqual([],);
306
+ expect(messages({ recording: on, },),)
307
+ .toEqual(['x',],);
308
+ },
309
+ },),
310
+
311
+ it({
312
+ name: 'drops a sink whose verify throws or rejects',
313
+ fn: async () => {
314
+ const thrower = recordingSink({
315
+ verify: function verifyThrows(): Promise<boolean> {
316
+ // Throws synchronously, before returning a promise; the logger's
317
+ // try around `await verify()` still catches it.
318
+ throw new Error('sync verify failed',);
319
+ },
320
+ },);
321
+ const rejecter = recordingSink({
322
+ verify: async function verifyRejects(): Promise<boolean> {
323
+ // Rejects after a microtask; awaiting it in the logger rejects and
324
+ // is caught.
325
+ await Promise.resolve();
326
+ throw new Error('async verify failed',);
327
+ },
328
+ },);
329
+ const on = recordingSink();
330
+ const {
331
+ logger,
332
+ initPromise,
333
+ } = createLogger({ sinks: [thrower.sink, rejecter.sink, on.sink,], },);
334
+ await initPromise;
335
+
336
+ logger.info('x',);
337
+ await logger.flush();
338
+ expect(thrower.records,)
339
+ .toEqual([],);
340
+ expect(rejecter.records,)
341
+ .toEqual([],);
342
+ expect(messages({ recording: on, },),)
343
+ .toEqual(['x',],);
344
+ },
345
+ },),
346
+
347
+ it({
348
+ name: 'throws once initialized with no available backend',
349
+ fn: async () => {
350
+ const off = recordingSink({
351
+ verify: function verifyUnavailable(): Promise<boolean> {
352
+ return Promise.resolve(false,);
353
+ },
354
+ },);
355
+ const {
356
+ logger,
357
+ initPromise,
358
+ } = createLogger({ sinks: [off.sink,], },);
359
+ await initPromise;
360
+
361
+ expect(function logWithNoBackend() {
362
+ logger.info('x',);
363
+ },)
364
+ .toThrow('No logging backends available',);
365
+ },
366
+ },),
367
+
368
+ it({
369
+ name: 'a rejecting write does not retire the sink',
370
+ fn: async () => {
371
+ /**
372
+ * Write-attempt counter; a retired sink would stop receiving writes,
373
+ * so a second attempt proves the rejection left the backend available.
374
+ */
375
+ const counters: { attempts: number; } = { attempts: 0, };
376
+ const flaky: Sink = {
377
+ verify: function verifyAvailable(): Promise<boolean> {
378
+ return Promise.resolve(true,);
379
+ },
380
+ write: async function write(): Promise<void> {
381
+ counters.attempts++;
382
+ throw new Error('transient write failure',);
383
+ },
384
+ };
385
+ const {
386
+ logger,
387
+ initPromise,
388
+ } = createLogger({ sinks: [flaky,], },);
389
+ await initPromise;
390
+
391
+ logger.info('one',);
392
+ await logger.flush();
393
+ // Still available, so this neither throws nor is skipped.
394
+ logger.info('two',);
395
+ await logger.flush();
396
+
397
+ expect(counters.attempts,)
398
+ .toBe(2,);
399
+ },
400
+ },),
401
+
402
+ it({
403
+ name: 'flush drains a still-pending write before resolving',
404
+ fn: async () => {
405
+ const slow = recordingSink({ writeDelayMs: SLOW_WRITE_MS, },);
406
+ const {
407
+ logger,
408
+ initPromise,
409
+ } = createLogger({ sinks: [slow.sink,], },);
410
+ await initPromise;
411
+
412
+ logger.info('drains',);
413
+ // The slow write is still parked, so nothing has been recorded yet.
414
+ expect(slow.records,)
415
+ .toEqual([],);
416
+
417
+ await logger.flush();
418
+ expect(messages({ recording: slow, },),)
419
+ .toEqual(['drains',],);
420
+ },
421
+ },),
422
+
423
+ it({
424
+ name: 'flush runs every available sink flush hook',
425
+ fn: async () => {
426
+ /**
427
+ * Hook-invocation counter proving `flush()` reached the sink's own hook.
428
+ */
429
+ const counters: { flushes: number; } = { flushes: 0, };
430
+ const hooked = recordingSink({
431
+ flush: async function flushHook(): Promise<void> {
432
+ counters.flushes++;
433
+ },
434
+ },);
435
+ const {
436
+ logger,
437
+ initPromise,
438
+ } = createLogger({ sinks: [hooked.sink,], },);
439
+ await initPromise;
440
+
441
+ await logger.flush();
442
+ expect(counters.flushes,)
443
+ .toBe(1,);
444
+ },
445
+ },),
446
+
447
+ it({
448
+ name: 'a rejecting flush hook disables that sink without failing the aggregate flush',
449
+ fn: async () => {
450
+ const bad = recordingSink({
451
+ flush: async function flushHook(): Promise<void> {
452
+ throw new Error('flush hook failed',);
453
+ },
454
+ },);
455
+ const good = recordingSink();
456
+ const {
457
+ logger,
458
+ initPromise,
459
+ } = createLogger({ sinks: [bad.sink, good.sink,], },);
460
+ await initPromise;
461
+
462
+ await expect(logger.flush(),)
463
+ .resolves
464
+ .toBeUndefined();
465
+
466
+ // The rejecting hook retired its sink; subsequent records skip it.
467
+ logger.info('after',);
468
+ await logger.flush();
469
+ expect(bad.records,)
470
+ .toEqual([],);
471
+ expect(messages({ recording: good, },),)
472
+ .toEqual(['after',],);
473
+ },
474
+ },),
475
+
476
+ it({
477
+ name: 'writes a mid-init record immediately to an already-available sink and replays it once to a still-verifying sink',
478
+ fn: async () => {
479
+ const eager = recordingSink();
480
+ const late = recordingSink({
481
+ verify: async function verifyLate(): Promise<boolean> {
482
+ // Still parked when the record is logged, so the eager sink (whose
483
+ // microtask verify already resolved) takes the immediate write
484
+ // while this sink only receives the record via replay on verify.
485
+ await wait(SLOW_WRITE_MS,);
486
+ return true;
487
+ },
488
+ },);
489
+ const {
490
+ logger,
491
+ initPromise,
492
+ } = createLogger({ sinks: [eager.sink, late.sink,], },);
493
+
494
+ // Halfway through the late sink's verify: eager has flipped available,
495
+ // late has not, and `initialize()` has not yet completed.
496
+ await wait(SLOW_WRITE_MS / 2,);
497
+ logger.info('mid',);
498
+
499
+ await initPromise;
500
+ await logger.flush();
501
+
502
+ // Eager via the immediate mid-init write, late via replay: each exactly
503
+ // once. A regression that replayed startup records to already-available
504
+ // sinks would make `eager` ['mid', 'mid'].
505
+ expect(messages({ recording: eager, },),)
506
+ .toEqual(['mid',],);
507
+ expect(messages({ recording: late, },),)
508
+ .toEqual(['mid',],);
509
+ },
510
+ },),
511
+
512
+ it({
513
+ name: 'a synchronously-throwing write does not retire the sink',
514
+ fn: async () => {
515
+ /**
516
+ * Write-attempt counter; a retired sink would stop receiving writes, so
517
+ * a second attempt proves the synchronous throw left it available.
518
+ */
519
+ const counters: { attempts: number; } = { attempts: 0, };
520
+ const flaky: Sink = {
521
+ verify: function verifyAvailable(): Promise<boolean> {
522
+ return Promise.resolve(true,);
523
+ },
524
+ write: function write(): Promise<void> {
525
+ counters.attempts++;
526
+ // Throws synchronously, before returning a promise; the logger's
527
+ // try around the `write()` call swallows it without retiring the
528
+ // sink (distinct from a rejected promise, handled by `trackWrite`).
529
+ throw new Error('synchronous write failure',);
530
+ },
531
+ };
532
+ const on = recordingSink();
533
+ const {
534
+ logger,
535
+ initPromise,
536
+ } = createLogger({ sinks: [flaky, on.sink,], },);
537
+ await initPromise;
538
+
539
+ logger.info('one',);
540
+ await logger.flush();
541
+ // Still available, so this neither throws nor is skipped.
542
+ logger.info('two',);
543
+ await logger.flush();
544
+
545
+ expect(counters.attempts,)
546
+ .toBe(2,);
547
+ // The healthy sibling keeps receiving every record.
548
+ expect(messages({ recording: on, },),)
549
+ .toEqual(['one', 'two',],);
550
+ },
551
+ },),
552
+
553
+ it({
554
+ name: 'does not run the flush hook of a sink that failed verification',
555
+ fn: async () => {
556
+ /**
557
+ * Flush-hook counter; stays zero because an unavailable sink's hook
558
+ * must be skipped by `flushAll`.
559
+ */
560
+ const counters: { flushes: number; } = { flushes: 0, };
561
+ const off = recordingSink({
562
+ verify: function verifyUnavailable(): Promise<boolean> {
563
+ return Promise.resolve(false,);
564
+ },
565
+ flush: async function flushHook(): Promise<void> {
566
+ counters.flushes++;
567
+ },
568
+ },);
569
+ const on = recordingSink();
570
+ const {
571
+ logger,
572
+ initPromise,
573
+ } = createLogger({ sinks: [off.sink, on.sink,], },);
574
+ await initPromise;
575
+
576
+ await logger.flush();
577
+ expect(counters.flushes,)
578
+ .toBe(0,);
579
+ },
580
+ },),
581
+
582
+ it({
583
+ name: 'throws once initialized with an empty sink list',
584
+ fn: async () => {
585
+ const {
586
+ logger,
587
+ initPromise,
588
+ } = createLogger({ sinks: [], },);
589
+ await initPromise;
590
+
591
+ expect(function logWithNoSinks() {
592
+ logger.info('x',);
593
+ },)
594
+ .toThrow('No logging backends available',);
595
+ },
596
+ },),
597
+
598
+ //region Flush deadline
599
+
600
+ describe({
601
+ name: 'flush deadline',
602
+ // One test at a time: each stubs the shared console.warn.
603
+ concurrency: 1,
604
+ children: [
605
+ it({
606
+ name: 'exports a positive default flush deadline',
607
+ fn: async () => {
608
+ expect(DEFAULT_FLUSH_DEADLINE_MS,)
609
+ .toBeGreaterThan(0,);
610
+ },
611
+ },),
612
+
613
+ it({
614
+ name: 'flush resolves once the deadline elapses when a write never settles',
615
+ timeout: DEADLINE_TEST_TIMEOUT_MS,
616
+ fn: async ({ sinon, },) => {
617
+ const warn = sinon.stub(
618
+ console,
619
+ 'warn',
620
+ );
621
+ const {
622
+ logger,
623
+ initPromise,
624
+ } = createLogger({
625
+ flushDeadlineMs: SHORT_DEADLINE_MS,
626
+ sinks: [wedgedWriteSink(),],
627
+ },);
628
+ await initPromise;
629
+ logger.info('stuck',);
630
+
631
+ const elapsed = await timeFlush({ flush: logger.flush, },);
632
+ expect(elapsed,)
633
+ .toBeGreaterThanOrEqual(SHORT_DEADLINE_MS - DEADLINE_TOLERANCE_MS,);
634
+ expect(deadlineBreadcrumbs({ warn, },),)
635
+ .toBe(1,);
636
+ expect(deadlineBreadcrumbMessages({ warn, },)[0],)
637
+ .toContain(`${SHORT_DEADLINE_MS}ms`,);
638
+ },
639
+ },),
640
+
641
+ it({
642
+ name: 'a second flush after an abandoned write does not wait out the deadline again',
643
+ timeout: DEADLINE_TEST_TIMEOUT_MS,
644
+ fn: async ({ sinon, },) => {
645
+ sinon.stub(
646
+ console,
647
+ 'warn',
648
+ );
649
+ const {
650
+ logger,
651
+ initPromise,
652
+ } = createLogger({
653
+ flushDeadlineMs: SHORT_DEADLINE_MS,
654
+ sinks: [wedgedWriteSink(),],
655
+ },);
656
+ await initPromise;
657
+ logger.info('stuck',);
658
+ await logger.flush();
659
+
660
+ const elapsed = await timeFlush({ flush: logger.flush, },);
661
+ expect(elapsed,)
662
+ .toBeLessThan(FAST_FLUSH_MS,);
663
+ },
664
+ },),
665
+
666
+ it({
667
+ name: 'flush resolves once the deadline elapses when a flush hook never settles',
668
+ timeout: DEADLINE_TEST_TIMEOUT_MS,
669
+ fn: async ({ sinon, },) => {
670
+ const warn = sinon.stub(
671
+ console,
672
+ 'warn',
673
+ );
674
+ const hookWedged = recordingSink({
675
+ flush: function flushForever(): Promise<void> {
676
+ return neverSettles();
677
+ },
678
+ },);
679
+ const {
680
+ logger,
681
+ initPromise,
682
+ } = createLogger({
683
+ flushDeadlineMs: SHORT_DEADLINE_MS,
684
+ sinks: [hookWedged.sink,],
685
+ },);
686
+ await initPromise;
687
+
688
+ const elapsed = await timeFlush({ flush: logger.flush, },);
689
+ expect(elapsed,)
690
+ .toBeGreaterThanOrEqual(SHORT_DEADLINE_MS - DEADLINE_TOLERANCE_MS,);
691
+ expect(deadlineBreadcrumbs({ warn, },),)
692
+ .toBe(1,);
693
+ },
694
+ },),
695
+
696
+ it({
697
+ name: 'flush resolves once the deadline elapses when a verify never settles',
698
+ timeout: DEADLINE_TEST_TIMEOUT_MS,
699
+ fn: async ({ sinon, },) => {
700
+ const warn = sinon.stub(
701
+ console,
702
+ 'warn',
703
+ );
704
+ const verifyWedged = recordingSink({
705
+ verify: function verifyForever(): Promise<boolean> {
706
+ return neverSettles();
707
+ },
708
+ },);
709
+ const { logger, } = createLogger({
710
+ flushDeadlineMs: SHORT_DEADLINE_MS,
711
+ sinks: [verifyWedged.sink,],
712
+ },);
713
+
714
+ const elapsed = await timeFlush({ flush: logger.flush, },);
715
+ expect(elapsed,)
716
+ .toBeGreaterThanOrEqual(SHORT_DEADLINE_MS - DEADLINE_TOLERANCE_MS,);
717
+ expect(deadlineBreadcrumbs({ warn, },),)
718
+ .toBe(1,);
719
+ },
720
+ },),
721
+
722
+ it({
723
+ name: 'a flush that settles inside the deadline reports no breadcrumb',
724
+ fn: async ({ sinon, },) => {
725
+ const warn = sinon.stub(
726
+ console,
727
+ 'warn',
728
+ );
729
+ const quick = recordingSink({ writeDelayMs: 1, },);
730
+ const {
731
+ logger,
732
+ initPromise,
733
+ } = createLogger({
734
+ flushDeadlineMs: SHORT_DEADLINE_MS,
735
+ sinks: [quick.sink,],
736
+ },);
737
+ await initPromise;
738
+ logger.info('fast',);
739
+ await logger.flush();
740
+
741
+ expect(messages({ recording: quick, },),)
742
+ .toEqual(['fast',],);
743
+ expect(deadlineBreadcrumbs({ warn, },),)
744
+ .toBe(0,);
745
+ },
746
+ },),
747
+ ],
748
+ },),
749
+
750
+ //endregion Flush deadline
751
+ ],
752
+ },);