@route-forge/core 0.3.0 → 1.0.2

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
@@ -26,17 +26,19 @@ var RouteCache = class {
26
26
  return this.getFromMemory(level);
27
27
  }
28
28
  const raw = this.backend.getItem(this.key(level));
29
- if (!raw) return void 0;
30
- try {
31
- const entry = JSON.parse(raw);
32
- if (this.isExpired(entry)) {
33
- this.del(level);
29
+ if (raw) {
30
+ try {
31
+ const entry = JSON.parse(raw);
32
+ if (this.isExpired(entry)) {
33
+ this.del(level);
34
+ return void 0;
35
+ }
36
+ return entry;
37
+ } catch {
34
38
  return void 0;
35
39
  }
36
- return entry;
37
- } catch {
38
- return void 0;
39
40
  }
41
+ return this.getFromMemory(level);
40
42
  }
41
43
  getFromMemory(level) {
42
44
  const entry = this.memory.get(level);
@@ -48,7 +50,12 @@ var RouteCache = class {
48
50
  return entry;
49
51
  }
50
52
  set(resp) {
51
- 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
+ }
52
59
  const entry = {
53
60
  level: resp.level,
54
61
  routes: resp.routes,
@@ -133,14 +140,6 @@ var MissingRouteParamError = class extends ForgeError {
133
140
  });
134
141
  }
135
142
  };
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
143
  var AdapterNotFoundError = class extends ForgeError {
145
144
  constructor(adapter) {
146
145
  super(`Adapter "${adapter}" not available; install axios or use 'builtin'`, {
@@ -241,6 +240,10 @@ function createInterceptorManager() {
241
240
  }
242
241
 
243
242
  // src/adapters/builtin-http.ts
243
+ function isPassthroughBody(body) {
244
+ if (body === null) return false;
245
+ 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;
246
+ }
244
247
  function createBuiltinHttp(forgeInterceptors) {
245
248
  const requestMgr = forgeInterceptors?.request ?? createInterceptorManager();
246
249
  const responseMgr = forgeInterceptors?.response ?? createInterceptorManager();
@@ -254,15 +257,33 @@ function createBuiltinHttp(forgeInterceptors) {
254
257
  url = url.includes("?") ? `${url}&${qs}` : `${url}?${qs}`;
255
258
  }
256
259
  }
260
+ const headers = new Headers(finalConfig.headers);
257
261
  const fetchInit = {
258
262
  method: finalConfig.method,
259
- headers: new Headers(finalConfig.headers)
263
+ headers
260
264
  };
261
265
  if (signal) fetchInit.signal = signal;
262
266
  if (finalConfig.body !== void 0 && !["GET", "HEAD"].includes(finalConfig.method.toUpperCase())) {
263
- fetchInit.body = typeof finalConfig.body === "string" ? finalConfig.body : JSON.stringify(finalConfig.body);
267
+ if (typeof finalConfig.body === "string" || isPassthroughBody(finalConfig.body)) {
268
+ fetchInit.body = finalConfig.body;
269
+ } else {
270
+ fetchInit.body = JSON.stringify(finalConfig.body);
271
+ if (!headers.has("Content-Type")) {
272
+ headers.set("Content-Type", "application/json");
273
+ }
274
+ }
275
+ }
276
+ let res;
277
+ try {
278
+ res = await fetch(url, fetchInit);
279
+ } catch (e) {
280
+ throw new NetworkError(
281
+ e instanceof Error ? e.message : String(e),
282
+ finalConfig.route,
283
+ finalConfig.level,
284
+ e
285
+ );
264
286
  }
265
- const res = await fetch(url, fetchInit);
266
287
  const text = await res.text();
267
288
  let data = text;
268
289
  const contentType = res.headers.get("content-type") ?? "";
@@ -282,9 +303,21 @@ function createBuiltinHttp(forgeInterceptors) {
282
303
  data,
283
304
  config: finalConfig
284
305
  };
306
+ const source = res.status >= 200 && res.status < 300 ? Promise.resolve(responseData) : Promise.reject(
307
+ new HTTPError(
308
+ `HTTP ${res.status} for route "${finalConfig.route}" (${finalConfig.method} ${url})`,
309
+ {
310
+ route: finalConfig.route,
311
+ level: finalConfig.level,
312
+ status: res.status,
313
+ url,
314
+ method: finalConfig.method
315
+ }
316
+ )
317
+ );
285
318
  return runResponseInterceptors(
286
319
  responseMgr,
287
- Promise.resolve(responseData)
320
+ source
288
321
  );
289
322
  }
290
323
  const get = (url, config) => request({ ...config, url, method: "GET" });
@@ -308,6 +341,15 @@ function createBuiltinHttp(forgeInterceptors) {
308
341
  }
309
342
 
310
343
  // src/adapters/axios.ts
344
+ function toHeaders(raw) {
345
+ if (!raw) return new Headers();
346
+ const init = typeof raw.toJSON === "function" ? raw.toJSON() : raw;
347
+ try {
348
+ return new Headers(init);
349
+ } catch {
350
+ return new Headers();
351
+ }
352
+ }
311
353
  async function wrapAxiosAdapter() {
312
354
  let axios;
313
355
  try {
@@ -324,23 +366,41 @@ async function wrapAxiosAdapter() {
324
366
  if (!axios || typeof axios.request !== "function") return null;
325
367
  async function request(config) {
326
368
  const headers = { ...config.headers };
327
- const res = await axios.request({
328
- url: config.url,
329
- method: config.method,
330
- headers,
331
- data: config.body
332
- // axios 会处理 baseURL/transformRequest 等 defaults
333
- });
334
- return {
335
- route: config.route,
336
- level: config.level,
337
- method: config.method,
338
- url: config.url,
339
- status: res.status,
340
- headers: new Headers(res.headers ?? {}),
341
- data: res.data,
342
- config
343
- };
369
+ try {
370
+ const res = await axios.request({
371
+ url: config.url,
372
+ method: config.method,
373
+ headers,
374
+ data: config.body,
375
+ // axios 会处理 baseURL/transformRequest 等 defaults;timeout 透传保证超时语义与 builtin 一致
376
+ timeout: config.timeout
377
+ });
378
+ return {
379
+ route: config.route,
380
+ level: config.level,
381
+ method: config.method,
382
+ url: config.url,
383
+ status: res.status,
384
+ headers: toHeaders(res.headers),
385
+ data: res.data,
386
+ config
387
+ };
388
+ } catch (e) {
389
+ if (e && e.response) {
390
+ const resp = e.response;
391
+ return {
392
+ route: config.route,
393
+ level: config.level,
394
+ method: config.method,
395
+ url: config.url,
396
+ status: resp.status,
397
+ headers: toHeaders(resp.headers),
398
+ data: resp.data,
399
+ config
400
+ };
401
+ }
402
+ throw e;
403
+ }
344
404
  }
345
405
  return { request, interceptors: void 0 };
346
406
  }
@@ -367,6 +427,65 @@ async function resolveAdapter(opts) {
367
427
  return createBuiltinHttp(opts.forgeInterceptors);
368
428
  }
369
429
 
430
+ // src/loading.ts
431
+ var LoadingTracker = class {
432
+ constructor() {
433
+ /** 当前并发请求计数 */
434
+ this.count = 0;
435
+ /** 订阅者集合 */
436
+ this.subscribers = /* @__PURE__ */ new Set();
437
+ }
438
+ /**
439
+ * 开始一次加载(计数器 +1)
440
+ */
441
+ start() {
442
+ this.count++;
443
+ this.notify();
444
+ }
445
+ /**
446
+ * 结束一次加载(计数器 -1)
447
+ */
448
+ stop() {
449
+ this.count = Math.max(0, this.count - 1);
450
+ this.notify();
451
+ }
452
+ /**
453
+ * 查询当前是否处于加载中
454
+ */
455
+ isLoading() {
456
+ return this.count > 0;
457
+ }
458
+ /**
459
+ * 获取当前并发计数
460
+ */
461
+ getCount() {
462
+ return this.count;
463
+ }
464
+ /**
465
+ * 订阅加载状态变更
466
+ * @returns 取消订阅函数
467
+ */
468
+ subscribe(cb) {
469
+ this.subscribers.add(cb);
470
+ return () => {
471
+ this.subscribers.delete(cb);
472
+ };
473
+ }
474
+ /** 通知所有订阅者 */
475
+ notify() {
476
+ const event = {
477
+ loading: this.count > 0,
478
+ count: this.count
479
+ };
480
+ for (const cb of this.subscribers) {
481
+ try {
482
+ cb(event);
483
+ } catch {
484
+ }
485
+ }
486
+ }
487
+ };
488
+
370
489
  // src/forge.ts
371
490
  var DEFAULT_TIMEOUT = 3e4;
372
491
  var DEFAULT_CACHE_TTL = 3600;
@@ -376,10 +495,10 @@ function createRouteForge(options) {
376
495
  adapter = "auto",
377
496
  timeout = DEFAULT_TIMEOUT,
378
497
  baseURL = "",
379
- auth,
380
498
  interceptors: declarativeInterceptors,
381
499
  cache: cacheOpts = {}
382
500
  } = options;
501
+ const loadingTracker = new LoadingTracker();
383
502
  const explicitLevels = options.levels;
384
503
  const explicitEager = options.eager;
385
504
  const explicitStrict = options.strict ?? false;
@@ -413,6 +532,12 @@ function createRouteForge(options) {
413
532
  if (summary === null) {
414
533
  return;
415
534
  }
535
+ const schemaVersion = summary.schemaVersion ?? 1;
536
+ if (schemaVersion > 1) {
537
+ console.warn(
538
+ `[route-forge] backend schemaVersion=${schemaVersion} > client supported 1; some features may be unavailable`
539
+ );
540
+ }
416
541
  if (summary.config.endpoint_prefix && summary.config.endpoint_prefix !== explicitEndpoint) {
417
542
  console.warn(
418
543
  `[route-forge] backend endpoint_prefix "${summary.config.endpoint_prefix}" overrides frontend endpoint "${explicitEndpoint}"`
@@ -440,8 +565,12 @@ function createRouteForge(options) {
440
565
  } else {
441
566
  effectiveLevels = backendLevels;
442
567
  }
568
+ const backendEager = backendLevels.filter((lvl) => summary.levels[lvl]?.load === "eager");
443
569
  if (!explicitEager) {
444
- effectiveEager = backendLevels.filter((lvl) => summary.levels[lvl]?.load === "eager");
570
+ effectiveEager = backendEager;
571
+ } else {
572
+ const union = /* @__PURE__ */ new Set([...backendEager, ...explicitEager]);
573
+ effectiveEager = [...union];
445
574
  }
446
575
  });
447
576
  autoDiscoveryPromise.catch(() => {
@@ -452,15 +581,23 @@ function createRouteForge(options) {
452
581
  const requestInterceptors = new InterceptorManagerImpl();
453
582
  const responseInterceptors = new InterceptorManagerImpl();
454
583
  if (declarativeInterceptors?.request) {
455
- for (const pair of declarativeInterceptors.request) {
456
- const [onFulfilled, onRejected] = pair;
457
- requestInterceptors.use(onFulfilled, onRejected);
584
+ for (const entry of declarativeInterceptors.request) {
585
+ if (typeof entry === "function") {
586
+ requestInterceptors.use(entry);
587
+ } else {
588
+ const [onFulfilled, onRejected] = entry;
589
+ requestInterceptors.use(onFulfilled, onRejected);
590
+ }
458
591
  }
459
592
  }
460
593
  if (declarativeInterceptors?.response) {
461
- for (const pair of declarativeInterceptors.response) {
462
- const [onFulfilled, onRejected] = pair;
463
- responseInterceptors.use(onFulfilled, onRejected);
594
+ for (const entry of declarativeInterceptors.response) {
595
+ if (typeof entry === "function") {
596
+ responseInterceptors.use(entry);
597
+ } else {
598
+ const [onFulfilled, onRejected] = entry;
599
+ responseInterceptors.use(onFulfilled, onRejected);
600
+ }
464
601
  }
465
602
  }
466
603
  const adapterPromise = resolveAdapter({
@@ -480,14 +617,6 @@ function createRouteForge(options) {
480
617
  return adapterObj;
481
618
  }
482
619
  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
620
  function assertLevelDeclared(level) {
492
621
  if (!effectiveLevels.includes(level)) {
493
622
  throw new UnknownLevelError(level);
@@ -518,14 +647,16 @@ function createRouteForge(options) {
518
647
  };
519
648
  const resp = await adp.request(config);
520
649
  if (!resp || resp.status < 200 || resp.status >= 300) {
521
- throw new Error(`Failed to load level ${level}: HTTP ${resp?.status}`);
650
+ throw new HTTPError(
651
+ `Failed to load level "${level}": HTTP ${resp?.status}`,
652
+ { level, status: resp?.status, url: buildUrl(level), method: "GET" }
653
+ );
522
654
  }
523
655
  const data = resp.data;
524
656
  return data;
525
657
  }
526
658
  async function loadOne(level) {
527
659
  assertLevelDeclared(level);
528
- assertAuth(level);
529
660
  if (cache.get(level)) return;
530
661
  const existing = inflight.get(level);
531
662
  if (existing) return existing;
@@ -553,27 +684,33 @@ function createRouteForge(options) {
553
684
  return buildRequestUrl(meta, params ?? {});
554
685
  }
555
686
  function buildRequestUrl(meta, params) {
556
- let uri = meta.uri;
557
687
  const defaults = meta.parameter_defaults ?? {};
558
688
  const missingRequired = [];
689
+ const values = {};
559
690
  for (const p of meta.parameters) {
560
691
  let v = params[p];
561
692
  if ((v === void 0 || v === null) && p in defaults) {
562
693
  v = defaults[p];
563
694
  }
564
695
  if (v === void 0 || v === null) {
565
- if (uri.includes(`{${p}?}`)) {
566
- uri = uri.replace(`{${p}?}`, "");
567
- continue;
696
+ if (!meta.uri.includes(`{${p}?}`)) {
697
+ missingRequired.push(p);
568
698
  }
569
- missingRequired.push(p);
570
699
  } else {
571
- uri = uri.replace(`{${p}?}`, encodeURIComponent(String(v))).replace(`{${p}}`, encodeURIComponent(String(v)));
700
+ values[p] = v;
572
701
  }
573
702
  }
574
703
  if (missingRequired.length > 0) {
575
704
  throw new MissingRouteParamError(meta.name, missingRequired);
576
705
  }
706
+ let uri = meta.uri.replace(/\{([^{}]+)\}/g, (match, raw) => {
707
+ const optional = raw.endsWith("?");
708
+ const name = optional ? raw.slice(0, -1) : raw;
709
+ if (values[name] !== void 0) {
710
+ return encodeURIComponent(String(values[name]));
711
+ }
712
+ return optional ? "" : match;
713
+ });
577
714
  uri = uri.replace(/\/+/g, "/").replace(/\/$/, "");
578
715
  if (effectiveUrlPrefix.includes("://")) {
579
716
  const prefix2 = effectiveUrlPrefix.endsWith("/") ? effectiveUrlPrefix.slice(0, -1) : effectiveUrlPrefix;
@@ -601,8 +738,7 @@ function createRouteForge(options) {
601
738
  return doApiCall(meta, params);
602
739
  }
603
740
  async function doApiCall(meta, params) {
604
- assertAuth(meta.level ?? "");
605
- const { pathParams, query, body, headers } = resolveApiParams(params);
741
+ const { pathParams, query, body, headers, timeout: perCallTimeout } = resolveApiParams(params);
606
742
  const method = pickMethod(meta);
607
743
  const urlWithQuery = appendQuery(buildRequestUrl(meta, pathParams), query);
608
744
  const config = {
@@ -613,47 +749,57 @@ function createRouteForge(options) {
613
749
  headers: { Accept: "application/json", ...headers ?? {} },
614
750
  body,
615
751
  params: pathParams,
616
- timeout,
752
+ timeout: perCallTimeout ?? timeout,
617
753
  meta
618
754
  };
619
755
  const adp = await ensureAdapter();
620
756
  const finalConfig = adp.runsInterceptors ? config : await runRequestInterceptors(requestInterceptors, config);
621
- const source = adp.request(finalConfig).then(
622
- (resp) => {
623
- if (resp.status < 200 || resp.status >= 300) {
624
- throw new HTTPError(
625
- `HTTP ${resp.status} for route "${resp.route}" (${resp.method} ${resp.url})`,
626
- {
627
- route: resp.route,
628
- level: resp.level,
629
- status: resp.status,
630
- url: resp.url,
631
- method: resp.method
632
- }
757
+ loadingTracker.start();
758
+ try {
759
+ const source = adp.request(finalConfig).then(
760
+ (resp) => {
761
+ if (resp.status < 200 || resp.status >= 300) {
762
+ throw new HTTPError(
763
+ `HTTP ${resp.status} for route "${resp.route}" (${resp.method} ${resp.url})`,
764
+ {
765
+ route: resp.route,
766
+ level: resp.level,
767
+ status: resp.status,
768
+ url: resp.url,
769
+ method: resp.method
770
+ }
771
+ );
772
+ }
773
+ return resp;
774
+ },
775
+ (err) => {
776
+ if (err instanceof ForgeError) throw err;
777
+ throw new NetworkError(
778
+ err instanceof Error ? err.message : String(err),
779
+ meta.name,
780
+ meta.level,
781
+ err
633
782
  );
634
783
  }
635
- return resp;
636
- },
637
- (err) => {
638
- if (err instanceof ForgeError) throw err;
639
- throw new NetworkError(
640
- err instanceof Error ? err.message : String(err),
641
- meta.name,
642
- meta.level,
643
- err
644
- );
645
- }
646
- );
647
- if (adp.runsInterceptors) return source;
648
- return runResponseInterceptors(responseInterceptors, source);
784
+ );
785
+ const result = adp.runsInterceptors ? await source : await runResponseInterceptors(responseInterceptors, source);
786
+ return result;
787
+ } finally {
788
+ loadingTracker.stop();
789
+ }
649
790
  }
650
791
  function invalidate(level) {
651
- if (level) cache.del(level);
652
- else cache.clear();
792
+ if (level === void 0) {
793
+ cache.clear();
794
+ } else if (Array.isArray(level)) {
795
+ for (const lvl of level) cache.del(lvl);
796
+ } else {
797
+ cache.del(level);
798
+ }
653
799
  }
654
800
  function isLoaded(level) {
655
801
  if (level) return cache.get(level) !== void 0;
656
- return effectiveLevels.every((lvl) => cache.get(lvl) !== void 0);
802
+ return effectiveLevels.length > 0 && effectiveLevels.every((lvl) => cache.get(lvl) !== void 0);
657
803
  }
658
804
  function hasRoute(level, name) {
659
805
  return findRouteMeta(level, name) !== void 0;
@@ -698,6 +844,8 @@ function createRouteForge(options) {
698
844
  isLoaded,
699
845
  hasRoute,
700
846
  getRoutes,
847
+ isLoading: () => loadingTracker.isLoading(),
848
+ onLoadingChange: (cb) => loadingTracker.subscribe(cb),
701
849
  interceptors: {
702
850
  request: requestInterceptors,
703
851
  response: responseInterceptors
@@ -725,6 +873,7 @@ function resolveApiParams(input) {
725
873
  query: rawQuery,
726
874
  body: rawBody,
727
875
  headers: rawHeaders,
876
+ timeout: perCallTimeout,
728
877
  ...flatRest
729
878
  } = input;
730
879
  const pathParams = explicitParams ? { ...explicitParams } : {};
@@ -757,9 +906,9 @@ function resolveApiParams(input) {
757
906
  pathParams.headers = rawHeaders;
758
907
  }
759
908
  }
760
- return { pathParams, query, body, headers };
909
+ return { pathParams, query, body, headers, timeout: perCallTimeout };
761
910
  }
762
911
 
763
- export { AdapterNotFoundError, ForgeError, HTTPError, InsufficientAuthError, InterceptorManagerImpl, InvalidInterceptorReturnError, MissingRouteParamError, NetworkError, RouteCache, UnknownLevelError, UnknownRouteError, createInterceptorManager, createRouteForge };
912
+ export { AdapterNotFoundError, ForgeError, HTTPError, InterceptorManagerImpl, InvalidInterceptorReturnError, LoadingTracker, MissingRouteParamError, NetworkError, RouteCache, UnknownLevelError, UnknownRouteError, createInterceptorManager, createRouteForge };
764
913
  //# sourceMappingURL=index.js.map
765
914
  //# sourceMappingURL=index.js.map