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