@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.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) {
@@ -245,32 +275,7 @@ var DiscoveryNotReadyError = class extends ForgeError {
245
275
  }
246
276
  };
247
277
 
248
- // src/interceptors.ts
249
- var InterceptorManagerImpl = class {
250
- constructor() {
251
- this.handlers = [];
252
- this.nextId = 0;
253
- }
254
- use(onFulfilled, onRejected) {
255
- const id = this.nextId++;
256
- this.handlers.push({ id, onFulfilled, onRejected });
257
- return id;
258
- }
259
- eject(id) {
260
- const idx = this.handlers.findIndex((h) => h.id === id);
261
- if (idx >= 0) this.handlers.splice(idx, 1);
262
- }
263
- clear() {
264
- this.handlers = [];
265
- }
266
- forEach(fn) {
267
- for (const h of this.handlers) fn(h);
268
- }
269
- /** 测试用:当前注册数量 */
270
- get size() {
271
- return this.handlers.length;
272
- }
273
- };
278
+ // src/interceptors/runner.ts
274
279
  async function runRequestInterceptors(manager, initial) {
275
280
  const handlers = [];
276
281
  manager.forEach((h) => handlers.push(h));
@@ -307,9 +312,8 @@ async function runResponseInterceptors(manager, source) {
307
312
  }
308
313
  return p;
309
314
  }
