@route-forge/core 2.2.1 → 3.1.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.cjs CHANGED
@@ -131,6 +131,36 @@ var RouteCache = class {
131
131
  }
132
132
  };
133
133
 
134
+ // src/interceptors/manager.ts
135
+ var InterceptorManagerImpl = class {
136
+ constructor() {
137
+ this.handlers = [];
138
+ this.nextId = 0;
139
+ }
140
+ use(onFulfilled, onRejected) {
141
+ const id = this.nextId++;
142
+ this.handlers.push({ id, onFulfilled, onRejected });
143
+ return id;
144
+ }
145
+ eject(id) {
146
+ const idx = this.handlers.findIndex((h) => h.id === id);
147
+ if (idx >= 0) this.handlers.splice(idx, 1);
148
+ }
149
+ clear() {
150
+ this.handlers = [];
151
+ }
152
+ forEach(fn) {
153
+ for (const h of this.handlers) fn(h);
154
+ }
155
+ /** 测试用:当前注册数量 */
156
+ get size() {
157
+ return this.handlers.length;
158
+ }
159
+ };
160
+ function createInterceptorManager() {
161
+ return new InterceptorManagerImpl();
162
+ }
163
+
134
164
  // src/errors.ts
135
165
  var ForgeError = class extends Error {
136
166
  constructor(message, opts) {
@@ -143,30 +173,56 @@ var ForgeError = class extends Error {
143
173
  if (opts.cause !== void 0) this.cause = opts.cause;
144
174
  }
145
175
  };
176
+ function formatCandidates(candidates) {
177
+ if (!candidates || candidates.length === 0) return "";
178
+ const MAX = 5;
179
+ const shown = candidates.slice(0, MAX).join(", ");
180
+ const more = candidates.length > MAX ? ` (+${candidates.length - MAX} more)` : "";
181
+ return ` Available: ${shown}${more}`;
182
+ }
146
183
  var UnknownRouteError = class extends ForgeError {
147
- constructor(route, level) {
148
- super(`Route "${route}" not found${level ? ` in level "${level}"` : ""}`, {
149
- code: "RF_FE_001",
150
- route,
151
- level
152
- });
184
+ constructor(route, level, candidates) {
185
+ super(
186
+ `Route "${route}" not found${level ? ` in level "${level}"` : ""}.${formatCandidates(candidates)}`,
187
+ {
188
+ code: "RF_FE_001",
189
+ route,
190
+ level,
191
+ context: candidates && candidates.length > 0 ? { candidates } : void 0
192
+ }
193
+ );
153
194
  }
154
195
  };
155
196
  var UnknownLevelError = class extends ForgeError {
156
- constructor(level) {
157
- super(`Level "${level}" not declared in options.levels`, {
158
- code: "RF_FE_002",
159
- level
160
- });
197
+ constructor(level, candidates) {
198
+ super(
199
+ `Level "${level}" not declared in options.levels.${formatCandidates(candidates)}`,
200
+ {
201
+ code: "RF_FE_002",
202
+ level,
203
+ context: candidates && candidates.length > 0 ? { candidates } : void 0
204
+ }
205
+ );
161
206
  }
162
207
  };
163
208
  var MissingRouteParamError = class extends ForgeError {
164
- constructor(route, missingParams) {
165
- super(`Missing path parameter(s) ${missingParams.join(", ")} for route "${route}"`, {
166
- code: "RF_FE_003",
167
- route,
168
- context: { missingParams }
169
- });
209
+ constructor(route, missingParams, uri) {
210
+ super(
211
+ `Missing path parameter(s) ${missingParams.join(", ")} for route "${route}"${uri ? ` (${uri})` : ""}`,
212
+ {
213
+ code: "RF_FE_003",
214
+ route,
215
+ context: { missingParams, ...uri !== void 0 ? { uri } : {} }
216
+ }
217
+ );
218
+ }
219
+ };
220
+ var InvalidPathParamError = class extends ForgeError {
221
+ constructor(route, param, value) {
222
+ super(
223
+ `Path parameter "${param}" must be a primitive value (string, number, boolean), got ${typeof value}`,
224
+ { code: "RF_FE_003", route, context: { param, value } }
225
+ );
170
226
  }
171
227
  };
172
228
  var AdapterNotFoundError = class extends ForgeError {
@@ -199,6 +255,7 @@ var HTTPError = class extends ForgeError {
199
255
  context: { status: opts.status, url: opts.url, method: opts.method },
200
256
  cause: opts.cause
201
257
  });
258
+ if (opts.response !== void 0) this.response = opts.response;
202
259
  }
203
260
  };
204
261
  var RequestAbortedError = class extends ForgeError {
@@ -211,33 +268,16 @@ var RequestAbortedError = class extends ForgeError {
211
268
  });
212
269
  }
213
270
  };
