@route-forge/core 1.3.1 → 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.
@@ -17,9 +17,18 @@ var RouteForge = (function (exports) {
17
17
  var RouteCache = class {
18
18
  constructor(opts) {
19
19
  this.memory = /* @__PURE__ */ new Map();
20
+ /** 其他 tab 修改 storage 时失效对应镜像(storage 事件:自己的写不触发,自己的写经 set/del 已同步) */
21
+ this.onStorageEvent = (e) => {
22
+ if (!e.key) return;
23
+ if (!e.key.startsWith(KEY_PREFIX)) return;
24
+ this.memory.delete(e.key.slice(KEY_PREFIX.length));
25
+ };
20
26
  this.storage = opts.storage;
21
27
  this.fallbackTtl = opts.ttl;
22
28
  this.backend = pickStorage(opts.storage);
29
+ if (this.backend && typeof globalThis.addEventListener === "function") {
30
+ globalThis.addEventListener("storage", this.onStorageEvent);
31
+ }
23
32
  }
24
33
  key(level) {
25
34
  return `${KEY_PREFIX}${level}`;
@@ -28,6 +37,8 @@ var RouteForge = (function (exports) {
28
37
  if (this.storage === "memory" || !this.backend) {
29
38
  return this.getFromMemory(level);
30
39
  }
40
+ const mirrored = this.getFromMemory(level);
41
+ if (mirrored) return mirrored;
31
42
  const raw = this.backend.getItem(this.key(level));
32
43
  if (raw) {
33
44
  try {
@@ -36,12 +47,13 @@ var RouteForge = (function (exports) {
36
47
  this.del(level);
37
48
  return void 0;
38
49
  }
50
+ this.memory.set(level, entry);
39
51
  return entry;
40
52
  } catch {
41
53
  return void 0;
42
54
  }
43
55
  }
44
- return this.getFromMemory(level);
56
+ return void 0;
45
57
  }
46
58
  getFromMemory(level) {
47
59
  const entry = this.memory.get(level);
@@ -65,14 +77,13 @@ var RouteForge = (function (exports) {
65
77
  ttl,
66
78
  cachedAt: Date.now()
67
79
  };
80
+ this.memory.set(resp.level, entry);
68
81
  if (this.storage === "memory" || !this.backend) {
69
- this.memory.set(resp.level, entry);
70
82
  return;
71
83
  }
72
84
  try {
73
85
  this.backend.setItem(this.key(resp.level), JSON.stringify(entry));
74
86
  } catch {
75
- this.memory.set(resp.level, entry);
76
87
  }
77
88
  }
78
89
  del(level) {
@@ -252,14 +263,14 @@ var RouteForge = (function (exports) {
252
263
  return new InterceptorManagerImpl();
253
264
  }
254
265
 
255
- // src/adapters/builtin-http.ts
256
- function isPassthroughBody(body) {
257
- if (body === null) return false;
258
- 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;
259
- }
260
- function isAbortError(err) {
266
+ // src/adapters/fetch-core.ts
267
+ function isAbortError(err, signal) {
268
+ if (signal?.aborted) return true;
261
269
  const e = err;
262
- return !!e && e.name === "AbortError";
270
+ if (!e) return false;
271
+ if (e.name === "AbortError") return true;
272
+ if (e.code === "ERR_CANCELED") return true;
273
+ return false;
263
274
  }
264
275
  function combineSignals(...signals) {
265
276
  const valid = signals.filter((s) => !!s);
@@ -278,81 +289,92 @@ var RouteForge = (function (exports) {
278
289
  }
279
290
  return controller.signal;
280
291
  }
281
- function createBuiltinHttp(forgeInterceptors) {
282
- const requestMgr = forgeInterceptors?.request ?? createInterceptorManager();
283
- const responseMgr = forgeInterceptors?.response ?? createInterceptorManager();
284
- async function request(config) {
285
- const finalConfig = await runRequestInterceptors(requestMgr, config);
286
- const signal = combineSignals(
287
- finalConfig.signal,
288
- finalConfig.timeout && finalConfig.timeout > 0 ? AbortSignal.timeout(finalConfig.timeout) : void 0
289
- );
290
- let url = finalConfig.url;
291
- if (finalConfig.paramsSerializer && finalConfig.params) {
292
- const qs = finalConfig.paramsSerializer(finalConfig.params);
293
- if (qs) {
294
- url = url.includes("?") ? `${url}&${qs}` : `${url}?${qs}`;
295
- }
292
+ function isPassthroughBody(body) {
293
+ if (body === null) return false;
294
+ 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;
295
+ }
296
+ async function rawFetch(config) {
297
+ const signal = combineSignals(
298
+ config.signal,
299
+ config.timeout && config.timeout > 0 ? AbortSignal.timeout(config.timeout) : void 0
300
+ );
301
+ let url = config.url;
302
+ if (config.paramsSerializer && config.params) {
303
+ const qs = config.paramsSerializer(config.params);
304
+ if (qs) {
305
+ url = url.includes("?") ? `${url}&${qs}` : `${url}?${qs}`;
296
306
  }
297
- const headers = new Headers(finalConfig.headers);
298
- const fetchInit = {
299
- method: finalConfig.method,
300
- headers
301
- };
302
- if (signal) fetchInit.signal = signal;
303
- if (finalConfig.body !== void 0 && !["GET", "HEAD"].includes(finalConfig.method.toUpperCase())) {
304
- if (typeof finalConfig.body === "string" || isPassthroughBody(finalConfig.body)) {
305
- fetchInit.body = finalConfig.body;
306
- } else {
307
- fetchInit.body = JSON.stringify(finalConfig.body);
308
- if (!headers.has("Content-Type")) {
309
- headers.set("Content-Type", "application/json");
310
- }
307
+ }
308
+ const headers = new Headers(config.headers);
309
+ const fetchInit = {
310
+ method: config.method,
311
+ headers
312
+ };
313
+ if (signal) fetchInit.signal = signal;
314
+ if (config.body !== void 0 && !["GET", "HEAD"].includes(config.method.toUpperCase())) {
315
+ if (typeof config.body === "string" || isPassthroughBody(config.body)) {
316
+ fetchInit.body = config.body;
317
+ } else {
318
+ fetchInit.body = JSON.stringify(config.body);
319
+ if (!headers.has("Content-Type")) {
320
+ headers.set("Content-Type", "application/json");
311
321
  }
312
322
  }
313
- let res;
323
+ }
324
+ let res;
325
+ try {
326
+ res = await fetch(url, fetchInit);
327
+ } catch (e) {
328
+ if (isAbortError(e)) throw e;
329
+ throw new NetworkError(
330
+ e instanceof Error ? e.message : String(e),
331
+ config.route,
332
+ config.level,
333
+ e
334
+ );
335
+ }
336
+ const text = await res.text();
337
+ let data = text;
338
+ const contentType = res.headers.get("content-type") ?? "";
339
+ if (contentType.includes("application/json")) {
314
340
  try {
315
- res = await fetch(url, fetchInit);
316
- } catch (e) {
317
- if (isAbortError(e)) throw e;
318
- throw new NetworkError(
319
- e instanceof Error ? e.message : String(e),
320
- finalConfig.route,
321
- finalConfig.level,
322
- e
323
- );
324
- }
325
- const text = await res.text();
326
- let data = text;
327
- const contentType = res.headers.get("content-type") ?? "";
328
- if (contentType.includes("application/json")) {
329
- try {
330
- data = JSON.parse(text);
331
- } catch {
332
- }
341
+ data = JSON.parse(text);
342
+ } catch {
333
343
  }
334
- const responseData = {
335
- route: finalConfig.route,
336
- level: finalConfig.level,
337
- method: finalConfig.method,
338
- url,
344
+ }
345
+ const responseData = {
346
+ route: config.route,
347
+ level: config.level,
348
+ method: config.method,
349
+ url,
350
+ status: res.status,
351
+ headers: res.headers,
352
+ data,
353
+ config
354
+ };
355
+ if (res.status >= 200 && res.status < 300) {
356
+ return responseData;
357
+ }
358
+ throw new HTTPError(
359
+ `HTTP ${res.status} for route "${config.route}" (${config.method} ${url})`,
360
+ {
361
+ route: config.route,
362
+ level: config.level,
339
363
  status: res.status,
340
- headers: res.headers,
341
- data,
342
- config: finalConfig
343
- };
344
- const source = res.status >= 200 && res.status < 300 ? Promise.resolve(responseData) : Promise.reject(
345
- new HTTPError(
346
- `HTTP ${res.status} for route "${finalConfig.route}" (${finalConfig.method} ${url})`,
347
- {
348
- route: finalConfig.route,
349
- level: finalConfig.level,
350
- status: res.status,
351
- url,
352
- method: finalConfig.method
353
- }
354
- )
355
- );
364
+ url,
365
+ method: config.method
366
+ }
367
+ );
368
+ }
369
+
370
+ // src/adapters/builtin-http.ts
371
+ function createBuiltinHttp(forgeInterceptors) {
372
+ const requestMgr = forgeInterceptors?.request ?? createInterceptorManager();
373
+ const responseMgr = forgeInterceptors?.response ?? createInterceptorManager();
374
+ const requestRaw = rawFetch;
375
+ async function request(config) {
376
+ const finalConfig = await runRequestInterceptors(requestMgr, config);
377
+ const source = Promise.resolve(finalConfig).then(requestRaw);
356
378
  return runResponseInterceptors(
357
379
  responseMgr,
358
380
  source
@@ -365,6 +387,7 @@ var RouteForge = (function (exports) {
365
387
  const del = (url, config) => request({ ...config, url, method: "DELETE" });
366
388
  return {
367
389
  request,
390
+ requestRaw,
368
391
  interceptors: {
369
392
  request: requestMgr,
370
393
  response: responseMgr
@@ -576,7 +599,6 @@ var RouteForge = (function (exports) {
576
599
  const loadingTracker = new LoadingTracker();
577
600
  const explicitLevels = options.levels;
578
601
  const explicitEager = options.eager;
579
- const explicitStrict = options.strict ?? false;
580
602
  const explicitEndpoint = options.endpoint;
581
603
  let effectiveLevels = explicitLevels ?? [];
582
604
  let effectiveEager = explicitEager ?? [];
@@ -584,17 +606,12 @@ var RouteForge = (function (exports) {
584
606
  let effectiveUrlPrefix = "";
585
607
  let summaryUnassigned;
586
608
  let backendHasUnassignedLevel = false;
587
- const summaryPromise = (async () => {
609
+ const fetchSummary = async () => {
588
610
  try {
589
- const summaryUrl = explicitEndpoint;
590
- const resp = await fetch(summaryUrl, { method: "GET" });
591
- if (!resp.ok) {
592
- console.warn(
593
- `[route-forge] summary endpoint ${summaryUrl} unreachable (HTTP ${resp.status}); falling back to explicit options`
594
- );
595
- return null;
596
- }
597
- return await resp.json();
611
+ const base = baseURL.endsWith("/") ? baseURL.slice(0, -1) : baseURL;
612
+ const ep = explicitEndpoint.startsWith("/") ? explicitEndpoint : `/${explicitEndpoint}`;
613
+ const data = await fetchMeta("__forge__.summary", `${base}${ep}`);
614
+ return data;
598
615
  } catch (e) {
599
616
  if (explicitLevels && explicitLevels.length > 0) {
600
617
  console.warn(
@@ -604,8 +621,11 @@ var RouteForge = (function (exports) {
604
621
  }
605
622
  throw new UnknownLevelError("(auto-discovery)");
606
623
  }
607
- })();
608
- const autoDiscoveryPromise = summaryPromise.then((summary) => {
624
+ };
625
+ let summaryPromise;
626
+ const summaryWaiters = [];
627
+ const whenSummary = () => summaryPromise ?? new Promise((resolve) => summaryWaiters.push(resolve));
628
+ const autoDiscoveryPromise = whenSummary().then((summary) => {
609
629
  if (summary === null) {
610
630
  return;
611
631
  }
@@ -624,11 +644,6 @@ var RouteForge = (function (exports) {
624
644
  if (summary.config.url_prefix) {
625
645
  effectiveUrlPrefix = summary.config.url_prefix.endsWith("/") ? summary.config.url_prefix.slice(0, -1) : summary.config.url_prefix;
626
646
  }
627
- if (summary.config.strict_mode && !explicitStrict) {
628
- console.warn(
629
- "[route-forge] backend strict_mode=true overrides frontend strict=false; forcing strict=true"
630
- );
631
- }
632
647
  const backendLevels = Object.keys(summary.levels);
633
648
  backendHasUnassignedLevel = backendLevels.includes(UNASSIGNED_LEVEL);
634
649
  if (Array.isArray(summary.unassigned) && summary.unassigned.length > 0 && !backendHasUnassignedLevel) {
@@ -664,8 +679,12 @@ var RouteForge = (function (exports) {
664
679
  });
665
680
  let autoDiscoveryCompleted = false;
666
681
  let resolveReady;
667
- const readyPromise = new Promise((resolve) => {
682
+ let rejectReady;
683
+ const readyPromise = new Promise((resolve, reject) => {
668
684
  resolveReady = resolve;
685
+ rejectReady = reject;
686
+ });
687
+ readyPromise.catch(() => {
669
688
  });
670
689
  const cacheTtl = cacheOpts.ttl ?? DEFAULT_CACHE_TTL;
671
690
  const cacheStorage = cacheOpts.storage ?? "memory";
@@ -696,13 +715,20 @@ var RouteForge = (function (exports) {
696
715
  adapter,
697
716
  forgeInterceptors: { request: requestInterceptors, response: responseInterceptors }
698
717
  });
718
+ Promise.resolve().then(() => {
719
+ summaryPromise = fetchSummary();
720
+ for (const resolve of summaryWaiters) resolve(summaryPromise);
721
+ });
699
722
  let adapterResolved = false;
700
723
  let adapterObj = null;
701
724
  async function ensureAdapter() {
702
725
  if (!adapterResolved) {
703
726
  adapterObj = await adapterPromise.catch((e) => {
704
727
  if (e instanceof AdapterNotFoundError) throw e;
705
- return resolveAdapter({ adapter: "builtin" });
728
+ return resolveAdapter({
729
+ adapter: "builtin",
730
+ forgeInterceptors: { request: requestInterceptors, response: responseInterceptors }
731
+ });
706
732
  });
707
733
  adapterResolved = true;
708
734
  }
@@ -728,33 +754,36 @@ var RouteForge = (function (exports) {
728
754
  const ep = effectiveEndpoint.startsWith("/") ? effectiveEndpoint : `/${effectiveEndpoint}`;
729
755
  return `${base}${ep}/${encodeURIComponent(level)}`;
730
756
  }
731
- async function fetchLevel(level) {
757
+ async function fetchMeta(routeTag, url, level = "") {
732
758
  const adp = await ensureAdapter();
733
759
  const config = {
734
- route: `__forge__.load.${level}`,
760
+ route: routeTag,
735
761
  level,
736
762
  method: "GET",
737
- url: buildUrl(level),
763
+ url,
738
764
  headers: { Accept: "application/json" },
739
765
  params: {},
740
766
  timeout,
741
767
  meta: {
742
- name: `__forge__.load.${level}`,
743
- uri: buildUrl(level),
768
+ name: routeTag,
769
+ uri: url,
744
770
  methods: ["GET"],
745
771
  parameters: [],
746
772
  level
747
773
  }
748
774
  };
749
- const resp = await adp.request(config);
775
+ const doRawRequest = adp.requestRaw ?? adp.request;
776
+ const resp = await doRawRequest(config);
750
777
  if (!resp || resp.status < 200 || resp.status >= 300) {
751
778
  throw new HTTPError(
752
- `Failed to load level "${level}": HTTP ${resp?.status}`,
753
- { level, status: resp?.status, url: buildUrl(level), method: "GET" }
779
+ `Failed to fetch "${routeTag}": HTTP ${resp?.status}`,
780
+ { level, status: resp?.status, url, method: "GET" }
754
781
  );
755
782
  }
756
- const data = resp.data;
757
- return data;
783
+ return resp.data;
784
+ }
785
+ async function fetchLevel(level) {
786
+ return await fetchMeta(`__forge__.load.${level}`, buildUrl(level), level);
758
787
  }
759
788
  async function loadOne(level) {
760
789
  if (autoDiscoveryError) throw autoDiscoveryError;
@@ -926,7 +955,7 @@ var RouteForge = (function (exports) {
926
955
  },
927
956
  (err) => {
928
957
  if (err instanceof ForgeError) throw err;
929
- if (isAbortError2(err, signal)) {
958
+ if (isAbortError(err, signal)) {
930
959
  throw new RequestAbortedError(meta.name, meta.level, err);
931
960
  }
932
961
  throw new NetworkError(
@@ -995,15 +1024,22 @@ var RouteForge = (function (exports) {
995
1024
  }
996
1025
  void autoDiscoveryPromise.then(() => {
997
1026
  autoDiscoveryCompleted = true;
998
- options.onSummaryReady?.();
999
1027
  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}`);
1028
+ return Promise.allSettled(effectiveEager.map((lvl) => load(lvl))).then((results) => {
1029
+ results.forEach((r, i) => {
1030
+ if (r.status === "rejected") {
1031
+ console.error(
1032
+ `[route-forge] eager load failed for level "${effectiveEager[i]}":`,
1033
+ r.reason
1034
+ );
1035
+ }
1036
+ });
1002
1037
  });
1003
1038
  }
1004
1039
  }).then(() => {
1005
1040
  resolveReady(forgeInstance);
1006
- }).catch(() => {
1041
+ }).catch((e) => {
1042
+ rejectReady(e);
1007
1043
  });
1008
1044
  function ready(onFulfilled, onRejected) {
1009
1045
  if (onFulfilled) {
@@ -1014,7 +1050,8 @@ var RouteForge = (function (exports) {
1014
1050
  }
1015
1051
  const forgeResolver = { load, hasRoute };
1016
1052
  function createBoundForge(level, prefix) {
1017
- const levelLoadedPromise = load(level).catch(() => {
1053
+ const levelLoadedPromise = load(level);
1054
+ levelLoadedPromise.catch(() => {
1018
1055
  });
1019
1056
  const apiFn = prefix ? (name, params) => resolveRouteName(forgeResolver, level, prefix, name).then(
1020
1057
  (resolved) => api(level, resolved, params)
@@ -1148,14 +1185,6 @@ var RouteForge = (function (exports) {
1148
1185
  }
1149
1186
  return { pathParams, query, body, headers, timeout: perCallTimeout };
1150
1187
  }
1151
- function isAbortError2(err, signal) {
1152
- if (signal?.aborted) return true;
1153
- const e = err;
1154
- if (!e) return false;
1155
- if (e.name === "AbortError") return true;
1156
- if (e.code === "ERR_CANCELED") return true;
1157
- return false;
1158
- }
1159
1188
 
1160
1189
  exports.AdapterNotFoundError = AdapterNotFoundError;
1161
1190
  exports.ForgeError = ForgeError;