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