214
-
215
- // src/interceptors.ts
216
- var InterceptorManagerImpl = class {
271
+ var DiscoveryNotReadyError = class extends ForgeError {
217
272
  constructor() {
218
- this.handlers = [];
219
- this.nextId = 0;
220
- }
221
- use(onFulfilled, onRejected) {
222
- const id = this.nextId++;
223
- this.handlers.push({ id, onFulfilled, onRejected });
224
- return id;
225
- }
226
- eject(id) {
227
- const idx = this.handlers.findIndex((h) => h.id === id);
228
- if (idx >= 0) this.handlers.splice(idx, 1);
229
- }
230
- clear() {
231
- this.handlers = [];
232
- }
233
- forEach(fn) {
234
- for (const h of this.handlers) fn(h);
235
- }
236
- /** 测试用:当前注册数量 */
237
- get size() {
238
- return this.handlers.length;
273
+ super(
274
+ "Route data not available. Auto-discovery has not completed. Use forge.ready() or forge.use(level) first, or await ready() before calling route()/hasRoute().",
275
+ { code: "RF_FE_010" }
276
+ );
239
277
  }
240
278
  };
279
+
280
+ // src/interceptors/runner.ts
241
281
  async function runRequestInterceptors(manager, initial) {
242
282
  const handlers = [];
243
283
  manager.forEach((h) => handlers.push(h));
@@ -274,8 +314,44 @@ async function runResponseInterceptors(manager, source) {
274
314
  }
275
315
  return p;
276
316
  }
277
- function createInterceptorManager() {
278
- return new InterceptorManagerImpl();
317
+
318
+ // src/interceptors/normalize.ts
319
+ function normalizeInterceptorDeclaration(value) {
320
+ if (value === null || value === void 0) return {};
321
+ let rawResolve;
322
+ let rawReject;
323
+ if (typeof value === "function") {
324
+ rawResolve = value;
325
+ } else if (Array.isArray(value)) {
326
+ rawResolve = value[0];
327
+ rawReject = value[1];
328
+ } else if (typeof value === "object") {
329
+ const obj = value;
330
+ rawResolve = obj.resolve;
331
+ rawReject = obj.reject;
332
+ } else {
333
+ throw new TypeError(
334
+ `Interceptor declaration must be a function, a [resolve, reject] tuple, or a { resolve, reject } object; received ${typeof value}.`
335
+ );
336
+ }
337
+ const handler = {};
338
+ if (rawResolve !== void 0 && rawResolve !== null) {
339
+ if (typeof rawResolve !== "function") {
340
+ throw new TypeError(
341
+ `Interceptor "resolve" (onFulfilled) must be a function; received ${typeof rawResolve}.`
342
+ );
343
+ }
344
+ handler.onFulfilled = rawResolve;
345
+ }
346
+ if (rawReject !== void 0 && rawReject !== null) {
347
+ if (typeof rawReject !== "function") {
348
+ throw new TypeError(
349
+ `Interceptor "reject" (onRejected) must be a function; received ${typeof rawReject}.`
350
+ );
351
+ }
352
+ handler.onRejected = rawReject;
353
+ }
354
+ return handler;
279
355
  }
280
356
 
281
357
  // src/adapters/fetch-core.ts
@@ -377,7 +453,10 @@ async function rawFetch(config) {
377
453
  level: config.level,
378
454
  status: res.status,
379
455
  url,
380
- method: config.method
456
+ method: config.method,
457
+ // 完整 ResponseData 随错误逐段传递(响应拦截器 onRejected 链 → 最终 catch),
458
+ // 供调用方检查响应体,如 Laravel 422 校验错误 err.response.data.errors
459
+ response: responseData
381
460
  }
382
461
  );
383
462
  }
@@ -506,6 +585,69 @@ async function resolveAdapter(opts) {
506
585
  return createBuiltinHttp(opts.forgeInterceptors);
507
586
  }
508
587
 
588
+ // src/adapter-bootstrap.ts
589
+ function createAdapterBootstrap(deps) {
590
+ const { adapter, requestInterceptors, responseInterceptors, warnings } = deps;
591
+ const adapterPromise = resolveAdapter({
592
+ adapter,
593
+ forgeInterceptors: { request: requestInterceptors, response: responseInterceptors }
594
+ });
595
+ let adapterResolved = false;
596
+ let adapterObj = null;
597
+ return async function ensureAdapter() {
598
+ if (!adapterResolved) {
599
+ adapterObj = await adapterPromise.catch((e) => {
600
+ if (e instanceof AdapterNotFoundError) throw e;
601
+ if (warnings) {
602
+ console.warn(
603
+ `[route-forge] adapter initialization failed (${e?.message ?? String(e)}); falling back to builtin`
604
+ );
605
+ }
606
+ return resolveAdapter({
607
+ adapter: "builtin",
608
+ forgeInterceptors: { request: requestInterceptors, response: responseInterceptors }
609
+ });
610
+ });
611
+ adapterResolved = true;
612
+ }
613
+ return adapterObj;
614
+ };
615
+ }
616
+
617
+ // src/ready-latch.ts
618
+ function createReadyLatch() {
619
+ let resolveReady;
620
+ let rejectReady;
621
+ const readyPromise = new Promise((resolve, reject) => {
622
+ resolveReady = resolve;
623
+ rejectReady = reject;
624
+ });
625
+ let settledValue;
626
+ readyPromise.catch(() => {
627
+ });
628
+ let readySettledOk = false;
629
+ readyPromise.then(
630
+ (value) => {
631
+ settledValue = value;
632
+ readySettledOk = true;
633
+ },
634
+ () => {
635
+ }
636
+ );
637
+ function ready(onFulfilled, onRejected) {
638
+ if (onFulfilled) {
639
+ return readyPromise.then(onFulfilled, onRejected).then(() => settledValue);
640
+ }
641
+ return readyPromise;
642
+ }
643
+ return {
644
+ resolve: (value) => resolveReady(value),
645
+ reject: (reason) => rejectReady(reason),
646
+ isReady: () => readySettledOk,
647
+ ready
648
+ };
649
+ }
650
+
509
651
  // src/loading.ts
