@richardmcquiston01/error-log-adapter 0.6.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
@@ -88,6 +88,7 @@ Every adapter takes an optional `prefix` (default `[error-log-adapter]`) that is
88
88
  | `IndexedDbErrorAdapter(database)` | An IndexedDB object store | Browser | `storeName`, `timeoutMs` |
89
89
  | `WebStorageErrorAdapter(storage)` | `sessionStorage` or `localStorage`, as one capped array | Browser | `key`, `maxEntries`, `locks` |
90
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) |
91
92
  | `CompositeErrorAdapter([...])` | Several adapters at once; succeeds only if all succeed | Anywhere | `timeoutMs` |
92
93
 
93
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.
@@ -101,6 +102,7 @@ What changes durability or size, by adapter:
101
102
  - **Redis and Web Storage** (`maxEntries`): the oldest entries are dropped past the cap.
102
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.
103
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.
104
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.
105
107
 
106
108
  ### When a sink is slow or down
@@ -245,6 +247,16 @@ target.addEventListener('error-log', (event) =>
245
247
  const errorLog = new EventEmitterErrorAdapter(emitter);
246
248
  ```
247
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
+
248
260
  Write your own adapter by implementing the interface. Catch failures and return them rather than throwing:
249
261
 
250
262
  ```ts
package/dist/index.cjs CHANGED
@@ -35,6 +35,7 @@ __export(index_exports, {
35
35
  S3ErrorAdapter: () => S3ErrorAdapter,
36
36
  SqliteErrorAdapter: () => SqliteErrorAdapter,
37
37
  SupabaseErrorAdapter: () => SupabaseErrorAdapter,
38
+ WebSocketErrorAdapter: () => WebSocketErrorAdapter,
38
39
  WebStorageErrorAdapter: () => WebStorageErrorAdapter,
39
40
  WebhookErrorAdapter: () => WebhookErrorAdapter,
40
41
  buildCreateErrorLogTableSql: () => buildCreateErrorLogTableSql,
@@ -1820,6 +1821,42 @@ var EventEmitterErrorAdapter = class {
1820
1821
  }
1821
1822
  }
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
+ };
1823
1860
  // Annotate the CommonJS export names for ESM import in node:
1824
1861
  0 && (module.exports = {
1825
1862
  CompositeErrorAdapter,
@@ -1837,6 +1874,7 @@ var EventEmitterErrorAdapter = class {
1837
1874
  S3ErrorAdapter,
1838
1875
  SqliteErrorAdapter,
1839
1876
  SupabaseErrorAdapter,
1877
+ WebSocketErrorAdapter,
1840
1878
  WebStorageErrorAdapter,
1841
1879
  WebhookErrorAdapter,
1842
1880
  buildCreateErrorLogTableSql,