zlient 3.3.4 → 4.0.0

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