@dunx/http 3.0.0 → 3.0.1
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 +2 -2
- package/dist/chunk-b699cfes.js +130 -0
- package/dist/{chunk-ywdpxbkf.js → chunk-f6pw36av.js} +827 -1101
- package/dist/chunk-sz4pvqxy.js +108 -0
- package/dist/client.d.ts +0 -7
- package/dist/client.js +3 -13
- package/dist/index.d.ts +0 -7
- package/dist/index.js +9 -61
- package/dist/internal.d.ts +4 -4
- package/dist/internal.js +75 -10
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -77,8 +77,8 @@ The guide is canonical for every row; this table is the index.
|
|
|
77
77
|
|
|
78
78
|
`@dunx/http/internal` holds route-table construction, the middleware fold, the
|
|
79
79
|
relay codec and the discovery readers - what `@dunx/dashboard`, `@dunx/mcp` and
|
|
80
|
-
`@dunx/openapi` call and an app does not.
|
|
81
|
-
|
|
80
|
+
`@dunx/openapi` call and an app does not. It is the only place they are exported
|
|
81
|
+
from, and it may change in any release.
|
|
82
82
|
|
|
83
83
|
## Notes
|
|
84
84
|
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
HttpStatusCode
|
|
4
|
+
} from "./chunk-sz4pvqxy.js";
|
|
5
|
+
|
|
6
|
+
// src/client/errors.ts
|
|
7
|
+
import { AppError } from "@dunx/core";
|
|
8
|
+
|
|
9
|
+
class FetchError extends AppError {
|
|
10
|
+
status;
|
|
11
|
+
statusText;
|
|
12
|
+
body;
|
|
13
|
+
response;
|
|
14
|
+
name = "FetchError";
|
|
15
|
+
constructor(status, statusText, body, response) {
|
|
16
|
+
super(`HTTP ${status} ${statusText} from ${response.method} ${response.url}`);
|
|
17
|
+
this.status = status;
|
|
18
|
+
this.statusText = statusText;
|
|
19
|
+
this.body = body;
|
|
20
|
+
this.response = response;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
Object.defineProperty(FetchError, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "readonly status: number" }, { unresolved: "readonly statusText: string" }, { unresolved: "readonly body: unknown" }, { unresolved: `readonly response: {
|
|
24
|
+
readonly method: string;
|
|
25
|
+
readonly url: string;
|
|
26
|
+
readonly headers: Headers;
|
|
27
|
+
}` }] });
|
|
28
|
+
|
|
29
|
+
class FetchTransportError extends AppError {
|
|
30
|
+
response;
|
|
31
|
+
aborted;
|
|
32
|
+
name = "FetchTransportError";
|
|
33
|
+
constructor(response, aborted, options) {
|
|
34
|
+
super(`${response.method} ${response.url} failed: ${aborted ? "aborted" : "transport error"}`, options);
|
|
35
|
+
this.response = response;
|
|
36
|
+
this.aborted = aborted;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
Object.defineProperty(FetchTransportError, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "readonly response: { readonly method: string; readonly url: string }" }, { unresolved: "readonly aborted: boolean" }, ErrorOptions] });
|
|
40
|
+
|
|
41
|
+
// src/client/json.ts
|
|
42
|
+
var safeStringify = (value) => {
|
|
43
|
+
const seen = new WeakSet;
|
|
44
|
+
return JSON.stringify(value, (_key, entry) => {
|
|
45
|
+
if (typeof entry === "object" && entry !== null) {
|
|
46
|
+
if (seen.has(entry))
|
|
47
|
+
return "[Circular]";
|
|
48
|
+
seen.add(entry);
|
|
49
|
+
}
|
|
50
|
+
return entry;
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
var isPlainObject = (value) => {
|
|
54
|
+
if (typeof value !== "object" || value === null)
|
|
55
|
+
return false;
|
|
56
|
+
const proto = Object.getPrototypeOf(value);
|
|
57
|
+
return proto === Object.prototype || proto === null;
|
|
58
|
+
};
|
|
59
|
+
var isJsonBody = (payload) => {
|
|
60
|
+
if (payload === null || payload === undefined)
|
|
61
|
+
return false;
|
|
62
|
+
if (typeof payload !== "object")
|
|
63
|
+
return typeof payload !== "string";
|
|
64
|
+
return !(payload instanceof FormData || payload instanceof URLSearchParams || payload instanceof Blob || payload instanceof ArrayBuffer || payload instanceof ReadableStream || ArrayBuffer.isView(payload));
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// src/client/retry.ts
|
|
68
|
+
var uniform = () => {
|
|
69
|
+
const buffer = new Uint32Array(1);
|
|
70
|
+
crypto.getRandomValues(buffer);
|
|
71
|
+
return (buffer[0] ?? 0) / 2 ** 32;
|
|
72
|
+
};
|
|
73
|
+
var backoffDelay = (attempt, { baseMs, power = 2, jitterMs = 1000, maxMs = 30000 }) => Math.min(baseMs * power ** attempt + uniform() * jitterMs, maxMs);
|
|
74
|
+
var retryAfterMs = (headers, now = Date.now()) => {
|
|
75
|
+
const header = headers.get("retry-after");
|
|
76
|
+
if (header === null)
|
|
77
|
+
return;
|
|
78
|
+
const seconds = Number(header);
|
|
79
|
+
if (Number.isFinite(seconds))
|
|
80
|
+
return Math.max(0, seconds * 1000);
|
|
81
|
+
const at = Date.parse(header);
|
|
82
|
+
return Number.isNaN(at) ? undefined : Math.max(0, at - now);
|
|
83
|
+
};
|
|
84
|
+
var isRetryableStatus = (status) => status >= HttpStatusCode.INTERNAL_SERVER_ERROR || status === HttpStatusCode.REQUEST_TIMEOUT || status === HttpStatusCode.TOO_MANY_REQUESTS;
|
|
85
|
+
var decide = (error, attempt, options) => {
|
|
86
|
+
const {
|
|
87
|
+
retryDelayMs = 1000,
|
|
88
|
+
backoff,
|
|
89
|
+
shouldRetryOnStatus = isRetryableStatus,
|
|
90
|
+
respectRetryAfter = true
|
|
91
|
+
} = options;
|
|
92
|
+
const computed = backoffDelay(attempt, { baseMs: retryDelayMs, ...backoff });
|
|
93
|
+
if (error instanceof FetchTransportError) {
|
|
94
|
+
return { retry: !error.aborted, delayMs: computed };
|
|
95
|
+
}
|
|
96
|
+
if (error instanceof FetchError) {
|
|
97
|
+
if (!shouldRetryOnStatus(error.status))
|
|
98
|
+
return { retry: false, delayMs: 0 };
|
|
99
|
+
const asked = respectRetryAfter ? retryAfterMs(error.response.headers) : undefined;
|
|
100
|
+
const maxMs = backoff?.maxMs ?? 30000;
|
|
101
|
+
return {
|
|
102
|
+
retry: true,
|
|
103
|
+
delayMs: asked === undefined ? computed : Math.min(asked, maxMs)
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return { retry: true, delayMs: computed };
|
|
107
|
+
};
|
|
108
|
+
var executeWithRetry = async (operation, options = {}) => {
|
|
109
|
+
const { maxRetries = 3, onAttempt, onError, onSuccess } = options;
|
|
110
|
+
let lastError;
|
|
111
|
+
for (let attempt = 0;attempt <= maxRetries; attempt += 1) {
|
|
112
|
+
onAttempt?.(attempt + 1, attempt > 0);
|
|
113
|
+
try {
|
|
114
|
+
const result = await operation();
|
|
115
|
+
onSuccess?.(result, attempt + 1);
|
|
116
|
+
return result;
|
|
117
|
+
} catch (error) {
|
|
118
|
+
lastError = error;
|
|
119
|
+
const { retry, delayMs } = decide(error, attempt, options);
|
|
120
|
+
const willRetry = retry && attempt < maxRetries;
|
|
121
|
+
onError?.(error, attempt + 1, willRetry);
|
|
122
|
+
if (!willRetry)
|
|
123
|
+
throw error;
|
|
124
|
+
await Bun.sleep(delayMs);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
throw lastError;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export { FetchError, FetchTransportError, safeStringify, isPlainObject, isJsonBody, backoffDelay, retryAfterMs, isRetryableStatus, executeWithRetry };
|