@twinedo/app-error 1.0.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 ADDED
@@ -0,0 +1,128 @@
1
+ # @twinedo/app-error
2
+
3
+ A small, dependency-free error normalization layer that turns any thrown error
4
+ (fetch, axios-like, or runtime) into a predictable `AppError` shape. Use it to
5
+ standardize UI messaging, retry logic, and logging across mixed stacks.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @twinedo/app-error
11
+ ```
12
+
13
+ ## What problem this solves
14
+
15
+ Different APIs and HTTP clients emit wildly different error shapes. This library
16
+ normalizes them into a single `AppError` so your app can consistently:
17
+ - show a safe UI message
18
+ - decide whether to retry
19
+ - log a stable fingerprint
20
+
21
+ ## Axios example
22
+
23
+ ```ts
24
+ import axios from "axios";
25
+ import { toAppError } from "@twinedo/app-error";
26
+
27
+ try {
28
+ await axios.get("/api/user");
29
+ } catch (error) {
30
+ const appError = toAppError(error);
31
+ console.log(appError.kind, appError.status, appError.message);
32
+ }
33
+ ```
34
+
35
+ ## Fetch example
36
+
37
+ ```ts
38
+ import { fromFetch } from "@twinedo/app-error";
39
+
40
+ const response = await fetch("/api/user");
41
+ const body = await response.json().catch(() => undefined);
42
+
43
+ if (!response.ok) {
44
+ throw fromFetch(response, body);
45
+ }
46
+ ```
47
+
48
+ ## Project A vs Project B policy configuration
49
+
50
+ ```ts
51
+ import { defineErrorPolicy } from "@twinedo/app-error";
52
+
53
+ const projectAPolicy = defineErrorPolicy({
54
+ http: {
55
+ message: (data) => (typeof data === "object" && data ? data.message : undefined),
56
+ code: (data) => (typeof data === "object" && data ? data.code : undefined),
57
+ requestId: (headers) => headers?.get?.("x-request-id") ?? undefined,
58
+ },
59
+ });
60
+
61
+ const projectBPolicy = defineErrorPolicy({
62
+ http: {
63
+ message: (data) =>
64
+ typeof data === "object" && data && "error" in data
65
+ ? String((data as { error: unknown }).error)
66
+ : undefined,
67
+ code: (data) =>
68
+ typeof data === "object" && data && "errorCode" in data
69
+ ? String((data as { errorCode: unknown }).errorCode)
70
+ : undefined,
71
+ requestId: (headers) => headers?.get?.("x-correlation-id") ?? undefined,
72
+ retryable: (status) => status === 429 || (status ? status >= 500 : false),
73
+ },
74
+ });
75
+ ```
76
+
77
+ ## React / React Native usage
78
+
79
+ ```tsx
80
+ import { useEffect, useState } from "react";
81
+ import { fromFetch, isRetryable, toAppError } from "@twinedo/app-error";
82
+
83
+ export function ProfileScreen() {
84
+ const [error, setError] = useState<ReturnType<typeof toAppError> | null>(null);
85
+
86
+ useEffect(() => {
87
+ let mounted = true;
88
+ fetch("/api/profile")
89
+ .then(async (response) => {
90
+ const body = await response.json().catch(() => undefined);
91
+ if (!response.ok) throw fromFetch(response, body);
92
+ return body;
93
+ })
94
+ .catch((err) => {
95
+ if (!mounted) return;
96
+ setError(toAppError(err));
97
+ });
98
+
99
+ return () => {
100
+ mounted = false;
101
+ };
102
+ }, []);
103
+
104
+ if (!error) return null;
105
+
106
+ return (
107
+ <>
108
+ <Text>{error.message}</Text>
109
+ {isRetryable(error) ? <Button title="Retry" /> : null}
110
+ </>
111
+ );
112
+ }
113
+ ```
114
+
115
+ ## Retry decision example
116
+
117
+ ```ts
118
+ import { isRetryable, toAppError } from "@twinedo/app-error";
119
+
120
+ try {
121
+ await apiCall();
122
+ } catch (error) {
123
+ const appError = toAppError(error);
124
+ if (isRetryable(appError)) {
125
+ // show retry action or auto-retry
126
+ }
127
+ }
128
+ ```
package/dist/index.cjs ADDED
@@ -0,0 +1,552 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ attempt: () => attempt,
24
+ defineErrorPolicy: () => defineErrorPolicy,
25
+ errorKey: () => errorKey,
26
+ fromFetch: () => fromFetch,
27
+ isAppError: () => isAppError,
28
+ isRetryable: () => isRetryable,
29
+ toAppError: () => toAppError
30
+ });
31
+ module.exports = __toCommonJS(index_exports);
32
+
33
+ // src/policy.ts
34
+ var DEFAULT_REQUEST_ID_HEADERS = [
35
+ "x-request-id",
36
+ "x-correlation-id",
37
+ "x-trace-id",
38
+ "traceparent",
39
+ "x-amzn-trace-id"
40
+ ];
41
+ var isRecord = (value) => typeof value === "object" && value !== null;
42
+ var normalizeString = (value) => {
43
+ if (typeof value !== "string") return void 0;
44
+ const trimmed = value.trim();
45
+ return trimmed.length > 0 ? trimmed : void 0;
46
+ };
47
+ var normalizeHeaderValue = (value) => {
48
+ if (typeof value === "number") return String(value);
49
+ if (typeof value === "string") return value;
50
+ return void 0;
51
+ };
52
+ var getHeaderValue = (headers, name) => {
53
+ if (!headers) return void 0;
54
+ const lowerName = name.toLowerCase();
55
+ const getter = headers.get;
56
+ if (typeof getter === "function") {
57
+ const value = getter.call(headers, name) ?? getter.call(headers, lowerName);
58
+ const normalized = normalizeString(value);
59
+ if (normalized) return normalized;
60
+ }
61
+ if (isRecord(headers)) {
62
+ const recordHeaders = headers;
63
+ for (const key of Object.keys(headers)) {
64
+ if (key.toLowerCase() !== lowerName) continue;
65
+ const raw = recordHeaders[key];
66
+ if (Array.isArray(raw)) {
67
+ const first = normalizeHeaderValue(raw[0]);
68
+ const normalized = normalizeString(first);
69
+ if (normalized) return normalized;
70
+ } else {
71
+ const normalized = normalizeString(normalizeHeaderValue(raw));
72
+ if (normalized) return normalized;
73
+ }
74
+ }
75
+ }
76
+ return void 0;
77
+ };
78
+ var extractString = (value) => normalizeString(value);
79
+ var extractFromArray = (value) => {
80
+ for (const item of value) {
81
+ const message = extractMessageFromData(item);
82
+ if (message) return message;
83
+ }
84
+ return void 0;
85
+ };
86
+ var extractMessageFromData = (data) => {
87
+ const direct = extractString(data);
88
+ if (direct) return direct;
89
+ if (Array.isArray(data)) return extractFromArray(data);
90
+ if (!isRecord(data)) return void 0;
91
+ const directKeys = ["message", "error", "detail", "title", "description"];
92
+ for (const key of directKeys) {
93
+ const value = extractString(data[key]);
94
+ if (value) return value;
95
+ }
96
+ const errorValue = data.error;
97
+ if (isRecord(errorValue)) {
98
+ const nested = extractString(errorValue.message) ?? extractString(errorValue.detail);
99
+ if (nested) return nested;
100
+ }
101
+ const errorsValue = data.errors;
102
+ if (Array.isArray(errorsValue)) {
103
+ const nested = extractFromArray(errorsValue);
104
+ if (nested) return nested;
105
+ }
106
+ if (isRecord(errorsValue)) {
107
+ for (const key of Object.keys(errorsValue)) {
108
+ const fieldValue = errorsValue[key];
109
+ if (Array.isArray(fieldValue)) {
110
+ const nested = extractFromArray(fieldValue);
111
+ if (nested) return nested;
112
+ } else {
113
+ const nested = extractString(fieldValue);
114
+ if (nested) return nested;
115
+ }
116
+ }
117
+ }
118
+ return void 0;
119
+ };
120
+ var extractCodeFromData = (data) => {
121
+ if (Array.isArray(data)) {
122
+ for (const item of data) {
123
+ const code = extractCodeFromData(item);
124
+ if (code) return code;
125
+ }
126
+ return void 0;
127
+ }
128
+ if (!isRecord(data)) return void 0;
129
+ const directKeys = ["code", "errorCode", "error_code"];
130
+ for (const key of directKeys) {
131
+ const value = extractString(data[key]);
132
+ if (value) return value;
133
+ }
134
+ const errorValue = data.error;
135
+ if (isRecord(errorValue)) {
136
+ const nested = extractString(errorValue.code) ?? extractString(errorValue.errorCode);
137
+ if (nested) return nested;
138
+ }
139
+ return void 0;
140
+ };
141
+ var defaultHttpMessage = (data, response) => {
142
+ const fromData = extractMessageFromData(data);
143
+ if (fromData) return fromData;
144
+ return extractString(response?.statusText);
145
+ };
146
+ var defaultHttpCode = (data) => extractCodeFromData(data);
147
+ var defaultRequestId = (headers) => {
148
+ for (const header of DEFAULT_REQUEST_ID_HEADERS) {
149
+ const value = getHeaderValue(headers, header);
150
+ if (value) return value;
151
+ }
152
+ return void 0;
153
+ };
154
+ var defaultHttpRetryable = (status) => {
155
+ if (typeof status !== "number") return false;
156
+ return status >= 500 && status <= 599;
157
+ };
158
+ var DEFAULT_HTTP_POLICY = {
159
+ message: defaultHttpMessage,
160
+ code: defaultHttpCode,
161
+ requestId: defaultRequestId,
162
+ retryable: defaultHttpRetryable
163
+ };
164
+ var defineErrorPolicy = (...configs) => {
165
+ const merged = {};
166
+ for (const config of configs) {
167
+ if (!config?.http) continue;
168
+ Object.assign(merged, config.http);
169
+ }
170
+ return {
171
+ http: {
172
+ message: merged.message ?? DEFAULT_HTTP_POLICY.message,
173
+ code: merged.code ?? DEFAULT_HTTP_POLICY.code,
174
+ requestId: merged.requestId ?? DEFAULT_HTTP_POLICY.requestId,
175
+ retryable: merged.retryable ?? DEFAULT_HTTP_POLICY.retryable
176
+ }
177
+ };
178
+ };
179
+
180
+ // src/types.ts
181
+ var APP_ERROR_KINDS = {
182
+ http: true,
183
+ network: true,
184
+ timeout: true,
185
+ parse: true,
186
+ validation: true,
187
+ unknown: true
188
+ };
189
+ var isRecord2 = (value) => typeof value === "object" && value !== null;
190
+ var isAppError = (value) => {
191
+ if (!isRecord2(value)) return false;
192
+ const kind = value.kind;
193
+ if (typeof kind !== "string" || !(kind in APP_ERROR_KINDS)) return false;
194
+ return typeof value.message === "string";
195
+ };
196
+
197
+ // src/fromFetch.ts
198
+ var DEFAULT_MESSAGE = "Something went wrong";
199
+ var normalizeMessage = (value) => {
200
+ if (typeof value !== "string") return void 0;
201
+ const trimmed = value.trim();
202
+ return trimmed.length > 0 ? trimmed : void 0;
203
+ };
204
+ var defaultRetryable = (status) => {
205
+ if (typeof status !== "number") return false;
206
+ return status >= 500 && status <= 599;
207
+ };
208
+ var safeInvoke = (fn) => {
209
+ try {
210
+ return fn();
211
+ } catch {
212
+ return void 0;
213
+ }
214
+ };
215
+ var fromFetch = (response, body, policy) => {
216
+ const resolvedPolicy = defineErrorPolicy(policy);
217
+ const status = typeof response.status === "number" ? response.status : void 0;
218
+ const message = normalizeMessage(
219
+ safeInvoke(() => resolvedPolicy.http.message(body, response))
220
+ ) ?? DEFAULT_MESSAGE;
221
+ const code = normalizeMessage(
222
+ safeInvoke(() => resolvedPolicy.http.code(body, response))
223
+ );
224
+ const requestId = normalizeMessage(
225
+ safeInvoke(() => resolvedPolicy.http.requestId(response.headers))
226
+ );
227
+ const retryable = safeInvoke(() => resolvedPolicy.http.retryable(status)) ?? defaultRetryable(status);
228
+ return {
229
+ kind: "http",
230
+ message,
231
+ retryable,
232
+ ...status !== void 0 ? { status } : {},
233
+ ...code ? { code } : {},
234
+ ...requestId ? { requestId } : {},
235
+ ...body !== void 0 ? { details: body } : {},
236
+ ...response !== void 0 ? { cause: response } : {}
237
+ };
238
+ };
239
+
240
+ // src/adapters/axiosLike.ts
241
+ var TIMEOUT_CODES = /* @__PURE__ */ new Set(["ECONNABORTED", "ETIMEDOUT", "ESOCKETTIMEDOUT"]);
242
+ var NETWORK_CODES = /* @__PURE__ */ new Set([
243
+ "ERR_NETWORK",
244
+ "ENOTFOUND",
245
+ "ECONNREFUSED",
246
+ "ECONNRESET",
247
+ "EAI_AGAIN",
248
+ "ETIMEDOUT",
249
+ "EHOSTUNREACH",
250
+ "ENETUNREACH"
251
+ ]);
252
+ var isRecord3 = (value) => typeof value === "object" && value !== null;
253
+ var getString = (value) => typeof value === "string" ? value : void 0;
254
+ var getStatus = (value) => typeof value === "number" ? value : void 0;
255
+ var getResponseLike = (value) => {
256
+ if (!isRecord3(value)) return void 0;
257
+ const status = getStatus(value.status);
258
+ const statusText = getString(value.statusText);
259
+ const headers = value.headers;
260
+ return {
261
+ ...status !== void 0 ? { status } : {},
262
+ ...statusText ? { statusText } : {},
263
+ ...value.data !== void 0 ? { data: value.data } : {},
264
+ ...headers !== void 0 ? { headers } : {}
265
+ };
266
+ };
267
+ var getAxiosLikeErrorInfo = (error) => {
268
+ if (!isRecord3(error)) return null;
269
+ const isAxiosMarker = error.isAxiosError === true;
270
+ const response = getResponseLike(error.response);
271
+ const request = error.request;
272
+ const looksAxios = isAxiosMarker || response !== void 0 || request !== void 0;
273
+ if (!looksAxios) return null;
274
+ const code = getString(error.code);
275
+ const message = getString(error.message);
276
+ const status = response?.status;
277
+ const data = response?.data;
278
+ const headers = response?.headers;
279
+ const messageLower = message?.toLowerCase();
280
+ const isTimeout = (code ? TIMEOUT_CODES.has(code) : false) || (messageLower ? messageLower.includes("timeout") : false);
281
+ const isNetworkError2 = !response && (request !== void 0 || (code ? NETWORK_CODES.has(code) : false) || (messageLower ? messageLower.includes("network error") : false));
282
+ return {
283
+ isTimeout,
284
+ isNetworkError: isNetworkError2,
285
+ ...response ? { response } : {},
286
+ ...status !== void 0 ? { status } : {},
287
+ ...data !== void 0 ? { data } : {},
288
+ ...headers !== void 0 ? { headers } : {},
289
+ ...code ? { code } : {},
290
+ ...message ? { message } : {}
291
+ };
292
+ };
293
+ var toHttpResponseLike = (info) => {
294
+ if (!info.response) return void 0;
295
+ const statusText = info.response.statusText;
296
+ const headers = info.headers;
297
+ return {
298
+ ...info.status !== void 0 ? { status: info.status } : {},
299
+ ...statusText ? { statusText } : {},
300
+ ...headers !== void 0 ? { headers } : {}
301
+ };
302
+ };
303
+
304
+ // src/toAppError.ts
305
+ var DEFAULT_MESSAGE2 = "Something went wrong";
306
+ var NETWORK_ERROR_CODES = /* @__PURE__ */ new Set([
307
+ "ENOTFOUND",
308
+ "ECONNREFUSED",
309
+ "ECONNRESET",
310
+ "EAI_AGAIN",
311
+ "EHOSTUNREACH",
312
+ "ENETUNREACH",
313
+ "ERR_NETWORK"
314
+ ]);
315
+ var TIMEOUT_ERROR_CODES = /* @__PURE__ */ new Set([
316
+ "ETIMEDOUT",
317
+ "ESOCKETTIMEDOUT",
318
+ "ECONNABORTED"
319
+ ]);
320
+ var isRecord4 = (value) => typeof value === "object" && value !== null;
321
+ var getString2 = (value) => typeof value === "string" ? value : void 0;
322
+ var normalizeMessage2 = (value) => {
323
+ if (typeof value !== "string") return void 0;
324
+ const trimmed = value.trim();
325
+ return trimmed.length > 0 ? trimmed : void 0;
326
+ };
327
+ var defaultRetryable2 = (kind, status) => {
328
+ if (kind === "network") return true;
329
+ if (kind === "http" && typeof status === "number") {
330
+ return status >= 500 && status <= 599;
331
+ }
332
+ return false;
333
+ };
334
+ var safeInvoke2 = (fn) => {
335
+ try {
336
+ return fn();
337
+ } catch {
338
+ return void 0;
339
+ }
340
+ };
341
+ var getErrorInfo = (error) => {
342
+ if (!isRecord4(error)) return { name: void 0, message: void 0, code: void 0 };
343
+ return {
344
+ name: getString2(error.name),
345
+ message: getString2(error.message),
346
+ code: getString2(error.code)
347
+ };
348
+ };
349
+ var isTimeoutError = (name, message, code) => {
350
+ if (name === "AbortError") return true;
351
+ if (code && TIMEOUT_ERROR_CODES.has(code)) return true;
352
+ const lowered = message?.toLowerCase();
353
+ return lowered ? lowered.includes("timeout") : false;
354
+ };
355
+ var isNetworkError = (name, message, code) => {
356
+ if (name === "TypeError") {
357
+ const lowered2 = message?.toLowerCase() ?? "";
358
+ if (lowered2.includes("failed to fetch") || lowered2.includes("network request failed") || lowered2.includes("networkerror") || lowered2.includes("load failed")) {
359
+ return true;
360
+ }
361
+ }
362
+ if (code && NETWORK_ERROR_CODES.has(code)) return true;
363
+ const lowered = message?.toLowerCase() ?? "";
364
+ return lowered.includes("network error");
365
+ };
366
+ var isParseError = (name) => name === "SyntaxError";
367
+ var isValidationError = (error, name) => {
368
+ if (name && name.toLowerCase().includes("validation")) return true;
369
+ if (!isRecord4(error)) return false;
370
+ return Array.isArray(error.issues) || Array.isArray(error.errors);
371
+ };
372
+ var normalizeExisting = (error) => {
373
+ const message = normalizeMessage2(error.message) ?? DEFAULT_MESSAGE2;
374
+ const retryable = typeof error.retryable === "boolean" ? error.retryable : defaultRetryable2(error.kind, error.status);
375
+ return {
376
+ ...error,
377
+ message,
378
+ retryable
379
+ };
380
+ };
381
+ var buildAppError = (options) => ({
382
+ kind: options.kind,
383
+ message: options.message,
384
+ retryable: options.retryable,
385
+ ...options.status !== void 0 ? { status: options.status } : {},
386
+ ...options.code ? { code: options.code } : {},
387
+ ...options.requestId ? { requestId: options.requestId } : {},
388
+ ...options.details !== void 0 ? { details: options.details } : {},
389
+ ...options.cause !== void 0 ? { cause: options.cause } : {}
390
+ });
391
+ var fromStatusObject = (error, policy) => {
392
+ if (!isRecord4(error)) return null;
393
+ if (typeof error.status !== "number" || error.status < 400) return null;
394
+ const status = error.status;
395
+ const statusText = getString2(error.statusText);
396
+ const headers = error.headers;
397
+ const response = {
398
+ status,
399
+ ...statusText ? { statusText } : {},
400
+ ...headers !== void 0 ? { headers } : {}
401
+ };
402
+ const details = error.data !== void 0 ? error.data : error.body !== void 0 ? error.body : error.details;
403
+ const message = normalizeMessage2(safeInvoke2(() => policy.http.message(details, response))) ?? DEFAULT_MESSAGE2;
404
+ const code = safeInvoke2(() => policy.http.code(details, response));
405
+ const requestId = safeInvoke2(() => policy.http.requestId(response.headers));
406
+ const retryable = safeInvoke2(() => policy.http.retryable(status)) ?? defaultRetryable2("http", status);
407
+ return buildAppError({
408
+ kind: "http",
409
+ message,
410
+ status,
411
+ code: normalizeMessage2(code),
412
+ retryable,
413
+ requestId: normalizeMessage2(requestId),
414
+ details,
415
+ cause: error
416
+ });
417
+ };
418
+ var toAppError = (error, policy) => {
419
+ const resolvedPolicy = defineErrorPolicy(policy);
420
+ try {
421
+ if (isAppError(error)) return normalizeExisting(error);
422
+ const axiosInfo = getAxiosLikeErrorInfo(error);
423
+ if (axiosInfo) {
424
+ if (axiosInfo.response) {
425
+ const response = toHttpResponseLike(axiosInfo);
426
+ const message2 = normalizeMessage2(
427
+ safeInvoke2(() => resolvedPolicy.http.message(axiosInfo.data, response))
428
+ ) ?? DEFAULT_MESSAGE2;
429
+ const code2 = safeInvoke2(
430
+ () => resolvedPolicy.http.code(axiosInfo.data, response)
431
+ );
432
+ const requestId = safeInvoke2(
433
+ () => resolvedPolicy.http.requestId(axiosInfo.headers)
434
+ );
435
+ const retryable = safeInvoke2(() => resolvedPolicy.http.retryable(axiosInfo.status)) ?? defaultRetryable2("http", axiosInfo.status);
436
+ return buildAppError({
437
+ kind: "http",
438
+ message: message2,
439
+ status: axiosInfo.status,
440
+ code: normalizeMessage2(code2),
441
+ retryable,
442
+ requestId: normalizeMessage2(requestId),
443
+ details: axiosInfo.data,
444
+ cause: error
445
+ });
446
+ }
447
+ if (axiosInfo.isTimeout) {
448
+ return buildAppError({
449
+ kind: "timeout",
450
+ message: DEFAULT_MESSAGE2,
451
+ retryable: false,
452
+ cause: error
453
+ });
454
+ }
455
+ if (axiosInfo.isNetworkError) {
456
+ return buildAppError({
457
+ kind: "network",
458
+ message: DEFAULT_MESSAGE2,
459
+ retryable: true,
460
+ cause: error
461
+ });
462
+ }
463
+ }
464
+ const { name, message, code } = getErrorInfo(error);
465
+ if (isTimeoutError(name, message, code)) {
466
+ return buildAppError({
467
+ kind: "timeout",
468
+ message: DEFAULT_MESSAGE2,
469
+ retryable: false,
470
+ cause: error
471
+ });
472
+ }
473
+ if (isNetworkError(name, message, code)) {
474
+ return buildAppError({
475
+ kind: "network",
476
+ message: DEFAULT_MESSAGE2,
477
+ retryable: true,
478
+ cause: error
479
+ });
480
+ }
481
+ if (isParseError(name)) {
482
+ return buildAppError({
483
+ kind: "parse",
484
+ message: DEFAULT_MESSAGE2,
485
+ retryable: false,
486
+ cause: error
487
+ });
488
+ }
489
+ if (isValidationError(error, name)) {
490
+ return buildAppError({
491
+ kind: "validation",
492
+ message: DEFAULT_MESSAGE2,
493
+ retryable: false,
494
+ cause: error,
495
+ details: error
496
+ });
497
+ }
498
+ const httpFromStatus = fromStatusObject(error, resolvedPolicy);
499
+ if (httpFromStatus) return httpFromStatus;
500
+ } catch {
501
+ }
502
+ return buildAppError({
503
+ kind: "unknown",
504
+ message: DEFAULT_MESSAGE2,
505
+ retryable: false,
506
+ cause: error
507
+ });
508
+ };
509
+
510
+ // src/helpers.ts
511
+ var normalizeMessage3 = (value) => {
512
+ if (typeof value !== "string") return void 0;
513
+ const trimmed = value.trim();
514
+ return trimmed.length > 0 ? trimmed : void 0;
515
+ };
516
+ var errorKey = (error) => {
517
+ const normalized = isAppError(error) ? error : toAppError(error);
518
+ const parts = [
519
+ normalized.kind,
520
+ normalized.status !== void 0 ? String(normalized.status) : void 0,
521
+ normalizeMessage3(normalized.code),
522
+ normalizeMessage3(normalized.message)
523
+ ].filter((value) => Boolean(value));
524
+ return parts.join("|");
525
+ };
526
+ var isRetryable = (error) => {
527
+ const normalized = isAppError(error) ? error : toAppError(error);
528
+ if (typeof normalized.retryable === "boolean") return normalized.retryable;
529
+ if (normalized.kind === "network") return true;
530
+ if (normalized.kind === "http") {
531
+ return typeof normalized.status === "number" && normalized.status >= 500;
532
+ }
533
+ return false;
534
+ };
535
+ var attempt = async (fn, policy) => {
536
+ try {
537
+ const data = await fn();
538
+ return { ok: true, data };
539
+ } catch (error) {
540
+ return { ok: false, error: toAppError(error, policy) };
541
+ }
542
+ };
543
+ // Annotate the CommonJS export names for ESM import in node:
544
+ 0 && (module.exports = {
545
+ attempt,
546
+ defineErrorPolicy,
547
+ errorKey,
548
+ fromFetch,
549
+ isAppError,
550
+ isRetryable,
551
+ toAppError
552
+ });