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