@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.
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { a as RouteForgeOptions, b as RouteForge, I as InterceptorManager, c as InterceptorHandler, C as CacheStorage, R as RouteMeta, L as LevelRoutesResponse } from './types-C51rXaiY.cjs';
2
- export { A as AdapterOption, d as ApiCallParams, B as BoundForge, F as Fetcher, e as ForgeApiParams, f as ForgeApiResponse, g as ForgeRequest, h as ForgeRouteMap, i as ForgeRouteName, j as LoadingChangeCallback, k as LoadingChangeEvent, l as LoadingTracker, m as RequestConfig, n as ResponseData, S as SummaryResponse, U as UseForgeApiBoundCall, o as UseForgeApiBoundReturn, p as UseForgeApiCall, q as UseForgeApiReturn } from './types-C51rXaiY.cjs';
1
+ import { a as RouteForgeOptions, b as RouteForge, I as InterceptorManager, c as InterceptorHandler, C as CacheStorage, R as RouteMeta, L as LevelRoutesResponse } from './types-CDKE8rw-.cjs';
2
+ export { A as AdapterOption, d as ApiCallParams, B as BoundForge, F as Fetcher, e as ForgeApiParams, f as ForgeApiResponse, g as ForgeRequest, h as ForgeRouteMap, i as ForgeRouteName, j as LoadingChangeCallback, k as LoadingChangeEvent, l as LoadingTracker, m as RequestConfig, n as ResponseData, S as SummaryResponse, U as UseForgeApiBoundCall, o as UseForgeApiBoundReturn, p as UseForgeApiCall, q as UseForgeApiReturn } from './types-CDKE8rw-.cjs';
3
3
 
4
4
  /**
5
5
  * Route Forge 主入口:createRouteForge
@@ -13,7 +13,7 @@ export { A as AdapterOption, d as ApiCallParams, B as BoundForge, F as Fetcher,
13
13
  * - Adapter auto 检测 / builtin / axios / 自定义 Fetcher(§4.3)
14
14
  */
15
15
 
16
- declare function createRouteForge(options: RouteForgeOptions): RouteForge;
16
+ declare function createRouteForge(options?: RouteForgeOptions): RouteForge;
17
17
 
18
18
  /**
19
19
  * 拦截器管理器实现
@@ -57,7 +57,8 @@ declare function createInterceptorManager<TIn, TOut = TIn>(): InterceptorManager
57
57
  *
58
58
  * 规则:
59
59
  * - 每层级独立条目,互不污染(cache key = `route-forge:${level}`)
60
- * - TTL 优先用后端响应里的 cache 字段,本地 cache.ttl 仅作兜底
60
+ * - 后端 TTL 唯一来源为摘要 config.cache_ttl(全局,经 set backendTtl 参数传入):
61
+ * null=不缓存(只留内存镜像、不落存储)、0=永久、正整数=min(后端, 前端 cache.ttl 兜底)
61
62
  * - storage: 'memory' | 'sessionStorage' | 'localStorage'
62
63
  * - storage 模式下维护内存镜像:首次读盘解析后驻留内存,后续 get 直接命中
63
64
  * (读盘 + JSON.parse 整层路由表是同步阻塞操作,热路径重复执行代价高);
@@ -85,7 +86,14 @@ declare class RouteCache {
85
86
  private key;
86
87
  get(level: string): CacheEntry | undefined;
87
88
  private getFromMemory;
88
- set(resp: LevelRoutesResponse): void;
89
+ /**
90
+ * 写入某层级缓存。后端 TTL 唯一来源为摘要 config.cache_ttl(全局),经参数 backendTtl 传入:
91
+ * - null → 后端声明"不缓存":不写 storage,仅留内存镜像供 route() 同步读(load 层"每次重取"由调用方跳过 get 短路实现)
92
+ * - 0 → 永久缓存(内存 + storage)
93
+ * - undefined → 后端未下发该字段:用前端兜底 TTL(options.cache.ttl)
94
+ * - 正整数 → min(后端, 前端兜底):后端为上限,前端只能缩短不能延长(SPEC §5.3)
95
+ */
96
+ set(resp: LevelRoutesResponse, backendTtl: number | null | undefined): void;
89
97
  del(level: string): void;
