@route-forge/core 2.0.0 → 2.1.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
@@ -488,39 +488,6 @@ async function resolveAdapter(opts) {
488
488
  return createBuiltinHttp(opts.forgeInterceptors);
489
489
  }
490
490
 
491
- // src/resolveRouteName.ts
492
- async function resolveRouteName(forge, level, prefix, suffix, separator = ".") {
493
- if (!suffix) return prefix;
494
- const joined = `${prefix}${separator}${suffix}`;
495
- if (!suffix.startsWith(`${prefix}${separator}`)) return joined;
496
- await forge.load(level);
497
- if (forge.hasRoute(level, joined)) return joined;
498
- if (forge.hasRoute(level, suffix)) return suffix;
499
- throw new UnknownRouteError(joined, level);
500
- }
501
- function resolveRouteNameSync(forge, level, prefix, suffix, separator = ".") {
502
- if (!suffix) return prefix;
503
- const joined = `${prefix}${separator}${suffix}`;
504
- if (!suffix.startsWith(`${prefix}${separator}`)) return joined;
505
- if (forge.hasRoute(level, joined)) return joined;
506
- if (forge.hasRoute(level, suffix)) return suffix;
507
- throw new UnknownRouteError(joined, level);
508
- }
509
-
510
- // src/defineImmutableProps.ts
511
- function defineImmutableProps(target, props) {
512
- for (const key of Object.keys(props)) {
513
- const val = props[key];
514
- Object.defineProperty(target, key, {
515
- value: val !== null && typeof val === "object" ? Object.freeze(val) : val,
516
- writable: false,
517
- enumerable: false,
518
- configurable: false
519
- });
520
- }
521
- return target;
522
- }
523
-
524
491
  // src/loading.ts
