zlient 3.3.3 → 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 +528 -381
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +527 -382
- 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 -9
- 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,39 +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 buffer = "";
|
|
552
|
-
while (true) {
|
|
553
|
-
const { done, value } = await reader.read();
|
|
554
|
-
if (done) break;
|
|
555
|
-
buffer += decoder.decode(value, { stream: true });
|
|
556
|
-
const parts = buffer.split(/\r\n|\r|\n/);
|
|
557
|
-
buffer = parts.pop() || "";
|
|
558
|
-
let eventData = "";
|
|
559
|
-
let eventName = "message";
|
|
560
|
-
for (const line of parts) {
|
|
561
|
-
if (line === "") {
|
|
562
|
-
if (eventData) {
|
|
563
|
-
this.handleEvent(eventName, eventData.trim());
|
|
564
|
-
eventData = "";
|
|
565
|
-
eventName = "message";
|
|
566
|
-
}
|
|
567
|
-
continue;
|
|
568
|
-
}
|
|
569
|
-
if (line.startsWith(":")) continue;
|
|
570
|
-
const colonIndex = line.indexOf(":");
|
|
571
|
-
if (colonIndex === -1) this.processField(line, "", (name, data) => {
|
|
572
|
-
eventName = name || eventName;
|
|
573
|
-
eventData += data;
|
|
574
|
-
});
|
|
575
|
-
else {
|
|
576
|
-
const field = line.slice(0, colonIndex);
|
|
577
|
-
const value = line.slice(colonIndex + 1).trim();
|
|
578
|
-
if (field === "event") eventName = value;
|
|
579
|
-
else if (field === "data") eventData += (eventData ? "\n" : "") + value;
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
}
|
|
671
|
+
await this.readStream(reader);
|
|
583
672
|
} catch (error) {
|
|
584
673
|
if (error.name === "AbortError") return;
|
|
585
674
|
this._readyState = 2;
|
|
@@ -588,16 +677,78 @@ var SSEConnectionImpl = class {
|
|
|
588
677
|
this._readyState = 2;
|
|
589
678
|
}
|
|
590
679
|
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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
|
+
}
|
|
594
748
|
}
|
|
595
749
|
async handleEvent(event, data) {
|
|
596
|
-
let parsedData = data;
|
|
750
|
+
let parsedData = parseRealtimeData(data);
|
|
597
751
|
try {
|
|
598
|
-
if (typeof data === "string") try {
|
|
599
|
-
parsedData = JSON.parse(data);
|
|
600
|
-
} catch {}
|
|
601
752
|
const schema = this.getSchema(event);
|
|
602
753
|
if (!this.options.skipResponseValidation && schema) parsedData = await parseOrThrow(schema, parsedData);
|
|
603
754
|
this.emit(event, parsedData);
|
|
@@ -607,24 +758,9 @@ var SSEConnectionImpl = class {
|
|
|
607
758
|
}
|
|
608
759
|
getSchema(event) {
|
|
609
760
|
if (!this.responseSchema) return void 0;
|
|
610
|
-
if ("~standard" in this.responseSchema)
|
|
611
|
-
if (event === "message") return this.responseSchema;
|
|
612
|
-
return;
|
|
613
|
-
}
|
|
761
|
+
if ("~standard" in this.responseSchema) return event === "message" ? this.responseSchema : void 0;
|
|
614
762
|
return this.responseSchema[event];
|
|
615
763
|
}
|
|
616
|
-
on(event, handler) {
|
|
617
|
-
if (!this.handlers.has(event)) this.handlers.set(event, /* @__PURE__ */ new Set());
|
|
618
|
-
this.handlers.get(event).add(handler);
|
|
619
|
-
}
|
|
620
|
-
off(event, handler) {
|
|
621
|
-
const handlers = this.handlers.get(event);
|
|
622
|
-
if (handlers) handlers.delete(handler);
|
|
623
|
-
}
|
|
624
|
-
emit(event, ...args) {
|
|
625
|
-
const handlers = this.handlers.get(event);
|
|
626
|
-
if (handlers) handlers.forEach((handler) => handler(...args));
|
|
627
|
-
}
|
|
628
764
|
close() {
|
|
629
765
|
this.abortController.abort();
|
|
630
766
|
this._readyState = 2;
|
|
@@ -643,18 +779,16 @@ var SSEEndpointImpl = class {
|
|
|
643
779
|
createCall() {
|
|
644
780
|
return async (params) => {
|
|
645
781
|
const { query, pathParams, data, headers, signal } = params || {};
|
|
646
|
-
const
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
} else pathStr = this.config.path;
|
|
657
|
-
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, {
|
|
658
792
|
skipResponseValidation: this.config.advanced?.skipResponseValidation,
|
|
659
793
|
withCredentials: this.config.advanced?.withCredentials,
|
|
660
794
|
method: this.config.method,
|
|
@@ -666,31 +800,29 @@ var SSEEndpointImpl = class {
|
|
|
666
800
|
},
|
|
667
801
|
signal,
|
|
668
802
|
auth: this.config.advanced?.skipAuth ? void 0 : this.client.getAuth(),
|
|
669
|
-
logger: this.client.getLogger()
|
|
803
|
+
logger: this.client.getLogger(),
|
|
804
|
+
fetch: this.client.getFetch()
|
|
670
805
|
});
|
|
671
806
|
};
|
|
672
807
|
}
|
|
673
808
|
};
|
|
674
809
|
//#endregion
|
|
675
810
|
//#region lib/ws/ws-client.ts
|
|
676
|
-
var WSConnectionImpl = class {
|
|
811
|
+
var WSConnectionImpl = class extends EventEmitter {
|
|
677
812
|
constructor(url, sendSchema, receiveSchema, skipRequestValidation = false, skipResponseValidation = false, protocols) {
|
|
813
|
+
super();
|
|
678
814
|
this.sendSchema = sendSchema;
|
|
679
815
|
this.receiveSchema = receiveSchema;
|
|
680
816
|
this.skipRequestValidation = skipRequestValidation;
|
|
681
817
|
this.skipResponseValidation = skipResponseValidation;
|
|
682
|
-
this.handlers = /* @__PURE__ */ new Map();
|
|
683
818
|
if (typeof WebSocket === "undefined") throw new Error("WebSocket is not defined. Ensure you are in a supported environment.");
|
|
684
819
|
this.ws = new WebSocket(url, protocols);
|
|
685
820
|
this.ws.onopen = () => this.emit("open");
|
|
686
821
|
this.ws.onclose = (event) => this.emit("close", event);
|
|
687
822
|
this.ws.onerror = (event) => this.emit("error", event);
|
|
688
823
|
this.ws.onmessage = async (event) => {
|
|
689
|
-
let data = event.data;
|
|
824
|
+
let data = parseRealtimeData(event.data);
|
|
690
825
|
try {
|
|
691
|
-
if (typeof data === "string") try {
|
|
692
|
-
data = JSON.parse(data);
|
|
693
|
-
} catch {}
|
|
694
826
|
if (!this.skipResponseValidation && this.receiveSchema) data = await parseOrThrow(this.receiveSchema, data);
|
|
695
827
|
this.emit("message", data);
|
|
696
828
|
} catch (error) {
|
|
@@ -700,20 +832,13 @@ var WSConnectionImpl = class {
|
|
|
700
832
|
}
|
|
701
833
|
async send(data) {
|
|
702
834
|
if (!this.skipRequestValidation && this.sendSchema) await parseOrThrow(this.sendSchema, data);
|
|
703
|
-
|
|
704
|
-
this.ws.send(message);
|
|
835
|
+
this.ws.send(serializeRealtimeData(data));
|
|
705
836
|
}
|
|
706
837
|
on(event, handler) {
|
|
707
|
-
|
|
708
|
-
this.handlers.get(event).add(handler);
|
|
838
|
+
super.on(event, handler);
|
|
709
839
|
}
|
|
710
840
|
off(event, handler) {
|
|
711
|
-
|
|
712
|
-
if (handlers) handlers.delete(handler);
|
|
713
|
-
}
|
|
714
|
-
emit(event, ...args) {
|
|
715
|
-
const handlers = this.handlers.get(event);
|
|
716
|
-
if (handlers) handlers.forEach((handler) => handler(...args));
|
|
841
|
+
super.off(event, handler);
|
|
717
842
|
}
|
|
718
843
|
close(code, reason) {
|
|
719
844
|
this.ws.close(code, reason);
|
|
@@ -730,14 +855,16 @@ var WSEndpointImpl = class {
|
|
|
730
855
|
this.config = config;
|
|
731
856
|
}
|
|
732
857
|
createCall() {
|
|
733
|
-
return (params) => {
|
|
858
|
+
return async (params) => {
|
|
734
859
|
const { query, pathParams, protocols } = params || {};
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
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);
|
|
741
868
|
};
|
|
742
869
|
}
|
|
743
870
|
};
|
|
@@ -751,42 +878,185 @@ var EndpointImpl = class {
|
|
|
751
878
|
async call(params) {
|
|
752
879
|
const { data, query, pathParams, signal } = params;
|
|
753
880
|
const headers = "headers" in params ? params.headers : void 0;
|
|
754
|
-
|
|
755
|
-
const
|
|
756
|
-
|
|
757
|
-
const missingHeaders = this.config.mustHeaderKeys.filter((key) => !headers || !(key in headers));
|
|
758
|
-
if (missingHeaders.length > 0) throw new Error(`Missing required header(s): ${missingHeaders.join(", ")}`);
|
|
759
|
-
}
|
|
760
|
-
if (!skipRequestValidation && this.config.request && data !== void 0) await parseOrThrow(this.config.request, data);
|
|
761
|
-
if (!skipRequestValidation && this.config.query && query !== void 0) await parseOrThrow(this.config.query, query);
|
|
762
|
-
if (!skipRequestValidation && this.config.pathParams && pathParams !== void 0) await parseOrThrow(this.config.pathParams, pathParams);
|
|
763
|
-
if (this.config.request && data === void 0) throw new Error("Missing required request body (data)");
|
|
764
|
-
if (this.config.pathParams && pathParams === void 0) throw new Error("Missing required path parameters (pathParams)");
|
|
765
|
-
let pathStr;
|
|
766
|
-
if (typeof this.config.path === "function") {
|
|
767
|
-
if (!pathParams) throw new Error("Path function requires pathParams");
|
|
768
|
-
pathStr = this.config.path(pathParams);
|
|
769
|
-
} else pathStr = this.config.path;
|
|
770
|
-
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,
|
|
771
884
|
query,
|
|
885
|
+
pathParams
|
|
886
|
+
});
|
|
887
|
+
const { data: responseData, status } = await this.client.request(this.config.method, pathStr, data, {
|
|
888
|
+
query: toRequestQuery(parsedQuery),
|
|
772
889
|
headers,
|
|
773
890
|
baseUrlKey: this.config.advanced?.baseUrlKey,
|
|
774
891
|
skipAuth: this.config.advanced?.skipAuth,
|
|
775
892
|
skipRetry: this.config.advanced?.skipRetry,
|
|
776
893
|
signal
|
|
777
894
|
});
|
|
895
|
+
if (this.config.advanced?.skipResponseValidation) return responseData;
|
|
778
896
|
const schema = this.config.response;
|
|
779
|
-
if (
|
|
780
|
-
|
|
897
|
+
if (isStandardSchema(schema)) return await parseOrThrow(schema, responseData, {
|
|
898
|
+
label: `Response body for status ${status}`,
|
|
899
|
+
status
|
|
900
|
+
});
|
|
781
901
|
const specificSchema = schema[status];
|
|
782
902
|
if (!specificSchema) throw new SchemaDefinitionError(status);
|
|
783
|
-
return await parseOrThrow(specificSchema, responseData
|
|
903
|
+
return await parseOrThrow(specificSchema, responseData, {
|
|
904
|
+
label: `Response body for status ${status}`,
|
|
905
|
+
status
|
|
906
|
+
});
|
|
784
907
|
}
|
|
785
908
|
};
|
|
786
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
|
|
787
1057
|
//#region lib/http/http-client.ts
|
|
788
1058
|
/**
|
|
789
|
-
* HTTP client with built-in authentication, and interceptors.
|
|
1059
|
+
* HTTP client with built-in authentication, retry, and interceptors.
|
|
790
1060
|
* Supports multiple base URLs, type-safe requests, and comprehensive error handling.
|
|
791
1061
|
*
|
|
792
1062
|
* @example
|
|
@@ -796,17 +1066,9 @@ var EndpointImpl = class {
|
|
|
796
1066
|
* headers: { 'Content-Type': 'application/json' },
|
|
797
1067
|
* timeout: { requestTimeoutMs: 30000 }
|
|
798
1068
|
* });
|
|
799
|
-
*
|
|
800
|
-
* const { data } = await client.request('GET', '/users', undefined, { query: { page: 1 } });
|
|
801
1069
|
* ```
|
|
802
1070
|
*/
|
|
803
1071
|
var HttpClient = class {
|
|
804
|
-
/**
|
|
805
|
-
* Creates a new HTTP client instance.
|
|
806
|
-
*
|
|
807
|
-
* @param opts - Client configuration options
|
|
808
|
-
* @throws {Error} If no fetch implementation is available
|
|
809
|
-
*/
|
|
810
1072
|
constructor(opts) {
|
|
811
1073
|
this.fetchImpl = opts.fetch ?? globalThis.fetch?.bind(globalThis);
|
|
812
1074
|
if (!this.fetchImpl) throw new Error("No fetch implementation found. Pass one via options.fetch.");
|
|
@@ -823,20 +1085,12 @@ var HttpClient = class {
|
|
|
823
1085
|
if (this.retryPolicy.baseDelayMs < 0) throw new Error("retry.baseDelayMs must be non-negative");
|
|
824
1086
|
this.timeoutMs = opts.timeout?.requestTimeoutMs;
|
|
825
1087
|
if (this.timeoutMs !== void 0 && this.timeoutMs < 0) throw new Error("timeout.requestTimeoutMs must be non-negative");
|
|
826
|
-
this.auth = opts
|
|
1088
|
+
this.auth = opts.auth ?? new NoAuth();
|
|
827
1089
|
this.logger = new LoggerUtil(opts.logger ?? new NoOpLogger());
|
|
828
1090
|
this.metrics = opts.metrics ?? new NoOpMetricsCollector();
|
|
829
1091
|
this.onUnauthenticated = opts.onUnauthenticated;
|
|
830
1092
|
}
|
|
831
|
-
/**
|
|
832
|
-
* Set or update the authentication provider.
|
|
833
|
-
*
|
|
834
|
-
* @param auth - Authentication provider instance
|
|
835
|
-
* @example
|
|
836
|
-
* ```ts
|
|
837
|
-
* client.setAuth(new BearerTokenAuth(() => getToken()));
|
|
838
|
-
* ```
|
|
839
|
-
*/
|
|
1093
|
+
/** Set or update the authentication provider at runtime. */
|
|
840
1094
|
setAuth(auth) {
|
|
841
1095
|
this.auth = auth;
|
|
842
1096
|
}
|
|
@@ -849,20 +1103,12 @@ var HttpClient = class {
|
|
|
849
1103
|
}
|
|
850
1104
|
return url.replace(/\/$/, "");
|
|
851
1105
|
}
|
|
852
|
-
/**
|
|
853
|
-
* Run all registered before-request hooks.
|
|
854
|
-
* @private
|
|
855
|
-
*/
|
|
856
1106
|
async runBeforeHooks(url, init) {
|
|
857
1107
|
for (const h of this.interceptors.beforeRequest ?? []) await h({
|
|
858
1108
|
url,
|
|
859
1109
|
init
|
|
860
1110
|
});
|
|
861
1111
|
}
|
|
862
|
-
/**
|
|
863
|
-
* Run all registered after-response hooks.
|
|
864
|
-
* @private
|
|
865
|
-
*/
|
|
866
1112
|
async runAfterHooks(req, res, parsed) {
|
|
867
1113
|
for (const h of this.interceptors.afterResponse ?? []) await h({
|
|
868
1114
|
request: req,
|
|
@@ -871,19 +1117,40 @@ var HttpClient = class {
|
|
|
871
1117
|
});
|
|
872
1118
|
}
|
|
873
1119
|
/**
|
|
874
|
-
*
|
|
875
|
-
*
|
|
876
|
-
* @returns Object mapping base URL keys to their resolved URLs
|
|
1120
|
+
* Returns retry info if the response should be retried, or null if not.
|
|
877
1121
|
*/
|
|
878
|
-
|
|
879
|
-
|
|
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
|
+
});
|
|
880
1143
|
}
|
|
881
1144
|
/**
|
|
882
|
-
*
|
|
883
|
-
*
|
|
884
|
-
* @param key - Base URL key (defaults to 'default' if not provided)
|
|
885
|
-
* @returns Resolved base URL string
|
|
1145
|
+
* Checks whether the 401 response should trigger a token refresh + retry.
|
|
886
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
|
+
}
|
|
887
1154
|
getBaseUrl(key) {
|
|
888
1155
|
return this.resolveBaseUrl(key);
|
|
889
1156
|
}
|
|
@@ -899,257 +1166,120 @@ var HttpClient = class {
|
|
|
899
1166
|
getLogger() {
|
|
900
1167
|
return this.logger;
|
|
901
1168
|
}
|
|
1169
|
+
/** @internal */
|
|
1170
|
+
getFetch() {
|
|
1171
|
+
return this.fetchImpl;
|
|
1172
|
+
}
|
|
902
1173
|
/**
|
|
903
1174
|
* Make an HTTP request with automatic retry, authentication, and validation.
|
|
904
1175
|
*
|
|
905
|
-
* @param method - HTTP method (GET, POST, PUT, etc.)
|
|
906
|
-
* @param path - Request path (will be appended to base URL)
|
|
907
|
-
* @param body - Request body (will be JSON.stringify'd if Content-Type is json)
|
|
908
|
-
* @param options - Additional request options (headers, query params, etc.)
|
|
909
|
-
* @returns Promise resolving to response data and Response object
|
|
910
|
-
* @throws {ApiError} If request fails or response validation fails
|
|
911
|
-
*
|
|
912
1176
|
* @example
|
|
913
1177
|
* ```ts
|
|
914
|
-
* const { data
|
|
915
|
-
* query: { page: 1
|
|
1178
|
+
* const { data } = await client.request('GET', '/users', undefined, {
|
|
1179
|
+
* query: { page: 1 },
|
|
916
1180
|
* headers: { 'X-Custom': 'value' }
|
|
917
1181
|
* });
|
|
918
1182
|
* ```
|
|
919
1183
|
*/
|
|
920
1184
|
async request(method, path, body, options) {
|
|
921
1185
|
const startTime = Date.now();
|
|
922
|
-
|
|
923
|
-
this.logger.debug("HTTP request initiated", {
|
|
924
|
-
method,
|
|
925
|
-
path,
|
|
926
|
-
baseUrlKey: options?.baseUrlKey,
|
|
927
|
-
hasBody: body !== void 0
|
|
928
|
-
});
|
|
1186
|
+
const base = this.resolveBaseUrl(options?.baseUrlKey);
|
|
929
1187
|
const headers = {
|
|
930
1188
|
...this.headers,
|
|
931
1189
|
...options?.headers ?? {}
|
|
932
1190
|
};
|
|
933
1191
|
const controller = new AbortController();
|
|
934
1192
|
const signal = options?.signal ?? controller.signal;
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
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
|
+
});
|
|
942
1199
|
const init = {
|
|
943
1200
|
method,
|
|
944
1201
|
headers,
|
|
945
|
-
body:
|
|
1202
|
+
body: serializeRequestBody(body, headers),
|
|
946
1203
|
signal
|
|
947
1204
|
};
|
|
948
|
-
|
|
949
|
-
url
|
|
1205
|
+
const authCtx = {
|
|
1206
|
+
url: `${base}${path}${toQueryString(options?.query)}`,
|
|
950
1207
|
init,
|
|
951
1208
|
options
|
|
952
|
-
}
|
|
953
|
-
if (
|
|
954
|
-
await this.runBeforeHooks(url, init);
|
|
1209
|
+
};
|
|
1210
|
+
if (!options?.skipAuth) await this.auth.apply(authCtx);
|
|
1211
|
+
await this.runBeforeHooks(authCtx.url, authCtx.init);
|
|
955
1212
|
let refreshAttempted = false;
|
|
956
1213
|
let retryAttempt = 0;
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
if (
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
controller.abort(timeoutError);
|
|
964
|
-
}, 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;
|
|
965
1220
|
try {
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
url,
|
|
973
|
-
init: freshInit,
|
|
974
|
-
options
|
|
975
|
-
});
|
|
976
|
-
init.headers = freshInit.headers;
|
|
977
|
-
}
|
|
978
|
-
const req = new Request(url, init);
|
|
979
|
-
const res = await this.fetchImpl(req);
|
|
980
|
-
if (res.status === 401 && this.onUnauthenticated && !refreshAttempted) {
|
|
981
|
-
if (await this.onUnauthenticated(res.clone())) {
|
|
982
|
-
refreshAttempted = true;
|
|
983
|
-
if (timeoutId) clearTimeout(timeoutId);
|
|
984
|
-
continue;
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
const status = res.status;
|
|
988
|
-
const contentType = res.headers.get("content-type") || "";
|
|
989
|
-
if (!res.ok) {
|
|
990
|
-
if (!options?.skipRetry && this.retryPolicy.maxAttempts > 0 && retryAttempt < this.retryPolicy.maxAttempts && this.retryPolicy.retryStatusCodes?.includes(status) && this.retryPolicy.retryMethods?.includes(method)) {
|
|
991
|
-
let shouldRetry = true;
|
|
992
|
-
if (this.retryPolicy.shouldRetry) shouldRetry = await this.retryPolicy.shouldRetry({
|
|
993
|
-
url,
|
|
994
|
-
method,
|
|
995
|
-
status,
|
|
996
|
-
attempt: retryAttempt,
|
|
997
|
-
response: res.clone()
|
|
998
|
-
});
|
|
999
|
-
if (shouldRetry) {
|
|
1000
|
-
retryAttempt++;
|
|
1001
|
-
let delay = this.retryPolicy.baseDelayMs * 2 ** (retryAttempt - 1);
|
|
1002
|
-
if (this.retryPolicy.respectRetryAfter) {
|
|
1003
|
-
const retryAfter = res.headers.get("Retry-After") || res.headers.get("retry-after");
|
|
1004
|
-
if (retryAfter) {
|
|
1005
|
-
delay = parseInt(retryAfter, 10) * 1e3;
|
|
1006
|
-
this.logger.warn(`Request failed with status ${status}. Retrying after ${delay}ms due to Retry-After header...`, {
|
|
1007
|
-
method,
|
|
1008
|
-
url,
|
|
1009
|
-
status,
|
|
1010
|
-
retryAttempt: retryAttempt + 1
|
|
1011
|
-
});
|
|
1012
|
-
}
|
|
1013
|
-
} else this.logger.warn(`Request failed with status ${status}. Retrying attempt ${retryAttempt} after ${delay}ms...`, {
|
|
1014
|
-
method,
|
|
1015
|
-
url,
|
|
1016
|
-
status,
|
|
1017
|
-
retryAttempt
|
|
1018
|
-
});
|
|
1019
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1020
|
-
continue;
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
let data;
|
|
1025
|
-
if (contentType.includes("json")) data = await res.json();
|
|
1026
|
-
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();
|
|
1027
|
-
else data = await res.text();
|
|
1028
|
-
await this.runAfterHooks(new Request(url, init), res, data);
|
|
1029
|
-
const duration = Date.now() - startTime;
|
|
1030
|
-
this.logger.info("HTTP request successful", {
|
|
1031
|
-
method,
|
|
1032
|
-
url,
|
|
1033
|
-
status: res.status,
|
|
1034
|
-
durationMs: duration
|
|
1035
|
-
});
|
|
1036
|
-
this.metrics.collect({
|
|
1037
|
-
method,
|
|
1038
|
-
path,
|
|
1039
|
-
status: res.status,
|
|
1040
|
-
durationMs: duration,
|
|
1041
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1042
|
-
success: true
|
|
1043
|
-
});
|
|
1044
|
-
return {
|
|
1045
|
-
data,
|
|
1046
|
-
status
|
|
1047
|
-
};
|
|
1048
|
-
} catch (error) {
|
|
1049
|
-
const duration = Date.now() - startTime;
|
|
1050
|
-
this.logger.error("HTTP request failed", error, {
|
|
1051
|
-
method,
|
|
1052
|
-
url,
|
|
1053
|
-
durationMs: duration
|
|
1054
|
-
});
|
|
1055
|
-
this.metrics.collect({
|
|
1056
|
-
method,
|
|
1057
|
-
path,
|
|
1058
|
-
status: error instanceof ApiError ? error.status : void 0,
|
|
1059
|
-
durationMs: duration,
|
|
1060
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1061
|
-
success: false,
|
|
1062
|
-
error: error instanceof Error ? error.message : String(error)
|
|
1063
|
-
});
|
|
1064
|
-
throw error;
|
|
1065
|
-
} 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;
|
|
1066
1227
|
if (timeoutId) clearTimeout(timeoutId);
|
|
1228
|
+
continue;
|
|
1067
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
|
+
}
|
|
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);
|
|
1068
1253
|
}
|
|
1069
|
-
}
|
|
1070
|
-
return doFetch();
|
|
1254
|
+
}
|
|
1071
1255
|
}
|
|
1072
|
-
/**
|
|
1073
|
-
* Convenience method for GET requests.
|
|
1074
|
-
*
|
|
1075
|
-
* @example
|
|
1076
|
-
* ```ts
|
|
1077
|
-
* const { data } = await client.get('/users', { query: { page: 1 } });
|
|
1078
|
-
* ```
|
|
1079
|
-
*/
|
|
1080
1256
|
async get(path, options) {
|
|
1081
1257
|
return this.request("GET", path, void 0, options);
|
|
1082
1258
|
}
|
|
1083
|
-
/**
|
|
1084
|
-
* Convenience method for POST requests.
|
|
1085
|
-
*
|
|
1086
|
-
* @example
|
|
1087
|
-
* ```ts
|
|
1088
|
-
* const { data } = await client.post('/users', { name: 'John' });
|
|
1089
|
-
* ```
|
|
1090
|
-
*/
|
|
1091
1259
|
async post(path, body, options) {
|
|
1092
1260
|
return this.request("POST", path, body, options);
|
|
1093
1261
|
}
|
|
1094
|
-
/**
|
|
1095
|
-
* Convenience method for PUT requests.
|
|
1096
|
-
*
|
|
1097
|
-
* @example
|
|
1098
|
-
* ```ts
|
|
1099
|
-
* const { data } = await client.put('/users/1', { name: 'John Updated' });
|
|
1100
|
-
* ```
|
|
1101
|
-
*/
|
|
1102
1262
|
async put(path, body, options) {
|
|
1103
1263
|
return this.request("PUT", path, body, options);
|
|
1104
1264
|
}
|
|
1105
|
-
/**
|
|
1106
|
-
* Convenience method for PATCH requests.
|
|
1107
|
-
*
|
|
1108
|
-
* @example
|
|
1109
|
-
* ```ts
|
|
1110
|
-
* const { data } = await client.patch('/users/1', { name: 'John' });
|
|
1111
|
-
* ```
|
|
1112
|
-
*/
|
|
1113
1265
|
async patch(path, body, options) {
|
|
1114
1266
|
return this.request("PATCH", path, body, options);
|
|
1115
1267
|
}
|
|
1116
|
-
/**
|
|
1117
|
-
* Convenience method for DELETE requests.
|
|
1118
|
-
*
|
|
1119
|
-
* @example
|
|
1120
|
-
* ```ts
|
|
1121
|
-
* const { data } = await client.delete('/users/1');
|
|
1122
|
-
* ```
|
|
1123
|
-
*/
|
|
1124
1268
|
async delete(path, options) {
|
|
1125
1269
|
return this.request("DELETE", path, void 0, options);
|
|
1126
1270
|
}
|
|
1127
1271
|
/**
|
|
1128
|
-
* Create a strongly-typed endpoint
|
|
1272
|
+
* Create a strongly-typed HTTP endpoint.
|
|
1129
1273
|
* Works with any Standard Schema-compatible library (Zod, Valibot, ArkType, etc.)
|
|
1130
1274
|
*
|
|
1131
|
-
* @param config - Endpoint configuration with schemas
|
|
1132
|
-
* @returns Endpoint call function
|
|
1133
|
-
*
|
|
1134
1275
|
* @example
|
|
1135
1276
|
* ```ts
|
|
1136
|
-
* // With Zod
|
|
1137
|
-
* import { z } from 'zod';
|
|
1138
1277
|
* const getUser = client.createEndpoint({
|
|
1139
1278
|
* method: 'GET',
|
|
1140
1279
|
* path: '/users/:id',
|
|
1141
1280
|
* response: z.object({ id: z.string(), name: z.string() }),
|
|
1142
1281
|
* pathParams: z.object({ id: z.string() }),
|
|
1143
1282
|
* });
|
|
1144
|
-
*
|
|
1145
|
-
* // With Valibot
|
|
1146
|
-
* import * as v from 'valibot';
|
|
1147
|
-
* const getUser = client.createEndpoint({
|
|
1148
|
-
* method: 'GET',
|
|
1149
|
-
* path: '/users/:id',
|
|
1150
|
-
* response: v.object({ id: v.string(), name: v.string() }),
|
|
1151
|
-
* pathParams: v.object({ id: v.string() }),
|
|
1152
|
-
* });
|
|
1153
1283
|
* ```
|
|
1154
1284
|
*/
|
|
1155
1285
|
createEndpoint(config) {
|
|
@@ -1157,19 +1287,34 @@ var HttpClient = class {
|
|
|
1157
1287
|
return (params) => endpoint.call(params);
|
|
1158
1288
|
}
|
|
1159
1289
|
/**
|
|
1160
|
-
* Create a strongly-typed WebSocket endpoint
|
|
1290
|
+
* Create a strongly-typed WebSocket endpoint.
|
|
1161
1291
|
*
|
|
1162
|
-
* @
|
|
1163
|
-
*
|
|
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
|
+
* ```
|
|
1164
1301
|
*/
|
|
1165
1302
|
createWebSocket(config) {
|
|
1166
1303
|
return new WSEndpointImpl(this, config).createCall();
|
|
1167
1304
|
}
|
|
1168
1305
|
/**
|
|
1169
|
-
* Create a strongly-typed Server-Sent Events
|
|
1306
|
+
* Create a strongly-typed Server-Sent Events endpoint.
|
|
1170
1307
|
*
|
|
1171
|
-
* @
|
|
1172
|
-
*
|
|
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
|
+
* ```
|
|
1173
1318
|
*/
|
|
1174
1319
|
createSSE(config) {
|
|
1175
1320
|
return new SSEEndpointImpl(this, config).createCall();
|
|
@@ -1196,9 +1341,11 @@ exports.SSEEndpointImpl = SSEEndpointImpl;
|
|
|
1196
1341
|
exports.SchemaDefinitionError = SchemaDefinitionError;
|
|
1197
1342
|
exports.WSConnectionImpl = WSConnectionImpl;
|
|
1198
1343
|
exports.WSEndpointImpl = WSEndpointImpl;
|
|
1344
|
+
exports.formatValidationIssues = formatValidationIssues;
|
|
1199
1345
|
exports.isStandardSchema = isStandardSchema;
|
|
1200
1346
|
exports.parseOrThrow = parseOrThrow;
|
|
1201
1347
|
exports.safeParse = safeParse;
|
|
1202
1348
|
exports.toQueryString = toQueryString;
|
|
1349
|
+
exports.toRequestQuery = toRequestQuery;
|
|
1203
1350
|
|
|
1204
1351
|
//# sourceMappingURL=index.cjs.map
|