90
98
  clear(): void;
91
99
  private isExpired;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { a as RouteForgeOptions, b as RouteForge, I as InterceptorManager, c as InterceptorHandler, C as CacheStorage, R as RouteMeta, L as LevelRoutesResponse } from './types-C51rXaiY.js';
2
- export { A as AdapterOption, d as ApiCallParams, B as BoundForge, F as Fetcher, e as ForgeApiParams, f as ForgeApiResponse, g as ForgeRequest, h as ForgeRouteMap, i as ForgeRouteName, j as LoadingChangeCallback, k as LoadingChangeEvent, l as LoadingTracker, m as RequestConfig, n as ResponseData, S as SummaryResponse, U as UseForgeApiBoundCall, o as UseForgeApiBoundReturn, p as UseForgeApiCall, q as UseForgeApiReturn } from './types-C51rXaiY.js';
1
+ import { a as RouteForgeOptions, b as RouteForge, I as InterceptorManager, c as InterceptorHandler, C as CacheStorage, R as RouteMeta, L as LevelRoutesResponse } from './types-CDKE8rw-.js';
2
+ export { A as AdapterOption, d as ApiCallParams, B as BoundForge, F as Fetcher, e as ForgeApiParams, f as ForgeApiResponse, g as ForgeRequest, h as ForgeRouteMap, i as ForgeRouteName, j as LoadingChangeCallback, k as LoadingChangeEvent, l as LoadingTracker, m as RequestConfig, n as ResponseData, S as SummaryResponse, U as UseForgeApiBoundCall, o as UseForgeApiBoundReturn, p as UseForgeApiCall, q as UseForgeApiReturn } from './types-CDKE8rw-.js';
3
3
 
4
4
  /**
5
5
  * Route Forge 主入口:createRouteForge
@@ -13,7 +13,7 @@ export { A as AdapterOption, d as ApiCallParams, B as BoundForge, F as Fetcher,
13
13
  * - Adapter auto 检测 / builtin / axios / 自定义 Fetcher(§4.3)
14
14
  */
15
15
 
16
- declare function createRouteForge(options: RouteForgeOptions): RouteForge;
16
+ declare function createRouteForge(options?: RouteForgeOptions): RouteForge;
17
17
 
18
18
  /**
19
19
  * 拦截器管理器实现
@@ -57,7 +57,8 @@ declare function createInterceptorManager<TIn, TOut = TIn>(): InterceptorManager
57
57
  *
58
58
  * 规则:
59
59
  * - 每层级独立条目,互不污染(cache key = `route-forge:${level}`)
60
- * - TTL 优先用后端响应里的 cache 字段,本地 cache.ttl 仅作兜底
60
+ * - 后端 TTL 唯一来源为摘要 config.cache_ttl(全局,经 set backendTtl 参数传入):
61
+ * null=不缓存(只留内存镜像、不落存储)、0=永久、正整数=min(后端, 前端 cache.ttl 兜底)
61
62
  * - storage: 'memory' | 'sessionStorage' | 'localStorage'
62
63
  * - storage 模式下维护内存镜像:首次读盘解析后驻留内存,后续 get 直接命中
63
64
  * (读盘 + JSON.parse 整层路由表是同步阻塞操作,热路径重复执行代价高);
@@ -85,7 +86,14 @@ declare class RouteCache {
85
86
  private key;
86
87
  get(level: string): CacheEntry | undefined;
87
88
  private getFromMemory;
88
- set(resp: LevelRoutesResponse): void;
89
+ /**
90
+ * 写入某层级缓存。后端 TTL 唯一来源为摘要 config.cache_ttl(全局),经参数 backendTtl 传入:
91
+ * - null → 后端声明"不缓存":不写 storage,仅留内存镜像供 route() 同步读(load 层"每次重取"由调用方跳过 get 短路实现)
92
+ * - 0 → 永久缓存(内存 + storage)
93
+ * - undefined → 后端未下发该字段:用前端兜底 TTL(options.cache.ttl)
94
+ * - 正整数 → min(后端, 前端兜底):后端为上限,前端只能缩短不能延长(SPEC §5.3)
95
+ */
96
+ set(resp: LevelRoutesResponse, backendTtl: number | null | undefined): void;
89
97
  del(level: string): void;
