@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/dist/index.d.cts CHANGED
@@ -89,17 +89,50 @@ interface WebhookErrorOptions {
89
89
  * still have received the entry.
90
90
  */
91
91
  timeoutMs?: number;
92
+ /**
93
+ * How many times to try again after a failure that may go away on its own: a
94
+ * network error, a timeout, HTTP 408, 425, 429 or any 5xx. Other 4xx answers and
95
+ * redirects are never retried. Defaults to 0 (send once). The wait doubles
96
+ * after each attempt, starting at `retryDelayMs`, with jitter, and a
97
+ * `Retry-After` header is honored. `timeoutMs` applies to each attempt, and
98
+ * `log()` resolves only after the last one. Every attempt sends the same body,
99
+ * and with `retries` above 0 the body always carries an `id`, so the receiver
100
+ * can drop a repeat if an earlier attempt did arrive.
101
+ */
102
+ retries?: number;
103
+ /** Wait before the first retry, in milliseconds. Defaults to 500. */
104
+ retryDelayMs?: number;
105
+ /** Longest wait between attempts, in milliseconds. Defaults to 30000. */
106
+ maxRetryDelayMs?: number;
107
+ /**
108
+ * Shared secret for signing each request, so the receiver can verify the
109
+ * entry came from you and was not altered. Adds `X-Signature-Timestamp`
110
+ * (Unix seconds) and `X-Signature-256` (`sha256=` and the hex HMAC-SHA256 of
111
+ * `<timestamp>.<body>`). The receiver should recompute it and reject old
112
+ * timestamps. Needs Web Crypto (Node 20+, browsers, Deno, Bun); without it
113
+ * `log()` fails rather than sending an unsigned entry.
114
+ */
115
+ signingSecret?: string;
92
116
  }
93
117
  declare class WebhookErrorAdapter<TCode extends string = string> implements ErrorLogAdapter<TCode> {
94
118
  private readonly url;
95
119
  private readonly options;
96
120
  private readonly prefix;
97
121
  private readonly timeoutMs;
122
+ private readonly retries;
123
+ private readonly retryDelayMs;
124
+ private readonly maxRetryDelayMs;
98
125
  /** Scheme and host only. Webhook URLs often carry a secret in the path or query. */
99
126
  private readonly target;
100
- /** @throws {RangeError} if `url` is not a valid `https://` URL (see `allowInsecure`) or `timeoutMs` is invalid. */
127
+ /** @throws {RangeError} if `url` is not a valid `https://` URL (see `allowInsecure`) or a timeout, retry or signing option is invalid. */
101
128
  constructor(url: string, options?: WebhookErrorOptions);
102
129
  log(entry: ErrorLogEntry<TCode>): Promise<ErrorLogResult>;
130
+ /** Milliseconds to wait before retry number `retryNumber` (1 for the first). */
131
+ private retryWaitMs;
132
+ private send;
133
+ /** Timestamp and signature headers, or none when no `signingSecret` is set. */
134
+ private signatureHeaders;
135
+ private rejected;
103
136
  private redirectMessage;
104
137
  /** Replaces the full URL in a message with its origin, in case the runtime put it there. */
105
138
  private redact;
@@ -929,4 +962,101 @@ declare class SupabaseErrorAdapter<TCode extends string = string> implements Err
929
962
  private fail;
930
963
  }
931
964
 