525
492
  var LoadingTracker = class {
526
493
  constructor() {
@@ -580,328 +547,308 @@ var LoadingTracker = class {
580
547
  }
581
548
  };
582
549
 
583
- // src/forge.ts
584
- var DEFAULT_TIMEOUT = 3e4;
585
- var DEFAULT_CACHE_TTL = 3600;
586
- var UNASSIGNED_LEVEL = "unassigned";
587
- function createRouteForge(options) {
588
- if (!options.endpoint) throw new TypeError("options.endpoint is required");
589
- const {
590
- adapter = "auto",
591
- timeout = DEFAULT_TIMEOUT,
592
- baseURL = "",
593
- interceptors: declarativeInterceptors,
594
- cache: cacheOpts = {}
595
- } = options;
596
- const loadingTracker = new LoadingTracker();
597
- const explicitLevels = options.levels;
598
- const explicitEager = options.eager;
599
- const explicitEndpoint = options.endpoint;
600
- let effectiveLevels = explicitLevels ?? [];
601
- let effectiveEager = explicitEager ?? [];
602
- let effectiveEndpoint = explicitEndpoint;
603
- let effectiveUrlPrefix = "";
604
- let summaryUnassigned;
605
- let backendHasUnassignedLevel = false;
606
- const fetchSummary = async () => {
607
- try {
608
- const base = baseURL.endsWith("/") ? baseURL.slice(0, -1) : baseURL;
609
- const ep = explicitEndpoint.startsWith("/") ? explicitEndpoint : `/${explicitEndpoint}`;
610
- const data = await fetchMeta("__forge__.summary", `${base}${ep}`);
611
- return data;
612
- } catch (e) {
613
- if (explicitLevels && explicitLevels.length > 0) {
614
- console.warn(
615
- `[route-forge] summary endpoint unreachable: ${e.message}; using explicit levels`
616
- );
617
- return null;
618
- }
619
- throw new UnknownLevelError("(auto-discovery)");
620
- }
621
- };
622
- let summaryPromise;
623
- const summaryWaiters = [];
624
- const whenSummary = () => summaryPromise ?? new Promise((resolve) => summaryWaiters.push(resolve));
625
- const autoDiscoveryPromise = whenSummary().then((summary) => {
626
- if (summary === null) {
627
- return;
628
- }
629
- const schemaVersion = summary.schemaVersion ?? 1;
630
- if (schemaVersion > 1) {
631
- console.warn(
632
- `[route-forge] backend schemaVersion=${schemaVersion} > client supported 1; some features may be unavailable`
633
- );
634
- }
635
- if (summary.config.endpoint_prefix && summary.config.endpoint_prefix !== explicitEndpoint) {
636
- console.warn(
637
- `[route-forge] backend endpoint_prefix "${summary.config.endpoint_prefix}" overrides frontend endpoint "${explicitEndpoint}"`
638
- );
639
- effectiveEndpoint = summary.config.endpoint_prefix;
640
- }
641
- if (summary.config.url_prefix) {
642
- effectiveUrlPrefix = summary.config.url_prefix.endsWith("/") ? summary.config.url_prefix.slice(0, -1) : summary.config.url_prefix;
643
- }
644
- const backendLevels = Object.keys(summary.levels);
645
- backendHasUnassignedLevel = backendLevels.includes(UNASSIGNED_LEVEL);
646
- if (Array.isArray(summary.unassigned) && summary.unassigned.length > 0 && !backendHasUnassignedLevel) {
647
- summaryUnassigned = summary.unassigned;
648
- }
649
- const availableLevels = backendLevels.slice();
650
- if (summaryUnassigned && !backendHasUnassignedLevel) {
651
- availableLevels.push(UNASSIGNED_LEVEL);
550
+ // src/url-builder.ts
551
+ function buildUrl(level, ctx) {
552
+ const base = ctx.baseURL.endsWith("/") ? ctx.baseURL.slice(0, -1) : ctx.baseURL;
553
+ const ep = ctx.endpoint.startsWith("/") ? ctx.endpoint : `/${ctx.endpoint}`;
554
+ return `${base}${ep}/${encodeURIComponent(level)}`;
555
+ }
556
+ function buildRequestUrl(meta, params, ctx) {
557
+ const defaults = meta.parameter_defaults ?? {};
558
+ const missingRequired = [];
559
+ const values = {};
560
+ for (const p of meta.parameters) {
561
+ let v = params[p];
562
+ if ((v === void 0 || v === null) && p in defaults) {
563
+ v = defaults[p];
652
564
  }
653
- if (explicitLevels && explicitLevels.length > 0) {
654
- const intersection = explicitLevels.filter((l) => availableLevels.includes(l));
655
- const removed = explicitLevels.filter((l) => !availableLevels.includes(l));
656
- if (removed.length > 0) {
657
- console.warn(
658
- `[route-forge] levels not in backend summary and dropped: ${removed.join(", ")}`
659
- );
565
+ if (v === void 0 || v === null) {
566
+ if (!meta.uri.includes(`{${p}?}`)) {
567
+ missingRequired.push(p);
660
568
  }
661
- effectiveLevels = intersection;
662
569
  } else {
663
- effectiveLevels = availableLevels;
570
+ values[p] = v;
664
571
  }
665
- const backendEager = backendLevels.filter((lvl) => summary.levels[lvl]?.load === "eager");
666
- if (!explicitEager) {
667
- effectiveEager = backendEager;
668
- } else {
669
- const union = /* @__PURE__ */ new Set([...backendEager, ...explicitEager]);
670
- effectiveEager = [...union];
572
+ }
573
+ if (missingRequired.length > 0) {
574
+ throw new MissingRouteParamError(meta.name, missingRequired);
575
+ }
576
+ let uri = meta.uri.replace(/\{([^{}]+)\}/g, (match, raw) => {
577
+ const optional = raw.endsWith("?");
578
+ const name = optional ? raw.slice(0, -1) : raw;
579
+ if (values[name] !== void 0) {
580
+ const val = values[name];
581
+ if (typeof val === "object") {
582
+ throw new ForgeError(
583
+ `Path parameter "${name}" must be a primitive value (string, number, boolean), got ${typeof val}`,
584
+ { code: "RF_FE_003", route: meta.name, context: { param: name, value: val } }
585
+ );
586
+ }
587
+ return encodeURIComponent(String(val));
671
588
  }
589
+ return optional ? "" : match;
672
590
  });
673
- let autoDiscoveryError = null;
674
- autoDiscoveryPromise.catch((e) => {
675
- autoDiscoveryError = e;
676
- });
677
- let autoDiscoveryCompleted = false;
678
- let resolveReady;
679
- let rejectReady;
680
- const readyPromise = new Promise((resolve, reject) => {
681
- resolveReady = resolve;
682
- rejectReady = reject;
683
- });
684
- readyPromise.catch(() => {
685
- });
686
- const cacheTtl = cacheOpts.ttl ?? DEFAULT_CACHE_TTL;
687
- const cacheStorage = cacheOpts.storage ?? "memory";
688
- const cache = new RouteCache({ storage: cacheStorage, ttl: cacheTtl });
689
- const requestInterceptors = new InterceptorManagerImpl();
690
- const responseInterceptors = new InterceptorManagerImpl();
691
- if (declarativeInterceptors?.request) {
692
- for (const entry of declarativeInterceptors.request) {
693
- if (typeof entry === "function") {
694
- requestInterceptors.use(entry);
695
- } else {
696
- const [onFulfilled, onRejected] = entry;
697
- requestInterceptors.use(onFulfilled, onRejected);
698
- }
591
+ uri = uri.replace(/\/+/g, "/").replace(/\/$/, "");
592
+ if (/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(ctx.urlPrefix)) {
593
+ const prefix2 = ctx.urlPrefix.endsWith("/") ? ctx.urlPrefix.slice(0, -1) : ctx.urlPrefix;
594
+ return uri.startsWith("/") ? `${prefix2}${uri}` : `${prefix2}/${uri}`;
595
+ }
596
+ const base = ctx.baseURL.endsWith("/") ? ctx.baseURL.slice(0, -1) : ctx.baseURL;
597
+ const prefix = ctx.urlPrefix;
598
+ return uri.startsWith("/") ? `${base}${prefix}${uri}` : `${base}${prefix}/${uri}`;
599
+ }
600
+ function pickMethod(meta) {
601
+ const m = meta.methods.find((x) => x.toUpperCase() !== "HEAD");
602
+ return (m ?? meta.methods[0] ?? "GET").toUpperCase();
603
+ }
604
+ function appendQuery(url, query) {
605
+ if (!query) return url;
606
+ const usp = new URLSearchParams();
607
+ for (const [k, v] of Object.entries(query)) {
608
+ if (v === void 0 || v === null) continue;
609
+ usp.append(k, String(v));
610
+ }
611
+ const qs = usp.toString();
612
+ if (!qs) return url;
613
+ return url.includes("?") ? `${url}&${qs}` : `${url}?${qs}`;
614
+ }
615
+ function resolveApiParams(input) {
616
+ const {
617
+ params: explicitParams,
618
+ query: rawQuery,
619
+ body: rawBody,
620
+ headers: rawHeaders,
621
+ timeout: perCallTimeout,
622
+ ...flatRest
623
+ } = input;
624
+ const pathParams = explicitParams ? { ...explicitParams } : {};
625
+ for (const [k, v] of Object.entries(flatRest)) {
626
+ if (!(k in pathParams)) {
627
+ pathParams[k] = v;
699
628
  }
700
629
  }
701
- if (declarativeInterceptors?.response) {
702
- for (const entry of declarativeInterceptors.response) {
703
- if (typeof entry === "function") {
704
- responseInterceptors.use(entry);
705
- } else {
706
- const [onFulfilled, onRejected] = entry;
707
- responseInterceptors.use(onFulfilled, onRejected);
708
- }
630
+ let query;
631
+ let body;
632
+ let headers;
633
+ if (rawQuery !== void 0) {
634
+ if (typeof rawQuery === "object" && rawQuery !== null) {
635
+ query = rawQuery;
636
+ } else if (!("query" in pathParams)) {
637
+ pathParams.query = rawQuery;
709
638
  }
710
639
  }
711
- const adapterPromise = resolveAdapter({
712
- adapter,
713
- forgeInterceptors: { request: requestInterceptors, response: responseInterceptors }
714
- });
715
- Promise.resolve().then(() => {
716
- summaryPromise = fetchSummary();
717
- for (const resolve of summaryWaiters) resolve(summaryPromise);
718
- });
719
- let adapterResolved = false;
720
- let adapterObj = null;
721
- async function ensureAdapter() {
722
- if (!adapterResolved) {
723
- adapterObj = await adapterPromise.catch((e) => {
724
- if (e instanceof AdapterNotFoundError) throw e;
725
- return resolveAdapter({
726
- adapter: "builtin",
727
- forgeInterceptors: { request: requestInterceptors, response: responseInterceptors }
728
- });
729
- });
730
- adapterResolved = true;
640
+ if (rawBody !== void 0) {
641
+ if (typeof rawBody !== "string" && typeof rawBody !== "number") {
642
+ body = rawBody;
643
+ } else if (!("body" in pathParams)) {
644
+ pathParams.body = rawBody;
731
645
  }
732
- return adapterObj;
733
646
  }
734
- const inflight = /* @__PURE__ */ new Map();
735
- const invalidationGens = /* @__PURE__ */ new Map();
736
- function assertLevelDeclared(level) {
737
- if (!effectiveLevels.includes(level)) {
738
- throw new UnknownLevelError(level);
647
+ if (rawHeaders !== void 0) {
648
+ if (typeof rawHeaders === "object" && rawHeaders !== null) {
649
+ headers = rawHeaders;
650
+ } else if (!("headers" in pathParams)) {
651
+ pathParams.headers = rawHeaders;
739
652
  }
740
653
  }
741
- function assertDiscoveryReady() {
742
- if (!autoDiscoveryCompleted && !explicitLevels?.length) {
743
- throw new ForgeError(
744
- "Route data not available. Auto-discovery has not completed. Use forge.ready() or forge.use(level) first.",
745
- { code: "RF_FE_010" }
654
+ return { pathParams, query, body, headers, timeout: perCallTimeout };
655
+ }
656
+
657
+ // src/auto-discovery.ts
658
+ var UNASSIGNED_LEVEL = "unassigned";
659
+ async function fetchSummary(inputs, baseURL, fetchMeta) {
660
+ const { explicitLevels, explicitEndpoint } = inputs;
661
+ try {
662
+ const base = baseURL.endsWith("/") ? baseURL.slice(0, -1) : baseURL;
663
+ const ep = explicitEndpoint.startsWith("/") ? explicitEndpoint : `/${explicitEndpoint}`;
664
+ const data = await fetchMeta("__forge__.summary", `${base}${ep}`);
665
+ return data;
666
+ } catch (e) {
667
+ if (explicitLevels && explicitLevels.length > 0) {
668
+ console.warn(
669
+ `[route-forge] summary endpoint unreachable: ${e.message}; using explicit levels`
746
670
  );
671
+ return null;
747
672
  }
673
+ throw new UnknownLevelError("(auto-discovery)");
748
674
  }
749
- function buildUrl(level) {
750
- const base = baseURL.endsWith("/") ? baseURL.slice(0, -1) : baseURL;
751
- const ep = effectiveEndpoint.startsWith("/") ? effectiveEndpoint : `/${effectiveEndpoint}`;
752
- return `${base}${ep}/${encodeURIComponent(level)}`;
675
+ }
676
+ function applySummaryToState(summary, state, inputs) {
677
+ const { explicitLevels, explicitEager, explicitEndpoint } = inputs;
678
+ const schemaVersion = summary.schemaVersion ?? 1;
679
+ if (schemaVersion > 1) {
680
+ console.warn(
681
+ `[route-forge] backend schemaVersion=${schemaVersion} > client supported 1; some features may be unavailable`
682
+ );
753
683
  }
754
- async function fetchMeta(routeTag, url, level = "") {
755
- const adp = await ensureAdapter();
756
- const config = {
757
- route: routeTag,
758
- level,
759
- method: "GET",
760
- url,
761
- headers: { Accept: "application/json" },
762
- params: {},
763
- timeout,
764
- meta: {
765
- name: routeTag,
766
- uri: url,
767
- methods: ["GET"],
768
- parameters: [],
769
- level
770
- }
771
- };
772
- const doRawRequest = adp.requestRaw ?? adp.request;
773
- const resp = await doRawRequest(config);
774
- if (!resp || resp.status < 200 || resp.status >= 300) {
775
- throw new HTTPError(
776
- `Failed to fetch "${routeTag}": HTTP ${resp?.status}`,
777
- { level, status: resp?.status, url, method: "GET" }
684
+ if (summary.config.endpoint_prefix && summary.config.endpoint_prefix !== explicitEndpoint) {
685
+ console.warn(
686
+ `[route-forge] backend endpoint_prefix "${summary.config.endpoint_prefix}" overrides frontend endpoint "${explicitEndpoint}"`
687
+ );
688
+ state.endpoint = summary.config.endpoint_prefix;
689
+ }
690
+ if (summary.config.url_prefix) {
691
+ state.urlPrefix = summary.config.url_prefix.endsWith("/") ? summary.config.url_prefix.slice(0, -1) : summary.config.url_prefix;
692
+ }
693
+ const backendLevels = Object.keys(summary.levels);
694
+ state.backendHasUnassignedLevel = backendLevels.includes(UNASSIGNED_LEVEL);
695
+ if (Array.isArray(summary.unassigned) && summary.unassigned.length > 0 && !state.backendHasUnassignedLevel) {
696
+ state.summaryUnassigned = summary.unassigned;
697
+ }
698
+ const availableLevels = backendLevels.slice();
699
+ if (state.summaryUnassigned && !state.backendHasUnassignedLevel) {
700
+ availableLevels.push(UNASSIGNED_LEVEL);
701
+ }
702
+ if (explicitLevels && explicitLevels.length > 0) {
703
+ const intersection = explicitLevels.filter((l) => availableLevels.includes(l));
704
+ const removed = explicitLevels.filter((l) => !availableLevels.includes(l));
705
+ if (removed.length > 0) {
706
+ console.warn(
707
+ `[route-forge] levels not in backend summary and dropped: ${removed.join(", ")}`
778
708
  );
779
709
  }
780
- return resp.data;
710
+ state.levels = intersection;
711
+ } else {
712
+ state.levels = availableLevels;
713
+ }
714
+ const backendEager = backendLevels.filter((lvl) => summary.levels[lvl]?.load === "eager");
715
+ if (!explicitEager) {
716
+ state.eager = backendEager;
717
+ } else {
718
+ const union = /* @__PURE__ */ new Set([...backendEager, ...explicitEager]);
719
+ state.eager = [...union];
720
+ }
721
+ }
722
+
723
+ // src/route-store.ts
724
+ var RouteStore = class {
725
+ constructor(deps) {
726
+ this.inflight = /* @__PURE__ */ new Map();
727
+ /** 每个层级的失效代数,用于检测 loadOne 期间是否发生了 invalidate */
728
+ this.invalidationGens = /* @__PURE__ */ new Map();
729
+ this.cache = deps.cache;
730
+ this.state = deps.state;
731
+ this.baseURL = deps.baseURL;
732
+ this.fetchMeta = deps.fetchMeta;
733
+ this.autoDiscoveryPromise = deps.autoDiscoveryPromise;
734
+ this.getAutoDiscoveryError = deps.getAutoDiscoveryError;
735
+ }
736
+ assertLevelDeclared(level) {
737
+ if (!this.state.levels.includes(level)) {
738
+ throw new UnknownLevelError(level);
739
+ }
781
740
  }
782
- async function fetchLevel(level) {
783
- return await fetchMeta(`__forge__.load.${level}`, buildUrl(level), level);
741
+ async fetchLevel(level) {
742
+ return await this.fetchMeta(
743
+ `__forge__.load.${level}`,
744
+ buildUrl(level, { baseURL: this.baseURL, endpoint: this.state.endpoint }),
745
+ level
746
+ );
784
747
  }
785
- async function loadOne(level) {
748
+ async loadOne(level) {
749
+ const autoDiscoveryError = this.getAutoDiscoveryError();
786
750
  if (autoDiscoveryError) throw autoDiscoveryError;
787
- assertLevelDeclared(level);
788
- if (cache.get(level)) return;
789
- const existing = inflight.get(level);
751
+ this.assertLevelDeclared(level);
752
+ if (this.cache.get(level)) return;
753
+ const existing = this.inflight.get(level);
790
754
  if (existing) return existing;
791
- const gen = invalidationGens.get(level) ?? 0;
755
+ const gen = this.invalidationGens.get(level) ?? 0;
792
756
  const p = (async () => {
793
757
  try {
794
- if (level === UNASSIGNED_LEVEL && !backendHasUnassignedLevel && summaryUnassigned) {
758
+ if (level === UNASSIGNED_LEVEL && !this.state.backendHasUnassignedLevel && this.state.summaryUnassigned) {
795
759
  const routes = {};
796
- for (const r of summaryUnassigned) {
760
+ for (const r of this.state.summaryUnassigned) {
797
761
  routes[r.name] = { ...r, level: UNASSIGNED_LEVEL };
798
762
  }
799
- cache.set({ level: UNASSIGNED_LEVEL, routes, cache: null });
763
+ this.cache.set({ level: UNASSIGNED_LEVEL, routes, cache: null });
800
764
  return;
801
765
  }
802
- const resp = await fetchLevel(level);
803
- if ((invalidationGens.get(level) ?? 0) === gen) {
804
- cache.set(resp);
766
+ const resp = await this.fetchLevel(level);
767
+ if ((this.invalidationGens.get(level) ?? 0) === gen) {
768
+ this.cache.set(resp);
805
769
  }
806
770
  } finally {
807
- inflight.delete(level);
771
+ this.inflight.delete(level);
808
772
  }
809
773
  })();
810
- inflight.set(level, p);
774
+ this.inflight.set(level, p);
811
775
  return p;
812
776
  }
813
- async function load(level) {
814
- await autoDiscoveryPromise;
777
+ async load(level) {
778
+ await this.autoDiscoveryPromise;
815
779
  const list = Array.isArray(level) ? level : [level];
816
- await Promise.all(list.map(loadOne));
780
+ await Promise.all(list.map((l) => this.loadOne(l)));
817
781
  }
818
- function route(level, name, params) {
819
- assertDiscoveryReady();
820
- const meta = findRouteMeta(level, name);
821
- if (!meta) {
822
- throw new UnknownRouteError(name, level);
823
- }
824
- return buildRequestUrl(meta, params ?? {});
825
- }
826
- function buildRequestUrl(meta, params) {
827
- const defaults = meta.parameter_defaults ?? {};
828
- const missingRequired = [];
829
- const values = {};
830
- for (const p of meta.parameters) {
831
- let v = params[p];
832
- if ((v === void 0 || v === null) && p in defaults) {
833
- v = defaults[p];
834
- }
835
- if (v === void 0 || v === null) {
836
- if (!meta.uri.includes(`{${p}?}`)) {
837
- missingRequired.push(p);
838
- }
839
- } else {
840
- values[p] = v;
782
+ invalidate(level) {
783
+ if (level === void 0) {
784
+ this.cache.clear();
785
+ this.inflight.clear();
786
+ for (const lvl of this.state.levels) {
787
+ this.invalidationGens.set(lvl, (this.invalidationGens.get(lvl) ?? 0) + 1);
841
788
  }
842
- }
843
- if (missingRequired.length > 0) {
844
- throw new MissingRouteParamError(meta.name, missingRequired);
845
- }
846
- let uri = meta.uri.replace(/\{([^{}]+)\}/g, (match, raw) => {
847
- const optional = raw.endsWith("?");
848
- const name = optional ? raw.slice(0, -1) : raw;
849
- if (values[name] !== void 0) {
850
- const val = values[name];
851
- if (typeof val === "object") {
852
- throw new ForgeError(
853
- `Path parameter "${name}" must be a primitive value (string, number, boolean), got ${typeof val}`,
854
- { code: "RF_FE_003", route: meta.name, context: { param: name, value: val } }
855
- );
856
- }
857
- return encodeURIComponent(String(val));
789
+ } else if (Array.isArray(level)) {
790
+ for (const lvl of level) {
791
+ this.cache.del(lvl);
792
+ this.inflight.delete(lvl);
793
+ this.invalidationGens.set(lvl, (this.invalidationGens.get(lvl) ?? 0) + 1);
858
794
  }
859
- return optional ? "" : match;
860
- });
861
- uri = uri.replace(/\/+/g, "/").replace(/\/$/, "");
862
- if (/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(effectiveUrlPrefix)) {
863
- const prefix2 = effectiveUrlPrefix.endsWith("/") ? effectiveUrlPrefix.slice(0, -1) : effectiveUrlPrefix;
864
- return uri.startsWith("/") ? `${prefix2}${uri}` : `${prefix2}/${uri}`;
795
+ } else {
796
+ this.cache.del(level);
797
+ this.inflight.delete(level);
798
+ this.invalidationGens.set(level, (this.invalidationGens.get(level) ?? 0) + 1);
865
799
  }
866
- const base = baseURL.endsWith("/") ? baseURL.slice(0, -1) : baseURL;
867
- const prefix = effectiveUrlPrefix;
868
- return uri.startsWith("/") ? `${base}${prefix}${uri}` : `${base}${prefix}/${uri}`;
869
800
  }
870
- function findRouteMeta(level, name) {
871
- const entry = cache.get(level);
801
+ isLoaded(level) {
802
+ if (level) return this.cache.get(level) !== void 0;
803
+ return this.state.levels.length > 0 && this.state.levels.every((lvl) => this.cache.get(lvl) !== void 0);
804
+ }
805
+ findRouteMeta(level, name) {
806
+ const entry = this.cache.get(level);
872
807
  const meta = entry?.routes[name];
873
808
  if (meta) {
874
809
  return { ...meta, level };
875
810
  }
876
811
  return void 0;
877
812
  }
878
- function api(level, name, params = {}) {
879
- let ctrl;
880
- let abortedBeforeInit = false;
881
- let abortReason;
882
- const work = (async () => {
883
- ctrl = new AbortController();
884
- if (abortedBeforeInit) {
885
- ctrl.abort(abortReason);
886
- }
887
- await autoDiscoveryPromise;
888
- await load(level);
889
- const meta = findRouteMeta(level, name);
890
- if (!meta) {
891
- throw new UnknownRouteError(name, level);
813
+ getRoutes(level) {
814
+ if (level !== void 0) {
815
+ const entry = this.cache.get(level);
816
+ const routes = entry?.routes ?? {};
817
+ const result2 = {};
818
+ for (const [k, v] of Object.entries(routes)) {
819
+ result2[k] = JSON.parse(JSON.stringify(v));
892
820
  }
893
- return doApiCall(meta, params, ctrl.signal);
894
- })();
895
- const request = work;
896
- request.abort = () => {
897
- if (ctrl) {
898
- ctrl.abort();
899
- } else {
900
- abortedBeforeInit = true;
821
+ return result2;
822
+ }
823
+ const result = {};
824
+ for (const lvl of this.state.levels) {
825
+ const entry = this.cache.get(lvl);
826
+ if (entry) {
827
+ const levelRoutes = {};
828
+ for (const [k, v] of Object.entries(entry.routes)) {
829
+ levelRoutes[k] = JSON.parse(JSON.stringify(v));
830
+ }
831
+ result[lvl] = levelRoutes;
901
832
  }
902
- };
903
- return request;
833
+ }
834
+ return result;
904
835
  }
836
+ };
837
+
838
+ // src/http-runner.ts
839
+ function createHttpRunner(deps) {
840
+ const {
841
+ ensureAdapter,
842
+ requestInterceptors,
843
+ responseInterceptors,
844
+ load,
845
+ findRouteMeta,
846
+ baseURL,
847
+ state,
848
+ timeout,
849
+ loadingTracker,
850
+ autoDiscoveryPromise
851
+ } = deps;
905
852
  async function doApiCall(meta, params, signal) {
906
853
  const {
907
854
  pathParams,
@@ -914,7 +861,10 @@ function createRouteForge(options) {
914
861
  throw new RequestAbortedError(meta.name, meta.level, signal.reason);
915
862
  }
916
863
  const method = pickMethod(meta);
917
- const urlWithQuery = appendQuery(buildRequestUrl(meta, pathParams), query);
864
+ const urlWithQuery = appendQuery(
865
+ buildRequestUrl(meta, pathParams, { baseURL, urlPrefix: state.urlPrefix }),
866
+ query
867
+ );
918
868
  const config = {
919
869
  route: meta.name,
920
870
  level: meta.level ?? "",
@@ -969,88 +919,77 @@ function createRouteForge(options) {
969
919
  loadingTracker.stop();
970
920
  }
971
921
  }
972
- function invalidate(level) {
973
- if (level === void 0) {
974
- cache.clear();
975
- inflight.clear();
976
- for (const lvl of effectiveLevels) {
977
- invalidationGens.set(lvl, (invalidationGens.get(lvl) ?? 0) + 1);
978
- }
979
- } else if (Array.isArray(level)) {
980
- for (const lvl of level) {
981
- cache.del(lvl);
982
- inflight.delete(lvl);
983
- invalidationGens.set(lvl, (invalidationGens.get(lvl) ?? 0) + 1);
922
+ return function api(level, name, params = {}) {
923
+ let ctrl;
924
+ let abortedBeforeInit = false;
925
+ let abortReason;
926
+ const work = (async () => {
927
+ ctrl = new AbortController();
928
+ if (abortedBeforeInit) {
929
+ ctrl.abort(abortReason);
984
930
  }
985
- } else {
986
- cache.del(level);
987
- inflight.delete(level);
988
- invalidationGens.set(level, (invalidationGens.get(level) ?? 0) + 1);
989
- }
990
- }
991
- function isLoaded(level) {
992
- if (level) return cache.get(level) !== void 0;
993
- return effectiveLevels.length > 0 && effectiveLevels.every((lvl) => cache.get(lvl) !== void 0);
994
- }
995
- function hasRoute(level, name) {
996
- assertDiscoveryReady();
997
- return findRouteMeta(level, name) !== void 0;
998
- }
999
- function getRoutes(level) {
1000
- if (level !== void 0) {
1001
- const entry = cache.get(level);
1002
- const routes = entry?.routes ?? {};
1003
- const result2 = {};
1004
- for (const [k, v] of Object.entries(routes)) {
1005
- result2[k] = JSON.parse(JSON.stringify(v));
931
+ await autoDiscoveryPromise;
932
+ await load(level);
933
+ const meta = findRouteMeta(level, name);
934
+ if (!meta) {
935
+ throw new UnknownRouteError(name, level);
1006
936
  }
1007
- return result2;
1008
- }
1009
- const result = {};
1010
- for (const lvl of effectiveLevels) {
1011
- const entry = cache.get(lvl);
1012
- if (entry) {
1013
- const levelRoutes = {};
1014
- for (const [k, v] of Object.entries(entry.routes)) {
1015
- levelRoutes[k] = JSON.parse(JSON.stringify(v));
1016
- }
1017
- result[lvl] = levelRoutes;
937
+ return doApiCall(meta, params, ctrl.signal);
938
+ })();
939
+ const request = work;
940
+ request.abort = () => {
941
+ if (ctrl) {
942
+ ctrl.abort();
943
+ } else {
944
+ abortedBeforeInit = true;
1018
945
  }
1019
- }
1020
- return result;
1021
- }
1022
- void autoDiscoveryPromise.then(() => {
1023
- autoDiscoveryCompleted = true;
1024
- if (effectiveEager.length > 0) {
1025
- return Promise.allSettled(effectiveEager.map((lvl) => load(lvl))).then((results) => {
1026
- results.forEach((r, i) => {
1027
- if (r.status === "rejected") {
1028
- console.error(
1029
- `[route-forge] eager load failed for level "${effectiveEager[i]}":`,
1030
- r.reason
1031
- );
1032
- }
1033
- });
1034
- });
1035
- }
1036
- }).then(() => {
1037
- resolveReady(forgeInstance);
1038
- }).catch((e) => {
1039
- rejectReady(e);
1040
- });
1041
- function ready(onFulfilled, onRejected) {
1042
- if (onFulfilled) {
1043
- const p = readyPromise.then(onFulfilled, onRejected);
1044
- return p.then(() => forgeInstance);
1045
- }
1046
- return readyPromise;
946
+ };
947
+ return request;
948
+ };
949
+ }
950
+
951
+ // src/resolveRouteName.ts
952
+ async function resolveRouteName(forge, level, prefix, suffix, separator = ".") {
953
+ if (!suffix) return prefix;
954
+ const joined = `${prefix}${separator}${suffix}`;
955
+ if (!suffix.startsWith(`${prefix}${separator}`)) return joined;
956
+ await forge.load(level);
957
+ if (forge.hasRoute(level, joined)) return joined;
958
+ if (forge.hasRoute(level, suffix)) return suffix;
959
+ throw new UnknownRouteError(joined, level);
960
+ }
961
+ function resolveRouteNameSync(forge, level, prefix, suffix, separator = ".") {
962
+ if (!suffix) return prefix;
963
+ const joined = `${prefix}${separator}${suffix}`;
964
+ if (!suffix.startsWith(`${prefix}${separator}`)) return joined;
965
+ if (forge.hasRoute(level, joined)) return joined;
966
+ if (forge.hasRoute(level, suffix)) return suffix;
967
+ throw new UnknownRouteError(joined, level);
968
+ }
969
+
970
+ // src/defineImmutableProps.ts
971
+ function defineImmutableProps(target, props) {
972
+ for (const key of Object.keys(props)) {
973
+ const val = props[key];
974
+ Object.defineProperty(target, key, {
975
+ value: val !== null && typeof val === "object" ? Object.freeze(val) : val,
976
+ writable: false,
977
+ enumerable: false,
978
+ configurable: false
979
+ });
1047
980
  }
1048
- const forgeResolver = { load, hasRoute };
981
+ return target;
982
+ }
983
+
984
+ // src/bound-forge.ts
985
+ function createBoundForgeFactory(deps) {
986
+ const { load, api, route, hasRoute, getRoutes, invalidate, isLoaded, loadingTracker } = deps;
987
+ const resolver = { load, hasRoute };
1049
988
  function createBoundForge(level, prefix) {
1050
989
  const levelLoadedPromise = load(level);
1051
990
  levelLoadedPromise.catch(() => {
1052
991
  });
1053
- const apiFn = prefix ? (name, params) => resolveRouteName(forgeResolver, level, prefix, name).then(
992
+ const apiFn = prefix ? (name, params) => resolveRouteName(resolver, level, prefix, name).then(
1054
993
  (resolved) => api(level, resolved, params)
1055
994
  ) : (name, params) => api(level, name, params);
1056
995
  const callable = apiFn;
@@ -1058,8 +997,8 @@ function createRouteForge(options) {
1058
997
  level,
1059
998
  ...prefix !== void 0 ? { prefix } : {},
1060
999
  api: apiFn,
1061
- route: prefix ? (name, params) => route(level, resolveRouteNameSync(forgeResolver, level, prefix, name), params) : (name, params) => route(level, name, params),
1062
- url: prefix ? (name, params) => route(level, resolveRouteNameSync(forgeResolver, level, prefix, name), params) : (name, params) => route(level, name, params),
1000
+ route: prefix ? (name, params) => route(level, resolveRouteNameSync(resolver, level, prefix, name), params) : (name, params) => route(level, name, params),
1001
+ url: prefix ? (name, params) => route(level, resolveRouteNameSync(resolver, level, prefix, name), params) : (name, params) => route(level, name, params),
1063
1002
  hasRoute: (name) => hasRoute(level, name),
1064
1003
  getRoutes: () => getRoutes(level),
1065
1004
  load: () => load(level),
@@ -1103,6 +1042,208 @@ function createRouteForge(options) {
1103
1042
  attachBoundMethods(bound, levelLoadedPromise, level);
1104
1043
  return bound;
1105
1044
  }
1045
+ return createBoundForgeWithMethods;
1046
+ }
1047
+
1048
+ // src/forge.ts
1049
+ var DEFAULT_TIMEOUT = 3e4;
1050
+ var DEFAULT_CACHE_TTL = 3600;
1051
+ function createRouteForge(options) {
1052
+ if (!options.endpoint) throw new TypeError("options.endpoint is required");
1053
+ const {
1054
+ adapter = "auto",
1055
+ timeout = DEFAULT_TIMEOUT,
1056
+ baseURL = "",
1057
+ interceptors: declarativeInterceptors,
1058
+ cache: cacheOpts = {}
1059
+ } = options;
1060
+ const loadingTracker = new LoadingTracker();
1061
+ const explicitLevels = options.levels;
1062
+ const explicitEager = options.eager;
1063
+ const explicitEndpoint = options.endpoint;
1064
+ const discoveryState = {
1065
+ levels: explicitLevels ?? [],
1066
+ eager: explicitEager ?? [],
1067
+ endpoint: explicitEndpoint,
1068
+ urlPrefix: "",
1069
+ summaryUnassigned: void 0,
1070
+ backendHasUnassignedLevel: false
1071
+ };
1072
+ const discoveryInputs = { explicitLevels, explicitEager, explicitEndpoint };
1073
+ let autoDiscoveryCompleted = false;
1074
+ let resolveReady;
1075
+ let rejectReady;
1076
+ const readyPromise = new Promise((resolve, reject) => {
1077
+ resolveReady = resolve;
1078
+ rejectReady = reject;
1079
+ });
1080
+ readyPromise.catch(() => {
1081
+ });
1082
+ const cacheTtl = cacheOpts.ttl ?? DEFAULT_CACHE_TTL;
1083
+ const cacheStorage = cacheOpts.storage ?? "memory";
1084
+ const cache = new RouteCache({ storage: cacheStorage, ttl: cacheTtl });
1085
+ const requestInterceptors = new InterceptorManagerImpl();
1086
+ const responseInterceptors = new InterceptorManagerImpl();
1087
+ if (declarativeInterceptors?.request) {
1088
+ for (const entry of declarativeInterceptors.request) {
1089
+ if (typeof entry === "function") {
1090
+ requestInterceptors.use(entry);
1091
+ } else {
1092
+ const [onFulfilled, onRejected] = entry;
1093
+ requestInterceptors.use(onFulfilled, onRejected);
1094
+ }
1095
+ }
1096
+ }
1097
+ if (declarativeInterceptors?.response) {
1098
+ for (const entry of declarativeInterceptors.response) {
1099
+ if (typeof entry === "function") {
1100
+ responseInterceptors.use(entry);
1101
+ } else {
1102
+ const [onFulfilled, onRejected] = entry;
1103
+ responseInterceptors.use(onFulfilled, onRejected);
1104
+ }
1105
+ }
1106
+ }
1107
+ const adapterPromise = resolveAdapter({
1108
+ adapter,
1109
+ forgeInterceptors: { request: requestInterceptors, response: responseInterceptors }
1110
+ });
1111
+ let adapterResolved = false;
1112
+ let adapterObj = null;
1113
+ async function ensureAdapter() {
1114
+ if (!adapterResolved) {
1115
+ adapterObj = await adapterPromise.catch((e) => {
1116
+ if (e instanceof AdapterNotFoundError) throw e;
1117
+ return resolveAdapter({
1118
+ adapter: "builtin",
1119
+ forgeInterceptors: { request: requestInterceptors, response: responseInterceptors }
1120
+ });
1121
+ });
1122
+ adapterResolved = true;
1123
+ }
1124
+ return adapterObj;
1125
+ }
1126
+ function assertDiscoveryReady() {
1127
+ if (!autoDiscoveryCompleted && !explicitLevels?.length) {
1128
+ throw new ForgeError(
1129
+ "Route data not available. Auto-discovery has not completed. Use forge.ready() or forge.use(level) first.",
1130
+ { code: "RF_FE_010" }
1131
+ );
1132
+ }
1133
+ }
1134
+ async function fetchMeta(routeTag, url, level = "") {
1135
+ const adp = await ensureAdapter();
1136
+ const config = {
1137
+ route: routeTag,
1138
+ level,
1139
+ method: "GET",
1140
+ url,
1141
+ headers: { Accept: "application/json" },
1142
+ params: {},
1143
+ timeout,
1144
+ meta: {
1145
+ name: routeTag,
1146
+ uri: url,
1147
+ methods: ["GET"],
1148
+ parameters: [],
1149
+ level
1150
+ }
1151
+ };
1152
+ const doRawRequest = adp.requestRaw ?? adp.request;
1153
+ const resp = await doRawRequest(config);
1154
+ if (!resp || resp.status < 200 || resp.status >= 300) {
1155
+ throw new HTTPError(
1156
+ `Failed to fetch "${routeTag}": HTTP ${resp?.status}`,
1157
+ { level, status: resp?.status, url, method: "GET" }
1158
+ );
1159
+ }
1160
+ return resp.data;
1161
+ }
1162
+ const autoDiscoveryPromise = fetchSummary(discoveryInputs, baseURL, fetchMeta).then((summary) => {
1163
+ if (summary === null) {
1164
+ return;
1165
+ }
1166
+ applySummaryToState(summary, discoveryState, discoveryInputs);
1167
+ });
1168
+ let autoDiscoveryError = null;
1169
+ autoDiscoveryPromise.catch((e) => {
1170
+ autoDiscoveryError = e;
1171
+ });
1172
+ const store = new RouteStore({
1173
+ cache,
1174
+ state: discoveryState,
1175
+ baseURL,
1176
+ fetchMeta,
1177
+ autoDiscoveryPromise,
1178
+ getAutoDiscoveryError: () => autoDiscoveryError
1179
+ });
1180
+ const load = (level) => store.load(level);
1181
+ const findRouteMeta = (level, name) => store.findRouteMeta(level, name);
1182
+ const invalidate = (level) => store.invalidate(level);
1183
+ const isLoaded = (level) => store.isLoaded(level);
1184
+ function getRoutes(level) {
1185
+ return level === void 0 ? store.getRoutes() : store.getRoutes(level);
1186
+ }
1187
+ function route(level, name, params) {
1188
+ assertDiscoveryReady();
1189
+ const meta = findRouteMeta(level, name);
1190
+ if (!meta) {
1191
+ throw new UnknownRouteError(name, level);
1192
+ }
1193
+ return buildRequestUrl(meta, params ?? {}, { baseURL, urlPrefix: discoveryState.urlPrefix });
1194
+ }
1195
+ const api = createHttpRunner({
1196
+ ensureAdapter,
1197
+ requestInterceptors,
1198
+ responseInterceptors,
1199
+ load,
1200
+ findRouteMeta,
1201
+ baseURL,
1202
+ state: discoveryState,
1203
+ timeout,
1204
+ loadingTracker,
1205
+ autoDiscoveryPromise
1206
+ });
1207
+ function hasRoute(level, name) {
1208
+ assertDiscoveryReady();
1209
+ return findRouteMeta(level, name) !== void 0;
1210
+ }
1211
+ void autoDiscoveryPromise.then(() => {
1212
+ autoDiscoveryCompleted = true;
1213
+ if (discoveryState.eager.length > 0) {
1214
+ return Promise.allSettled(discoveryState.eager.map((lvl) => load(lvl))).then((results) => {
1215
+ results.forEach((r, i) => {
1216
+ if (r.status === "rejected") {
1217
+ console.error(
1218
+ `[route-forge] eager load failed for level "${discoveryState.eager[i]}":`,
1219
+ r.reason
1220
+ );
1221
+ }
1222
+ });
1223
+ });
1224
+ }
1225
+ }).then(() => {
1226
+ resolveReady(forgeInstance);
1227
+ }).catch((e) => {
1228
+ rejectReady(e);
1229
+ });
1230
+ function ready(onFulfilled, onRejected) {
1231
+ if (onFulfilled) {
1232
+ const p = readyPromise.then(onFulfilled, onRejected);
1233
+ return p.then(() => forgeInstance);
1234
+ }
1235
+ return readyPromise;
1236
+ }
1237
+ const createBoundForgeWithMethods = createBoundForgeFactory({
1238
+ load,
1239
+ api,
1240
+ route,
1241
+ hasRoute,
1242
+ getRoutes,
1243
+ invalidate,
1244
+ isLoaded,
1245
+ loadingTracker
1246
+ });
1106
1247
  const forgeInstance = {
1107
1248
  api,
1108
1249
  load,
@@ -1126,62 +1267,6 @@ function createRouteForge(options) {
1126
1267
  };
1127
1268
  return forgeInstance;
1128
1269
  }
1129
- function pickMethod(meta) {
1130
- const m = meta.methods.find((x) => x.toUpperCase() !== "HEAD");
1131
- return (m ?? meta.methods[0] ?? "GET").toUpperCase();
1132
- }
1133
- function appendQuery(url, query) {
1134
- if (!query) return url;
1135
- const usp = new URLSearchParams();
1136
- for (const [k, v] of Object.entries(query)) {
1137
- if (v === void 0 || v === null) continue;
1138
- usp.append(k, String(v));
1139
- }
1140
- const qs = usp.toString();
1141
- if (!qs) return url;
1142
- return url.includes("?") ? `${url}&${qs}` : `${url}?${qs}`;
1143
- }
1144
- function resolveApiParams(input) {
1145
- const {
1146
- params: explicitParams,
1147
- query: rawQuery,
1148
- body: rawBody,
1149
- headers: rawHeaders,
1150
- timeout: perCallTimeout,
1151
- ...flatRest
1152
- } = input;
1153
- const pathParams = explicitParams ? { ...explicitParams } : {};
1154
- for (const [k, v] of Object.entries(flatRest)) {
1155
- if (!(k in pathParams)) {
1156
- pathParams[k] = v;
1157
- }
1158
- }
1159
- let query;
1160
- let body;
1161
- let headers;
1162
- if (rawQuery !== void 0) {
1163
- if (typeof rawQuery === "object" && rawQuery !== null) {
1164
- query = rawQuery;
1165
- } else if (!("query" in pathParams)) {
1166
- pathParams.query = rawQuery;
1167
- }
1168
- }
1169
- if (rawBody !== void 0) {
1170
- if (typeof rawBody !== "string" && typeof rawBody !== "number") {
1171
- body = rawBody;
1172
- } else if (!("body" in pathParams)) {
1173
- pathParams.body = rawBody;
1174
- }
1175
- }
1176
- if (rawHeaders !== void 0) {
1177
- if (typeof rawHeaders === "object" && rawHeaders !== null) {
1178
- headers = rawHeaders;
1179
- } else if (!("headers" in pathParams)) {
1180
- pathParams.headers = rawHeaders;
1181
- }
1182
- }
1183
- return { pathParams, query, body, headers, timeout: perCallTimeout };
1184
- }
1185
1270
 
1186
1271
  export { AdapterNotFoundError, ForgeError, HTTPError, InterceptorManagerImpl, InvalidInterceptorReturnError, LoadingTracker, MissingRouteParamError, NetworkError, RequestAbortedError, RouteCache, UnknownLevelError, UnknownRouteError, createInterceptorManager, createRouteForge, resolveRouteName, resolveRouteNameSync };
1187
1272
  //# sourceMappingURL=index.js.map