@bymax-one/nest-logger 1.2.7 → 1.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.
@@ -351,10 +351,30 @@ function assignIfDefined(payload, key, value) {
351
351
  }
352
352
  var PinoLoggerService = class {
353
353
  /**
354
+ * **A hand-built instance is NOT this library's behaviour.** Constructing
355
+ * `new PinoLoggerService(pino({...}, sink))` directly works in the sense of not
356
+ * throwing, and the parameter default below exists for exactly that — but the
357
+ * Pino instance you built has none of the wiring `buildPinoInstance` installs,
358
+ * and the omissions are silent:
359
+ *
360
+ * - **no `err` serializer**, so an `Error` is serialized by Pino's default.
361
+ * The `cause` chain disappears ENTIRELY — no marker, no warning, and a
362
+ * `cause` that reads as "the field was never set";
363
+ * - **no name-based redaction** of serializer output or child bindings;
364
+ * - **no trace-context mixin**, so `requestId` / `tenantId` / OTel ids are
365
+ * absent;
366
+ * - **no size bound**, so nothing is truncated.
367
+ *
368
+ * That matters most for tests, which is the use this doc used to invite without
369
+ * qualification: a consumer wrote one to check whether a `cause` carried its
370
+ * `code`, measured absence, and nearly filed it against this library. The
371
+ * absence was their harness. For a faithful harness, boot the module (or use
372
+ * `BymaxLoggerModule.forRoot` in a testing module) rather than hand-building.
373
+ *
354
374
  * @param pino - The wrapped Pino instance.
355
375
  * @param redact - The DEFAULT-coverage redactor, applied to `child()` bindings.
356
- * Defaults to the identity so a hand-built instance (benchmarks, unit tests)
357
- * still works; the module always injects the configured one.
376
+ * Defaults to the identity so a hand-built instance still works; the module
377
+ * always injects the configured one.
358
378
  */
359
379
  constructor(pino2, redact = (value) => value) {
360
380
  this.pino = pino2;
@@ -1094,9 +1114,35 @@ function reportDestinationFailure(logKey, name, cause, msg) {
1094
1114
  } catch {
1095
1115
  }
1096
1116
  }
1117
+ function safeDestinationName(destination) {
1118
+ try {
1119
+ return String(destination.name);
1120
+ } catch {
1121
+ return "unknown";
1122
+ }
1123
+ }
1124
+ function isLogLevel(value) {
1125
+ return LOG_LEVEL_PRIORITY.includes(value);
1126
+ }
1127
+ var resolvedMinLevels = /* @__PURE__ */ new WeakMap();
1128
+ function safeMinLevel(destination) {
1129
+ if (resolvedMinLevels.has(destination)) {
1130
+ return resolvedMinLevels.get(destination);
1131
+ }
1132
+ let resolved;
1133
+ try {
1134
+ const read = destination.minLevel;
1135
+ resolved = isLogLevel(read) ? read : void 0;
1136
+ } catch {
1137
+ resolved = void 0;
1138
+ }
1139
+ resolvedMinLevels.set(destination, resolved);
1140
+ return resolved;
1141
+ }
1097
1142
 
1098
1143
  // src/server/utils/destination-to-stream.ts
1099
- function reportWriteFailure(name, cause) {
1144
+ function reportWriteFailure(destination, cause) {
1145
+ const name = safeDestinationName(destination);
1100
1146
  reportDestinationFailure(
1101
1147
  RESERVED_LOG_KEYS.LOGGER_DESTINATION_WRITE_FAILED,
1102
1148
  name,
@@ -1116,7 +1162,8 @@ function destinationToStream(destination, health) {
1116
1162
  decodeStrings: false,
1117
1163
  write(chunk, _encoding, callback) {
1118
1164
  const onFailure = (cause) => {
1119
- reportWriteFailure(destination.name, cause);
1165
+ health.markWriteFailed(destination);
1166
+ reportWriteFailure(destination, cause);
1120
1167
  callback();
1121
1168
  };
1122
1169
  try {
@@ -1129,10 +1176,20 @@ function destinationToStream(destination, health) {
1129
1176
  return;
1130
1177
  }
1131
1178
  const result = destination.write(payload);
1132
- if (result instanceof Promise) {
1133
- result.then(() => callback(), onFailure);
1134
- } else {
1179
+ if (result === void 0) {
1135
1180
  callback();
1181
+ } else {
1182
+ health.markWritePending(destination);
1183
+ Promise.resolve(result).then(
1184
+ () => {
1185
+ health.markWriteSettled(destination);
1186
+ callback();
1187
+ },
1188
+ (cause) => {
1189
+ health.markWriteSettled(destination);
1190
+ onFailure(cause);
1191
+ }
1192
+ );
1136
1193
  }
1137
1194
  } catch (cause) {
1138
1195
  onFailure(cause);
@@ -1164,10 +1221,11 @@ function resolveToJson(value) {
1164
1221
  };
1165
1222
  }
1166
1223
  function cloneError(error) {
1167
- const clone = Object.create(
1168
- Object.getPrototypeOf(error),
1169
- Object.getOwnPropertyDescriptors(error)
1170
- );
1224
+ const descriptors = Object.getOwnPropertyDescriptors(error);
1225
+ for (const descriptor of Object.values(descriptors)) {
1226
+ descriptor.configurable = true;
1227
+ }
1228
+ const clone = Object.create(Object.getPrototypeOf(error), descriptors);
1171
1229
  const { stack } = error;
1172
1230
  if (typeof stack === "string") {
1173
1231
  Reflect.defineProperty(clone, "stack", { value: stack, enumerable: false });
@@ -1175,11 +1233,8 @@ function cloneError(error) {
1175
1233
  return clone;
1176
1234
  }
1177
1235
  var BUFFER_TO_JSON = Buffer.prototype.toJSON;
1178
- var WRITE_FAILED = new Error();
1179
1236
  function defineOwn(target, key, value) {
1180
- if (!Reflect.defineProperty(target, key, { value, enumerable: true })) {
1181
- throw WRITE_FAILED;
1182
- }
1237
+ Reflect.defineProperty(target, key, { value, enumerable: true });
1183
1238
  }
1184
1239
  function walkArray(value, sensitive, censor, depth2, ancestors) {
1185
1240
  const length = value.length;
@@ -1577,7 +1632,11 @@ function buildPinoInstance(options, logContext, destinations, health) {
1577
1632
  }
1578
1633
  };
1579
1634
  const streams = destinations.map((destination) => ({
1580
- level: destination.minLevel ?? options.level,
1635
+ // Guarded: this runs at provider construction, BEFORE any lifecycle hook, so a
1636
+ // consumer implementing `minLevel` as a throwing getter would fail the factory
1637
+ // and the application would never start — earlier than any fail-soft path can
1638
+ // contain. Unreadable and absent give the same answer: the module level.
1639
+ level: safeMinLevel(destination) ?? options.level,
1581
1640
  stream: destinationToStream(destination, health)
1582
1641
  }));
1583
1642
  return pino(pinoOptions, pino.multistream(streams));
@@ -1587,24 +1646,91 @@ var DestinationHealth = class {
1587
1646
  /** Destinations whose `onInit()` rejected. Identity-keyed, not name-keyed. */
1588
1647
  this.failed = /* @__PURE__ */ new Set();
1589
1648
  /**
1590
- * Whether ANY destination initialized successfully.
1649
+ * Destinations that initialized, each with its effective level as an index into
1650
+ * {@link LOG_LEVEL_PRIORITY}.
1651
+ *
1652
+ * Keyed by identity rather than reduced to a flag or a minimum, because the
1653
+ * question a buffering destination asks is "did SOMEONE ELSE receive what I
1654
+ * received?" — and a minimum cannot exclude the asker. A destination that is
1655
+ * itself the only healthy sink would otherwise be told its held entries were
1656
+ * delivered elsewhere, and discard its only copies.
1657
+ *
1658
+ * The level matters for the same reason it does everywhere here: `pino.multistream`
1659
+ * filters per stream, so "alive" and "received what I received" are different
1660
+ * facts.
1661
+ */
1662
+ this.healthy = /* @__PURE__ */ new Map();
1663
+ /**
1664
+ * Destinations whose `write()` threw or rejected at least once.
1665
+ *
1666
+ * Init health says a sink is LIVE; it does not say the sink accepted anything.
1667
+ * A destination that throws on write is reported and skipped per entry, so
1668
+ * inferring delivery from level alone would credit it with entries it dropped.
1669
+ * Uncertainty drains, so any write failure disqualifies it as proof of delivery
1670
+ * for the whole window — coarser than per-entry tracking, and wrong only in the
1671
+ * direction that duplicates rather than loses.
1672
+ */
1673
+ this.writeFailed = /* @__PURE__ */ new Set();
1674
+ /**
1675
+ * In-flight asynchronous writes per destination.
1676
+ *
1677
+ * A rejected async write is only recorded when its promise settles, and the
1678
+ * registry can compute readiness while one is still pending — so "no failure
1679
+ * recorded" is not "every write landed". A destination with anything in flight
1680
+ * is UNPROVEN rather than trusted: counting it as delivery would let a
1681
+ * buffering sink discard its copy moments before the pending write rejects.
1591
1682
  *
1592
- * A boolean rather than a count, deliberately: the only question ever asked of
1593
- * it is "is there somewhere to write?". Mutation testing made the point — a
1594
- * counter incremented with `+=` survived being flipped to `-=`, because
1595
- * `count === 0` means the same thing either way. A value whose arithmetic
1596
- * cannot be observed was not a counter, it was a flag wearing a number.
1683
+ * A counter rather than a flag because writes overlap; it returns to zero only
1684
+ * when the last one settles.
1597
1685
  */
1598
- this.hasHealthy = false;
1686
+ this.pendingWrites = /* @__PURE__ */ new Map();
1599
1687
  /**
1600
1688
  * The rescuer's severity index within {@link LOG_LEVEL_PRIORITY}. Starts above
1601
1689
  * every real level so the first `markFailed` always wins the comparison.
1602
1690
  */
1603
1691
  this.rescuerLevel = Number.POSITIVE_INFINITY;
1604
1692
  }
1605
- /** Record that a destination initialized successfully. */
1606
- markHealthy() {
1607
- this.hasHealthy = true;
1693
+ /**
1694
+ * Record that a destination's `write()` threw or rejected.
1695
+ *
1696
+ * Called from the fan-out's failure path, which already reports the entry as
1697
+ * dropped. This is what stops {@link deliveredByHealthySink} from crediting a
1698
+ * throwing sink with entries it never accepted.
1699
+ *
1700
+ * @param destination - The destination whose write failed.
1701
+ */
1702
+ markWriteFailed(destination) {
1703
+ this.writeFailed.add(destination);
1704
+ }
1705
+ /**
1706
+ * Record that an asynchronous write to a destination has started.
1707
+ *
1708
+ * @param destination - The destination being written to.
1709
+ */
1710
+ markWritePending(destination) {
1711
+ this.pendingWrites.set(destination, (this.pendingWrites.get(destination) ?? 0) + 1);
1712
+ }
1713
+ /**
1714
+ * Record that an asynchronous write has settled, however it settled.
1715
+ *
1716
+ * Paired with {@link markWritePending} on both the fulfilled and rejected
1717
+ * paths — a counter that only decremented on success would never return to
1718
+ * zero for a failing sink, which is a different claim from the one this makes.
1719
+ *
1720
+ * @param destination - The destination whose write settled.
1721
+ */
1722
+ markWriteSettled(destination) {
1723
+ this.pendingWrites.set(destination, (this.pendingWrites.get(destination) ?? 1) - 1);
1724
+ }
1725
+ /**
1726
+ * Record that a destination initialized successfully.
1727
+ *
1728
+ * @param destination - The destination that initialized.
1729
+ * @param effectiveLevel - Its multistream level: `minLevel` when set, otherwise
1730
+ * the module-wide `level`.
1731
+ */
1732
+ markHealthy(destination, effectiveLevel) {
1733
+ this.healthy.set(destination, LOG_LEVEL_PRIORITY.indexOf(effectiveLevel));
1608
1734
  }
1609
1735
  /**
1610
1736
  * Record that a destination failed to initialize, and re-elect the rescuer.
@@ -1655,7 +1781,31 @@ var DestinationHealth = class {
1655
1781
  * rescuer.
1656
1782
  */
1657
1783
  shouldRescue(destination) {
1658
- return !this.hasHealthy && this.rescuer === destination;
1784
+ return this.healthy.size === 0 && this.rescuer === destination;
1785
+ }
1786
+ /**
1787
+ * Whether a LIVE sink received everything a destination at `effectiveLevel`
1788
+ * received — the question a destination holding pre-init entries actually has
1789
+ * before discarding them.
1790
+ *
1791
+ * Not "did any sink survive": `pino.multistream` filters per stream, so a
1792
+ * healthy `error` sink never saw the `info` entries a pretty sink at `info`
1793
+ * buffered. Discarding those because something else was alive would lose them,
1794
+ * which is the failure the buffer exists to prevent. A healthy sink whose level
1795
+ * is at or below this one accepted a superset, so those entries are delivered.
1796
+ *
1797
+ * @param effectiveLevel - The asking destination's multistream level.
1798
+ * @returns `true` when a live sink accepted everything this destination did.
1799
+ */
1800
+ deliveredByHealthySink(asking, effectiveLevel) {
1801
+ const level = LOG_LEVEL_PRIORITY.indexOf(effectiveLevel);
1802
+ for (const [destination, healthyLevel] of this.healthy) {
1803
+ const unproven = this.writeFailed.has(destination) || (this.pendingWrites.get(destination) ?? 0) > 0;
1804
+ if (destination !== asking && !unproven && healthyLevel <= level) {
1805
+ return true;
1806
+ }
1807
+ }
1808
+ return false;
1659
1809
  }
1660
1810
  };
1661
1811
  DestinationHealth = __decorateClass([
@@ -1707,14 +1857,70 @@ var DestinationRegistry = class {
1707
1857
  try {
1708
1858
  await destination.onInit?.();
1709
1859
  this.active.push(destination);
1710
- this.health.markHealthy();
1860
+ this.health.markHealthy(destination, this.effectiveLevelOf(destination));
1711
1861
  } catch (cause) {
1712
- this.health.markFailed(destination, destination.minLevel ?? this.options.level);
1713
- this.reportInitFailure(destination.name, cause);
1862
+ this.health.markFailed(destination, this.effectiveLevelOf(destination));
1863
+ this.reportInitFailure(destination, cause);
1714
1864
  }
1715
1865
  }
1866
+ await this.notifyRegistryReady();
1716
1867
  this.announceBootstrap();
1717
1868
  }
1869
+ /**
1870
+ * Tell every REGISTERED destination — live or failed — what happened to the
1871
+ * entries it may be holding, before the first post-init entry is emitted.
1872
+ *
1873
+ * The signal it carries is one the destination cannot compute: whether ANOTHER
1874
+ * live sink appears to have accepted everything this one accepted.
1875
+ * `pino.multistream` filters per stream, a destination is not its own witness, a
1876
+ * sink whose write threw did not receive that entry, and one whose write has not
1877
+ * settled has shown nothing yet — all of which live in `deliveredByHealthySink`.
1878
+ *
1879
+ * A signal rather than a proof, deliberately: a write still queued inside the
1880
+ * `Writable` adapter is invisible to that accounting, so the flag says which
1881
+ * risk is smaller, not that the entry is safe. See `ILogDestination.onRegistryReady`.
1882
+ *
1883
+ * Each notification is isolated: a destination that throws here is reported and
1884
+ * skipped rather than aborting the remaining ones or the bootstrap entry.
1885
+ * Failing at this hook cannot be allowed to cost more than the hook was worth.
1886
+ */
1887
+ async notifyRegistryReady() {
1888
+ for (const destination of this.registered) {
1889
+ try {
1890
+ await destination.onRegistryReady?.({
1891
+ heldEntriesDeliveredElsewhere: this.health.deliveredByHealthySink(
1892
+ destination,
1893
+ this.effectiveLevelOf(destination)
1894
+ )
1895
+ });
1896
+ } catch (cause) {
1897
+ this.reportHookFailure(destination, cause);
1898
+ }
1899
+ }
1900
+ }
1901
+ /**
1902
+ * The severity a destination actually receives: the STRICTER of the module
1903
+ * level and its own `minLevel`.
1904
+ *
1905
+ * Pino filters at the instance level BEFORE `pino.multistream` sees an entry,
1906
+ * so a `minLevel` below the global one cannot widen what a destination gets —
1907
+ * the factory's own docs say so. Recording the raw `minLevel` therefore
1908
+ * understated delivery: with a global `error` and a healthy sink at `info`,
1909
+ * both sinks in fact receive the same `error` entries, but a pretty sink
1910
+ * declared at `trace` compared as `trace`, delivery read as unproven, and the
1911
+ * buffer drained raw — a duplicate produced by arithmetic rather than by any
1912
+ * real gap.
1913
+ *
1914
+ * @param destination - The destination whose threshold is being computed.
1915
+ * @returns The level entries must clear to reach it.
1916
+ */
1917
+ effectiveLevelOf(destination) {
1918
+ const configured = safeMinLevel(destination);
1919
+ if (configured === void 0) {
1920
+ return this.options.level;
1921
+ }
1922
+ return LOG_LEVEL_PRIORITY.indexOf(configured) > LOG_LEVEL_PRIORITY.indexOf(this.options.level) ? configured : this.options.level;
1923
+ }
1718
1924
  /**
1719
1925
  * Report one destination init failure as a structured
1720
1926
  * `LOGGER_DESTINATION_INIT_FAILED` line on `process.stderr`.
@@ -1723,10 +1929,13 @@ var DestinationRegistry = class {
1723
1929
  * than restating the shape, so an operator greps one `logKey` regardless of
1724
1930
  * which stage failed — and the two cannot drift apart.
1725
1931
  *
1726
- * @param name - The failing destination's name.
1932
+ * @param destination - The failing destination. Passed whole, so its `name` is
1933
+ * read under `safeDestinationName` rather than at the call site — which sits
1934
+ * inside the catch, where a throwing getter would abort bootstrap.
1727
1935
  * @param cause - The thrown or rejected value.
1728
1936
  */
1729
- reportInitFailure(name, cause) {
1937
+ reportInitFailure(destination, cause) {
1938
+ const name = safeDestinationName(destination);
1730
1939
  reportDestinationFailure(
1731
1940
  RESERVED_LOG_KEYS.LOGGER_DESTINATION_INIT_FAILED,
1732
1941
  name,
@@ -1734,6 +1943,30 @@ var DestinationRegistry = class {
1734
1943
  `Log destination "${name}" failed to initialize and will receive no entries. If no destination initializes, entries fall back to raw NDJSON on stdout.`
1735
1944
  );
1736
1945
  }
1946
+ /**
1947
+ * Report a failure from the readiness hook, with wording that matches the
1948
+ * destination's actual state.
1949
+ *
1950
+ * The loop calls every registered destination, healthy and failed alike, so one
1951
+ * message cannot be true for both. A healthy one keeps receiving entries and was
1952
+ * never an init failure; a failed one was already dropped from the fan-out and
1953
+ * saying it "remains active" would contradict the report it just got. The key
1954
+ * stays `LOGGER_DESTINATION_INIT_FAILED` either way so an operator greps one
1955
+ * thing across every destination-lifecycle problem.
1956
+ *
1957
+ * @param destination - The destination whose hook threw.
1958
+ * @param cause - The thrown value.
1959
+ */
1960
+ reportHookFailure(destination, cause) {
1961
+ const stillActive = !this.health.isFailed(destination);
1962
+ const name = safeDestinationName(destination);
1963
+ reportDestinationFailure(
1964
+ RESERVED_LOG_KEYS.LOGGER_DESTINATION_INIT_FAILED,
1965
+ name,
1966
+ cause,
1967
+ `Log destination "${name}" threw from onRegistryReady. ` + (stillActive ? "It initialized successfully and keeps receiving entries; only anything it was holding from before init may be affected." : "It had already failed to initialize and receives no entries; anything it was still holding is lost.")
1968
+ );
1969
+ }
1737
1970
  /**
1738
1971
  * Emit the one-shot bootstrap entries, AFTER every destination has been
1739
1972
  * initialized.
@@ -1790,14 +2023,50 @@ var DestinationRegistry = class {
1790
2023
  try {
1791
2024
  await destination.onShutdown?.();
1792
2025
  } catch (cause) {
1793
- const detail = cause instanceof Error ? cause.stack ?? cause.message : String(cause);
1794
- writeStderrSafely(
1795
- `[DestinationRegistry] Shutdown failed for "${destination.name}": ${detail}
1796
- `
1797
- );
2026
+ this.reportShutdownFailure(destination, cause);
1798
2027
  }
1799
2028
  }
1800
2029
  }
2030
+ /**
2031
+ * Report a failing `onShutdown` on stderr without letting the report itself
2032
+ * abort the teardown of the destinations still queued behind it.
2033
+ *
2034
+ * The coercion is INSIDE the guard, not above it. Reading `stack`/`message` on
2035
+ * an `Error` with hostile getters, and `String(cause)` on a value with a
2036
+ * throwing `toString`/`Symbol.toPrimitive`, both throw — and this runs inside
2037
+ * the `catch` that exists to keep one bad sink from mattering, so an escape
2038
+ * here would propagate out of `onApplicationShutdown` and skip every remaining
2039
+ * destination's flush. The never-throw contract has to cover READING the value,
2040
+ * not just writing it, which is the same correction `reportDestinationFailure`
2041
+ * already carries.
2042
+ *
2043
+ * The text is escaped for the same reason every other terminal-bound path is:
2044
+ * a stack is legitimately multi-line, so `escapeControlCharacters` keeps its
2045
+ * newlines and neutralizes everything else that can drive a terminal.
2046
+ *
2047
+ * The DESTINATION is passed rather than its name, so that read happens in here
2048
+ * too. `name` is declared `readonly name: string` but nothing stops a consumer
2049
+ * implementing it as a getter, and reading it at the call site would put it back
2050
+ * outside the guard — inside the `catch`, where a throw has the same cost.
2051
+ *
2052
+ * Two separate guards, not one: a name that cannot be read must not also cost
2053
+ * the failure detail, and a detail that cannot be read must not cost the name.
2054
+ * Either alone still identifies which sink failed.
2055
+ *
2056
+ * @param destination - The destination whose `onShutdown` threw.
2057
+ * @param cause - The thrown value, of any shape.
2058
+ */
2059
+ reportShutdownFailure(destination, cause) {
2060
+ const name = toSingleLineMessage(safeDestinationName(destination));
2061
+ let detail = "UnknownError";
2062
+ try {
2063
+ const stack = isErrorLike(cause) ? cause.stack : void 0;
2064
+ detail = stack === void 0 ? toSingleLineMessage(isErrorLike(cause) ? String(cause.message) : String(cause)) : escapeControlCharacters(String(stack));
2065
+ } catch {
2066
+ }
2067
+ writeStderrSafely(`[DestinationRegistry] Shutdown failed for "${name}": ${detail}
2068
+ `);
2069
+ }
1801
2070
  /**
1802
2071
  * The destinations that initialized successfully.
1803
2072
  *
@@ -2148,9 +2417,6 @@ var PrettyDevDestination = class {
2148
2417
  });
2149
2418
  } catch {
2150
2419
  this.initFailed = true;
2151
- this.flushBuffer((payload) => {
2152
- writeRawToStdout(payload);
2153
- });
2154
2420
  throw new Error(
2155
2421
  "[PrettyDevDestination] pino-pretty is not installed. Install it as a peer dependency (`pnpm add -D pino-pretty`) or remove PrettyDevDestination from `destinations`."
2156
2422
  );
@@ -2201,6 +2467,41 @@ var PrettyDevDestination = class {
2201
2467
  this.buffer.push(payload);
2202
2468
  this.bufferedBytes += payloadBytes;
2203
2469
  }
2470
+ /**
2471
+ * Resolve anything still held, now that the registry knows what became of it.
2472
+ *
2473
+ * **Discard only where another sink is believed to have taken the same entries;
2474
+ * emit everything else.** Losing a boot entry is not an acceptable outcome, and
2475
+ * every richer policy tried here had a losing branch: trusting "a sink survived"
2476
+ * lost entries to a sink at a higher level, trusting a level lost them to a sink
2477
+ * that was itself the asker, then to one whose writes were throwing, then to one
2478
+ * whose write had not settled yet. Each of those is now folded into the single
2479
+ * signal handed in — and where it is uncertain, it says `false` and this drains.
2480
+ *
2481
+ * That signal is not a proof, and this hook does not treat it as one. It cannot
2482
+ * see an entry still queued inside the `Writable` adapter, because the adapter
2483
+ * has not called `destination.write()` for it yet. Discarding on it is a bet
2484
+ * that a duplicated boot line costs less than a lost one — which is the trade
2485
+ * this destination accepts, and which a destination that cannot lose anything
2486
+ * should decline by ignoring the flag and always emitting.
2487
+ *
2488
+ * The cost is stated rather than hidden: when nothing survived, every holding
2489
+ * destination drains, so N of them produce N copies of the boot entries. A
2490
+ * duplicated line in an already-degraded boot is a smaller harm than a lost one,
2491
+ * and it is loud rather than silent.
2492
+ *
2493
+ * A no-op once {@link onInit} succeeded — the buffer was flushed through the
2494
+ * transform there and is already empty.
2495
+ *
2496
+ * @param status.heldEntriesDeliveredElsewhere - Another live sink appears to
2497
+ * have taken everything this one took. A deduplication signal, not a proof —
2498
+ * see `ILogDestination.onRegistryReady` for what it cannot see.
2499
+ */
2500
+ onRegistryReady(status) {
2501
+ this.flushBuffer(
2502
+ status.heldEntriesDeliveredElsewhere ? () => void 0 : writeRawToStdout
2503
+ );
2504
+ }
2204
2505
  /**
2205
2506
  * Hand every buffered entry to `emit`, in arrival order, and empty the buffer.
2206
2507
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bymax-one/nest-logger",
3
- "version": "1.2.7",
3
+ "version": "1.3.0",
4
4
  "description": "Structured JSON logging for NestJS based on Pino 10, with optional OpenTelemetry correlation.",
5
5
  "author": "Bymax One <support@bymax.one>",
6
6
  "license": "MIT",
@@ -57,6 +57,7 @@
57
57
  "bench": "node --expose-gc --import tsx bench/throughput.bench.ts",
58
58
  "build": "pnpm clean && tsup",
59
59
  "check:exports": "attw --pack .",
60
+ "check:docs": "node scripts/check-doc-snippets.mjs",
60
61
  "check:mutants": "node scripts/check-mutation-directives.mjs",
61
62
  "check:published": "node scripts/check-published-surface.mjs",
62
63
  "clean": "rm -rf dist coverage",
@@ -66,7 +67,7 @@
66
67
  "mutation:full": "node -e \"require('node:fs').rmSync('reports/stryker-incremental.json',{force:true,recursive:true})\" && stryker run",
67
68
  "mutation:dry-run": "stryker run --dryRunOnly",
68
69
  "prepare": "husky",
69
- "prepublishOnly": "pnpm clean && pnpm typecheck && pnpm lint && pnpm check:mutants && pnpm test:cov:all && pnpm build && pnpm check:published",
70
+ "prepublishOnly": "pnpm clean && pnpm typecheck && pnpm lint && pnpm check:mutants && pnpm check:docs && pnpm test:cov:all && pnpm build && pnpm check:published",
70
71
  "release": "npm publish --provenance --access public",
71
72
  "size": "node scripts/check-size.mjs",
72
73
  "test": "jest",