@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/README.md CHANGED
@@ -196,18 +196,22 @@ unsub()
196
196
  | Level load | 拉取某层级路由元数据 | `forge.isLoaded(level)` |
197
197
  | API request | 业务接口请求 | `forge.isLoading()` |
198
198
 
199
- ### `onSummaryReady` 回调
199
+ ### `ready()` 后挂载应用
200
200
 
201
- 推荐在回调中挂载应用,确保路由数据就绪:
201
+ 推荐在 `ready()` resolve 后挂载应用(auto-discovery + eager 层级全部完成),
202
+ 失败走 `catch`(onSummaryReady 回调已移除,统一走 ready——完整成功/失败语义链):
202
203
 
203
204
  ```ts
204
205
  const forge = createRouteForge({
205
206
  endpoint: '/_forge/routes',
206
- onSummaryReady: () => {
207
- // 摘要端点完成,路由数据已可用
208
- app.mount('#app')
209
- },
210
207
  })
208
+ // 成功:摘要 + eager 完成后挂载;失败:接住 reject,避免静默白屏
209
+ forge.ready()
210
+ .then(() => app.mount('#app'))
211
+ .catch((err) => {
212
+ console.error('[route-forge] init failed', err)
213
+ // 按业务需要降级:渲染错误页 / 重试 / 上报
214
+ })
211
215
  ```
212
216
 
213
217
  ### `forge.ready()` 方法
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { R as RouteMeta } from './types-BFlrOTrN.cjs';
2
+ import { R as RouteMeta } from './types-BvLdl02f.cjs';
3
3
 
4
4
  /**
5
5
  * @route-forge/core codegen CLI
package/dist/codegen.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { R as RouteMeta } from './types-BFlrOTrN.js';
2
+ import { R as RouteMeta } from './types-BvLdl02f.js';
3
3
 
4
4
  /**
5
5
  * @route-forge/core codegen CLI
package/dist/index.cjs CHANGED
@@ -16,9 +16,18 @@ function pickStorage(storage) {
16
16
  var RouteCache = class {
17
17
  constructor(opts) {
18
18
  this.memory = /* @__PURE__ */ new Map();
19
+ /** 其他 tab 修改 storage 时失效对应镜像(storage 事件:自己的写不触发,自己的写经 set/del 已同步) */
20
+ this.onStorageEvent = (e) => {
21
+ if (!e.key) return;
22
+ if (!e.key.startsWith(KEY_PREFIX)) return;
23
+ this.memory.delete(e.key.slice(KEY_PREFIX.length));
24
+ };
19
25
  this.storage = opts.storage;
20
26
  this.fallbackTtl = opts.ttl;
21
27
  this.backend = pickStorage(opts.storage);
28
+ if (this.backend && typeof globalThis.addEventListener === "function") {
29
+ globalThis.addEventListener("storage", this.onStorageEvent);
30
+ }
22
31
  }
