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