@route-forge/core 0.2.0 → 1.0.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
@@ -50,7 +50,12 @@ var RouteCache = class {
50
50
  return entry;
51
51
  }
52
52
  set(resp) {
53
- const ttl = resp.cache !== void 0 && resp.cache !== null ? resp.cache : this.fallbackTtl;
53
+ let ttl;
54
+ if (resp.cache !== void 0 && resp.cache !== null) {
55
+ ttl = resp.cache > 0 ? Math.min(resp.cache, this.fallbackTtl) : resp.cache;
56
+ } else {
57
+ ttl = this.fallbackTtl;
58
+ }
54
59
  const entry = {
55
60
  level: resp.level,
56
61
  routes: resp.routes,
@@ -135,14 +140,6 @@ var MissingRouteParamError = class extends ForgeError {
135
140
  });
136
141
  }
137
142
  };
138
- var InsufficientAuthError = class extends ForgeError {
139
- constructor(level) {
140
- super(`Insufficient auth: level "${level}" requires login`, {
141
- code: "RF_FE_004",
142
- level
143
- });
144
- }
145
- };
146
143
  var AdapterNotFoundError = class extends ForgeError {
147
144
  constructor(adapter) {
148
145
  super(`Adapter "${adapter}" not available; install axios or use 'builtin'`, {
@@ -369,6 +366,65 @@ async function resolveAdapter(opts) {
369
366
  return createBuiltinHttp(opts.forgeInterceptors);
370
367
  }
371
368
 
369
+ // src/loading.ts
370
+ var LoadingTracker = class {
371
+ constructor() {
372
+ /** 当前并发请求计数 */
373
+ this.count = 0;
374
+ /** 订阅者集合 */
375
+ this.subscribers = /* @__PURE__ */ new Set();
376
+ }
377
+ /**
378
+ * 开始一次加载(计数器 +1)
379
+ */
380
+ start() {
381
+ this.count++;
382
+ this.notify();
383
+ }
384
+ /**
385
+ * 结束一次加载(计数器 -1)
386
+ */
387
+ stop() {
388
+ this.count = Math.max(0, this.count - 1);
389
+ this.notify();
390
+ }
391
+ /**
392
+ * 查询当前是否处于加载中
393
+ */
394
+ isLoading() {
395
+ return this.count > 0;
396
+ }
397
+ /**
398
+ * 获取当前并发计数
399
+ */
400
+ getCount() {
401
+ return this.count;
402
+ }
403
+ /**
404
+ * 订阅加载状态变更
405
+ * @returns 取消订阅函数
406
+ */
407
+ subscribe(cb) {
408
+ this.subscribers.add(cb);
409
+ return () => {
410
+ this.subscribers.delete(cb);
411
+ };
412
+ }
413
+ /** 通知所有订阅者 */
414
+ notify() {
415
+ const event = {
416
+ loading: this.count > 0,
417
+ count: this.count
418
+ };
419
+ for (const cb of this.subscribers) {
420
+ try {
421
+ cb(event);
422
+ } catch {
423
+ }
424
+ }
425
+ }
426
+ };
427
+
372
428
  // src/forge.ts
373
429
  var DEFAULT_TIMEOUT = 3e4;
374
430
  var DEFAULT_CACHE_TTL = 3600;
@@ -378,18 +434,18 @@ function createRouteForge(options) {
378
434
  adapter = "auto",
379
435
  timeout = DEFAULT_TIMEOUT,
380
436
  baseURL = "",
381
- auth,
382
437
  interceptors: declarativeInterceptors,
383
438
  cache: cacheOpts = {}
384
439
  } = options;
440
+ const loadingTracker = new LoadingTracker();
385
441
  const explicitLevels = options.levels;
386
442
  const explicitEager = options.eager;
387
443
  const explicitStrict = options.strict ?? false;
388
444
  const explicitEndpoint = options.endpoint;
389
445
  let effectiveLevels = explicitLevels ?? [];
390
446
  let effectiveEager = explicitEager ?? [];
391
- let effectiveStrict = explicitStrict;
392
447
  let effectiveEndpoint = explicitEndpoint;
448
+ let effectiveUrlPrefix = "";
393
449
  const summaryPromise = (async () => {
394
450
  try {
395
451
  const summaryUrl = explicitEndpoint;
@@ -421,11 +477,13 @@ function createRouteForge(options) {
421
477
  );
422
478
  effectiveEndpoint = summary.config.endpoint_prefix;
423
479
  }
480
+ if (summary.config.url_prefix) {
481
+ effectiveUrlPrefix = summary.config.url_prefix.endsWith("/") ? summary.config.url_prefix.slice(0, -1) : summary.config.url_prefix;
482
+ }
424
483
  if (summary.config.strict_mode && !explicitStrict) {
425
484
  console.warn(
426
485
  "[route-forge] backend strict_mode=true overrides frontend strict=false; forcing strict=true"
427
486
  );
428
- effectiveStrict = true;
429
487
  }
430
488
  const backendLevels = Object.keys(summary.levels);
431
489
  if (explicitLevels && explicitLevels.length > 0) {
@@ -440,8 +498,12 @@ function createRouteForge(options) {
440
498
  } else {
441
499
  effectiveLevels = backendLevels;
442
500
  }
501
+ const backendEager = backendLevels.filter((lvl) => summary.levels[lvl]?.load === "eager");
443
502
  if (!explicitEager) {
444
- effectiveEager = backendLevels.filter((lvl) => summary.levels[lvl]?.load === "eager");
503
+ effectiveEager = backendEager;
504
+ } else {
505
+ const union = /* @__PURE__ */ new Set([...backendEager, ...explicitEager]);
506
+ effectiveEager = [...union];
445
507
  }
446
508
  });
447
509
  autoDiscoveryPromise.catch(() => {
@@ -452,15 +514,23 @@ function createRouteForge(options) {
452
514
  const requestInterceptors = new InterceptorManagerImpl();
453
515
  const responseInterceptors = new InterceptorManagerImpl();
454
516
  if (declarativeInterceptors?.request) {
455
- for (const pair of declarativeInterceptors.request) {
456
- const [onFulfilled, onRejected] = pair;
457
- requestInterceptors.use(onFulfilled, onRejected);
517
+ for (const entry of declarativeInterceptors.request) {
518
+ if (typeof entry === "function") {
519
+ requestInterceptors.use(entry);
520
+ } else {
521
+ const [onFulfilled, onRejected] = entry;
522
+ requestInterceptors.use(onFulfilled, onRejected);
523
+ }
458
524
  }
459
525
  }
460
526
  if (declarativeInterceptors?.response) {
461
- for (const pair of declarativeInterceptors.response) {
462
- const [onFulfilled, onRejected] = pair;
463
- responseInterceptors.use(onFulfilled, onRejected);
527
+ for (const entry of declarativeInterceptors.response) {
528
+ if (typeof entry === "function") {
529
+ responseInterceptors.use(entry);
530
+ } else {
531
+ const [onFulfilled, onRejected] = entry;
532
+ responseInterceptors.use(onFulfilled, onRejected);
533
+ }
464
534
  }
465
535
  }
466
536
  const adapterPromise = resolveAdapter({
@@ -480,14 +550,6 @@ function createRouteForge(options) {
480
550
  return adapterObj;
481
551
  }
482
552
  const inflight = /* @__PURE__ */ new Map();
483
- function isAuthRequired(level) {
484
- return Boolean(auth?.levels?.[level]);
485
- }
486
- function assertAuth(level) {
487
- if (isAuthRequired(level) && auth?.state && !auth.state()) {
488
- throw new InsufficientAuthError(level);
489
- }
490
- }
491
553
  function assertLevelDeclared(level) {
492
554
  if (!effectiveLevels.includes(level)) {
493
555
  throw new UnknownLevelError(level);
@@ -525,7 +587,6 @@ function createRouteForge(options) {
525
587
  }
526
588
  async function loadOne(level) {
527
589
  assertLevelDeclared(level);
528
- assertAuth(level);
529
590
  if (cache.get(level)) return;
530
591
  const existing = inflight.get(level);
531
592
  if (existing) return existing;
@@ -548,24 +609,40 @@ function createRouteForge(options) {
548
609
  function route(level, name, params) {
549
610
  const meta = findRouteMeta(level, name);
550
611
  if (!meta) {
551
- if (effectiveStrict) throw new UnknownRouteError(name, level);
552
- return "";
612
+ throw new UnknownRouteError(name, level);
553
613
  }
554
614
  return buildRequestUrl(meta, params ?? {});
555
615
  }
556
616
  function buildRequestUrl(meta, params) {
557
617
  let uri = meta.uri;
618
+ const defaults = meta.parameter_defaults ?? {};
619
+ const missingRequired = [];
558
620
  for (const p of meta.parameters) {
559
- const v = params[p];
560
- if (v === void 0) {
561
- if (effectiveStrict) throw new MissingRouteParamError(meta.name, [p]);
562
- uri = uri.replace(`{${p}}`, "");
621
+ let v = params[p];
622
+ if ((v === void 0 || v === null) && p in defaults) {
623
+ v = defaults[p];
624
+ }
625
+ if (v === void 0 || v === null) {
626
+ if (uri.includes(`{${p}?}`)) {
627
+ uri = uri.replace(`{${p}?}`, "");
628
+ continue;
629
+ }
630
+ missingRequired.push(p);
563
631
  } else {
564
- uri = uri.replace(`{${p}}`, encodeURIComponent(String(v)));
632
+ uri = uri.replace(`{${p}?}`, encodeURIComponent(String(v))).replace(`{${p}}`, encodeURIComponent(String(v)));
565
633
  }
566
634
  }
635
+ if (missingRequired.length > 0) {
636
+ throw new MissingRouteParamError(meta.name, missingRequired);
637
+ }
638
+ uri = uri.replace(/\/+/g, "/").replace(/\/$/, "");
639
+ if (effectiveUrlPrefix.includes("://")) {
640
+ const prefix2 = effectiveUrlPrefix.endsWith("/") ? effectiveUrlPrefix.slice(0, -1) : effectiveUrlPrefix;
641
+ return uri.startsWith("/") ? `${prefix2}${uri}` : `${prefix2}/${uri}`;
642
+ }
567
643
  const base = baseURL.endsWith("/") ? baseURL.slice(0, -1) : baseURL;
568
- return uri.startsWith("/") ? `${base}${uri}` : `${base}/${uri}`;
644
+ const prefix = effectiveUrlPrefix;
645
+ return uri.startsWith("/") ? `${base}${prefix}${uri}` : `${base}${prefix}/${uri}`;
569
646
  }
570
647
  function findRouteMeta(level, name) {
571
648
  const entry = cache.get(level);
@@ -580,19 +657,12 @@ function createRouteForge(options) {
580
657
  await load(level);
581
658
  const meta = findRouteMeta(level, name);
582
659
  if (!meta) {
583
- if (effectiveStrict) throw new UnknownRouteError(name, level);
584
- return void 0;
660
+ throw new UnknownRouteError(name, level);
585
661
  }
586
662
  return doApiCall(meta, params);
587
663
  }
588
664
  async function doApiCall(meta, params) {
589
- assertAuth(meta.level ?? "");
590
- const { query, body, headers, ...pathParams } = params;
591
- for (const p of meta.parameters) {
592
- if (pathParams[p] === void 0) {
593
- if (effectiveStrict) throw new MissingRouteParamError(meta.name, [p]);
594
- }
595
- }
665
+ const { pathParams, query, body, headers } = resolveApiParams(params);
596
666
  const method = pickMethod(meta);
597
667
  const urlWithQuery = appendQuery(buildRequestUrl(meta, pathParams), query);
598
668
  const config = {
@@ -608,39 +678,74 @@ function createRouteForge(options) {
608
678
  };
609
679
  const adp = await ensureAdapter();
610
680
  const finalConfig = adp.runsInterceptors ? config : await runRequestInterceptors(requestInterceptors, config);
611
- const source = adp.request(finalConfig).then(
612
- (resp) => {
613
- if (resp.status < 200 || resp.status >= 300) {
614
- throw new HTTPError(
615
- `HTTP ${resp.status} for route "${resp.route}" (${resp.method} ${resp.url})`,
616
- {
617
- route: resp.route,
618
- level: resp.level,
619
- status: resp.status,
620
- url: resp.url,
621
- method: resp.method
622
- }
681
+ loadingTracker.start();
682
+ try {
683
+ const source = adp.request(finalConfig).then(
684
+ (resp) => {
685
+ if (resp.status < 200 || resp.status >= 300) {
686
+ throw new HTTPError(
687
+ `HTTP ${resp.status} for route "${resp.route}" (${resp.method} ${resp.url})`,
688
+ {
689
+ route: resp.route,
690
+ level: resp.level,
691
+ status: resp.status,
692
+ url: resp.url,
693
+ method: resp.method
694
+ }
695
+ );
696
+ }
697
+ return resp;
698
+ },
699
+ (err) => {
700
+ if (err instanceof ForgeError) throw err;
701
+ throw new NetworkError(
702
+ err instanceof Error ? err.message : String(err),
703
+ meta.name,
704
+ meta.level,
705
+ err
623
706
  );
624
707
  }
625
- return resp;
626
- },
627
- (err) => {
628
- if (err instanceof ForgeError) throw err;
629
- throw new NetworkError(
630
- err instanceof Error ? err.message : String(err),
631
- meta.name,
632
- meta.level,
633
- err
634
- );
635
- }
636
- );
637
- if (adp.runsInterceptors) return source;
638
- return runResponseInterceptors(responseInterceptors, source);
708
+ );
709
+ const result = adp.runsInterceptors ? await source : await runResponseInterceptors(responseInterceptors, source);
710
+ return result;
711
+ } finally {
712
+ loadingTracker.stop();
713
+ }
639
714
  }
640
715
  function invalidate(level) {
641
716
  if (level) cache.del(level);
642
717
  else cache.clear();
643
718
  }
719
+ function isLoaded(level) {
720
+ if (level) return cache.get(level) !== void 0;
721
+ return effectiveLevels.every((lvl) => cache.get(lvl) !== void 0);
722
+ }
723
+ function hasRoute(level, name) {
724
+ return findRouteMeta(level, name) !== void 0;
725
+ }
726
+ function getRoutes(level) {
727
+ if (level !== void 0) {
728
+ const entry = cache.get(level);
729
+ const routes = entry?.routes ?? {};
730
+ const result2 = {};
731
+ for (const [k, v] of Object.entries(routes)) {
732
+ result2[k] = { ...v };
733
+ }
734
+ return result2;
735
+ }
736
+ const result = {};
737
+ for (const lvl of effectiveLevels) {
738
+ const entry = cache.get(lvl);
739
+ if (entry) {
740
+ const levelRoutes = {};
741
+ for (const [k, v] of Object.entries(entry.routes)) {
742
+ levelRoutes[k] = { ...v };
743
+ }
744
+ result[lvl] = levelRoutes;
745
+ }
746
+ }
747
+ return result;
748
+ }
644
749
  void autoDiscoveryPromise.then(() => {
645
750
  if (effectiveEager.length > 0) {
646
751
  void Promise.all(effectiveEager.map((lvl) => load(lvl))).catch((e) => {
@@ -653,7 +758,13 @@ function createRouteForge(options) {
653
758
  api,
654
759
  load,
655
760
  route,
761
+ url: route,
656
762
  invalidate,
763
+ isLoaded,
764
+ hasRoute,
765
+ getRoutes,
766
+ isLoading: () => loadingTracker.isLoading(),
767
+ onLoadingChange: (cb) => loadingTracker.subscribe(cb),
657
768
  interceptors: {
658
769
  request: requestInterceptors,
659
770
  response: responseInterceptors
@@ -675,13 +786,53 @@ function appendQuery(url, query) {
675
786
  if (!qs) return url;
676
787
  return url.includes("?") ? `${url}&${qs}` : `${url}?${qs}`;
677
788
  }
789
+ function resolveApiParams(input) {
790
+ const {
791
+ params: explicitParams,
792
+ query: rawQuery,
793
+ body: rawBody,
794
+ headers: rawHeaders,
795
+ ...flatRest
796
+ } = input;
797
+ const pathParams = explicitParams ? { ...explicitParams } : {};
798
+ for (const [k, v] of Object.entries(flatRest)) {
799
+ if (!(k in pathParams)) {
800
+ pathParams[k] = v;
801
+ }
802
+ }
803
+ let query;
804
+ let body;
805
+ let headers;
806
+ if (rawQuery !== void 0) {
807
+ if (typeof rawQuery === "object" && rawQuery !== null) {
808
+ query = rawQuery;
809
+ } else if (!("query" in pathParams)) {
810
+ pathParams.query = rawQuery;
811
+ }
812
+ }
813
+ if (rawBody !== void 0) {
814
+ if (typeof rawBody !== "string" && typeof rawBody !== "number") {
815
+ body = rawBody;
816
+ } else if (!("body" in pathParams)) {
817
+ pathParams.body = rawBody;
818
+ }
819
+ }
820
+ if (rawHeaders !== void 0) {
821
+ if (typeof rawHeaders === "object" && rawHeaders !== null) {
822
+ headers = rawHeaders;
823
+ } else if (!("headers" in pathParams)) {
824
+ pathParams.headers = rawHeaders;
825
+ }
826
+ }
827
+ return { pathParams, query, body, headers };
828
+ }
678
829
 
679
830
  exports.AdapterNotFoundError = AdapterNotFoundError;
680
831
  exports.ForgeError = ForgeError;
681
832
  exports.HTTPError = HTTPError;
682
- exports.InsufficientAuthError = InsufficientAuthError;
683
833
  exports.InterceptorManagerImpl = InterceptorManagerImpl;
684
834
  exports.InvalidInterceptorReturnError = InvalidInterceptorReturnError;
835
+ exports.LoadingTracker = LoadingTracker;
685
836
  exports.MissingRouteParamError = MissingRouteParamError;
686
837
  exports.NetworkError = NetworkError;
687
838
  exports.RouteCache = RouteCache;