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