zlient 3.3.4 → 4.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/dist/auth.d.ts +12 -12
- package/dist/auth.d.ts.map +1 -1
- package/dist/endpoint-utils.d.ts +36 -0
- package/dist/endpoint-utils.d.ts.map +1 -0
- package/dist/event-emitter.d.ts +12 -0
- package/dist/event-emitter.d.ts.map +1 -0
- package/dist/http/http-client.d.ts +35 -105
- package/dist/http/http-client.d.ts.map +1 -1
- package/dist/http/http-endpoint.d.ts +2 -2
- package/dist/http/http-endpoint.d.ts.map +1 -1
- package/dist/http/request-utils.d.ts +23 -0
- package/dist/http/request-utils.d.ts.map +1 -0
- package/dist/index.cjs +529 -419
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +528 -420
- package/dist/index.js.map +1 -1
- package/dist/realtime-utils.d.ts +4 -0
- package/dist/realtime-utils.d.ts.map +1 -0
- package/dist/sse/sse-client.d.ts +14 -8
- package/dist/sse/sse-client.d.ts.map +1 -1
- package/dist/sse/sse-endpoint.d.ts.map +1 -1
- package/dist/types.d.ts +24 -12
- package/dist/types.d.ts.map +1 -1
- package/dist/validation.d.ts +10 -2
- package/dist/validation.d.ts.map +1 -1
- package/dist/ws/ws-client.d.ts +9 -6
- package/dist/ws/ws-client.d.ts.map +1 -1
- package/dist/ws/ws-endpoint.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -5,7 +5,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
5
5
|
* Use this when you don't need authentication.
|
|
6
6
|
*/
|
|
7
7
|
var NoAuth = class {
|
|
8
|
-
|
|
8
|
+
apply(_ctx) {}
|
|
9
9
|
};
|
|
10
10
|
/**
|
|
11
11
|
* API Key authentication provider.
|
|
@@ -26,18 +26,18 @@ var ApiKeyAuth = class {
|
|
|
26
26
|
if (!opts.header && !opts.query) throw new Error("ApiKeyAuth requires either \"header\" or \"query\" option");
|
|
27
27
|
if (opts.header && opts.query) throw new Error("ApiKeyAuth cannot use both \"header\" and \"query\" options");
|
|
28
28
|
}
|
|
29
|
-
apply(
|
|
29
|
+
apply(ctx) {
|
|
30
30
|
const value = this.opts.value;
|
|
31
|
-
if (this.opts.header) if (init.headers instanceof Headers) init.headers.set(this.opts.header, value);
|
|
32
|
-
else if (Array.isArray(init.headers)) init.headers.push([this.opts.header, value]);
|
|
33
|
-
else init.headers = {
|
|
34
|
-
...init.headers,
|
|
31
|
+
if (this.opts.header) if (ctx.init.headers instanceof Headers) ctx.init.headers.set(this.opts.header, value);
|
|
32
|
+
else if (Array.isArray(ctx.init.headers)) ctx.init.headers.push([this.opts.header, value]);
|
|
33
|
+
else ctx.init.headers = {
|
|
34
|
+
...ctx.init.headers,
|
|
35
35
|
[this.opts.header]: value
|
|
36
36
|
};
|
|
37
37
|
else if (this.opts.query) {
|
|
38
|
-
const u = new URL(url);
|
|
38
|
+
const u = new URL(ctx.url);
|
|
39
39
|
u.searchParams.set(this.opts.query, value);
|
|
40
|
-
|
|
40
|
+
ctx.url = u.toString();
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
};
|
|
@@ -60,14 +60,14 @@ var BearerTokenAuth = class {
|
|
|
60
60
|
constructor(getToken) {
|
|
61
61
|
this.getToken = getToken;
|
|
62
62
|
}
|
|
63
|
-
async apply(
|
|
63
|
+
async apply(ctx) {
|
|
64
64
|
const token = await this.getToken();
|
|
65
65
|
if (!token) throw new Error("BearerTokenAuth: token is empty or undefined");
|
|
66
66
|
const authHeader = `Bearer ${token}`;
|
|
67
|
-
if (init.headers instanceof Headers) init.headers.set("Authorization", authHeader);
|
|
68
|
-
else if (Array.isArray(init.headers)) init.headers.push(["Authorization", authHeader]);
|
|
69
|
-
else init.headers = {
|
|
70
|
-
...init.headers,
|
|
67
|
+
if (ctx.init.headers instanceof Headers) ctx.init.headers.set("Authorization", authHeader);
|
|
68
|
+
else if (Array.isArray(ctx.init.headers)) ctx.init.headers.push(["Authorization", authHeader]);
|
|
69
|
+
else ctx.init.headers = {
|
|
70
|
+
...ctx.init.headers,
|
|
71
71
|
Authorization: authHeader
|
|
72
72
|
};
|
|
73
73
|
}
|
|
@@ -320,6 +320,9 @@ const HTTPStatusCode = {
|
|
|
320
320
|
NOT_EXTENDED: 510,
|
|
321
321
|
NETWORK_AUTHENTICATION_REQUIRED: 511
|
|
322
322
|
};
|
|
323
|
+
function captureStackTrace(targetObject, constructorOpt) {
|
|
324
|
+
Error.captureStackTrace?.(targetObject, constructorOpt);
|
|
325
|
+
}
|
|
323
326
|
/**
|
|
324
327
|
* Custom error class for API-related errors.
|
|
325
328
|
* Includes HTTP status codes, response details, and validation errors.
|
|
@@ -337,10 +340,12 @@ var ApiError = class ApiError extends Error {
|
|
|
337
340
|
super(message);
|
|
338
341
|
this.name = "ApiError";
|
|
339
342
|
this.status = options?.status;
|
|
343
|
+
this.method = options?.method;
|
|
344
|
+
this.url = options?.url;
|
|
340
345
|
this.details = options?.details;
|
|
341
346
|
this.cause = options?.cause;
|
|
342
347
|
this.validationIssues = options?.validationIssues;
|
|
343
|
-
|
|
348
|
+
captureStackTrace(this, ApiError);
|
|
344
349
|
}
|
|
345
350
|
/**
|
|
346
351
|
* Check if this is a validation error (has validationIssues)
|
|
@@ -368,6 +373,8 @@ var ApiError = class ApiError extends Error {
|
|
|
368
373
|
name: this.name,
|
|
369
374
|
message: this.message,
|
|
370
375
|
status: this.status,
|
|
376
|
+
method: this.method,
|
|
377
|
+
url: this.url,
|
|
371
378
|
details: this.details,
|
|
372
379
|
validationIssues: this.validationIssues,
|
|
373
380
|
stack: this.stack
|
|
@@ -383,7 +390,7 @@ var SchemaDefinitionError = class SchemaDefinitionError extends Error {
|
|
|
383
390
|
super(`No schema defined for status code ${status}`);
|
|
384
391
|
this.status = status;
|
|
385
392
|
this.name = "SchemaDefinitionError";
|
|
386
|
-
|
|
393
|
+
captureStackTrace(this, SchemaDefinitionError);
|
|
387
394
|
}
|
|
388
395
|
};
|
|
389
396
|
/**
|
|
@@ -405,15 +412,34 @@ function toQueryString(q) {
|
|
|
405
412
|
const s = q.toString();
|
|
406
413
|
return s ? `?${s}` : "";
|
|
407
414
|
}
|
|
415
|
+
if (typeof q !== "object") throw new TypeError("Query parameters must be a URLSearchParams instance or an object");
|
|
408
416
|
const params = new URLSearchParams();
|
|
409
417
|
Object.entries(q).forEach(([k, v]) => {
|
|
410
|
-
if (v !== void 0) params.append(k, String(v));
|
|
418
|
+
if (v !== void 0 && v !== null) params.append(k, String(v));
|
|
411
419
|
});
|
|
412
420
|
const s = params.toString();
|
|
413
421
|
return s ? `?${s}` : "";
|
|
414
422
|
}
|
|
423
|
+
function toRequestQuery(q) {
|
|
424
|
+
if (q === void 0 || q instanceof URLSearchParams) return q;
|
|
425
|
+
if (q !== null && typeof q === "object") return q;
|
|
426
|
+
throw new TypeError("Query parameters must be a URLSearchParams instance or an object");
|
|
427
|
+
}
|
|
415
428
|
//#endregion
|
|
416
429
|
//#region lib/validation.ts
|
|
430
|
+
function formatPath(path) {
|
|
431
|
+
if (!path || path.length === 0) return "";
|
|
432
|
+
return path.map((segment) => {
|
|
433
|
+
const key = typeof segment === "object" && segment !== null ? segment.key : segment;
|
|
434
|
+
return typeof key === "symbol" ? key.toString() : String(key);
|
|
435
|
+
}).join(".");
|
|
436
|
+
}
|
|
437
|
+
function formatValidationIssues(issues, maxIssues = 3) {
|
|
438
|
+
return issues.slice(0, maxIssues).map((issue) => {
|
|
439
|
+
const path = formatPath(issue.path);
|
|
440
|
+
return path ? `${path}: ${issue.message}` : issue.message;
|
|
441
|
+
}).join("; ");
|
|
442
|
+
}
|
|
417
443
|
/**
|
|
418
444
|
* Safely parse/validate data with any Standard Schema-compatible library (Zod, Valibot, ArkType, etc.).
|
|
419
445
|
* Returns a result object with success status and data or issues.
|
|
@@ -476,9 +502,19 @@ async function safeParse(schema, data) {
|
|
|
476
502
|
* }
|
|
477
503
|
* ```
|
|
478
504
|
*/
|
|
479
|
-
async function parseOrThrow(schema, data) {
|
|
505
|
+
async function parseOrThrow(schema, data, context = {}) {
|
|
480
506
|
const result = await schema["~standard"].validate(data);
|
|
481
|
-
if (result.issues)
|
|
507
|
+
if (result.issues) {
|
|
508
|
+
const messages = formatValidationIssues(result.issues);
|
|
509
|
+
const issueCount = result.issues.length > 3 ? ` (${result.issues.length} issues total)` : "";
|
|
510
|
+
throw new ApiError(`${context.label ? `${context.label} validation failed` : "Validation failed"}: ${messages}${issueCount}`, {
|
|
511
|
+
status: context.status,
|
|
512
|
+
method: context.method,
|
|
513
|
+
url: context.url,
|
|
514
|
+
details: context.details,
|
|
515
|
+
validationIssues: result.issues
|
|
516
|
+
});
|
|
517
|
+
}
|
|
482
518
|
return result.value;
|
|
483
519
|
}
|
|
484
520
|
/**
|
|
@@ -495,47 +531,132 @@ function isStandardSchema(value) {
|
|
|
495
531
|
return "~standard" in schema && typeof schema["~standard"] === "object" && schema["~standard"] !== null && schema["~standard"].version === 1 && typeof schema["~standard"].validate === "function";
|
|
496
532
|
}
|
|
497
533
|
//#endregion
|
|
534
|
+
//#region lib/endpoint-utils.ts
|
|
535
|
+
function validateRequiredHeaders(mustHeaderKeys, headers) {
|
|
536
|
+
if (!mustHeaderKeys || mustHeaderKeys.length === 0) return;
|
|
537
|
+
const missing = mustHeaderKeys.filter((key) => !headers || !(key in headers));
|
|
538
|
+
if (missing.length > 0) throw new ApiError(`Missing required header(s): ${missing.join(", ")}`, { details: { missingHeaders: missing } });
|
|
539
|
+
}
|
|
540
|
+
async function parseEndpointValue(schema, value, skipValidation, label) {
|
|
541
|
+
if (skipValidation || !schema || value === void 0) return value;
|
|
542
|
+
return parseOrThrow(schema, value, { label });
|
|
543
|
+
}
|
|
544
|
+
function assertRequiredEndpointValue(schema, value, missingParam, message) {
|
|
545
|
+
if (!schema || value !== void 0) return;
|
|
546
|
+
throw new ApiError(message, { details: { missingParam } });
|
|
547
|
+
}
|
|
548
|
+
function resolveEndpointPath(path, rawPathParams, parsedPathParams) {
|
|
549
|
+
if (typeof path !== "function") return path;
|
|
550
|
+
if (!rawPathParams) throw new ApiError("Path function requires pathParams", { details: { missingParam: "pathParams" } });
|
|
551
|
+
return path(parsedPathParams);
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Shared validation pipeline used by HTTP, SSE, and WebSocket endpoints.
|
|
555
|
+
* Validates request body, query params, and path params; asserts required
|
|
556
|
+
* fields are present; and resolves the final path string.
|
|
557
|
+
*/
|
|
558
|
+
async function validateEndpointParams(config, raw, labels = {}) {
|
|
559
|
+
const skip = config.advanced?.skipRequestValidation ?? false;
|
|
560
|
+
const parsedData = await parseEndpointValue(config.request, raw.data, skip, labels.request ?? "Request body");
|
|
561
|
+
const parsedQuery = await parseEndpointValue(config.query, raw.query, skip, labels.query ?? "Query parameters");
|
|
562
|
+
const parsedPathParams = await parseEndpointValue(config.pathParams, raw.pathParams, skip, labels.path ?? "Path parameters");
|
|
563
|
+
assertRequiredEndpointValue(config.request, raw.data, "data", "Missing required request body (data)");
|
|
564
|
+
assertRequiredEndpointValue(config.pathParams, raw.pathParams, "pathParams", "Missing required path parameters (pathParams)");
|
|
565
|
+
return {
|
|
566
|
+
parsedData,
|
|
567
|
+
parsedQuery,
|
|
568
|
+
parsedPathParams,
|
|
569
|
+
pathStr: resolveEndpointPath(config.path, raw.pathParams, parsedPathParams)
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
//#endregion
|
|
573
|
+
//#region lib/event-emitter.ts
|
|
574
|
+
/**
|
|
575
|
+
* Minimal typed event emitter used internally by SSEConnectionImpl and WSConnectionImpl.
|
|
576
|
+
* Not part of the public API.
|
|
577
|
+
*/
|
|
578
|
+
var EventEmitter = class {
|
|
579
|
+
constructor() {
|
|
580
|
+
this.handlers = /* @__PURE__ */ new Map();
|
|
581
|
+
}
|
|
582
|
+
on(event, handler) {
|
|
583
|
+
let set = this.handlers.get(event);
|
|
584
|
+
if (!set) {
|
|
585
|
+
set = /* @__PURE__ */ new Set();
|
|
586
|
+
this.handlers.set(event, set);
|
|
587
|
+
}
|
|
588
|
+
set.add(handler);
|
|
589
|
+
}
|
|
590
|
+
off(event, handler) {
|
|
591
|
+
this.handlers.get(event)?.delete(handler);
|
|
592
|
+
}
|
|
593
|
+
emit(event, ...args) {
|
|
594
|
+
const set = this.handlers.get(event);
|
|
595
|
+
if (set) set.forEach((handler) => handler(...args));
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
//#endregion
|
|
599
|
+
//#region lib/realtime-utils.ts
|
|
600
|
+
function looksLikeJsonValue(value) {
|
|
601
|
+
return value.startsWith("{") && value.endsWith("}") || value.startsWith("[") && value.endsWith("]") || value.startsWith("\"") && value.endsWith("\"");
|
|
602
|
+
}
|
|
603
|
+
function parseRealtimeData(data) {
|
|
604
|
+
if (typeof data !== "string" || data.length === 0 || !looksLikeJsonValue(data)) return data;
|
|
605
|
+
try {
|
|
606
|
+
return JSON.parse(data);
|
|
607
|
+
} catch {
|
|
608
|
+
return data;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
function serializeRealtimeData(data) {
|
|
612
|
+
if (data != null && typeof data === "object") return JSON.stringify(data);
|
|
613
|
+
if (typeof data === "string") return data;
|
|
614
|
+
return String(data);
|
|
615
|
+
}
|
|
616
|
+
//#endregion
|
|
498
617
|
//#region lib/sse/sse-client.ts
|
|
499
|
-
var SSEConnectionImpl = class {
|
|
618
|
+
var SSEConnectionImpl = class extends EventEmitter {
|
|
500
619
|
constructor(url, responseSchema, options = {}) {
|
|
620
|
+
super();
|
|
501
621
|
this.url = url;
|
|
502
622
|
this.responseSchema = responseSchema;
|
|
503
623
|
this.options = options;
|
|
504
624
|
this.abortController = new AbortController();
|
|
505
|
-
this.handlers = /* @__PURE__ */ new Map();
|
|
506
625
|
this._readyState = 0;
|
|
507
626
|
this.start();
|
|
508
627
|
}
|
|
509
628
|
async start() {
|
|
510
629
|
try {
|
|
511
|
-
|
|
512
|
-
|
|
630
|
+
const { method = "GET", data, withCredentials, signal, auth, logger } = this.options;
|
|
631
|
+
const requestHeaders = {
|
|
632
|
+
Accept: "text/event-stream",
|
|
633
|
+
...this.options.headers
|
|
634
|
+
};
|
|
513
635
|
const init = {
|
|
514
636
|
method,
|
|
515
|
-
headers:
|
|
516
|
-
Accept: "text/event-stream",
|
|
517
|
-
...headers
|
|
518
|
-
},
|
|
637
|
+
headers: requestHeaders,
|
|
519
638
|
signal: signal || this.abortController.signal
|
|
520
639
|
};
|
|
640
|
+
let url = this.url;
|
|
521
641
|
if (auth) {
|
|
522
|
-
|
|
642
|
+
const ctx = {
|
|
523
643
|
url,
|
|
524
644
|
init
|
|
525
|
-
}
|
|
526
|
-
|
|
645
|
+
};
|
|
646
|
+
await auth.apply(ctx);
|
|
647
|
+
url = ctx.url;
|
|
527
648
|
}
|
|
528
649
|
if (withCredentials) init.credentials = "include";
|
|
529
|
-
if (data) {
|
|
650
|
+
if (data != null) {
|
|
530
651
|
init.body = typeof data === "object" ? JSON.stringify(data) : String(data);
|
|
531
|
-
if (!
|
|
652
|
+
if (!("Content-Type" in requestHeaders)) requestHeaders["Content-Type"] = "application/json";
|
|
532
653
|
}
|
|
533
654
|
if (logger) logger.debug("SSE connection initiated", {
|
|
534
655
|
method,
|
|
535
656
|
url,
|
|
536
|
-
hasData:
|
|
657
|
+
hasData: data != null
|
|
537
658
|
});
|
|
538
|
-
const response = await fetch(url, init);
|
|
659
|
+
const response = await (this.options.fetch ?? globalThis.fetch.bind(globalThis))(url, init);
|
|
539
660
|
if (!response.ok) {
|
|
540
661
|
if (logger) logger.error(`SSE request failed with status ${response.status}`, /* @__PURE__ */ new Error("SSE Error"), {
|
|
541
662
|
url,
|
|
@@ -547,80 +668,7 @@ var SSEConnectionImpl = class {
|
|
|
547
668
|
this.emit("open", { type: "open" });
|
|
548
669
|
const reader = response.body?.getReader();
|
|
549
670
|
if (!reader) throw new Error("Response body is not readable");
|
|
550
|
-
|
|
551
|
-
let lineBuffer = "";
|
|
552
|
-
let eventData = "";
|
|
553
|
-
let eventName = "message";
|
|
554
|
-
let lastCharWasCR = false;
|
|
555
|
-
const processLine = (line) => {
|
|
556
|
-
if (line === "") {
|
|
557
|
-
if (eventData) {
|
|
558
|
-
this.handleEvent(eventName, eventData.endsWith("\n") ? eventData.slice(0, -1) : eventData);
|
|
559
|
-
eventData = "";
|
|
560
|
-
}
|
|
561
|
-
eventName = "message";
|
|
562
|
-
return;
|
|
563
|
-
}
|
|
564
|
-
if (line.startsWith(":")) return;
|
|
565
|
-
const colonIndex = line.indexOf(":");
|
|
566
|
-
let field;
|
|
567
|
-
let value;
|
|
568
|
-
if (colonIndex === -1) {
|
|
569
|
-
field = line;
|
|
570
|
-
value = "";
|
|
571
|
-
} else {
|
|
572
|
-
field = line.slice(0, colonIndex);
|
|
573
|
-
value = line.slice(colonIndex + 1);
|
|
574
|
-
if (value.startsWith(" ")) value = value.slice(1);
|
|
575
|
-
}
|
|
576
|
-
if (field === "event") eventName = value;
|
|
577
|
-
else if (field === "data") eventData += value + "\n";
|
|
578
|
-
};
|
|
579
|
-
while (true) {
|
|
580
|
-
const { done, value } = await reader.read();
|
|
581
|
-
const chunk = decoder.decode(value, { stream: true });
|
|
582
|
-
for (let i = 0; i < chunk.length; i++) {
|
|
583
|
-
const char = chunk[i];
|
|
584
|
-
if (char === "\n") if (lastCharWasCR) lastCharWasCR = false;
|
|
585
|
-
else {
|
|
586
|
-
processLine(lineBuffer);
|
|
587
|
-
lineBuffer = "";
|
|
588
|
-
}
|
|
589
|
-
else if (char === "\r") {
|
|
590
|
-
processLine(lineBuffer);
|
|
591
|
-
lineBuffer = "";
|
|
592
|
-
lastCharWasCR = true;
|
|
593
|
-
} else {
|
|
594
|
-
if (lastCharWasCR) lastCharWasCR = false;
|
|
595
|
-
lineBuffer += char;
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
if (done) {
|
|
599
|
-
const finalChunk = decoder.decode();
|
|
600
|
-
for (const char of finalChunk) if (char === "\n") if (lastCharWasCR) lastCharWasCR = false;
|
|
601
|
-
else {
|
|
602
|
-
processLine(lineBuffer);
|
|
603
|
-
lineBuffer = "";
|
|
604
|
-
}
|
|
605
|
-
else if (char === "\r") {
|
|
606
|
-
processLine(lineBuffer);
|
|
607
|
-
lineBuffer = "";
|
|
608
|
-
lastCharWasCR = true;
|
|
609
|
-
} else {
|
|
610
|
-
if (lastCharWasCR) lastCharWasCR = false;
|
|
611
|
-
lineBuffer += char;
|
|
612
|
-
}
|
|
613
|
-
if (lineBuffer) {
|
|
614
|
-
processLine(lineBuffer);
|
|
615
|
-
lineBuffer = "";
|
|
616
|
-
}
|
|
617
|
-
if (eventData) {
|
|
618
|
-
this.handleEvent(eventName, eventData.endsWith("\n") ? eventData.slice(0, -1) : eventData);
|
|
619
|
-
eventData = "";
|
|
620
|
-
}
|
|
621
|
-
break;
|
|
622
|
-
}
|
|
623
|
-
}
|
|
671
|
+
await this.readStream(reader);
|
|
624
672
|
} catch (error) {
|
|
625
673
|
if (error.name === "AbortError") return;
|
|
626
674
|
this._readyState = 2;
|
|
@@ -629,12 +677,78 @@ var SSEConnectionImpl = class {
|
|
|
629
677
|
this._readyState = 2;
|
|
630
678
|
}
|
|
631
679
|
}
|
|
680
|
+
/**
|
|
681
|
+
* Reads the SSE stream according to the spec:
|
|
682
|
+
* - Lines are separated by LF, CR, or CRLF
|
|
683
|
+
* - Empty line dispatches the accumulated event
|
|
684
|
+
* - Lines starting with ':' are comments
|
|
685
|
+
* - 'event' field sets the event name; 'data' field accumulates the payload
|
|
686
|
+
*/
|
|
687
|
+
async readStream(reader) {
|
|
688
|
+
const decoder = new TextDecoder();
|
|
689
|
+
let lineBuffer = "";
|
|
690
|
+
let eventData = "";
|
|
691
|
+
let eventName = "message";
|
|
692
|
+
let lastCharWasCR = false;
|
|
693
|
+
const processLine = (line) => {
|
|
694
|
+
if (line === "") {
|
|
695
|
+
if (eventData) {
|
|
696
|
+
this.handleEvent(eventName, eventData.endsWith("\n") ? eventData.slice(0, -1) : eventData);
|
|
697
|
+
eventData = "";
|
|
698
|
+
}
|
|
699
|
+
eventName = "message";
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
if (line.startsWith(":")) return;
|
|
703
|
+
const colonIndex = line.indexOf(":");
|
|
704
|
+
let field;
|
|
705
|
+
let value;
|
|
706
|
+
if (colonIndex === -1) {
|
|
707
|
+
field = line;
|
|
708
|
+
value = "";
|
|
709
|
+
} else {
|
|
710
|
+
field = line.slice(0, colonIndex);
|
|
711
|
+
value = line.slice(colonIndex + 1);
|
|
712
|
+
if (value.startsWith(" ")) value = value.slice(1);
|
|
713
|
+
}
|
|
714
|
+
if (field === "event") eventName = value;
|
|
715
|
+
else if (field === "data") eventData += value + "\n";
|
|
716
|
+
};
|
|
717
|
+
const processChunk = (chunk) => {
|
|
718
|
+
for (let i = 0; i < chunk.length; i++) {
|
|
719
|
+
const char = chunk[i];
|
|
720
|
+
if (char === "\n") if (lastCharWasCR) lastCharWasCR = false;
|
|
721
|
+
else {
|
|
722
|
+
processLine(lineBuffer);
|
|
723
|
+
lineBuffer = "";
|
|
724
|
+
}
|
|
725
|
+
else if (char === "\r") {
|
|
726
|
+
processLine(lineBuffer);
|
|
727
|
+
lineBuffer = "";
|
|
728
|
+
lastCharWasCR = true;
|
|
729
|
+
} else {
|
|
730
|
+
lastCharWasCR = false;
|
|
731
|
+
lineBuffer += char;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
};
|
|
735
|
+
while (true) {
|
|
736
|
+
const { done, value } = await reader.read();
|
|
737
|
+
processChunk(decoder.decode(value, { stream: true }));
|
|
738
|
+
if (done) {
|
|
739
|
+
processChunk(decoder.decode());
|
|
740
|
+
if (lineBuffer) {
|
|
741
|
+
processLine(lineBuffer);
|
|
742
|
+
lineBuffer = "";
|
|
743
|
+
}
|
|
744
|
+
if (eventData) this.handleEvent(eventName, eventData.endsWith("\n") ? eventData.slice(0, -1) : eventData);
|
|
745
|
+
break;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
632
749
|
async handleEvent(event, data) {
|
|
633
|
-
let parsedData = data;
|
|
750
|
+
let parsedData = parseRealtimeData(data);
|
|
634
751
|
try {
|
|
635
|
-
if (typeof data === "string" && data.length > 0) try {
|
|
636
|
-
if (data.startsWith("{") && data.endsWith("}") || data.startsWith("[") && data.endsWith("]") || data.startsWith("\"") && data.endsWith("\"")) parsedData = JSON.parse(data);
|
|
637
|
-
} catch {}
|
|
638
752
|
const schema = this.getSchema(event);
|
|
639
753
|
if (!this.options.skipResponseValidation && schema) parsedData = await parseOrThrow(schema, parsedData);
|
|
640
754
|
this.emit(event, parsedData);
|
|
@@ -644,24 +758,9 @@ var SSEConnectionImpl = class {
|
|
|
644
758
|
}
|
|
645
759
|
getSchema(event) {
|
|
646
760
|
if (!this.responseSchema) return void 0;
|
|
647
|
-
if ("~standard" in this.responseSchema)
|
|
648
|
-
if (event === "message") return this.responseSchema;
|
|
649
|
-
return;
|
|
650
|
-
}
|
|
761
|
+
if ("~standard" in this.responseSchema) return event === "message" ? this.responseSchema : void 0;
|
|
651
762
|
return this.responseSchema[event];
|
|
652
763
|
}
|
|
653
|
-
on(event, handler) {
|
|
654
|
-
if (!this.handlers.has(event)) this.handlers.set(event, /* @__PURE__ */ new Set());
|
|
655
|
-
this.handlers.get(event).add(handler);
|
|
656
|
-
}
|
|
657
|
-
off(event, handler) {
|
|
658
|
-
const handlers = this.handlers.get(event);
|
|
659
|
-
if (handlers) handlers.delete(handler);
|
|
660
|
-
}
|
|
661
|
-
emit(event, ...args) {
|
|
662
|
-
const handlers = this.handlers.get(event);
|
|
663
|
-
if (handlers) handlers.forEach((handler) => handler(...args));
|
|
664
|
-
}
|
|
665
764
|
close() {
|
|
666
765
|
this.abortController.abort();
|
|
667
766
|
this._readyState = 2;
|
|
@@ -680,18 +779,16 @@ var SSEEndpointImpl = class {
|
|
|
680
779
|
createCall() {
|
|
681
780
|
return async (params) => {
|
|
682
781
|
const { query, pathParams, data, headers, signal } = params || {};
|
|
683
|
-
const
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
} else pathStr = this.config.path;
|
|
694
|
-
return new SSEConnectionImpl(`${this.client.getBaseUrl(this.config.advanced?.baseUrlKey || "default")}${pathStr}${toQueryString(query)}`, this.config.response, {
|
|
782
|
+
const { parsedQuery, pathStr } = await validateEndpointParams(this.config, {
|
|
783
|
+
data,
|
|
784
|
+
query,
|
|
785
|
+
pathParams
|
|
786
|
+
}, {
|
|
787
|
+
request: "SSE request body",
|
|
788
|
+
query: "SSE query parameters",
|
|
789
|
+
path: "SSE path parameters"
|
|
790
|
+
});
|
|
791
|
+
return new SSEConnectionImpl(`${this.client.getBaseUrl(this.config.advanced?.baseUrlKey || "default")}${pathStr}${toQueryString(toRequestQuery(parsedQuery))}`, this.config.response, {
|
|
695
792
|
skipResponseValidation: this.config.advanced?.skipResponseValidation,
|
|
696
793
|
withCredentials: this.config.advanced?.withCredentials,
|
|
697
794
|
method: this.config.method,
|
|
@@ -703,31 +800,29 @@ var SSEEndpointImpl = class {
|
|
|
703
800
|
},
|
|
704
801
|
signal,
|
|
705
802
|
auth: this.config.advanced?.skipAuth ? void 0 : this.client.getAuth(),
|
|
706
|
-
logger: this.client.getLogger()
|
|
803
|
+
logger: this.client.getLogger(),
|
|
804
|
+
fetch: this.client.getFetch()
|
|
707
805
|
});
|
|
708
806
|
};
|
|
709
807
|
}
|
|
710
808
|
};
|
|
711
809
|
//#endregion
|
|
712
810
|
//#region lib/ws/ws-client.ts
|
|
713
|
-
var WSConnectionImpl = class {
|
|
811
|
+
var WSConnectionImpl = class extends EventEmitter {
|
|
714
812
|
constructor(url, sendSchema, receiveSchema, skipRequestValidation = false, skipResponseValidation = false, protocols) {
|
|
813
|
+
super();
|
|
715
814
|
this.sendSchema = sendSchema;
|
|
716
815
|
this.receiveSchema = receiveSchema;
|
|
717
816
|
this.skipRequestValidation = skipRequestValidation;
|
|
718
817
|
this.skipResponseValidation = skipResponseValidation;
|
|
719
|
-
this.handlers = /* @__PURE__ */ new Map();
|
|
720
818
|
if (typeof WebSocket === "undefined") throw new Error("WebSocket is not defined. Ensure you are in a supported environment.");
|
|
721
819
|
this.ws = new WebSocket(url, protocols);
|
|
722
820
|
this.ws.onopen = () => this.emit("open");
|
|
723
821
|
this.ws.onclose = (event) => this.emit("close", event);
|
|
724
822
|
this.ws.onerror = (event) => this.emit("error", event);
|
|
725
823
|
this.ws.onmessage = async (event) => {
|
|
726
|
-
let data = event.data;
|
|
824
|
+
let data = parseRealtimeData(event.data);
|
|
727
825
|
try {
|
|
728
|
-
if (typeof data === "string") try {
|
|
729
|
-
data = JSON.parse(data);
|
|
730
|
-
} catch {}
|
|
731
826
|
if (!this.skipResponseValidation && this.receiveSchema) data = await parseOrThrow(this.receiveSchema, data);
|
|
732
827
|
this.emit("message", data);
|
|
733
828
|
} catch (error) {
|
|
@@ -737,20 +832,13 @@ var WSConnectionImpl = class {
|
|
|
737
832
|
}
|
|
738
833
|
async send(data) {
|
|
739
834
|
if (!this.skipRequestValidation && this.sendSchema) await parseOrThrow(this.sendSchema, data);
|
|
740
|
-
|
|
741
|
-
this.ws.send(message);
|
|
835
|
+
this.ws.send(serializeRealtimeData(data));
|
|
742
836
|
}
|
|
743
837
|
on(event, handler) {
|
|
744
|
-
|
|
745
|
-
this.handlers.get(event).add(handler);
|
|
838
|
+
super.on(event, handler);
|
|
746
839
|
}
|
|
747
840
|
off(event, handler) {
|
|
748
|
-
|
|
749
|
-
if (handlers) handlers.delete(handler);
|
|
750
|
-
}
|
|
751
|
-
emit(event, ...args) {
|
|
752
|
-
const handlers = this.handlers.get(event);
|
|
753
|
-
if (handlers) handlers.forEach((handler) => handler(...args));
|
|
841
|
+
super.off(event, handler);
|
|
754
842
|
}
|
|
755
843
|
close(code, reason) {
|
|
756
844
|
this.ws.close(code, reason);
|
|
@@ -767,14 +855,16 @@ var WSEndpointImpl = class {
|
|
|
767
855
|
this.config = config;
|
|
768
856
|
}
|
|
769
857
|
createCall() {
|
|
770
|
-
return (params) => {
|
|
858
|
+
return async (params) => {
|
|
771
859
|
const { query, pathParams, protocols } = params || {};
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
860
|
+
const { parsedQuery, pathStr } = await validateEndpointParams(this.config, {
|
|
861
|
+
query,
|
|
862
|
+
pathParams
|
|
863
|
+
}, {
|
|
864
|
+
query: "WebSocket query parameters",
|
|
865
|
+
path: "WebSocket path parameters"
|
|
866
|
+
});
|
|
867
|
+
return new WSConnectionImpl(`${this.client.getBaseUrl(this.config.advanced?.baseUrlKey || "default").replace(/^http/, "ws")}${pathStr}${toQueryString(toRequestQuery(parsedQuery))}`, this.config.send, this.config.receive, this.config.advanced?.skipRequestValidation, this.config.advanced?.skipResponseValidation, protocols);
|
|
778
868
|
};
|
|
779
869
|
}
|
|
780
870
|
};
|
|
@@ -788,42 +878,185 @@ var EndpointImpl = class {
|
|
|
788
878
|
async call(params) {
|
|
789
879
|
const { data, query, pathParams, signal } = params;
|
|
790
880
|
const headers = "headers" in params ? params.headers : void 0;
|
|
791
|
-
|
|
792
|
-
const
|
|
793
|
-
|
|
794
|
-
const missingHeaders = this.config.mustHeaderKeys.filter((key) => !headers || !(key in headers));
|
|
795
|
-
if (missingHeaders.length > 0) throw new Error(`Missing required header(s): ${missingHeaders.join(", ")}`);
|
|
796
|
-
}
|
|
797
|
-
if (!skipRequestValidation && this.config.request && data !== void 0) await parseOrThrow(this.config.request, data);
|
|
798
|
-
if (!skipRequestValidation && this.config.query && query !== void 0) await parseOrThrow(this.config.query, query);
|
|
799
|
-
if (!skipRequestValidation && this.config.pathParams && pathParams !== void 0) await parseOrThrow(this.config.pathParams, pathParams);
|
|
800
|
-
if (this.config.request && data === void 0) throw new Error("Missing required request body (data)");
|
|
801
|
-
if (this.config.pathParams && pathParams === void 0) throw new Error("Missing required path parameters (pathParams)");
|
|
802
|
-
let pathStr;
|
|
803
|
-
if (typeof this.config.path === "function") {
|
|
804
|
-
if (!pathParams) throw new Error("Path function requires pathParams");
|
|
805
|
-
pathStr = this.config.path(pathParams);
|
|
806
|
-
} else pathStr = this.config.path;
|
|
807
|
-
const { data: responseData, status } = await this.client.request(this.config.method, pathStr, data, {
|
|
881
|
+
validateRequiredHeaders(this.config.mustHeaderKeys, headers);
|
|
882
|
+
const { parsedQuery, pathStr } = await validateEndpointParams(this.config, {
|
|
883
|
+
data,
|
|
808
884
|
query,
|
|
885
|
+
pathParams
|
|
886
|
+
});
|
|
887
|
+
const { data: responseData, status } = await this.client.request(this.config.method, pathStr, data, {
|
|
888
|
+
query: toRequestQuery(parsedQuery),
|
|
809
889
|
headers,
|
|
810
890
|
baseUrlKey: this.config.advanced?.baseUrlKey,
|
|
811
891
|
skipAuth: this.config.advanced?.skipAuth,
|
|
812
892
|
skipRetry: this.config.advanced?.skipRetry,
|
|
813
893
|
signal
|
|
814
894
|
});
|
|
895
|
+
if (this.config.advanced?.skipResponseValidation) return responseData;
|
|
815
896
|
const schema = this.config.response;
|
|
816
|
-
if (
|
|
817
|
-
|
|
897
|
+
if (isStandardSchema(schema)) return await parseOrThrow(schema, responseData, {
|
|
898
|
+
label: `Response body for status ${status}`,
|
|
899
|
+
status
|
|
900
|
+
});
|
|
818
901
|
const specificSchema = schema[status];
|
|
819
902
|
if (!specificSchema) throw new SchemaDefinitionError(status);
|
|
820
|
-
return await parseOrThrow(specificSchema, responseData
|
|
903
|
+
return await parseOrThrow(specificSchema, responseData, {
|
|
904
|
+
label: `Response body for status ${status}`,
|
|
905
|
+
status
|
|
906
|
+
});
|
|
821
907
|
}
|
|
822
908
|
};
|
|
823
909
|
//#endregion
|
|
910
|
+
//#region lib/http/request-utils.ts
|
|
911
|
+
function serializeRequestBody(body, headers) {
|
|
912
|
+
if (body == null) return void 0;
|
|
913
|
+
if (body instanceof FormData) {
|
|
914
|
+
delete headers["Content-Type"];
|
|
915
|
+
return body;
|
|
916
|
+
}
|
|
917
|
+
if (body instanceof Blob || body instanceof ArrayBuffer) return body;
|
|
918
|
+
if (headers["Content-Type"]?.includes("json")) return JSON.stringify(body);
|
|
919
|
+
return String(body);
|
|
920
|
+
}
|
|
921
|
+
function isBinaryContentType(contentType) {
|
|
922
|
+
return contentType.includes("application/octet-stream") || contentType.includes("application/pdf") || contentType.includes("image/") || contentType.includes("video/") || contentType.includes("audio/") || contentType.startsWith("application/zip") || contentType.startsWith("application/x-");
|
|
923
|
+
}
|
|
924
|
+
async function parseResponseData(res, method, url) {
|
|
925
|
+
if (res.status === 204 || res.status === 205) return void 0;
|
|
926
|
+
const contentType = res.headers.get("content-type") || "";
|
|
927
|
+
try {
|
|
928
|
+
if (contentType.includes("json")) {
|
|
929
|
+
const text = await res.text();
|
|
930
|
+
return text ? JSON.parse(text) : void 0;
|
|
931
|
+
}
|
|
932
|
+
if (isBinaryContentType(contentType)) return await res.blob();
|
|
933
|
+
return await res.text();
|
|
934
|
+
} catch (error) {
|
|
935
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
936
|
+
throw new ApiError(`Failed to parse response body from ${method} ${url} (status ${res.status}): ${reason}`, {
|
|
937
|
+
status: res.status,
|
|
938
|
+
method,
|
|
939
|
+
url,
|
|
940
|
+
cause: error,
|
|
941
|
+
details: { contentType }
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
function getResponseMessage(details) {
|
|
946
|
+
if (typeof details === "string") return details.trim() || void 0;
|
|
947
|
+
if (!details || typeof details !== "object") return void 0;
|
|
948
|
+
const record = details;
|
|
949
|
+
for (const key of [
|
|
950
|
+
"message",
|
|
951
|
+
"error",
|
|
952
|
+
"title",
|
|
953
|
+
"detail"
|
|
954
|
+
]) {
|
|
955
|
+
const value = record[key];
|
|
956
|
+
if (typeof value === "string" && value.trim()) return value;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
function createStatusError(method, url, res, details) {
|
|
960
|
+
const statusText = res.statusText ? ` ${res.statusText}` : "";
|
|
961
|
+
const responseMessage = getResponseMessage(details);
|
|
962
|
+
const suffix = responseMessage ? `: ${responseMessage}` : "";
|
|
963
|
+
return new ApiError(`Request failed: ${method} ${url} returned ${res.status}${statusText}${suffix}`, {
|
|
964
|
+
status: res.status,
|
|
965
|
+
method,
|
|
966
|
+
url,
|
|
967
|
+
details
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
function createNetworkError(method, url, error) {
|
|
971
|
+
if (error instanceof ApiError) return error;
|
|
972
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
973
|
+
const name = error instanceof Error ? error.name : "Error";
|
|
974
|
+
return new ApiError(`${name === "AbortError" || name === "TimeoutError" ? "Request aborted" : "Network request failed"}: ${method} ${url}: ${reason}`, {
|
|
975
|
+
method,
|
|
976
|
+
url,
|
|
977
|
+
cause: error,
|
|
978
|
+
details: error && typeof error === "object" ? { name } : void 0
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
function isRetryableResponse(retryPolicy, method, status, retryAttempt, skipRetry) {
|
|
982
|
+
return !skipRetry && retryPolicy.maxAttempts > 0 && retryAttempt < retryPolicy.maxAttempts && !!retryPolicy.retryStatusCodes?.includes(status) && !!retryPolicy.retryMethods?.includes(method);
|
|
983
|
+
}
|
|
984
|
+
function getRetryDelay(retryPolicy, retryAttempt, response) {
|
|
985
|
+
const exponentialDelay = retryPolicy.baseDelayMs * 2 ** (retryAttempt - 1);
|
|
986
|
+
if (!retryPolicy.respectRetryAfter) return {
|
|
987
|
+
delay: exponentialDelay,
|
|
988
|
+
usedRetryAfter: false
|
|
989
|
+
};
|
|
990
|
+
const retryAfterHeader = response.headers.get("Retry-After") || response.headers.get("retry-after");
|
|
991
|
+
if (retryAfterHeader) {
|
|
992
|
+
const parsed = parseInt(retryAfterHeader, 10);
|
|
993
|
+
if (Number.isFinite(parsed) && parsed > 0) return {
|
|
994
|
+
delay: parsed * 1e3,
|
|
995
|
+
usedRetryAfter: true
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
return {
|
|
999
|
+
delay: exponentialDelay,
|
|
1000
|
+
usedRetryAfter: false
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
function recordSuccessfulRequest(logger, metrics, method, path, url, status, durationMs) {
|
|
1004
|
+
logger.info("HTTP request successful", {
|
|
1005
|
+
method,
|
|
1006
|
+
url,
|
|
1007
|
+
status,
|
|
1008
|
+
durationMs
|
|
1009
|
+
});
|
|
1010
|
+
metrics.collect({
|
|
1011
|
+
method,
|
|
1012
|
+
path,
|
|
1013
|
+
status,
|
|
1014
|
+
durationMs,
|
|
1015
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1016
|
+
success: true
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
function recordFailedRequest(logger, metrics, method, path, url, durationMs, error) {
|
|
1020
|
+
logger.error("HTTP request failed", error, {
|
|
1021
|
+
method,
|
|
1022
|
+
url,
|
|
1023
|
+
durationMs
|
|
1024
|
+
});
|
|
1025
|
+
metrics.collect({
|
|
1026
|
+
method,
|
|
1027
|
+
path,
|
|
1028
|
+
status: error instanceof ApiError ? error.status : void 0,
|
|
1029
|
+
durationMs,
|
|
1030
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1031
|
+
success: false,
|
|
1032
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
/**
|
|
1036
|
+
* Re-applies auth to an existing AuthContext, cloning headers first so that
|
|
1037
|
+
* a fresh token overwrites the stale one from the previous attempt.
|
|
1038
|
+
*/
|
|
1039
|
+
async function reapplyAuth(auth, ctx) {
|
|
1040
|
+
const originalHeaders = ctx.init.headers;
|
|
1041
|
+
const clonedHeaders = typeof originalHeaders === "object" && !(originalHeaders instanceof Headers) && !Array.isArray(originalHeaders) ? { ...originalHeaders } : originalHeaders;
|
|
1042
|
+
ctx.init = {
|
|
1043
|
+
...ctx.init,
|
|
1044
|
+
headers: clonedHeaders
|
|
1045
|
+
};
|
|
1046
|
+
await auth.apply(ctx);
|
|
1047
|
+
}
|
|
1048
|
+
function startRequestTimeout(timeoutMs, hasExternalSignal, controller) {
|
|
1049
|
+
if (!timeoutMs || hasExternalSignal) return void 0;
|
|
1050
|
+
return setTimeout(() => {
|
|
1051
|
+
const timeoutError = /* @__PURE__ */ new Error("Request timeout");
|
|
1052
|
+
timeoutError.name = "TimeoutError";
|
|
1053
|
+
controller.abort(timeoutError);
|
|
1054
|
+
}, timeoutMs);
|
|
1055
|
+
}
|
|
1056
|
+
//#endregion
|
|
824
1057
|
//#region lib/http/http-client.ts
|
|
825
1058
|
/**
|
|
826
|
-
* HTTP client with built-in authentication, and interceptors.
|
|
1059
|
+
* HTTP client with built-in authentication, retry, and interceptors.
|
|
827
1060
|
* Supports multiple base URLs, type-safe requests, and comprehensive error handling.
|
|
828
1061
|
*
|
|
829
1062
|
* @example
|
|
@@ -833,17 +1066,9 @@ var EndpointImpl = class {
|
|
|
833
1066
|
* headers: { 'Content-Type': 'application/json' },
|
|
834
1067
|
* timeout: { requestTimeoutMs: 30000 }
|
|
835
1068
|
* });
|
|
836
|
-
*
|
|
837
|
-
* const { data } = await client.request('GET', '/users', undefined, { query: { page: 1 } });
|
|
838
1069
|
* ```
|
|
839
1070
|
*/
|
|
840
1071
|
var HttpClient = class {
|
|
841
|
-
/**
|
|
842
|
-
* Creates a new HTTP client instance.
|
|
843
|
-
*
|
|
844
|
-
* @param opts - Client configuration options
|
|
845
|
-
* @throws {Error} If no fetch implementation is available
|
|
846
|
-
*/
|
|
847
1072
|
constructor(opts) {
|
|
848
1073
|
this.fetchImpl = opts.fetch ?? globalThis.fetch?.bind(globalThis);
|
|
849
1074
|
if (!this.fetchImpl) throw new Error("No fetch implementation found. Pass one via options.fetch.");
|
|
@@ -860,20 +1085,12 @@ var HttpClient = class {
|
|
|
860
1085
|
if (this.retryPolicy.baseDelayMs < 0) throw new Error("retry.baseDelayMs must be non-negative");
|
|
861
1086
|
this.timeoutMs = opts.timeout?.requestTimeoutMs;
|
|
862
1087
|
if (this.timeoutMs !== void 0 && this.timeoutMs < 0) throw new Error("timeout.requestTimeoutMs must be non-negative");
|
|
863
|
-
this.auth = opts
|
|
1088
|
+
this.auth = opts.auth ?? new NoAuth();
|
|
864
1089
|
this.logger = new LoggerUtil(opts.logger ?? new NoOpLogger());
|
|
865
1090
|
this.metrics = opts.metrics ?? new NoOpMetricsCollector();
|
|
866
1091
|
this.onUnauthenticated = opts.onUnauthenticated;
|
|
867
1092
|
}
|
|
868
|
-
/**
|
|
869
|
-
* Set or update the authentication provider.
|
|
870
|
-
*
|
|
871
|
-
* @param auth - Authentication provider instance
|
|
872
|
-
* @example
|
|
873
|
-
* ```ts
|
|
874
|
-
* client.setAuth(new BearerTokenAuth(() => getToken()));
|
|
875
|
-
* ```
|
|
876
|
-
*/
|
|
1093
|
+
/** Set or update the authentication provider at runtime. */
|
|
877
1094
|
setAuth(auth) {
|
|
878
1095
|
this.auth = auth;
|
|
879
1096
|
}
|
|
@@ -886,20 +1103,12 @@ var HttpClient = class {
|
|
|
886
1103
|
}
|
|
887
1104
|
return url.replace(/\/$/, "");
|
|
888
1105
|
}
|
|
889
|
-
/**
|
|
890
|
-
* Run all registered before-request hooks.
|
|
891
|
-
* @private
|
|
892
|
-
*/
|
|
893
1106
|
async runBeforeHooks(url, init) {
|
|
894
1107
|
for (const h of this.interceptors.beforeRequest ?? []) await h({
|
|
895
1108
|
url,
|
|
896
1109
|
init
|
|
897
1110
|
});
|
|
898
1111
|
}
|
|
899
|
-
/**
|
|
900
|
-
* Run all registered after-response hooks.
|
|
901
|
-
* @private
|
|
902
|
-
*/
|
|
903
1112
|
async runAfterHooks(req, res, parsed) {
|
|
904
1113
|
for (const h of this.interceptors.afterResponse ?? []) await h({
|
|
905
1114
|
request: req,
|
|
@@ -908,19 +1117,40 @@ var HttpClient = class {
|
|
|
908
1117
|
});
|
|
909
1118
|
}
|
|
910
1119
|
/**
|
|
911
|
-
*
|
|
912
|
-
*
|
|
913
|
-
* @returns Object mapping base URL keys to their resolved URLs
|
|
1120
|
+
* Returns retry info if the response should be retried, or null if not.
|
|
914
1121
|
*/
|
|
915
|
-
|
|
916
|
-
|
|
1122
|
+
async getRetryInfo(res, url, method, status, attempt, skipRetry) {
|
|
1123
|
+
if (!isRetryableResponse(this.retryPolicy, method, status, attempt, skipRetry)) return null;
|
|
1124
|
+
if (this.retryPolicy.shouldRetry) {
|
|
1125
|
+
if (!await this.retryPolicy.shouldRetry({
|
|
1126
|
+
url,
|
|
1127
|
+
method,
|
|
1128
|
+
status,
|
|
1129
|
+
attempt,
|
|
1130
|
+
response: res.clone()
|
|
1131
|
+
})) return null;
|
|
1132
|
+
}
|
|
1133
|
+
return getRetryDelay(this.retryPolicy, attempt + 1, res);
|
|
1134
|
+
}
|
|
1135
|
+
logRetry(method, url, status, attempt, info) {
|
|
1136
|
+
const suffix = info.usedRetryAfter ? "due to Retry-After header" : `attempt ${attempt}`;
|
|
1137
|
+
this.logger.warn(`Request failed with status ${status}. Retrying ${suffix} after ${info.delay}ms...`, {
|
|
1138
|
+
method,
|
|
1139
|
+
url,
|
|
1140
|
+
status,
|
|
1141
|
+
retryAttempt: attempt
|
|
1142
|
+
});
|
|
917
1143
|
}
|
|
918
1144
|
/**
|
|
919
|
-
*
|
|
920
|
-
*
|
|
921
|
-
* @param key - Base URL key (defaults to 'default' if not provided)
|
|
922
|
-
* @returns Resolved base URL string
|
|
1145
|
+
* Checks whether the 401 response should trigger a token refresh + retry.
|
|
923
1146
|
*/
|
|
1147
|
+
async shouldRefresh(res, refreshAttempted) {
|
|
1148
|
+
if (res.status !== 401 || !this.onUnauthenticated || refreshAttempted) return false;
|
|
1149
|
+
return this.onUnauthenticated(res.clone());
|
|
1150
|
+
}
|
|
1151
|
+
getBaseUrls() {
|
|
1152
|
+
return this.baseUrls;
|
|
1153
|
+
}
|
|
924
1154
|
getBaseUrl(key) {
|
|
925
1155
|
return this.resolveBaseUrl(key);
|
|
926
1156
|
}
|
|
@@ -936,257 +1166,120 @@ var HttpClient = class {
|
|
|
936
1166
|
getLogger() {
|
|
937
1167
|
return this.logger;
|
|
938
1168
|
}
|
|
1169
|
+
/** @internal */
|
|
1170
|
+
getFetch() {
|
|
1171
|
+
return this.fetchImpl;
|
|
1172
|
+
}
|
|
939
1173
|
/**
|
|
940
1174
|
* Make an HTTP request with automatic retry, authentication, and validation.
|
|
941
1175
|
*
|
|
942
|
-
* @param method - HTTP method (GET, POST, PUT, etc.)
|
|
943
|
-
* @param path - Request path (will be appended to base URL)
|
|
944
|
-
* @param body - Request body (will be JSON.stringify'd if Content-Type is json)
|
|
945
|
-
* @param options - Additional request options (headers, query params, etc.)
|
|
946
|
-
* @returns Promise resolving to response data and Response object
|
|
947
|
-
* @throws {ApiError} If request fails or response validation fails
|
|
948
|
-
*
|
|
949
1176
|
* @example
|
|
950
1177
|
* ```ts
|
|
951
|
-
* const { data
|
|
952
|
-
* query: { page: 1
|
|
1178
|
+
* const { data } = await client.request('GET', '/users', undefined, {
|
|
1179
|
+
* query: { page: 1 },
|
|
953
1180
|
* headers: { 'X-Custom': 'value' }
|
|
954
1181
|
* });
|
|
955
1182
|
* ```
|
|
956
1183
|
*/
|
|
957
1184
|
async request(method, path, body, options) {
|
|
958
1185
|
const startTime = Date.now();
|
|
959
|
-
|
|
960
|
-
this.logger.debug("HTTP request initiated", {
|
|
961
|
-
method,
|
|
962
|
-
path,
|
|
963
|
-
baseUrlKey: options?.baseUrlKey,
|
|
964
|
-
hasBody: body !== void 0
|
|
965
|
-
});
|
|
1186
|
+
const base = this.resolveBaseUrl(options?.baseUrlKey);
|
|
966
1187
|
const headers = {
|
|
967
1188
|
...this.headers,
|
|
968
1189
|
...options?.headers ?? {}
|
|
969
1190
|
};
|
|
970
1191
|
const controller = new AbortController();
|
|
971
1192
|
const signal = options?.signal ?? controller.signal;
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
else requestBody = String(body);
|
|
1193
|
+
this.logger.debug("HTTP request initiated", {
|
|
1194
|
+
method,
|
|
1195
|
+
path,
|
|
1196
|
+
baseUrlKey: options?.baseUrlKey,
|
|
1197
|
+
hasBody: body !== void 0
|
|
1198
|
+
});
|
|
979
1199
|
const init = {
|
|
980
1200
|
method,
|
|
981
1201
|
headers,
|
|
982
|
-
body:
|
|
1202
|
+
body: serializeRequestBody(body, headers),
|
|
983
1203
|
signal
|
|
984
1204
|
};
|
|
985
|
-
|
|
986
|
-
url
|
|
1205
|
+
const authCtx = {
|
|
1206
|
+
url: `${base}${path}${toQueryString(options?.query)}`,
|
|
987
1207
|
init,
|
|
988
1208
|
options
|
|
989
|
-
}
|
|
990
|
-
if (
|
|
991
|
-
await this.runBeforeHooks(url, init);
|
|
1209
|
+
};
|
|
1210
|
+
if (!options?.skipAuth) await this.auth.apply(authCtx);
|
|
1211
|
+
await this.runBeforeHooks(authCtx.url, authCtx.init);
|
|
992
1212
|
let refreshAttempted = false;
|
|
993
1213
|
let retryAttempt = 0;
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
if (
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
controller.abort(timeoutError);
|
|
1001
|
-
}, this.timeoutMs);
|
|
1214
|
+
while (true) {
|
|
1215
|
+
const timeoutId = startRequestTimeout(this.timeoutMs, !!options?.signal, controller);
|
|
1216
|
+
try {
|
|
1217
|
+
if (refreshAttempted && !options?.skipAuth) await reapplyAuth(this.auth, authCtx);
|
|
1218
|
+
const req = new Request(authCtx.url, authCtx.init);
|
|
1219
|
+
let res;
|
|
1002
1220
|
try {
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
url,
|
|
1010
|
-
init: freshInit,
|
|
1011
|
-
options
|
|
1012
|
-
});
|
|
1013
|
-
init.headers = freshInit.headers;
|
|
1014
|
-
}
|
|
1015
|
-
const req = new Request(url, init);
|
|
1016
|
-
const res = await this.fetchImpl(req);
|
|
1017
|
-
if (res.status === 401 && this.onUnauthenticated && !refreshAttempted) {
|
|
1018
|
-
if (await this.onUnauthenticated(res.clone())) {
|
|
1019
|
-
refreshAttempted = true;
|
|
1020
|
-
if (timeoutId) clearTimeout(timeoutId);
|
|
1021
|
-
continue;
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
const status = res.status;
|
|
1025
|
-
const contentType = res.headers.get("content-type") || "";
|
|
1026
|
-
if (!res.ok) {
|
|
1027
|
-
if (!options?.skipRetry && this.retryPolicy.maxAttempts > 0 && retryAttempt < this.retryPolicy.maxAttempts && this.retryPolicy.retryStatusCodes?.includes(status) && this.retryPolicy.retryMethods?.includes(method)) {
|
|
1028
|
-
let shouldRetry = true;
|
|
1029
|
-
if (this.retryPolicy.shouldRetry) shouldRetry = await this.retryPolicy.shouldRetry({
|
|
1030
|
-
url,
|
|
1031
|
-
method,
|
|
1032
|
-
status,
|
|
1033
|
-
attempt: retryAttempt,
|
|
1034
|
-
response: res.clone()
|
|
1035
|
-
});
|
|
1036
|
-
if (shouldRetry) {
|
|
1037
|
-
retryAttempt++;
|
|
1038
|
-
let delay = this.retryPolicy.baseDelayMs * 2 ** (retryAttempt - 1);
|
|
1039
|
-
if (this.retryPolicy.respectRetryAfter) {
|
|
1040
|
-
const retryAfter = res.headers.get("Retry-After") || res.headers.get("retry-after");
|
|
1041
|
-
if (retryAfter) {
|
|
1042
|
-
delay = parseInt(retryAfter, 10) * 1e3;
|
|
1043
|
-
this.logger.warn(`Request failed with status ${status}. Retrying after ${delay}ms due to Retry-After header...`, {
|
|
1044
|
-
method,
|
|
1045
|
-
url,
|
|
1046
|
-
status,
|
|
1047
|
-
retryAttempt: retryAttempt + 1
|
|
1048
|
-
});
|
|
1049
|
-
}
|
|
1050
|
-
} else this.logger.warn(`Request failed with status ${status}. Retrying attempt ${retryAttempt} after ${delay}ms...`, {
|
|
1051
|
-
method,
|
|
1052
|
-
url,
|
|
1053
|
-
status,
|
|
1054
|
-
retryAttempt
|
|
1055
|
-
});
|
|
1056
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1057
|
-
continue;
|
|
1058
|
-
}
|
|
1059
|
-
}
|
|
1060
|
-
}
|
|
1061
|
-
let data;
|
|
1062
|
-
if (contentType.includes("json")) data = await res.json();
|
|
1063
|
-
else if (contentType.includes("application/octet-stream") || contentType.includes("application/pdf") || contentType.includes("image/") || contentType.includes("video/") || contentType.includes("audio/") || contentType.startsWith("application/zip") || contentType.startsWith("application/x-")) data = await res.blob();
|
|
1064
|
-
else data = await res.text();
|
|
1065
|
-
await this.runAfterHooks(new Request(url, init), res, data);
|
|
1066
|
-
const duration = Date.now() - startTime;
|
|
1067
|
-
this.logger.info("HTTP request successful", {
|
|
1068
|
-
method,
|
|
1069
|
-
url,
|
|
1070
|
-
status: res.status,
|
|
1071
|
-
durationMs: duration
|
|
1072
|
-
});
|
|
1073
|
-
this.metrics.collect({
|
|
1074
|
-
method,
|
|
1075
|
-
path,
|
|
1076
|
-
status: res.status,
|
|
1077
|
-
durationMs: duration,
|
|
1078
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1079
|
-
success: true
|
|
1080
|
-
});
|
|
1081
|
-
return {
|
|
1082
|
-
data,
|
|
1083
|
-
status
|
|
1084
|
-
};
|
|
1085
|
-
} catch (error) {
|
|
1086
|
-
const duration = Date.now() - startTime;
|
|
1087
|
-
this.logger.error("HTTP request failed", error, {
|
|
1088
|
-
method,
|
|
1089
|
-
url,
|
|
1090
|
-
durationMs: duration
|
|
1091
|
-
});
|
|
1092
|
-
this.metrics.collect({
|
|
1093
|
-
method,
|
|
1094
|
-
path,
|
|
1095
|
-
status: error instanceof ApiError ? error.status : void 0,
|
|
1096
|
-
durationMs: duration,
|
|
1097
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1098
|
-
success: false,
|
|
1099
|
-
error: error instanceof Error ? error.message : String(error)
|
|
1100
|
-
});
|
|
1101
|
-
throw error;
|
|
1102
|
-
} finally {
|
|
1221
|
+
res = await this.fetchImpl(req);
|
|
1222
|
+
} catch (err) {
|
|
1223
|
+
throw createNetworkError(method, authCtx.url, err);
|
|
1224
|
+
}
|
|
1225
|
+
if (await this.shouldRefresh(res, refreshAttempted)) {
|
|
1226
|
+
refreshAttempted = true;
|
|
1103
1227
|
if (timeoutId) clearTimeout(timeoutId);
|
|
1228
|
+
continue;
|
|
1229
|
+
}
|
|
1230
|
+
const status = res.status;
|
|
1231
|
+
if (!res.ok) {
|
|
1232
|
+
const retryInfo = await this.getRetryInfo(res, authCtx.url, method, status, retryAttempt, options?.skipRetry);
|
|
1233
|
+
if (retryInfo) {
|
|
1234
|
+
retryAttempt++;
|
|
1235
|
+
this.logRetry(method, authCtx.url, status, retryAttempt, retryInfo);
|
|
1236
|
+
await new Promise((resolve) => setTimeout(resolve, retryInfo.delay));
|
|
1237
|
+
continue;
|
|
1238
|
+
}
|
|
1104
1239
|
}
|
|
1240
|
+
const data = await parseResponseData(res, method, authCtx.url);
|
|
1241
|
+
await this.runAfterHooks(new Request(authCtx.url, authCtx.init), res, data);
|
|
1242
|
+
if (!res.ok) throw createStatusError(method, authCtx.url, res, data);
|
|
1243
|
+
recordSuccessfulRequest(this.logger, this.metrics, method, path, authCtx.url, res.status, Date.now() - startTime);
|
|
1244
|
+
return {
|
|
1245
|
+
data,
|
|
1246
|
+
status
|
|
1247
|
+
};
|
|
1248
|
+
} catch (error) {
|
|
1249
|
+
recordFailedRequest(this.logger, this.metrics, method, path, authCtx.url, Date.now() - startTime, error);
|
|
1250
|
+
throw error;
|
|
1251
|
+
} finally {
|
|
1252
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
1105
1253
|
}
|
|
1106
|
-
}
|
|
1107
|
-
return doFetch();
|
|
1254
|
+
}
|
|
1108
1255
|
}
|
|
1109
|
-
/**
|
|
1110
|
-
* Convenience method for GET requests.
|
|
1111
|
-
*
|
|
1112
|
-
* @example
|
|
1113
|
-
* ```ts
|
|
1114
|
-
* const { data } = await client.get('/users', { query: { page: 1 } });
|
|
1115
|
-
* ```
|
|
1116
|
-
*/
|
|
1117
1256
|
async get(path, options) {
|
|
1118
1257
|
return this.request("GET", path, void 0, options);
|
|
1119
1258
|
}
|
|
1120
|
-
/**
|
|
1121
|
-
* Convenience method for POST requests.
|
|
1122
|
-
*
|
|
1123
|
-
* @example
|
|
1124
|
-
* ```ts
|
|
1125
|
-
* const { data } = await client.post('/users', { name: 'John' });
|
|
1126
|
-
* ```
|
|
1127
|
-
*/
|
|
1128
1259
|
async post(path, body, options) {
|
|
1129
1260
|
return this.request("POST", path, body, options);
|
|
1130
1261
|
}
|
|
1131
|
-
/**
|
|
1132
|
-
* Convenience method for PUT requests.
|
|
1133
|
-
*
|
|
1134
|
-
* @example
|
|
1135
|
-
* ```ts
|
|
1136
|
-
* const { data } = await client.put('/users/1', { name: 'John Updated' });
|
|
1137
|
-
* ```
|
|
1138
|
-
*/
|
|
1139
1262
|
async put(path, body, options) {
|
|
1140
1263
|
return this.request("PUT", path, body, options);
|
|
1141
1264
|
}
|
|
1142
|
-
/**
|
|
1143
|
-
* Convenience method for PATCH requests.
|
|
1144
|
-
*
|
|
1145
|
-
* @example
|
|
1146
|
-
* ```ts
|
|
1147
|
-
* const { data } = await client.patch('/users/1', { name: 'John' });
|
|
1148
|
-
* ```
|
|
1149
|
-
*/
|
|
1150
1265
|
async patch(path, body, options) {
|
|
1151
1266
|
return this.request("PATCH", path, body, options);
|
|
1152
1267
|
}
|
|
1153
|
-
/**
|
|
1154
|
-
* Convenience method for DELETE requests.
|
|
1155
|
-
*
|
|
1156
|
-
* @example
|
|
1157
|
-
* ```ts
|
|
1158
|
-
* const { data } = await client.delete('/users/1');
|
|
1159
|
-
* ```
|
|
1160
|
-
*/
|
|
1161
1268
|
async delete(path, options) {
|
|
1162
1269
|
return this.request("DELETE", path, void 0, options);
|
|
1163
1270
|
}
|
|
1164
1271
|
/**
|
|
1165
|
-
* Create a strongly-typed endpoint
|
|
1272
|
+
* Create a strongly-typed HTTP endpoint.
|
|
1166
1273
|
* Works with any Standard Schema-compatible library (Zod, Valibot, ArkType, etc.)
|
|
1167
1274
|
*
|
|
1168
|
-
* @param config - Endpoint configuration with schemas
|
|
1169
|
-
* @returns Endpoint call function
|
|
1170
|
-
*
|
|
1171
1275
|
* @example
|
|
1172
1276
|
* ```ts
|
|
1173
|
-
* // With Zod
|
|
1174
|
-
* import { z } from 'zod';
|
|
1175
1277
|
* const getUser = client.createEndpoint({
|
|
1176
1278
|
* method: 'GET',
|
|
1177
1279
|
* path: '/users/:id',
|
|
1178
1280
|
* response: z.object({ id: z.string(), name: z.string() }),
|
|
1179
1281
|
* pathParams: z.object({ id: z.string() }),
|
|
1180
1282
|
* });
|
|
1181
|
-
*
|
|
1182
|
-
* // With Valibot
|
|
1183
|
-
* import * as v from 'valibot';
|
|
1184
|
-
* const getUser = client.createEndpoint({
|
|
1185
|
-
* method: 'GET',
|
|
1186
|
-
* path: '/users/:id',
|
|
1187
|
-
* response: v.object({ id: v.string(), name: v.string() }),
|
|
1188
|
-
* pathParams: v.object({ id: v.string() }),
|
|
1189
|
-
* });
|
|
1190
1283
|
* ```
|
|
1191
1284
|
*/
|
|
1192
1285
|
createEndpoint(config) {
|
|
@@ -1194,19 +1287,34 @@ var HttpClient = class {
|
|
|
1194
1287
|
return (params) => endpoint.call(params);
|
|
1195
1288
|
}
|
|
1196
1289
|
/**
|
|
1197
|
-
* Create a strongly-typed WebSocket endpoint
|
|
1290
|
+
* Create a strongly-typed WebSocket endpoint.
|
|
1198
1291
|
*
|
|
1199
|
-
* @
|
|
1200
|
-
*
|
|
1292
|
+
* @example
|
|
1293
|
+
* ```ts
|
|
1294
|
+
* const chat = client.createWebSocket({
|
|
1295
|
+
* path: '/ws/chat',
|
|
1296
|
+
* send: z.object({ text: z.string() }),
|
|
1297
|
+
* receive: z.object({ text: z.string(), user: z.string() }),
|
|
1298
|
+
* });
|
|
1299
|
+
* const conn = await chat();
|
|
1300
|
+
* ```
|
|
1201
1301
|
*/
|
|
1202
1302
|
createWebSocket(config) {
|
|
1203
1303
|
return new WSEndpointImpl(this, config).createCall();
|
|
1204
1304
|
}
|
|
1205
1305
|
/**
|
|
1206
|
-
* Create a strongly-typed Server-Sent Events
|
|
1306
|
+
* Create a strongly-typed Server-Sent Events endpoint.
|
|
1207
1307
|
*
|
|
1208
|
-
* @
|
|
1209
|
-
*
|
|
1308
|
+
* @example
|
|
1309
|
+
* ```ts
|
|
1310
|
+
* const stream = client.createSSE({
|
|
1311
|
+
* method: 'GET',
|
|
1312
|
+
* path: '/events',
|
|
1313
|
+
* response: z.object({ message: z.string() }),
|
|
1314
|
+
* });
|
|
1315
|
+
* const conn = await stream();
|
|
1316
|
+
* conn.on('message', (data) => console.log(data));
|
|
1317
|
+
* ```
|
|
1210
1318
|
*/
|
|
1211
1319
|
createSSE(config) {
|
|
1212
1320
|
return new SSEEndpointImpl(this, config).createCall();
|
|
@@ -1233,9 +1341,11 @@ exports.SSEEndpointImpl = SSEEndpointImpl;
|
|
|
1233
1341
|
exports.SchemaDefinitionError = SchemaDefinitionError;
|
|
1234
1342
|
exports.WSConnectionImpl = WSConnectionImpl;
|
|
1235
1343
|
exports.WSEndpointImpl = WSEndpointImpl;
|
|
1344
|
+
exports.formatValidationIssues = formatValidationIssues;
|
|
1236
1345
|
exports.isStandardSchema = isStandardSchema;
|
|
1237
1346
|
exports.parseOrThrow = parseOrThrow;
|
|
1238
1347
|
exports.safeParse = safeParse;
|
|
1239
1348
|
exports.toQueryString = toQueryString;
|
|
1349
|
+
exports.toRequestQuery = toRequestQuery;
|
|
1240
1350
|
|
|
1241
1351
|
//# sourceMappingURL=index.cjs.map
|