@route-forge/core 1.4.0 → 2.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
@@ -14,9 +14,18 @@ function pickStorage(storage) {
14
14
  var RouteCache = class {
15
15
  constructor(opts) {
16
16
  this.memory = /* @__PURE__ */ new Map();
17
+ /** 其他 tab 修改 storage 时失效对应镜像(storage 事件:自己的写不触发,自己的写经 set/del 已同步) */
18
+ this.onStorageEvent = (e) => {
19
+ if (!e.key) return;
20
+ if (!e.key.startsWith(KEY_PREFIX)) return;
21
+ this.memory.delete(e.key.slice(KEY_PREFIX.length));
22
+ };
17
23
  this.storage = opts.storage;
18
24
  this.fallbackTtl = opts.ttl;
19
25
  this.backend = pickStorage(opts.storage);
26
+ if (this.backend && typeof globalThis.addEventListener === "function") {
27
+ globalThis.addEventListener("storage", this.onStorageEvent);
28
+ }
20
29
  }
21
30
  key(level) {
22
31
  return `${KEY_PREFIX}${level}`;
@@ -25,6 +34,8 @@ var RouteCache = class {
25
34
  if (this.storage === "memory" || !this.backend) {
26
35
  return this.getFromMemory(level);
27
36
  }
37
+ const mirrored = this.getFromMemory(level);
38
+ if (mirrored) return mirrored;
28
39
  const raw = this.backend.getItem(this.key(level));
29
40
  if (raw) {
30
41
  try {
@@ -33,12 +44,13 @@ var RouteCache = class {
33
44
  this.del(level);
34
45
  return void 0;
35
46
  }
47
+ this.memory.set(level, entry);
36
48
  return entry;
37
49
  } catch {
38
50
  return void 0;
39
51
  }
40
52
  }
41
- return this.getFromMemory(level);
53
+ return void 0;
42
54
  }
43
55
  getFromMemory(level) {
44
56
  const entry = this.memory.get(level);
@@ -62,14 +74,13 @@ var RouteCache = class {
62
74
  ttl,
63
75
  cachedAt: Date.now()
64
76
  };
77
+ this.memory.set(resp.level, entry);
65
78
  if (this.storage === "memory" || !this.backend) {
66
- this.memory.set(resp.level, entry);
67
79
  return;
68
80
  }
69
81
  try {
70
82
  this.backend.setItem(this.key(resp.level), JSON.stringify(entry));
71
83
  } catch {
72
- this.memory.set(resp.level, entry);
73
84
  }
74
85
  }
75
86
  del(level) {
@@ -249,14 +260,14 @@ function createInterceptorManager() {
249
260
  return new InterceptorManagerImpl();
250
261
  }
251
262
 
252
- // src/adapters/builtin-http.ts
253
- function isPassthroughBody(body) {
254
- if (body === null) return false;
255
- return typeof FormData !== "undefined" && body instanceof FormData || typeof URLSearchParams !== "undefined" && body instanceof URLSearchParams || typeof Blob !== "undefined" && body instanceof Blob || typeof ArrayBuffer !== "undefined" && (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) || typeof ReadableStream !== "undefined" && body instanceof ReadableStream;
256
- }
257
- function isAbortError(err) {
263
+ // src/adapters/fetch-core.ts
264
+ function isAbortError(err, signal) {
265
+ if (signal?.aborted) return true;
258
266
  const e = err;
259
- return !!e && e.name === "AbortError";
267
+ if (!e) return false;
268
+ if (e.name === "AbortError") return true;
269
+ if (e.code === "ERR_CANCELED") return true;
270
+ return false;
260
271
  }
261
272
  function combineSignals(...signals) {
262
273
  const valid = signals.filter((s) => !!s);
@@ -275,82 +286,89 @@ function combineSignals(...signals) {
275
286
  }
276
287
  return controller.signal;
277
288
  }
278
- function createBuiltinHttp(forgeInterceptors) {
279
- const requestMgr = forgeInterceptors?.request ?? createInterceptorManager();
280
- const responseMgr = forgeInterceptors?.response ?? createInterceptorManager();
281
- async function requestRaw(config) {
282
- const signal = combineSignals(
283
- config.signal,
284
- config.timeout && config.timeout > 0 ? AbortSignal.timeout(config.timeout) : void 0
285
- );
286
- let url = config.url;
287
- if (config.paramsSerializer && config.params) {
288
- const qs = config.paramsSerializer(config.params);
289
- if (qs) {
290
- url = url.includes("?") ? `${url}&${qs}` : `${url}?${qs}`;
291
- }
289
+ function isPassthroughBody(body) {
290
+ if (body === null) return false;
291
+ return typeof FormData !== "undefined" && body instanceof FormData || typeof URLSearchParams !== "undefined" && body instanceof URLSearchParams || typeof Blob !== "undefined" && body instanceof Blob || typeof ArrayBuffer !== "undefined" && (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) || typeof ReadableStream !== "undefined" && body instanceof ReadableStream;
292
+ }
293
+ async function rawFetch(config) {
294
+ const signal = combineSignals(
295
+ config.signal,
296
+ config.timeout && config.timeout > 0 ? AbortSignal.timeout(config.timeout) : void 0
297
+ );
298
+ let url = config.url;
299
+ if (config.paramsSerializer && config.params) {
300
+ const qs = config.paramsSerializer(config.params);
301
+ if (qs) {
302
+ url = url.includes("?") ? `${url}&${qs}` : `${url}?${qs}`;
292
303
  }
293
- const headers = new Headers(config.headers);
294
- const fetchInit = {
295
- method: config.method,
296
- headers
297
- };
298
- if (signal) fetchInit.signal = signal;
299
- if (config.body !== void 0 && !["GET", "HEAD"].includes(config.method.toUpperCase())) {
300
- if (typeof config.body === "string" || isPassthroughBody(config.body)) {
301
- fetchInit.body = config.body;
302
- } else {
303
- fetchInit.body = JSON.stringify(config.body);
304
- if (!headers.has("Content-Type")) {
305
- headers.set("Content-Type", "application/json");
306
- }
304
+ }
305
+ const headers = new Headers(config.headers);
306
+ const fetchInit = {
307
+ method: config.method,
308
+ headers
309
+ };
310
+ if (signal) fetchInit.signal = signal;
311
+ if (config.body !== void 0 && !["GET", "HEAD"].includes(config.method.toUpperCase())) {
312
+ if (typeof config.body === "string" || isPassthroughBody(config.body)) {
313
+ fetchInit.body = config.body;
314
+ } else {
315
+ fetchInit.body = JSON.stringify(config.body);
316
+ if (!headers.has("Content-Type")) {
317
+ headers.set("Content-Type", "application/json");
307
318
  }
308
319
  }
309
- let res;
320
+ }
321
+ let res;
322
+ try {
323
+ res = await fetch(url, fetchInit);
324
+ } catch (e) {
325
+ if (isAbortError(e)) throw e;
326
+ throw new NetworkError(
327
+ e instanceof Error ? e.message : String(e),
328
+ config.route,
329
+ config.level,
330
+ e
331
+ );
332
+ }
333
+ const text = await res.text();
334
+ let data = text;
335
+ const contentType = res.headers.get("content-type") ?? "";
336
+ if (contentType.includes("application/json")) {
310
337
  try {
311
- res = await fetch(url, fetchInit);
312
- } catch (e) {
313
- if (isAbortError(e)) throw e;
314
- throw new NetworkError(
315
- e instanceof Error ? e.message : String(e),
316
- config.route,
317
- config.level,
318
- e
319
- );
320
- }
321
- const text = await res.text();
322
- let data = text;
323
- const contentType = res.headers.get("content-type") ?? "";
324
- if (contentType.includes("application/json")) {
325
- try {
326
- data = JSON.parse(text);
327
- } catch {
328
- }
338
+ data = JSON.parse(text);
339
+ } catch {
329
340
  }
330
- const responseData = {
341
+ }
342
+ const responseData = {
343
+ route: config.route,
344
+ level: config.level,
345
+ method: config.method,
346
+ url,
347
+ status: res.status,
348
+ headers: res.headers,
349
+ data,
350
+ config
351
+ };
352
+ if (res.status >= 200 && res.status < 300) {
353
+ return responseData;
354
+ }
355
+ throw new HTTPError(
356
+ `HTTP ${res.status} for route "${config.route}" (${config.method} ${url})`,
357
+ {
331
358
  route: config.route,
332
359
  level: config.level,
333
- method: config.method,
334
- url,
335
360
  status: res.status,
336
- headers: res.headers,
337
- data,
338
- config
339
- };
340
- if (res.status >= 200 && res.status < 300) {
341
- return responseData;
361
+ url,
362
+ method: config.method
342
363
  }
343
- throw new HTTPError(
344
- `HTTP ${res.status} for route "${config.route}" (${config.method} ${url})`,
345
- {
346
- route: config.route,
347
- level: config.level,
348
- status: res.status,
349
- url,
350
- method: config.method
351
- }
352
- );
353
- }
364
+ );
365
+ }
366
+
367
+ // src/adapters/builtin-http.ts
368
+ function createBuiltinHttp(forgeInterceptors) {
369
+ const requestMgr = forgeInterceptors?.request ?? createInterceptorManager();
370
+ const responseMgr = forgeInterceptors?.response ?? createInterceptorManager();
371
+ const requestRaw = rawFetch;
354
372
  async function request(config) {
355
373
  const finalConfig = await runRequestInterceptors(requestMgr, config);
356
374
  const source = Promise.resolve(finalConfig).then(requestRaw);
@@ -470,39 +488,6 @@ async function resolveAdapter(opts) {
470
488
  return createBuiltinHttp(opts.forgeInterceptors);
471
489
  }
472
490
 
473
- // src/resolveRouteName.ts
474
- async function resolveRouteName(forge, level, prefix, suffix, separator = ".") {
475
- if (!suffix) return prefix;
476
- const joined = `${prefix}${separator}${suffix}`;
477
- if (!suffix.startsWith(`${prefix}${separator}`)) return joined;
478
- await forge.load(level);
479
- if (forge.hasRoute(level, joined)) return joined;
480
- if (forge.hasRoute(level, suffix)) return suffix;
481
- throw new UnknownRouteError(joined, level);
482
- }
483
- function resolveRouteNameSync(forge, level, prefix, suffix, separator = ".") {
484
- if (!suffix) return prefix;
485
- const joined = `${prefix}${separator}${suffix}`;
486
- if (!suffix.startsWith(`${prefix}${separator}`)) return joined;
487
- if (forge.hasRoute(level, joined)) return joined;
488
- if (forge.hasRoute(level, suffix)) return suffix;
489
- throw new UnknownRouteError(joined, level);
490
- }
491
-
492
- // src/defineImmutableProps.ts
493
- function defineImmutableProps(target, props) {
494
- for (const key of Object.keys(props)) {
495
- const val = props[key];
496
- Object.defineProperty(target, key, {
497
- value: val !== null && typeof val === "object" ? Object.freeze(val) : val,
498
- writable: false,
499
- enumerable: false,
500
- configurable: false
501
- });
502
- }
503
- return target;
504
- }
505
-
506
491
  // src/loading.ts
507
492
  var LoadingTracker = class {
508
493
  constructor() {
@@ -562,320 +547,308 @@ var LoadingTracker = class {
562
547
  }
563
548
  };
564
549
 
565
- // src/forge.ts
566
- var DEFAULT_TIMEOUT = 3e4;
567
- var DEFAULT_CACHE_TTL = 3600;
568
- var UNASSIGNED_LEVEL = "unassigned";
569
- function createRouteForge(options) {
570
- if (!options.endpoint) throw new TypeError("options.endpoint is required");
571
- const {
572
- adapter = "auto",
573
- timeout = DEFAULT_TIMEOUT,
574
- baseURL = "",
575
- interceptors: declarativeInterceptors,
576
- cache: cacheOpts = {}
577
- } = options;
578
- const loadingTracker = new LoadingTracker();
579
- const explicitLevels = options.levels;
580
- const explicitEager = options.eager;
581
- const explicitEndpoint = options.endpoint;
582
- let effectiveLevels = explicitLevels ?? [];
583
- let effectiveEager = explicitEager ?? [];
584
- let effectiveEndpoint = explicitEndpoint;
585
- let effectiveUrlPrefix = "";
586
- let summaryUnassigned;
587
- let backendHasUnassignedLevel = false;
588
- const summaryPromise = (async () => {
589
- try {
590
- const summaryUrl = explicitEndpoint;
591
- const resp = await fetch(summaryUrl, { method: "GET" });
592
- if (!resp.ok) {
593
- console.warn(
594
- `[route-forge] summary endpoint ${summaryUrl} unreachable (HTTP ${resp.status}); falling back to explicit options`
595
- );
596
- return null;
597
- }
598
- return await resp.json();
599
- } catch (e) {
600
- if (explicitLevels && explicitLevels.length > 0) {
601
- console.warn(
602
- `[route-forge] summary endpoint unreachable: ${e.message}; using explicit levels`
603
- );
604
- return null;
605
- }
606
- throw new UnknownLevelError("(auto-discovery)");
607
- }
608
- })();
609
- const autoDiscoveryPromise = summaryPromise.then((summary) => {
610
- if (summary === null) {
611
- return;
612
- }
613
- const schemaVersion = summary.schemaVersion ?? 1;
614
- if (schemaVersion > 1) {
615
- console.warn(
616
- `[route-forge] backend schemaVersion=${schemaVersion} > client supported 1; some features may be unavailable`
617
- );
618
- }
619
- if (summary.config.endpoint_prefix && summary.config.endpoint_prefix !== explicitEndpoint) {
620
- console.warn(
621
- `[route-forge] backend endpoint_prefix "${summary.config.endpoint_prefix}" overrides frontend endpoint "${explicitEndpoint}"`
622
- );
623
- effectiveEndpoint = summary.config.endpoint_prefix;
624
- }
625
- if (summary.config.url_prefix) {
626
- effectiveUrlPrefix = summary.config.url_prefix.endsWith("/") ? summary.config.url_prefix.slice(0, -1) : summary.config.url_prefix;
627
- }
628
- const backendLevels = Object.keys(summary.levels);
629
- backendHasUnassignedLevel = backendLevels.includes(UNASSIGNED_LEVEL);
630
- if (Array.isArray(summary.unassigned) && summary.unassigned.length > 0 && !backendHasUnassignedLevel) {
631
- summaryUnassigned = summary.unassigned;
632
- }
633
- const availableLevels = backendLevels.slice();
634
- if (summaryUnassigned && !backendHasUnassignedLevel) {
635
- availableLevels.push(UNASSIGNED_LEVEL);
550
+ // src/url-builder.ts
551
+ function buildUrl(level, ctx) {
552
+ const base = ctx.baseURL.endsWith("/") ? ctx.baseURL.slice(0, -1) : ctx.baseURL;
553
+ const ep = ctx.endpoint.startsWith("/") ? ctx.endpoint : `/${ctx.endpoint}`;
554
+ return `${base}${ep}/${encodeURIComponent(level)}`;
555
+ }
556
+ function buildRequestUrl(meta, params, ctx) {
557
+ const defaults = meta.parameter_defaults ?? {};
558
+ const missingRequired = [];
559
+ const values = {};
560
+ for (const p of meta.parameters) {
561
+ let v = params[p];
562
+ if ((v === void 0 || v === null) && p in defaults) {
563
+ v = defaults[p];
636
564
  }
637
- if (explicitLevels && explicitLevels.length > 0) {
638
- const intersection = explicitLevels.filter((l) => availableLevels.includes(l));
639
- const removed = explicitLevels.filter((l) => !availableLevels.includes(l));
640
- if (removed.length > 0) {
641
- console.warn(
642
- `[route-forge] levels not in backend summary and dropped: ${removed.join(", ")}`
643
- );
565
+ if (v === void 0 || v === null) {
566
+ if (!meta.uri.includes(`{${p}?}`)) {
567
+ missingRequired.push(p);
644
568
  }
645
- effectiveLevels = intersection;
646
569
  } else {
647
- effectiveLevels = availableLevels;
570
+ values[p] = v;
648
571
  }
649
- const backendEager = backendLevels.filter((lvl) => summary.levels[lvl]?.load === "eager");
650
- if (!explicitEager) {
651
- effectiveEager = backendEager;
652
- } else {
653
- const union = /* @__PURE__ */ new Set([...backendEager, ...explicitEager]);
654
- effectiveEager = [...union];
572
+ }
573
+ if (missingRequired.length > 0) {
574
+ throw new MissingRouteParamError(meta.name, missingRequired);
575
+ }
576
+ let uri = meta.uri.replace(/\{([^{}]+)\}/g, (match, raw) => {
577
+ const optional = raw.endsWith("?");
578
+ const name = optional ? raw.slice(0, -1) : raw;
579
+ if (values[name] !== void 0) {
580
+ const val = values[name];
581
+ if (typeof val === "object") {
582
+ throw new ForgeError(
583
+ `Path parameter "${name}" must be a primitive value (string, number, boolean), got ${typeof val}`,
584
+ { code: "RF_FE_003", route: meta.name, context: { param: name, value: val } }
585
+ );
586
+ }
587
+ return encodeURIComponent(String(val));
655
588
  }
589
+ return optional ? "" : match;
656
590
  });
657
- let autoDiscoveryError = null;
658
- autoDiscoveryPromise.catch((e) => {
659
- autoDiscoveryError = e;
660
- });
661
- let autoDiscoveryCompleted = false;
662
- let resolveReady;
663
- const readyPromise = new Promise((resolve) => {
664
- resolveReady = resolve;
665
- });
666
- const cacheTtl = cacheOpts.ttl ?? DEFAULT_CACHE_TTL;
667
- const cacheStorage = cacheOpts.storage ?? "memory";
668
- const cache = new RouteCache({ storage: cacheStorage, ttl: cacheTtl });
669
- const requestInterceptors = new InterceptorManagerImpl();
670
- const responseInterceptors = new InterceptorManagerImpl();
671
- if (declarativeInterceptors?.request) {
672
- for (const entry of declarativeInterceptors.request) {
673
- if (typeof entry === "function") {
674
- requestInterceptors.use(entry);
675
- } else {
676
- const [onFulfilled, onRejected] = entry;
677
- requestInterceptors.use(onFulfilled, onRejected);
678
- }
591
+ uri = uri.replace(/\/+/g, "/").replace(/\/$/, "");
592
+ if (/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(ctx.urlPrefix)) {
593
+ const prefix2 = ctx.urlPrefix.endsWith("/") ? ctx.urlPrefix.slice(0, -1) : ctx.urlPrefix;
594
+ return uri.startsWith("/") ? `${prefix2}${uri}` : `${prefix2}/${uri}`;
595
+ }
596
+ const base = ctx.baseURL.endsWith("/") ? ctx.baseURL.slice(0, -1) : ctx.baseURL;
597
+ const prefix = ctx.urlPrefix;
598
+ return uri.startsWith("/") ? `${base}${prefix}${uri}` : `${base}${prefix}/${uri}`;
599
+ }
600
+ function pickMethod(meta) {
601
+ const m = meta.methods.find((x) => x.toUpperCase() !== "HEAD");
602
+ return (m ?? meta.methods[0] ?? "GET").toUpperCase();
603
+ }
604
+ function appendQuery(url, query) {
605
+ if (!query) return url;
606
+ const usp = new URLSearchParams();
607
+ for (const [k, v] of Object.entries(query)) {
608
+ if (v === void 0 || v === null) continue;
609
+ usp.append(k, String(v));
610
+ }
611
+ const qs = usp.toString();
612
+ if (!qs) return url;
613
+ return url.includes("?") ? `${url}&${qs}` : `${url}?${qs}`;
614
+ }
615
+ function resolveApiParams(input) {
616
+ const {
617
+ params: explicitParams,
618
+ query: rawQuery,
619
+ body: rawBody,
620
+ headers: rawHeaders,
621
+ timeout: perCallTimeout,
622
+ ...flatRest
623
+ } = input;
624
+ const pathParams = explicitParams ? { ...explicitParams } : {};
625
+ for (const [k, v] of Object.entries(flatRest)) {
626
+ if (!(k in pathParams)) {
627
+ pathParams[k] = v;
679
628
  }
680
629
  }
681
- if (declarativeInterceptors?.response) {
682
- for (const entry of declarativeInterceptors.response) {
683
- if (typeof entry === "function") {
684
- responseInterceptors.use(entry);
685
- } else {
686
- const [onFulfilled, onRejected] = entry;
687
- responseInterceptors.use(onFulfilled, onRejected);
688
- }
630
+ let query;
631
+ let body;
632
+ let headers;
633
+ if (rawQuery !== void 0) {
634
+ if (typeof rawQuery === "object" && rawQuery !== null) {
635
+ query = rawQuery;
636
+ } else if (!("query" in pathParams)) {
637
+ pathParams.query = rawQuery;
689
638
  }
690
639
  }
691
- const adapterPromise = resolveAdapter({
692
- adapter,
693
- forgeInterceptors: { request: requestInterceptors, response: responseInterceptors }
694
- });
695
- let adapterResolved = false;
696
- let adapterObj = null;
697
- async function ensureAdapter() {
698
- if (!adapterResolved) {
699
- adapterObj = await adapterPromise.catch((e) => {
700
- if (e instanceof AdapterNotFoundError) throw e;
701
- return resolveAdapter({
702
- adapter: "builtin",
703
- forgeInterceptors: { request: requestInterceptors, response: responseInterceptors }
704
- });
705
- });
706
- adapterResolved = true;
640
+ if (rawBody !== void 0) {
641
+ if (typeof rawBody !== "string" && typeof rawBody !== "number") {
642
+ body = rawBody;
643
+ } else if (!("body" in pathParams)) {
644
+ pathParams.body = rawBody;
707
645
  }
708
- return adapterObj;
709
646
  }
710
- const inflight = /* @__PURE__ */ new Map();
711
- const invalidationGens = /* @__PURE__ */ new Map();
712
- function assertLevelDeclared(level) {
713
- if (!effectiveLevels.includes(level)) {
714
- throw new UnknownLevelError(level);
647
+ if (rawHeaders !== void 0) {
648
+ if (typeof rawHeaders === "object" && rawHeaders !== null) {
649
+ headers = rawHeaders;
650
+ } else if (!("headers" in pathParams)) {
651
+ pathParams.headers = rawHeaders;
715
652
  }
716
653
  }
717
- function assertDiscoveryReady() {
718
- if (!autoDiscoveryCompleted && !explicitLevels?.length) {
719
- throw new ForgeError(
720
- "Route data not available. Auto-discovery has not completed. Use forge.ready() or forge.use(level) first.",
721
- { code: "RF_FE_010" }
654
+ return { pathParams, query, body, headers, timeout: perCallTimeout };
655
+ }
656
+
657
+ // src/auto-discovery.ts
658
+ var UNASSIGNED_LEVEL = "unassigned";
659
+ async function fetchSummary(inputs, baseURL, fetchMeta) {
660
+ const { explicitLevels, explicitEndpoint } = inputs;
661
+ try {
662
+ const base = baseURL.endsWith("/") ? baseURL.slice(0, -1) : baseURL;
663
+ const ep = explicitEndpoint.startsWith("/") ? explicitEndpoint : `/${explicitEndpoint}`;
664
+ const data = await fetchMeta("__forge__.summary", `${base}${ep}`);
665
+ return data;
666
+ } catch (e) {
667
+ if (explicitLevels && explicitLevels.length > 0) {
668
+ console.warn(
669
+ `[route-forge] summary endpoint unreachable: ${e.message}; using explicit levels`
722
670
  );
671
+ return null;
723
672
  }
673
+ throw new UnknownLevelError("(auto-discovery)");
724
674
  }
725
- function buildUrl(level) {
726
- const base = baseURL.endsWith("/") ? baseURL.slice(0, -1) : baseURL;
727
- const ep = effectiveEndpoint.startsWith("/") ? effectiveEndpoint : `/${effectiveEndpoint}`;
728
- return `${base}${ep}/${encodeURIComponent(level)}`;
675
+ }
676
+ function applySummaryToState(summary, state, inputs) {
677
+ const { explicitLevels, explicitEager, explicitEndpoint } = inputs;
678
+ const schemaVersion = summary.schemaVersion ?? 1;
679
+ if (schemaVersion > 1) {
680
+ console.warn(
681
+ `[route-forge] backend schemaVersion=${schemaVersion} > client supported 1; some features may be unavailable`
682
+ );
729
683
  }
730
- async function fetchLevel(level) {
731
- const adp = await ensureAdapter();
732
- const config = {
733
- route: `__forge__.load.${level}`,
734
- level,
735
- method: "GET",
736
- url: buildUrl(level),
737
- headers: { Accept: "application/json" },
738
- params: {},
739
- timeout,
740
- meta: {
741
- name: `__forge__.load.${level}`,
742
- uri: buildUrl(level),
743
- methods: ["GET"],
744
- parameters: [],
745
- level
746
- }
747
- };
748
- const doRawRequest = adp.requestRaw ?? adp.request;
749
- const resp = await doRawRequest(config);
750
- if (!resp || resp.status < 200 || resp.status >= 300) {
751
- throw new HTTPError(
752
- `Failed to load level "${level}": HTTP ${resp?.status}`,
753
- { level, status: resp?.status, url: buildUrl(level), method: "GET" }
684
+ if (summary.config.endpoint_prefix && summary.config.endpoint_prefix !== explicitEndpoint) {
685
+ console.warn(
686
+ `[route-forge] backend endpoint_prefix "${summary.config.endpoint_prefix}" overrides frontend endpoint "${explicitEndpoint}"`
687
+ );
688
+ state.endpoint = summary.config.endpoint_prefix;
689
+ }
690
+ if (summary.config.url_prefix) {
691
+ state.urlPrefix = summary.config.url_prefix.endsWith("/") ? summary.config.url_prefix.slice(0, -1) : summary.config.url_prefix;
692
+ }
693
+ const backendLevels = Object.keys(summary.levels);
694
+ state.backendHasUnassignedLevel = backendLevels.includes(UNASSIGNED_LEVEL);
695
+ if (Array.isArray(summary.unassigned) && summary.unassigned.length > 0 && !state.backendHasUnassignedLevel) {
696
+ state.summaryUnassigned = summary.unassigned;
697
+ }
698
+ const availableLevels = backendLevels.slice();
699
+ if (state.summaryUnassigned && !state.backendHasUnassignedLevel) {
700
+ availableLevels.push(UNASSIGNED_LEVEL);
701
+ }
702
+ if (explicitLevels && explicitLevels.length > 0) {
703
+ const intersection = explicitLevels.filter((l) => availableLevels.includes(l));
704
+ const removed = explicitLevels.filter((l) => !availableLevels.includes(l));
705
+ if (removed.length > 0) {
706
+ console.warn(
707
+ `[route-forge] levels not in backend summary and dropped: ${removed.join(", ")}`
754
708
  );
755
709
  }
756
- const data = resp.data;
757
- return data;
710
+ state.levels = intersection;
711
+ } else {
712
+ state.levels = availableLevels;
713
+ }
714
+ const backendEager = backendLevels.filter((lvl) => summary.levels[lvl]?.load === "eager");
715
+ if (!explicitEager) {
716
+ state.eager = backendEager;
717
+ } else {
718
+ const union = /* @__PURE__ */ new Set([...backendEager, ...explicitEager]);
719
+ state.eager = [...union];
758
720
  }
759
- async function loadOne(level) {
721
+ }
722
+
723
+ // src/route-store.ts
724
+ var RouteStore = class {
725
+ constructor(deps) {
726
+ this.inflight = /* @__PURE__ */ new Map();
727
+ /** 每个层级的失效代数,用于检测 loadOne 期间是否发生了 invalidate */
728
+ this.invalidationGens = /* @__PURE__ */ new Map();
729
+ this.cache = deps.cache;
730
+ this.state = deps.state;
731
+ this.baseURL = deps.baseURL;
732
+ this.fetchMeta = deps.fetchMeta;
733
+ this.autoDiscoveryPromise = deps.autoDiscoveryPromise;
734
+ this.getAutoDiscoveryError = deps.getAutoDiscoveryError;
735
+ }
736
+ assertLevelDeclared(level) {
737
+ if (!this.state.levels.includes(level)) {
738
+ throw new UnknownLevelError(level);
739
+ }
740
+ }
741
+ async fetchLevel(level) {
742
+ return await this.fetchMeta(
743
+ `__forge__.load.${level}`,
744
+ buildUrl(level, { baseURL: this.baseURL, endpoint: this.state.endpoint }),
745
+ level
746
+ );
747
+ }
748
+ async loadOne(level) {
749
+ const autoDiscoveryError = this.getAutoDiscoveryError();
760
750
  if (autoDiscoveryError) throw autoDiscoveryError;
761
- assertLevelDeclared(level);
762
- if (cache.get(level)) return;
763
- const existing = inflight.get(level);
751
+ this.assertLevelDeclared(level);
752
+ if (this.cache.get(level)) return;
753
+ const existing = this.inflight.get(level);
764
754
  if (existing) return existing;
765
- const gen = invalidationGens.get(level) ?? 0;
755
+ const gen = this.invalidationGens.get(level) ?? 0;
766
756
  const p = (async () => {
767
757
  try {
768
- if (level === UNASSIGNED_LEVEL && !backendHasUnassignedLevel && summaryUnassigned) {
758
+ if (level === UNASSIGNED_LEVEL && !this.state.backendHasUnassignedLevel && this.state.summaryUnassigned) {
769
759
  const routes = {};
770
- for (const r of summaryUnassigned) {
760
+ for (const r of this.state.summaryUnassigned) {
771
761
  routes[r.name] = { ...r, level: UNASSIGNED_LEVEL };
772
762
  }
773
- cache.set({ level: UNASSIGNED_LEVEL, routes, cache: null });
763
+ this.cache.set({ level: UNASSIGNED_LEVEL, routes, cache: null });
774
764
  return;
775
765
  }
776
- const resp = await fetchLevel(level);
777
- if ((invalidationGens.get(level) ?? 0) === gen) {
778
- cache.set(resp);
766
+ const resp = await this.fetchLevel(level);
767
+ if ((this.invalidationGens.get(level) ?? 0) === gen) {
768
+ this.cache.set(resp);
779
769
  }
780
770
  } finally {
781
- inflight.delete(level);
771
+ this.inflight.delete(level);
782
772
  }
783
773
  })();
784
- inflight.set(level, p);
774
+ this.inflight.set(level, p);
785
775
  return p;
786
776
  }
787
- async function load(level) {
788
- await autoDiscoveryPromise;
777
+ async load(level) {
778
+ await this.autoDiscoveryPromise;
789
779
  const list = Array.isArray(level) ? level : [level];
790
- await Promise.all(list.map(loadOne));
780
+ await Promise.all(list.map((l) => this.loadOne(l)));
791
781
  }
792
- function route(level, name, params) {
793
- assertDiscoveryReady();
794
- const meta = findRouteMeta(level, name);
795
- if (!meta) {
796
- throw new UnknownRouteError(name, level);
797
- }
798
- return buildRequestUrl(meta, params ?? {});
799
- }
800
- function buildRequestUrl(meta, params) {
801
- const defaults = meta.parameter_defaults ?? {};
802
- const missingRequired = [];
803
- const values = {};
804
- for (const p of meta.parameters) {
805
- let v = params[p];
806
- if ((v === void 0 || v === null) && p in defaults) {
807
- v = defaults[p];
808
- }
809
- if (v === void 0 || v === null) {
810
- if (!meta.uri.includes(`{${p}?}`)) {
811
- missingRequired.push(p);
812
- }
813
- } else {
814
- values[p] = v;
782
+ invalidate(level) {
783
+ if (level === void 0) {
784
+ this.cache.clear();
785
+ this.inflight.clear();
786
+ for (const lvl of this.state.levels) {
787
+ this.invalidationGens.set(lvl, (this.invalidationGens.get(lvl) ?? 0) + 1);
815
788
  }
816
- }
817
- if (missingRequired.length > 0) {
818
- throw new MissingRouteParamError(meta.name, missingRequired);
819
- }
820
- let uri = meta.uri.replace(/\{([^{}]+)\}/g, (match, raw) => {
821
- const optional = raw.endsWith("?");
822
- const name = optional ? raw.slice(0, -1) : raw;
823
- if (values[name] !== void 0) {
824
- const val = values[name];
825
- if (typeof val === "object") {
826
- throw new ForgeError(
827
- `Path parameter "${name}" must be a primitive value (string, number, boolean), got ${typeof val}`,
828
- { code: "RF_FE_003", route: meta.name, context: { param: name, value: val } }
829
- );
830
- }
831
- return encodeURIComponent(String(val));
789
+ } else if (Array.isArray(level)) {
790
+ for (const lvl of level) {
791
+ this.cache.del(lvl);
792
+ this.inflight.delete(lvl);
793
+ this.invalidationGens.set(lvl, (this.invalidationGens.get(lvl) ?? 0) + 1);
832
794
  }
833
- return optional ? "" : match;
834
- });
835
- uri = uri.replace(/\/+/g, "/").replace(/\/$/, "");
836
- if (/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(effectiveUrlPrefix)) {
837
- const prefix2 = effectiveUrlPrefix.endsWith("/") ? effectiveUrlPrefix.slice(0, -1) : effectiveUrlPrefix;
838
- return uri.startsWith("/") ? `${prefix2}${uri}` : `${prefix2}/${uri}`;
795
+ } else {
796
+ this.cache.del(level);
797
+ this.inflight.delete(level);
798
+ this.invalidationGens.set(level, (this.invalidationGens.get(level) ?? 0) + 1);
839
799
  }
840
- const base = baseURL.endsWith("/") ? baseURL.slice(0, -1) : baseURL;
841
- const prefix = effectiveUrlPrefix;
842
- return uri.startsWith("/") ? `${base}${prefix}${uri}` : `${base}${prefix}/${uri}`;
843
800
  }
844
- function findRouteMeta(level, name) {
845
- const entry = cache.get(level);
801
+ isLoaded(level) {
802
+ if (level) return this.cache.get(level) !== void 0;
803
+ return this.state.levels.length > 0 && this.state.levels.every((lvl) => this.cache.get(lvl) !== void 0);
804
+ }
805
+ findRouteMeta(level, name) {
806
+ const entry = this.cache.get(level);
846
807
  const meta = entry?.routes[name];
847
808
  if (meta) {
848
809
  return { ...meta, level };
849
810
  }
850
811
  return void 0;
851
812
  }
852
- function api(level, name, params = {}) {
853
- let ctrl;
854
- let abortedBeforeInit = false;
855
- let abortReason;
856
- const work = (async () => {
857
- ctrl = new AbortController();
858
- if (abortedBeforeInit) {
859
- ctrl.abort(abortReason);
860
- }
861
- await autoDiscoveryPromise;
862
- await load(level);
863
- const meta = findRouteMeta(level, name);
864
- if (!meta) {
865
- throw new UnknownRouteError(name, level);
813
+ getRoutes(level) {
814
+ if (level !== void 0) {
815
+ const entry = this.cache.get(level);
816
+ const routes = entry?.routes ?? {};
817
+ const result2 = {};
818
+ for (const [k, v] of Object.entries(routes)) {
819
+ result2[k] = JSON.parse(JSON.stringify(v));
866
820
  }
867
- return doApiCall(meta, params, ctrl.signal);
868
- })();
869
- const request = work;
870
- request.abort = () => {
871
- if (ctrl) {
872
- ctrl.abort();
873
- } else {
874
- abortedBeforeInit = true;
821
+ return result2;
822
+ }
823
+ const result = {};
824
+ for (const lvl of this.state.levels) {
825
+ const entry = this.cache.get(lvl);
826
+ if (entry) {
827
+ const levelRoutes = {};
828
+ for (const [k, v] of Object.entries(entry.routes)) {
829
+ levelRoutes[k] = JSON.parse(JSON.stringify(v));
830
+ }
831
+ result[lvl] = levelRoutes;
875
832
  }
876
- };
877
- return request;
833
+ }
834
+ return result;
878
835
  }
836
+ };
837
+
838
+ // src/http-runner.ts
839
+ function createHttpRunner(deps) {
840
+ const {
841
+ ensureAdapter,
842
+ requestInterceptors,
843
+ responseInterceptors,
844
+ load,
845
+ findRouteMeta,
846
+ baseURL,
847
+ state,
848
+ timeout,
849
+ loadingTracker,
850
+ autoDiscoveryPromise
851
+ } = deps;
879
852
  async function doApiCall(meta, params, signal) {
880
853
  const {
881
854
  pathParams,
@@ -888,7 +861,10 @@ function createRouteForge(options) {
888
861
  throw new RequestAbortedError(meta.name, meta.level, signal.reason);
889
862
  }
890
863
  const method = pickMethod(meta);
891
- const urlWithQuery = appendQuery(buildRequestUrl(meta, pathParams), query);
864
+ const urlWithQuery = appendQuery(
865
+ buildRequestUrl(meta, pathParams, { baseURL, urlPrefix: state.urlPrefix }),
866
+ query
867
+ );
892
868
  const config = {
893
869
  route: meta.name,
894
870
  level: meta.level ?? "",
@@ -926,7 +902,7 @@ function createRouteForge(options) {
926
902
  },
927
903
  (err) => {
928
904
  if (err instanceof ForgeError) throw err;
929
- if (isAbortError2(err, signal)) {
905
+ if (isAbortError(err, signal)) {
930
906
  throw new RequestAbortedError(meta.name, meta.level, err);
931
907
  }
932
908
  throw new NetworkError(
@@ -943,81 +919,77 @@ function createRouteForge(options) {
943
919
  loadingTracker.stop();
944
920
  }
945
921
  }
946
- function invalidate(level) {
947
- if (level === void 0) {
948
- cache.clear();
949
- inflight.clear();
950
- for (const lvl of effectiveLevels) {
951
- invalidationGens.set(lvl, (invalidationGens.get(lvl) ?? 0) + 1);
952
- }
953
- } else if (Array.isArray(level)) {
954
- for (const lvl of level) {
955
- cache.del(lvl);
956
- inflight.delete(lvl);
957
- invalidationGens.set(lvl, (invalidationGens.get(lvl) ?? 0) + 1);
922
+ return function api(level, name, params = {}) {
923
+ let ctrl;
924
+ let abortedBeforeInit = false;
925
+ let abortReason;
926
+ const work = (async () => {
927
+ ctrl = new AbortController();
928
+ if (abortedBeforeInit) {
929
+ ctrl.abort(abortReason);
958
930
  }
959
- } else {
960
- cache.del(level);
961
- inflight.delete(level);
962
- invalidationGens.set(level, (invalidationGens.get(level) ?? 0) + 1);
963
- }
964
- }
965
- function isLoaded(level) {
966
- if (level) return cache.get(level) !== void 0;
967
- return effectiveLevels.length > 0 && effectiveLevels.every((lvl) => cache.get(lvl) !== void 0);
968
- }
969
- function hasRoute(level, name) {
970
- assertDiscoveryReady();
971
- return findRouteMeta(level, name) !== void 0;
972
- }
973
- function getRoutes(level) {
974
- if (level !== void 0) {
975
- const entry = cache.get(level);
976
- const routes = entry?.routes ?? {};
977
- const result2 = {};
978
- for (const [k, v] of Object.entries(routes)) {
979
- result2[k] = JSON.parse(JSON.stringify(v));
931
+ await autoDiscoveryPromise;
932
+ await load(level);
933
+ const meta = findRouteMeta(level, name);
934
+ if (!meta) {
935
+ throw new UnknownRouteError(name, level);
980
936
  }
981
- return result2;
982
- }
983
- const result = {};
984
- for (const lvl of effectiveLevels) {
985
- const entry = cache.get(lvl);
986
- if (entry) {
987
- const levelRoutes = {};
988
- for (const [k, v] of Object.entries(entry.routes)) {
989
- levelRoutes[k] = JSON.parse(JSON.stringify(v));
990
- }
991
- result[lvl] = levelRoutes;
937
+ return doApiCall(meta, params, ctrl.signal);
938
+ })();
939
+ const request = work;
940
+ request.abort = () => {
941
+ if (ctrl) {
942
+ ctrl.abort();
943
+ } else {
944
+ abortedBeforeInit = true;
992
945
  }
993
- }
994
- return result;
995
- }
996
- void autoDiscoveryPromise.then(() => {
997
- autoDiscoveryCompleted = true;
998
- options.onSummaryReady?.();
999
- if (effectiveEager.length > 0) {
1000
- return Promise.all(effectiveEager.map((lvl) => load(lvl))).catch((e) => {
1001
- console.warn(`[route-forge] eager load failed: ${e.message}`);
1002
- });
1003
- }
1004
- }).then(() => {
1005
- resolveReady(forgeInstance);
1006
- }).catch(() => {
1007
- });
1008
- function ready(onFulfilled, onRejected) {
1009
- if (onFulfilled) {
1010
- const p = readyPromise.then(onFulfilled, onRejected);
1011
- return p.then(() => forgeInstance);
1012
- }
1013
- return readyPromise;
946
+ };
947
+ return request;
948
+ };
949
+ }
950
+
951
+ // src/resolveRouteName.ts
952
+ async function resolveRouteName(forge, level, prefix, suffix, separator = ".") {
953
+ if (!suffix) return prefix;
954
+ const joined = `${prefix}${separator}${suffix}`;
955
+ if (!suffix.startsWith(`${prefix}${separator}`)) return joined;
956
+ await forge.load(level);
957
+ if (forge.hasRoute(level, joined)) return joined;
958
+ if (forge.hasRoute(level, suffix)) return suffix;
959
+ throw new UnknownRouteError(joined, level);
960
+ }
961
+ function resolveRouteNameSync(forge, level, prefix, suffix, separator = ".") {
962
+ if (!suffix) return prefix;
963
+ const joined = `${prefix}${separator}${suffix}`;
964
+ if (!suffix.startsWith(`${prefix}${separator}`)) return joined;
965
+ if (forge.hasRoute(level, joined)) return joined;
966
+ if (forge.hasRoute(level, suffix)) return suffix;
967
+ throw new UnknownRouteError(joined, level);
968
+ }
969
+
970
+ // src/defineImmutableProps.ts
971
+ function defineImmutableProps(target, props) {
972
+ for (const key of Object.keys(props)) {
973
+ const val = props[key];
974
+ Object.defineProperty(target, key, {
975
+ value: val !== null && typeof val === "object" ? Object.freeze(val) : val,
976
+ writable: false,
977
+ enumerable: false,
978
+ configurable: false
979
+ });
1014
980
  }
1015
- const forgeResolver = { load, hasRoute };
981
+ return target;
982
+ }
983
+
984
+ // src/bound-forge.ts
985
+ function createBoundForgeFactory(deps) {
986
+ const { load, api, route, hasRoute, getRoutes, invalidate, isLoaded, loadingTracker } = deps;
987
+ const resolver = { load, hasRoute };
1016
988
  function createBoundForge(level, prefix) {
1017
989
  const levelLoadedPromise = load(level);
1018
990
  levelLoadedPromise.catch(() => {
1019
991
  });
1020
- const apiFn = prefix ? (name, params) => resolveRouteName(forgeResolver, level, prefix, name).then(
992
+ const apiFn = prefix ? (name, params) => resolveRouteName(resolver, level, prefix, name).then(
1021
993
  (resolved) => api(level, resolved, params)
1022
994
  ) : (name, params) => api(level, name, params);
1023
995
  const callable = apiFn;
@@ -1025,8 +997,8 @@ function createRouteForge(options) {
1025
997
  level,
1026
998
  ...prefix !== void 0 ? { prefix } : {},
1027
999
  api: apiFn,
1028
- route: prefix ? (name, params) => route(level, resolveRouteNameSync(forgeResolver, level, prefix, name), params) : (name, params) => route(level, name, params),
1029
- url: prefix ? (name, params) => route(level, resolveRouteNameSync(forgeResolver, level, prefix, name), params) : (name, params) => route(level, name, params),
1000
+ route: prefix ? (name, params) => route(level, resolveRouteNameSync(resolver, level, prefix, name), params) : (name, params) => route(level, name, params),
1001
+ url: prefix ? (name, params) => route(level, resolveRouteNameSync(resolver, level, prefix, name), params) : (name, params) => route(level, name, params),
1030
1002
  hasRoute: (name) => hasRoute(level, name),
1031
1003
  getRoutes: () => getRoutes(level),
1032
1004
  load: () => load(level),
@@ -1070,6 +1042,208 @@ function createRouteForge(options) {
1070
1042
  attachBoundMethods(bound, levelLoadedPromise, level);
1071
1043
  return bound;
1072
1044
  }
1045
+ return createBoundForgeWithMethods;
1046
+ }
1047
+
1048
+ // src/forge.ts
1049
+ var DEFAULT_TIMEOUT = 3e4;
1050
+ var DEFAULT_CACHE_TTL = 3600;
1051
+ function createRouteForge(options) {
1052
+ if (!options.endpoint) throw new TypeError("options.endpoint is required");
1053
+ const {
1054
+ adapter = "auto",
1055
+ timeout = DEFAULT_TIMEOUT,
1056
+ baseURL = "",
1057
+ interceptors: declarativeInterceptors,
1058
+ cache: cacheOpts = {}
1059
+ } = options;
1060
+ const loadingTracker = new LoadingTracker();
1061
+ const explicitLevels = options.levels;
1062
+ const explicitEager = options.eager;
1063
+ const explicitEndpoint = options.endpoint;
1064
+ const discoveryState = {
1065
+ levels: explicitLevels ?? [],
1066
+ eager: explicitEager ?? [],
1067
+ endpoint: explicitEndpoint,
1068
+ urlPrefix: "",
1069
+ summaryUnassigned: void 0,
1070
+ backendHasUnassignedLevel: false
1071
+ };
1072
+ const discoveryInputs = { explicitLevels, explicitEager, explicitEndpoint };
1073
+ let autoDiscoveryCompleted = false;
1074
+ let resolveReady;
1075
+ let rejectReady;
1076
+ const readyPromise = new Promise((resolve, reject) => {
1077
+ resolveReady = resolve;
1078
+ rejectReady = reject;
1079
+ });
1080
+ readyPromise.catch(() => {
1081
+ });
1082
+ const cacheTtl = cacheOpts.ttl ?? DEFAULT_CACHE_TTL;
1083
+ const cacheStorage = cacheOpts.storage ?? "memory";
1084
+ const cache = new RouteCache({ storage: cacheStorage, ttl: cacheTtl });
1085
+ const requestInterceptors = new InterceptorManagerImpl();
1086
+ const responseInterceptors = new InterceptorManagerImpl();
1087
+ if (declarativeInterceptors?.request) {
1088
+ for (const entry of declarativeInterceptors.request) {
1089
+ if (typeof entry === "function") {
1090
+ requestInterceptors.use(entry);
1091
+ } else {
1092
+ const [onFulfilled, onRejected] = entry;
1093
+ requestInterceptors.use(onFulfilled, onRejected);
1094
+ }
1095
+ }
1096
+ }
1097
+ if (declarativeInterceptors?.response) {
1098
+ for (const entry of declarativeInterceptors.response) {
1099
+ if (typeof entry === "function") {
1100
+ responseInterceptors.use(entry);
1101
+ } else {
1102
+ const [onFulfilled, onRejected] = entry;
1103
+ responseInterceptors.use(onFulfilled, onRejected);
1104
+ }
1105
+ }
1106
+ }
1107
+ const adapterPromise = resolveAdapter({
1108
+ adapter,
1109
+ forgeInterceptors: { request: requestInterceptors, response: responseInterceptors }
1110
+ });
1111
+ let adapterResolved = false;
1112
+ let adapterObj = null;
1113
+ async function ensureAdapter() {
1114
+ if (!adapterResolved) {
1115
+ adapterObj = await adapterPromise.catch((e) => {
1116
+ if (e instanceof AdapterNotFoundError) throw e;
1117
+ return resolveAdapter({
1118
+ adapter: "builtin",
1119
+ forgeInterceptors: { request: requestInterceptors, response: responseInterceptors }
1120
+ });
1121
+ });
1122
+ adapterResolved = true;
1123
+ }
1124
+ return adapterObj;
1125
+ }
1126
+ function assertDiscoveryReady() {
1127
+ if (!autoDiscoveryCompleted && !explicitLevels?.length) {
1128
+ throw new ForgeError(
1129
+ "Route data not available. Auto-discovery has not completed. Use forge.ready() or forge.use(level) first.",
1130
+ { code: "RF_FE_010" }
1131
+ );
1132
+ }
1133
+ }
1134
+ async function fetchMeta(routeTag, url, level = "") {
1135
+ const adp = await ensureAdapter();
1136
+ const config = {
1137
+ route: routeTag,
1138
+ level,
1139
+ method: "GET",
1140
+ url,
1141
+ headers: { Accept: "application/json" },
1142
+ params: {},
1143
+ timeout,
1144
+ meta: {
1145
+ name: routeTag,
1146
+ uri: url,
1147
+ methods: ["GET"],
1148
+ parameters: [],
1149
+ level
1150
+ }
1151
+ };
1152
+ const doRawRequest = adp.requestRaw ?? adp.request;
1153
+ const resp = await doRawRequest(config);
1154
+ if (!resp || resp.status < 200 || resp.status >= 300) {
1155
+ throw new HTTPError(
1156
+ `Failed to fetch "${routeTag}": HTTP ${resp?.status}`,
1157
+ { level, status: resp?.status, url, method: "GET" }
1158
+ );
1159
+ }
1160
+ return resp.data;
1161
+ }
1162
+ const autoDiscoveryPromise = fetchSummary(discoveryInputs, baseURL, fetchMeta).then((summary) => {
1163
+ if (summary === null) {
1164
+ return;
1165
+ }
1166
+ applySummaryToState(summary, discoveryState, discoveryInputs);
1167
+ });
1168
+ let autoDiscoveryError = null;
1169
+ autoDiscoveryPromise.catch((e) => {
1170
+ autoDiscoveryError = e;
1171
+ });
1172
+ const store = new RouteStore({
1173
+ cache,
1174
+ state: discoveryState,
1175
+ baseURL,
1176
+ fetchMeta,
1177
+ autoDiscoveryPromise,
1178
+ getAutoDiscoveryError: () => autoDiscoveryError
1179
+ });
1180
+ const load = (level) => store.load(level);
1181
+ const findRouteMeta = (level, name) => store.findRouteMeta(level, name);
1182
+ const invalidate = (level) => store.invalidate(level);
1183
+ const isLoaded = (level) => store.isLoaded(level);
1184
+ function getRoutes(level) {
1185
+ return level === void 0 ? store.getRoutes() : store.getRoutes(level);
1186
+ }
1187
+ function route(level, name, params) {
1188
+ assertDiscoveryReady();
1189
+ const meta = findRouteMeta(level, name);
1190
+ if (!meta) {
1191
+ throw new UnknownRouteError(name, level);
1192
+ }
1193
+ return buildRequestUrl(meta, params ?? {}, { baseURL, urlPrefix: discoveryState.urlPrefix });
1194
+ }
1195
+ const api = createHttpRunner({
1196
+ ensureAdapter,
1197
+ requestInterceptors,
1198
+ responseInterceptors,
1199
+ load,
1200
+ findRouteMeta,
1201
+ baseURL,
1202
+ state: discoveryState,
1203
+ timeout,
1204
+ loadingTracker,
1205
+ autoDiscoveryPromise
1206
+ });
1207
+ function hasRoute(level, name) {
1208
+ assertDiscoveryReady();
1209
+ return findRouteMeta(level, name) !== void 0;
1210
+ }
1211
+ void autoDiscoveryPromise.then(() => {
1212
+ autoDiscoveryCompleted = true;
1213
+ if (discoveryState.eager.length > 0) {
1214
+ return Promise.allSettled(discoveryState.eager.map((lvl) => load(lvl))).then((results) => {
1215
+ results.forEach((r, i) => {
1216
+ if (r.status === "rejected") {
1217
+ console.error(
1218
+ `[route-forge] eager load failed for level "${discoveryState.eager[i]}":`,
1219
+ r.reason
1220
+ );
1221
+ }
1222
+ });
1223
+ });
1224
+ }
1225
+ }).then(() => {
1226
+ resolveReady(forgeInstance);
1227
+ }).catch((e) => {
1228
+ rejectReady(e);
1229
+ });
1230
+ function ready(onFulfilled, onRejected) {
1231
+ if (onFulfilled) {
1232
+ const p = readyPromise.then(onFulfilled, onRejected);
1233
+ return p.then(() => forgeInstance);
1234
+ }
1235
+ return readyPromise;
1236
+ }
1237
+ const createBoundForgeWithMethods = createBoundForgeFactory({
1238
+ load,
1239
+ api,
1240
+ route,
1241
+ hasRoute,
1242
+ getRoutes,
1243
+ invalidate,
1244
+ isLoaded,
1245
+ loadingTracker
1246
+ });
1073
1247
  const forgeInstance = {
1074
1248
  api,
1075
1249
  load,
@@ -1093,70 +1267,6 @@ function createRouteForge(options) {
1093
1267
  };
1094
1268
  return forgeInstance;
1095
1269
  }
1096
- function pickMethod(meta) {
1097
- const m = meta.methods.find((x) => x.toUpperCase() !== "HEAD");
1098
- return (m ?? meta.methods[0] ?? "GET").toUpperCase();
1099
- }
1100
- function appendQuery(url, query) {
1101
- if (!query) return url;
1102
- const usp = new URLSearchParams();
1103
- for (const [k, v] of Object.entries(query)) {
1104
- if (v === void 0 || v === null) continue;
1105
- usp.append(k, String(v));
1106
- }
1107
- const qs = usp.toString();
1108
- if (!qs) return url;
1109
- return url.includes("?") ? `${url}&${qs}` : `${url}?${qs}`;
1110
- }
1111
- function resolveApiParams(input) {
1112
- const {
1113
- params: explicitParams,
1114
- query: rawQuery,
1115
- body: rawBody,
1116
- headers: rawHeaders,
1117
- timeout: perCallTimeout,
1118
- ...flatRest
1119
- } = input;
1120
- const pathParams = explicitParams ? { ...explicitParams } : {};
1121
- for (const [k, v] of Object.entries(flatRest)) {
1122
- if (!(k in pathParams)) {
1123
- pathParams[k] = v;
1124
- }
1125
- }
1126
- let query;
1127
- let body;
1128
- let headers;
1129
- if (rawQuery !== void 0) {
1130
- if (typeof rawQuery === "object" && rawQuery !== null) {
1131
- query = rawQuery;
1132
- } else if (!("query" in pathParams)) {
1133
- pathParams.query = rawQuery;
1134
- }
1135
- }
1136
- if (rawBody !== void 0) {
1137
- if (typeof rawBody !== "string" && typeof rawBody !== "number") {
1138
- body = rawBody;
1139
- } else if (!("body" in pathParams)) {
1140
- pathParams.body = rawBody;
1141
- }
1142
- }
1143
- if (rawHeaders !== void 0) {
1144
- if (typeof rawHeaders === "object" && rawHeaders !== null) {
1145
- headers = rawHeaders;
1146
- } else if (!("headers" in pathParams)) {
1147
- pathParams.headers = rawHeaders;
1148
- }
1149
- }
1150
- return { pathParams, query, body, headers, timeout: perCallTimeout };
1151
- }
1152
- function isAbortError2(err, signal) {
1153
- if (signal?.aborted) return true;
1154
- const e = err;
1155
- if (!e) return false;
1156
- if (e.name === "AbortError") return true;
1157
- if (e.code === "ERR_CANCELED") return true;
1158
- return false;
1159
- }
1160
1270
 
1161
1271
  export { AdapterNotFoundError, ForgeError, HTTPError, InterceptorManagerImpl, InvalidInterceptorReturnError, LoadingTracker, MissingRouteParamError, NetworkError, RequestAbortedError, RouteCache, UnknownLevelError, UnknownRouteError, createInterceptorManager, createRouteForge, resolveRouteName, resolveRouteNameSync };
1162
1272
  //# sourceMappingURL=index.js.map