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