@route-forge/core 3.0.0 → 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) {
@@ -248,32 +278,7 @@ var RouteForge = (function (exports) {
248
278
  }
249
279
  };
250
280
 
251
- // src/interceptors.ts
252
- var InterceptorManagerImpl = class {
253
- constructor() {
254
- this.handlers = [];
255
- this.nextId = 0;
256
- }
257
- use(onFulfilled, onRejected) {
258
- const id = this.nextId++;
259
- this.handlers.push({ id, onFulfilled, onRejected });
260
- return id;
261
- }
262
- eject(id) {
263
- const idx = this.handlers.findIndex((h) => h.id === id);
264
- if (idx >= 0) this.handlers.splice(idx, 1);
265
- }
266
- clear() {
267
- this.handlers = [];
268
- }
269
- forEach(fn) {
270
- for (const h of this.handlers) fn(h);
271
- }
272
- /** 测试用:当前注册数量 */
273
- get size() {
274
- return this.handlers.length;
275
- }
276
- };
281
+ // src/interceptors/runner.ts
277
282
  async function runRequestInterceptors(manager, initial) {
278
283
  const handlers = [];
279
284
  manager.forEach((h) => handlers.push(h));
@@ -310,9 +315,8 @@ var RouteForge = (function (exports) {
310
315
  }
311
316
  return p;
312
317
  }
313
- function createInterceptorManager() {
314
- return new InterceptorManagerImpl();
315
- }
318
+
319
+ // src/interceptors/normalize.ts
316
320
  function normalizeInterceptorDeclaration(value) {
317
321
  if (value === null || value === void 0) return {};
318
322
  let rawResolve;
@@ -582,6 +586,69 @@ var RouteForge = (function (exports) {
582
586
  return createBuiltinHttp(opts.forgeInterceptors);
583
587
  }
584
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
+
585
652
  // src/loading.ts
586
653
  var LoadingTracker = class {
587
654
  constructor() {
@@ -641,12 +708,107 @@ var RouteForge = (function (exports) {
641
708
  }
642
709
  };
643
710
 
644
- // 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
645
763
  function buildUrl(level, ctx) {
646
- const base = ctx.baseURL.endsWith("/") ? ctx.baseURL.slice(0, -1) : ctx.baseURL;
647
- const ep = ctx.endpoint.startsWith("/") ? ctx.endpoint : `/${ctx.endpoint}`;
648
- return `${base}${ep}/${encodeURIComponent(level)}`;
764
+ return `${joinBaseAndPath(ctx.baseURL, withLeadingSlash(ctx.endpoint))}/${encodeURIComponent(level)}`;
649
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
650
812
  function buildRequestUrl(meta, params, ctx) {
651
813
  const defaults = meta.parameter_defaults ?? {};
652
814
  const missingRequired = [];
@@ -681,10 +843,10 @@ var RouteForge = (function (exports) {
681
843
  });
682
844
  uri = uri.replace(/\/+/g, "/").replace(/\/$/, "");
683
845
  if (/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(ctx.urlPrefix)) {
684
- const prefix2 = ctx.urlPrefix.endsWith("/") ? ctx.urlPrefix.slice(0, -1) : ctx.urlPrefix;
846
+ const prefix2 = trimTrailingSlash(ctx.urlPrefix);
685
847
  return uri.startsWith("/") ? `${prefix2}${uri}` : `${prefix2}/${uri}`;
686
848
  }
687
- const base = ctx.baseURL.endsWith("/") ? ctx.baseURL.slice(0, -1) : ctx.baseURL;
849
+ const base = trimTrailingSlash(ctx.baseURL);
688
850
  const prefix = ctx.urlPrefix;
689
851
  return uri.startsWith("/") ? `${base}${prefix}${uri}` : `${base}${prefix}/${uri}`;
690
852
  }
@@ -703,46 +865,18 @@ var RouteForge = (function (exports) {
703
865
  if (!qs) return url;
704
866
  return url.includes("?") ? `${url}&${qs}` : `${url}?${qs}`;
705
867
  }
706
- function resolveApiParams(input) {
707
- const {
708
- params: explicitParams,
709
- query: rawQuery,
710
- body: rawBody,
711
- headers: rawHeaders,
712
- timeout: perCallTimeout,
713
- ...flatRest
714
- } = input;
715
- const pathParams = explicitParams ? { ...explicitParams } : {};
716
- for (const [k, v] of Object.entries(flatRest)) {
717
- if (!(k in pathParams)) {
718
- pathParams[k] = v;
719
- }
720
- }
721
- let query;
722
- let body;
723
- let headers;
724
- if (rawQuery !== void 0) {
725
- if (typeof rawQuery === "object" && rawQuery !== null) {
726
- query = rawQuery;
727
- } else if (!("query" in pathParams)) {
728
- pathParams.query = rawQuery;
729
- }
730
- }
731
- if (rawBody !== void 0) {
732
- if (typeof rawBody !== "string" && typeof rawBody !== "number") {
733
- body = rawBody;
734
- } else if (!("body" in pathParams)) {
735
- pathParams.body = rawBody;
736
- }
737
- }
738
- if (rawHeaders !== void 0) {
739
- if (typeof rawHeaders === "object" && rawHeaders !== null) {
740
- headers = rawHeaders;
741
- } else if (!("headers" in pathParams)) {
742
- 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);
743
874
  }
875
+ } else {
876
+ return buildRequestUrl(meta, {}, ctx);
744
877
  }
745
- return { pathParams, query, body, headers, timeout: perCallTimeout };
878
+ const { pathParams, query } = resolveApiParams(params);
879
+ return appendQuery(buildRequestUrl(meta, pathParams, ctx), query);
746
880
  }
747
881
 
748
882
  // src/auto-discovery.ts
@@ -750,9 +884,7 @@ var RouteForge = (function (exports) {
750
884
  async function fetchSummary(inputs, baseURL, fetchMeta) {
751
885
  const { explicitLevels, explicitEndpoint, warnings } = inputs;
752
886
  const endpoint = explicitEndpoint ?? DEFAULT_ENDPOINT;
753
- const base = baseURL.endsWith("/") ? baseURL.slice(0, -1) : baseURL;
754
- const ep = endpoint.startsWith("/") ? endpoint : `/${endpoint}`;
755
- const url = `${base}${ep}`;
887
+ const url = joinBaseAndPath(baseURL, endpoint);
756
888
  try {
757
889
  const data = await fetchMeta("__forge__.summary", url);
758
890
  return data;
@@ -858,6 +990,7 @@ var RouteForge = (function (exports) {
858
990
  this.fetchMeta = deps.fetchMeta;
859
991
  this.autoDiscoveryPromise = deps.autoDiscoveryPromise;
860
992
  this.getAutoDiscoveryError = deps.getAutoDiscoveryError;
993
+ this.onChange = deps.onChange;
861
994
  }
862
995
  assertLevelDeclared(level) {
863
996
  if (!this.state.levels.includes(level)) {
@@ -866,15 +999,9 @@ var RouteForge = (function (exports) {
866
999
  }
867
1000
  async fetchLevel(level) {
868
1001
  const uri = this.state.levelRoutes[level]?.uri;
869
- 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 });
870
1003
  return await this.fetchMeta(`route-forge.${level}`, url, level);
871
1004
  }
872
- /** baseURL 与后端下发的绝对 path 拼接(规范化斜杠),与 buildUrl 的 base 处理一致 */
873
- joinBaseAndPath(baseURL, path) {
874
- const base = baseURL.endsWith("/") ? baseURL.slice(0, -1) : baseURL;
875
- const p = path.startsWith("/") ? path : `/${path}`;
876
- return `${base}${p}`;
877
- }
878
1005
  async loadOne(level) {
879
1006
  const autoDiscoveryError = this.getAutoDiscoveryError();
880
1007
  if (autoDiscoveryError) throw autoDiscoveryError;
@@ -889,6 +1016,7 @@ var RouteForge = (function (exports) {
889
1016
  const resp = await this.fetchLevel(level);
890
1017
  if ((this.invalidationGens.get(level) ?? 0) === gen) {
891
1018
  this.cache.set(resp, this.state.cacheTtl);
1019
+ this.onChange?.(level);
892
1020
  }
893
1021
  } finally {
894
1022
  this.inflight.delete(level);
@@ -902,6 +1030,36 @@ var RouteForge = (function (exports) {
902
1030
  const list = Array.isArray(level) ? level : [level];
903
1031
  await Promise.all(list.map((l) => this.loadOne(l)));
904
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
+ }
905
1063
  invalidate(level) {
906
1064
  if (level === void 0) {
907
1065
  this.cache.clear();
@@ -909,16 +1067,19 @@ var RouteForge = (function (exports) {
909
1067
  for (const lvl of this.state.levels) {
910
1068
  this.invalidationGens.set(lvl, (this.invalidationGens.get(lvl) ?? 0) + 1);
911
1069
  }
1070
+ for (const lvl of this.state.levels) this.onChange?.(lvl);
912
1071
  } else if (Array.isArray(level)) {
913
1072
  for (const lvl of level) {
914
1073
  this.cache.del(lvl);
915
1074
  this.inflight.delete(lvl);
916
1075
  this.invalidationGens.set(lvl, (this.invalidationGens.get(lvl) ?? 0) + 1);
917
1076
  }
1077
+ for (const lvl of level) this.onChange?.(lvl);
918
1078
  } else {
919
1079
  this.cache.del(level);
920
1080
  this.inflight.delete(level);
921
1081
  this.invalidationGens.set(level, (this.invalidationGens.get(level) ?? 0) + 1);
1082
+ this.onChange?.(level);
922
1083
  }
923
1084
  }
924
1085
  isLoaded(level) {
@@ -933,6 +1094,16 @@ var RouteForge = (function (exports) {
933
1094
  }
934
1095
  return void 0;
935
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
+ }
936
1107
  getRoutes(level) {
937
1108
  if (level !== void 0) {
938
1109
  this.assertLevelDeclared(level);
@@ -940,7 +1111,7 @@ var RouteForge = (function (exports) {
940
1111
  const routes = entry?.routes ?? {};
941
1112
  const result2 = {};
942
1113
  for (const [k, v] of Object.entries(routes)) {
943
- result2[k] = JSON.parse(JSON.stringify(v));
1114
+ result2[k] = structuredClone(v);
944
1115
  }
945
1116
  return result2;
946
1117
  }
@@ -950,7 +1121,7 @@ var RouteForge = (function (exports) {
950
1121
  if (entry) {
951
1122
  const levelRoutes = {};
952
1123
  for (const [k, v] of Object.entries(entry.routes)) {
953
- levelRoutes[k] = JSON.parse(JSON.stringify(v));
1124
+ levelRoutes[k] = structuredClone(v);
954
1125
  }
955
1126
  result[lvl] = levelRoutes;
956
1127
  }
@@ -1051,18 +1222,37 @@ var RouteForge = (function (exports) {
1051
1222
  let ctrl;
1052
1223
  let abortedBeforeInit = false;
1053
1224
  let abortReason;
1054
- const work = (async () => {
1055
- ctrl = new AbortController();
1056
- if (abortedBeforeInit) {
1057
- 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;
1058
1232
  }
1059
- await autoDiscoveryPromise;
1060
- await load(level);
1061
- const meta = findRouteMeta(level, name);
1062
- if (!meta) {
1063
- throw new UnknownRouteError(name, level, getRouteNames(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);
1064
1255
  }
1065
- return doApiCall(meta, params, ctrl.signal);
1066
1256
  })();
1067
1257
  const request = work;
1068
1258
  request.abort = () => {
@@ -1120,8 +1310,8 @@ var RouteForge = (function (exports) {
1120
1310
 
1121
1311
  // src/bound-forge.ts
1122
1312
  function createBoundForgeFactory(deps) {
1123
- const { load, api, route, hasRoute, getRoutes, invalidate, isLoaded, loadingTracker } = deps;
1124
- const resolver = { load, hasRoute, getRouteNames: (lvl) => Object.keys(getRoutes(lvl)) };
1313
+ const { load, api, route, hasRoute, getRoutes, getRouteNames, invalidate, isLoaded, loadingTracker } = deps;
1314
+ const resolver = { load, hasRoute, getRouteNames };
1125
1315
  function createBoundForge(level, prefix) {
1126
1316
  const levelLoadedPromise = load(level);
1127
1317
  levelLoadedPromise.catch(() => {
@@ -1196,6 +1386,7 @@ var RouteForge = (function (exports) {
1196
1386
  } = options;
1197
1387
  const warnings = options.warnings ?? true;
1198
1388
  const loadingTracker = new LoadingTracker();
1389
+ const routesTracker = new RouteChangeTracker();
1199
1390
  const explicitLevels = options.levels;
1200
1391
  const explicitEager = options.eager;
1201
1392
  const explicitEndpoint = options.endpoint;
@@ -1209,22 +1400,7 @@ var RouteForge = (function (exports) {
1209
1400
  };
1210
1401
  const discoveryInputs = { explicitLevels, explicitEager, explicitEndpoint, warnings };
1211
1402
  let autoDiscoveryCompleted = false;
1212
- let resolveReady;
1213
- let rejectReady;
1214
- const readyPromise = new Promise((resolve, reject) => {
1215
- resolveReady = resolve;
1216
- rejectReady = reject;
1217
- });
1218
- readyPromise.catch(() => {
1219
- });
1220
- let readySettledOk = false;
1221
- readyPromise.then(
1222
- () => {
1223
- readySettledOk = true;
1224
- },
1225
- () => {
1226
- }
1227
- );
1403
+ const latch = createReadyLatch();
1228
1404
  const cacheTtl = cacheOpts.ttl ?? DEFAULT_CACHE_TTL;
1229
1405
  const cacheStorage = cacheOpts.storage ?? "memory";
1230
1406
  const cache = new RouteCache({ storage: cacheStorage, ttl: cacheTtl });
@@ -1242,30 +1418,12 @@ var RouteForge = (function (exports) {
1242
1418
  if (resDecl.onFulfilled || resDecl.onRejected) {
1243
1419
  responseInterceptors.use(resDecl.onFulfilled, resDecl.onRejected);
1244
1420
  }
1245
- const adapterPromise = resolveAdapter({
1421
+ const ensureAdapter = createAdapterBootstrap({
1246
1422
  adapter,
1247
- forgeInterceptors: { request: requestInterceptors, response: responseInterceptors }
1423
+ requestInterceptors,
1424
+ responseInterceptors,
1425
+ warnings
1248
1426
  });
1249
- let adapterResolved = false;
1250
- let adapterObj = null;
1251
- async function ensureAdapter() {
1252
- if (!adapterResolved) {
1253
- adapterObj = await adapterPromise.catch((e) => {
1254
- if (e instanceof AdapterNotFoundError) throw e;
1255
- if (warnings) {
1256
- console.warn(
1257
- `[route-forge] adapter initialization failed (${e?.message ?? String(e)}); falling back to builtin`
1258
- );
1259
- }
1260
- return resolveAdapter({
1261
- adapter: "builtin",
1262
- forgeInterceptors: { request: requestInterceptors, response: responseInterceptors }
1263
- });
1264
- });
1265
- adapterResolved = true;
1266
- }
1267
- return adapterObj;
1268
- }
1269
1427
  function assertDiscoveryReady() {
1270
1428
  if (!autoDiscoveryCompleted && !explicitLevels?.length) {
1271
1429
  throw new DiscoveryNotReadyError();
@@ -1322,15 +1480,15 @@ var RouteForge = (function (exports) {
1322
1480
  baseURL,
1323
1481
  fetchMeta,
1324
1482
  autoDiscoveryPromise,
1325
- getAutoDiscoveryError: () => autoDiscoveryError
1483
+ getAutoDiscoveryError: () => autoDiscoveryError,
1484
+ onChange: (level) => routesTracker.notify(level)
1326
1485
  });
1327
1486
  const load = (level) => store.load(level);
1487
+ const revalidate = (level) => store.revalidate(level);
1328
1488
  const findRouteMeta = (level, name) => store.findRouteMeta(level, name);
1329
1489
  const invalidate = (level) => store.invalidate(level);
1330
1490
  const isLoaded = (level) => store.isLoaded(level);
1331
- function getRoutes(level) {
1332
- return level === void 0 ? store.getRoutes() : store.getRoutes(level);
1333
- }
1491
+ const getRoutes = store.getRoutes.bind(store);
1334
1492
  function getLevels() {
1335
1493
  return [...discoveryState.levels];
1336
1494
  }
@@ -1338,9 +1496,9 @@ var RouteForge = (function (exports) {
1338
1496
  assertDiscoveryReady();
1339
1497
  const meta = findRouteMeta(level, name);
1340
1498
  if (!meta) {
1341
- throw new UnknownRouteError(name, level, Object.keys(store.getRoutes(level)));
1499
+ throw new UnknownRouteError(name, level, store.routeNames(level));
1342
1500
  }
1343
- return buildRequestUrl(meta, params ?? {}, { baseURL, urlPrefix: discoveryState.urlPrefix });
1501
+ return buildRouteUrl(meta, params ?? {}, { baseURL, urlPrefix: discoveryState.urlPrefix });
1344
1502
  }
1345
1503
  const api = createHttpRunner({
1346
1504
  ensureAdapter,
@@ -1348,7 +1506,7 @@ var RouteForge = (function (exports) {
1348
1506
  responseInterceptors,
1349
1507
  load,
1350
1508
  findRouteMeta,
1351
- getRouteNames: (lvl) => Object.keys(store.getRoutes(lvl)),
1509
+ getRouteNames: (lvl) => store.routeNames(lvl),
1352
1510
  baseURL,
1353
1511
  state: discoveryState,
1354
1512
  timeout,
@@ -1374,23 +1532,17 @@ var RouteForge = (function (exports) {
1374
1532
  });
1375
1533
  }
1376
1534
  }).then(() => {
1377
- resolveReady(forgeInstance);
1535
+ latch.resolve(forgeInstance);
1378
1536
  }).catch((e) => {
1379
- rejectReady(e);
1537
+ latch.reject(e);
1380
1538
  });
1381
- function ready(onFulfilled, onRejected) {
1382
- if (onFulfilled) {
1383
- const p = readyPromise.then(onFulfilled, onRejected);
1384
- return p.then(() => forgeInstance);
1385
- }
1386
- return readyPromise;
1387
- }
1388
1539
  const createBoundForgeWithMethods = createBoundForgeFactory({
1389
1540
  load,
1390
1541
  api,
1391
1542
  route,
1392
1543
  hasRoute,
1393
1544
  getRoutes,
1545
+ getRouteNames: (lvl) => store.routeNames(lvl),
1394
1546
  invalidate,
1395
1547
  isLoaded,
1396
1548
  loadingTracker
@@ -1398,6 +1550,7 @@ var RouteForge = (function (exports) {
1398
1550
  const forgeInstance = {
1399
1551
  api,
1400
1552
  load,
1553
+ revalidate,
1401
1554
  route,
1402
1555
  url: route,
1403
1556
  invalidate,
@@ -1407,13 +1560,14 @@ var RouteForge = (function (exports) {
1407
1560
  getLevels,
1408
1561
  warnings,
1409
1562
  isLoading: () => loadingTracker.isLoading(),
1410
- isReady: () => readySettledOk,
1563
+ isReady: () => latch.isReady(),
1411
1564
  onLoadingChange: (cb) => loadingTracker.subscribe(cb),
1565
+ onRoutesChange: (cb) => routesTracker.subscribe(cb),
1412
1566
  interceptors: {
1413
1567
  request: requestInterceptors,
1414
1568
  response: responseInterceptors
1415
1569
  },
1416
- ready,
1570
+ ready: latch.ready,
1417
1571
  use(level, prefix) {
1418
1572
  if (level === void 0) return forgeInstance;
1419
1573
  return createBoundForgeWithMethods(level, prefix);
@@ -1434,6 +1588,7 @@ var RouteForge = (function (exports) {
1434
1588
  exports.NetworkError = NetworkError;
1435
1589
  exports.RequestAbortedError = RequestAbortedError;
1436
1590
  exports.RouteCache = RouteCache;
1591
+ exports.RouteChangeTracker = RouteChangeTracker;
1437
1592
  exports.UnknownLevelError = UnknownLevelError;
1438
1593
  exports.UnknownRouteError = UnknownRouteError;
1439
1594
  exports.createInterceptorManager = createInterceptorManager;