932
- export { type CollectionLike, CompositeErrorAdapter, type CompositeErrorOptions, ConsoleErrorAdapter, type ConsoleErrorOptions, type ErrorLogAdapter, type ErrorLogEntry, type ErrorLogResult, type FileSystemLike, type FirebaseErrorOptions, type FirebaseErrorRecord, type FirestoreCollectionLike, FirestoreErrorAdapter, FlatFileErrorAdapter, type FlatFileErrorOptions, type IdbDatabaseLike, type IdbObjectStoreLike, type IdbRequestLike, type IdbTransactionLike, type IdbUpgradeDatabaseLike, IndexedDbErrorAdapter, type IndexedDbErrorOptions, type IndexedDbErrorRecord, JsonFileErrorAdapter, type JsonFileErrorOptions, type JsonFileErrorRecord, type LockManagerLike, MongoDbErrorAdapter, type MongoDbErrorOptions, MySqlErrorAdapter, PostgresErrorAdapter, type ReadWriteFileSystemLike, RealtimeDatabaseErrorAdapter, type RealtimeDatabaseRefLike, type RedisClientLike, RedisErrorAdapter, type RedisErrorOptions, type S3BatchOptions, type S3ClientLike, S3ErrorAdapter, type S3ErrorOptions, type S3PutObjectInput, type SerializedErrorLogEntry, type SqlClientLike, type SqlDialect, type SqlErrorOptions, type SqliteClientLike, SqliteErrorAdapter, type SqliteErrorOptions, type StorageLike, type SupabaseClientLike, SupabaseErrorAdapter, type SupabaseErrorLike, type SupabaseErrorOptions, type SupabaseErrorRow, type SupabaseInsertResult, type SupabaseSetupOptions, WebStorageErrorAdapter, type WebStorageErrorOptions, type WebStorageErrorRecord, WebhookErrorAdapter, type WebhookErrorOptions, buildCreateErrorLogTableSql, buildErrorLogTableDdl, buildMySqlWidenColumnsSql, buildSupabaseSetupSql, createErrorLogStore, formatErrorLogLine, serializeCause, serializeEntry };
965
+ /** What listeners receive: the serialized entry plus its UUID. */
966
+ interface EventEmitterErrorRecord extends SerializedErrorLogEntry {
967
+ id: string;
968
+ }
969
+ /**
970
+ * The only thing this adapter needs from an emitter: `emit(eventName, payload)`.
971
+ * Node's `EventEmitter` and mitt-style emitters fit as they are. A browser
972
+ * `EventTarget` needs a one-line wrapper:
973
+ *
974
+ * ```ts
975
+ * const target = new EventTarget();
976
+ * const emitter: EmitterLike = {
977
+ * emit: (name, payload) =>
978
+ * target.dispatchEvent(new CustomEvent(name, { detail: payload })),
979
+ * };
980
+ * ```
981
+ */
982
+ interface EmitterLike {
983
+ emit(eventName: string, payload: EventEmitterErrorRecord): unknown;
984
+ }
985
+ interface EventEmitterErrorOptions {
986
+ /**
987
+ * Name of the event to emit. Defaults to `error-log`. Avoid `error` on a Node
988
+ * `EventEmitter`: it throws when no listener is attached, and `log` would then
989
+ * return `ok: false`.
990
+ */
991
+ eventName?: string;
992
+ /**
993
+ * Treat an emit that reached nobody as a failure. Node's `EventEmitter.emit`
994
+ * returns `false` when there are no listeners; with this set to `true`, `log`
995
+ * then returns `ok: false` instead of letting the entry vanish. Emitters whose
996
+ * `emit` returns anything else (mitt, an `EventTarget` wrapper) cannot tell,
997
+ * so they always count as delivered. Defaults to `false`.
998
+ */
999
+ requireListener?: boolean;
1000
+ /** Text printed before failure messages. Defaults to `[error-log-adapter]`. */
1001
+ prefix?: string;
1002
+ }
1003
+ /**
1004
+ * Emits each entry as an event on an emitter you own, for in-process hooks such
1005
+ * as a toast, a metrics counter, or a test assertion. Listeners run
1006
+ * synchronously inside `emit`, so a listener that throws makes `log` return
1007
+ * `ok: false` with the listener's message; the application is not affected.
1008
+ *
1009
+ * ```ts
1010
+ * const bus = new EventEmitter();
1011
+ * bus.on('error-log', (record) => showToast(record.message));
1012
+ * const adapter = new EventEmitterErrorAdapter(bus);
1013
+ * ```
1014
+ */
1015
+ declare class EventEmitterErrorAdapter<TCode extends string = string> implements ErrorLogAdapter<TCode> {
1016
+ private readonly emitter;
1017
+ private readonly eventName;
1018
+ private readonly requireListener;
1019
+ private readonly prefix;
1020
+ /** @throws {RangeError} if `options.eventName` is empty. */
1021
+ constructor(emitter: EmitterLike, options?: EventEmitterErrorOptions);
1022
+ log(entry: ErrorLogEntry<TCode>): Promise<ErrorLogResult>;
1023
+ }
1024
+
1025
+ /**
1026
+ * The only things this adapter needs from a socket: `send` and `readyState`. The
1027
+ * browser `WebSocket`, Node's global `WebSocket`, Deno, Bun, and the `ws` package
1028
+ * all fit as they are.
1029
+ */
1030
+ interface WebSocketLike {
1031
+ send(data: string): unknown;
1032
+ readonly readyState: number;
1033
+ }
1034
+ interface WebSocketErrorOptions {
1035
+ /** Text printed before failure messages. Defaults to `[error-log-adapter]`. */
1036
+ prefix?: string;
1037
+ }
1038
+ /**
1039
+ * Sends each entry as one JSON text message (the serialized entry plus its UUID
1040
+ * `id`) over a socket you own. You connect, and reconnect, the socket; this
1041
+ * adapter never opens one and keeps no buffer. If the socket is not open when
1042
+ * `log` is called, the entry is not sent and `log` returns `ok: false`, so
1043
+ * combine it with a durable adapter in a `CompositeErrorAdapter` when entries
1044
+ * must not be lost while the socket is down.
1045
+ *
1046
+ * `ok: true` means the socket accepted the message, not that the server
1047
+ * received it: a WebSocket gives no acknowledgement, and a connection that
1048
+ * drops after `send` loses the message.
1049
+ *
1050
+ * ```ts
1051
+ * const socket = new WebSocket('wss://logs.example.com/errors');
1052
+ * const adapter = new WebSocketErrorAdapter(socket);
1053
+ * ```
1054
+ */
1055
+ declare class WebSocketErrorAdapter<TCode extends string = string> implements ErrorLogAdapter<TCode> {
1056
+ private readonly socket;
1057
+ private readonly prefix;
1058
+ constructor(socket: WebSocketLike, options?: WebSocketErrorOptions);
1059
+ log(entry: ErrorLogEntry<TCode>): Promise<ErrorLogResult>;
1060
+ }
1061
+
1062
+ export { type CollectionLike, CompositeErrorAdapter, type CompositeErrorOptions, ConsoleErrorAdapter, type ConsoleErrorOptions, type EmitterLike, type ErrorLogAdapter, type ErrorLogEntry, type ErrorLogResult, EventEmitterErrorAdapter, type EventEmitterErrorOptions, type EventEmitterErrorRecord, type FileSystemLike, type FirebaseErrorOptions, type FirebaseErrorRecord, type FirestoreCollectionLike, FirestoreErrorAdapter, FlatFileErrorAdapter, type FlatFileErrorOptions, type IdbDatabaseLike, type IdbObjectStoreLike, type IdbRequestLike, type IdbTransactionLike, type IdbUpgradeDatabaseLike, IndexedDbErrorAdapter, type IndexedDbErrorOptions, type IndexedDbErrorRecord, JsonFileErrorAdapter, type JsonFileErrorOptions, type JsonFileErrorRecord, type LockManagerLike, MongoDbErrorAdapter, type MongoDbErrorOptions, MySqlErrorAdapter, PostgresErrorAdapter, type ReadWriteFileSystemLike, RealtimeDatabaseErrorAdapter, type RealtimeDatabaseRefLike, type RedisClientLike, RedisErrorAdapter, type RedisErrorOptions, type S3BatchOptions, type S3ClientLike, S3ErrorAdapter, type S3ErrorOptions, type S3PutObjectInput, type SerializedErrorLogEntry, type SqlClientLike, type SqlDialect, type SqlErrorOptions, type SqliteClientLike, SqliteErrorAdapter, type SqliteErrorOptions, type StorageLike, type SupabaseClientLike, SupabaseErrorAdapter, type SupabaseErrorLike, type SupabaseErrorOptions, type SupabaseErrorRow, type SupabaseInsertResult, type SupabaseSetupOptions, WebSocketErrorAdapter, type WebSocketErrorOptions, type WebSocketLike, WebStorageErrorAdapter, type WebStorageErrorOptions, type WebStorageErrorRecord, WebhookErrorAdapter, type WebhookErrorOptions, buildCreateErrorLogTableSql, buildErrorLogTableDdl, buildMySqlWidenColumnsSql, buildSupabaseSetupSql, createErrorLogStore, formatErrorLogLine, serializeCause, serializeEntry };
package/dist/index.d.ts CHANGED
@@ -89,17 +89,50 @@ interface WebhookErrorOptions {
89
89
  * still have received the entry.
90
90
  */
91
91
  timeoutMs?: number;
92
+ /**
93
+ * How many times to try again after a failure that may go away on its own: a
94
+ * network error, a timeout, HTTP 408, 425, 429 or any 5xx. Other 4xx answers and
95
+ * redirects are never retried. Defaults to 0 (send once). The wait doubles
96
+ * after each attempt, starting at `retryDelayMs`, with jitter, and a
97
+ * `Retry-After` header is honored. `timeoutMs` applies to each attempt, and
98
+ * `log()` resolves only after the last one. Every attempt sends the same body,
99
+ * and with `retries` above 0 the body always carries an `id`, so the receiver
100
+ * can drop a repeat if an earlier attempt did arrive.
101
+ */
102
+ retries?: number;
103
+ /** Wait before the first retry, in milliseconds. Defaults to 500. */
104
+ retryDelayMs?: number;
105
+ /** Longest wait between attempts, in milliseconds. Defaults to 30000. */
106
+ maxRetryDelayMs?: number;
107
+ /**
108
+ * Shared secret for signing each request, so the receiver can verify the
109
+ * entry came from you and was not altered. Adds `X-Signature-Timestamp`
110
+ * (Unix seconds) and `X-Signature-256` (`sha256=` and the hex HMAC-SHA256 of
111
+ * `<timestamp>.<body>`). The receiver should recompute it and reject old
112
+ * timestamps. Needs Web Crypto (Node 20+, browsers, Deno, Bun); without it
113
+ * `log()` fails rather than sending an unsigned entry.
114
+ */
115
+ signingSecret?: string;
92
116
  }
