@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.
Files changed (63) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +58 -12
  3. package/dist/final/neutral/browser.d.mts +60 -0
  4. package/dist/final/neutral/browser.mjs +1 -0
  5. package/dist/final/neutral/index.d.mts +366 -572
  6. package/dist/final/neutral/index.mjs +2 -3
  7. package/dist/final/neutral/indexed-db-hsIfv7Cv.mjs +2 -0
  8. package/dist/final/neutral/types-BkkBXgY3.d.mts +76 -0
  9. package/dist/final/node/file-CRGb1hDK.mjs +1 -0
  10. package/dist/final/node/index.d.mts +366 -572
  11. package/dist/final/node/index.mjs +3 -3
  12. package/dist/final/node/node.d.mts +103 -0
  13. package/dist/final/node/node.mjs +1 -0
  14. package/dist/final/node/types-BkkBXgY3.d.mts +76 -0
  15. package/package.json +19 -5
  16. package/src/artifact-platform-split.unit.test.ts +140 -0
  17. package/src/browser.ts +14 -0
  18. package/src/create-logger.ts +249 -151
  19. package/src/create-logger.unit.test.ts +527 -75
  20. package/src/default-sinks.neutral.ts +34 -0
  21. package/src/default-sinks.node.ts +32 -0
  22. package/src/error-format.ts +23 -23
  23. package/src/index.ts +2 -0
  24. package/src/logger.ts +23 -50
  25. package/src/node.ts +23 -0
  26. package/src/sink/console-control-chars.ts +64 -64
  27. package/src/sink/console-control-chars.unit.test.ts +14 -14
  28. package/src/sink/console.ts +194 -194
  29. package/src/sink/console.unit.test.ts +18 -18
  30. package/src/sink/file.ts +136 -140
  31. package/src/sink/file.unit.test.ts +19 -26
  32. package/src/sink/index.ts +4 -7
  33. package/src/sink/indexed-db-util.ts +42 -42
  34. package/src/sink/indexed-db.browser.test.ts +7 -7
  35. package/src/sink/indexed-db.ts +109 -109
  36. package/src/sink/indexed-db.unit.test.ts +5 -13
  37. package/src/sink/local-storage-key.ts +73 -73
  38. package/src/sink/local-storage-key.unit.test.ts +8 -8
  39. package/src/sink/local-storage-quota.ts +37 -37
  40. package/src/sink/local-storage-quota.unit.test.ts +8 -8
  41. package/src/sink/local-storage-store.ts +113 -113
  42. package/src/sink/local-storage-store.unit.test.ts +35 -35
  43. package/src/sink/local-storage.ts +72 -72
  44. package/src/sink/local-storage.unit.test.ts +27 -27
  45. package/src/sink/noop.ts +20 -20
  46. package/src/sink/noop.unit.test.ts +1 -1
  47. package/src/sink/opfs.browser.test.ts +7 -7
  48. package/src/sink/opfs.ts +62 -62
  49. package/src/sink/opfs.unit.test.ts +5 -13
  50. package/src/sink/record-buffer.ts +84 -84
  51. package/src/sink/record-buffer.unit.test.ts +20 -20
  52. package/src/sink/session-storage-quota.ts +34 -34
  53. package/src/sink/session-storage-quota.unit.test.ts +8 -8
  54. package/src/sink/session-storage-store.ts +72 -72
  55. package/src/sink/session-storage.ts +48 -48
  56. package/src/sink/session-storage.unit.test.ts +39 -39
  57. package/src/sink/web-storage-quota-error.ts +22 -22
  58. package/src/sink/web-storage-quota-error.unit.test.ts +2 -2
  59. package/src/sink/web-storage-runtime.ts +24 -24
  60. package/src/startup.unit.test.ts +18 -18
  61. package/src/tagged.ts +35 -35
  62. package/src/tagged.unit.test.ts +8 -8
  63. 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
- * 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.
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
- * Flush deadline the deadline tests inject: short enough to keep the suite
25
- * fast, long enough that timer granularity cannot fire it early.
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
- * Timer slack subtracted from the deadline when asserting a flush waited it
31
- * out, covering setTimeout clamping and scheduler jitter.
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
- * 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
+ 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
- * Harness timeout for the deadline tests: a regression that hangs forever
43
- * fails here instead of stalling the suite.
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
- * Promise that never settles, standing in for a wedged sink operation.
49
- *
50
- * @returns Pending promise whose resolver is unreachable.
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
- * Times one `flush()` call.
58
- *
59
- * @param flush - Flush function to time.
60
- *
61
- * @returns Elapsed milliseconds.
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
- * Start timestamp.
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
- * 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
+ 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
- * 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.
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 deadlineBreadcrumbMessages({ warn, }: { readonly warn: RecordedCalls; },): string[] {
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 isDeadlineBreadcrumb(message,) {
96
- return message.includes('flush deadline',);
106
+ .filter(function matchesNeedle(message,) {
107
+ return message.includes(needle,);
97
108
  },);
98
109
  }
99
110
 
100
111
  /**
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.
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
- * Builds a verified sink whose every write never settles.
113
- *
114
- * @returns Sink standing in for a wedged backend.
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
- * Recording sink plus the array it appends every written record to, so a test
129
- * can assert exactly which records crossed the seam.
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
- * 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.
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
- * Records this sink has received, in arrival order.
218
+ Records this sink has received, in arrival order.
165
219
  */
166
220
  const records: LogRecord[] = [];
167
221
 
168
222
  /**
169
- * Records every received record after the optional delay.
170
- *
171
- * @param record - Record handed to the sink.
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
- * 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.
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
- * Write-attempt counter; a retired sink would stop receiving writes,
373
- * so a second attempt proves the rejection left the backend available.
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
- * Hook-invocation counter proving `flush()` reached the sink's own hook.
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
- * Write-attempt counter; a retired sink would stop receiving writes, so
517
- * a second attempt proves the synchronous throw left it available.
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
- * Flush-hook counter; stays zero because an unavailable sink's hook
558
- * must be skipped by `flushAll`.
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 Flush deadline
746
+ //region Breadcrumb suites (stub the shared console.warn, so sequential)
599
747
 
600
748
  describe({
601
- name: 'flush deadline',
602
- // One test at a time: each stubs the shared console.warn.
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
- //endregion Flush deadline
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
  },);