@richardmcquiston01/error-log-adapter 0.8.0 → 0.9.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/README.md CHANGED
@@ -70,27 +70,28 @@ Constructor options are checked when the adapter is created and throw a descript
70
70
 
71
71
  Every adapter takes an optional `prefix` (default `[error-log-adapter]`) that is printed before its failure messages.
72
72
 
73
- | Adapter | Sink | Runs in | Main options |
74
- | --------------------------------------------------- | --------------------------------------------------------- | ------------- | ------------------------------------------------------------------- |
75
- | `ConsoleErrorAdapter` | `console.error` | Anywhere | |
76
- | `WebhookErrorAdapter(url)` | HTTP `POST` of the entry as JSON | Anywhere | `headers`, `timeoutMs`, `retries`, `signingSecret`, `allowInsecure` |
77
- | `MongoDbErrorAdapter(collection)` | Anything with `insertOne`, such as a MongoDB `Collection` | Node | `timeoutMs` |
78
- | `PostgresErrorAdapter(client)` | A Postgres table, through `pg` | Node | `tableName`, `createTable`, `timeoutMs` |
79
- | `MySqlErrorAdapter(client)` | A MySQL / MariaDB table, through `mysql2/promise` | Node | `tableName`, `createTable`, `timeoutMs` |
80
- | `SqliteErrorAdapter(client)` | A SQLite table, through any driver | Node, Bun | `tableName`, `createTable`, `timeoutMs` |
81
- | `SupabaseErrorAdapter(client)` | A Supabase table, through `supabase-js` | Anywhere | `tableName`, `timeoutMs` |
82
- | `RedisErrorAdapter(client)` | A Redis list or stream | Node | `mode`, `key`, `maxEntries`, `side`, `timeoutMs` |
83
- | `S3ErrorAdapter(client, bucket)` | S3, DigitalOcean Spaces, R2, MinIO objects | Node | `keyPrefix`, `batch`, `contentType`, `timeoutMs` |
84
- | `FirestoreErrorAdapter(collection)` | A Firestore collection | Node, browser | `timeoutMs` |
85
- | `RealtimeDatabaseErrorAdapter(ref)` | A Realtime Database reference | Node, browser | `timeoutMs` |
86
- | `FlatFileErrorAdapter(path)` | One text line per entry | Node, Bun | `formatLine`, `maxBytes`, `maxFiles` |
87
- | `JsonFileErrorAdapter(path)` | NDJSON lines or one JSON array | Node, Bun | `mode`, `maxEntries`, `indent` |
88
- | `IndexedDbErrorAdapter(database)` | An IndexedDB object store | Browser | `storeName`, `timeoutMs` |
89
- | `WebStorageErrorAdapter(storage)` | `sessionStorage` or `localStorage`, as one capped array | Browser | `key`, `maxEntries`, `locks` |
90
- | `EventEmitterErrorAdapter(emitter)` | An event on an emitter you own (in-process hooks) | Anywhere | `eventName`, `requireListener` |
91
- | `WebSocketErrorAdapter(socket)` | One JSON message over a socket you own | Anywhere | (none) |
92
- | `CloudWatchLogsErrorAdapter(client, group, stream)` | AWS CloudWatch Logs events | Node | `batch`, `createMissing`, `timeoutMs` |
93
- | `CompositeErrorAdapter([...])` | Several adapters at once; succeeds only if all succeed | Anywhere | `timeoutMs` |
73
+ | Adapter | Sink | Runs in | Main options |
74
+ | --------------------------------------------------- | -------------------------------------------------------------- | ------------- | ------------------------------------------------------------------- |
75
+ | `ConsoleErrorAdapter` | `console.error` | Anywhere | |
76
+ | `WebhookErrorAdapter(url)` | HTTP `POST` of the entry as JSON | Anywhere | `headers`, `timeoutMs`, `retries`, `signingSecret`, `allowInsecure` |
77
+ | `MongoDbErrorAdapter(collection)` | Anything with `insertOne`, such as a MongoDB `Collection` | Node | `timeoutMs` |
78
+ | `PostgresErrorAdapter(client)` | A Postgres table, through `pg` | Node | `tableName`, `createTable`, `timeoutMs` |
79
+ | `MySqlErrorAdapter(client)` | A MySQL / MariaDB table, through `mysql2/promise` | Node | `tableName`, `createTable`, `timeoutMs` |
80
+ | `SqliteErrorAdapter(client)` | A SQLite table, through any driver | Node, Bun | `tableName`, `createTable`, `timeoutMs` |
81
+ | `SupabaseErrorAdapter(client)` | A Supabase table, through `supabase-js` | Anywhere | `tableName`, `timeoutMs` |
82
+ | `RedisErrorAdapter(client)` | A Redis list or stream | Node | `mode`, `key`, `maxEntries`, `side`, `timeoutMs` |
83
+ | `S3ErrorAdapter(client, bucket)` | S3, DigitalOcean Spaces, R2, MinIO objects | Node | `keyPrefix`, `batch`, `contentType`, `timeoutMs` |
84
+ | `FirestoreErrorAdapter(collection)` | A Firestore collection | Node, browser | `timeoutMs` |
85
+ | `RealtimeDatabaseErrorAdapter(ref)` | A Realtime Database reference | Node, browser | `timeoutMs` |
86
+ | `FlatFileErrorAdapter(path)` | One text line per entry | Node, Bun | `formatLine`, `maxBytes`, `maxFiles` |
87
+ | `JsonFileErrorAdapter(path)` | NDJSON lines or one JSON array | Node, Bun | `mode`, `maxEntries`, `indent` |
88
+ | `IndexedDbErrorAdapter(database)` | An IndexedDB object store | Browser | `storeName`, `timeoutMs` |
89
+ | `WebStorageErrorAdapter(storage)` | `sessionStorage` or `localStorage`, as one capped array | Browser | `key`, `maxEntries`, `locks` |
90
+ | `EventEmitterErrorAdapter(emitter)` | An event on an emitter you own (in-process hooks) | Anywhere | `eventName`, `requireListener` |
91
+ | `WebSocketErrorAdapter(socket)` | One JSON message over a socket you own | Anywhere | (none) |
92
+ | `CloudWatchLogsErrorAdapter(client, group, stream)` | AWS CloudWatch Logs events | Node | `batch`, `createMissing`, `timeoutMs` |
93
+ | `BatchingErrorAdapter(adapter)` | A queue in front of any adapter: batches, retries with backoff | Anywhere | `batchSize`, `maxWaitMs`, `retries`, `maxBuffered`, `onDrop` |
94
+ | `CompositeErrorAdapter([...])` | Several adapters at once; succeeds only if all succeed | Anywhere | `timeoutMs` |
94
95
 
