@route-forge/core 1.4.0 → 2.0.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.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { I as InterceptorManager, a as InterceptorHandler, C as CacheStorage, R as RouteMeta, L as LevelRoutesResponse, b as RouteForgeOptions, c as RouteForge } from './types-BFlrOTrN.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, r as UseForgeByPrefixReturn } from './types-BFlrOTrN.js';
1
+ import { I as InterceptorManager, a as InterceptorHandler, C as CacheStorage, R as RouteMeta, L as LevelRoutesResponse, b as RouteForgeOptions, c as RouteForge } from './types-BvLdl02f.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-BvLdl02f.js';
3
3
 
4
4
  /**
5
5
  * 拦截器管理器实现
@@ -45,6 +45,10 @@ declare function createInterceptorManager<TIn, TOut = TIn>(): InterceptorManager
45
45
  * - 每层级独立条目,互不污染(cache key = `route-forge:${level}`)
46
46
  * - TTL 优先用后端响应里的 cache 字段,本地 cache.ttl 仅作兜底
47
47
  * - storage: 'memory' | 'sessionStorage' | 'localStorage'
48
+ * - storage 模式下维护内存镜像:首次读盘解析后驻留内存,后续 get 直接命中
49
+ * (读盘 + JSON.parse 整层路由表是同步阻塞操作,热路径重复执行代价高);
50
+ * 写操作(set/del/clear)同步更新镜像,并通过 storage 事件感知其他
51
+ * tab 的写入/失效,保证跨 tab 新鲜度与无镜像时一致
48
52
  */
49
53
 
