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/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
- async apply(_req) {}
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({ url, init }) {
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
- init.__urlOverride = u.toString();
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({ init }) {
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
- if (Error.captureStackTrace) Error.captureStackTrace(this, ApiError);
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
- if (Error.captureStackTrace) Error.captureStackTrace(this, SchemaDefinitionError);
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) throw new ApiError(`Validation failed: ${result.issues.map((issue) => issue.message).join(", ")}`, { validationIssues: 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
- let { method = "GET", data, headers = {}, withCredentials, signal, auth, logger } = this.options;
512
- let url = this.url;
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
- await auth.apply({
642
+ const ctx = {
523
643
  url,
524
644
  init
525
- });
526
- if (init.__urlOverride) url = init.__urlOverride;
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 (!init.headers || !("Content-Type" in init.headers)) init.headers["Content-Type"] = "application/json";
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: !!data
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
- const decoder = new TextDecoder();
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
- processField(field, value, callback) {
592
- if (field === "event") callback(value, "");
593
- else if (field === "data") callback("", value);
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 skipRequestValidation = this.config.advanced?.skipRequestValidation ?? false;
647
- if (!skipRequestValidation && this.config.request && data !== void 0) await parseOrThrow(this.config.request, data);
648
- if (!skipRequestValidation && this.config.query && query !== void 0) await parseOrThrow(this.config.query, query);
649
- if (!skipRequestValidation && this.config.pathParams && pathParams !== void 0) await parseOrThrow(this.config.pathParams, pathParams);
650
- if (this.config.request && data === void 0) throw new Error("Missing required request body (data)");
651
- if (this.config.pathParams && pathParams === void 0) throw new Error("Missing required path parameters (pathParams)");
652
- let pathStr;
653
- if (typeof this.config.path === "function") {
654
- if (!pathParams) throw new Error("Path function requires pathParams");
655
- pathStr = this.config.path(pathParams);
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
- const message = data != null && typeof data === "object" ? JSON.stringify(data) : data;
704
- this.ws.send(message);
835
+ this.ws.send(serializeRealtimeData(data));
705
836
  }
706
837
  on(event, handler) {
707
- if (!this.handlers.has(event)) this.handlers.set(event, /* @__PURE__ */ new Set());
708
- this.handlers.get(event).add(handler);
838
+ super.on(event, handler);
709
839
  }
710
840
  off(event, handler) {
711
- const handlers = this.handlers.get(event);
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
- let pathStr;
736
- if (typeof this.config.path === "function") {
737
- if (!pathParams) throw new Error("Path function requires pathParams");
738
- pathStr = this.config.path(pathParams);
739
- } else pathStr = this.config.path;
740
- return new WSConnectionImpl(`${this.client.getBaseUrl(this.config.advanced?.baseUrlKey || "default").replace(/^http/, "ws")}${pathStr}${toQueryString(query)}`, this.config.send, this.config.receive, this.config.advanced?.skipRequestValidation, this.config.advanced?.skipResponseValidation, protocols);
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
- const skipRequestValidation = this.config.advanced?.skipRequestValidation ?? false;
755
- const skipResponseValidation = this.config.advanced?.skipResponseValidation ?? false;
756
- if (this.config.mustHeaderKeys && this.config.mustHeaderKeys.length > 0) {
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 (skipResponseValidation) return responseData;
780
- if (isStandardSchema(schema)) return await parseOrThrow(schema, responseData);
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["auth"] ?? new NoAuth();
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
- * Get all configured base URLs.
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
- getBaseUrls() {
879
- return this.baseUrls;
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
- * Get the resolved base URL for a given key.
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, response } = await client.request('GET', '/users', undefined, {
915
- * query: { page: 1, limit: 10 },
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
- let url = `${this.resolveBaseUrl(options?.baseUrlKey)}${path}${toQueryString(options?.query)}`;
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
- let requestBody;
936
- if (body != null) if (body instanceof FormData) {
937
- requestBody = body;
938
- delete headers["Content-Type"];
939
- } else if (body instanceof Blob || body instanceof ArrayBuffer) requestBody = body;
940
- else if (headers["Content-Type"]?.includes("json")) requestBody = JSON.stringify(body);
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: requestBody,
1202
+ body: serializeRequestBody(body, headers),
946
1203
  signal
947
1204
  };
948
- if (!options?.skipAuth) await this.auth.apply({
949
- url,
1205
+ const authCtx = {
1206
+ url: `${base}${path}${toQueryString(options?.query)}`,
950
1207
  init,
951
1208
  options
952
- });
953
- if (init.__urlOverride) url = init.__urlOverride;
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
- const doFetch = async () => {
958
- while (true) {
959
- let timeoutId;
960
- if (this.timeoutMs && !options?.signal) timeoutId = setTimeout(() => {
961
- const timeoutError = /* @__PURE__ */ new Error("Request timeout");
962
- timeoutError.name = "TimeoutError";
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
- if (refreshAttempted && !options?.skipAuth) {
967
- const freshInit = {
968
- ...init,
969
- headers: typeof init.headers === "object" && !(init.headers instanceof Headers) && !Array.isArray(init.headers) ? { ...init.headers } : init.headers
970
- };
971
- await this.auth.apply({
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 builder.
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 builder.
1290
+ * Create a strongly-typed WebSocket endpoint.
1161
1291
  *
1162
- * @param config - WebSocket endpoint configuration
1163
- * @returns WebSocket endpoint call function
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 (SSE) endpoint builder.
1306
+ * Create a strongly-typed Server-Sent Events endpoint.
1170
1307
  *
1171
- * @param config - SSE endpoint configuration
1172
- * @returns SSE endpoint call function
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