93
117
  declare class WebhookErrorAdapter<TCode extends string = string> implements ErrorLogAdapter<TCode> {
94
118
  private readonly url;
95
119
  private readonly options;
96
120
  private readonly prefix;
97
121
  private readonly timeoutMs;
122
+ private readonly retries;
123
+ private readonly retryDelayMs;
124
+ private readonly maxRetryDelayMs;
98
125
  /** Scheme and host only. Webhook URLs often carry a secret in the path or query. */
99
126
  private readonly target;
100
- /** @throws {RangeError} if `url` is not a valid `https://` URL (see `allowInsecure`) or `timeoutMs` is invalid. */
127
+ /** @throws {RangeError} if `url` is not a valid `https://` URL (see `allowInsecure`) or a timeout, retry or signing option is invalid. */
101
128
  constructor(url: string, options?: WebhookErrorOptions);
102
129
  log(entry: ErrorLogEntry<TCode>): Promise<ErrorLogResult>;
130
+ /** Milliseconds to wait before retry number `retryNumber` (1 for the first). */
131
+ private retryWaitMs;
132
+ private send;
133
+ /** Timestamp and signature headers, or none when no `signingSecret` is set. */
134
+ private signatureHeaders;
135
+ private rejected;
103
136
  private redirectMessage;
104
137
  /** Replaces the full URL in a message with its origin, in case the runtime put it there. */
105
138
  private redact;
@@ -929,4 +962,101 @@ declare class SupabaseErrorAdapter<TCode extends string = string> implements Err
929
962
  private fail;
930
963
  }