90
98
  clear(): void;
91
99
  private isExpired;
package/dist/index.js CHANGED
@@ -61,12 +61,28 @@ var RouteCache = class {
61
61
  }
62
62
  return entry;
63
63
  }
64
- set(resp) {
64
+ /**
65
+ * 写入某层级缓存。后端 TTL 唯一来源为摘要 config.cache_ttl(全局),经参数 backendTtl 传入:
66
+ * - null → 后端声明"不缓存":不写 storage,仅留内存镜像供 route() 同步读(load 层"每次重取"由调用方跳过 get 短路实现)
67
+ * - 0 → 永久缓存(内存 + storage)
68
+ * - undefined → 后端未下发该字段:用前端兜底 TTL(options.cache.ttl)
69
+ * - 正整数 → min(后端, 前端兜底):后端为上限,前端只能缩短不能延长(SPEC §5.3)
70
+ */
71
+ set(resp, backendTtl) {
65
72
  let ttl;
66
- if (resp.cache !== void 0 && resp.cache !== null) {
67
- ttl = resp.cache > 0 ? Math.min(resp.cache, this.fallbackTtl) : resp.cache;
68
- } else {
73
+ let persist;
74
+ if (backendTtl === null) {
75
+ ttl = 0;
76
+ persist = false;
77
+ } else if (backendTtl === void 0) {
69
78
  ttl = this.fallbackTtl;
79
+ persist = true;
80
+ } else if (backendTtl === 0) {
81
+ ttl = 0;
82
+ persist = true;
83
+ } else {
84
+ ttl = Math.min(backendTtl, this.fallbackTtl);
85
+ persist = true;
70
86
  }
71
87
  const entry = {
72
88
  level: resp.level,
@@ -75,7 +91,7 @@ var RouteCache = class {
75
91
  cachedAt: Date.now()
76
92
  };
77
93
  this.memory.set(resp.level, entry);
78
- if (this.storage === "memory" || !this.backend) {
94
+ if (!persist || this.storage === "memory" || !this.backend) {
79
95
  return;
80
96
  }
81
97
  try {
@@ -655,9 +671,11 @@ function resolveApiParams(input) {
655
671
  }
656
672
 
657
673
  // src/auto-discovery.ts
658
- var UNASSIGNED_LEVEL = "unassigned";
659
674
  async function fetchSummary(inputs, baseURL, fetchMeta) {
660
675
  const { explicitLevels, explicitEndpoint } = inputs;
676
+ if (!explicitEndpoint) {
677
+ throw new UnknownLevelError("(auto-discovery)");
678
+ }
661
679
  try {
662
680
  const base = baseURL.endsWith("/") ? baseURL.slice(0, -1) : baseURL;
663
681
  const ep = explicitEndpoint.startsWith("/") ? explicitEndpoint : `/${explicitEndpoint}`;
@@ -673,35 +691,42 @@ async function fetchSummary(inputs, baseURL, fetchMeta) {
673
691
  throw new UnknownLevelError("(auto-discovery)");
674
692
  }
675
693
  }
694
+ function normalizeCacheTtl(raw) {
695
+ if (typeof raw === "number" && raw < 0) return null;
696
+ return raw;
697
+ }
676
698
  function applySummaryToState(summary, state, inputs) {
677
699
  const { explicitLevels, explicitEager, explicitEndpoint } = inputs;
678
- const schemaVersion = summary.schemaVersion ?? 1;
679
- if (schemaVersion > 1) {
700
+ const schemeVersion = summary.schemeVersion ?? 1;
701
+ if (schemeVersion > 1) {
680
702
  console.warn(
681
- `[route-forge] backend schemaVersion=${schemaVersion} > client supported 1; some features may be unavailable`
703
+ `[route-forge] backend schemeVersion=${schemeVersion} > client supported 1; some features may be unavailable`
682
704
  );
683
705
  }
684
706
  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
- );
707
+ if (explicitEndpoint) {
708
+ console.warn(
709
+ `[route-forge] backend endpoint_prefix "${summary.config.endpoint_prefix}" overrides frontend endpoint "${explicitEndpoint}"`
710
+ );
711
+ }
688
712
  state.endpoint = summary.config.endpoint_prefix;
689
713
  }
690
714
  if (summary.config.url_prefix) {
691
715
  state.urlPrefix = summary.config.url_prefix.endsWith("/") ? summary.config.url_prefix.slice(0, -1) : summary.config.url_prefix;
692
716
  }
717
+ state.cacheTtl = normalizeCacheTtl(summary.config.cache_ttl);
693
718
  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);
719
+ const levelRoutes = {};
720
+ for (const lvl of backendLevels) {
721
+ const r = summary.levels[lvl]?.route;
722
+ if (r && typeof r.uri === "string") {
723
+ levelRoutes[lvl] = { uri: r.uri, methods: r.methods ?? ["GET", "HEAD"] };
724
+ }
701
725
  }
726
+ state.levelRoutes = levelRoutes;
702
727
  if (explicitLevels && explicitLevels.length > 0) {
703
- const intersection = explicitLevels.filter((l) => availableLevels.includes(l));
704
- const removed = explicitLevels.filter((l) => !availableLevels.includes(l));
728
+ const intersection = explicitLevels.filter((l) => backendLevels.includes(l));
729
+ const removed = explicitLevels.filter((l) => !backendLevels.includes(l));
705
730
  if (removed.length > 0) {
706
731
  console.warn(
707
732
  `[route-forge] levels not in backend summary and dropped: ${removed.join(", ")}`
@@ -709,7 +734,7 @@ function applySummaryToState(summary, state, inputs) {
709
734
  }
710
735
  state.levels = intersection;
711
736
  } else {
712
- state.levels = availableLevels;
737
+ state.levels = backendLevels.slice();
713
738
  }
714
739
  const backendEager = backendLevels.filter((lvl) => summary.levels[lvl]?.load === "eager");
715
740
  if (!explicitEager) {
@@ -720,6 +745,25 @@ function applySummaryToState(summary, state, inputs) {
720
745
  }
721
746
  }
722
747
 
748
+ // src/embedded-summary.ts
749
+ var EMBEDDED_GLOBAL_KEY = "__ROUTE_FORGE__";
750
+ var memo;
751
+ function readEmbeddedSummary() {
752
+ if (memo !== void 0) return memo;
753
+ if (typeof window === "undefined") {
754
+ memo = null;
755
+ return null;
756
+ }
757
+ let raw;
758
+ try {
759
+ raw = window[EMBEDDED_GLOBAL_KEY];
760
+ } catch {
761
+ raw = void 0;
762
+ }
763
+ memo = raw !== null && typeof raw === "object" ? raw : null;
764
+ return memo;
765
+ }
766
+
723
767
  // src/route-store.ts
724
768
  var RouteStore = class {
725
769
  constructor(deps) {
@@ -739,33 +783,30 @@ var RouteStore = class {
739
783
  }
740
784
  }
741
785
  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
- );
786
+ const uri = this.state.levelRoutes[level]?.uri;
787
+ const url = uri ? this.joinBaseAndPath(this.baseURL, uri) : buildUrl(level, { baseURL: this.baseURL, endpoint: this.state.endpoint });
788
+ return await this.fetchMeta(`route-forge.${level}`, url, level);
789
+ }
790
+ /** baseURL 与后端下发的绝对 path 拼接(规范化斜杠),与 buildUrl 的 base 处理一致 */
791
+ joinBaseAndPath(baseURL, path) {
792
+ const base = baseURL.endsWith("/") ? baseURL.slice(0, -1) : baseURL;
793
+ const p = path.startsWith("/") ? path : `/${path}`;
794
+ return `${base}${p}`;
747
795
  }
748
796
  async loadOne(level) {
749
797
  const autoDiscoveryError = this.getAutoDiscoveryError();
750
798
  if (autoDiscoveryError) throw autoDiscoveryError;
751
799
  this.assertLevelDeclared(level);
752
- if (this.cache.get(level)) return;
800
+ const noCache = this.state.cacheTtl === null;
801
+ if (!noCache && this.cache.get(level)) return;
753
802
  const existing = this.inflight.get(level);
754
803
  if (existing) return existing;
755
804
  const gen = this.invalidationGens.get(level) ?? 0;
756
805
  const p = (async () => {
757
806
  try {
758
- if (level === UNASSIGNED_LEVEL && !this.state.backendHasUnassignedLevel && this.state.summaryUnassigned) {
759
- const routes = {};
760
- for (const r of this.state.summaryUnassigned) {
761
- routes[r.name] = { ...r, level: UNASSIGNED_LEVEL };
762
- }
763
- this.cache.set({ level: UNASSIGNED_LEVEL, routes, cache: null });
764
- return;
765
- }
766
807
  const resp = await this.fetchLevel(level);
767
808
  if ((this.invalidationGens.get(level) ?? 0) === gen) {
768
- this.cache.set(resp);
809
+ this.cache.set(resp, this.state.cacheTtl);
769
810
  }
770
811
  } finally {
771
812
  this.inflight.delete(level);
@@ -1048,8 +1089,13 @@ function createBoundForgeFactory(deps) {
1048
1089
  // src/forge.ts
1049
1090
  var DEFAULT_TIMEOUT = 3e4;
1050
1091
  var DEFAULT_CACHE_TTL = 3600;
1051
- function createRouteForge(options) {
1052
- if (!options.endpoint) throw new TypeError("options.endpoint is required");
1092
+ function createRouteForge(options = {}) {
1093
+ const bootstrapSummary = readEmbeddedSummary() ?? options.summary ?? null;
1094
+ if (!bootstrapSummary && !options.endpoint) {
1095
+ throw new TypeError(
1096
+ "createRouteForge: \u9700\u8981 options.endpoint\uFF0C\u6216 options.summary\uFF0C\u6216\u9875\u9762\u5185\u5D4C window.__ROUTE_FORGE__"
1097
+ );
1098
+ }
1053
1099
  const {
1054
1100
  adapter = "auto",
1055
1101
  timeout = DEFAULT_TIMEOUT,
@@ -1064,10 +1110,10 @@ function createRouteForge(options) {
1064
1110
  const discoveryState = {
1065
1111
  levels: explicitLevels ?? [],
1066
1112
  eager: explicitEager ?? [],
1067
- endpoint: explicitEndpoint,
1113
+ endpoint: explicitEndpoint ?? bootstrapSummary?.config?.endpoint_prefix ?? "",
1068
1114
  urlPrefix: "",
1069
- summaryUnassigned: void 0,
1070
- backendHasUnassignedLevel: false
1115
+ cacheTtl: void 0,
1116
+ levelRoutes: {}
1071
1117
  };
1072
1118
  const discoveryInputs = { explicitLevels, explicitEager, explicitEndpoint };
1073
1119
  let autoDiscoveryCompleted = false;
@@ -1159,12 +1205,19 @@ function createRouteForge(options) {
1159
1205
  }
1160
1206
  return resp.data;
1161
1207
  }
1162
- const autoDiscoveryPromise = fetchSummary(discoveryInputs, baseURL, fetchMeta).then((summary) => {
1163
- if (summary === null) {
1164
- return;
1165
- }
1166
- applySummaryToState(summary, discoveryState, discoveryInputs);
1167
- });
1208
+ let autoDiscoveryPromise;
1209
+ if (bootstrapSummary) {
1210
+ applySummaryToState(bootstrapSummary, discoveryState, discoveryInputs);
1211
+ autoDiscoveryCompleted = true;
1212
+ autoDiscoveryPromise = Promise.resolve();
1213
+ } else {
1214
+ autoDiscoveryPromise = fetchSummary(discoveryInputs, baseURL, fetchMeta).then((summary) => {
1215
+ if (summary === null) {
1216
+ return;
1217
+ }
1218
+ applySummaryToState(summary, discoveryState, discoveryInputs);
1219
+ });
1220
+ }
1168
1221
  let autoDiscoveryError = null;
1169
1222
  autoDiscoveryPromise.catch((e) => {
1170
1223
  autoDiscoveryError = e;