@route-forge/core 1.0.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);
@@ -238,6 +240,10 @@ function createInterceptorManager() {
238
240
  }
239
241
 
240
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
+ }
241
247
  function createBuiltinHttp(forgeInterceptors) {
242
248
  const requestMgr = forgeInterceptors?.request ?? createInterceptorManager();
243
249
  const responseMgr = forgeInterceptors?.response ?? createInterceptorManager();
@@ -251,15 +257,33 @@ function createBuiltinHttp(forgeInterceptors) {
251
257
  url = url.includes("?") ? `${url}&${qs}` : `${url}?${qs}`;
252
258
  }
253
259
  }
260
+ const headers = new Headers(finalConfig.headers);
254
261
  const fetchInit = {
255
262
  method: finalConfig.method,
256
- headers: new Headers(finalConfig.headers)
263
+ headers
257
264
  };
258
265
  if (signal) fetchInit.signal = signal;
259
266
  if (finalConfig.body !== void 0 && !["GET", "HEAD"].includes(finalConfig.method.toUpperCase())) {
260
- 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
+ );
261
286
  }
262
- const res = await fetch(url, fetchInit);
263
287
  const text = await res.text();
264
288
  let data = text;
265
289
  const contentType = res.headers.get("content-type") ?? "";
@@ -279,9 +303,21 @@ function createBuiltinHttp(forgeInterceptors) {
279
303
  data,
280
304
  config: finalConfig
281
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
+ );
282
318
  return runResponseInterceptors(
283
319
  responseMgr,
284
- Promise.resolve(responseData)
320
+ source
285
321
  );
286
322
  }
287
323
  const get = (url, config) => request({ ...config, url, method: "GET" });
@@ -305,6 +341,15 @@ function createBuiltinHttp(forgeInterceptors) {
305
341
  }
306
342
 
307
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
+ }
308
353
  async function wrapAxiosAdapter() {
309
354
  let axios;
310
355
  try {
@@ -321,23 +366,41 @@ async function wrapAxiosAdapter() {
321
366
  if (!axios || typeof axios.request !== "function") return null;
322
367
  async function request(config) {
323
368
  const headers = { ...config.headers };
324
- const res = await axios.request({
325
- url: config.url,
326
- method: config.method,
327
- headers,
328
- data: config.body
329
- // axios 会处理 baseURL/transformRequest 等 defaults
330
- });
331
- return {
332
- route: config.route,
333
- level: config.level,
334
- method: config.method,
335
- url: config.url,
336
- status: res.status,
337
- headers: new Headers(res.headers ?? {}),
338
- data: res.data,
339
- config
340
- };
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
+ }
341
404
  }
342
405
  return { request, interceptors: void 0 };
343
406
  }
@@ -469,6 +532,12 @@ function createRouteForge(options) {
469
532
  if (summary === null) {
470
533
  return;
471
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
+ }
472
541
  if (summary.config.endpoint_prefix && summary.config.endpoint_prefix !== explicitEndpoint) {
473
542
  console.warn(
474
543
  `[route-forge] backend endpoint_prefix "${summary.config.endpoint_prefix}" overrides frontend endpoint "${explicitEndpoint}"`
@@ -578,7 +647,10 @@ function createRouteForge(options) {
578
647
  };
579
648
  const resp = await adp.request(config);
580
649
  if (!resp || resp.status < 200 || resp.status >= 300) {
581
- 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
+ );
582
654
  }
583
655
  const data = resp.data;
584
656
  return data;
@@ -612,27 +684,33 @@ function createRouteForge(options) {
612
684
  return buildRequestUrl(meta, params ?? {});
613
685
  }
614
686
  function buildRequestUrl(meta, params) {
615
- let uri = meta.uri;
616
687
  const defaults = meta.parameter_defaults ?? {};
617
688
  const missingRequired = [];
689
+ const values = {};
618
690
  for (const p of meta.parameters) {
619
691
  let v = params[p];
620
692
  if ((v === void 0 || v === null) && p in defaults) {
621
693
  v = defaults[p];
622
694
  }
623
695
  if (v === void 0 || v === null) {
624
- if (uri.includes(`{${p}?}`)) {
625
- uri = uri.replace(`{${p}?}`, "");
626
- continue;
696
+ if (!meta.uri.includes(`{${p}?}`)) {
697
+ missingRequired.push(p);
627
698
  }
628
- missingRequired.push(p);
629
699
  } else {
630
- uri = uri.replace(`{${p}?}`, encodeURIComponent(String(v))).replace(`{${p}}`, encodeURIComponent(String(v)));
700
+ values[p] = v;
631
701
  }
632
702
  }
633
703
  if (missingRequired.length > 0) {
634
704
  throw new MissingRouteParamError(meta.name, missingRequired);
635
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
+ });
636
714
  uri = uri.replace(/\/+/g, "/").replace(/\/$/, "");
637
715
  if (effectiveUrlPrefix.includes("://")) {
638
716
  const prefix2 = effectiveUrlPrefix.endsWith("/") ? effectiveUrlPrefix.slice(0, -1) : effectiveUrlPrefix;
@@ -660,7 +738,7 @@ function createRouteForge(options) {
660
738
  return doApiCall(meta, params);
661
739
  }
662
740
  async function doApiCall(meta, params) {
663
- const { pathParams, query, body, headers } = resolveApiParams(params);
741
+ const { pathParams, query, body, headers, timeout: perCallTimeout } = resolveApiParams(params);
664
742
  const method = pickMethod(meta);
665
743
  const urlWithQuery = appendQuery(buildRequestUrl(meta, pathParams), query);
666
744
  const config = {
@@ -671,7 +749,7 @@ function createRouteForge(options) {
671
749
  headers: { Accept: "application/json", ...headers ?? {} },
672
750
  body,
673
751
  params: pathParams,
674
- timeout,
752
+ timeout: perCallTimeout ?? timeout,
675
753
  meta
676
754
  };
677
755
  const adp = await ensureAdapter();
@@ -711,12 +789,17 @@ function createRouteForge(options) {
711
789
  }
712
790
  }
713
791
  function invalidate(level) {
714
- if (level) cache.del(level);
715
- 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
+ }
716
799
  }
717
800
  function isLoaded(level) {
718
801
  if (level) return cache.get(level) !== void 0;
719
- return effectiveLevels.every((lvl) => cache.get(lvl) !== void 0);
802
+ return effectiveLevels.length > 0 && effectiveLevels.every((lvl) => cache.get(lvl) !== void 0);
720
803
  }
721
804
  function hasRoute(level, name) {
722
805
  return findRouteMeta(level, name) !== void 0;
@@ -790,6 +873,7 @@ function resolveApiParams(input) {
790
873
  query: rawQuery,
791
874
  body: rawBody,
792
875
  headers: rawHeaders,
876
+ timeout: perCallTimeout,
793
877
  ...flatRest
794
878
  } = input;
795
879
  const pathParams = explicitParams ? { ...explicitParams } : {};
@@ -822,7 +906,7 @@ function resolveApiParams(input) {
822
906
  pathParams.headers = rawHeaders;
823
907
  }
824
908
  }
825
- return { pathParams, query, body, headers };
909
+ return { pathParams, query, body, headers, timeout: perCallTimeout };
826
910
  }
827
911
 
828
912
  export { AdapterNotFoundError, ForgeError, HTTPError, InterceptorManagerImpl, InvalidInterceptorReturnError, LoadingTracker, MissingRouteParamError, NetworkError, RouteCache, UnknownLevelError, UnknownRouteError, createInterceptorManager, createRouteForge };