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