@dunx/http 0.5.0 → 0.6.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 +95 -0
- package/dist/chunk-x80f562w.js +40 -0
- package/dist/chunk-x80f562w.js.map +10 -0
- package/dist/client/errors.d.ts +64 -0
- package/dist/client/json.d.ts +34 -0
- package/dist/client/module.d.ts +66 -0
- package/dist/client/options.d.ts +57 -0
- package/dist/client/retry.d.ts +49 -0
- package/dist/client/service.d.ts +104 -0
- package/dist/client.d.ts +13 -0
- package/dist/client.js +462 -0
- package/dist/client.js.map +15 -0
- package/dist/index.js +5 -36
- package/dist/index.js.map +3 -4
- package/package.json +9 -2
package/dist/client.js
ADDED
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
HttpStatusCode
|
|
4
|
+
} from "./chunk-x80f562w.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"), {
|
|
24
|
+
value: () => [{ unresolved: "readonly status: number" }, { unresolved: "readonly statusText: string" }, { unresolved: "readonly body: unknown" }, { unresolved: `readonly response: {
|
|
25
|
+
readonly method: string;
|
|
26
|
+
readonly url: string;
|
|
27
|
+
readonly headers: Headers;
|
|
28
|
+
}` }]
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
class FetchTransportError extends AppError {
|
|
32
|
+
response;
|
|
33
|
+
aborted;
|
|
34
|
+
name = "FetchTransportError";
|
|
35
|
+
constructor(response, aborted, options) {
|
|
36
|
+
super(`${response.method} ${response.url} failed: ${aborted ? "aborted" : "transport error"}`, options);
|
|
37
|
+
this.response = response;
|
|
38
|
+
this.aborted = aborted;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
Object.defineProperty(FetchTransportError, Symbol.for("dunx.deps"), {
|
|
42
|
+
value: () => [{ unresolved: "readonly response: { readonly method: string; readonly url: string }" }, { unresolved: "readonly aborted: boolean" }, ErrorOptions]
|
|
43
|
+
});
|
|
44
|
+
// src/client/json.ts
|
|
45
|
+
var safeStringify = (value) => {
|
|
46
|
+
const seen = new WeakSet;
|
|
47
|
+
return JSON.stringify(value, (_key, entry) => {
|
|
48
|
+
if (typeof entry === "object" && entry !== null) {
|
|
49
|
+
if (seen.has(entry))
|
|
50
|
+
return "[Circular]";
|
|
51
|
+
seen.add(entry);
|
|
52
|
+
}
|
|
53
|
+
return entry;
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
var isPlainObject = (value) => {
|
|
57
|
+
if (typeof value !== "object" || value === null)
|
|
58
|
+
return false;
|
|
59
|
+
const proto = Object.getPrototypeOf(value);
|
|
60
|
+
return proto === Object.prototype || proto === null;
|
|
61
|
+
};
|
|
62
|
+
var isJsonBody = (payload) => {
|
|
63
|
+
if (payload === null || payload === undefined)
|
|
64
|
+
return false;
|
|
65
|
+
if (typeof payload !== "object")
|
|
66
|
+
return typeof payload !== "string";
|
|
67
|
+
return !(payload instanceof FormData || payload instanceof URLSearchParams || payload instanceof Blob || payload instanceof ArrayBuffer || payload instanceof ReadableStream || ArrayBuffer.isView(payload));
|
|
68
|
+
};
|
|
69
|
+
// src/client/options.ts
|
|
70
|
+
var DEFAULT_REQUEST_ID_HEADER = "x-request-id";
|
|
71
|
+
|
|
72
|
+
class HttpClientOptions {
|
|
73
|
+
baseUrl;
|
|
74
|
+
timeoutMs;
|
|
75
|
+
headers;
|
|
76
|
+
retry;
|
|
77
|
+
requestIdHeader;
|
|
78
|
+
name;
|
|
79
|
+
fetchOptions;
|
|
80
|
+
constructor(init = {}) {
|
|
81
|
+
this.baseUrl = init.baseUrl === undefined ? undefined : String(init.baseUrl);
|
|
82
|
+
this.timeoutMs = init.timeoutMs ?? 30000;
|
|
83
|
+
this.headers = init.headers ?? {};
|
|
84
|
+
this.retry = init.retry ?? {};
|
|
85
|
+
this.name = init.name;
|
|
86
|
+
const propagate = init.propagateRequestId ?? true;
|
|
87
|
+
this.requestIdHeader = propagate === false ? undefined : propagate === true ? DEFAULT_REQUEST_ID_HEADER : propagate;
|
|
88
|
+
this.fetchOptions = Object.fromEntries([
|
|
89
|
+
["proxy", init.proxy],
|
|
90
|
+
["tls", init.tls],
|
|
91
|
+
["unix", init.unix],
|
|
92
|
+
["decompress", init.decompress],
|
|
93
|
+
["verbose", init.verbose]
|
|
94
|
+
].filter(([, value]) => value !== undefined));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
Object.defineProperty(HttpClientOptions, Symbol.for("dunx.deps"), {
|
|
98
|
+
value: () => [{ unresolved: "init: HttpClientOptionsInit = {}" }]
|
|
99
|
+
});
|
|
100
|
+
// src/client/retry.ts
|
|
101
|
+
var uniform = () => {
|
|
102
|
+
const buffer = new Uint32Array(1);
|
|
103
|
+
crypto.getRandomValues(buffer);
|
|
104
|
+
return (buffer[0] ?? 0) / 2 ** 32;
|
|
105
|
+
};
|
|
106
|
+
var backoffDelay = (attempt, { baseMs, power = 2, jitterMs = 1000, maxMs = 30000 }) => Math.min(baseMs * power ** attempt + uniform() * jitterMs, maxMs);
|
|
107
|
+
var retryAfterMs = (headers, now = Date.now()) => {
|
|
108
|
+
const header = headers.get("retry-after");
|
|
109
|
+
if (header === null)
|
|
110
|
+
return;
|
|
111
|
+
const seconds = Number(header);
|
|
112
|
+
if (Number.isFinite(seconds))
|
|
113
|
+
return Math.max(0, seconds * 1000);
|
|
114
|
+
const at = Date.parse(header);
|
|
115
|
+
return Number.isNaN(at) ? undefined : Math.max(0, at - now);
|
|
116
|
+
};
|
|
117
|
+
var isRetryableStatus = (status) => status >= HttpStatusCode.INTERNAL_SERVER_ERROR || status === HttpStatusCode.REQUEST_TIMEOUT || status === HttpStatusCode.TOO_MANY_REQUESTS;
|
|
118
|
+
var decide = (error, attempt, options) => {
|
|
119
|
+
const {
|
|
120
|
+
retryDelayMs = 1000,
|
|
121
|
+
backoff,
|
|
122
|
+
shouldRetryOnStatus = isRetryableStatus,
|
|
123
|
+
respectRetryAfter = true
|
|
124
|
+
} = options;
|
|
125
|
+
const computed = backoffDelay(attempt, { baseMs: retryDelayMs, ...backoff });
|
|
126
|
+
if (error instanceof FetchTransportError) {
|
|
127
|
+
return { retry: !error.aborted, delayMs: computed };
|
|
128
|
+
}
|
|
129
|
+
if (error instanceof FetchError) {
|
|
130
|
+
if (!shouldRetryOnStatus(error.status))
|
|
131
|
+
return { retry: false, delayMs: 0 };
|
|
132
|
+
const asked = respectRetryAfter ? retryAfterMs(error.response.headers) : undefined;
|
|
133
|
+
const maxMs = backoff?.maxMs ?? 30000;
|
|
134
|
+
return {
|
|
135
|
+
retry: true,
|
|
136
|
+
delayMs: asked === undefined ? computed : Math.min(asked, maxMs)
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
return { retry: true, delayMs: computed };
|
|
140
|
+
};
|
|
141
|
+
var executeWithRetry = async (operation, options = {}) => {
|
|
142
|
+
const { maxRetries = 3, onAttempt, onError, onSuccess } = options;
|
|
143
|
+
let lastError;
|
|
144
|
+
for (let attempt = 0;attempt <= maxRetries; attempt += 1) {
|
|
145
|
+
onAttempt?.(attempt + 1, attempt > 0);
|
|
146
|
+
try {
|
|
147
|
+
const result = await operation();
|
|
148
|
+
onSuccess?.(result, attempt + 1);
|
|
149
|
+
return result;
|
|
150
|
+
} catch (error) {
|
|
151
|
+
lastError = error;
|
|
152
|
+
const { retry, delayMs } = decide(error, attempt, options);
|
|
153
|
+
const willRetry = retry && attempt < maxRetries;
|
|
154
|
+
onError?.(error, attempt + 1, willRetry);
|
|
155
|
+
if (!willRetry)
|
|
156
|
+
throw error;
|
|
157
|
+
await Bun.sleep(delayMs);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
throw lastError;
|
|
161
|
+
};
|
|
162
|
+
// src/client/module.ts
|
|
163
|
+
import {
|
|
164
|
+
Logger as Logger2,
|
|
165
|
+
provide,
|
|
166
|
+
RequestContext as RequestContext2,
|
|
167
|
+
token
|
|
168
|
+
} from "@dunx/core";
|
|
169
|
+
|
|
170
|
+
// src/client/service.ts
|
|
171
|
+
import { Logger, RequestContext } from "@dunx/core";
|
|
172
|
+
import { UrlHelper } from "@arkv/shared";
|
|
173
|
+
class HttpService extends UrlHelper {
|
|
174
|
+
options;
|
|
175
|
+
logger;
|
|
176
|
+
requestContext;
|
|
177
|
+
constructor(options, logger, requestContext) {
|
|
178
|
+
super();
|
|
179
|
+
this.options = options;
|
|
180
|
+
this.logger = logger;
|
|
181
|
+
this.requestContext = requestContext;
|
|
182
|
+
}
|
|
183
|
+
async request(config) {
|
|
184
|
+
const url = this.urlFor(config);
|
|
185
|
+
const startedAt = Date.now();
|
|
186
|
+
let attempts = 0;
|
|
187
|
+
let status;
|
|
188
|
+
const { body, serialised } = this.bodyFor(config.payload);
|
|
189
|
+
const replayable = !(config.payload instanceof ReadableStream);
|
|
190
|
+
const attempt = async () => {
|
|
191
|
+
attempts += 1;
|
|
192
|
+
const response = await this.send(config, url, body, serialised);
|
|
193
|
+
status = response.status;
|
|
194
|
+
if (!response.ok) {
|
|
195
|
+
throw new FetchError(response.status, response.statusText, await readBody(response), {
|
|
196
|
+
method: config.method,
|
|
197
|
+
url: url.href,
|
|
198
|
+
headers: response.headers
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
return await readBody(response);
|
|
202
|
+
};
|
|
203
|
+
const describe = () => `${config.method} ${url.href}`;
|
|
204
|
+
try {
|
|
205
|
+
const result = await this.requestContext.runWithContext({
|
|
206
|
+
...config.flow === undefined ? {} : { flow: config.flow },
|
|
207
|
+
event: config.path ?? url.pathname
|
|
208
|
+
}, () => executeWithRetry(attempt, {
|
|
209
|
+
...this.options.retry,
|
|
210
|
+
...config.retry,
|
|
211
|
+
...replayable ? {} : { maxRetries: 0 }
|
|
212
|
+
}));
|
|
213
|
+
this.logger.debug(`${describe()} succeeded`, {
|
|
214
|
+
status,
|
|
215
|
+
attempts,
|
|
216
|
+
elapsedMs: Date.now() - startedAt
|
|
217
|
+
});
|
|
218
|
+
return result;
|
|
219
|
+
} catch (error) {
|
|
220
|
+
this.logger.error(`${describe()} failed`, {
|
|
221
|
+
err: safeStringify(describeError(error)),
|
|
222
|
+
attempts,
|
|
223
|
+
elapsedMs: Date.now() - startedAt
|
|
224
|
+
});
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
get(url, options) {
|
|
229
|
+
return this.request({
|
|
230
|
+
method: "GET",
|
|
231
|
+
...options,
|
|
232
|
+
...urlOf(url)
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
post(url, payload, options) {
|
|
236
|
+
return this.request({
|
|
237
|
+
method: "POST",
|
|
238
|
+
...options,
|
|
239
|
+
...urlOf(url),
|
|
240
|
+
...payload === undefined ? {} : { payload }
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
put(url, payload, options) {
|
|
244
|
+
return this.request({
|
|
245
|
+
method: "PUT",
|
|
246
|
+
...options,
|
|
247
|
+
...urlOf(url),
|
|
248
|
+
...payload === undefined ? {} : { payload }
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
patch(url, payload, options) {
|
|
252
|
+
return this.request({
|
|
253
|
+
method: "PATCH",
|
|
254
|
+
...options,
|
|
255
|
+
...urlOf(url),
|
|
256
|
+
...payload === undefined ? {} : { payload }
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
delete(url, options) {
|
|
260
|
+
return this.request({
|
|
261
|
+
method: "DELETE",
|
|
262
|
+
...options,
|
|
263
|
+
...urlOf(url)
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
async* streamSse(config) {
|
|
267
|
+
const url = this.urlFor(config);
|
|
268
|
+
const method = config.method ?? "POST";
|
|
269
|
+
const startedAt = Date.now();
|
|
270
|
+
const { body, serialised } = this.bodyFor(config.payload);
|
|
271
|
+
const response = await this.send({ ...config, method }, url, body, serialised, "text/event-stream");
|
|
272
|
+
if (!response.ok || response.body === null) {
|
|
273
|
+
throw new FetchError(response.status, response.statusText, await readBody(response), { method, url: url.href, headers: response.headers });
|
|
274
|
+
}
|
|
275
|
+
const decoder = new TextDecoder;
|
|
276
|
+
let buffer = "";
|
|
277
|
+
try {
|
|
278
|
+
for await (const chunk of response.body) {
|
|
279
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
280
|
+
let newline = buffer.indexOf(`
|
|
281
|
+
`);
|
|
282
|
+
while (newline !== -1) {
|
|
283
|
+
const line = buffer.slice(0, newline).trim();
|
|
284
|
+
buffer = buffer.slice(newline + 1);
|
|
285
|
+
newline = buffer.indexOf(`
|
|
286
|
+
`);
|
|
287
|
+
if (!line.startsWith("data:"))
|
|
288
|
+
continue;
|
|
289
|
+
const data = line.slice(5).trim();
|
|
290
|
+
if (data === "[DONE]")
|
|
291
|
+
return;
|
|
292
|
+
yield data;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
} finally {
|
|
296
|
+
this.logger.debug(`SSE ${method} ${url.href} closed`, {
|
|
297
|
+
elapsedMs: Date.now() - startedAt
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
urlFor(config) {
|
|
302
|
+
const given = config.url === undefined ? undefined : String(config.url);
|
|
303
|
+
const absolute = given !== undefined && given !== "" && URL.canParse(given) ? given : undefined;
|
|
304
|
+
const relative = given === "" || absolute !== undefined ? undefined : given;
|
|
305
|
+
const base = absolute ?? this.options.baseUrl;
|
|
306
|
+
if (base === undefined) {
|
|
307
|
+
throw new FetchTransportError({ method: "GET", url: given ?? config.path ?? "(none)" }, false, {
|
|
308
|
+
cause: new Error("No url to call. Pass an absolute url, or set baseUrl on " + "HttpModule.forRoot and pass a path.")
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
const path = config.path ?? relative;
|
|
312
|
+
return this.buildUrl({
|
|
313
|
+
base,
|
|
314
|
+
...path === undefined ? {} : { path },
|
|
315
|
+
...config.pathParams === undefined ? {} : { pathParams: config.pathParams },
|
|
316
|
+
...config.queryParams === undefined ? {} : { queryParams: config.queryParams }
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
bodyFor(payload) {
|
|
320
|
+
if (payload === undefined || payload === null) {
|
|
321
|
+
return { body: undefined, serialised: "", json: false };
|
|
322
|
+
}
|
|
323
|
+
if (!isJsonBody(payload)) {
|
|
324
|
+
return { body: payload, serialised: "", json: false };
|
|
325
|
+
}
|
|
326
|
+
const serialised = JSON.stringify(payload);
|
|
327
|
+
return { body: serialised, serialised, json: true };
|
|
328
|
+
}
|
|
329
|
+
async send(config, url, body, serialised, accept = "application/json") {
|
|
330
|
+
const requestId = this.options.requestIdHeader === undefined ? undefined : this.requestContext.getContext().requestId;
|
|
331
|
+
const headers = {
|
|
332
|
+
accept,
|
|
333
|
+
...serialised === "" ? {} : { "content-type": "application/json" },
|
|
334
|
+
...this.options.headers,
|
|
335
|
+
...requestId === undefined || this.options.requestIdHeader === undefined ? {} : { [this.options.requestIdHeader]: requestId },
|
|
336
|
+
...config.headerFactory?.({
|
|
337
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
338
|
+
method: config.method,
|
|
339
|
+
requestPath: url.pathname + url.search,
|
|
340
|
+
body: serialised
|
|
341
|
+
}),
|
|
342
|
+
...config.headers
|
|
343
|
+
};
|
|
344
|
+
const timeoutMs = config.timeoutMs ?? this.options.timeoutMs;
|
|
345
|
+
const signals = [
|
|
346
|
+
...timeoutMs > 0 ? [AbortSignal.timeout(timeoutMs)] : [],
|
|
347
|
+
...config.signal === undefined ? [] : [config.signal]
|
|
348
|
+
];
|
|
349
|
+
try {
|
|
350
|
+
return await fetch(url.href, {
|
|
351
|
+
method: config.method,
|
|
352
|
+
headers,
|
|
353
|
+
...body === undefined ? {} : { body },
|
|
354
|
+
...signals.length === 0 ? {} : { signal: AbortSignal.any(signals) },
|
|
355
|
+
...this.options.fetchOptions
|
|
356
|
+
});
|
|
357
|
+
} catch (error) {
|
|
358
|
+
const aborted = error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
|
|
359
|
+
throw new FetchTransportError({ method: config.method, url: url.href }, aborted, { cause: error });
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
Object.defineProperty(HttpService, Symbol.for("dunx.deps"), {
|
|
364
|
+
value: () => [HttpClientOptions, Logger, RequestContext]
|
|
365
|
+
});
|
|
366
|
+
var urlOf = (url) => url === undefined ? {} : { url };
|
|
367
|
+
var readBody = async (response) => {
|
|
368
|
+
const text = await response.text().catch(() => "");
|
|
369
|
+
if (text === "")
|
|
370
|
+
return;
|
|
371
|
+
try {
|
|
372
|
+
return JSON.parse(text);
|
|
373
|
+
} catch {
|
|
374
|
+
return text;
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
var describeError = (error) => {
|
|
378
|
+
if (error instanceof FetchError) {
|
|
379
|
+
return {
|
|
380
|
+
name: error.name,
|
|
381
|
+
message: error.message,
|
|
382
|
+
status: error.status,
|
|
383
|
+
body: error.body
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
if (error instanceof Error) {
|
|
387
|
+
return { name: error.name, message: error.message };
|
|
388
|
+
}
|
|
389
|
+
return { message: String(error) };
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
// src/client/module.ts
|
|
393
|
+
var tokens = new Map;
|
|
394
|
+
var httpClient = (name) => {
|
|
395
|
+
const existing = tokens.get(name);
|
|
396
|
+
if (existing)
|
|
397
|
+
return existing;
|
|
398
|
+
const created = token(`HttpService(${name})`);
|
|
399
|
+
tokens.set(name, created);
|
|
400
|
+
return created;
|
|
401
|
+
};
|
|
402
|
+
var serviceFrom = (target, optionsToken) => provide(target, {
|
|
403
|
+
useFactory: (options, logger, context) => new HttpService(options, logger, context),
|
|
404
|
+
inject: [optionsToken, Logger2, RequestContext2]
|
|
405
|
+
});
|
|
406
|
+
var namedModule = (name, options) => {
|
|
407
|
+
const optionsToken = token(`HttpClientOptions(${name})`);
|
|
408
|
+
const optionsProvider = options instanceof HttpClientOptions ? provide(optionsToken, { useValue: options }) : provide(optionsToken, options);
|
|
409
|
+
return {
|
|
410
|
+
module: HttpModule,
|
|
411
|
+
providers: [optionsProvider, serviceFrom(httpClient(name), optionsToken)]
|
|
412
|
+
};
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
class HttpModule {
|
|
416
|
+
static forRoot(init = {}) {
|
|
417
|
+
const options = new HttpClientOptions(init);
|
|
418
|
+
if (options.name !== undefined)
|
|
419
|
+
return namedModule(options.name, options);
|
|
420
|
+
return {
|
|
421
|
+
module: HttpModule,
|
|
422
|
+
providers: [
|
|
423
|
+
provide(HttpClientOptions, { useValue: options }),
|
|
424
|
+
serviceFrom(HttpService, HttpClientOptions)
|
|
425
|
+
]
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
static forRootAsync(source, name) {
|
|
429
|
+
const load = typeof source === "function" ? source : source.useFactory;
|
|
430
|
+
const inject = typeof source === "function" ? [] : source.inject ?? [];
|
|
431
|
+
const useFactory = async (...deps) => new HttpClientOptions(await load(...deps));
|
|
432
|
+
if (name !== undefined) {
|
|
433
|
+
return namedModule(name, { useFactory, inject });
|
|
434
|
+
}
|
|
435
|
+
return {
|
|
436
|
+
module: HttpModule,
|
|
437
|
+
providers: [
|
|
438
|
+
provide(HttpClientOptions, { useFactory, inject }),
|
|
439
|
+
serviceFrom(HttpService, HttpClientOptions)
|
|
440
|
+
]
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
export {
|
|
445
|
+
safeStringify,
|
|
446
|
+
retryAfterMs,
|
|
447
|
+
isRetryableStatus,
|
|
448
|
+
isPlainObject,
|
|
449
|
+
isJsonBody,
|
|
450
|
+
httpClient,
|
|
451
|
+
executeWithRetry,
|
|
452
|
+
backoffDelay,
|
|
453
|
+
HttpService,
|
|
454
|
+
HttpModule,
|
|
455
|
+
HttpClientOptions,
|
|
456
|
+
FetchTransportError,
|
|
457
|
+
FetchError,
|
|
458
|
+
DEFAULT_REQUEST_ID_HEADER
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
//# debugId=4FED9F52C0748CFA64756E2164756E21
|
|
462
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/client/errors.ts", "../src/client/json.ts", "../src/client/options.ts", "../src/client/retry.ts", "../src/client/module.ts", "../src/client/service.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"import { AppError } from '@dunx/core';\n\n/**\n * Any non-2xx response from an outbound call, carrying the parsed body.\n *\n * **Deliberately not an `HttpError`.** `HttpError` is the inbound contract - the\n * default error mapper reads its `status` and answers the caller with it - so an\n * upstream 401 arriving as an `HttpError(401)` would make this service reply 401,\n * telling *its* client \"you are unauthorized\" when what actually happened is that\n * this service could not authenticate upstream. Extending `AppError` instead means\n * an unhandled upstream failure surfaces as a 500, which is the honest default, and\n * a caller who knows better maps it:\n *\n * ```ts\n * try {\n * return await this.http.get(url);\n * } catch (error) {\n * if (error instanceof FetchError && error.status === 404) return null;\n * throw new HttpError(HttpStatusCode.BAD_GATEWAY, 'upstream unavailable');\n * }\n * ```\n */\nexport class FetchError extends AppError {\n override readonly name = 'FetchError';\n\n constructor(\n readonly status: number,\n readonly statusText: string,\n /** The response body, parsed as JSON when it was, else text, else undefined. */\n readonly body: unknown,\n readonly response: {\n readonly method: string;\n readonly url: string;\n readonly headers: Headers;\n },\n ) {\n super(\n `HTTP ${status} ${statusText} from ${response.method} ${response.url}`,\n );\n }\n}\nObject.defineProperty(FetchError, Symbol.for('dunx.deps'), {\n value: () => [{ unresolved: \"readonly status: number\" }, { unresolved: \"readonly statusText: string\" }, { unresolved: \"readonly body: unknown\" }, { unresolved: \"readonly response: {\\n readonly method: string;\\n readonly url: string;\\n readonly headers: Headers;\\n }\" }],\n});\n\n/**\n * The request never produced a response: DNS failure, connection refused, TLS\n * rejection, or the timeout firing. `fetch` reports these as a `TypeError` or an\n * `AbortError`, neither of which says which call died.\n *\n * Separate from {@link FetchError} because there is no status to branch on and the\n * retry decision is different: a transport failure is worth retrying by default,\n * while a 400 never is.\n */\nexport class FetchTransportError extends AppError {\n override readonly name = 'FetchTransportError';\n\n constructor(\n readonly response: { readonly method: string; readonly url: string },\n /** True when the timeout or the caller's signal aborted it. */\n readonly aborted: boolean,\n options?: ErrorOptions,\n ) {\n super(\n `${response.method} ${response.url} failed: ${\n aborted ? 'aborted' : 'transport error'\n }`,\n options,\n );\n }\n}\nObject.defineProperty(FetchTransportError, Symbol.for('dunx.deps'), {\n value: () => [{ unresolved: \"readonly response: { readonly method: string; readonly url: string }\" }, { unresolved: \"readonly aborted: boolean\" }, ErrorOptions],\n});\n",
|
|
6
|
+
"/**\n * A `JSON.stringify` that survives a cycle. **For logging only.**\n *\n * Never for a request body. The implementation this was ported from used it for\n * both, so a circular payload was *sent* upstream as `\"[Circular]\"` - a wrong body\n * that reads as a successful call and comes back as someone else's 400. A body goes\n * through plain `JSON.stringify`, which throws, because a cycle there is a bug in\n * the caller and should say so.\n */\nexport const safeStringify = (value: unknown): string => {\n const seen = new WeakSet<object>();\n return JSON.stringify(value, (_key, entry: unknown) => {\n if (typeof entry === 'object' && entry !== null) {\n if (seen.has(entry)) return '[Circular]';\n seen.add(entry);\n }\n return entry;\n });\n};\n\n/**\n * A plain object: `{}`, `Object.create(null)`, or a JSON-parsed value. Anything\n * with its own prototype - `Date`, `Map`, `Error`, a class instance - is not one.\n *\n * The prototype check rather than the reference's `typeof === 'object' && !Array\n * && !(instanceof Error)`, which answered `true` for a `Date` and for every class\n * instance, so \"is this a plain object\" did not mean what it said. Body routing\n * does not use this - see {@link isJsonBody} - so tightening it changes no\n * behaviour beyond making the predicate honest.\n */\nexport const isPlainObject = (\n value: unknown,\n): value is Record<string, unknown> => {\n if (typeof value !== 'object' || value === null) return false;\n const proto = Object.getPrototypeOf(value) as object | null;\n return proto === Object.prototype || proto === null;\n};\n\n/**\n * Whether a payload should be JSON-encoded, or handed to `fetch` as-is.\n *\n * `fetch` already knows what to do with a `BodyInit` - it sets the boundary for a\n * `FormData`, the content type for a `URLSearchParams`, streams a `ReadableStream`\n * - so the only question is whether this value is one. Everything else, including\n * a `Date` or a class instance, is JSON: that is what `JSON.stringify` is for.\n *\n * Listed explicitly rather than inferred from `isPlainObject`, because the two\n * questions have different answers. `new Date()` is not a plain object but is\n * JSON-encodable; a `Blob` is neither.\n */\nexport const isJsonBody = (payload: unknown): boolean => {\n if (payload === null || payload === undefined) return false;\n if (typeof payload !== 'object') return typeof payload !== 'string';\n return !(\n payload instanceof FormData ||\n payload instanceof URLSearchParams ||\n payload instanceof Blob ||\n payload instanceof ArrayBuffer ||\n payload instanceof ReadableStream ||\n ArrayBuffer.isView(payload)\n );\n};\n",
|
|
7
|
+
"import type { RetryOptions } from './retry.js';\n\n/**\n * Named `HttpClientOptions`, not `HttpOptions`: the server half already exports\n * that from `@dunx/http` for `HttpFactory.create`, and two things called\n * `HttpOptions` meaning opposite directions of traffic is the confusion this\n * subpath exists to avoid.\n */\nexport interface HttpClientOptionsInit {\n /**\n * Prefixed to a relative `path`. With it, calls name a path; without it, every\n * call passes a whole url.\n */\n readonly baseUrl?: string | URL;\n /** Per-request budget, enforced with `AbortSignal.timeout`. @default 30000 */\n readonly timeoutMs?: number;\n /** Sent on every request, under anything a call sets itself. */\n readonly headers?: Readonly<Record<string, string>>;\n readonly retry?: RetryOptions<unknown>;\n /**\n * Forward the inbound request id to the upstream, so one trace spans both\n * services. `true` uses `x-request-id`; a string names the header. Read from\n * `RequestContext`, so it only carries when there is a request in scope.\n *\n * @default true\n */\n readonly propagateRequestId?: boolean | string;\n /** Bound as its own token, so a second client can be injected by name. */\n readonly name?: string;\n /**\n * Bun-only `fetch` extensions, passed straight through. None of these exist on\n * Node's fetch, and they are the reason an outbound client on Bun can do things a\n * ported one cannot: talk through a proxy, pin a certificate, or reach a unix\n * socket, with no dependency.\n */\n readonly proxy?: string;\n readonly tls?: Bun.TLSOptions;\n readonly unix?: string;\n /** @default true - Bun decompresses by default. */\n readonly decompress?: boolean;\n /** Bun's own request/response tracing on stderr. Never on in production. */\n readonly verbose?: boolean;\n}\n\nexport const DEFAULT_REQUEST_ID_HEADER = 'x-request-id';\n\n/**\n * The resolved options, as a class so it is both the injection token and the type\n * a factory annotates - the same trick `RedisOptions` and `ConfigService` use.\n */\nexport class HttpClientOptions {\n readonly baseUrl: string | undefined;\n readonly timeoutMs: number;\n readonly headers: Readonly<Record<string, string>>;\n readonly retry: RetryOptions<unknown>;\n readonly requestIdHeader: string | undefined;\n readonly name: string | undefined;\n readonly fetchOptions: Readonly<Record<string, unknown>>;\n\n constructor(init: HttpClientOptionsInit = {}) {\n this.baseUrl =\n init.baseUrl === undefined ? undefined : String(init.baseUrl);\n this.timeoutMs = init.timeoutMs ?? 30_000;\n this.headers = init.headers ?? {};\n this.retry = init.retry ?? {};\n this.name = init.name;\n\n const propagate = init.propagateRequestId ?? true;\n this.requestIdHeader =\n propagate === false\n ? undefined\n : propagate === true\n ? DEFAULT_REQUEST_ID_HEADER\n : propagate;\n\n // Only the keys actually set: `exactOptionalPropertyTypes` means passing\n // `proxy: undefined` is not the same as omitting it, and Bun reads presence.\n this.fetchOptions = Object.fromEntries(\n (\n [\n ['proxy', init.proxy],\n ['tls', init.tls],\n ['unix', init.unix],\n ['decompress', init.decompress],\n ['verbose', init.verbose],\n ] as const\n ).filter(([, value]) => value !== undefined),\n );\n }\n}\nObject.defineProperty(HttpClientOptions, Symbol.for('dunx.deps'), {\n value: () => [{ unresolved: \"init: HttpClientOptionsInit = {}\" }],\n});\n",
|
|
8
|
+
"import { HttpStatusCode } from '../server/status.js';\nimport { FetchError, FetchTransportError } from './errors.js';\n\n/**\n * Retry, backoff and `Retry-After`, with no dependency.\n *\n * `crypto.getRandomValues` supplies the jitter. `Math.random` is what the source\n * this was ported from used and is banned repo-wide for anything that matters -\n * jitter matters, because decorrelating retries is the whole reason it exists. The\n * alternative, `@arkv/rng`, is a 64 KB WebAssembly PRNG, which is a lot of weight\n * to put in every deployment of the most-imported package to choose a number of\n * milliseconds. `crypto.getRandomValues` is a Web standard Bun implements natively,\n * is a CSPRNG, and costs nothing.\n */\nconst uniform = (): number => {\n const buffer = new Uint32Array(1);\n crypto.getRandomValues(buffer);\n // 2**32 rather than 0xffffffff, so the result is [0, 1) and never exactly 1.\n return (buffer[0] ?? 0) / 2 ** 32;\n};\n\nexport interface BackoffOptions {\n /** Base delay, doubled each attempt. */\n readonly baseMs: number;\n /** @default 2 */\n readonly power?: number;\n /** Upper bound of the random component added to each delay. @default 1000 */\n readonly jitterMs?: number;\n /** @default 30000 */\n readonly maxMs?: number;\n}\n\n/** `base * power^attempt + jitter`, capped. `attempt` is 0 for the first retry. */\nexport const backoffDelay = (\n attempt: number,\n { baseMs, power = 2, jitterMs = 1000, maxMs = 30_000 }: BackoffOptions,\n): number => Math.min(baseMs * power ** attempt + uniform() * jitterMs, maxMs);\n\n/**\n * The wait an upstream asked for, in ms, or undefined.\n *\n * RFC 9110 allows either a delay in seconds or an HTTP date, and both appear in\n * the wild - GitHub sends seconds, some CDNs send a date. Ignoring the header, as\n * the reference did, means retrying straight back into a rate limit that had just\n * told you exactly how long to wait.\n */\nexport const retryAfterMs = (\n headers: Headers,\n now: number = Date.now(),\n): number | undefined => {\n const header = headers.get('retry-after');\n if (header === null) return undefined;\n\n const seconds = Number(header);\n if (Number.isFinite(seconds)) return Math.max(0, seconds * 1000);\n\n const at = Date.parse(header);\n return Number.isNaN(at) ? undefined : Math.max(0, at - now);\n};\n\n/**\n * Statuses worth trying again: a server that failed, one that is overloaded, and\n * one that timed out. Deliberately narrower than the source, which also retried\n * 409 and 422 - both of those are the server rejecting the *request*, and sending\n * it again unchanged gets the same answer.\n */\nexport const isRetryableStatus = (status: number): boolean =>\n status >= HttpStatusCode.INTERNAL_SERVER_ERROR ||\n status === HttpStatusCode.REQUEST_TIMEOUT ||\n status === HttpStatusCode.TOO_MANY_REQUESTS;\n\nexport interface RetryOptions<T> {\n /** Retries *after* the first attempt, so 3 means up to 4 calls. @default 3 */\n readonly maxRetries?: number;\n /** @default 1000 */\n readonly retryDelayMs?: number;\n readonly backoff?: Omit<BackoffOptions, 'baseMs'>;\n /** @default isRetryableStatus */\n readonly shouldRetryOnStatus?: (status: number) => boolean;\n /** Honour a `Retry-After` header over the computed backoff. @default true */\n readonly respectRetryAfter?: boolean;\n readonly onAttempt?: (attempt: number, isRetry: boolean) => void;\n readonly onError?: (\n error: unknown,\n attempt: number,\n willRetry: boolean,\n ) => void;\n readonly onSuccess?: (result: T, attempt: number) => void;\n}\n\n/**\n * Whether an error is worth another attempt, and how long to wait first.\n *\n * An abort is never retried: the caller's signal fired or the timeout expired, and\n * both mean the budget for this call is spent. A transport failure is retried,\n * because a refused connection is the case retrying exists for.\n */\nconst decide = <T>(\n error: unknown,\n attempt: number,\n options: RetryOptions<T>,\n): { readonly retry: boolean; readonly delayMs: number } => {\n const {\n retryDelayMs = 1000,\n backoff,\n shouldRetryOnStatus = isRetryableStatus,\n respectRetryAfter = true,\n } = options;\n const computed = backoffDelay(attempt, { baseMs: retryDelayMs, ...backoff });\n\n if (error instanceof FetchTransportError) {\n return { retry: !error.aborted, delayMs: computed };\n }\n\n if (error instanceof FetchError) {\n if (!shouldRetryOnStatus(error.status)) return { retry: false, delayMs: 0 };\n const asked = respectRetryAfter\n ? retryAfterMs(error.response.headers)\n : undefined;\n // Still capped by the backoff ceiling: an upstream asking for an hour should\n // not park a request handler for an hour.\n const maxMs = backoff?.maxMs ?? 30_000;\n return {\n retry: true,\n delayMs: asked === undefined ? computed : Math.min(asked, maxMs),\n };\n }\n\n // Something other than a fetch failure - a JSON parse, a callback throwing.\n // Retried, matching the source, because a non-HTTP error carries no verdict.\n return { retry: true, delayMs: computed };\n};\n\n/**\n * Runs `operation`, retrying per `options`.\n *\n * `Bun.sleep` rather than a `setTimeout` promise: it is the runtime's own timer and\n * needs no wrapper.\n */\nexport const executeWithRetry = async <T>(\n operation: () => Promise<T> | T,\n options: RetryOptions<T> = {},\n): Promise<T> => {\n const { maxRetries = 3, onAttempt, onError, onSuccess } = options;\n let lastError: unknown;\n\n for (let attempt = 0; attempt <= maxRetries; attempt += 1) {\n onAttempt?.(attempt + 1, attempt > 0);\n try {\n const result = await operation();\n onSuccess?.(result, attempt + 1);\n return result;\n } catch (error) {\n lastError = error;\n const { retry, delayMs } = decide(error, attempt, options);\n const willRetry = retry && attempt < maxRetries;\n onError?.(error, attempt + 1, willRetry);\n\n if (!willRetry) throw error;\n await Bun.sleep(delayMs);\n }\n }\n\n // Unreachable: the loop either returns or throws. Kept so the signature does not\n // need `T | undefined`.\n throw lastError;\n};\n",
|
|
9
|
+
"import {\n Logger,\n provide,\n RequestContext,\n token,\n type Deps,\n type DynamicModule,\n type FactoryProvider,\n type Token,\n} from '@dunx/core';\nimport { HttpClientOptions, type HttpClientOptionsInit } from './options.js';\nimport { HttpService } from './service.js';\n\nconst tokens = new Map<string, Token<HttpService>>();\n\n/**\n * The token a named client is bound to.\n *\n * Memoised, because `token()` returns a fresh object every call - without this the\n * module and the consumer would hold different tokens for `'stripe'` and the lookup\n * would miss. Same name in, same token out.\n *\n * A `Token` is not a constructor type, so a named client cannot be a constructor\n * parameter. Reach it with `inject()` in a field initialiser:\n *\n * ```ts\n * class Payments {\n * readonly stripe = inject(httpClient('stripe'));\n * }\n * ```\n */\nexport const httpClient = (name: string): Token<HttpService> => {\n const existing = tokens.get(name);\n if (existing) return existing;\n const created = token<HttpService>(`HttpService(${name})`);\n tokens.set(name, created);\n return created;\n};\n\nconst serviceFrom = (\n target: Token<HttpService> | typeof HttpService,\n optionsToken: Token<HttpClientOptions> | typeof HttpClientOptions,\n) =>\n provide(target, {\n useFactory: (\n options: HttpClientOptions,\n logger: Logger,\n context: RequestContext,\n ) => new HttpService(options, logger, context),\n inject: [optionsToken, Logger, RequestContext] as const,\n });\n\n/**\n * A named client binds its own options token, so two of them do not collide on\n * `HttpClientOptions` - the flat container reports that as a duplicate binding.\n */\nconst namedModule = (\n name: string,\n options: HttpClientOptions | FactoryProvider<HttpClientOptions, Deps>,\n): DynamicModule => {\n const optionsToken = token<HttpClientOptions>(`HttpClientOptions(${name})`);\n const optionsProvider =\n options instanceof HttpClientOptions\n ? provide(optionsToken, { useValue: options })\n : provide(optionsToken, options);\n\n return {\n module: HttpModule,\n providers: [optionsProvider, serviceFrom(httpClient(name), optionsToken)],\n };\n};\n\n/**\n * The outbound half of `@dunx/http`.\n *\n * Named `HttpModule` and `HttpService` under the `./client` subpath rather than in\n * the root barrel, where `HttpFactory` already means the inbound direction. The\n * subpath is what keeps the name unambiguous at the import site:\n *\n * ```ts\n * import { HttpFactory } from '@dunx/http'; // serving\n * import { HttpModule } from '@dunx/http/client'; // calling out\n * ```\n *\n * It depends on `Logger` and `RequestContext`, both of which core always binds, so\n * it works in an app that imported no logging module at all.\n */\nexport class HttpModule {\n /**\n * Binds `HttpService` and `HttpClientOptions`, or `httpClient(init.name)` alone\n * when `name` is set - a named registration deliberately does not also claim\n * `HttpService`, so several upstreams can coexist alongside one default.\n */\n static forRoot(init: HttpClientOptionsInit = {}): DynamicModule {\n const options = new HttpClientOptions(init);\n if (options.name !== undefined) return namedModule(options.name, options);\n\n return {\n module: HttpModule,\n providers: [\n provide(HttpClientOptions, { useValue: options }),\n serviceFrom(HttpService, HttpClientOptions),\n ],\n };\n }\n\n /**\n * `forRoot` with the options behind a factory, which is the one thing a\n * zero-argument `forRoot` cannot do: read the base url or the timeout off\n * `ConfigService`.\n *\n * There is no separate async machinery - the container resolves eagerly and\n * awaits factories before any constructor runs, so awaited config is settled by\n * the time anything is built.\n *\n * ```ts\n * HttpModule.forRootAsync({\n * useFactory: (config: AppConfigService) => ({\n * baseUrl: config.get('upstream').url,\n * }),\n * inject: [AppConfigService],\n * });\n * ```\n *\n * `name` is a parameter rather than a field of the awaited init, because the\n * token has to exist before the factory runs.\n */\n static forRootAsync(\n load: () => HttpClientOptionsInit | Promise<HttpClientOptionsInit>,\n name?: string,\n ): DynamicModule;\n static forRootAsync<const D extends Deps>(\n config: FactoryProvider<HttpClientOptionsInit, D>,\n name?: string,\n ): DynamicModule;\n static forRootAsync(\n source:\n | (() => HttpClientOptionsInit | Promise<HttpClientOptionsInit>)\n | FactoryProvider<HttpClientOptionsInit, Deps>,\n name?: string,\n ): DynamicModule {\n const load = typeof source === 'function' ? source : source.useFactory;\n const inject = typeof source === 'function' ? [] : (source.inject ?? []);\n const useFactory = async (\n ...deps: readonly unknown[]\n ): Promise<HttpClientOptions> => new HttpClientOptions(await load(...deps));\n\n if (name !== undefined) {\n return namedModule(name, { useFactory, inject } as FactoryProvider<\n HttpClientOptions,\n Deps\n >);\n }\n\n return {\n module: HttpModule,\n providers: [\n provide(HttpClientOptions, { useFactory, inject } as FactoryProvider<\n HttpClientOptions,\n Deps\n >),\n serviceFrom(HttpService, HttpClientOptions),\n ],\n };\n }\n}\n",
|
|
10
|
+
"import { Logger, RequestContext } from '@dunx/core';\nimport { UrlHelper, type ParamsType } from '@arkv/shared';\nimport type { HttpMethod } from '../route/marker.js';\nimport { FetchError, FetchTransportError } from './errors.js';\nimport { isJsonBody, safeStringify } from './json.js';\nimport { HttpClientOptions } from './options.js';\nimport { executeWithRetry, type RetryOptions } from './retry.js';\n\n/** The client speaks two more verbs than a route can declare. */\nexport type RequestMethod = HttpMethod | 'HEAD' | 'OPTIONS';\n\n/**\n * `@arkv/shared`'s own param type, imported rather than restated - a local copy\n * would drift from what `buildUrl` actually accepts, which is how `null` ended up\n * in the first draft of this file and `interpolate` would never have seen it.\n */\ntype Params = ParamsType;\n\n/**\n * `fetch`'s own body type, derived from its signature. `BodyInit` is not a global\n * here: the root tsconfig sets `lib: [\"ESNext\"]` with no DOM, so the name does not\n * exist even though the value does. Reading it off `typeof fetch` needs no lib and\n * cannot disagree with the runtime.\n */\ntype FetchBody = NonNullable<NonNullable<Parameters<typeof fetch>[1]>['body']>;\n\nexport type HeaderFactory = (params: {\n /** Unix seconds, which is what every HMAC scheme signs. */\n readonly timestamp: number;\n readonly method: RequestMethod;\n /** `pathname + search`, the part such schemes sign. */\n readonly requestPath: string;\n /** The serialised body, or `''`. */\n readonly body: string;\n}) => Record<string, string>;\n\nexport interface RequestConfig<TRequest = unknown, TResponse = unknown> {\n readonly method: RequestMethod;\n /** Absolute, or relative to `baseUrl`. Omit when `baseUrl` plus `path` is enough. */\n readonly url?: string | URL;\n readonly payload?: TRequest;\n readonly headers?: Readonly<Record<string, string>>;\n /** Appended to the base, with `{param}` interpolated from `pathParams`. */\n readonly path?: string;\n readonly pathParams?: Params;\n readonly queryParams?: Params;\n /** Overrides the client's default budget. */\n readonly timeoutMs?: number;\n /** Called once per attempt, so a signature covers the body it is sent with. */\n readonly headerFactory?: HeaderFactory;\n /** Merged into the async context for this call, so its logs carry it. */\n readonly flow?: string;\n readonly retry?: RetryOptions<TResponse>;\n /** Cancels the call. Combined with the timeout, whichever fires first. */\n readonly signal?: AbortSignal;\n}\n\ntype BaseOptions<TRequest, TResponse> = Omit<\n RequestConfig<TRequest, TResponse>,\n 'method' | 'url' | 'payload'\n>;\n\n/**\n * What `send` reads. Narrower than `RequestConfig` on purpose: `RetryOptions<T>` is\n * invariant in `T` - its `onSuccess` takes a `T` and its callbacks return one - so a\n * `RequestConfig<_, TResponse>` is not assignable to a `RequestConfig<_, unknown>`.\n * `send` never touches `retry`, so leaving it out is both true and assignable.\n */\ninterface SendConfig {\n readonly method: RequestMethod;\n readonly headers?: Readonly<Record<string, string>>;\n readonly timeoutMs?: number;\n readonly headerFactory?: HeaderFactory;\n readonly signal?: AbortSignal;\n}\n\n/**\n * A `fetch` client with a per-request timeout, retry with backoff, request-id\n * propagation and one log line per call.\n *\n * `fetch` and nothing else: it is a Web standard Bun implements natively, so there\n * is no client dependency to justify - which is also why `axios` and `node-fetch`\n * are banned repo-wide. What this adds over calling `fetch` yourself is the parts\n * every caller otherwise reimplements slightly differently: the timeout, the\n * retry policy, `Retry-After`, url building, and a failure that says which call\n * failed.\n *\n * Extends `UrlHelper` from `@arkv/shared`, so `buildUrl` and `interpolate` are\n * available on the service, and there is one implementation of them across the\n * owner's projects rather than a fork per repo.\n */\nexport class HttpService extends UrlHelper {\n constructor(\n private readonly options: HttpClientOptions,\n private readonly logger: Logger,\n private readonly requestContext: RequestContext,\n ) {\n super();\n }\n\n async request<TRequest = unknown, TResponse = unknown>(\n config: RequestConfig<TRequest, TResponse>,\n ): Promise<TResponse> {\n const url = this.urlFor(config);\n const startedAt = Date.now();\n let attempts = 0;\n let status: number | undefined;\n\n /**\n * Serialised **once**, outside the retry loop. A body does not change between\n * attempts - only the signature over it does, and `headerFactory` gets a fresh\n * timestamp per attempt from `send`.\n *\n * Doing it inside meant a caller's own `JSON.stringify` failure, a circular\n * payload, was treated as a retryable error: three attempts and eight seconds of\n * backoff before surfacing a bug that no amount of retrying could fix. It also\n * re-serialised a large body on every attempt.\n */\n const { body, serialised } = this.bodyFor(config.payload);\n\n /**\n * A stream body is consumed by the first attempt, so a second would send an\n * empty one. Retrying is switched off rather than left to fail as a confusing\n * \"body already used\" on the retry.\n */\n const replayable = !(config.payload instanceof ReadableStream);\n\n const attempt = async (): Promise<TResponse> => {\n attempts += 1;\n const response = await this.send(config, url, body, serialised);\n status = response.status;\n\n if (!response.ok) {\n throw new FetchError(\n response.status,\n response.statusText,\n await readBody(response),\n {\n method: config.method,\n url: url.href,\n headers: response.headers,\n },\n );\n }\n\n return (await readBody(response)) as TResponse;\n };\n\n const describe = (): string => `${config.method} ${url.href}`;\n\n try {\n const result = await this.requestContext.runWithContext(\n {\n ...(config.flow === undefined ? {} : { flow: config.flow }),\n event: config.path ?? url.pathname,\n },\n () =>\n executeWithRetry(attempt, {\n ...this.options.retry,\n ...config.retry,\n ...(replayable ? {} : { maxRetries: 0 }),\n } as RetryOptions<TResponse>),\n );\n\n this.logger.debug(`${describe()} succeeded`, {\n status,\n attempts,\n elapsedMs: Date.now() - startedAt,\n });\n return result;\n } catch (error) {\n this.logger.error(`${describe()} failed`, {\n // `safeStringify`, not the error object: an upstream body can carry a cycle\n // and this is the one place that must not throw while reporting a throw.\n err: safeStringify(describeError(error)),\n attempts,\n elapsedMs: Date.now() - startedAt,\n });\n throw error;\n }\n }\n\n get<TResponse = unknown>(\n url?: string | URL,\n options?: BaseOptions<never, TResponse>,\n ): Promise<TResponse> {\n return this.request<never, TResponse>({\n method: 'GET',\n ...options,\n ...urlOf(url),\n });\n }\n\n post<TRequest = unknown, TResponse = unknown>(\n url?: string | URL,\n payload?: TRequest,\n options?: BaseOptions<TRequest, TResponse>,\n ): Promise<TResponse> {\n return this.request<TRequest, TResponse>({\n method: 'POST',\n ...options,\n ...urlOf(url),\n ...(payload === undefined ? {} : { payload }),\n });\n }\n\n put<TRequest = unknown, TResponse = unknown>(\n url?: string | URL,\n payload?: TRequest,\n options?: BaseOptions<TRequest, TResponse>,\n ): Promise<TResponse> {\n return this.request<TRequest, TResponse>({\n method: 'PUT',\n ...options,\n ...urlOf(url),\n ...(payload === undefined ? {} : { payload }),\n });\n }\n\n patch<TRequest = unknown, TResponse = unknown>(\n url?: string | URL,\n payload?: TRequest,\n options?: BaseOptions<TRequest, TResponse>,\n ): Promise<TResponse> {\n return this.request<TRequest, TResponse>({\n method: 'PATCH',\n ...options,\n ...urlOf(url),\n ...(payload === undefined ? {} : { payload }),\n });\n }\n\n delete<TResponse = unknown>(\n url?: string | URL,\n options?: BaseOptions<never, TResponse>,\n ): Promise<TResponse> {\n return this.request<never, TResponse>({\n method: 'DELETE',\n ...options,\n ...urlOf(url),\n });\n }\n\n /**\n * Yields each `data:` payload of a Server-Sent-Events response, consuming the\n * terminating `[DONE]` sentinel rather than yielding it.\n *\n * **No retry**, deliberately: a partially consumed stream cannot be replayed, so\n * retrying would re-deliver events the caller has already seen. The timeout\n * covers the connect only - it is dropped once headers arrive, or a long-lived\n * stream would be cut off mid-flight.\n *\n * Hand-rolled rather than delegated: Bun exposes no `EventSource` global and no\n * SSE parser, which was measured rather than assumed.\n */\n async *streamSse<TRequest = unknown>(\n config: Omit<RequestConfig<TRequest>, 'method' | 'retry'> & {\n readonly method?: 'GET' | 'POST';\n },\n ): AsyncGenerator<string> {\n const url = this.urlFor(config);\n const method = config.method ?? 'POST';\n const startedAt = Date.now();\n const { body, serialised } = this.bodyFor(config.payload);\n\n const response = await this.send(\n { ...config, method },\n url,\n body,\n serialised,\n 'text/event-stream',\n );\n\n if (!response.ok || response.body === null) {\n throw new FetchError(\n response.status,\n response.statusText,\n await readBody(response),\n { method, url: url.href, headers: response.headers },\n );\n }\n\n const decoder = new TextDecoder();\n let buffer = '';\n\n try {\n // Async iteration, not `getReader()`: it acquires the reader and releases it\n // on completion, on `break`, and on the `return` below when `[DONE]` arrives -\n // which is the case the manual form needed `releaseLock()` in a `finally` for.\n for await (const chunk of response.body) {\n buffer += decoder.decode(chunk, { stream: true });\n\n let newline = buffer.indexOf('\\n');\n while (newline !== -1) {\n const line = buffer.slice(0, newline).trim();\n buffer = buffer.slice(newline + 1);\n newline = buffer.indexOf('\\n');\n\n if (!line.startsWith('data:')) continue;\n const data = line.slice(5).trim();\n if (data === '[DONE]') return;\n yield data;\n }\n }\n } finally {\n this.logger.debug(`SSE ${method} ${url.href} closed`, {\n elapsedMs: Date.now() - startedAt,\n });\n }\n }\n\n /**\n * Resolves the target, accepting the three forms a caller actually reaches for:\n * an absolute url, a path relative to `baseUrl`, or `baseUrl` plus an explicit\n * `path`.\n *\n * `get('/users')` is the one worth calling out. A relative first argument is what\n * every HTTP client takes once a base url exists, and passing it straight to\n * `buildUrl` throws `ERR_INVALID_URL` from inside `new URL()` - a message naming\n * neither the call nor the missing base. So a first argument that is not an\n * absolute url is treated as the path, which is what it reads as.\n *\n * `URL.canParse` decides, rather than a regex over `//` or `:` - it is the same\n * parser `new URL` uses, so the two cannot disagree.\n */\n private urlFor(config: {\n readonly url?: string | URL;\n readonly path?: string;\n readonly pathParams?: Params;\n readonly queryParams?: Params;\n }): URL {\n const given = config.url === undefined ? undefined : String(config.url);\n const absolute =\n given !== undefined && given !== '' && URL.canParse(given)\n ? given\n : undefined;\n const relative = given === '' || absolute !== undefined ? undefined : given;\n const base = absolute ?? this.options.baseUrl;\n\n if (base === undefined) {\n throw new FetchTransportError(\n { method: 'GET', url: given ?? config.path ?? '(none)' },\n false,\n {\n cause: new Error(\n 'No url to call. Pass an absolute url, or set baseUrl on ' +\n 'HttpModule.forRoot and pass a path.',\n ),\n },\n );\n }\n\n // An explicit `path` wins over a relative first argument, so a call cannot\n // silently request two different paths.\n const path = config.path ?? relative;\n\n return this.buildUrl({\n base,\n ...(path === undefined ? {} : { path }),\n ...(config.pathParams === undefined\n ? {}\n : { pathParams: config.pathParams }),\n ...(config.queryParams === undefined\n ? {}\n : { queryParams: config.queryParams }),\n });\n }\n\n /** `serialised` is what a `headerFactory` signs, and is `''` for no body. */\n private bodyFor(payload: unknown): {\n body: FetchBody | undefined;\n serialised: string;\n json: boolean;\n } {\n if (payload === undefined || payload === null) {\n return { body: undefined, serialised: '', json: false };\n }\n if (!isJsonBody(payload)) {\n return { body: payload as FetchBody, serialised: '', json: false };\n }\n // Plain `JSON.stringify`, deliberately not `safeStringify`: a cycle here must\n // throw rather than be sent upstream as \"[Circular]\".\n const serialised = JSON.stringify(payload);\n return { body: serialised, serialised, json: true };\n }\n\n private async send(\n config: SendConfig,\n url: URL,\n body: FetchBody | undefined,\n serialised: string,\n accept = 'application/json',\n ): Promise<Response> {\n const requestId =\n this.options.requestIdHeader === undefined\n ? undefined\n : this.requestContext.getContext().requestId;\n\n const headers: Record<string, string> = {\n accept,\n ...(serialised === '' ? {} : { 'content-type': 'application/json' }),\n ...this.options.headers,\n ...(requestId === undefined || this.options.requestIdHeader === undefined\n ? {}\n : { [this.options.requestIdHeader]: requestId }),\n ...config.headerFactory?.({\n timestamp: Math.floor(Date.now() / 1000),\n method: config.method,\n requestPath: url.pathname + url.search,\n body: serialised,\n }),\n ...config.headers,\n };\n\n /**\n * `AbortSignal.timeout` plus `AbortSignal.any`, rather than an\n * `AbortController` with a `setTimeout` and a `clearTimeout` in a `finally`.\n * Both are Web standards Bun implements, the timer is the runtime's to cancel,\n * and combining the caller's signal with the budget is one call instead of a\n * second listener that has to be removed.\n */\n const timeoutMs = config.timeoutMs ?? this.options.timeoutMs;\n const signals = [\n ...(timeoutMs > 0 ? [AbortSignal.timeout(timeoutMs)] : []),\n ...(config.signal === undefined ? [] : [config.signal]),\n ];\n\n try {\n return await fetch(url.href, {\n method: config.method,\n headers,\n ...(body === undefined ? {} : { body }),\n ...(signals.length === 0 ? {} : { signal: AbortSignal.any(signals) }),\n ...this.options.fetchOptions,\n });\n } catch (error) {\n // `fetch` reports a refused connection, a DNS failure and an abort all as\n // exceptions with nothing naming the call. Wrapped so the message does.\n const aborted =\n error instanceof Error &&\n (error.name === 'AbortError' || error.name === 'TimeoutError');\n throw new FetchTransportError(\n { method: config.method, url: url.href },\n aborted,\n { cause: error },\n );\n }\n }\n}\nObject.defineProperty(HttpService, Symbol.for('dunx.deps'), {\n value: () => [HttpClientOptions, Logger, RequestContext],\n});\n\nconst urlOf = (url?: string | URL): { url?: string | URL } =>\n url === undefined ? {} : { url };\n\n/** JSON when the upstream said so or the body parses; text otherwise; undefined for empty. */\nconst readBody = async (response: Response): Promise<unknown> => {\n const text = await response.text().catch(() => '');\n if (text === '') return undefined;\n try {\n return JSON.parse(text) as unknown;\n } catch {\n return text;\n }\n};\n\nconst describeError = (error: unknown): Record<string, unknown> => {\n if (error instanceof FetchError) {\n return {\n name: error.name,\n message: error.message,\n status: error.status,\n body: error.body,\n };\n }\n if (error instanceof Error) {\n return { name: error.name, message: error.message };\n }\n return { message: String(error) };\n};\n"
|
|
11
|
+
],
|
|
12
|
+
"mappings": "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",
|
|
13
|
+
"debugId": "4FED9F52C0748CFA64756E2164756E21",
|
|
14
|
+
"names": []
|
|
15
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
import {
|
|
3
|
+
HttpStatusCode
|
|
4
|
+
} from "./chunk-x80f562w.js";
|
|
5
|
+
|
|
2
6
|
// src/route/marker.ts
|
|
3
7
|
var ROUTE = Symbol.for("dunx.route");
|
|
4
8
|
var CONTROLLER = Symbol.for("dunx.controller");
|
|
@@ -143,41 +147,6 @@ var buildContext = (route) => {
|
|
|
143
147
|
get: (key) => record.get(key.id)
|
|
144
148
|
});
|
|
145
149
|
};
|
|
146
|
-
// src/server/status.ts
|
|
147
|
-
var HttpStatusCode = Object.freeze({
|
|
148
|
-
OK: 200,
|
|
149
|
-
CREATED: 201,
|
|
150
|
-
ACCEPTED: 202,
|
|
151
|
-
NO_CONTENT: 204,
|
|
152
|
-
MOVED_PERMANENTLY: 301,
|
|
153
|
-
FOUND: 302,
|
|
154
|
-
NOT_MODIFIED: 304,
|
|
155
|
-
TEMPORARY_REDIRECT: 307,
|
|
156
|
-
PERMANENT_REDIRECT: 308,
|
|
157
|
-
BAD_REQUEST: 400,
|
|
158
|
-
UNAUTHORIZED: 401,
|
|
159
|
-
PAYMENT_REQUIRED: 402,
|
|
160
|
-
FORBIDDEN: 403,
|
|
161
|
-
NOT_FOUND: 404,
|
|
162
|
-
METHOD_NOT_ALLOWED: 405,
|
|
163
|
-
NOT_ACCEPTABLE: 406,
|
|
164
|
-
REQUEST_TIMEOUT: 408,
|
|
165
|
-
CONFLICT: 409,
|
|
166
|
-
GONE: 410,
|
|
167
|
-
PRECONDITION_FAILED: 412,
|
|
168
|
-
PAYLOAD_TOO_LARGE: 413,
|
|
169
|
-
URI_TOO_LONG: 414,
|
|
170
|
-
UNSUPPORTED_MEDIA_TYPE: 415,
|
|
171
|
-
IM_A_TEAPOT: 418,
|
|
172
|
-
UNPROCESSABLE_ENTITY: 422,
|
|
173
|
-
TOO_MANY_REQUESTS: 429,
|
|
174
|
-
INTERNAL_SERVER_ERROR: 500,
|
|
175
|
-
NOT_IMPLEMENTED: 501,
|
|
176
|
-
BAD_GATEWAY: 502,
|
|
177
|
-
SERVICE_UNAVAILABLE: 503,
|
|
178
|
-
GATEWAY_TIMEOUT: 504
|
|
179
|
-
});
|
|
180
|
-
|
|
181
150
|
// src/server/cors.ts
|
|
182
151
|
var ORIGIN = "access-control-allow-origin";
|
|
183
152
|
var allowedOrigin = (options, requested) => {
|
|
@@ -1476,5 +1445,5 @@ export {
|
|
|
1476
1445
|
ApiHidden
|
|
1477
1446
|
};
|
|
1478
1447
|
|
|
1479
|
-
//# debugId=
|
|
1448
|
+
//# debugId=13D7672397DC21C164756E2164756E21
|
|
1480
1449
|
//# sourceMappingURL=index.js.map
|