510
652
  var LoadingTracker = class {
511
653
  constructor() {
@@ -565,12 +707,107 @@ var LoadingTracker = class {
565
707
  }
566
708
  };
567
709
 
568
- // src/url-builder.ts
710
+ // src/route-change.ts
711
+ var RouteChangeTracker = class {
712
+ constructor() {
713
+ this.subscribers = /* @__PURE__ */ new Set();
714
+ /** 本微任务周期内累积的变更层级(去重) */
715
+ this.pending = /* @__PURE__ */ new Set();
716
+ /** 是否已排定一次微任务 flush */
717
+ this.scheduled = false;
718
+ }
719
+ /** 订阅路由表数据变更;返回取消订阅函数 */
720
+ subscribe(cb) {
721
+ this.subscribers.add(cb);
722
+ return () => {
723
+ this.subscribers.delete(cb);
724
+ };
725
+ }
726
+ /** 记录某层级数据已变更;同一 tick 多次调用合并为一次投递(每层级各投一次) */
727
+ notify(level) {
728
+ if (this.subscribers.size === 0) return;
729
+ this.pending.add(level);
730
+ if (this.scheduled) return;
731
+ this.scheduled = true;
732
+ queueMicrotask(() => this.flush());
733
+ }
734
+ /** 一次性投递本周期累积的所有变更层级;单订阅者抛错不影响其它 */
735
+ flush() {
736
+ this.scheduled = false;
737
+ const levels = [...this.pending];
738
+ this.pending.clear();
739
+ for (const level of levels) {
740
+ for (const cb of this.subscribers) {
741
+ try {
742
+ cb(level);
743
+ } catch {
744
+ }
745
+ }
746
+ }
747
+ }
748
+ };
749
+
750
+ // src/url/utils.ts
751
+ function trimTrailingSlash(s) {
752
+ return s.endsWith("/") ? s.slice(0, -1) : s;
753
+ }
754
+ function withLeadingSlash(s) {
755
+ return s.startsWith("/") ? s : `/${s}`;
756
+ }
757
+ function joinBaseAndPath(base, path) {
758
+ return `${trimTrailingSlash(base)}${withLeadingSlash(path)}`;
759
+ }
760
+
761
+ // src/url/endpoint.ts
569
762
  function buildUrl(level, ctx) {
570
- const base = ctx.baseURL.endsWith("/") ? ctx.baseURL.slice(0, -1) : ctx.baseURL;
571
- const ep = ctx.endpoint.startsWith("/") ? ctx.endpoint : `/${ctx.endpoint}`;
572
- return `${base}${ep}/${encodeURIComponent(level)}`;
763
+ return `${joinBaseAndPath(ctx.baseURL, withLeadingSlash(ctx.endpoint))}/${encodeURIComponent(level)}`;
573
764
  }
765
+
766
+ // src/url/params.ts
767
+ function resolveApiParams(input) {
768
+ const {
769
+ params: explicitParams,
770
+ query: rawQuery,
771
+ body: rawBody,
772
+ headers: rawHeaders,
773
+ timeout: perCallTimeout,
774
+ signal: rawSignal,
775
+ ...flatRest
776
+ } = input;
777
+ const pathParams = explicitParams ? { ...explicitParams } : {};
778
+ for (const [k, v] of Object.entries(flatRest)) {
779
+ if (!(k in pathParams)) {
780
+ pathParams[k] = v;
781
+ }
782
+ }
783
+ let query;
784
+ let body;
785
+ let headers;
786
+ if (rawQuery !== void 0) {
787
+ if (typeof rawQuery === "object" && rawQuery !== null) {
788
+ query = rawQuery;
789
+ } else if (!("query" in pathParams)) {
790
+ pathParams.query = rawQuery;
791
+ }
792
+ }
793
+ if (rawBody !== void 0) {
794
+ if (typeof rawBody !== "string" && typeof rawBody !== "number") {
795
+ body = rawBody;
796
+ } else if (!("body" in pathParams)) {
797
+ pathParams.body = rawBody;
798
+ }
799
+ }
800
+ if (rawHeaders !== void 0) {
801
+ if (typeof rawHeaders === "object" && rawHeaders !== null) {
802
+ headers = rawHeaders;
803
+ } else if (!("headers" in pathParams)) {
804
+ pathParams.headers = rawHeaders;
805
+ }
806
+ }
807
+ return { pathParams, query, body, headers, timeout: perCallTimeout, signal: rawSignal };
808
+ }
809
+
810
+ // src/url/request.ts
574
811
  function buildRequestUrl(meta, params, ctx) {
575
812
  const defaults = meta.parameter_defaults ?? {};
576
813
  const missingRequired = [];
@@ -589,7 +826,7 @@ function buildRequestUrl(meta, params, ctx) {
589
826
  }
590
827
  }
591
828
  if (missingRequired.length > 0) {
592
- throw new MissingRouteParamError(meta.name, missingRequired);
829
+ throw new MissingRouteParamError(meta.name, missingRequired, meta.uri);
593
830
  }
594
831
  let uri = meta.uri.replace(/\{([^{}]+)\}/g, (match, raw) => {
595
832
  const optional = raw.endsWith("?");
@@ -597,10 +834,7 @@ function buildRequestUrl(meta, params, ctx) {
597
834
  if (values[name] !== void 0) {
598
835
  const val = values[name];
599
836
  if (typeof val === "object") {
600
- throw new ForgeError(
601
- `Path parameter "${name}" must be a primitive value (string, number, boolean), got ${typeof val}`,
602
- { code: "RF_FE_003", route: meta.name, context: { param: name, value: val } }
603
- );
837
+ throw new InvalidPathParamError(meta.name, name, val);
604
838
  }
605
839
  return encodeURIComponent(String(val));
606
840
  }
@@ -608,10 +842,10 @@ function buildRequestUrl(meta, params, ctx) {
608
842
  });
609
843
  uri = uri.replace(/\/+/g, "/").replace(/\/$/, "");
610
844
  if (/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(ctx.urlPrefix)) {
611
- const prefix2 = ctx.urlPrefix.endsWith("/") ? ctx.urlPrefix.slice(0, -1) : ctx.urlPrefix;
845
+ const prefix2 = trimTrailingSlash(ctx.urlPrefix);
612
846
  return uri.startsWith("/") ? `${prefix2}${uri}` : `${prefix2}/${uri}`;
613
847
  }
614
- const base = ctx.baseURL.endsWith("/") ? ctx.baseURL.slice(0, -1) : ctx.baseURL;
848
+ const base = trimTrailingSlash(ctx.baseURL);
615
849
  const prefix = ctx.urlPrefix;
616
850
  return uri.startsWith("/") ? `${base}${prefix}${uri}` : `${base}${prefix}/${uri}`;
617
851
  }
@@ -630,67 +864,44 @@ function appendQuery(url, query) {
630
864
  if (!qs) return url;
631
865
  return url.includes("?") ? `${url}&${qs}` : `${url}?${qs}`;
632
866
  }
633
- function resolveApiParams(input) {
634
- const {
635
- params: explicitParams,
636
- query: rawQuery,
637
- body: rawBody,
638
- headers: rawHeaders,
639
- timeout: perCallTimeout,
640
- ...flatRest
641
- } = input;
642
- const pathParams = explicitParams ? { ...explicitParams } : {};
643
- for (const [k, v] of Object.entries(flatRest)) {
644
- if (!(k in pathParams)) {
645
- pathParams[k] = v;
646
- }
647
- }
648
- let query;
649
- let body;
650
- let headers;
651
- if (rawQuery !== void 0) {
652
- if (typeof rawQuery === "object" && rawQuery !== null) {
653
- query = rawQuery;
654
- } else if (!("query" in pathParams)) {
655
- pathParams.query = rawQuery;
656
- }
657
- }
658
- if (rawBody !== void 0) {
659
- if (typeof rawBody !== "string" && typeof rawBody !== "number") {
660
- body = rawBody;
661
- } else if (!("body" in pathParams)) {
662
- pathParams.body = rawBody;
663
- }
664
- }
665
- if (rawHeaders !== void 0) {
666
- if (typeof rawHeaders === "object" && rawHeaders !== null) {
667
- headers = rawHeaders;
668
- } else if (!("headers" in pathParams)) {
669
- pathParams.headers = rawHeaders;
867
+ var RESERVED_API_KEYS = ["params", "query", "body", "headers", "timeout", "signal"];
868
+ function buildRouteUrl(meta, params, ctx) {
869
+ if (params) {
870
+ const hasReservedKey = RESERVED_API_KEYS.some((k) => k in params);
871
+ if (!hasReservedKey) {
872
+ return buildRequestUrl(meta, params, ctx);
670
873
  }
874
+ } else {
875
+ return buildRequestUrl(meta, {}, ctx);
671
876
  }
672
- return { pathParams, query, body, headers, timeout: perCallTimeout };
877
+ const { pathParams, query } = resolveApiParams(params);
878
+ return appendQuery(buildRequestUrl(meta, pathParams, ctx), query);
673
879
  }
674
880
 
675
881
  // src/auto-discovery.ts
882
+ var DEFAULT_ENDPOINT = "/_forge/routes";
676
883
  async function fetchSummary(inputs, baseURL, fetchMeta) {
677
- const { explicitLevels, explicitEndpoint } = inputs;
678
- if (!explicitEndpoint) {
679
- throw new UnknownLevelError("(auto-discovery)");
680
- }
884
+ const { explicitLevels, explicitEndpoint, warnings } = inputs;
885
+ const endpoint = explicitEndpoint ?? DEFAULT_ENDPOINT;
886
+ const url = joinBaseAndPath(baseURL, endpoint);
681
887
  try {
682
- const base = baseURL.endsWith("/") ? baseURL.slice(0, -1) : baseURL;
683
- const ep = explicitEndpoint.startsWith("/") ? explicitEndpoint : `/${explicitEndpoint}`;
684
- const data = await fetchMeta("__forge__.summary", `${base}${ep}`);
888
+ const data = await fetchMeta("__forge__.summary", url);
685
889
  return data;
686
890
  } catch (e) {
687
891
  if (explicitLevels && explicitLevels.length > 0) {
688
- console.warn(
689
- `[route-forge] summary endpoint unreachable: ${e.message}; using explicit levels`
690
- );
892
+ if (warnings) {
893
+ console.warn(
894
+ `[route-forge] summary endpoint unreachable: ${e.message}; using explicit levels`
895
+ );
896
+ }
691
897
  return null;
692
898
  }
693
- throw new UnknownLevelError("(auto-discovery)");
899
+ throw new NetworkError(
900
+ `Failed to fetch route summary from "${url}": ${e?.message ?? String(e)}; check the backend manifest endpoint or options.endpoint`,
901
+ void 0,
902
+ void 0,
903
+ e
904
+ );
694
905
  }
695
906
  }
696
907
  function normalizeCacheTtl(raw) {
@@ -698,15 +909,15 @@ function normalizeCacheTtl(raw) {
698
909
  return raw;
699
910
  }
700
911
  function applySummaryToState(summary, state, inputs) {
701
- const { explicitLevels, explicitEager, explicitEndpoint } = inputs;
912
+ const { explicitLevels, explicitEager, explicitEndpoint, warnings } = inputs;
702
913
  const schemeVersion = summary.schemeVersion ?? 1;
703
- if (schemeVersion > 1) {
914
+ if (schemeVersion > 1 && warnings) {
704
915
  console.warn(
705
916
  `[route-forge] backend schemeVersion=${schemeVersion} > client supported 1; some features may be unavailable`
706
917
  );
707
918
  }
708
919
  if (summary.config.endpoint_prefix && summary.config.endpoint_prefix !== explicitEndpoint) {
709
- if (explicitEndpoint) {
920
+ if (explicitEndpoint && warnings) {
710
921
  console.warn(
711
922
  `[route-forge] backend endpoint_prefix "${summary.config.endpoint_prefix}" overrides frontend endpoint "${explicitEndpoint}"`
712
923
  );
@@ -729,7 +940,7 @@ function applySummaryToState(summary, state, inputs) {
729
940
  if (explicitLevels && explicitLevels.length > 0) {
730
941
  const intersection = explicitLevels.filter((l) => backendLevels.includes(l));
731
942
  const removed = explicitLevels.filter((l) => !backendLevels.includes(l));
732
- if (removed.length > 0) {
943
+ if (removed.length > 0 && warnings) {
733
944
  console.warn(
734
945
  `[route-forge] levels not in backend summary and dropped: ${removed.join(", ")}`
735
946
  );
@@ -778,23 +989,18 @@ var RouteStore = class {
778
989
  this.fetchMeta = deps.fetchMeta;
779
990
  this.autoDiscoveryPromise = deps.autoDiscoveryPromise;
780
991
  this.getAutoDiscoveryError = deps.getAutoDiscoveryError;
992
+ this.onChange = deps.onChange;
781
993
  }
782
994
  assertLevelDeclared(level) {
783
995
  if (!this.state.levels.includes(level)) {
784
- throw new UnknownLevelError(level);
996
+ throw new UnknownLevelError(level, this.state.levels);
785
997
  }
786
998
  }
787
999
  async fetchLevel(level) {
788
1000
  const uri = this.state.levelRoutes[level]?.uri;
789
- const url = uri ? this.joinBaseAndPath(this.baseURL, uri) : buildUrl(level, { baseURL: this.baseURL, endpoint: this.state.endpoint });
1001
+ const url = uri ? joinBaseAndPath(this.baseURL, uri) : buildUrl(level, { baseURL: this.baseURL, endpoint: this.state.endpoint });
790
1002
  return await this.fetchMeta(`route-forge.${level}`, url, level);
791
1003
  }
792
- /** baseURL 与后端下发的绝对 path 拼接(规范化斜杠),与 buildUrl 的 base 处理一致 */
793
- joinBaseAndPath(baseURL, path) {
794
- const base = baseURL.endsWith("/") ? baseURL.slice(0, -1) : baseURL;
795
- const p = path.startsWith("/") ? path : `/${path}`;
796
- return `${base}${p}`;
797
- }
798
1004
  async loadOne(level) {
799
1005
  const autoDiscoveryError = this.getAutoDiscoveryError();
800
1006
  if (autoDiscoveryError) throw autoDiscoveryError;
@@ -809,6 +1015,7 @@ var RouteStore = class {
809
1015
  const resp = await this.fetchLevel(level);
810
1016
  if ((this.invalidationGens.get(level) ?? 0) === gen) {
811
1017
  this.cache.set(resp, this.state.cacheTtl);
1018
+ this.onChange?.(level);
812
1019
  }
813
1020
  } finally {
814
1021
  this.inflight.delete(level);
@@ -822,6 +1029,36 @@ var RouteStore = class {
822
1029
  const list = Array.isArray(level) ? level : [level];
823
1030
  await Promise.all(list.map((l) => this.loadOne(l)));
824
1031
  }
1032
+ /**
1033
+ * 强制刷新:跳过缓存命中短路重新拉取指定层级,成功后覆盖缓存、失败保留旧值。
1034
+ * 与 invalidate→load 的区别——全程不清空缓存,故读取旧数据不受影响、无空窗。
1035
+ */
1036
+ async revalidate(level) {
1037
+ await this.autoDiscoveryPromise;
1038
+ const list = Array.isArray(level) ? level : [level];
1039
+ await Promise.all(list.map((l) => this.revalidateOne(l)));
1040
+ }
1041
+ async revalidateOne(level) {
1042
+ const autoDiscoveryError = this.getAutoDiscoveryError();
1043
+ if (autoDiscoveryError) throw autoDiscoveryError;
1044
+ this.assertLevelDeclared(level);
1045
+ const existing = this.inflight.get(level);
1046
+ if (existing) return existing;
1047
+ const gen = this.invalidationGens.get(level) ?? 0;
1048
+ const p = (async () => {
1049
+ try {
1050
+ const resp = await this.fetchLevel(level);
1051
+ if ((this.invalidationGens.get(level) ?? 0) === gen) {
1052
+ this.cache.set(resp, this.state.cacheTtl);
1053
+ this.onChange?.(level);
1054
+ }
1055
+ } finally {
1056
+ this.inflight.delete(level);
1057
+ }
1058
+ })();
1059
+ this.inflight.set(level, p);
1060
+ return p;
1061
+ }
825
1062
  invalidate(level) {
826
1063
  if (level === void 0) {
827
1064
  this.cache.clear();
@@ -829,16 +1066,19 @@ var RouteStore = class {
829
1066
  for (const lvl of this.state.levels) {
830
1067
  this.invalidationGens.set(lvl, (this.invalidationGens.get(lvl) ?? 0) + 1);
831
1068
  }
1069
+ for (const lvl of this.state.levels) this.onChange?.(lvl);
832
1070
  } else if (Array.isArray(level)) {
833
1071
  for (const lvl of level) {
834
1072
  this.cache.del(lvl);
835
1073
  this.inflight.delete(lvl);
836
1074
  this.invalidationGens.set(lvl, (this.invalidationGens.get(lvl) ?? 0) + 1);
837
1075
  }
1076
+ for (const lvl of level) this.onChange?.(lvl);
838
1077
  } else {
839
1078
  this.cache.del(level);
840
1079
  this.inflight.delete(level);
841
1080
  this.invalidationGens.set(level, (this.invalidationGens.get(level) ?? 0) + 1);
1081
+ this.onChange?.(level);
842
1082
  }
843
1083
  }
844
1084
  isLoaded(level) {
@@ -853,13 +1093,24 @@ var RouteStore = class {
853
1093
  }
854
1094
  return void 0;
855
1095
  }
1096
+ /**
1097
+ * 该层级当前已加载的路由名列表(不深拷贝,仅读键名)。
1098
+ * 供错误候选与 prefix 解析使用——避免为取名字而 getRoutes 深拷贝整表。
1099
+ * 层级未声明抛 UnknownLevelError(与 getRoutes/route 一致)。
1100
+ */
1101
+ routeNames(level) {
1102
+ this.assertLevelDeclared(level);
1103
+ const entry = this.cache.get(level);
1104
+ return entry ? Object.keys(entry.routes) : [];
1105
+ }
856
1106
  getRoutes(level) {
857
1107
  if (level !== void 0) {
1108
+ this.assertLevelDeclared(level);
858
1109
  const entry = this.cache.get(level);
859
1110
  const routes = entry?.routes ?? {};
860
1111
  const result2 = {};
861
1112
  for (const [k, v] of Object.entries(routes)) {
862
- result2[k] = JSON.parse(JSON.stringify(v));
1113
+ result2[k] = structuredClone(v);
863
1114
  }
864
1115
  return result2;
865
1116
  }
@@ -869,7 +1120,7 @@ var RouteStore = class {
869
1120
  if (entry) {
870
1121
  const levelRoutes = {};
871
1122
  for (const [k, v] of Object.entries(entry.routes)) {
872
- levelRoutes[k] = JSON.parse(JSON.stringify(v));
1123
+ levelRoutes[k] = structuredClone(v);
873
1124
  }
874
1125
  result[lvl] = levelRoutes;
875
1126
  }
@@ -886,6 +1137,7 @@ function createHttpRunner(deps) {
886
1137
  responseInterceptors,
887
1138
  load,
888
1139
  findRouteMeta,
1140
+ getRouteNames,
889
1141
  baseURL,
890
1142
  state,
891
1143
  timeout,
@@ -937,7 +1189,10 @@ function createHttpRunner(deps) {
937
1189
  level: resp.level,
938
1190
  status: resp.status,
939
1191
  url: resp.url,
940
- method: resp.method
1192
+ method: resp.method,
1193
+ // 完整 ResponseData 随错误逐段传递(响应拦截器 onRejected 链 → 最终 catch),
1194
+ // 供调用方检查响应体,如 Laravel 422 校验错误 err.response.data.errors
1195
+ response: resp
941
1196
  }
942
1197
  );
943
1198
  }
@@ -966,18 +1221,37 @@ function createHttpRunner(deps) {
966
1221
  let ctrl;
967
1222
  let abortedBeforeInit = false;
968
1223
  let abortReason;
969
- const work = (async () => {
970
- ctrl = new AbortController();
971
- if (abortedBeforeInit) {
972
- ctrl.abort(abortReason);
1224
+ const externalSignal = params.signal;
1225
+ const onExternalAbort = () => {
1226
+ if (ctrl) {
1227
+ ctrl.abort(externalSignal?.reason);
1228
+ } else {
1229
+ abortedBeforeInit = true;
1230
+ abortReason = externalSignal?.reason;
973
1231
  }
974
- await autoDiscoveryPromise;
975
- await load(level);
976
- const meta = findRouteMeta(level, name);
977
- if (!meta) {
978
- throw new UnknownRouteError(name, level);
1232
+ };
1233
+ if (externalSignal?.aborted) {
1234
+ abortedBeforeInit = true;
1235
+ abortReason = externalSignal.reason;
1236
+ } else {
1237
+ externalSignal?.addEventListener("abort", onExternalAbort, { once: true });
1238
+ }
1239
+ const work = (async () => {
1240
+ try {
1241
+ ctrl = new AbortController();
1242
+ if (abortedBeforeInit) {
1243
+ ctrl.abort(abortReason);
1244
+ }
1245
+ await autoDiscoveryPromise;
1246
+ await load(level);
1247
+ const meta = findRouteMeta(level, name);
1248
+ if (!meta) {
1249
+ throw new UnknownRouteError(name, level, getRouteNames(level));
1250
+ }
1251
+ return await doApiCall(meta, params, ctrl.signal);
1252
+ } finally {
1253
+ externalSignal?.removeEventListener("abort", onExternalAbort);
979
1254
  }
980
- return doApiCall(meta, params, ctrl.signal);
981
1255
  })();
982
1256
  const request = work;
983
1257
  request.abort = () => {
@@ -992,22 +1266,31 @@ function createHttpRunner(deps) {
992
1266
  }
993
1267
 
994
1268
  // src/resolveRouteName.ts
1269
+ function stripTrailingSeparator(prefix, separator) {
1270
+ let normalized = prefix;
1271
+ while (normalized.endsWith(separator)) {
1272
+ normalized = normalized.slice(0, -separator.length);
1273
+ }
1274
+ return normalized;
1275
+ }
995
1276
  async function resolveRouteName(forge, level, prefix, suffix, separator = ".") {
996
- if (!suffix) return prefix;
997
- const joined = `${prefix}${separator}${suffix}`;
998
- if (!suffix.startsWith(`${prefix}${separator}`)) return joined;
1277
+ const normalized = stripTrailingSeparator(prefix, separator);
1278
+ if (!suffix) return normalized;
1279
+ const joined = `${normalized}${separator}${suffix}`;
1280
+ if (!suffix.startsWith(`${normalized}${separator}`)) return joined;
999
1281
  await forge.load(level);
1000
1282
  if (forge.hasRoute(level, joined)) return joined;
1001
1283
  if (forge.hasRoute(level, suffix)) return suffix;
1002
- throw new UnknownRouteError(joined, level);
1284
+ throw new UnknownRouteError(joined, level, forge.getRouteNames?.(level));
1003
1285
  }
1004
1286
  function resolveRouteNameSync(forge, level, prefix, suffix, separator = ".") {
1005
- if (!suffix) return prefix;
1006
- const joined = `${prefix}${separator}${suffix}`;
1007
- if (!suffix.startsWith(`${prefix}${separator}`)) return joined;
1287
+ const normalized = stripTrailingSeparator(prefix, separator);
1288
+ if (!suffix) return normalized;
1289
+ const joined = `${normalized}${separator}${suffix}`;
1290
+ if (!suffix.startsWith(`${normalized}${separator}`)) return joined;
1008
1291
  if (forge.hasRoute(level, joined)) return joined;
1009
1292
  if (forge.hasRoute(level, suffix)) return suffix;
1010
- throw new UnknownRouteError(joined, level);
1293
+ throw new UnknownRouteError(joined, level, forge.getRouteNames?.(level));
1011
1294
  }
1012
1295
 
1013
1296
  // src/defineImmutableProps.ts
@@ -1026,8 +1309,8 @@ function defineImmutableProps(target, props) {
1026
1309
 
1027
1310
  // src/bound-forge.ts
1028
1311
  function createBoundForgeFactory(deps) {
1029
- const { load, api, route, hasRoute, getRoutes, invalidate, isLoaded, loadingTracker } = deps;
1030
- const resolver = { load, hasRoute };
1312
+ const { load, api, route, hasRoute, getRoutes, getRouteNames, invalidate, isLoaded, loadingTracker } = deps;
1313
+ const resolver = { load, hasRoute, getRouteNames };
1031
1314
  function createBoundForge(level, prefix) {
1032
1315
  const levelLoadedPromise = load(level);
1033
1316
  levelLoadedPromise.catch(() => {
@@ -1093,11 +1376,6 @@ var DEFAULT_TIMEOUT = 3e4;
1093
1376
  var DEFAULT_CACHE_TTL = 3600;
1094
1377
  function createRouteForge(options = {}) {
1095
1378
  const bootstrapSummary = readEmbeddedSummary() ?? options.summary ?? null;
1096
- if (!bootstrapSummary && !options.endpoint) {
1097
- throw new TypeError(
1098
- "createRouteForge: \u9700\u8981 options.endpoint\uFF0C\u6216 options.summary\uFF0C\u6216\u9875\u9762\u5185\u5D4C window.__ROUTE_FORGE__"
1099
- );
1100
- }
1101
1379
  const {
1102
1380
  adapter = "auto",
1103
1381
  timeout = DEFAULT_TIMEOUT,
@@ -1105,78 +1383,49 @@ function createRouteForge(options = {}) {
1105
1383
  interceptors: declarativeInterceptors,
1106
1384
  cache: cacheOpts = {}
1107
1385
  } = options;
1386
+ const warnings = options.warnings ?? true;
1108
1387
  const loadingTracker = new LoadingTracker();
1388
+ const routesTracker = new RouteChangeTracker();
1109
1389
  const explicitLevels = options.levels;
1110
1390
  const explicitEager = options.eager;
1111
1391
  const explicitEndpoint = options.endpoint;
1112
1392
  const discoveryState = {
1113
1393
  levels: explicitLevels ?? [],
1114
1394
  eager: explicitEager ?? [],
1115
- endpoint: explicitEndpoint ?? bootstrapSummary?.config?.endpoint_prefix ?? "",
1395
+ endpoint: explicitEndpoint ?? bootstrapSummary?.config?.endpoint_prefix ?? DEFAULT_ENDPOINT,
1116
1396
  urlPrefix: "",
1117
1397
  cacheTtl: void 0,
1118
1398
  levelRoutes: {}
1119
1399
  };
1120
- const discoveryInputs = { explicitLevels, explicitEager, explicitEndpoint };
1400
+ const discoveryInputs = { explicitLevels, explicitEager, explicitEndpoint, warnings };
1121
1401
  let autoDiscoveryCompleted = false;
1122
- let resolveReady;
1123
- let rejectReady;
1124
- const readyPromise = new Promise((resolve, reject) => {
1125
- resolveReady = resolve;
1126
- rejectReady = reject;
1127
- });
1128
- readyPromise.catch(() => {
1129
- });
1402
+ const latch = createReadyLatch();
1130
1403
  const cacheTtl = cacheOpts.ttl ?? DEFAULT_CACHE_TTL;
1131
1404
  const cacheStorage = cacheOpts.storage ?? "memory";
1132
1405
  const cache = new RouteCache({ storage: cacheStorage, ttl: cacheTtl });
1133
1406
  const requestInterceptors = new InterceptorManagerImpl();
1134
1407
  const responseInterceptors = new InterceptorManagerImpl();
1135
- if (declarativeInterceptors?.request) {
1136
- for (const entry of declarativeInterceptors.request) {
1137
- if (typeof entry === "function") {
1138
- requestInterceptors.use(entry);
1139
- } else {
1140
- const [onFulfilled, onRejected] = entry;
1141
- requestInterceptors.use(onFulfilled, onRejected);
1142
- }
1143
- }
1408
+ const reqDecl = normalizeInterceptorDeclaration(
1409
+ declarativeInterceptors?.request
1410
+ );
1411
+ if (reqDecl.onFulfilled || reqDecl.onRejected) {
1412
+ requestInterceptors.use(reqDecl.onFulfilled, reqDecl.onRejected);
1144
1413
  }
1145
- if (declarativeInterceptors?.response) {
1146
- for (const entry of declarativeInterceptors.response) {
1147
- if (typeof entry === "function") {
1148
- responseInterceptors.use(entry);
1149
- } else {
1150
- const [onFulfilled, onRejected] = entry;
1151
- responseInterceptors.use(onFulfilled, onRejected);
1152
- }
1153
- }
1414
+ const resDecl = normalizeInterceptorDeclaration(
1415
+ declarativeInterceptors?.response
1416
+ );
1417
+ if (resDecl.onFulfilled || resDecl.onRejected) {
1418
+ responseInterceptors.use(resDecl.onFulfilled, resDecl.onRejected);
1154
1419
  }
1155
- const adapterPromise = resolveAdapter({
1420
+ const ensureAdapter = createAdapterBootstrap({
1156
1421
  adapter,
1157
- forgeInterceptors: { request: requestInterceptors, response: responseInterceptors }
1422
+ requestInterceptors,
1423
+ responseInterceptors,
1424
+ warnings
1158
1425
  });
1159
- let adapterResolved = false;
1160
- let adapterObj = null;
1161
- async function ensureAdapter() {
1162
- if (!adapterResolved) {
1163
- adapterObj = await adapterPromise.catch((e) => {
1164
- if (e instanceof AdapterNotFoundError) throw e;
1165
- return resolveAdapter({
1166
- adapter: "builtin",
1167
- forgeInterceptors: { request: requestInterceptors, response: responseInterceptors }
1168
- });
1169
- });
1170
- adapterResolved = true;
1171
- }
1172
- return adapterObj;
1173
- }
1174
1426
  function assertDiscoveryReady() {
1175
1427
  if (!autoDiscoveryCompleted && !explicitLevels?.length) {
1176
- throw new ForgeError(
1177
- "Route data not available. Auto-discovery has not completed. Use forge.ready() or forge.use(level) first.",
1178
- { code: "RF_FE_010" }
1179
- );
1428
+ throw new DiscoveryNotReadyError();
1180
1429
  }
1181
1430
  }
1182
1431
  async function fetchMeta(routeTag, url, level = "") {
@@ -1201,8 +1450,8 @@ function createRouteForge(options = {}) {
1201
1450
  const resp = await doRawRequest(config);
1202
1451
  if (!resp || resp.status < 200 || resp.status >= 300) {
1203
1452
  throw new HTTPError(
1204
- `Failed to fetch "${routeTag}": HTTP ${resp?.status}`,
1205
- { level, status: resp?.status, url, method: "GET" }
1453
+ `Failed to fetch "${routeTag}" (${url}): HTTP ${resp?.status}`,
1454
+ { level, status: resp?.status, url, method: "GET", response: resp ?? void 0 }
1206
1455
  );
1207
1456
  }
1208
1457
  return resp.data;
@@ -1230,22 +1479,25 @@ function createRouteForge(options = {}) {
1230
1479
  baseURL,
1231
1480
  fetchMeta,
1232
1481
  autoDiscoveryPromise,
1233
- getAutoDiscoveryError: () => autoDiscoveryError
1482
+ getAutoDiscoveryError: () => autoDiscoveryError,
1483
+ onChange: (level) => routesTracker.notify(level)
1234
1484
  });
1235
1485
  const load = (level) => store.load(level);
1486
+ const revalidate = (level) => store.revalidate(level);
1236
1487
  const findRouteMeta = (level, name) => store.findRouteMeta(level, name);
1237
1488
  const invalidate = (level) => store.invalidate(level);
1238
1489
  const isLoaded = (level) => store.isLoaded(level);
1239
- function getRoutes(level) {
1240
- return level === void 0 ? store.getRoutes() : store.getRoutes(level);
1490
+ const getRoutes = store.getRoutes.bind(store);
1491
+ function getLevels() {
1492
+ return [...discoveryState.levels];
1241
1493
  }
1242
1494
  function route(level, name, params) {
1243
1495
  assertDiscoveryReady();
1244
1496
  const meta = findRouteMeta(level, name);
1245
1497
  if (!meta) {
1246
- throw new UnknownRouteError(name, level);
1498
+ throw new UnknownRouteError(name, level, store.routeNames(level));
1247
1499
  }
1248
- return buildRequestUrl(meta, params ?? {}, { baseURL, urlPrefix: discoveryState.urlPrefix });
1500
+ return buildRouteUrl(meta, params ?? {}, { baseURL, urlPrefix: discoveryState.urlPrefix });
1249
1501
  }
1250
1502
  const api = createHttpRunner({
1251
1503
  ensureAdapter,
@@ -1253,6 +1505,7 @@ function createRouteForge(options = {}) {
1253
1505
  responseInterceptors,
1254
1506
  load,
1255
1507
  findRouteMeta,
1508
+ getRouteNames: (lvl) => store.routeNames(lvl),
1256
1509
  baseURL,
1257
1510
  state: discoveryState,
1258
1511
  timeout,
@@ -1278,23 +1531,17 @@ function createRouteForge(options = {}) {
1278
1531
  });
1279
1532
  }
1280
1533
  }).then(() => {
1281
- resolveReady(forgeInstance);
1534
+ latch.resolve(forgeInstance);
1282
1535
  }).catch((e) => {
1283
- rejectReady(e);
1536
+ latch.reject(e);
1284
1537
  });
1285
- function ready(onFulfilled, onRejected) {
1286
- if (onFulfilled) {
1287
- const p = readyPromise.then(onFulfilled, onRejected);
1288
- return p.then(() => forgeInstance);
1289
- }
1290
- return readyPromise;
1291
- }
1292
1538
  const createBoundForgeWithMethods = createBoundForgeFactory({
1293
1539
  load,
1294
1540
  api,
1295
1541
  route,
1296
1542
  hasRoute,
1297
1543
  getRoutes,
1544
+ getRouteNames: (lvl) => store.routeNames(lvl),
1298
1545
  invalidate,
1299
1546
  isLoaded,
1300
1547
  loadingTracker
@@ -1302,19 +1549,24 @@ function createRouteForge(options = {}) {
1302
1549
  const forgeInstance = {
1303
1550
  api,
1304
1551
  load,
1552
+ revalidate,
1305
1553
  route,
1306
1554
  url: route,
1307
1555
  invalidate,
1308
1556
  isLoaded,
1309
1557
  hasRoute,
1310
1558
  getRoutes,
1559
+ getLevels,
1560
+ warnings,
1311
1561
  isLoading: () => loadingTracker.isLoading(),
1562
+ isReady: () => latch.isReady(),
1312
1563
  onLoadingChange: (cb) => loadingTracker.subscribe(cb),
1564
+ onRoutesChange: (cb) => routesTracker.subscribe(cb),
1313
1565
  interceptors: {
1314
1566
  request: requestInterceptors,
1315
1567
  response: responseInterceptors
1316
1568
  },
1317
- ready,
1569
+ ready: latch.ready,
1318
1570
  use(level, prefix) {
1319
1571
  if (level === void 0) return forgeInstance;
1320
1572
  return createBoundForgeWithMethods(level, prefix);
@@ -1324,15 +1576,18 @@ function createRouteForge(options = {}) {
1324
1576
  }
1325
1577
 
1326
1578
  exports.AdapterNotFoundError = AdapterNotFoundError;
1579
+ exports.DiscoveryNotReadyError = DiscoveryNotReadyError;
1327
1580
  exports.ForgeError = ForgeError;
1328
1581
  exports.HTTPError = HTTPError;
1329
1582
  exports.InterceptorManagerImpl = InterceptorManagerImpl;
1330
1583
  exports.InvalidInterceptorReturnError = InvalidInterceptorReturnError;
1584
+ exports.InvalidPathParamError = InvalidPathParamError;
1331
1585
  exports.LoadingTracker = LoadingTracker;
1332
1586
  exports.MissingRouteParamError = MissingRouteParamError;
1333
1587
  exports.NetworkError = NetworkError;
1334
1588
  exports.RequestAbortedError = RequestAbortedError;
1335
1589
  exports.RouteCache = RouteCache;
1590
+ exports.RouteChangeTracker = RouteChangeTracker;
1336
1591
  exports.UnknownLevelError = UnknownLevelError;
1337
1592
  exports.UnknownRouteError = UnknownRouteError;
1338
1593
  exports.createInterceptorManager = createInterceptorManager;