931
964
 
932
- export { type CollectionLike, CompositeErrorAdapter, type CompositeErrorOptions, ConsoleErrorAdapter, type ConsoleErrorOptions, type ErrorLogAdapter, type ErrorLogEntry, type ErrorLogResult, type FileSystemLike, type FirebaseErrorOptions, type FirebaseErrorRecord, type FirestoreCollectionLike, FirestoreErrorAdapter, FlatFileErrorAdapter, type FlatFileErrorOptions, type IdbDatabaseLike, type IdbObjectStoreLike, type IdbRequestLike, type IdbTransactionLike, type IdbUpgradeDatabaseLike, IndexedDbErrorAdapter, type IndexedDbErrorOptions, type IndexedDbErrorRecord, JsonFileErrorAdapter, type JsonFileErrorOptions, type JsonFileErrorRecord, type LockManagerLike, MongoDbErrorAdapter, type MongoDbErrorOptions, MySqlErrorAdapter, PostgresErrorAdapter, type ReadWriteFileSystemLike, RealtimeDatabaseErrorAdapter, type RealtimeDatabaseRefLike, type RedisClientLike, RedisErrorAdapter, type RedisErrorOptions, type S3BatchOptions, type S3ClientLike, S3ErrorAdapter, type S3ErrorOptions, type S3PutObjectInput, type SerializedErrorLogEntry, type SqlClientLike, type SqlDialect, type SqlErrorOptions, type SqliteClientLike, SqliteErrorAdapter, type SqliteErrorOptions, type StorageLike, type SupabaseClientLike, SupabaseErrorAdapter, type SupabaseErrorLike, type SupabaseErrorOptions, type SupabaseErrorRow, type SupabaseInsertResult, type SupabaseSetupOptions, WebStorageErrorAdapter, type WebStorageErrorOptions, type WebStorageErrorRecord, WebhookErrorAdapter, type WebhookErrorOptions, buildCreateErrorLogTableSql, buildErrorLogTableDdl, buildMySqlWidenColumnsSql, buildSupabaseSetupSql, createErrorLogStore, formatErrorLogLine, serializeCause, serializeEntry };
965
+ /** What listeners receive: the serialized entry plus its UUID. */
966
+ interface EventEmitterErrorRecord extends SerializedErrorLogEntry {
967
+ id: string;
968
+ }
969
+ /**
970
+ * The only thing this adapter needs from an emitter: `emit(eventName, payload)`.
971
+ * Node's `EventEmitter` and mitt-style emitters fit as they are. A browser
972
+ * `EventTarget` needs a one-line wrapper:
973
+ *
974
+ * ```ts
975
+ * const target = new EventTarget();
976
+ * const emitter: EmitterLike = {
977
+ * emit: (name, payload) =>
978
+ * target.dispatchEvent(new CustomEvent(name, { detail: payload })),
979
+ * };
980
+ * ```
981
+ */
982
+ interface EmitterLike {
983
+ emit(eventName: string, payload: EventEmitterErrorRecord): unknown;
984
+ }
985
+ interface EventEmitterErrorOptions {
986
+ /**
987
+ * Name of the event to emit. Defaults to `error-log`. Avoid `error` on a Node
988
+ * `EventEmitter`: it throws when no listener is attached, and `log` would then
989
+ * return `ok: false`.
990
+ */
991
+ eventName?: string;
992
+ /**
993
+ * Treat an emit that reached nobody as a failure. Node's `EventEmitter.emit`
994
+ * returns `false` when there are no listeners; with this set to `true`, `log`
995
+ * then returns `ok: false` instead of letting the entry vanish. Emitters whose
996
+ * `emit` returns anything else (mitt, an `EventTarget` wrapper) cannot tell,
997
+ * so they always count as delivered. Defaults to `false`.
998
+ */
999
+ requireListener?: boolean;
1000
+ /** Text printed before failure messages. Defaults to `[error-log-adapter]`. */
1001
+ prefix?: string;
1002
+ }
1003
+ /**
1004
+ * Emits each entry as an event on an emitter you own, for in-process hooks such
1005
+ * as a toast, a metrics counter, or a test assertion. Listeners run
1006
+ * synchronously inside `emit`, so a listener that throws makes `log` return
1007
+ * `ok: false` with the listener's message; the application is not affected.
1008
+ *
1009
+ * ```ts
1010
+ * const bus = new EventEmitter();
1011
+ * bus.on('error-log', (record) => showToast(record.message));
1012
+ * const adapter = new EventEmitterErrorAdapter(bus);
1013
+ * ```
1014
+ */
1015
+ declare class EventEmitterErrorAdapter<TCode extends string = string> implements ErrorLogAdapter<TCode> {
1016
+ private readonly emitter;
1017
+ private readonly eventName;
1018
+ private readonly requireListener;
1019
+ private readonly prefix;
1020
+ /** @throws {RangeError} if `options.eventName` is empty. */
1021
+ constructor(emitter: EmitterLike, options?: EventEmitterErrorOptions);
1022
+ log(entry: ErrorLogEntry<TCode>): Promise<ErrorLogResult>;
1023
+ }
1024
+
1025
+ /**
1026
+ * The only things this adapter needs from a socket: `send` and `readyState`. The
1027
+ * browser `WebSocket`, Node's global `WebSocket`, Deno, Bun, and the `ws` package
1028
+ * all fit as they are.
1029
+ */
1030
+ interface WebSocketLike {
1031
+ send(data: string): unknown;
1032
+ readonly readyState: number;
1033
+ }
1034
+ interface WebSocketErrorOptions {
1035
+ /** Text printed before failure messages. Defaults to `[error-log-adapter]`. */
1036
+ prefix?: string;
1037
+ }
1038
+ /**
1039
+ * Sends each entry as one JSON text message (the serialized entry plus its UUID
1040
+ * `id`) over a socket you own. You connect, and reconnect, the socket; this
1041
+ * adapter never opens one and keeps no buffer. If the socket is not open when
1042
+ * `log` is called, the entry is not sent and `log` returns `ok: false`, so
1043
+ * combine it with a durable adapter in a `CompositeErrorAdapter` when entries
1044
+ * must not be lost while the socket is down.
1045
+ *
1046
+ * `ok: true` means the socket accepted the message, not that the server
1047
+ * received it: a WebSocket gives no acknowledgement, and a connection that
1048
+ * drops after `send` loses the message.
1049
+ *
1050
+ * ```ts
1051
+ * const socket = new WebSocket('wss://logs.example.com/errors');
1052
+ * const adapter = new WebSocketErrorAdapter(socket);
1053
+ * ```
1054
+ */
1055
+ declare class WebSocketErrorAdapter<TCode extends string = string> implements ErrorLogAdapter<TCode> {
1056
+ private readonly socket;
1057
+ private readonly prefix;
1058
+ constructor(socket: WebSocketLike, options?: WebSocketErrorOptions);
1059
+ log(entry: ErrorLogEntry<TCode>): Promise<ErrorLogResult>;
1060
+ }
1061
+
1062
+ export { type CollectionLike, CompositeErrorAdapter, type CompositeErrorOptions, ConsoleErrorAdapter, type ConsoleErrorOptions, type EmitterLike, type ErrorLogAdapter, type ErrorLogEntry, type ErrorLogResult, EventEmitterErrorAdapter, type EventEmitterErrorOptions, type EventEmitterErrorRecord, type FileSystemLike, type FirebaseErrorOptions, type FirebaseErrorRecord, type FirestoreCollectionLike, FirestoreErrorAdapter, FlatFileErrorAdapter, type FlatFileErrorOptions, type IdbDatabaseLike, type IdbObjectStoreLike, type IdbRequestLike, type IdbTransactionLike, type IdbUpgradeDatabaseLike, IndexedDbErrorAdapter, type IndexedDbErrorOptions, type IndexedDbErrorRecord, JsonFileErrorAdapter, type JsonFileErrorOptions, type JsonFileErrorRecord, type LockManagerLike, MongoDbErrorAdapter, type MongoDbErrorOptions, MySqlErrorAdapter, PostgresErrorAdapter, type ReadWriteFileSystemLike, RealtimeDatabaseErrorAdapter, type RealtimeDatabaseRefLike, type RedisClientLike, RedisErrorAdapter, type RedisErrorOptions, type S3BatchOptions, type S3ClientLike, S3ErrorAdapter, type S3ErrorOptions, type S3PutObjectInput, type SerializedErrorLogEntry, type SqlClientLike, type SqlDialect, type SqlErrorOptions, type SqliteClientLike, SqliteErrorAdapter, type SqliteErrorOptions, type StorageLike, type SupabaseClientLike, SupabaseErrorAdapter, type SupabaseErrorLike, type SupabaseErrorOptions, type SupabaseErrorRow, type SupabaseInsertResult, type SupabaseSetupOptions, WebSocketErrorAdapter, type WebSocketErrorOptions, type WebSocketLike, WebStorageErrorAdapter, type WebStorageErrorOptions, type WebStorageErrorRecord, WebhookErrorAdapter, type WebhookErrorOptions, buildCreateErrorLogTableSql, buildErrorLogTableDdl, buildMySqlWidenColumnsSql, buildSupabaseSetupSql, createErrorLogStore, formatErrorLogLine, serializeCause, serializeEntry };
package/dist/index.mjs CHANGED
@@ -229,6 +229,78 @@ function withTimeout(start, timeoutMs, message) {
229
229
  }
