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