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