230
230
 
231
231
  // src/adapters/webhook-error-adapter.ts
232
+ var DEFAULT_RETRY_DELAY_MS = 500;
233
+ var DEFAULT_MAX_RETRY_DELAY_MS = 3e4;
234
+ var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([408, 425, 429]);
235
+ var SigningUnavailableError = class extends Error {
236
+ };
237
+ async function hmacSha256Hex(secret, message) {
238
+ const subtle = globalThis.crypto?.subtle;
239
+ if (subtle === void 0) {
240
+ throw new SigningUnavailableError(
241
+ "signingSecret needs Web Crypto (crypto.subtle), which this runtime does not provide, so the entry was not sent unsigned"
242
+ );
243
+ }
244
+ const encoder = new TextEncoder();
245
+ const key = await subtle.importKey(
246
+ "raw",
247
+ encoder.encode(secret),
248
+ { name: "HMAC", hash: "SHA-256" },
249
+ false,
250
+ ["sign"]
251
+ );
252
+ const signature = await subtle.sign(
253
+ "HMAC",
254
+ key,
255
+ encoder.encode(message)
256
+ );
257
+ return Array.from(
258
+ new Uint8Array(signature),
259
+ (byte) => byte.toString(16).padStart(2, "0")
260
+ ).join("");
261
+ }
262
+ function parseRetryAfterMs(value) {
263
+ if (value === null || value === void 0 || value.trim() === "") {
264
+ return void 0;
265
+ }
266
+ const seconds = Number(value);
267
+ if (Number.isFinite(seconds) && seconds >= 0) {
268
+ return seconds * 1e3;
269
+ }
270
+ const date = Date.parse(value);
271
+ return Number.isNaN(date) ? void 0 : Math.max(0, date - Date.now());
272
+ }
273
+ function validateRetryOptions(options) {
274
+ const retries = options.retries ?? 0;
275
+ if (!Number.isInteger(retries) || retries < 0) {
276
+ throw new RangeError(
277
+ `WebhookErrorAdapter: retries must be a whole number of 0 or more, received ${String(options.retries)}.`
278
+ );
279
+ }
280
+ const retryDelayMs = options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS;
281
+ if (!Number.isFinite(retryDelayMs) || retryDelayMs <= 0) {
282
+ throw new RangeError(
283
+ `WebhookErrorAdapter: retryDelayMs must be a positive number, received ${String(options.retryDelayMs)}.`
284
+ );
285
+ }
286
+ const maxRetryDelayMs = options.maxRetryDelayMs ?? DEFAULT_MAX_RETRY_DELAY_MS;
287
+ if (!Number.isFinite(maxRetryDelayMs) || maxRetryDelayMs < retryDelayMs) {
288
+ throw new RangeError(
289
+ `WebhookErrorAdapter: maxRetryDelayMs must be a number no smaller than retryDelayMs (${retryDelayMs}), received ${String(options.maxRetryDelayMs)}.`
290
+ );
291
+ }
292
+ if (options.signingSecret === "") {
293
+ throw new RangeError(
294
+ "WebhookErrorAdapter: signingSecret must not be empty; leave it out to send unsigned."
295
+ );
296
+ }
297
+ return { retries, retryDelayMs, maxRetryDelayMs };
298
+ }
299
+ function sleep(ms) {
300
+ return new Promise((resolve) => {
301
+ setTimeout(resolve, ms);
302
+ });
303
+ }
232
304
  var LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
