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