95
96
  Every stored record has the same shape: a UUID `id`, `code`, `message`, `timestamp` (ISO 8601), and optional `source` and `cause`. An `Error` cause is stored as `{ name, message, stack }`. `serializeEntry` and `serializeCause` are exported if you build your own sink.
96
97
 
@@ -105,6 +106,7 @@ What changes durability or size, by adapter:
105
106
  - **Event emitter**: each entry is emitted as an `error-log` event whose payload is the stored record, for in-process hooks such as a toast, a metrics counter or a test assertion. Listeners run synchronously, so one that throws makes `log()` return `ok: false` with its message. Node's `EventEmitter` and mitt work as they are; a browser `EventTarget` needs a one-line wrapper (shown in the example below). Don't name the event `error` on a Node `EventEmitter`: it throws when nobody listens. Node's `emit` returns `false` when there are no listeners; pass `requireListener: true` to report that as a failure instead of letting the entry vanish.
106
107
  - **WebSocket**: each entry is sent as one JSON text message (the serialized entry plus its UUID `id`) over a socket you own; the adapter never connects, reconnects or buffers. If the socket's `readyState` is not open (connecting, closing or closed), `log()` returns `ok: false` and says which. `ok: true` means the socket accepted the message, not that the server got it, since a WebSocket has no acknowledgement. Browsers, Node's global `WebSocket`, Deno, Bun and `ws` all fit. Pair it with a durable adapter in a `CompositeErrorAdapter` if entries must survive the socket being down.