50
54
  interface CacheEntry {
@@ -58,6 +62,8 @@ declare class RouteCache {
58
62
  private readonly fallbackTtl;
59
63
  private readonly backend;
60
64
  private readonly memory;
65
+ /** 其他 tab 修改 storage 时失效对应镜像(storage 事件:自己的写不触发,自己的写经 set/del 已同步) */
66
+ private readonly onStorageEvent;
61
67
  constructor(opts: {
62
68
  storage: CacheStorage;
63
69
  ttl: number;
@@ -78,14 +84,19 @@ declare class RouteCache {
78
84
  interface ForgeErrorContext {
79
85
  [key: string]: unknown;
80
86
  }
87
+ /**
88
+ * Route Forge 错误码字面量联合。
89
+ * 用户侧 `switch (e.code)` 可获得穷尽检查(漏分支编译报错)。
90
+ */
91
+ type ForgeErrorCode = 'RF_FE_001' | 'RF_FE_002' | 'RF_FE_003' | 'RF_FE_005' | 'RF_FE_006' | 'RF_FE_007' | 'RF_FE_008' | 'RF_FE_009' | 'RF_FE_010';
81
92
  declare class ForgeError extends Error {
82
- readonly code: string;
93
+ readonly code: ForgeErrorCode;
83
94
  readonly route?: string;
84
95
  readonly level?: string;
85
96
  readonly context?: ForgeErrorContext;
86
97
  readonly cause?: unknown;
87
98
  constructor(message: string, opts: {
88
- code: string;
99
+ code: ForgeErrorCode;
89
100
  route?: string;
90
101
  level?: string;
91
102
  context?: ForgeErrorContext;
@@ -177,4 +188,4 @@ declare function resolveRouteName(forge: RouteResolver, level: string, prefix: s
177
188
  */
178
189
  declare function resolveRouteNameSync(forge: RouteResolver, level: string, prefix: string, suffix: string, separator?: string): string;
179
190
 
180
- export { AdapterNotFoundError, CacheStorage, ForgeError, HTTPError, InterceptorHandler, InterceptorManager, InterceptorManagerImpl, InvalidInterceptorReturnError, LevelRoutesResponse, MissingRouteParamError, NetworkError, RequestAbortedError, RouteCache, RouteForge, RouteForgeOptions, RouteMeta, type RouteResolver, UnknownLevelError, UnknownRouteError, createInterceptorManager, createRouteForge, resolveRouteName, resolveRouteNameSync };
191
+ export { AdapterNotFoundError, CacheStorage, ForgeError, type ForgeErrorCode, HTTPError, InterceptorHandler, InterceptorManager, InterceptorManagerImpl, InvalidInterceptorReturnError, LevelRoutesResponse, MissingRouteParamError, NetworkError, RequestAbortedError, RouteCache, RouteForge, RouteForgeOptions, RouteMeta, type RouteResolver, UnknownLevelError, UnknownRouteError, createInterceptorManager, createRouteForge, resolveRouteName, resolveRouteNameSync };
package/dist/index.js CHANGED
@@ -14,9 +14,18 @@ function pickStorage(storage) {
14
14
  var RouteCache = class {
15
15
  constructor(opts) {
16
16
  this.memory = /* @__PURE__ */ new Map();
17
+ /** 其他 tab 修改 storage 时失效对应镜像(storage 事件:自己的写不触发,自己的写经 set/del 已同步) */
18
+ this.onStorageEvent = (e) => {
19
+ if (!e.key) return;
20
+ if (!e.key.startsWith(KEY_PREFIX)) return;
21
+ this.memory.delete(e.key.slice(KEY_PREFIX.length));
22
+ };
17
23
  this.storage = opts.storage;
18
24
  this.fallbackTtl = opts.ttl;
19
25
  this.backend = pickStorage(opts.storage);
26
+ if (this.backend && typeof globalThis.addEventListener === "function") {
27
+ globalThis.addEventListener("storage", this.onStorageEvent);
28
+ }
20
29
  }
21
30
  key(level) {
22
31
  return `${KEY_PREFIX}${level}`;
@@ -25,6 +34,8 @@ var RouteCache = class {
25
34
  if (this.storage === "memory" || !this.backend) {
26
35
  return this.getFromMemory(level);
27
36
  }
37
+ const mirrored = this.getFromMemory(level);
38
+ if (mirrored) return mirrored;
28
39
  const raw = this.backend.getItem(this.key(level));
29
40
  if (raw) {
30
41
  try {
@@ -33,12 +44,13 @@ var RouteCache = class {
33
44
  this.del(level);
34
45
  return void 0;
35
46
  }
47
+ this.memory.set(level, entry);
36
48
  return entry;
37
49
  } catch {
38
50
  return void 0;
39
51
  }
40
52
  }
41
- return this.getFromMemory(level);
53
+ return void 0;
42
54
  }
43
55
  getFromMemory(level) {
44
56
  const entry = this.memory.get(level);
@@ -62,14 +74,13 @@ var RouteCache = class {
62
74
  ttl,
63
75
  cachedAt: Date.now()
64
76
  };
77
+ this.memory.set(resp.level, entry);
65
78
  if (this.storage === "memory" || !this.backend) {
66
- this.memory.set(resp.level, entry);
67
79
  return;
68
80
  }
69
81
  try {
70
82
  this.backend.setItem(this.key(resp.level), JSON.stringify(entry));
71
83
  } catch {
72
- this.memory.set(resp.level, entry);
73
84
  }
74
85
  }
75
86
  del(level) {
@@ -249,14 +260,14 @@ function createInterceptorManager() {
249
260
  return new InterceptorManagerImpl();
250
261
  }
251
262
 
252
- // src/adapters/builtin-http.ts
253
- function isPassthroughBody(body) {
254
- if (body === null) return false;
255
- return typeof FormData !== "undefined" && body instanceof FormData || typeof URLSearchParams !== "undefined" && body instanceof URLSearchParams || typeof Blob !== "undefined" && body instanceof Blob || typeof ArrayBuffer !== "undefined" && (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) || typeof ReadableStream !== "undefined" && body instanceof ReadableStream;
256
- }
257
- function isAbortError(err) {
263
+ // src/adapters/fetch-core.ts
264
+ function isAbortError(err, signal) {
265
+ if (signal?.aborted) return true;
258
266
  const e = err;
259
- return !!e && e.name === "AbortError";
267
+ if (!e) return false;
268
+ if (e.name === "AbortError") return true;
269
+ if (e.code === "ERR_CANCELED") return true;
270
+ return false;
260
271
  }
261
272
  function combineSignals(...signals) {
262
273
  const valid = signals.filter((s) => !!s);
@@ -275,82 +286,89 @@ function combineSignals(...signals) {
275
286
  }
276
287
  return controller.signal;
277
288
  }
278
- function createBuiltinHttp(forgeInterceptors) {
279
- const requestMgr = forgeInterceptors?.request ?? createInterceptorManager();
280
- const responseMgr = forgeInterceptors?.response ?? createInterceptorManager();
281
- async function requestRaw(config) {
282
- const signal = combineSignals(
283
- config.signal,
284
- config.timeout && config.timeout > 0 ? AbortSignal.timeout(config.timeout) : void 0
285
- );
286
- let url = config.url;
287
- if (config.paramsSerializer && config.params) {
288
- const qs = config.paramsSerializer(config.params);
289
- if (qs) {
290
- url = url.includes("?") ? `${url}&${qs}` : `${url}?${qs}`;
291
- }
289
+ function isPassthroughBody(body) {
290
+ if (body === null) return false;
291
+ return typeof FormData !== "undefined" && body instanceof FormData || typeof URLSearchParams !== "undefined" && body instanceof URLSearchParams || typeof Blob !== "undefined" && body instanceof Blob || typeof ArrayBuffer !== "undefined" && (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) || typeof ReadableStream !== "undefined" && body instanceof ReadableStream;
292
+ }
293
+ async function rawFetch(config) {
294
+ const signal = combineSignals(
295
+ config.signal,
296
+ config.timeout && config.timeout > 0 ? AbortSignal.timeout(config.timeout) : void 0
297
+ );
298
+ let url = config.url;
299
+ if (config.paramsSerializer && config.params) {
300
+ const qs = config.paramsSerializer(config.params);
301
+ if (qs) {
302
+ url = url.includes("?") ? `${url}&${qs}` : `${url}?${qs}`;
292
303
  }
293
- const headers = new Headers(config.headers);
294
- const fetchInit = {
295
- method: config.method,
296
- headers
297
- };
298
- if (signal) fetchInit.signal = signal;
299
- if (config.body !== void 0 && !["GET", "HEAD"].includes(config.method.toUpperCase())) {
300
- if (typeof config.body === "string" || isPassthroughBody(config.body)) {
301
- fetchInit.body = config.body;
302
- } else {
303
- fetchInit.body = JSON.stringify(config.body);
304
- if (!headers.has("Content-Type")) {
305
- headers.set("Content-Type", "application/json");
306
- }
304
+ }
305
+ const headers = new Headers(config.headers);
306
+ const fetchInit = {
307
+ method: config.method,
308
+ headers
309
+ };
310
+ if (signal) fetchInit.signal = signal;
311
+ if (config.body !== void 0 && !["GET", "HEAD"].includes(config.method.toUpperCase())) {
312
+ if (typeof config.body === "string" || isPassthroughBody(config.body)) {
313
+ fetchInit.body = config.body;
314
+ } else {
315
+ fetchInit.body = JSON.stringify(config.body);
316
+ if (!headers.has("Content-Type")) {
317
+ headers.set("Content-Type", "application/json");
307
318
  }
308
319
  }
309
- let res;
320
+ }
321
+ let res;
322
+ try {
323
+ res = await fetch(url, fetchInit);
324
+ } catch (e) {
325
+ if (isAbortError(e)) throw e;
326
+ throw new NetworkError(
327
+ e instanceof Error ? e.message : String(e),
328
+ config.route,
329
+ config.level,
330
+ e
331
+ );
332
+ }
333
+ const text = await res.text();
334
+ let data = text;
335
+ const contentType = res.headers.get("content-type") ?? "";
336
+ if (contentType.includes("application/json")) {
310
337
  try {
311
- res = await fetch(url, fetchInit);
312
- } catch (e) {
313
- if (isAbortError(e)) throw e;
314
- throw new NetworkError(
315
- e instanceof Error ? e.message : String(e),
316
- config.route,
317
- config.level,
318
- e
319
- );
320
- }
321
- const text = await res.text();
322
- let data = text;
323
- const contentType = res.headers.get("content-type") ?? "";
324
- if (contentType.includes("application/json")) {
325
- try {
326
- data = JSON.parse(text);
327
- } catch {
328
- }
338
+ data = JSON.parse(text);
339
+ } catch {
329
340
  }
330
- const responseData = {
341
+ }
342
+ const responseData = {
343
+ route: config.route,
344
+ level: config.level,
345
+ method: config.method,
346
+ url,
347
+ status: res.status,
348
+ headers: res.headers,
349
+ data,
350
+ config
351
+ };
352
+ if (res.status >= 200 && res.status < 300) {
353
+ return responseData;
354
+ }
355
+ throw new HTTPError(
356
+ `HTTP ${res.status} for route "${config.route}" (${config.method} ${url})`,
357
+ {
331
358
  route: config.route,
332
359
  level: config.level,
333
- method: config.method,
334
- url,
335
360
  status: res.status,
336
- headers: res.headers,
337
- data,
338
- config
339
- };
340
- if (res.status >= 200 && res.status < 300) {
341
- return responseData;
361
+ url,
362
+ method: config.method
342
363
  }
343
- throw new HTTPError(
344
- `HTTP ${res.status} for route "${config.route}" (${config.method} ${url})`,
345
- {
346
- route: config.route,
347
- level: config.level,
348
- status: res.status,
349
- url,
350
- method: config.method
351
- }
352
- );
353
- }
364
+ );
365
+ }
366
+
367
+ // src/adapters/builtin-http.ts
368
+ function createBuiltinHttp(forgeInterceptors) {
369
+ const requestMgr = forgeInterceptors?.request ?? createInterceptorManager();
370
+ const responseMgr = forgeInterceptors?.response ?? createInterceptorManager();
371
+ const requestRaw = rawFetch;
354
372
  async function request(config) {
355
373
  const finalConfig = await runRequestInterceptors(requestMgr, config);
356
374
  const source = Promise.resolve(finalConfig).then(requestRaw);
@@ -585,17 +603,12 @@ function createRouteForge(options) {
585
603
  let effectiveUrlPrefix = "";
586
604
  let summaryUnassigned;
587
605
  let backendHasUnassignedLevel = false;
588
- const summaryPromise = (async () => {
606
+ const fetchSummary = async () => {
589
607
  try {
590
- const summaryUrl = explicitEndpoint;
591
- const resp = await fetch(summaryUrl, { method: "GET" });
592
- if (!resp.ok) {
593
- console.warn(
594
- `[route-forge] summary endpoint ${summaryUrl} unreachable (HTTP ${resp.status}); falling back to explicit options`
595
- );
596
- return null;
597
- }
598
- return await resp.json();
608
+ const base = baseURL.endsWith("/") ? baseURL.slice(0, -1) : baseURL;
609
+ const ep = explicitEndpoint.startsWith("/") ? explicitEndpoint : `/${explicitEndpoint}`;
610
+ const data = await fetchMeta("__forge__.summary", `${base}${ep}`);
611
+ return data;
599
612
  } catch (e) {
600
613
  if (explicitLevels && explicitLevels.length > 0) {
601
614
  console.warn(
@@ -605,8 +618,11 @@ function createRouteForge(options) {
605
618
  }
606
619
  throw new UnknownLevelError("(auto-discovery)");
607
620
  }
608
- })();
609
- const autoDiscoveryPromise = summaryPromise.then((summary) => {
621
+ };
622
+ let summaryPromise;
623
+ const summaryWaiters = [];
624
+ const whenSummary = () => summaryPromise ?? new Promise((resolve) => summaryWaiters.push(resolve));
625
+ const autoDiscoveryPromise = whenSummary().then((summary) => {
610
626
  if (summary === null) {
611
627
  return;
612
628
  }
@@ -660,8 +676,12 @@ function createRouteForge(options) {
660
676
  });
661
677
  let autoDiscoveryCompleted = false;
662
678
  let resolveReady;
663
- const readyPromise = new Promise((resolve) => {
679
+ let rejectReady;
680
+ const readyPromise = new Promise((resolve, reject) => {
664
681
  resolveReady = resolve;
682
+ rejectReady = reject;
683
+ });
684
+ readyPromise.catch(() => {
665
685
  });
666
686
  const cacheTtl = cacheOpts.ttl ?? DEFAULT_CACHE_TTL;
667
687
  const cacheStorage = cacheOpts.storage ?? "memory";
@@ -692,6 +712,10 @@ function createRouteForge(options) {
692
712
  adapter,
693
713
  forgeInterceptors: { request: requestInterceptors, response: responseInterceptors }
694
714
  });
715
+ Promise.resolve().then(() => {
716
+ summaryPromise = fetchSummary();
717
+ for (const resolve of summaryWaiters) resolve(summaryPromise);
718
+ });
695
719
  let adapterResolved = false;
696
720
  let adapterObj = null;
697
721
  async function ensureAdapter() {
@@ -727,19 +751,19 @@ function createRouteForge(options) {
727
751
  const ep = effectiveEndpoint.startsWith("/") ? effectiveEndpoint : `/${effectiveEndpoint}`;
728
752
  return `${base}${ep}/${encodeURIComponent(level)}`;
729
753
  }
730
- async function fetchLevel(level) {
754
+ async function fetchMeta(routeTag, url, level = "") {
731
755
  const adp = await ensureAdapter();
732
756
  const config = {
733
- route: `__forge__.load.${level}`,
757
+ route: routeTag,
734
758
  level,
735
759
  method: "GET",
736
- url: buildUrl(level),
760
+ url,
737
761
  headers: { Accept: "application/json" },
738
762
  params: {},
739
763
  timeout,
740
764
  meta: {
741
- name: `__forge__.load.${level}`,
742
- uri: buildUrl(level),
765
+ name: routeTag,
766
+ uri: url,
743
767
  methods: ["GET"],
744
768
  parameters: [],
745
769
  level
@@ -749,12 +773,14 @@ function createRouteForge(options) {
749
773
  const resp = await doRawRequest(config);
750
774
  if (!resp || resp.status < 200 || resp.status >= 300) {
751
775
  throw new HTTPError(
752
- `Failed to load level "${level}": HTTP ${resp?.status}`,
753
- { level, status: resp?.status, url: buildUrl(level), method: "GET" }
776
+ `Failed to fetch "${routeTag}": HTTP ${resp?.status}`,
777
+ { level, status: resp?.status, url, method: "GET" }
754
778
  );
755
779
  }
756
- const data = resp.data;
757
- return data;
780
+ return resp.data;
781
+ }
782
+ async function fetchLevel(level) {
783
+ return await fetchMeta(`__forge__.load.${level}`, buildUrl(level), level);
758
784
  }
759
785
  async function loadOne(level) {
760
786
  if (autoDiscoveryError) throw autoDiscoveryError;
@@ -926,7 +952,7 @@ function createRouteForge(options) {
926
952
  },
927
953
  (err) => {
928
954
  if (err instanceof ForgeError) throw err;
929
- if (isAbortError2(err, signal)) {
955
+ if (isAbortError(err, signal)) {
930
956
  throw new RequestAbortedError(meta.name, meta.level, err);
931
957
  }
932
958
  throw new NetworkError(
@@ -995,15 +1021,22 @@ function createRouteForge(options) {
995
1021
  }
996
1022
  void autoDiscoveryPromise.then(() => {
997
1023
  autoDiscoveryCompleted = true;
998
- options.onSummaryReady?.();
999
1024
  if (effectiveEager.length > 0) {
1000
- return Promise.all(effectiveEager.map((lvl) => load(lvl))).catch((e) => {
1001
- console.warn(`[route-forge] eager load failed: ${e.message}`);
1025
+ return Promise.allSettled(effectiveEager.map((lvl) => load(lvl))).then((results) => {
1026
+ results.forEach((r, i) => {
1027
+ if (r.status === "rejected") {
1028
+ console.error(
1029
+ `[route-forge] eager load failed for level "${effectiveEager[i]}":`,
1030
+ r.reason
1031
+ );
1032
+ }
1033
+ });
1002
1034
  });
1003
1035
  }
1004
1036
  }).then(() => {
1005
1037
  resolveReady(forgeInstance);
1006
- }).catch(() => {
1038
+ }).catch((e) => {
1039
+ rejectReady(e);
1007
1040
  });
1008
1041
  function ready(onFulfilled, onRejected) {
1009
1042
  if (onFulfilled) {
@@ -1149,14 +1182,6 @@ function resolveApiParams(input) {
1149
1182
  }
1150
1183
  return { pathParams, query, body, headers, timeout: perCallTimeout };
1151
1184
  }
1152
- function isAbortError2(err, signal) {
1153
- if (signal?.aborted) return true;
1154
- const e = err;
1155
- if (!e) return false;
1156
- if (e.name === "AbortError") return true;
1157
- if (e.code === "ERR_CANCELED") return true;
1158
- return false;
1159
- }
1160
1185
 
1161
1186
  export { AdapterNotFoundError, ForgeError, HTTPError, InterceptorManagerImpl, InvalidInterceptorReturnError, LoadingTracker, MissingRouteParamError, NetworkError, RequestAbortedError, RouteCache, UnknownLevelError, UnknownRouteError, createInterceptorManager, createRouteForge, resolveRouteName, resolveRouteNameSync };
1162
1187
  //# sourceMappingURL=index.js.map