233
305
  "localhost",
234
306
  "127.0.0.1",
@@ -254,15 +326,57 @@ function parseWebhookUrl(url, allowInsecure) {
254
326
  );
255
327
  }
256
328
  var WebhookErrorAdapter = class {
257
- /** @throws {RangeError} if `url` is not a valid `https://` URL (see `allowInsecure`) or `timeoutMs` is invalid. */
329
+ /** @throws {RangeError} if `url` is not a valid `https://` URL (see `allowInsecure`) or a timeout, retry or signing option is invalid. */
258
330
  constructor(url, options = {}) {
259
331
  this.url = url;
260
332
  this.options = options;
261
333
  this.prefix = options.prefix ?? DEFAULT_PREFIX;
262
334
  this.timeoutMs = resolveTimeoutMs(options.timeoutMs, "WebhookErrorAdapter");
335
+ ({
336
+ retries: this.retries,
337
+ retryDelayMs: this.retryDelayMs,
338
+ maxRetryDelayMs: this.maxRetryDelayMs
339
+ } = validateRetryOptions(options));
263
340
  this.target = parseWebhookUrl(url, options.allowInsecure ?? false).origin;
264
341
  }
265
342
  async log(entry) {
343
+ let body;
344
+ try {
345
+ body = JSON.stringify(
346
+ hasCallerId(entry) || this.retries > 0 ? { id: resolveEntryId(entry), ...serializeEntry(entry) } : serializeEntry(entry)
347
+ );
348
+ } catch (error) {
349
+ console.error(`${this.prefix} WebhookErrorAdapter:`, error);
350
+ return this.fail(
351
+ `could not prepare the entry: ${this.redact(describeError(error))}`
352
+ );
353
+ }
354
+ let attempt = await this.send(body);
355
+ let attempts = 1;
356
+ while (!attempt.ok && attempt.retryable && attempts <= this.retries) {
357
+ await sleep(this.retryWaitMs(attempts, attempt.retryAfterMs));
358
+ attempt = await this.send(body);
359
+ attempts += 1;
360
+ }
361
+ if (attempt.ok) {
362
+ return { ok: true };
363
+ }
364
+ return this.fail(
365
+ attempts > 1 ? `${attempt.reason} (after ${attempts} attempts)` : attempt.reason
366
+ );
367
+ }
368
+ /** Milliseconds to wait before retry number `retryNumber` (1 for the first). */
369
+ retryWaitMs(retryNumber, retryAfterMs) {
370
+ if (retryAfterMs !== void 0) {
371
+ return Math.min(retryAfterMs, this.maxRetryDelayMs);
372
+ }
373
+ const ceiling = Math.min(
374
+ this.maxRetryDelayMs,
375
+ this.retryDelayMs * 2 ** (retryNumber - 1)
376
+ );
377
+ return ceiling / 2 + Math.random() * ceiling / 2;
378
+ }
379
+ async send(body) {
266
380
  const controller = new AbortController();
267
381
  let timedOut = false;
268
382
  const timer = this.timeoutMs === Infinity ? void 0 : setTimeout(() => {
@@ -274,43 +388,70 @@ var WebhookErrorAdapter = class {
274
388
  method: "POST",
275
389
  headers: {
276
390
  "Content-Type": "application/json",
277
- ...this.options.headers
391
+ ...this.options.headers,
392
+ ...await this.signatureHeaders(body)
278
393
  },
279
- // A caller-supplied id goes along so the receiver can drop a repeat.
280
- body: JSON.stringify(
281
- hasCallerId(entry) ? { id: resolveEntryId(entry), ...serializeEntry(entry) } : serializeEntry(entry)
282
- ),
394
+ body,
283
395
  // A redirect would turn the POST into a GET without a body (302, 303)
284
396
  // or re-send the entry and headers to another host (307, 308).
285
397
  redirect: "manual",
286
398
  signal: controller.signal
287
399
  });
288
400
  if (response.type === "opaqueredirect") {
289
- return this.fail(this.redirectMessage("a redirect"));
401
+ return this.rejected(this.redirectMessage("a redirect"));
290
402
  }
291
403
  if (!response.ok) {
292
404
  if (response.status >= 300 && response.status < 400) {
293
- return this.fail(this.redirectMessage(`HTTP ${response.status}`));
405
+ return this.rejected(this.redirectMessage(`HTTP ${response.status}`));
294
406
  }
295
- return this.fail(
296
- `webhook ${this.target} responded with HTTP ${response.status}`
297
- );
407
+ return {
408
+ ok: false,
409
+ reason: `webhook ${this.target} responded with HTTP ${response.status}`,
410
+ retryable: response.status >= 500 || RETRYABLE_STATUSES.has(response.status),
411
+ retryAfterMs: parseRetryAfterMs(response.headers?.get("retry-after"))
412
+ };
298
413
  }
299
414
  return { ok: true };
300
415
  } catch (error) {
301
416
  console.error(`${this.prefix} WebhookErrorAdapter:`, error);
417
+ if (error instanceof SigningUnavailableError) {
418
+ return this.rejected(error.message);
419
+ }
302
420
  if (timedOut) {
303
- return this.fail(
304
- timeoutMessage(`webhook ${this.target}`, this.timeoutMs)
305
- );
421
+ return {
422
+ ok: false,
423
+ reason: timeoutMessage(`webhook ${this.target}`, this.timeoutMs),
424
+ retryable: true
425
+ };
306
426
  }
307
- return this.fail(
308
- `could not reach webhook ${this.target}: ${this.redact(describeError(error))}`
309
- );
427
+ return {
428
+ ok: false,
429
+ reason: `could not reach webhook ${this.target}: ${this.redact(describeError(error))}`,
430
+ retryable: true
431
+ };
310
432
  } finally {
311
433
  clearTimeout(timer);
312
434
  }
313
435
  }
436
+ /** Timestamp and signature headers, or none when no `signingSecret` is set. */
437
+ async signatureHeaders(body) {
438
+ const secret = this.options.signingSecret;
439
+ if (secret === void 0) {
440
+ return {};
441
+ }
442
+ const timestamp = String(Math.floor(Date.now() / 1e3));
443
+ const signature = await hmacSha256Hex(
444
+ secret,
445
+ `${timestamp}.${body}`
446
+ );
447
+ return {
448
+ "X-Signature-Timestamp": timestamp,
449
+ "X-Signature-256": `sha256=${signature}`
450
+ };
451
+ }
452
+ rejected(reason) {
453
+ return { ok: false, reason, retryable: false };
454
+ }
314
455
  redirectMessage(what) {
315
456
  return `webhook ${this.target} responded with ${what}, which is not followed because the entry would not arrive intact; use the final URL instead`;
316
457
  }
@@ -1590,9 +1731,85 @@ var SupabaseErrorAdapter = class {
1590
1731
  });
