@route-forge/core 2.1.0 → 2.2.1

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.
@@ -64,12 +64,28 @@ var RouteForge = (function (exports) {
64
64
  }
65
65
  return entry;
66
66
  }
67
- set(resp) {
67
+ /**
68
+ * 写入某层级缓存。后端 TTL 唯一来源为摘要 config.cache_ttl(全局),经参数 backendTtl 传入:
69
+ * - null → 后端声明"不缓存":不写 storage,仅留内存镜像供 route() 同步读(load 层"每次重取"由调用方跳过 get 短路实现)
70
+ * - 0 → 永久缓存(内存 + storage)
71
+ * - undefined → 后端未下发该字段:用前端兜底 TTL(options.cache.ttl)
72
+ * - 正整数 → min(后端, 前端兜底):后端为上限,前端只能缩短不能延长(SPEC §5.3)
73
+ */
74
+ set(resp, backendTtl) {
68
75
  let ttl;
69
- if (resp.cache !== void 0 && resp.cache !== null) {
70
- ttl = resp.cache > 0 ? Math.min(resp.cache, this.fallbackTtl) : resp.cache;
71
- } else {
76
+ let persist;
77
+ if (backendTtl === null) {
78
+ ttl = 0;
79
+ persist = false;
80
+ } else if (backendTtl === void 0) {
72
81
  ttl = this.fallbackTtl;
82
+ persist = true;
83
+ } else if (backendTtl === 0) {
84
+ ttl = 0;
85
+ persist = true;
86
+ } else {
87
+ ttl = Math.min(backendTtl, this.fallbackTtl);
88
+ persist = true;
73
89
  }
74
90
  const entry = {
75
91
  level: resp.level,
@@ -78,7 +94,7 @@ var RouteForge = (function (exports) {
78
94
  cachedAt: Date.now()
79
95
  };
80
96
  this.memory.set(resp.level, entry);
81
- if (this.storage === "memory" || !this.backend) {
97
+ if (!persist || this.storage === "memory" || !this.backend) {
82
98
  return;
83
99
  }
84
100
  try {
@@ -658,9 +674,11 @@ var RouteForge = (function (exports) {
658
674
  }
659
675
 
660
676
  // src/auto-discovery.ts
661
- var UNASSIGNED_LEVEL = "unassigned";
662
677
  async function fetchSummary(inputs, baseURL, fetchMeta) {
663
678
  const { explicitLevels, explicitEndpoint } = inputs;
679
+ if (!explicitEndpoint) {
680
+ throw new UnknownLevelError("(auto-discovery)");
681
+ }
664
682
  try {
665
683
  const base = baseURL.endsWith("/") ? baseURL.slice(0, -1) : baseURL;
666
684
  const ep = explicitEndpoint.startsWith("/") ? explicitEndpoint : `/${explicitEndpoint}`;
@@ -676,35 +694,42 @@ var RouteForge = (function (exports) {
676
694
  throw new UnknownLevelError("(auto-discovery)");
677
695
  }
678
696
  }
697
+ function normalizeCacheTtl(raw) {
698
+ if (typeof raw === "number" && raw < 0) return null;
699
+ return raw;
700
+ }
679
701
  function applySummaryToState(summary, state, inputs) {
680
702
  const { explicitLevels, explicitEager, explicitEndpoint } = inputs;
681
- const schemaVersion = summary.schemaVersion ?? 1;
682
- if (schemaVersion > 1) {
703
+ const schemeVersion = summary.schemeVersion ?? 1;
704
+ if (schemeVersion > 1) {
683
705
  console.warn(
684
- `[route-forge] backend schemaVersion=${schemaVersion} > client supported 1; some features may be unavailable`
706
+ `[route-forge] backend schemeVersion=${schemeVersion} > client supported 1; some features may be unavailable`
685
707
  );
686
708
  }
687
709
  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
- );
710
+ if (explicitEndpoint) {
711
+ console.warn(
712
+ `[route-forge] backend endpoint_prefix "${summary.config.endpoint_prefix}" overrides frontend endpoint "${explicitEndpoint}"`
713
+ );
714
+ }
691
715
  state.endpoint = summary.config.endpoint_prefix;
692
716
  }
693
717
  if (summary.config.url_prefix) {
694
718
  state.urlPrefix = summary.config.url_prefix.endsWith("/") ? summary.config.url_prefix.slice(0, -1) : summary.config.url_prefix;
695
719
  }
720
+ state.cacheTtl = normalizeCacheTtl(summary.config.cache_ttl);
696
721
  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);
722
+ const levelRoutes = {};
723
+ for (const lvl of backendLevels) {
724
+ const r = summary.levels[lvl]?.route;
725
+ if (r && typeof r.uri === "string") {
726
+ levelRoutes[lvl] = { uri: r.uri, methods: r.methods ?? ["GET", "HEAD"] };
727
+ }
704
728
  }
729
+ state.levelRoutes = levelRoutes;
705
730
  if (explicitLevels && explicitLevels.length > 0) {
706
- const intersection = explicitLevels.filter((l) => availableLevels.includes(l));
707
- const removed = explicitLevels.filter((l) => !availableLevels.includes(l));
731
+ const intersection = explicitLevels.filter((l) => backendLevels.includes(l));
732
+ const removed = explicitLevels.filter((l) => !backendLevels.includes(l));
708
733
  if (removed.length > 0) {
709
734
  console.warn(
710
735
  `[route-forge] levels not in backend summary and dropped: ${removed.join(", ")}`
@@ -712,7 +737,7 @@ var RouteForge = (function (exports) {
712
737
  }
713
738
  state.levels = intersection;
714
739
  } else {
715
- state.levels = availableLevels;
740
+ state.levels = backendLevels.slice();
716
741
  }
717
742
  const backendEager = backendLevels.filter((lvl) => summary.levels[lvl]?.load === "eager");
718
743
  if (!explicitEager) {
@@ -723,6 +748,25 @@ var RouteForge = (function (exports) {
723
748
  }
724
749
  }
725
750
 
751
+ // src/embedded-summary.ts
752
+ var EMBEDDED_GLOBAL_KEY = "__ROUTE_FORGE__";
753
+ var memo;
754
+ function readEmbeddedSummary() {
755
+ if (memo !== void 0) return memo;
756
+ if (typeof window === "undefined") {
757
+ memo = null;
758
+ return null;
759
+ }
760
+ let raw;
761
+ try {
762
+ raw = window[EMBEDDED_GLOBAL_KEY];
763
+ } catch {
764
+ raw = void 0;
765
+ }
766
+ memo = raw !== null && typeof raw === "object" ? raw : null;
767
+ return memo;
768
+ }
769
+
726
770
  // src/route-store.ts
727
771
  var RouteStore = class {
728
772
  constructor(deps) {
@@ -742,33 +786,30 @@ var RouteForge = (function (exports) {
742
786
  }
743
787
  }
744
788
  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
- );
789
+ const uri = this.state.levelRoutes[level]?.uri;
790
+ const url = uri ? this.joinBaseAndPath(this.baseURL, uri) : buildUrl(level, { baseURL: this.baseURL, endpoint: this.state.endpoint });
791
+ return await this.fetchMeta(`route-forge.${level}`, url, level);
792
+ }
793
+ /** baseURL 与后端下发的绝对 path 拼接(规范化斜杠),与 buildUrl 的 base 处理一致 */
794
+ joinBaseAndPath(baseURL, path) {
795
+ const base = baseURL.endsWith("/") ? baseURL.slice(0, -1) : baseURL;
796
+ const p = path.startsWith("/") ? path : `/${path}`;
797
+ return `${base}${p}`;
750
798
  }
751
799
  async loadOne(level) {
752
800
  const autoDiscoveryError = this.getAutoDiscoveryError();
753
801
  if (autoDiscoveryError) throw autoDiscoveryError;
754
802
  this.assertLevelDeclared(level);
755
- if (this.cache.get(level)) return;
803
+ const noCache = this.state.cacheTtl === null;
804
+ if (!noCache && this.cache.get(level)) return;
756
805
  const existing = this.inflight.get(level);
757
806
  if (existing) return existing;
758
807
  const gen = this.invalidationGens.get(level) ?? 0;
759
808
  const p = (async () => {
760
809
  try {
761
- if (level === UNASSIGNED_LEVEL && !this.state.backendHasUnassignedLevel && this.state.summaryUnassigned) {
762
- const routes = {};
763
- for (const r of this.state.summaryUnassigned) {
764
- routes[r.name] = { ...r, level: UNASSIGNED_LEVEL };
765
- }
766
- this.cache.set({ level: UNASSIGNED_LEVEL, routes, cache: null });
767
- return;
768
- }
769
810
  const resp = await this.fetchLevel(level);
770
811
  if ((this.invalidationGens.get(level) ?? 0) === gen) {
771
- this.cache.set(resp);
812
+ this.cache.set(resp, this.state.cacheTtl);
772
813
  }
773
814
  } finally {
774
815
  this.inflight.delete(level);
@@ -1051,8 +1092,13 @@ var RouteForge = (function (exports) {
1051
1092
  // src/forge.ts
1052
1093
  var DEFAULT_TIMEOUT = 3e4;
1053
1094
  var DEFAULT_CACHE_TTL = 3600;
1054
- function createRouteForge(options) {
1055
- if (!options.endpoint) throw new TypeError("options.endpoint is required");
1095
+ function createRouteForge(options = {}) {
1096
+ const bootstrapSummary = readEmbeddedSummary() ?? options.summary ?? null;
1097
+ if (!bootstrapSummary && !options.endpoint) {
1098
+ throw new TypeError(
1099
+ "createRouteForge: \u9700\u8981 options.endpoint\uFF0C\u6216 options.summary\uFF0C\u6216\u9875\u9762\u5185\u5D4C window.__ROUTE_FORGE__"
1100
+ );
1101
+ }
1056
1102
  const {
1057
1103
  adapter = "auto",
1058
1104
  timeout = DEFAULT_TIMEOUT,
@@ -1067,10 +1113,10 @@ var RouteForge = (function (exports) {
1067
1113
  const discoveryState = {
1068
1114
  levels: explicitLevels ?? [],
1069
1115
  eager: explicitEager ?? [],
1070
- endpoint: explicitEndpoint,
1116
+ endpoint: explicitEndpoint ?? bootstrapSummary?.config?.endpoint_prefix ?? "",
1071
1117
  urlPrefix: "",
1072
- summaryUnassigned: void 0,
1073
- backendHasUnassignedLevel: false
1118
+ cacheTtl: void 0,
1119
+ levelRoutes: {}
1074
1120
  };
1075
1121
  const discoveryInputs = { explicitLevels, explicitEager, explicitEndpoint };
1076
1122
  let autoDiscoveryCompleted = false;
@@ -1162,12 +1208,19 @@ var RouteForge = (function (exports) {
1162
1208
  }
1163
1209
  return resp.data;
1164
1210
  }
1165
- const autoDiscoveryPromise = fetchSummary(discoveryInputs, baseURL, fetchMeta).then((summary) => {
1166
- if (summary === null) {
1167
- return;
1168
- }
1169
- applySummaryToState(summary, discoveryState, discoveryInputs);
1170
- });
1211
+ let autoDiscoveryPromise;
1212
+ if (bootstrapSummary) {
1213
+ applySummaryToState(bootstrapSummary, discoveryState, discoveryInputs);
1214
+ autoDiscoveryCompleted = true;
1215
+ autoDiscoveryPromise = Promise.resolve();
1216
+ } else {
1217
+ autoDiscoveryPromise = fetchSummary(discoveryInputs, baseURL, fetchMeta).then((summary) => {
1218
+ if (summary === null) {
1219
+ return;
1220
+ }
1221
+ applySummaryToState(summary, discoveryState, discoveryInputs);
1222
+ });
1223
+ }
1171
1224
  let autoDiscoveryError = null;
1172
1225
  autoDiscoveryPromise.catch((e) => {
1173
1226
  autoDiscoveryError = e;