107
108
  - **CloudWatch Logs**: each entry is sent through `PutLogEvents` as a JSON event (the serialized entry plus its UUID `id`) stamped with the entry's time, so Logs Insights can query its fields. Pass an AWS SDK v3 `CloudWatchLogs` client. AWS no longer needs sequence tokens, so none are tracked. If the stream (or group) does not exist, the adapter creates it and tries again once (`createMissing`, on by default; it needs `logs:CreateLogStream` and `logs:CreateLogGroup`, and a group created this way never expires its logs, so set a retention policy). CloudWatch can answer ok and still refuse events older than 14 days or more than 2 hours ahead; that is reported as `ok: false`. An event over 256 KiB is refused before sending. With `batch: { batchSize, maxWaitMs }`, events are buffered and sent together (up to 10,000 events and 1 MiB per request, oldest first), `log()` returns `ok` as soon as the event is buffered, and you should call `flush()` before the process exits. In Lambda and ECS, stdout already reaches CloudWatch; this is for apps running elsewhere.
109
+ - **Batching wrapper**: `BatchingErrorAdapter` puts a queue in front of any other adapter. `log()` returns `ok: true` as soon as the entry is queued, so a slow or failing sink never holds up the app, and entries are delivered in order in batches (`batchSize`, default 20, or after `maxWaitMs`, default 5000). A failed delivery is retried with exponential backoff (`retries`, default 3) and then given up on: the entries go to `onDrop` and the console, and `flush()` returns `ok: false` once. Delivery stops at the first failure so a dead sink is not hammered. If the wrapped adapter has a `logBatch` method (the webhook does) the batch goes in one call, which for the webhook means one request carrying a JSON array, so the receiver must accept an array; pass `useBatchMethod: false` to keep one request per entry. `ok: true` means _queued_, not stored, and the queue lives in memory: call `flush()` before the process exits, and put a durable adapter beside it in a `CompositeErrorAdapter` if entries must survive a crash. Do not also enable the wrapped adapter's own retries (the webhook's `retries`); they multiply. The buffer holds `maxBuffered` entries (default 1000) and drops the oldest past that.
108
110
  - **Webhook**: the URL must be `https://` (or `http://localhost`, `127.0.0.1`, `[::1]`) unless you pass `allowInsecure: true`, because entries carry messages and stack traces. Redirects are not followed, since a redirect would turn the `POST` into a body-less `GET` or re-send the entry and your headers to another host; a redirect is reported as `ok: false`. Failure messages contain only the origin, never the path or query, because webhook URLs often hold a secret. Set `retries` to try again after a network error, a timeout, HTTP 408, 425, 429 or 5xx (exponential backoff with jitter from `retryDelayMs`, capped by `maxRetryDelayMs`, honoring `Retry-After`; other 4xx answers are never retried). With `retries` set, every attempt sends the same body including an `id`, so the receiver can drop a repeat. Set `signingSecret` to add `X-Signature-Timestamp` and `X-Signature-256` (`sha256=` + hex HMAC-SHA256 of `<timestamp>.<body>`); the receiver recomputes it and rejects old timestamps.
109
111
 
110
112
  ### When a sink is slow or down