1591
1732
  }
1592
1733
  };
1734
+
1735
+ // src/adapters/event-emitter-error-adapter.ts
1736
+ var DEFAULT_EVENT_NAME = "error-log";
1737
+ var EventEmitterErrorAdapter = class {
1738
+ /** @throws {RangeError} if `options.eventName` is empty. */
1739
+ constructor(emitter, options = {}) {
1740
+ this.emitter = emitter;
1741
+ this.eventName = options.eventName ?? DEFAULT_EVENT_NAME;
1742
+ if (this.eventName === "") {
1743
+ throw new RangeError(
1744
+ "EventEmitterErrorAdapter: eventName must not be empty."
1745
+ );
1746
+ }
1747
+ this.requireListener = options.requireListener ?? false;
1748
+ this.prefix = options.prefix ?? DEFAULT_PREFIX;
1749
+ }
1750
+ async log(entry) {
1751
+ try {
1752
+ const record = {
1753
+ id: resolveEntryId(entry),
1754
+ ...serializeEntry(entry)
1755
+ };
1756
+ const delivered = this.emitter.emit(this.eventName, record);
1757
+ if (this.requireListener && delivered === false) {
1758
+ throw new Error(
1759
+ `no listener is attached to "${this.eventName}", so nothing received the entry`
1760
+ );
1761
+ }
1762
+ return { ok: true };
1763
+ } catch (error) {
1764
+ return reportFailure({
1765
+ adapterName: "EventEmitterErrorAdapter",
1766
+ prefix: this.prefix,
1767
+ action: `to emit "${this.eventName}"`,
1768
+ error
1769
+ });
1770
+ }
1771
+ }
1772
+ };
1773
+
1774
+ // src/adapters/websocket-error-adapter.ts
1775
+ var OPEN = 1;
1776
+ var STATE_NAMES = {
1777
+ 0: "still connecting",
1778
+ 1: "open",
1779
+ 2: "closing",
1780
+ 3: "closed"
1781
+ };
1782
+ var WebSocketErrorAdapter = class {
1783
+ constructor(socket, options = {}) {
1784
+ this.socket = socket;
1785
+ this.prefix = options.prefix ?? DEFAULT_PREFIX;
1786
+ }
1787
+ async log(entry) {
1788
+ try {
1789
+ const state = this.socket.readyState;
1790
+ if (state !== OPEN) {
1791
+ throw new Error(
1792
+ `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`
1793
+ );
1794
+ }
1795
+ this.socket.send(
1796
+ JSON.stringify({ id: resolveEntryId(entry), ...serializeEntry(entry) })
1797
+ );
1798
+ return { ok: true };
1799
+ } catch (error) {
1800
+ return reportFailure({
1801
+ adapterName: "WebSocketErrorAdapter",
1802
+ prefix: this.prefix,
1803
+ action: "to send over the socket",
1804
+ error
1805
+ });
1806
+ }
1807
+ }
1808
+ };
1593
1809
  export {
1594
1810
  CompositeErrorAdapter,
1595
1811
  ConsoleErrorAdapter,
1812
+ EventEmitterErrorAdapter,
1596
1813
  FirestoreErrorAdapter,
1597
1814
  FlatFileErrorAdapter,
1598
1815
  IndexedDbErrorAdapter,
@@ -1605,6 +1822,7 @@ export {
1605
1822
  S3ErrorAdapter,
1606
1823
  SqliteErrorAdapter,
1607
1824
  SupabaseErrorAdapter,
1825
+ WebSocketErrorAdapter,
1608
1826
  WebStorageErrorAdapter,
1609
1827
  WebhookErrorAdapter,
1610
1828
  buildCreateErrorLogTableSql,