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