310
- function createInterceptorManager() {
311
- return new InterceptorManagerImpl();
312
- }
315
+
316
+ // src/interceptors/normalize.ts
313
317
  function normalizeInterceptorDeclaration(value) {
314
318
  if (value === null || value === void 0) return {};
315
319
  let rawResolve;
@@ -579,6 +583,69 @@ async function resolveAdapter(opts) {
579
583
  return createBuiltinHttp(opts.forgeInterceptors);
580
584
  }
581
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
+
582
649
  // src/loading.ts
583
650
  var LoadingTracker = class {
584
651
  constructor() {
@@ -638,12 +705,107 @@ var LoadingTracker = class {
638
705
  }
639
706
  };
640
707
 
641
- // 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
642
760
  function buildUrl(level, ctx) {
643
- const base = ctx.baseURL.endsWith("/") ? ctx.baseURL.slice(0, -1) : ctx.baseURL;
644
- const ep = ctx.endpoint.startsWith("/") ? ctx.endpoint : `/${ctx.endpoint}`;
645
- return `${base}${ep}/${encodeURIComponent(level)}`;
761
+ return `${joinBaseAndPath(ctx.baseURL, withLeadingSlash(ctx.endpoint))}/${encodeURIComponent(level)}`;
646
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
647
809
  function buildRequestUrl(meta, params, ctx) {
648
810
  const defaults = meta.parameter_defaults ?? {};
649
811
  const missingRequired = [];
@@ -678,10 +840,10 @@ function buildRequestUrl(meta, params, ctx) {
678
840
  });
679
841
  uri = uri.replace(/\/+/g, "/").replace(/\/$/, "");
680
842
  if (/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(ctx.urlPrefix)) {
681
- const prefix2 = ctx.urlPrefix.endsWith("/") ? ctx.urlPrefix.slice(0, -1) : ctx.urlPrefix;
843
+ const prefix2 = trimTrailingSlash(ctx.urlPrefix);
682
844
  return uri.startsWith("/") ? `${prefix2}${uri}` : `${prefix2}/${uri}`;
683
845
  }
684
- const base = ctx.baseURL.endsWith("/") ? ctx.baseURL.slice(0, -1) : ctx.baseURL;
846
+ const base = trimTrailingSlash(ctx.baseURL);
685
847
  const prefix = ctx.urlPrefix;
686
848
  return uri.startsWith("/") ? `${base}${prefix}${uri}` : `${base}${prefix}/${uri}`;
687
849
  }
@@ -700,46 +862,18 @@ function appendQuery(url, query) {
700
862
  if (!qs) return url;
701
863
  return url.includes("?") ? `${url}&${qs}` : `${url}?${qs}`;
702
864
  }
703
- function resolveApiParams(input) {
704
- const {
705
- params: explicitParams,
706
- query: rawQuery,
707
- body: rawBody,
708
- headers: rawHeaders,
709
- timeout: perCallTimeout,
710
- ...flatRest
711
- } = input;
712
- const pathParams = explicitParams ? { ...explicitParams } : {};
713
- for (const [k, v] of Object.entries(flatRest)) {
714
- if (!(k in pathParams)) {
715
- pathParams[k] = v;
716
- }
717
- }
718
- let query;
719
- let body;
720
- let headers;
721
- if (rawQuery !== void 0) {
722
- if (typeof rawQuery === "object" && rawQuery !== null) {
723
- query = rawQuery;
724
- } else if (!("query" in pathParams)) {
725
- pathParams.query = rawQuery;
726
- }
727
- }
728
- if (rawBody !== void 0) {
729
- if (typeof rawBody !== "string" && typeof rawBody !== "number") {
730
- body = rawBody;
731
- } else if (!("body" in pathParams)) {
732
- pathParams.body = rawBody;
733
- }
734
- }
735
- if (rawHeaders !== void 0) {
736
- if (typeof rawHeaders === "object" && rawHeaders !== null) {
737
- headers = rawHeaders;
738
- } else if (!("headers" in pathParams)) {
739
- 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);
740
871
  }
872
+ } else {
873
+ return buildRequestUrl(meta, {}, ctx);
741
874
  }
742
- return { pathParams, query, body, headers, timeout: perCallTimeout };
875
+ const { pathParams, query } = resolveApiParams(params);
876
+ return appendQuery(buildRequestUrl(meta, pathParams, ctx), query);
743
877
  }
744
878
 
745
879
  // src/auto-discovery.ts
@@ -747,9 +881,7 @@ var DEFAULT_ENDPOINT = "/_forge/routes";
747
881
  async function fetchSummary(inputs, baseURL, fetchMeta) {
748
882
  const { explicitLevels, explicitEndpoint, warnings } = inputs;
749
883
  const endpoint = explicitEndpoint ?? DEFAULT_ENDPOINT;
750
- const base = baseURL.endsWith("/") ? baseURL.slice(0, -1) : baseURL;
751
- const ep = endpoint.startsWith("/") ? endpoint : `/${endpoint}`;
752
- const url = `${base}${ep}`;
884
+ const url = joinBaseAndPath(baseURL, endpoint);
753
885
  try {
754
886
  const data = await fetchMeta("__forge__.summary", url);
755
887
  return data;
@@ -855,6 +987,7 @@ var RouteStore = class {
855
987
  this.fetchMeta = deps.fetchMeta;
856
988
  this.autoDiscoveryPromise = deps.autoDiscoveryPromise;
857
989
  this.getAutoDiscoveryError = deps.getAutoDiscoveryError;
990
+ this.onChange = deps.onChange;
858
991
  }
859
992
  assertLevelDeclared(level) {
860
993
  if (!this.state.levels.includes(level)) {
@@ -863,15 +996,9 @@ var RouteStore = class {
863
996
  }
864
997
  async fetchLevel(level) {
865
998
  const uri = this.state.levelRoutes[level]?.uri;
866
- 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 });
867
1000
  return await this.fetchMeta(`route-forge.${level}`, url, level);
868
1001
  }
869
- /** baseURL 与后端下发的绝对 path 拼接(规范化斜杠),与 buildUrl 的 base 处理一致 */
870
- joinBaseAndPath(baseURL, path) {
871
- const base = baseURL.endsWith("/") ? baseURL.slice(0, -1) : baseURL;
872
- const p = path.startsWith("/") ? path : `/${path}`;
873
- return `${base}${p}`;
874
- }
875
1002
  async loadOne(level) {
876
1003
  const autoDiscoveryError = this.getAutoDiscoveryError();
877
1004
  if (autoDiscoveryError) throw autoDiscoveryError;
@@ -886,6 +1013,7 @@ var RouteStore = class {
886
1013
  const resp = await this.fetchLevel(level);
887
1014
  if ((this.invalidationGens.get(level) ?? 0) === gen) {
888
1015
  this.cache.set(resp, this.state.cacheTtl);
1016
+ this.onChange?.(level);
889
1017
  }
890
1018
  } finally {
891
1019
  this.inflight.delete(level);
@@ -899,6 +1027,36 @@ var RouteStore = class {
899
1027
  const list = Array.isArray(level) ? level : [level];
900
1028
  await Promise.all(list.map((l) => this.loadOne(l)));
901
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
+ }
902
1060
  invalidate(level) {
903
1061
  if (level === void 0) {
904
1062
  this.cache.clear();
@@ -906,16 +1064,19 @@ var RouteStore = class {
906
1064
  for (const lvl of this.state.levels) {
907
1065
  this.invalidationGens.set(lvl, (this.invalidationGens.get(lvl) ?? 0) + 1);
908
1066
  }
1067
+ for (const lvl of this.state.levels) this.onChange?.(lvl);
909
1068
  } else if (Array.isArray(level)) {
910
1069
  for (const lvl of level) {
911
1070
  this.cache.del(lvl);
912
1071
  this.inflight.delete(lvl);
913
1072
  this.invalidationGens.set(lvl, (this.invalidationGens.get(lvl) ?? 0) + 1);
914
1073
  }
1074
+ for (const lvl of level) this.onChange?.(lvl);
915
1075
  } else {
916
1076
  this.cache.del(level);
917
1077
  this.inflight.delete(level);
918
1078
  this.invalidationGens.set(level, (this.invalidationGens.get(level) ?? 0) + 1);
1079
+ this.onChange?.(level);
919
1080
  }
920
1081
  }
921
1082
  isLoaded(level) {
@@ -930,6 +1091,16 @@ var RouteStore = class {
930
1091
  }
931
1092
  return void 0;
932
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
+ }
933
1104
  getRoutes(level) {
934
1105
  if (level !== void 0) {
935
1106
  this.assertLevelDeclared(level);
@@ -937,7 +1108,7 @@ var RouteStore = class {
937
1108
  const routes = entry?.routes ?? {};
938
1109
  const result2 = {};
939
1110
  for (const [k, v] of Object.entries(routes)) {
940
- result2[k] = JSON.parse(JSON.stringify(v));
1111
+ result2[k] = structuredClone(v);
941
1112
  }
942
1113
  return result2;
943
1114
  }
@@ -947,7 +1118,7 @@ var RouteStore = class {
947
1118
  if (entry) {
948
1119
  const levelRoutes = {};
949
1120
  for (const [k, v] of Object.entries(entry.routes)) {
950
- levelRoutes[k] = JSON.parse(JSON.stringify(v));
1121
+ levelRoutes[k] = structuredClone(v);
951
1122
  }
952
1123
  result[lvl] = levelRoutes;
953
1124
  }
@@ -1048,18 +1219,37 @@ function createHttpRunner(deps) {
1048
1219
  let ctrl;
1049
1220
  let abortedBeforeInit = false;
1050
1221
  let abortReason;
1051
- const work = (async () => {
1052
- ctrl = new AbortController();
1053
- if (abortedBeforeInit) {
1054
- 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;
1055
1229
  }
1056
- await autoDiscoveryPromise;
1057
- await load(level);
1058
- const meta = findRouteMeta(level, name);
1059
- if (!meta) {
1060
- throw new UnknownRouteError(name, level, getRouteNames(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);
1061
1252
  }
1062
- return doApiCall(meta, params, ctrl.signal);
1063
1253
  })();
1064
1254
  const request = work;
1065
1255
  request.abort = () => {
@@ -1117,8 +1307,8 @@ function defineImmutableProps(target, props) {
1117
1307
 
1118
1308
  // src/bound-forge.ts
1119
1309
  function createBoundForgeFactory(deps) {
1120
- const { load, api, route, hasRoute, getRoutes, invalidate, isLoaded, loadingTracker } = deps;
1121
- const resolver = { load, hasRoute, getRouteNames: (lvl) => Object.keys(getRoutes(lvl)) };
1310
+ const { load, api, route, hasRoute, getRoutes, getRouteNames, invalidate, isLoaded, loadingTracker } = deps;
1311
+ const resolver = { load, hasRoute, getRouteNames };
1122
1312
  function createBoundForge(level, prefix) {
1123
1313
  const levelLoadedPromise = load(level);
1124
1314
  levelLoadedPromise.catch(() => {
@@ -1193,6 +1383,7 @@ function createRouteForge(options = {}) {
1193
1383
  } = options;
1194
1384
  const warnings = options.warnings ?? true;
1195
1385
  const loadingTracker = new LoadingTracker();
1386
+ const routesTracker = new RouteChangeTracker();
1196
1387
  const explicitLevels = options.levels;
1197
1388
  const explicitEager = options.eager;
1198
1389
  const explicitEndpoint = options.endpoint;
@@ -1206,22 +1397,7 @@ function createRouteForge(options = {}) {
1206
1397
  };
1207
1398
  const discoveryInputs = { explicitLevels, explicitEager, explicitEndpoint, warnings };
1208
1399
  let autoDiscoveryCompleted = false;
1209
- let resolveReady;
1210
- let rejectReady;
1211
- const readyPromise = new Promise((resolve, reject) => {
1212
- resolveReady = resolve;
1213
- rejectReady = reject;
1214
- });
1215
- readyPromise.catch(() => {
1216
- });
1217
- let readySettledOk = false;
1218
- readyPromise.then(
1219
- () => {
1220
- readySettledOk = true;
1221
- },
1222
- () => {
1223
- }
1224
- );
1400
+ const latch = createReadyLatch();
1225
1401
  const cacheTtl = cacheOpts.ttl ?? DEFAULT_CACHE_TTL;
1226
1402
  const cacheStorage = cacheOpts.storage ?? "memory";
1227
1403
  const cache = new RouteCache({ storage: cacheStorage, ttl: cacheTtl });
@@ -1239,30 +1415,12 @@ function createRouteForge(options = {}) {
1239
1415
  if (resDecl.onFulfilled || resDecl.onRejected) {
1240
1416
  responseInterceptors.use(resDecl.onFulfilled, resDecl.onRejected);
1241
1417
  }
1242
- const adapterPromise = resolveAdapter({
1418
+ const ensureAdapter = createAdapterBootstrap({
1243
1419
  adapter,
1244
- forgeInterceptors: { request: requestInterceptors, response: responseInterceptors }
1420
+ requestInterceptors,
1421
+ responseInterceptors,
1422
+ warnings
1245
1423
  });
1246
- let adapterResolved = false;
1247
- let adapterObj = null;
1248
- async function ensureAdapter() {
1249
- if (!adapterResolved) {
1250
- adapterObj = await adapterPromise.catch((e) => {
1251
- if (e instanceof AdapterNotFoundError) throw e;
1252
- if (warnings) {
1253
- console.warn(
1254
- `[route-forge] adapter initialization failed (${e?.message ?? String(e)}); falling back to builtin`
1255
- );
1256
- }
1257
- return resolveAdapter({
1258
- adapter: "builtin",
1259
- forgeInterceptors: { request: requestInterceptors, response: responseInterceptors }
1260
- });
1261
- });
1262
- adapterResolved = true;
1263
- }
1264
- return adapterObj;
1265
- }
1266
1424
  function assertDiscoveryReady() {
1267
1425
  if (!autoDiscoveryCompleted && !explicitLevels?.length) {
1268
1426
  throw new DiscoveryNotReadyError();
@@ -1319,15 +1477,15 @@ function createRouteForge(options = {}) {
1319
1477
  baseURL,
1320
1478
  fetchMeta,
1321
1479
  autoDiscoveryPromise,
1322
- getAutoDiscoveryError: () => autoDiscoveryError
1480
+ getAutoDiscoveryError: () => autoDiscoveryError,
1481
+ onChange: (level) => routesTracker.notify(level)
1323
1482
  });
1324
1483
  const load = (level) => store.load(level);
1484
+ const revalidate = (level) => store.revalidate(level);
1325
1485
  const findRouteMeta = (level, name) => store.findRouteMeta(level, name);
1326
1486
  const invalidate = (level) => store.invalidate(level);
1327
1487
  const isLoaded = (level) => store.isLoaded(level);
1328
- function getRoutes(level) {
1329
- return level === void 0 ? store.getRoutes() : store.getRoutes(level);
1330
- }
1488
+ const getRoutes = store.getRoutes.bind(store);
1331
1489
  function getLevels() {
1332
1490
  return [...discoveryState.levels];
1333
1491
  }
@@ -1335,9 +1493,9 @@ function createRouteForge(options = {}) {
1335
1493
  assertDiscoveryReady();
1336
1494
  const meta = findRouteMeta(level, name);
1337
1495
  if (!meta) {
1338
- throw new UnknownRouteError(name, level, Object.keys(store.getRoutes(level)));
1496
+ throw new UnknownRouteError(name, level, store.routeNames(level));
1339
1497
  }
1340
- return buildRequestUrl(meta, params ?? {}, { baseURL, urlPrefix: discoveryState.urlPrefix });
1498
+ return buildRouteUrl(meta, params ?? {}, { baseURL, urlPrefix: discoveryState.urlPrefix });
1341
1499
  }
1342
1500
  const api = createHttpRunner({
1343
1501
  ensureAdapter,
@@ -1345,7 +1503,7 @@ function createRouteForge(options = {}) {
1345
1503
  responseInterceptors,
1346
1504
  load,
1347
1505
  findRouteMeta,
1348
- getRouteNames: (lvl) => Object.keys(store.getRoutes(lvl)),
1506
+ getRouteNames: (lvl) => store.routeNames(lvl),
1349
1507
  baseURL,
1350
1508
  state: discoveryState,
1351
1509
  timeout,
@@ -1371,23 +1529,17 @@ function createRouteForge(options = {}) {
1371
1529
  });
1372
1530
  }
1373
1531
  }).then(() => {
1374
- resolveReady(forgeInstance);
1532
+ latch.resolve(forgeInstance);
1375
1533
  }).catch((e) => {
1376
- rejectReady(e);
1534
+ latch.reject(e);
1377
1535
  });
1378
- function ready(onFulfilled, onRejected) {
1379
- if (onFulfilled) {
1380
- const p = readyPromise.then(onFulfilled, onRejected);
1381
- return p.then(() => forgeInstance);
1382
- }
1383
- return readyPromise;
1384
- }
1385
1536
  const createBoundForgeWithMethods = createBoundForgeFactory({
1386
1537
  load,
1387
1538
  api,
1388
1539
  route,
1389
1540
  hasRoute,
1390
1541
  getRoutes,
1542
+ getRouteNames: (lvl) => store.routeNames(lvl),
1391
1543
  invalidate,
1392
1544
  isLoaded,
1393
1545
  loadingTracker
@@ -1395,6 +1547,7 @@ function createRouteForge(options = {}) {
1395
1547
  const forgeInstance = {
1396
1548
  api,
1397
1549
  load,
1550
+ revalidate,
1398
1551
  route,
1399
1552
  url: route,
1400
1553
  invalidate,
@@ -1404,13 +1557,14 @@ function createRouteForge(options = {}) {
1404
1557
  getLevels,
1405
1558
  warnings,
1406
1559
  isLoading: () => loadingTracker.isLoading(),
1407
- isReady: () => readySettledOk,
1560
+ isReady: () => latch.isReady(),
1408
1561
  onLoadingChange: (cb) => loadingTracker.subscribe(cb),
1562
+ onRoutesChange: (cb) => routesTracker.subscribe(cb),
1409
1563
  interceptors: {
1410
1564
  request: requestInterceptors,
1411
1565
  response: responseInterceptors
1412
1566
  },
1413
- ready,
1567
+ ready: latch.ready,
1414
1568
  use(level, prefix) {
1415
1569
  if (level === void 0) return forgeInstance;
1416
1570
  return createBoundForgeWithMethods(level, prefix);
@@ -1419,6 +1573,6 @@ function createRouteForge(options = {}) {
1419
1573
  return forgeInstance;
1420
1574
  }
1421
1575
 
1422
- export { AdapterNotFoundError, DiscoveryNotReadyError, ForgeError, HTTPError, InterceptorManagerImpl, InvalidInterceptorReturnError, InvalidPathParamError, 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 };
1423
1577
  //# sourceMappingURL=index.js.map
1424
1578
  //# sourceMappingURL=index.js.map