@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.
package/dist/index.cjs CHANGED
@@ -131,6 +131,36 @@ var RouteCache = class {
131
131
  }
132
132
  };
133
133
 
134
+ // src/interceptors/manager.ts
135
+ var InterceptorManagerImpl = class {
136
+ constructor() {
137
+ this.handlers = [];
138
+ this.nextId = 0;
139
+ }
140
+ use(onFulfilled, onRejected) {
141
+ const id = this.nextId++;
142
+ this.handlers.push({ id, onFulfilled, onRejected });
143
+ return id;
144
+ }
145
+ eject(id) {
146
+ const idx = this.handlers.findIndex((h) => h.id === id);
147
+ if (idx >= 0) this.handlers.splice(idx, 1);
148
+ }
149
+ clear() {
150
+ this.handlers = [];
151
+ }
152
+ forEach(fn) {
153
+ for (const h of this.handlers) fn(h);
154
+ }
155
+ /** 测试用:当前注册数量 */
156
+ get size() {
157
+ return this.handlers.length;
158
+ }
159
+ };
160
+ function createInterceptorManager() {
161
+ return new InterceptorManagerImpl();
162
+ }
163
+
134
164
  // src/errors.ts
135
165
  var ForgeError = class extends Error {
136
166
  constructor(message, opts) {
@@ -247,32 +277,7 @@ var DiscoveryNotReadyError = class extends ForgeError {
247
277
  }
248
278
  };
249
279
 
250
- // src/interceptors.ts
251
- var InterceptorManagerImpl = class {
252
- constructor() {
253
- this.handlers = [];
254
- this.nextId = 0;
255
- }
256
- use(onFulfilled, onRejected) {
257
- const id = this.nextId++;
258
- this.handlers.push({ id, onFulfilled, onRejected });
259
- return id;
260
- }
261
- eject(id) {
262
- const idx = this.handlers.findIndex((h) => h.id === id);
263
- if (idx >= 0) this.handlers.splice(idx, 1);
264
- }
265
- clear() {
266
- this.handlers = [];
267
- }
268
- forEach(fn) {
269
- for (const h of this.handlers) fn(h);
270
- }
271
- /** 测试用:当前注册数量 */
272
- get size() {
273
- return this.handlers.length;
274
- }
275
- };
280
+ // src/interceptors/runner.ts
276
281
  async function runRequestInterceptors(manager, initial) {
277
282
  const handlers = [];
278
283
  manager.forEach((h) => handlers.push(h));
@@ -309,9 +314,8 @@ async function runResponseInterceptors(manager, source) {
309
314
  }
310
315
  return p;
311
316
  }
312
- function createInterceptorManager() {
313
- return new InterceptorManagerImpl();
314
- }
317
+
318
+ // src/interceptors/normalize.ts
315
319
  function normalizeInterceptorDeclaration(value) {
316
320
  if (value === null || value === void 0) return {};
317
321
  let rawResolve;
@@ -581,6 +585,69 @@ async function resolveAdapter(opts) {
581
585
  return createBuiltinHttp(opts.forgeInterceptors);
582
586
  }
583
587
 
588
+ // src/adapter-bootstrap.ts
589
+ function createAdapterBootstrap(deps) {
590
+ const { adapter, requestInterceptors, responseInterceptors, warnings } = deps;
591
+ const adapterPromise = resolveAdapter({
592
+ adapter,
593
+ forgeInterceptors: { request: requestInterceptors, response: responseInterceptors }
594
+ });
595
+ let adapterResolved = false;
596
+ let adapterObj = null;
597
+ return async function ensureAdapter() {
598
+ if (!adapterResolved) {
599
+ adapterObj = await adapterPromise.catch((e) => {
600
+ if (e instanceof AdapterNotFoundError) throw e;
601
+ if (warnings) {
602
+ console.warn(
603
+ `[route-forge] adapter initialization failed (${e?.message ?? String(e)}); falling back to builtin`
604
+ );
605
+ }
606
+ return resolveAdapter({
607
+ adapter: "builtin",
608
+ forgeInterceptors: { request: requestInterceptors, response: responseInterceptors }
609
+ });
610
+ });
611
+ adapterResolved = true;
612
+ }
613
+ return adapterObj;
614
+ };
615
+ }
616
+
617
+ // src/ready-latch.ts
618
+ function createReadyLatch() {
619
+ let resolveReady;
620
+ let rejectReady;
621
+ const readyPromise = new Promise((resolve, reject) => {
622
+ resolveReady = resolve;
623
+ rejectReady = reject;
624
+ });
625
+ let settledValue;
626
+ readyPromise.catch(() => {
627
+ });
628
+ let readySettledOk = false;
629
+ readyPromise.then(
630
+ (value) => {
631
+ settledValue = value;
632
+ readySettledOk = true;
633
+ },
634
+ () => {
635
+ }
636
+ );
637
+ function ready(onFulfilled, onRejected) {
638
+ if (onFulfilled) {
639
+ return readyPromise.then(onFulfilled, onRejected).then(() => settledValue);
640
+ }
641
+ return readyPromise;
642
+ }
643
+ return {
644
+ resolve: (value) => resolveReady(value),
645
+ reject: (reason) => rejectReady(reason),
646
+ isReady: () => readySettledOk,
647
+ ready
648
+ };
649
+ }
650
+
584
651
  // src/loading.ts
585
652
  var LoadingTracker = class {
586
653
  constructor() {
@@ -640,12 +707,107 @@ var LoadingTracker = class {
640
707
  }
641
708
  };
642
709
 
643
- // src/url-builder.ts
710
+ // src/route-change.ts
711
+ var RouteChangeTracker = class {
712
+ constructor() {
713
+ this.subscribers = /* @__PURE__ */ new Set();
714
+ /** 本微任务周期内累积的变更层级(去重) */
715
+ this.pending = /* @__PURE__ */ new Set();
716
+ /** 是否已排定一次微任务 flush */
717
+ this.scheduled = false;
718
+ }
719
+ /** 订阅路由表数据变更;返回取消订阅函数 */
720
+ subscribe(cb) {
721
+ this.subscribers.add(cb);
722
+ return () => {
723
+ this.subscribers.delete(cb);
724
+ };
725
+ }
726
+ /** 记录某层级数据已变更;同一 tick 多次调用合并为一次投递(每层级各投一次) */
727
+ notify(level) {
728
+ if (this.subscribers.size === 0) return;
729
+ this.pending.add(level);
730
+ if (this.scheduled) return;
731
+ this.scheduled = true;
732
+ queueMicrotask(() => this.flush());
733
+ }
734
+ /** 一次性投递本周期累积的所有变更层级;单订阅者抛错不影响其它 */
735
+ flush() {
736
+ this.scheduled = false;
737
+ const levels = [...this.pending];
738
+ this.pending.clear();
739
+ for (const level of levels) {
740
+ for (const cb of this.subscribers) {
741
+ try {
742
+ cb(level);
743
+ } catch {
744
+ }
745
+ }
746
+ }
747
+ }
748
+ };
749
+
750
+ // src/url/utils.ts
751
+ function trimTrailingSlash(s) {
752
+ return s.endsWith("/") ? s.slice(0, -1) : s;
753
+ }
754
+ function withLeadingSlash(s) {
755
+ return s.startsWith("/") ? s : `/${s}`;
756
+ }
757
+ function joinBaseAndPath(base, path) {
758
+ return `${trimTrailingSlash(base)}${withLeadingSlash(path)}`;
759
+ }
760
+
761
+ // src/url/endpoint.ts
644
762
  function buildUrl(level, ctx) {
645
- const base = ctx.baseURL.endsWith("/") ? ctx.baseURL.slice(0, -1) : ctx.baseURL;
646
- const ep = ctx.endpoint.startsWith("/") ? ctx.endpoint : `/${ctx.endpoint}`;
647
- return `${base}${ep}/${encodeURIComponent(level)}`;
763
+ return `${joinBaseAndPath(ctx.baseURL, withLeadingSlash(ctx.endpoint))}/${encodeURIComponent(level)}`;
648
764
  }
765
+
766
+ // src/url/params.ts
767
+ function resolveApiParams(input) {
768
+ const {
769
+ params: explicitParams,
770
+ query: rawQuery,
771
+ body: rawBody,
772
+ headers: rawHeaders,
773
+ timeout: perCallTimeout,
774
+ signal: rawSignal,
775
+ ...flatRest
776
+ } = input;
777
+ const pathParams = explicitParams ? { ...explicitParams } : {};
778
+ for (const [k, v] of Object.entries(flatRest)) {
779
+ if (!(k in pathParams)) {
780
+ pathParams[k] = v;
781
+ }
782
+ }
783
+ let query;
784
+ let body;
785
+ let headers;
786
+ if (rawQuery !== void 0) {
787
+ if (typeof rawQuery === "object" && rawQuery !== null) {
788
+ query = rawQuery;
789
+ } else if (!("query" in pathParams)) {
790
+ pathParams.query = rawQuery;
791
+ }
792
+ }
793
+ if (rawBody !== void 0) {
794
+ if (typeof rawBody !== "string" && typeof rawBody !== "number") {
795
+ body = rawBody;
796
+ } else if (!("body" in pathParams)) {
797
+ pathParams.body = rawBody;
798
+ }
799
+ }
800
+ if (rawHeaders !== void 0) {
801
+ if (typeof rawHeaders === "object" && rawHeaders !== null) {
802
+ headers = rawHeaders;
803
+ } else if (!("headers" in pathParams)) {
804
+ pathParams.headers = rawHeaders;
805
+ }
806
+ }
807
+ return { pathParams, query, body, headers, timeout: perCallTimeout, signal: rawSignal };
808
+ }
809
+
810
+ // src/url/request.ts
649
811
  function buildRequestUrl(meta, params, ctx) {
650
812
  const defaults = meta.parameter_defaults ?? {};
651
813
  const missingRequired = [];
@@ -680,10 +842,10 @@ function buildRequestUrl(meta, params, ctx) {
680
842
  });
681
843
  uri = uri.replace(/\/+/g, "/").replace(/\/$/, "");
682
844
  if (/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(ctx.urlPrefix)) {
683
- const prefix2 = ctx.urlPrefix.endsWith("/") ? ctx.urlPrefix.slice(0, -1) : ctx.urlPrefix;
845
+ const prefix2 = trimTrailingSlash(ctx.urlPrefix);
684
846
  return uri.startsWith("/") ? `${prefix2}${uri}` : `${prefix2}/${uri}`;
685
847
  }
686
- const base = ctx.baseURL.endsWith("/") ? ctx.baseURL.slice(0, -1) : ctx.baseURL;
848
+ const base = trimTrailingSlash(ctx.baseURL);
687
849
  const prefix = ctx.urlPrefix;
688
850
  return uri.startsWith("/") ? `${base}${prefix}${uri}` : `${base}${prefix}/${uri}`;
689
851
  }
@@ -702,46 +864,18 @@ function appendQuery(url, query) {
702
864
  if (!qs) return url;
703
865
  return url.includes("?") ? `${url}&${qs}` : `${url}?${qs}`;
704
866
  }
705
- function resolveApiParams(input) {
706
- const {
707
- params: explicitParams,
708
- query: rawQuery,
709
- body: rawBody,
710
- headers: rawHeaders,
711
- timeout: perCallTimeout,
712
- ...flatRest
713
- } = input;
714
- const pathParams = explicitParams ? { ...explicitParams } : {};
715
- for (const [k, v] of Object.entries(flatRest)) {
716
- if (!(k in pathParams)) {
717
- pathParams[k] = v;
718
- }
719
- }
720
- let query;
721
- let body;
722
- let headers;
723
- if (rawQuery !== void 0) {
724
- if (typeof rawQuery === "object" && rawQuery !== null) {
725
- query = rawQuery;
726
- } else if (!("query" in pathParams)) {
727
- pathParams.query = rawQuery;
728
- }
729
- }
730
- if (rawBody !== void 0) {
731
- if (typeof rawBody !== "string" && typeof rawBody !== "number") {
732
- body = rawBody;
733
- } else if (!("body" in pathParams)) {
734
- pathParams.body = rawBody;
735
- }
736
- }
737
- if (rawHeaders !== void 0) {
738
- if (typeof rawHeaders === "object" && rawHeaders !== null) {
739
- headers = rawHeaders;
740
- } else if (!("headers" in pathParams)) {
741
- pathParams.headers = rawHeaders;
867
+ var RESERVED_API_KEYS = ["params", "query", "body", "headers", "timeout", "signal"];
868
+ function buildRouteUrl(meta, params, ctx) {
869
+ if (params) {
870
+ const hasReservedKey = RESERVED_API_KEYS.some((k) => k in params);
871
+ if (!hasReservedKey) {
872
+ return buildRequestUrl(meta, params, ctx);
742
873
  }
874
+ } else {
875
+ return buildRequestUrl(meta, {}, ctx);
743
876
  }
744
- return { pathParams, query, body, headers, timeout: perCallTimeout };
877
+ const { pathParams, query } = resolveApiParams(params);
878
+ return appendQuery(buildRequestUrl(meta, pathParams, ctx), query);
745
879
  }
746
880
 
747
881
  // src/auto-discovery.ts
@@ -749,9 +883,7 @@ var DEFAULT_ENDPOINT = "/_forge/routes";
749
883
  async function fetchSummary(inputs, baseURL, fetchMeta) {
750
884
  const { explicitLevels, explicitEndpoint, warnings } = inputs;
751
885
  const endpoint = explicitEndpoint ?? DEFAULT_ENDPOINT;
752
- const base = baseURL.endsWith("/") ? baseURL.slice(0, -1) : baseURL;
753
- const ep = endpoint.startsWith("/") ? endpoint : `/${endpoint}`;
754
- const url = `${base}${ep}`;
886
+ const url = joinBaseAndPath(baseURL, endpoint);
755
887
  try {
756
888
  const data = await fetchMeta("__forge__.summary", url);
757
889
  return data;
@@ -857,6 +989,7 @@ var RouteStore = class {
857
989
  this.fetchMeta = deps.fetchMeta;
858
990
  this.autoDiscoveryPromise = deps.autoDiscoveryPromise;
859
991
  this.getAutoDiscoveryError = deps.getAutoDiscoveryError;
992
+ this.onChange = deps.onChange;
860
993
  }
861
994
  assertLevelDeclared(level) {
862
995
  if (!this.state.levels.includes(level)) {
@@ -865,15 +998,9 @@ var RouteStore = class {
865
998
  }
866
999
  async fetchLevel(level) {
867
1000
  const uri = this.state.levelRoutes[level]?.uri;
868
- const url = uri ? this.joinBaseAndPath(this.baseURL, uri) : buildUrl(level, { baseURL: this.baseURL, endpoint: this.state.endpoint });
1001
+ const url = uri ? joinBaseAndPath(this.baseURL, uri) : buildUrl(level, { baseURL: this.baseURL, endpoint: this.state.endpoint });
869
1002
  return await this.fetchMeta(`route-forge.${level}`, url, level);
870
1003
  }
871
- /** baseURL 与后端下发的绝对 path 拼接(规范化斜杠),与 buildUrl 的 base 处理一致 */
872
- joinBaseAndPath(baseURL, path) {
873
- const base = baseURL.endsWith("/") ? baseURL.slice(0, -1) : baseURL;
874
- const p = path.startsWith("/") ? path : `/${path}`;
875
- return `${base}${p}`;
876
- }
877
1004
  async loadOne(level) {
878
1005
  const autoDiscoveryError = this.getAutoDiscoveryError();
879
1006
  if (autoDiscoveryError) throw autoDiscoveryError;
@@ -888,6 +1015,7 @@ var RouteStore = class {
888
1015
  const resp = await this.fetchLevel(level);
889
1016
  if ((this.invalidationGens.get(level) ?? 0) === gen) {
890
1017
  this.cache.set(resp, this.state.cacheTtl);
1018
+ this.onChange?.(level);
891
1019
  }
892
1020
  } finally {
893
1021
  this.inflight.delete(level);
@@ -901,6 +1029,36 @@ var RouteStore = class {
901
1029
  const list = Array.isArray(level) ? level : [level];
902
1030
  await Promise.all(list.map((l) => this.loadOne(l)));
903
1031
  }
1032
+ /**
1033
+ * 强制刷新:跳过缓存命中短路重新拉取指定层级,成功后覆盖缓存、失败保留旧值。
1034
+ * 与 invalidate→load 的区别——全程不清空缓存,故读取旧数据不受影响、无空窗。
1035
+ */
1036
+ async revalidate(level) {
1037
+ await this.autoDiscoveryPromise;
1038
+ const list = Array.isArray(level) ? level : [level];
1039
+ await Promise.all(list.map((l) => this.revalidateOne(l)));
1040
+ }
1041
+ async revalidateOne(level) {
1042
+ const autoDiscoveryError = this.getAutoDiscoveryError();
1043
+ if (autoDiscoveryError) throw autoDiscoveryError;
1044
+ this.assertLevelDeclared(level);
1045
+ const existing = this.inflight.get(level);
1046
+ if (existing) return existing;
1047
+ const gen = this.invalidationGens.get(level) ?? 0;
1048
+ const p = (async () => {
1049
+ try {
1050
+ const resp = await this.fetchLevel(level);
1051
+ if ((this.invalidationGens.get(level) ?? 0) === gen) {
1052
+ this.cache.set(resp, this.state.cacheTtl);
1053
+ this.onChange?.(level);
1054
+ }
1055
+ } finally {
1056
+ this.inflight.delete(level);
1057
+ }
1058
+ })();
1059
+ this.inflight.set(level, p);
1060
+ return p;
1061
+ }
904
1062
  invalidate(level) {
905
1063
  if (level === void 0) {
906
1064
  this.cache.clear();
@@ -908,16 +1066,19 @@ var RouteStore = class {
908
1066
  for (const lvl of this.state.levels) {
909
1067
  this.invalidationGens.set(lvl, (this.invalidationGens.get(lvl) ?? 0) + 1);
910
1068
  }
1069
+ for (const lvl of this.state.levels) this.onChange?.(lvl);
911
1070
  } else if (Array.isArray(level)) {
912
1071
  for (const lvl of level) {
913
1072
  this.cache.del(lvl);
914
1073
  this.inflight.delete(lvl);
915
1074
  this.invalidationGens.set(lvl, (this.invalidationGens.get(lvl) ?? 0) + 1);
916
1075
  }
1076
+ for (const lvl of level) this.onChange?.(lvl);
917
1077
  } else {
918
1078
  this.cache.del(level);
919
1079
  this.inflight.delete(level);
920
1080
  this.invalidationGens.set(level, (this.invalidationGens.get(level) ?? 0) + 1);
1081
+ this.onChange?.(level);
921
1082
  }
922
1083
  }
923
1084
  isLoaded(level) {
@@ -932,6 +1093,16 @@ var RouteStore = class {
932
1093
  }
933
1094
  return void 0;
934
1095
  }
1096
+ /**
1097
+ * 该层级当前已加载的路由名列表(不深拷贝,仅读键名)。
1098
+ * 供错误候选与 prefix 解析使用——避免为取名字而 getRoutes 深拷贝整表。
1099
+ * 层级未声明抛 UnknownLevelError(与 getRoutes/route 一致)。
1100
+ */
1101
+ routeNames(level) {
1102
+ this.assertLevelDeclared(level);
1103
+ const entry = this.cache.get(level);
1104
+ return entry ? Object.keys(entry.routes) : [];
1105
+ }
935
1106
  getRoutes(level) {
936
1107
  if (level !== void 0) {
937
1108
  this.assertLevelDeclared(level);
@@ -939,7 +1110,7 @@ var RouteStore = class {
939
1110
  const routes = entry?.routes ?? {};
940
1111
  const result2 = {};
941
1112
  for (const [k, v] of Object.entries(routes)) {
942
- result2[k] = JSON.parse(JSON.stringify(v));
1113
+ result2[k] = structuredClone(v);
943
1114
  }
944
1115
  return result2;
945
1116
  }
@@ -949,7 +1120,7 @@ var RouteStore = class {
949
1120
  if (entry) {
950
1121
  const levelRoutes = {};
951
1122
  for (const [k, v] of Object.entries(entry.routes)) {
952
- levelRoutes[k] = JSON.parse(JSON.stringify(v));
1123
+ levelRoutes[k] = structuredClone(v);
953
1124
  }
954
1125
  result[lvl] = levelRoutes;
955
1126
  }
@@ -1050,18 +1221,37 @@ function createHttpRunner(deps) {
1050
1221
  let ctrl;
1051
1222
  let abortedBeforeInit = false;
1052
1223
  let abortReason;
1053
- const work = (async () => {
1054
- ctrl = new AbortController();
1055
- if (abortedBeforeInit) {
1056
- ctrl.abort(abortReason);
1224
+ const externalSignal = params.signal;
1225
+ const onExternalAbort = () => {
1226
+ if (ctrl) {
1227
+ ctrl.abort(externalSignal?.reason);
1228
+ } else {
1229
+ abortedBeforeInit = true;
1230
+ abortReason = externalSignal?.reason;
1057
1231
  }
1058
- await autoDiscoveryPromise;
1059
- await load(level);
1060
- const meta = findRouteMeta(level, name);
1061
- if (!meta) {
1062
- throw new UnknownRouteError(name, level, getRouteNames(level));
1232
+ };
1233
+ if (externalSignal?.aborted) {
1234
+ abortedBeforeInit = true;
1235
+ abortReason = externalSignal.reason;
1236
+ } else {
1237
+ externalSignal?.addEventListener("abort", onExternalAbort, { once: true });
1238
+ }
1239
+ const work = (async () => {
1240
+ try {
1241
+ ctrl = new AbortController();
1242
+ if (abortedBeforeInit) {
1243
+ ctrl.abort(abortReason);
1244
+ }
1245
+ await autoDiscoveryPromise;
1246
+ await load(level);
1247
+ const meta = findRouteMeta(level, name);
1248
+ if (!meta) {
1249
+ throw new UnknownRouteError(name, level, getRouteNames(level));
1250
+ }
1251
+ return await doApiCall(meta, params, ctrl.signal);
1252
+ } finally {
1253
+ externalSignal?.removeEventListener("abort", onExternalAbort);
1063
1254
  }
1064
- return doApiCall(meta, params, ctrl.signal);
1065
1255
  })();
1066
1256
  const request = work;
1067
1257
  request.abort = () => {
@@ -1119,8 +1309,8 @@ function defineImmutableProps(target, props) {
1119
1309
 
1120
1310
  // src/bound-forge.ts
1121
1311
  function createBoundForgeFactory(deps) {
1122
- const { load, api, route, hasRoute, getRoutes, invalidate, isLoaded, loadingTracker } = deps;
1123
- const resolver = { load, hasRoute, getRouteNames: (lvl) => Object.keys(getRoutes(lvl)) };
1312
+ const { load, api, route, hasRoute, getRoutes, getRouteNames, invalidate, isLoaded, loadingTracker } = deps;
1313
+ const resolver = { load, hasRoute, getRouteNames };
1124
1314
  function createBoundForge(level, prefix) {
1125
1315
  const levelLoadedPromise = load(level);
1126
1316
  levelLoadedPromise.catch(() => {
@@ -1195,6 +1385,7 @@ function createRouteForge(options = {}) {
1195
1385
  } = options;
1196
1386
  const warnings = options.warnings ?? true;
1197
1387
  const loadingTracker = new LoadingTracker();
1388
+ const routesTracker = new RouteChangeTracker();
1198
1389
  const explicitLevels = options.levels;
1199
1390
  const explicitEager = options.eager;
1200
1391
  const explicitEndpoint = options.endpoint;
@@ -1208,22 +1399,7 @@ function createRouteForge(options = {}) {
1208
1399
  };
1209
1400
  const discoveryInputs = { explicitLevels, explicitEager, explicitEndpoint, warnings };
1210
1401
  let autoDiscoveryCompleted = false;
1211
- let resolveReady;
1212
- let rejectReady;
1213
- const readyPromise = new Promise((resolve, reject) => {
1214
- resolveReady = resolve;
1215
- rejectReady = reject;
1216
- });
1217
- readyPromise.catch(() => {
1218
- });
1219
- let readySettledOk = false;
1220
- readyPromise.then(
1221
- () => {
1222
- readySettledOk = true;
1223
- },
1224
- () => {
1225
- }
1226
- );
1402
+ const latch = createReadyLatch();
1227
1403
  const cacheTtl = cacheOpts.ttl ?? DEFAULT_CACHE_TTL;
1228
1404
  const cacheStorage = cacheOpts.storage ?? "memory";
1229
1405
  const cache = new RouteCache({ storage: cacheStorage, ttl: cacheTtl });
@@ -1241,30 +1417,12 @@ function createRouteForge(options = {}) {
1241
1417
  if (resDecl.onFulfilled || resDecl.onRejected) {
1242
1418
  responseInterceptors.use(resDecl.onFulfilled, resDecl.onRejected);
1243
1419
  }
1244
- const adapterPromise = resolveAdapter({
1420
+ const ensureAdapter = createAdapterBootstrap({
1245
1421
  adapter,
1246
- forgeInterceptors: { request: requestInterceptors, response: responseInterceptors }
1422
+ requestInterceptors,
1423
+ responseInterceptors,
1424
+ warnings
1247
1425
  });
1248
- let adapterResolved = false;
1249
- let adapterObj = null;
1250
- async function ensureAdapter() {
1251
- if (!adapterResolved) {
1252
- adapterObj = await adapterPromise.catch((e) => {
1253
- if (e instanceof AdapterNotFoundError) throw e;
1254
- if (warnings) {
1255
- console.warn(
1256
- `[route-forge] adapter initialization failed (${e?.message ?? String(e)}); falling back to builtin`
1257
- );
1258
- }
1259
- return resolveAdapter({
1260
- adapter: "builtin",
1261
- forgeInterceptors: { request: requestInterceptors, response: responseInterceptors }
1262
- });
1263
- });
1264
- adapterResolved = true;
1265
- }
1266
- return adapterObj;
1267
- }
1268
1426
  function assertDiscoveryReady() {
1269
1427
  if (!autoDiscoveryCompleted && !explicitLevels?.length) {
1270
1428
  throw new DiscoveryNotReadyError();
@@ -1321,15 +1479,15 @@ function createRouteForge(options = {}) {
1321
1479
  baseURL,
1322
1480
  fetchMeta,
1323
1481
  autoDiscoveryPromise,
1324
- getAutoDiscoveryError: () => autoDiscoveryError
1482
+ getAutoDiscoveryError: () => autoDiscoveryError,
1483
+ onChange: (level) => routesTracker.notify(level)
1325
1484
  });
1326
1485
  const load = (level) => store.load(level);
1486
+ const revalidate = (level) => store.revalidate(level);
1327
1487
  const findRouteMeta = (level, name) => store.findRouteMeta(level, name);
1328
1488
  const invalidate = (level) => store.invalidate(level);
1329
1489
  const isLoaded = (level) => store.isLoaded(level);
1330
- function getRoutes(level) {
1331
- return level === void 0 ? store.getRoutes() : store.getRoutes(level);
1332
- }
1490
+ const getRoutes = store.getRoutes.bind(store);
1333
1491
  function getLevels() {
1334
1492
  return [...discoveryState.levels];
1335
1493
  }
@@ -1337,9 +1495,9 @@ function createRouteForge(options = {}) {
1337
1495
  assertDiscoveryReady();
1338
1496
  const meta = findRouteMeta(level, name);
1339
1497
  if (!meta) {
1340
- throw new UnknownRouteError(name, level, Object.keys(store.getRoutes(level)));
1498
+ throw new UnknownRouteError(name, level, store.routeNames(level));
1341
1499
  }
1342
- return buildRequestUrl(meta, params ?? {}, { baseURL, urlPrefix: discoveryState.urlPrefix });
1500
+ return buildRouteUrl(meta, params ?? {}, { baseURL, urlPrefix: discoveryState.urlPrefix });
1343
1501
  }
1344
1502
  const api = createHttpRunner({
1345
1503
  ensureAdapter,
@@ -1347,7 +1505,7 @@ function createRouteForge(options = {}) {
1347
1505
  responseInterceptors,
1348
1506
  load,
1349
1507
  findRouteMeta,
1350
- getRouteNames: (lvl) => Object.keys(store.getRoutes(lvl)),
1508
+ getRouteNames: (lvl) => store.routeNames(lvl),
1351
1509
  baseURL,
1352
1510
  state: discoveryState,
1353
1511
  timeout,
@@ -1373,23 +1531,17 @@ function createRouteForge(options = {}) {
1373
1531
  });
1374
1532
  }
1375
1533
  }).then(() => {
1376
- resolveReady(forgeInstance);
1534
+ latch.resolve(forgeInstance);
1377
1535
  }).catch((e) => {
1378
- rejectReady(e);
1536
+ latch.reject(e);
1379
1537
  });
1380
- function ready(onFulfilled, onRejected) {
1381
- if (onFulfilled) {
1382
- const p = readyPromise.then(onFulfilled, onRejected);
1383
- return p.then(() => forgeInstance);
1384
- }
1385
- return readyPromise;
1386
- }
1387
1538
  const createBoundForgeWithMethods = createBoundForgeFactory({
1388
1539
  load,
1389
1540
  api,
1390
1541
  route,
1391
1542
  hasRoute,
1392
1543
  getRoutes,
1544
+ getRouteNames: (lvl) => store.routeNames(lvl),
1393
1545
  invalidate,
1394
1546
  isLoaded,
1395
1547
  loadingTracker
@@ -1397,6 +1549,7 @@ function createRouteForge(options = {}) {
1397
1549
  const forgeInstance = {
1398
1550
  api,
1399
1551
  load,
1552
+ revalidate,
1400
1553
  route,
1401
1554
  url: route,
1402
1555
  invalidate,
@@ -1406,13 +1559,14 @@ function createRouteForge(options = {}) {
1406
1559
  getLevels,
1407
1560
  warnings,
1408
1561
  isLoading: () => loadingTracker.isLoading(),
1409
- isReady: () => readySettledOk,
1562
+ isReady: () => latch.isReady(),
1410
1563
  onLoadingChange: (cb) => loadingTracker.subscribe(cb),
1564
+ onRoutesChange: (cb) => routesTracker.subscribe(cb),
1411
1565
  interceptors: {
1412
1566
  request: requestInterceptors,
1413
1567
  response: responseInterceptors
1414
1568
  },
1415
- ready,
1569
+ ready: latch.ready,
1416
1570
  use(level, prefix) {
1417
1571
  if (level === void 0) return forgeInstance;
1418
1572
  return createBoundForgeWithMethods(level, prefix);
@@ -1433,6 +1587,7 @@ exports.MissingRouteParamError = MissingRouteParamError;
1433
1587
  exports.NetworkError = NetworkError;
1434
1588
  exports.RequestAbortedError = RequestAbortedError;
1435
1589
  exports.RouteCache = RouteCache;
1590
+ exports.RouteChangeTracker = RouteChangeTracker;
1436
1591
  exports.UnknownLevelError = UnknownLevelError;
1437
1592
  exports.UnknownRouteError = UnknownRouteError;
1438
1593
  exports.createInterceptorManager = createInterceptorManager;