23
32
  key(level) {
24
33
  return `${KEY_PREFIX}${level}`;
@@ -27,6 +36,8 @@ var RouteCache = class {
27
36
  if (this.storage === "memory" || !this.backend) {
28
37
  return this.getFromMemory(level);
29
38
  }
39
+ const mirrored = this.getFromMemory(level);
40
+ if (mirrored) return mirrored;
30
41
  const raw = this.backend.getItem(this.key(level));
31
42
  if (raw) {
32
43
  try {
@@ -35,12 +46,13 @@ var RouteCache = class {
35
46
  this.del(level);
36
47
  return void 0;
37
48
  }
49
+ this.memory.set(level, entry);
38
50
  return entry;
39
51
  } catch {
40
52
  return void 0;
41
53
  }
42
54
  }
43
- return this.getFromMemory(level);
55
+ return void 0;
44
56
  }
45
57
  getFromMemory(level) {
46
58
  const entry = this.memory.get(level);
@@ -64,14 +76,13 @@ var RouteCache = class {
64
76
  ttl,
65
77
  cachedAt: Date.now()
66
78
  };
79
+ this.memory.set(resp.level, entry);
67
80
  if (this.storage === "memory" || !this.backend) {
68
- this.memory.set(resp.level, entry);
69
81
  return;
70
82
  }
71
83
  try {
72
84
  this.backend.setItem(this.key(resp.level), JSON.stringify(entry));
73
85
  } catch {
74
- this.memory.set(resp.level, entry);
75
86
  }
76
87
  }
77
88
  del(level) {
@@ -251,14 +262,14 @@ function createInterceptorManager() {
251
262
  return new InterceptorManagerImpl();
252
263
  }
253
264
 
254
- // src/adapters/builtin-http.ts
255
- function isPassthroughBody(body) {
256
- if (body === null) return false;
257
- 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;
258
- }
259
- function isAbortError(err) {
265
+ // src/adapters/fetch-core.ts
266
+ function isAbortError(err, signal) {
267
+ if (signal?.aborted) return true;
260
268
  const e = err;
261
- return !!e && e.name === "AbortError";
269
+ if (!e) return false;
270
+ if (e.name === "AbortError") return true;
271
+ if (e.code === "ERR_CANCELED") return true;
272
+ return false;
262
273
  }
263
274
  function combineSignals(...signals) {
264
275
  const valid = signals.filter((s) => !!s);
@@ -277,82 +288,89 @@ function combineSignals(...signals) {
277
288
  }
278
289
  return controller.signal;
279
290
  }
280
- function createBuiltinHttp(forgeInterceptors) {
281
- const requestMgr = forgeInterceptors?.request ?? createInterceptorManager();
282
- const responseMgr = forgeInterceptors?.response ?? createInterceptorManager();
283
- async function requestRaw(config) {
284
- const signal = combineSignals(
285
- config.signal,
286
- config.timeout && config.timeout > 0 ? AbortSignal.timeout(config.timeout) : void 0
287
- );
288
- let url = config.url;
289
- if (config.paramsSerializer && config.params) {
290
- const qs = config.paramsSerializer(config.params);
291
- if (qs) {
292
- url = url.includes("?") ? `${url}&${qs}` : `${url}?${qs}`;
293
- }
291
+ function isPassthroughBody(body) {
292
+ if (body === null) return false;
293
+ 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;
294
+ }
295
+ async function rawFetch(config) {
296
+ const signal = combineSignals(
297
+ config.signal,
298
+ config.timeout && config.timeout > 0 ? AbortSignal.timeout(config.timeout) : void 0
299
+ );
300
+ let url = config.url;
301
+ if (config.paramsSerializer && config.params) {
302
+ const qs = config.paramsSerializer(config.params);
303
+ if (qs) {
304
+ url = url.includes("?") ? `${url}&${qs}` : `${url}?${qs}`;
294
305
  }
295
- const headers = new Headers(config.headers);
296
- const fetchInit = {
297
- method: config.method,
298
- headers
299
- };
300
- if (signal) fetchInit.signal = signal;
301
- if (config.body !== void 0 && !["GET", "HEAD"].includes(config.method.toUpperCase())) {
302
- if (typeof config.body === "string" || isPassthroughBody(config.body)) {
303
- fetchInit.body = config.body;
304
- } else {
305
- fetchInit.body = JSON.stringify(config.body);
306
- if (!headers.has("Content-Type")) {
307
- headers.set("Content-Type", "application/json");
308
- }
306
+ }
307
+ const headers = new Headers(config.headers);
308
+ const fetchInit = {
309
+ method: config.method,
310
+ headers
311
+ };
312
+ if (signal) fetchInit.signal = signal;
313
+ if (config.body !== void 0 && !["GET", "HEAD"].includes(config.method.toUpperCase())) {
314
+ if (typeof config.body === "string" || isPassthroughBody(config.body)) {
315
+ fetchInit.body = config.body;
316
+ } else {
317
+ fetchInit.body = JSON.stringify(config.body);
318
+ if (!headers.has("Content-Type")) {
319
+ headers.set("Content-Type", "application/json");
309
320
  }
310
321
  }
311
- let res;
322
+ }
323
+ let res;
324
+ try {
325
+ res = await fetch(url, fetchInit);
326
+ } catch (e) {
327
+ if (isAbortError(e)) throw e;
328
+ throw new NetworkError(
329
+ e instanceof Error ? e.message : String(e),
330
+ config.route,
331
+ config.level,
332
+ e
333
+ );
334
+ }
335
+ const text = await res.text();
336
+ let data = text;
337
+ const contentType = res.headers.get("content-type") ?? "";
338
+ if (contentType.includes("application/json")) {
312
339
  try {
313
- res = await fetch(url, fetchInit);
314
- } catch (e) {
315
- if (isAbortError(e)) throw e;
316
- throw new NetworkError(
317
- e instanceof Error ? e.message : String(e),
318
- config.route,
319
- config.level,
320
- e
321
- );
322
- }
323
- const text = await res.text();
324
- let data = text;
325
- const contentType = res.headers.get("content-type") ?? "";
326
- if (contentType.includes("application/json")) {
327
- try {
328
- data = JSON.parse(text);
329
- } catch {
330
- }
340
+ data = JSON.parse(text);
341
+ } catch {
331
342
  }
332
- const responseData = {
343
+ }
344
+ const responseData = {
345
+ route: config.route,
346
+ level: config.level,
347
+ method: config.method,
348
+ url,
349
+ status: res.status,
350
+ headers: res.headers,
351
+ data,
352
+ config
353
+ };
354
+ if (res.status >= 200 && res.status < 300) {
355
+ return responseData;
356
+ }
357
+ throw new HTTPError(
358
+ `HTTP ${res.status} for route "${config.route}" (${config.method} ${url})`,
359
+ {
333
360
  route: config.route,
334
361
  level: config.level,
335
- method: config.method,
336
- url,
337
362
  status: res.status,
338
- headers: res.headers,
339
- data,
340
- config
341
- };
342
- if (res.status >= 200 && res.status < 300) {
343
- return responseData;
363
+ url,
364
+ method: config.method
344
365
  }
345
- throw new HTTPError(
346
- `HTTP ${res.status} for route "${config.route}" (${config.method} ${url})`,
347
- {
348
- route: config.route,
349
- level: config.level,
350
- status: res.status,
351
- url,
352
- method: config.method
353
- }
354
- );
355
- }
366
+ );
367
+ }
368
+
369
+ // src/adapters/builtin-http.ts
370
+ function createBuiltinHttp(forgeInterceptors) {
371
+ const requestMgr = forgeInterceptors?.request ?? createInterceptorManager();
372
+ const responseMgr = forgeInterceptors?.response ?? createInterceptorManager();
373
+ const requestRaw = rawFetch;
356
374
  async function request(config) {
357
375
  const finalConfig = await runRequestInterceptors(requestMgr, config);
358
376
  const source = Promise.resolve(finalConfig).then(requestRaw);
@@ -587,17 +605,12 @@ function createRouteForge(options) {
587
605
  let effectiveUrlPrefix = "";
588
606
  let summaryUnassigned;
589
607
  let backendHasUnassignedLevel = false;
590
- const summaryPromise = (async () => {
608
+ const fetchSummary = async () => {
591
609
  try {
592
- const summaryUrl = explicitEndpoint;
593
- const resp = await fetch(summaryUrl, { method: "GET" });
594
- if (!resp.ok) {
595
- console.warn(
596
- `[route-forge] summary endpoint ${summaryUrl} unreachable (HTTP ${resp.status}); falling back to explicit options`
597
- );
598
- return null;
599
- }
600
- return await resp.json();
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;
601
614
  } catch (e) {
602
615
  if (explicitLevels && explicitLevels.length > 0) {
603
616
  console.warn(
@@ -607,8 +620,11 @@ function createRouteForge(options) {
607
620
  }
608
621
  throw new UnknownLevelError("(auto-discovery)");
609
622
  }
610
- })();
611
- const autoDiscoveryPromise = summaryPromise.then((summary) => {
623
+ };
624
+ let summaryPromise;
625
+ const summaryWaiters = [];
626
+ const whenSummary = () => summaryPromise ?? new Promise((resolve) => summaryWaiters.push(resolve));
627
+ const autoDiscoveryPromise = whenSummary().then((summary) => {
612
628
  if (summary === null) {
613
629
  return;
614
630
  }
@@ -662,8 +678,12 @@ function createRouteForge(options) {
662
678
  });
663
679
  let autoDiscoveryCompleted = false;
664
680
  let resolveReady;
665
- const readyPromise = new Promise((resolve) => {
681
+ let rejectReady;
682
+ const readyPromise = new Promise((resolve, reject) => {
666
683
  resolveReady = resolve;
684
+ rejectReady = reject;
685
+ });
686
+ readyPromise.catch(() => {
667
687
  });
668
688
  const cacheTtl = cacheOpts.ttl ?? DEFAULT_CACHE_TTL;
669
689
  const cacheStorage = cacheOpts.storage ?? "memory";
@@ -694,6 +714,10 @@ function createRouteForge(options) {
694
714
  adapter,
695
715
  forgeInterceptors: { request: requestInterceptors, response: responseInterceptors }
696
716
  });
717
+ Promise.resolve().then(() => {
718
+ summaryPromise = fetchSummary();
719
+ for (const resolve of summaryWaiters) resolve(summaryPromise);
720
+ });
697
721
  let adapterResolved = false;
698
722
  let adapterObj = null;
699
723
  async function ensureAdapter() {
@@ -729,19 +753,19 @@ function createRouteForge(options) {
729
753
  const ep = effectiveEndpoint.startsWith("/") ? effectiveEndpoint : `/${effectiveEndpoint}`;
730
754
  return `${base}${ep}/${encodeURIComponent(level)}`;
731
755
  }
732
- async function fetchLevel(level) {
756
+ async function fetchMeta(routeTag, url, level = "") {
733
757
  const adp = await ensureAdapter();
734
758
  const config = {
735
- route: `__forge__.load.${level}`,
759
+ route: routeTag,
736
760
  level,
737
761
  method: "GET",
738
- url: buildUrl(level),
762
+ url,
739
763
  headers: { Accept: "application/json" },
740
764
  params: {},
741
765
  timeout,
742
766
  meta: {
743
- name: `__forge__.load.${level}`,
744
- uri: buildUrl(level),
767
+ name: routeTag,
768
+ uri: url,
745
769
  methods: ["GET"],
746
770
  parameters: [],
747
771
  level
@@ -751,12 +775,14 @@ function createRouteForge(options) {
751
775
  const resp = await doRawRequest(config);
752
776
  if (!resp || resp.status < 200 || resp.status >= 300) {
753
777
  throw new HTTPError(
754
- `Failed to load level "${level}": HTTP ${resp?.status}`,
755
- { level, status: resp?.status, url: buildUrl(level), method: "GET" }
778
+ `Failed to fetch "${routeTag}": HTTP ${resp?.status}`,
779
+ { level, status: resp?.status, url, method: "GET" }
756
780
  );
757
781
  }
758
- const data = resp.data;
759
- return data;
782
+ return resp.data;
783
+ }
784
+ async function fetchLevel(level) {
785
+ return await fetchMeta(`__forge__.load.${level}`, buildUrl(level), level);
760
786
  }
761
787
  async function loadOne(level) {
762
788
  if (autoDiscoveryError) throw autoDiscoveryError;
@@ -928,7 +954,7 @@ function createRouteForge(options) {
928
954
  },
929
955
  (err) => {
930
956
  if (err instanceof ForgeError) throw err;
931
- if (isAbortError2(err, signal)) {
957
+ if (isAbortError(err, signal)) {
932
958
  throw new RequestAbortedError(meta.name, meta.level, err);
933
959
  }
934
960
  throw new NetworkError(
@@ -997,15 +1023,22 @@ function createRouteForge(options) {
997
1023
  }
998
1024
  void autoDiscoveryPromise.then(() => {
999
1025
  autoDiscoveryCompleted = true;
1000
- options.onSummaryReady?.();
1001
1026
  if (effectiveEager.length > 0) {
1002
- return Promise.all(effectiveEager.map((lvl) => load(lvl))).catch((e) => {
1003
- console.warn(`[route-forge] eager load failed: ${e.message}`);
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
+ });
1004
1036
  });
1005
1037
  }
1006
1038
  }).then(() => {
1007
1039
  resolveReady(forgeInstance);
1008
- }).catch(() => {
1040
+ }).catch((e) => {
1041
+ rejectReady(e);
1009
1042
  });
1010
1043
  function ready(onFulfilled, onRejected) {
1011
1044
  if (onFulfilled) {
@@ -1151,14 +1184,6 @@ function resolveApiParams(input) {
1151
1184
  }
1152
1185
  return { pathParams, query, body, headers, timeout: perCallTimeout };
1153
1186
  }
1154
- function isAbortError2(err, signal) {
1155
- if (signal?.aborted) return true;
1156
- const e = err;
1157
- if (!e) return false;
1158
- if (e.name === "AbortError") return true;
1159
- if (e.code === "ERR_CANCELED") return true;
1160
- return false;
1161
- }
1162
1187
 
1163
1188
  exports.AdapterNotFoundError = AdapterNotFoundError;
1164
1189
  exports.ForgeError = ForgeError;