@richardmcquiston01/error-log-adapter 0.5.0 → 0.6.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 +55 -19
- package/dist/index.cjs +199 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +95 -2
- package/dist/index.d.ts +95 -2
- package/dist/index.mjs +198 -17
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -70,24 +70,25 @@ 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`, `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
|
-
| `
|
|
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
|
+
| `CompositeErrorAdapter([...])` | Several adapters at once; succeeds only if all succeed | Anywhere | `timeoutMs` |
|
|
91
92
|
|
|
92
93
|
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.
|
|
93
94
|
|
|
@@ -99,7 +100,8 @@ What changes durability or size, by adapter:
|
|
|
99
100
|
- **Files** (`maxBytes` / `maxFiles`, `maxEntries`): the flat file adapter rotates by size; the JSON file adapter's `array` mode keeps the newest `maxEntries`.
|
|
100
101
|
- **Redis and Web Storage** (`maxEntries`): the oldest entries are dropped past the cap.
|
|
101
102
|
- **Browser storage**: `sessionStorage` belongs to one tab. `localStorage` is shared by every tab and has no atomic update, so two tabs logging at the same instant can overwrite each other's entry. `WebStorageErrorAdapter` takes a `navigator.locks` lock around each write, which reduces the loss but cannot remove it, since tabs in separate processes can still read a stale copy. Use `IndexedDbErrorAdapter` when every entry matters. Create its object store from your `onupgradeneeded` handler with `createErrorLogStore`. A stored value the adapter cannot read is copied to `<key>.corrupt` before it is replaced.
|
|
102
|
-
- **
|
|
103
|
+
- **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.
|
|
104
|
+
- **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.
|
|
103
105
|
|
|
104
106
|
### When a sink is slow or down
|
|
105
107
|
|
|
@@ -209,6 +211,40 @@ const errorLog = new S3ErrorAdapter(s3, 'my-space', {
|
|
|
209
211
|
process.on('beforeExit', () => void errorLog.flush());
|
|
210
212
|
```
|
|
211
213
|
|
|
214
|
+
Show a toast, count errors, and keep a file copy, all from one `log` call:
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
import { EventEmitter } from 'node:events';
|
|
218
|
+
import {
|
|
219
|
+
CompositeErrorAdapter,
|
|
220
|
+
EventEmitterErrorAdapter,
|
|
221
|
+
FlatFileErrorAdapter,
|
|
222
|
+
} from '@richardmcquiston01/error-log-adapter';
|
|
223
|
+
|
|
224
|
+
const bus = new EventEmitter();
|
|
225
|
+
bus.on('error-log', (record) => showToast(record.message));
|
|
226
|
+
bus.on('error-log', (record) => metrics.increment(`errors.${record.code}`));
|
|
227
|
+
|
|
228
|
+
const errorLog = new CompositeErrorAdapter([
|
|
229
|
+
new EventEmitterErrorAdapter(bus),
|
|
230
|
+
new FlatFileErrorAdapter('logs/errors.log'),
|
|
231
|
+
]);
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
In a browser, wrap an `EventTarget`:
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
const target = new EventTarget();
|
|
238
|
+
const emitter = {
|
|
239
|
+
emit: (name: string, payload: unknown) =>
|
|
240
|
+
target.dispatchEvent(new CustomEvent(name, { detail: payload })),
|
|
241
|
+
};
|
|
242
|
+
target.addEventListener('error-log', (event) =>
|
|
243
|
+
console.log((event as CustomEvent).detail)
|
|
244
|
+
);
|
|
245
|
+
const errorLog = new EventEmitterErrorAdapter(emitter);
|
|
246
|
+
```
|
|
247
|
+
|
|
212
248
|
Write your own adapter by implementing the interface. Catch failures and return them rather than throwing:
|
|
213
249
|
|
|
214
250
|
```ts
|
package/dist/index.cjs
CHANGED
|
@@ -22,6 +22,7 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
CompositeErrorAdapter: () => CompositeErrorAdapter,
|
|
24
24
|
ConsoleErrorAdapter: () => ConsoleErrorAdapter,
|
|
25
|
+
EventEmitterErrorAdapter: () => EventEmitterErrorAdapter,
|
|
25
26
|
FirestoreErrorAdapter: () => FirestoreErrorAdapter,
|
|
26
27
|
FlatFileErrorAdapter: () => FlatFileErrorAdapter,
|
|
27
28
|
IndexedDbErrorAdapter: () => IndexedDbErrorAdapter,
|
|
@@ -278,6 +279,78 @@ function withTimeout(start, timeoutMs, message) {
|
|
|
278
279
|
}
|
|
279
280
|
|
|
280
281
|
// src/adapters/webhook-error-adapter.ts
|
|
282
|
+
var DEFAULT_RETRY_DELAY_MS = 500;
|
|
283
|
+
var DEFAULT_MAX_RETRY_DELAY_MS = 3e4;
|
|
284
|
+
var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([408, 425, 429]);
|
|
285
|
+
var SigningUnavailableError = class extends Error {
|
|
286
|
+
};
|
|
287
|
+
async function hmacSha256Hex(secret, message) {
|
|
288
|
+
const subtle = globalThis.crypto?.subtle;
|
|
289
|
+
if (subtle === void 0) {
|
|
290
|
+
throw new SigningUnavailableError(
|
|
291
|
+
"signingSecret needs Web Crypto (crypto.subtle), which this runtime does not provide, so the entry was not sent unsigned"
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
const encoder = new TextEncoder();
|
|
295
|
+
const key = await subtle.importKey(
|
|
296
|
+
"raw",
|
|
297
|
+
encoder.encode(secret),
|
|
298
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
299
|
+
false,
|
|
300
|
+
["sign"]
|
|
301
|
+
);
|
|
302
|
+
const signature = await subtle.sign(
|
|
303
|
+
"HMAC",
|
|
304
|
+
key,
|
|
305
|
+
encoder.encode(message)
|
|
306
|
+
);
|
|
307
|
+
return Array.from(
|
|
308
|
+
new Uint8Array(signature),
|
|
309
|
+
(byte) => byte.toString(16).padStart(2, "0")
|
|
310
|
+
).join("");
|
|
311
|
+
}
|
|
312
|
+
function parseRetryAfterMs(value) {
|
|
313
|
+
if (value === null || value === void 0 || value.trim() === "") {
|
|
314
|
+
return void 0;
|
|
315
|
+
}
|
|
316
|
+
const seconds = Number(value);
|
|
317
|
+
if (Number.isFinite(seconds) && seconds >= 0) {
|
|
318
|
+
return seconds * 1e3;
|
|
319
|
+
}
|
|
320
|
+
const date = Date.parse(value);
|
|
321
|
+
return Number.isNaN(date) ? void 0 : Math.max(0, date - Date.now());
|
|
322
|
+
}
|
|
323
|
+
function validateRetryOptions(options) {
|
|
324
|
+
const retries = options.retries ?? 0;
|
|
325
|
+
if (!Number.isInteger(retries) || retries < 0) {
|
|
326
|
+
throw new RangeError(
|
|
327
|
+
`WebhookErrorAdapter: retries must be a whole number of 0 or more, received ${String(options.retries)}.`
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
const retryDelayMs = options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS;
|
|
331
|
+
if (!Number.isFinite(retryDelayMs) || retryDelayMs <= 0) {
|
|
332
|
+
throw new RangeError(
|
|
333
|
+
`WebhookErrorAdapter: retryDelayMs must be a positive number, received ${String(options.retryDelayMs)}.`
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
const maxRetryDelayMs = options.maxRetryDelayMs ?? DEFAULT_MAX_RETRY_DELAY_MS;
|
|
337
|
+
if (!Number.isFinite(maxRetryDelayMs) || maxRetryDelayMs < retryDelayMs) {
|
|
338
|
+
throw new RangeError(
|
|
339
|
+
`WebhookErrorAdapter: maxRetryDelayMs must be a number no smaller than retryDelayMs (${retryDelayMs}), received ${String(options.maxRetryDelayMs)}.`
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
if (options.signingSecret === "") {
|
|
343
|
+
throw new RangeError(
|
|
344
|
+
"WebhookErrorAdapter: signingSecret must not be empty; leave it out to send unsigned."
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
return { retries, retryDelayMs, maxRetryDelayMs };
|
|
348
|
+
}
|
|
349
|
+
function sleep(ms) {
|
|
350
|
+
return new Promise((resolve) => {
|
|
351
|
+
setTimeout(resolve, ms);
|
|
352
|
+
});
|
|
353
|
+
}
|
|
281
354
|
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
|
|
282
355
|
"localhost",
|
|
283
356
|
"127.0.0.1",
|
|
@@ -303,15 +376,57 @@ function parseWebhookUrl(url, allowInsecure) {
|
|
|
303
376
|
);
|
|
304
377
|
}
|
|
305
378
|
var WebhookErrorAdapter = class {
|
|
306
|
-
/** @throws {RangeError} if `url` is not a valid `https://` URL (see `allowInsecure`) or
|
|
379
|
+
/** @throws {RangeError} if `url` is not a valid `https://` URL (see `allowInsecure`) or a timeout, retry or signing option is invalid. */
|
|
307
380
|
constructor(url, options = {}) {
|
|
308
381
|
this.url = url;
|
|
309
382
|
this.options = options;
|
|
310
383
|
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
311
384
|
this.timeoutMs = resolveTimeoutMs(options.timeoutMs, "WebhookErrorAdapter");
|
|
385
|
+
({
|
|
386
|
+
retries: this.retries,
|
|
387
|
+
retryDelayMs: this.retryDelayMs,
|
|
388
|
+
maxRetryDelayMs: this.maxRetryDelayMs
|
|
389
|
+
} = validateRetryOptions(options));
|
|
312
390
|
this.target = parseWebhookUrl(url, options.allowInsecure ?? false).origin;
|
|
313
391
|
}
|
|
314
392
|
async log(entry) {
|
|
393
|
+
let body;
|
|
394
|
+
try {
|
|
395
|
+
body = JSON.stringify(
|
|
396
|
+
hasCallerId(entry) || this.retries > 0 ? { id: resolveEntryId(entry), ...serializeEntry(entry) } : serializeEntry(entry)
|
|
397
|
+
);
|
|
398
|
+
} catch (error) {
|
|
399
|
+
console.error(`${this.prefix} WebhookErrorAdapter:`, error);
|
|
400
|
+
return this.fail(
|
|
401
|
+
`could not prepare the entry: ${this.redact(describeError(error))}`
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
let attempt = await this.send(body);
|
|
405
|
+
let attempts = 1;
|
|
406
|
+
while (!attempt.ok && attempt.retryable && attempts <= this.retries) {
|
|
407
|
+
await sleep(this.retryWaitMs(attempts, attempt.retryAfterMs));
|
|
408
|
+
attempt = await this.send(body);
|
|
409
|
+
attempts += 1;
|
|
410
|
+
}
|
|
411
|
+
if (attempt.ok) {
|
|
412
|
+
return { ok: true };
|
|
413
|
+
}
|
|
414
|
+
return this.fail(
|
|
415
|
+
attempts > 1 ? `${attempt.reason} (after ${attempts} attempts)` : attempt.reason
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
/** Milliseconds to wait before retry number `retryNumber` (1 for the first). */
|
|
419
|
+
retryWaitMs(retryNumber, retryAfterMs) {
|
|
420
|
+
if (retryAfterMs !== void 0) {
|
|
421
|
+
return Math.min(retryAfterMs, this.maxRetryDelayMs);
|
|
422
|
+
}
|
|
423
|
+
const ceiling = Math.min(
|
|
424
|
+
this.maxRetryDelayMs,
|
|
425
|
+
this.retryDelayMs * 2 ** (retryNumber - 1)
|
|
426
|
+
);
|
|
427
|
+
return ceiling / 2 + Math.random() * ceiling / 2;
|
|
428
|
+
}
|
|
429
|
+
async send(body) {
|
|
315
430
|
const controller = new AbortController();
|
|
316
431
|
let timedOut = false;
|
|
317
432
|
const timer = this.timeoutMs === Infinity ? void 0 : setTimeout(() => {
|
|
@@ -323,43 +438,70 @@ var WebhookErrorAdapter = class {
|
|
|
323
438
|
method: "POST",
|
|
324
439
|
headers: {
|
|
325
440
|
"Content-Type": "application/json",
|
|
326
|
-
...this.options.headers
|
|
441
|
+
...this.options.headers,
|
|
442
|
+
...await this.signatureHeaders(body)
|
|
327
443
|
},
|
|
328
|
-
|
|
329
|
-
body: JSON.stringify(
|
|
330
|
-
hasCallerId(entry) ? { id: resolveEntryId(entry), ...serializeEntry(entry) } : serializeEntry(entry)
|
|
331
|
-
),
|
|
444
|
+
body,
|
|
332
445
|
// A redirect would turn the POST into a GET without a body (302, 303)
|
|
333
446
|
// or re-send the entry and headers to another host (307, 308).
|
|
334
447
|
redirect: "manual",
|
|
335
448
|
signal: controller.signal
|
|
336
449
|
});
|
|
337
450
|
if (response.type === "opaqueredirect") {
|
|
338
|
-
return this.
|
|
451
|
+
return this.rejected(this.redirectMessage("a redirect"));
|
|
339
452
|
}
|
|
340
453
|
if (!response.ok) {
|
|
341
454
|
if (response.status >= 300 && response.status < 400) {
|
|
342
|
-
return this.
|
|
455
|
+
return this.rejected(this.redirectMessage(`HTTP ${response.status}`));
|
|
343
456
|
}
|
|
344
|
-
return
|
|
345
|
-
|
|
346
|
-
|
|
457
|
+
return {
|
|
458
|
+
ok: false,
|
|
459
|
+
reason: `webhook ${this.target} responded with HTTP ${response.status}`,
|
|
460
|
+
retryable: response.status >= 500 || RETRYABLE_STATUSES.has(response.status),
|
|
461
|
+
retryAfterMs: parseRetryAfterMs(response.headers?.get("retry-after"))
|
|
462
|
+
};
|
|
347
463
|
}
|
|
348
464
|
return { ok: true };
|
|
349
465
|
} catch (error) {
|
|
350
466
|
console.error(`${this.prefix} WebhookErrorAdapter:`, error);
|
|
467
|
+
if (error instanceof SigningUnavailableError) {
|
|
468
|
+
return this.rejected(error.message);
|
|
469
|
+
}
|
|
351
470
|
if (timedOut) {
|
|
352
|
-
return
|
|
353
|
-
|
|
354
|
-
|
|
471
|
+
return {
|
|
472
|
+
ok: false,
|
|
473
|
+
reason: timeoutMessage(`webhook ${this.target}`, this.timeoutMs),
|
|
474
|
+
retryable: true
|
|
475
|
+
};
|
|
355
476
|
}
|
|
356
|
-
return
|
|
357
|
-
|
|
358
|
-
|
|
477
|
+
return {
|
|
478
|
+
ok: false,
|
|
479
|
+
reason: `could not reach webhook ${this.target}: ${this.redact(describeError(error))}`,
|
|
480
|
+
retryable: true
|
|
481
|
+
};
|
|
359
482
|
} finally {
|
|
360
483
|
clearTimeout(timer);
|
|
361
484
|
}
|
|
362
485
|
}
|
|
486
|
+
/** Timestamp and signature headers, or none when no `signingSecret` is set. */
|
|
487
|
+
async signatureHeaders(body) {
|
|
488
|
+
const secret = this.options.signingSecret;
|
|
489
|
+
if (secret === void 0) {
|
|
490
|
+
return {};
|
|
491
|
+
}
|
|
492
|
+
const timestamp = String(Math.floor(Date.now() / 1e3));
|
|
493
|
+
const signature = await hmacSha256Hex(
|
|
494
|
+
secret,
|
|
495
|
+
`${timestamp}.${body}`
|
|
496
|
+
);
|
|
497
|
+
return {
|
|
498
|
+
"X-Signature-Timestamp": timestamp,
|
|
499
|
+
"X-Signature-256": `sha256=${signature}`
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
rejected(reason) {
|
|
503
|
+
return { ok: false, reason, retryable: false };
|
|
504
|
+
}
|
|
363
505
|
redirectMessage(what) {
|
|
364
506
|
return `webhook ${this.target} responded with ${what}, which is not followed because the entry would not arrive intact; use the final URL instead`;
|
|
365
507
|
}
|
|
@@ -1639,10 +1781,50 @@ var SupabaseErrorAdapter = class {
|
|
|
1639
1781
|
});
|
|
1640
1782
|
}
|
|
1641
1783
|
};
|
|
1784
|
+
|
|
1785
|
+
// src/adapters/event-emitter-error-adapter.ts
|
|
1786
|
+
var DEFAULT_EVENT_NAME = "error-log";
|
|
1787
|
+
var EventEmitterErrorAdapter = class {
|
|
1788
|
+
/** @throws {RangeError} if `options.eventName` is empty. */
|
|
1789
|
+
constructor(emitter, options = {}) {
|
|
1790
|
+
this.emitter = emitter;
|
|
1791
|
+
this.eventName = options.eventName ?? DEFAULT_EVENT_NAME;
|
|
1792
|
+
if (this.eventName === "") {
|
|
1793
|
+
throw new RangeError(
|
|
1794
|
+
"EventEmitterErrorAdapter: eventName must not be empty."
|
|
1795
|
+
);
|
|
1796
|
+
}
|
|
1797
|
+
this.requireListener = options.requireListener ?? false;
|
|
1798
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
1799
|
+
}
|
|
1800
|
+
async log(entry) {
|
|
1801
|
+
try {
|
|
1802
|
+
const record = {
|
|
1803
|
+
id: resolveEntryId(entry),
|
|
1804
|
+
...serializeEntry(entry)
|
|
1805
|
+
};
|
|
1806
|
+
const delivered = this.emitter.emit(this.eventName, record);
|
|
1807
|
+
if (this.requireListener && delivered === false) {
|
|
1808
|
+
throw new Error(
|
|
1809
|
+
`no listener is attached to "${this.eventName}", so nothing received the entry`
|
|
1810
|
+
);
|
|
1811
|
+
}
|
|
1812
|
+
return { ok: true };
|
|
1813
|
+
} catch (error) {
|
|
1814
|
+
return reportFailure({
|
|
1815
|
+
adapterName: "EventEmitterErrorAdapter",
|
|
1816
|
+
prefix: this.prefix,
|
|
1817
|
+
action: `to emit "${this.eventName}"`,
|
|
1818
|
+
error
|
|
1819
|
+
});
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
};
|
|
1642
1823
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1643
1824
|
0 && (module.exports = {
|
|
1644
1825
|
CompositeErrorAdapter,
|
|
1645
1826
|
ConsoleErrorAdapter,
|
|
1827
|
+
EventEmitterErrorAdapter,
|
|
1646
1828
|
FirestoreErrorAdapter,
|
|
1647
1829
|
FlatFileErrorAdapter,
|
|
1648
1830
|
IndexedDbErrorAdapter,
|