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