@@ -275,6 +277,23 @@ const errorLog = new CloudWatchLogsErrorAdapter(
275
277
  process.on('beforeExit', () => void errorLog.flush());
276
278
  ```
277
279
 
280
+ Batch and retry in front of a webhook, so a slow endpoint never holds up the app:
281
+
282
+ ```ts
283
+ const errorLog = new BatchingErrorAdapter(
284
+ new WebhookErrorAdapter('https://example.com/errors', {
285
+ signingSecret: process.env.ERROR_WEBHOOK_SECRET,
286
+ }),
287
+ {
288
+ batchSize: 50,
289
+ maxWaitMs: 2000,
290
+ onDrop: (entries, reason) =>
291
+ console.warn(`lost ${entries.length}: ${reason}`),
292
+ }
293
+ );
294
+ process.on('beforeExit', () => void errorLog.flush());
295
+ ```
296
+
278
297
  Write your own adapter by implementing the interface. Catch failures and return them rather than throwing:
279
298
 
280
299
  ```ts
@@ -303,6 +322,8 @@ class SlackErrorAdapter implements ErrorLogAdapter {
303
322
  }
304
323
  ```
305
324
 
325
+ The full guide, [docs/custom-adapters.md](./docs/custom-adapters.md), covers the contract, timeouts, entry ids, buffering adapters, testing, and a checklist.
326
+
306
327
  See [ROADMAP.md](./ROADMAP.md) for planned adapters and [CHANGELOG.md](./CHANGELOG.md) for what changed in each release.
307
328
 
308
329
  ### Development
package/dist/index.cjs CHANGED
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ BatchingErrorAdapter: () => BatchingErrorAdapter,
23
24
  CloudWatchLogsErrorAdapter: () => CloudWatchLogsErrorAdapter,
24
25
  CompositeErrorAdapter: () => CompositeErrorAdapter,
25
26
  ConsoleErrorAdapter: () => ConsoleErrorAdapter,
@@ -192,6 +193,12 @@ var ConsoleErrorAdapter = class {
192
193
  }
193
194
  };
194
195
 
196
+ // src/backoff.ts
197
+ function backoffDelayMs(retryNumber, baseMs, maxMs) {
198
+ const ceiling = Math.min(maxMs, baseMs * 2 ** (retryNumber - 1));
199
+ return ceiling / 2 + Math.random() * ceiling / 2;
200
+ }
201
+
195
202
  // src/uuid.ts
196
203
  function generateId() {
197
204
  const cryptoApi = globalThis.crypto;
@@ -403,6 +410,36 @@ var WebhookErrorAdapter = class {
403
410
  `could not prepare the entry: ${this.redact(describeError(error))}`
404
411
  );
405
412
  }
413
+ return this.deliver(body);
414
+ }
415
+ /**
416
+ * Sends several entries in one request, as a JSON array of the same records
417
+ * `log` sends one at a time (each with an `id`). `BatchingErrorAdapter` uses
418
+ * this when it wraps a webhook, so the receiver must accept an array. The
419
+ * whole request succeeds or fails together.
420
+ */
421
+ async logBatch(entries) {
422
+ if (entries.length === 0) {
423
+ return { ok: true };
424
+ }
425
+ let body;
426
+ try {
427
+ body = JSON.stringify(
428
+ entries.map((entry) => ({
429
+ id: resolveEntryId(entry),
430
+ ...serializeEntry(entry)
431
+ }))
432
+ );
433
+ } catch (error) {
434
+ console.error(`${this.prefix} WebhookErrorAdapter:`, error);
435
+ return this.fail(
436
+ `could not prepare the entries: ${this.redact(describeError(error))}`
437
+ );
438
+ }
439
+ return this.deliver(body);
440
+ }
441
+ /** Sends one prepared body, trying again as `retries` allows. */
442
+ async deliver(body) {
406
443
  let attempt = await this.send(body);
407
444
  let attempts = 1;
408
445
  while (!attempt.ok && attempt.retryable && attempts <= this.retries) {
@@ -422,11 +459,7 @@ var WebhookErrorAdapter = class {
422
459
  if (retryAfterMs !== void 0) {
423
460
  return Math.min(retryAfterMs, this.maxRetryDelayMs);
424
461
  }
425
- const ceiling = Math.min(
426
- this.maxRetryDelayMs,
427
- this.retryDelayMs * 2 ** (retryNumber - 1)
428
- );
429
- return ceiling / 2 + Math.random() * ceiling / 2;
462
+ return backoffDelayMs(retryNumber, this.retryDelayMs, this.maxRetryDelayMs);
430
463
  }
431
464
  async send(body) {
432
465
  const controller = new AbortController();
@@ -2146,8 +2179,259 @@ function describeRejection(output, eventCount) {
2146
2179
  }
2147
2180
  return reasons.length === 0 ? void 0 : `CloudWatch Logs accepted the request but rejected events: ${reasons.join("; ")}`;
2148
2181
  }
2182
+
2183
+ // src/adapters/batching-error-adapter.ts
2184
+ var DEFAULT_BATCH_SIZE = 20;
2185
+ var DEFAULT_MAX_WAIT_MS = 5e3;
2186
+ var DEFAULT_RETRIES = 3;
2187
+ var DEFAULT_RETRY_DELAY_MS2 = 500;
2188
+ var DEFAULT_MAX_RETRY_DELAY_MS2 = 3e4;
2189
+ var DEFAULT_MAX_BUFFERED = 1e3;
2190
+ function hasLogBatch(adapter) {
2191
+ return typeof adapter.logBatch === "function";
2192
+ }
2193
+ var BatchingErrorAdapter = class {
2194
+ /** @throws {RangeError} if an option is out of range. */
2195
+ constructor(inner, options = {}) {
2196
+ this.inner = inner;
2197
+ this.queue = [];
2198
+ this.waitTimer = null;
2199
+ this.backoffTimer = null;
2200
+ this.draining = null;
2201
+ this.flushers = 0;
2202
+ this.droppedSinceFlush = 0;
2203
+ this.lastDropReason = "";
2204
+ this.batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;
2205
+ this.maxWaitMs = options.maxWaitMs ?? DEFAULT_MAX_WAIT_MS;
2206
+ this.retries = options.retries ?? DEFAULT_RETRIES;
2207
+ this.retryDelayMs = options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS2;
2208
+ this.maxRetryDelayMs = options.maxRetryDelayMs ?? DEFAULT_MAX_RETRY_DELAY_MS2;
2209
+ this.maxBuffered = options.maxBuffered ?? DEFAULT_MAX_BUFFERED;
2210
+ this.useBatchMethod = options.useBatchMethod ?? true;
2211
+ this.onDrop = options.onDrop;
2212
+ this.logPrefix = options.prefix ?? DEFAULT_PREFIX;
2213
+ if (!Number.isInteger(this.batchSize) || this.batchSize < 1) {
2214
+ throw new RangeError(
2215
+ `BatchingErrorAdapter: batchSize must be a positive integer, received ${String(options.batchSize)}.`
2216
+ );
2217
+ }
2218
+ if (Number.isNaN(this.maxWaitMs) || this.maxWaitMs <= 0) {
2219
+ throw new RangeError(
2220
+ `BatchingErrorAdapter: maxWaitMs must be a positive number or Infinity, received ${String(options.maxWaitMs)}.`
2221
+ );
2222
+ }
2223
+ if (!Number.isInteger(this.retries) || this.retries < 0) {
2224
+ throw new RangeError(
2225
+ `BatchingErrorAdapter: retries must be a whole number of 0 or more, received ${String(options.retries)}.`
2226
+ );
2227
+ }
2228
+ if (!Number.isFinite(this.retryDelayMs) || this.retryDelayMs <= 0) {
2229
+ throw new RangeError(
2230
+ `BatchingErrorAdapter: retryDelayMs must be a positive number, received ${String(options.retryDelayMs)}.`
2231
+ );
2232
+ }
2233
+ if (!Number.isFinite(this.maxRetryDelayMs) || this.maxRetryDelayMs < this.retryDelayMs) {
2234
+ throw new RangeError(
2235
+ `BatchingErrorAdapter: maxRetryDelayMs must be a number no smaller than retryDelayMs (${this.retryDelayMs}), received ${String(options.maxRetryDelayMs)}.`
2236
+ );
2237
+ }
2238
+ if (!Number.isInteger(this.maxBuffered) || this.maxBuffered < this.batchSize) {
2239
+ throw new RangeError(
2240
+ `BatchingErrorAdapter: maxBuffered must be a whole number no smaller than batchSize (${this.batchSize}), received ${String(options.maxBuffered)}.`
2241
+ );
2242
+ }
2243
+ }
2244
+ /** Queues the entry. Resolves ok straight away; delivery happens in the background. */
2245
+ log(entry) {
2246
+ this.queue.push({ entry, failures: 0 });
2247
+ if (this.queue.length > this.maxBuffered) {
2248
+ const overflow = this.queue.splice(
2249
+ 0,
2250
+ this.queue.length - this.maxBuffered
2251
+ );
2252
+ this.drop(
2253
+ overflow,
2254
+ `the buffer of ${this.maxBuffered} entries was full because the wrapped adapter keeps failing`
2255
+ );
2256
+ }
2257
+ if (this.queue.length >= this.batchSize) {
2258
+ this.startDraining();
2259
+ } else {
2260
+ this.scheduleDrain();
2261
+ }
2262
+ return Promise.resolve({ ok: true });
2263
+ }
2264
+ /**
2265
+ * Delivers everything queued, retrying as configured, and resolves once the
2266
+ * queue is empty. Resolves `ok: false` if any entries were given up on since
2267
+ * the last flush. With retries on and the sink down this can take a while
2268
+ * (the waits add up to well over a minute with the defaults).
2269
+ */
2270
+ async flush() {
2271
+ this.flushers += 1;
2272
+ this.backoffTimer?.ref?.();
2273
+ try {
2274
+ this.startDraining();
2275
+ while (this.draining !== null) {
2276
+ await this.draining;
2277
+ }
2278
+ } finally {
2279
+ this.flushers -= 1;
2280
+ }
2281
+ if (this.droppedSinceFlush > 0) {
2282
+ const count = this.droppedSinceFlush;
2283
+ const reason = this.lastDropReason;
2284
+ this.droppedSinceFlush = 0;
2285
+ this.lastDropReason = "";
2286
+ return {
2287
+ ok: false,
2288
+ error: `BatchingErrorAdapter gave up on ${count} entr${count === 1 ? "y" : "ies"}: ${reason}`
2289
+ };
2290
+ }
2291
+ return { ok: true };
2292
+ }
2293
+ scheduleDrain() {
2294
+ if (this.waitTimer !== null || this.maxWaitMs === Infinity) {
2295
+ return;
2296
+ }
2297
+ const timer = setTimeout(() => {
2298
+ this.waitTimer = null;
2299
+ this.startDraining();
2300
+ }, this.maxWaitMs);
2301
+ timer.unref?.();
2302
+ this.waitTimer = timer;
2303
+ }
2304
+ startDraining() {
2305
+ if (this.waitTimer !== null) {
2306
+ clearTimeout(this.waitTimer);
2307
+ this.waitTimer = null;
2308
+ }
2309
+ if (this.draining !== null || this.queue.length === 0) {
2310
+ return;
2311
+ }
2312
+ this.draining = this.drain().catch((error) => {
2313
+ console.error(`${this.logPrefix} BatchingErrorAdapter:`, error);
2314
+ }).finally(() => {
2315
+ this.draining = null;
2316
+ if (this.queue.length > 0) {
2317
+ this.scheduleDrain();
2318
+ }
2319
+ });
2320
+ }
2321
+ /** Delivers batches until the queue is empty, waiting out a backoff after each failure. */
2322
+ async drain() {
2323
+ let failedRounds = 0;
2324
+ while (this.queue.length > 0) {
2325
+ const batch = this.queue.slice(0, this.batchSize);
2326
+ const failure = await this.deliver(batch);
2327
+ if (failure === void 0) {
2328
+ failedRounds = 0;
2329
+ continue;
2330
+ }
2331
+ failedRounds += 1;
2332
+ const exhausted = this.queue.filter(
2333
+ (item) => item.failures > this.retries
2334
+ );
2335
+ if (exhausted.length > 0) {
2336
+ this.queue = this.queue.filter(
2337
+ (item) => item.failures <= this.retries
2338
+ );
2339
+ this.drop(
2340
+ exhausted,
2341
+ `delivery failed ${this.retries + 1} time${this.retries === 0 ? "" : "s"}; last error: ${failure}`
2342
+ );
2343
+ }
2344
+ if (this.queue.length > 0) {
2345
+ await this.backoff(failedRounds);
2346
+ }
2347
+ }
2348
+ }
2349
+ /**
2350
+ * Tries to deliver `batch` (the front of the queue). Delivered entries leave
2351
+ * the queue; an entry that failed has its failure count raised and stays.
2352
+ * Resolves to the failure message, or undefined when everything went through.
2353
+ */
2354
+ async deliver(batch) {
2355
+ const remove = (delivered) => {
2356
+ const gone = new Set(delivered);
2357
+ this.queue = this.queue.filter(
2358
+ (item) => !gone.has(item)
2359
+ );
2360
+ };
2361
+ if (this.useBatchMethod && hasLogBatch(this.inner)) {
2362
+ const result = await this.safely(
2363
+ () => this.inner.logBatch(
2364
+ batch.map(
2365
+ (item) => item.entry
2366
+ )
2367
+ )
2368
+ );
2369
+ if (result.ok) {
2370
+ remove(batch);
2371
+ return void 0;
2372
+ }
2373
+ for (const item of batch) {
2374
+ item.failures += 1;
2375
+ }
2376
+ return result.error ?? "the wrapped adapter reported a failure";
2377
+ }
2378
+ for (const item of batch) {
2379
+ const result = await this.safely(
2380
+ () => this.inner.log(item.entry)
2381
+ );
2382
+ if (result.ok) {
2383
+ remove([item]);
2384
+ continue;
2385
+ }
2386
+ item.failures += 1;
2387
+ return result.error ?? "the wrapped adapter reported a failure";
2388
+ }
2389
+ return void 0;
2390
+ }
2391
+ /** The wrapped adapter should not throw, but a bug in one must not stop the queue. */
2392
+ async safely(call) {
2393
+ try {
2394
+ return await call();
2395
+ } catch (error) {
2396
+ return { ok: false, error: describeError(error) };
2397
+ }
2398
+ }
2399
+ backoff(failedRounds) {
2400
+ return new Promise((resolve) => {
2401
+ const timer = setTimeout(
2402
+ () => {
2403
+ this.backoffTimer = null;
2404
+ resolve();
2405
+ },
2406
+ backoffDelayMs(failedRounds, this.retryDelayMs, this.maxRetryDelayMs)
2407
+ );
2408
+ if (this.flushers === 0) {
2409
+ timer.unref?.();
2410
+ }
2411
+ this.backoffTimer = timer;
2412
+ });
2413
+ }
2414
+ drop(items, reason) {
2415
+ if (items.length === 0) {
2416
+ return;
2417
+ }
2418
+ this.droppedSinceFlush += items.length;
2419
+ this.lastDropReason = reason;
2420
+ console.error(
2421
+ `${this.logPrefix} BatchingErrorAdapter: gave up on ${items.length} entr${items.length === 1 ? "y" : "ies"}: ${reason}`
2422
+ );
2423
+ try {
2424
+ this.onDrop?.(
2425
+ items.map((item) => item.entry),
2426
+ reason
2427
+ );
2428
+ } catch {
2429
+ }
2430
+ }
2431
+ };
2149
2432
  // Annotate the CommonJS export names for ESM import in node:
2150
2433
  0 && (module.exports = {
2434
+ BatchingErrorAdapter,
2151
2435
  CloudWatchLogsErrorAdapter,
2152
2436
  CompositeErrorAdapter,
2153
2437
  ConsoleErrorAdapter,