@route-forge/core 1.4.0 → 2.1.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/README.md +10 -6
- package/dist/codegen.d.cts +1 -1
- package/dist/codegen.d.ts +1 -1
- package/dist/index.cjs +618 -508
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +61 -50
- package/dist/index.d.ts +61 -50
- package/dist/index.js +618 -508
- package/dist/index.js.map +1 -1
- package/dist/route-forge.global.js +618 -508
- package/dist/route-forge.global.js.map +1 -1
- package/dist/route-forge.global.min.js +2 -2
- package/dist/{types-BFlrOTrN.d.cts → types-C51rXaiY.d.cts} +6 -17
- package/dist/{types-BFlrOTrN.d.ts → types-C51rXaiY.d.ts} +6 -17
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -16,9 +16,18 @@ function pickStorage(storage) {
|
|
|
16
16
|
var RouteCache = class {
|
|
17
17
|
constructor(opts) {
|
|
18
18
|
this.memory = /* @__PURE__ */ new Map();
|
|
19
|
+
/** 其他 tab 修改 storage 时失效对应镜像(storage 事件:自己的写不触发,自己的写经 set/del 已同步) */
|
|
20
|
+
this.onStorageEvent = (e) => {
|
|
21
|
+
if (!e.key) return;
|
|
22
|
+
if (!e.key.startsWith(KEY_PREFIX)) return;
|
|
23
|
+
this.memory.delete(e.key.slice(KEY_PREFIX.length));
|
|
24
|
+
};
|
|
19
25
|
this.storage = opts.storage;
|
|
20
26
|
this.fallbackTtl = opts.ttl;
|
|
21
27
|
this.backend = pickStorage(opts.storage);
|
|
28
|
+
if (this.backend && typeof globalThis.addEventListener === "function") {
|
|
29
|
+
globalThis.addEventListener("storage", this.onStorageEvent);
|
|
30
|
+
}
|
|
22
31
|
}
|
|
23
32
|
key(level) {
|
|
24
33
|
return `${KEY_PREFIX}${level}`;
|
|
@@ -27,6 +36,8 @@ var RouteCache = class {
|
|
|
27
36
|
if (this.storage === "memory" || !this.backend) {
|
|
28
37
|
return this.getFromMemory(level);
|
|
29
38
|
}
|
|
39
|
+
const mirrored = this.getFromMemory(level);
|
|
40
|
+
if (mirrored) return mirrored;
|
|
30
41
|
const raw = this.backend.getItem(this.key(level));
|
|
31
42
|
if (raw) {
|
|
32
43
|
try {
|
|
@@ -35,12 +46,13 @@ var RouteCache = class {
|
|
|
35
46
|
this.del(level);
|
|
36
47
|
return void 0;
|
|
37
48
|
}
|
|
49
|
+
this.memory.set(level, entry);
|
|
38
50
|
return entry;
|
|
39
51
|
} catch {
|
|
40
52
|
return void 0;
|
|
41
53
|
}
|
|
42
54
|
}
|
|
43
|
-
return
|
|
55
|
+
return void 0;
|
|
44
56
|
}
|
|
45
57
|
getFromMemory(level) {
|
|
46
58
|
const entry = this.memory.get(level);
|
|
@@ -64,14 +76,13 @@ var RouteCache = class {
|
|
|
64
76
|
ttl,
|
|
65
77
|
cachedAt: Date.now()
|
|
66
78
|
};
|
|
79
|
+
this.memory.set(resp.level, entry);
|
|
67
80
|
if (this.storage === "memory" || !this.backend) {
|
|
68
|
-
this.memory.set(resp.level, entry);
|
|
69
81
|
return;
|
|
70
82
|
}
|
|
71
83
|
try {
|
|
72
84
|
this.backend.setItem(this.key(resp.level), JSON.stringify(entry));
|
|
73
85
|
} catch {
|
|
74
|
-
this.memory.set(resp.level, entry);
|
|
75
86
|
}
|
|
76
87
|
}
|
|
77
88
|
del(level) {
|
|
@@ -251,14 +262,14 @@ function createInterceptorManager() {
|
|
|
251
262
|
return new InterceptorManagerImpl();
|
|
252
263
|
}
|
|
253
264
|
|
|
254
|
-
// src/adapters/
|
|
255
|
-
function
|
|
256
|
-
if (
|
|
257
|
-
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;
|
|
258
|
-
}
|
|
259
|
-
function isAbortError(err) {
|
|
265
|
+
// src/adapters/fetch-core.ts
|
|
266
|
+
function isAbortError(err, signal) {
|
|
267
|
+
if (signal?.aborted) return true;
|
|
260
268
|
const e = err;
|
|
261
|
-
|
|
269
|
+
if (!e) return false;
|
|
270
|
+
if (e.name === "AbortError") return true;
|
|
271
|
+
if (e.code === "ERR_CANCELED") return true;
|
|
272
|
+
return false;
|
|
262
273
|
}
|
|
263
274
|
function combineSignals(...signals) {
|
|
264
275
|
const valid = signals.filter((s) => !!s);
|
|
@@ -277,82 +288,89 @@ function combineSignals(...signals) {
|
|
|
277
288
|
}
|
|
278
289
|
return controller.signal;
|
|
279
290
|
}
|
|
280
|
-
function
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
)
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
}
|
|
291
|
+
function isPassthroughBody(body) {
|
|
292
|
+
if (body === null) return false;
|
|
293
|
+
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;
|
|
294
|
+
}
|
|
295
|
+
async function rawFetch(config) {
|
|
296
|
+
const signal = combineSignals(
|
|
297
|
+
config.signal,
|
|
298
|
+
config.timeout && config.timeout > 0 ? AbortSignal.timeout(config.timeout) : void 0
|
|
299
|
+
);
|
|
300
|
+
let url = config.url;
|
|
301
|
+
if (config.paramsSerializer && config.params) {
|
|
302
|
+
const qs = config.paramsSerializer(config.params);
|
|
303
|
+
if (qs) {
|
|
304
|
+
url = url.includes("?") ? `${url}&${qs}` : `${url}?${qs}`;
|
|
294
305
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
306
|
+
}
|
|
307
|
+
const headers = new Headers(config.headers);
|
|
308
|
+
const fetchInit = {
|
|
309
|
+
method: config.method,
|
|
310
|
+
headers
|
|
311
|
+
};
|
|
312
|
+
if (signal) fetchInit.signal = signal;
|
|
313
|
+
if (config.body !== void 0 && !["GET", "HEAD"].includes(config.method.toUpperCase())) {
|
|
314
|
+
if (typeof config.body === "string" || isPassthroughBody(config.body)) {
|
|
315
|
+
fetchInit.body = config.body;
|
|
316
|
+
} else {
|
|
317
|
+
fetchInit.body = JSON.stringify(config.body);
|
|
318
|
+
if (!headers.has("Content-Type")) {
|
|
319
|
+
headers.set("Content-Type", "application/json");
|
|
309
320
|
}
|
|
310
321
|
}
|
|
311
|
-
|
|
322
|
+
}
|
|
323
|
+
let res;
|
|
324
|
+
try {
|
|
325
|
+
res = await fetch(url, fetchInit);
|
|
326
|
+
} catch (e) {
|
|
327
|
+
if (isAbortError(e)) throw e;
|
|
328
|
+
throw new NetworkError(
|
|
329
|
+
e instanceof Error ? e.message : String(e),
|
|
330
|
+
config.route,
|
|
331
|
+
config.level,
|
|
332
|
+
e
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
const text = await res.text();
|
|
336
|
+
let data = text;
|
|
337
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
338
|
+
if (contentType.includes("application/json")) {
|
|
312
339
|
try {
|
|
313
|
-
|
|
314
|
-
} catch
|
|
315
|
-
if (isAbortError(e)) throw e;
|
|
316
|
-
throw new NetworkError(
|
|
317
|
-
e instanceof Error ? e.message : String(e),
|
|
318
|
-
config.route,
|
|
319
|
-
config.level,
|
|
320
|
-
e
|
|
321
|
-
);
|
|
322
|
-
}
|
|
323
|
-
const text = await res.text();
|
|
324
|
-
let data = text;
|
|
325
|
-
const contentType = res.headers.get("content-type") ?? "";
|
|
326
|
-
if (contentType.includes("application/json")) {
|
|
327
|
-
try {
|
|
328
|
-
data = JSON.parse(text);
|
|
329
|
-
} catch {
|
|
330
|
-
}
|
|
340
|
+
data = JSON.parse(text);
|
|
341
|
+
} catch {
|
|
331
342
|
}
|
|
332
|
-
|
|
343
|
+
}
|
|
344
|
+
const responseData = {
|
|
345
|
+
route: config.route,
|
|
346
|
+
level: config.level,
|
|
347
|
+
method: config.method,
|
|
348
|
+
url,
|
|
349
|
+
status: res.status,
|
|
350
|
+
headers: res.headers,
|
|
351
|
+
data,
|
|
352
|
+
config
|
|
353
|
+
};
|
|
354
|
+
if (res.status >= 200 && res.status < 300) {
|
|
355
|
+
return responseData;
|
|
356
|
+
}
|
|
357
|
+
throw new HTTPError(
|
|
358
|
+
`HTTP ${res.status} for route "${config.route}" (${config.method} ${url})`,
|
|
359
|
+
{
|
|
333
360
|
route: config.route,
|
|
334
361
|
level: config.level,
|
|
335
|
-
method: config.method,
|
|
336
|
-
url,
|
|
337
362
|
status: res.status,
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
config
|
|
341
|
-
};
|
|
342
|
-
if (res.status >= 200 && res.status < 300) {
|
|
343
|
-
return responseData;
|
|
363
|
+
url,
|
|
364
|
+
method: config.method
|
|
344
365
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
}
|
|
354
|
-
);
|
|
355
|
-
}
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// src/adapters/builtin-http.ts
|
|
370
|
+
function createBuiltinHttp(forgeInterceptors) {
|
|
371
|
+
const requestMgr = forgeInterceptors?.request ?? createInterceptorManager();
|
|
372
|
+
const responseMgr = forgeInterceptors?.response ?? createInterceptorManager();
|
|
373
|
+
const requestRaw = rawFetch;
|
|
356
374
|
async function request(config) {
|
|
357
375
|
const finalConfig = await runRequestInterceptors(requestMgr, config);
|
|
358
376
|
const source = Promise.resolve(finalConfig).then(requestRaw);
|
|
@@ -472,39 +490,6 @@ async function resolveAdapter(opts) {
|
|
|
472
490
|
return createBuiltinHttp(opts.forgeInterceptors);
|
|
473
491
|
}
|
|
474
492
|
|
|
475
|
-
// src/resolveRouteName.ts
|
|
476
|
-
async function resolveRouteName(forge, level, prefix, suffix, separator = ".") {
|
|
477
|
-
if (!suffix) return prefix;
|
|
478
|
-
const joined = `${prefix}${separator}${suffix}`;
|
|
479
|
-
if (!suffix.startsWith(`${prefix}${separator}`)) return joined;
|
|
480
|
-
await forge.load(level);
|
|
481
|
-
if (forge.hasRoute(level, joined)) return joined;
|
|
482
|
-
if (forge.hasRoute(level, suffix)) return suffix;
|
|
483
|
-
throw new UnknownRouteError(joined, level);
|
|
484
|
-
}
|
|
485
|
-
function resolveRouteNameSync(forge, level, prefix, suffix, separator = ".") {
|
|
486
|
-
if (!suffix) return prefix;
|
|
487
|
-
const joined = `${prefix}${separator}${suffix}`;
|
|
488
|
-
if (!suffix.startsWith(`${prefix}${separator}`)) return joined;
|
|
489
|
-
if (forge.hasRoute(level, joined)) return joined;
|
|
490
|
-
if (forge.hasRoute(level, suffix)) return suffix;
|
|
491
|
-
throw new UnknownRouteError(joined, level);
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
// src/defineImmutableProps.ts
|
|
495
|
-
function defineImmutableProps(target, props) {
|
|
496
|
-
for (const key of Object.keys(props)) {
|
|
497
|
-
const val = props[key];
|
|
498
|
-
Object.defineProperty(target, key, {
|
|
499
|
-
value: val !== null && typeof val === "object" ? Object.freeze(val) : val,
|
|
500
|
-
writable: false,
|
|
501
|
-
enumerable: false,
|
|
502
|
-
configurable: false
|
|
503
|
-
});
|
|
504
|
-
}
|
|
505
|
-
return target;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
493
|
// src/loading.ts
|
|
509
494
|
var LoadingTracker = class {
|
|
510
495
|
constructor() {
|
|
@@ -564,320 +549,308 @@ var LoadingTracker = class {
|
|
|
564
549
|
}
|
|
565
550
|
};
|
|
566
551
|
|
|
567
|
-
// src/
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
const explicitLevels = options.levels;
|
|
582
|
-
const explicitEager = options.eager;
|
|
583
|
-
const explicitEndpoint = options.endpoint;
|
|
584
|
-
let effectiveLevels = explicitLevels ?? [];
|
|
585
|
-
let effectiveEager = explicitEager ?? [];
|
|
586
|
-
let effectiveEndpoint = explicitEndpoint;
|
|
587
|
-
let effectiveUrlPrefix = "";
|
|
588
|
-
let summaryUnassigned;
|
|
589
|
-
let backendHasUnassignedLevel = false;
|
|
590
|
-
const summaryPromise = (async () => {
|
|
591
|
-
try {
|
|
592
|
-
const summaryUrl = explicitEndpoint;
|
|
593
|
-
const resp = await fetch(summaryUrl, { method: "GET" });
|
|
594
|
-
if (!resp.ok) {
|
|
595
|
-
console.warn(
|
|
596
|
-
`[route-forge] summary endpoint ${summaryUrl} unreachable (HTTP ${resp.status}); falling back to explicit options`
|
|
597
|
-
);
|
|
598
|
-
return null;
|
|
599
|
-
}
|
|
600
|
-
return await resp.json();
|
|
601
|
-
} catch (e) {
|
|
602
|
-
if (explicitLevels && explicitLevels.length > 0) {
|
|
603
|
-
console.warn(
|
|
604
|
-
`[route-forge] summary endpoint unreachable: ${e.message}; using explicit levels`
|
|
605
|
-
);
|
|
606
|
-
return null;
|
|
607
|
-
}
|
|
608
|
-
throw new UnknownLevelError("(auto-discovery)");
|
|
609
|
-
}
|
|
610
|
-
})();
|
|
611
|
-
const autoDiscoveryPromise = summaryPromise.then((summary) => {
|
|
612
|
-
if (summary === null) {
|
|
613
|
-
return;
|
|
614
|
-
}
|
|
615
|
-
const schemaVersion = summary.schemaVersion ?? 1;
|
|
616
|
-
if (schemaVersion > 1) {
|
|
617
|
-
console.warn(
|
|
618
|
-
`[route-forge] backend schemaVersion=${schemaVersion} > client supported 1; some features may be unavailable`
|
|
619
|
-
);
|
|
620
|
-
}
|
|
621
|
-
if (summary.config.endpoint_prefix && summary.config.endpoint_prefix !== explicitEndpoint) {
|
|
622
|
-
console.warn(
|
|
623
|
-
`[route-forge] backend endpoint_prefix "${summary.config.endpoint_prefix}" overrides frontend endpoint "${explicitEndpoint}"`
|
|
624
|
-
);
|
|
625
|
-
effectiveEndpoint = summary.config.endpoint_prefix;
|
|
626
|
-
}
|
|
627
|
-
if (summary.config.url_prefix) {
|
|
628
|
-
effectiveUrlPrefix = summary.config.url_prefix.endsWith("/") ? summary.config.url_prefix.slice(0, -1) : summary.config.url_prefix;
|
|
629
|
-
}
|
|
630
|
-
const backendLevels = Object.keys(summary.levels);
|
|
631
|
-
backendHasUnassignedLevel = backendLevels.includes(UNASSIGNED_LEVEL);
|
|
632
|
-
if (Array.isArray(summary.unassigned) && summary.unassigned.length > 0 && !backendHasUnassignedLevel) {
|
|
633
|
-
summaryUnassigned = summary.unassigned;
|
|
634
|
-
}
|
|
635
|
-
const availableLevels = backendLevels.slice();
|
|
636
|
-
if (summaryUnassigned && !backendHasUnassignedLevel) {
|
|
637
|
-
availableLevels.push(UNASSIGNED_LEVEL);
|
|
552
|
+
// src/url-builder.ts
|
|
553
|
+
function buildUrl(level, ctx) {
|
|
554
|
+
const base = ctx.baseURL.endsWith("/") ? ctx.baseURL.slice(0, -1) : ctx.baseURL;
|
|
555
|
+
const ep = ctx.endpoint.startsWith("/") ? ctx.endpoint : `/${ctx.endpoint}`;
|
|
556
|
+
return `${base}${ep}/${encodeURIComponent(level)}`;
|
|
557
|
+
}
|
|
558
|
+
function buildRequestUrl(meta, params, ctx) {
|
|
559
|
+
const defaults = meta.parameter_defaults ?? {};
|
|
560
|
+
const missingRequired = [];
|
|
561
|
+
const values = {};
|
|
562
|
+
for (const p of meta.parameters) {
|
|
563
|
+
let v = params[p];
|
|
564
|
+
if ((v === void 0 || v === null) && p in defaults) {
|
|
565
|
+
v = defaults[p];
|
|
638
566
|
}
|
|
639
|
-
if (
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
if (removed.length > 0) {
|
|
643
|
-
console.warn(
|
|
644
|
-
`[route-forge] levels not in backend summary and dropped: ${removed.join(", ")}`
|
|
645
|
-
);
|
|
567
|
+
if (v === void 0 || v === null) {
|
|
568
|
+
if (!meta.uri.includes(`{${p}?}`)) {
|
|
569
|
+
missingRequired.push(p);
|
|
646
570
|
}
|
|
647
|
-
effectiveLevels = intersection;
|
|
648
571
|
} else {
|
|
649
|
-
|
|
572
|
+
values[p] = v;
|
|
650
573
|
}
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
574
|
+
}
|
|
575
|
+
if (missingRequired.length > 0) {
|
|
576
|
+
throw new MissingRouteParamError(meta.name, missingRequired);
|
|
577
|
+
}
|
|
578
|
+
let uri = meta.uri.replace(/\{([^{}]+)\}/g, (match, raw) => {
|
|
579
|
+
const optional = raw.endsWith("?");
|
|
580
|
+
const name = optional ? raw.slice(0, -1) : raw;
|
|
581
|
+
if (values[name] !== void 0) {
|
|
582
|
+
const val = values[name];
|
|
583
|
+
if (typeof val === "object") {
|
|
584
|
+
throw new ForgeError(
|
|
585
|
+
`Path parameter "${name}" must be a primitive value (string, number, boolean), got ${typeof val}`,
|
|
586
|
+
{ code: "RF_FE_003", route: meta.name, context: { param: name, value: val } }
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
return encodeURIComponent(String(val));
|
|
657
590
|
}
|
|
591
|
+
return optional ? "" : match;
|
|
658
592
|
});
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
const
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
const
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
if (
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
593
|
+
uri = uri.replace(/\/+/g, "/").replace(/\/$/, "");
|
|
594
|
+
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(ctx.urlPrefix)) {
|
|
595
|
+
const prefix2 = ctx.urlPrefix.endsWith("/") ? ctx.urlPrefix.slice(0, -1) : ctx.urlPrefix;
|
|
596
|
+
return uri.startsWith("/") ? `${prefix2}${uri}` : `${prefix2}/${uri}`;
|
|
597
|
+
}
|
|
598
|
+
const base = ctx.baseURL.endsWith("/") ? ctx.baseURL.slice(0, -1) : ctx.baseURL;
|
|
599
|
+
const prefix = ctx.urlPrefix;
|
|
600
|
+
return uri.startsWith("/") ? `${base}${prefix}${uri}` : `${base}${prefix}/${uri}`;
|
|
601
|
+
}
|
|
602
|
+
function pickMethod(meta) {
|
|
603
|
+
const m = meta.methods.find((x) => x.toUpperCase() !== "HEAD");
|
|
604
|
+
return (m ?? meta.methods[0] ?? "GET").toUpperCase();
|
|
605
|
+
}
|
|
606
|
+
function appendQuery(url, query) {
|
|
607
|
+
if (!query) return url;
|
|
608
|
+
const usp = new URLSearchParams();
|
|
609
|
+
for (const [k, v] of Object.entries(query)) {
|
|
610
|
+
if (v === void 0 || v === null) continue;
|
|
611
|
+
usp.append(k, String(v));
|
|
612
|
+
}
|
|
613
|
+
const qs = usp.toString();
|
|
614
|
+
if (!qs) return url;
|
|
615
|
+
return url.includes("?") ? `${url}&${qs}` : `${url}?${qs}`;
|
|
616
|
+
}
|
|
617
|
+
function resolveApiParams(input) {
|
|
618
|
+
const {
|
|
619
|
+
params: explicitParams,
|
|
620
|
+
query: rawQuery,
|
|
621
|
+
body: rawBody,
|
|
622
|
+
headers: rawHeaders,
|
|
623
|
+
timeout: perCallTimeout,
|
|
624
|
+
...flatRest
|
|
625
|
+
} = input;
|
|
626
|
+
const pathParams = explicitParams ? { ...explicitParams } : {};
|
|
627
|
+
for (const [k, v] of Object.entries(flatRest)) {
|
|
628
|
+
if (!(k in pathParams)) {
|
|
629
|
+
pathParams[k] = v;
|
|
681
630
|
}
|
|
682
631
|
}
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
632
|
+
let query;
|
|
633
|
+
let body;
|
|
634
|
+
let headers;
|
|
635
|
+
if (rawQuery !== void 0) {
|
|
636
|
+
if (typeof rawQuery === "object" && rawQuery !== null) {
|
|
637
|
+
query = rawQuery;
|
|
638
|
+
} else if (!("query" in pathParams)) {
|
|
639
|
+
pathParams.query = rawQuery;
|
|
691
640
|
}
|
|
692
641
|
}
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
let adapterObj = null;
|
|
699
|
-
async function ensureAdapter() {
|
|
700
|
-
if (!adapterResolved) {
|
|
701
|
-
adapterObj = await adapterPromise.catch((e) => {
|
|
702
|
-
if (e instanceof AdapterNotFoundError) throw e;
|
|
703
|
-
return resolveAdapter({
|
|
704
|
-
adapter: "builtin",
|
|
705
|
-
forgeInterceptors: { request: requestInterceptors, response: responseInterceptors }
|
|
706
|
-
});
|
|
707
|
-
});
|
|
708
|
-
adapterResolved = true;
|
|
642
|
+
if (rawBody !== void 0) {
|
|
643
|
+
if (typeof rawBody !== "string" && typeof rawBody !== "number") {
|
|
644
|
+
body = rawBody;
|
|
645
|
+
} else if (!("body" in pathParams)) {
|
|
646
|
+
pathParams.body = rawBody;
|
|
709
647
|
}
|
|
710
|
-
return adapterObj;
|
|
711
648
|
}
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
if (!
|
|
716
|
-
|
|
649
|
+
if (rawHeaders !== void 0) {
|
|
650
|
+
if (typeof rawHeaders === "object" && rawHeaders !== null) {
|
|
651
|
+
headers = rawHeaders;
|
|
652
|
+
} else if (!("headers" in pathParams)) {
|
|
653
|
+
pathParams.headers = rawHeaders;
|
|
717
654
|
}
|
|
718
655
|
}
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
656
|
+
return { pathParams, query, body, headers, timeout: perCallTimeout };
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// src/auto-discovery.ts
|
|
660
|
+
var UNASSIGNED_LEVEL = "unassigned";
|
|
661
|
+
async function fetchSummary(inputs, baseURL, fetchMeta) {
|
|
662
|
+
const { explicitLevels, explicitEndpoint } = inputs;
|
|
663
|
+
try {
|
|
664
|
+
const base = baseURL.endsWith("/") ? baseURL.slice(0, -1) : baseURL;
|
|
665
|
+
const ep = explicitEndpoint.startsWith("/") ? explicitEndpoint : `/${explicitEndpoint}`;
|
|
666
|
+
const data = await fetchMeta("__forge__.summary", `${base}${ep}`);
|
|
667
|
+
return data;
|
|
668
|
+
} catch (e) {
|
|
669
|
+
if (explicitLevels && explicitLevels.length > 0) {
|
|
670
|
+
console.warn(
|
|
671
|
+
`[route-forge] summary endpoint unreachable: ${e.message}; using explicit levels`
|
|
724
672
|
);
|
|
673
|
+
return null;
|
|
725
674
|
}
|
|
675
|
+
throw new UnknownLevelError("(auto-discovery)");
|
|
726
676
|
}
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
677
|
+
}
|
|
678
|
+
function applySummaryToState(summary, state, inputs) {
|
|
679
|
+
const { explicitLevels, explicitEager, explicitEndpoint } = inputs;
|
|
680
|
+
const schemaVersion = summary.schemaVersion ?? 1;
|
|
681
|
+
if (schemaVersion > 1) {
|
|
682
|
+
console.warn(
|
|
683
|
+
`[route-forge] backend schemaVersion=${schemaVersion} > client supported 1; some features may be unavailable`
|
|
684
|
+
);
|
|
731
685
|
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
const
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
686
|
+
if (summary.config.endpoint_prefix && summary.config.endpoint_prefix !== explicitEndpoint) {
|
|
687
|
+
console.warn(
|
|
688
|
+
`[route-forge] backend endpoint_prefix "${summary.config.endpoint_prefix}" overrides frontend endpoint "${explicitEndpoint}"`
|
|
689
|
+
);
|
|
690
|
+
state.endpoint = summary.config.endpoint_prefix;
|
|
691
|
+
}
|
|
692
|
+
if (summary.config.url_prefix) {
|
|
693
|
+
state.urlPrefix = summary.config.url_prefix.endsWith("/") ? summary.config.url_prefix.slice(0, -1) : summary.config.url_prefix;
|
|
694
|
+
}
|
|
695
|
+
const backendLevels = Object.keys(summary.levels);
|
|
696
|
+
state.backendHasUnassignedLevel = backendLevels.includes(UNASSIGNED_LEVEL);
|
|
697
|
+
if (Array.isArray(summary.unassigned) && summary.unassigned.length > 0 && !state.backendHasUnassignedLevel) {
|
|
698
|
+
state.summaryUnassigned = summary.unassigned;
|
|
699
|
+
}
|
|
700
|
+
const availableLevels = backendLevels.slice();
|
|
701
|
+
if (state.summaryUnassigned && !state.backendHasUnassignedLevel) {
|
|
702
|
+
availableLevels.push(UNASSIGNED_LEVEL);
|
|
703
|
+
}
|
|
704
|
+
if (explicitLevels && explicitLevels.length > 0) {
|
|
705
|
+
const intersection = explicitLevels.filter((l) => availableLevels.includes(l));
|
|
706
|
+
const removed = explicitLevels.filter((l) => !availableLevels.includes(l));
|
|
707
|
+
if (removed.length > 0) {
|
|
708
|
+
console.warn(
|
|
709
|
+
`[route-forge] levels not in backend summary and dropped: ${removed.join(", ")}`
|
|
756
710
|
);
|
|
757
711
|
}
|
|
758
|
-
|
|
759
|
-
|
|
712
|
+
state.levels = intersection;
|
|
713
|
+
} else {
|
|
714
|
+
state.levels = availableLevels;
|
|
715
|
+
}
|
|
716
|
+
const backendEager = backendLevels.filter((lvl) => summary.levels[lvl]?.load === "eager");
|
|
717
|
+
if (!explicitEager) {
|
|
718
|
+
state.eager = backendEager;
|
|
719
|
+
} else {
|
|
720
|
+
const union = /* @__PURE__ */ new Set([...backendEager, ...explicitEager]);
|
|
721
|
+
state.eager = [...union];
|
|
760
722
|
}
|
|
761
|
-
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// src/route-store.ts
|
|
726
|
+
var RouteStore = class {
|
|
727
|
+
constructor(deps) {
|
|
728
|
+
this.inflight = /* @__PURE__ */ new Map();
|
|
729
|
+
/** 每个层级的失效代数,用于检测 loadOne 期间是否发生了 invalidate */
|
|
730
|
+
this.invalidationGens = /* @__PURE__ */ new Map();
|
|
731
|
+
this.cache = deps.cache;
|
|
732
|
+
this.state = deps.state;
|
|
733
|
+
this.baseURL = deps.baseURL;
|
|
734
|
+
this.fetchMeta = deps.fetchMeta;
|
|
735
|
+
this.autoDiscoveryPromise = deps.autoDiscoveryPromise;
|
|
736
|
+
this.getAutoDiscoveryError = deps.getAutoDiscoveryError;
|
|
737
|
+
}
|
|
738
|
+
assertLevelDeclared(level) {
|
|
739
|
+
if (!this.state.levels.includes(level)) {
|
|
740
|
+
throw new UnknownLevelError(level);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
async fetchLevel(level) {
|
|
744
|
+
return await this.fetchMeta(
|
|
745
|
+
`__forge__.load.${level}`,
|
|
746
|
+
buildUrl(level, { baseURL: this.baseURL, endpoint: this.state.endpoint }),
|
|
747
|
+
level
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
async loadOne(level) {
|
|
751
|
+
const autoDiscoveryError = this.getAutoDiscoveryError();
|
|
762
752
|
if (autoDiscoveryError) throw autoDiscoveryError;
|
|
763
|
-
assertLevelDeclared(level);
|
|
764
|
-
if (cache.get(level)) return;
|
|
765
|
-
const existing = inflight.get(level);
|
|
753
|
+
this.assertLevelDeclared(level);
|
|
754
|
+
if (this.cache.get(level)) return;
|
|
755
|
+
const existing = this.inflight.get(level);
|
|
766
756
|
if (existing) return existing;
|
|
767
|
-
const gen = invalidationGens.get(level) ?? 0;
|
|
757
|
+
const gen = this.invalidationGens.get(level) ?? 0;
|
|
768
758
|
const p = (async () => {
|
|
769
759
|
try {
|
|
770
|
-
if (level === UNASSIGNED_LEVEL && !backendHasUnassignedLevel && summaryUnassigned) {
|
|
760
|
+
if (level === UNASSIGNED_LEVEL && !this.state.backendHasUnassignedLevel && this.state.summaryUnassigned) {
|
|
771
761
|
const routes = {};
|
|
772
|
-
for (const r of summaryUnassigned) {
|
|
762
|
+
for (const r of this.state.summaryUnassigned) {
|
|
773
763
|
routes[r.name] = { ...r, level: UNASSIGNED_LEVEL };
|
|
774
764
|
}
|
|
775
|
-
cache.set({ level: UNASSIGNED_LEVEL, routes, cache: null });
|
|
765
|
+
this.cache.set({ level: UNASSIGNED_LEVEL, routes, cache: null });
|
|
776
766
|
return;
|
|
777
767
|
}
|
|
778
|
-
const resp = await fetchLevel(level);
|
|
779
|
-
if ((invalidationGens.get(level) ?? 0) === gen) {
|
|
780
|
-
cache.set(resp);
|
|
768
|
+
const resp = await this.fetchLevel(level);
|
|
769
|
+
if ((this.invalidationGens.get(level) ?? 0) === gen) {
|
|
770
|
+
this.cache.set(resp);
|
|
781
771
|
}
|
|
782
772
|
} finally {
|
|
783
|
-
inflight.delete(level);
|
|
773
|
+
this.inflight.delete(level);
|
|
784
774
|
}
|
|
785
775
|
})();
|
|
786
|
-
inflight.set(level, p);
|
|
776
|
+
this.inflight.set(level, p);
|
|
787
777
|
return p;
|
|
788
778
|
}
|
|
789
|
-
async
|
|
790
|
-
await autoDiscoveryPromise;
|
|
779
|
+
async load(level) {
|
|
780
|
+
await this.autoDiscoveryPromise;
|
|
791
781
|
const list = Array.isArray(level) ? level : [level];
|
|
792
|
-
await Promise.all(list.map(loadOne));
|
|
782
|
+
await Promise.all(list.map((l) => this.loadOne(l)));
|
|
793
783
|
}
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
return buildRequestUrl(meta, params ?? {});
|
|
801
|
-
}
|
|
802
|
-
function buildRequestUrl(meta, params) {
|
|
803
|
-
const defaults = meta.parameter_defaults ?? {};
|
|
804
|
-
const missingRequired = [];
|
|
805
|
-
const values = {};
|
|
806
|
-
for (const p of meta.parameters) {
|
|
807
|
-
let v = params[p];
|
|
808
|
-
if ((v === void 0 || v === null) && p in defaults) {
|
|
809
|
-
v = defaults[p];
|
|
810
|
-
}
|
|
811
|
-
if (v === void 0 || v === null) {
|
|
812
|
-
if (!meta.uri.includes(`{${p}?}`)) {
|
|
813
|
-
missingRequired.push(p);
|
|
814
|
-
}
|
|
815
|
-
} else {
|
|
816
|
-
values[p] = v;
|
|
784
|
+
invalidate(level) {
|
|
785
|
+
if (level === void 0) {
|
|
786
|
+
this.cache.clear();
|
|
787
|
+
this.inflight.clear();
|
|
788
|
+
for (const lvl of this.state.levels) {
|
|
789
|
+
this.invalidationGens.set(lvl, (this.invalidationGens.get(lvl) ?? 0) + 1);
|
|
817
790
|
}
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
const optional = raw.endsWith("?");
|
|
824
|
-
const name = optional ? raw.slice(0, -1) : raw;
|
|
825
|
-
if (values[name] !== void 0) {
|
|
826
|
-
const val = values[name];
|
|
827
|
-
if (typeof val === "object") {
|
|
828
|
-
throw new ForgeError(
|
|
829
|
-
`Path parameter "${name}" must be a primitive value (string, number, boolean), got ${typeof val}`,
|
|
830
|
-
{ code: "RF_FE_003", route: meta.name, context: { param: name, value: val } }
|
|
831
|
-
);
|
|
832
|
-
}
|
|
833
|
-
return encodeURIComponent(String(val));
|
|
791
|
+
} else if (Array.isArray(level)) {
|
|
792
|
+
for (const lvl of level) {
|
|
793
|
+
this.cache.del(lvl);
|
|
794
|
+
this.inflight.delete(lvl);
|
|
795
|
+
this.invalidationGens.set(lvl, (this.invalidationGens.get(lvl) ?? 0) + 1);
|
|
834
796
|
}
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
const prefix2 = effectiveUrlPrefix.endsWith("/") ? effectiveUrlPrefix.slice(0, -1) : effectiveUrlPrefix;
|
|
840
|
-
return uri.startsWith("/") ? `${prefix2}${uri}` : `${prefix2}/${uri}`;
|
|
797
|
+
} else {
|
|
798
|
+
this.cache.del(level);
|
|
799
|
+
this.inflight.delete(level);
|
|
800
|
+
this.invalidationGens.set(level, (this.invalidationGens.get(level) ?? 0) + 1);
|
|
841
801
|
}
|
|
842
|
-
const base = baseURL.endsWith("/") ? baseURL.slice(0, -1) : baseURL;
|
|
843
|
-
const prefix = effectiveUrlPrefix;
|
|
844
|
-
return uri.startsWith("/") ? `${base}${prefix}${uri}` : `${base}${prefix}/${uri}`;
|
|
845
802
|
}
|
|
846
|
-
|
|
847
|
-
|
|
803
|
+
isLoaded(level) {
|
|
804
|
+
if (level) return this.cache.get(level) !== void 0;
|
|
805
|
+
return this.state.levels.length > 0 && this.state.levels.every((lvl) => this.cache.get(lvl) !== void 0);
|
|
806
|
+
}
|
|
807
|
+
findRouteMeta(level, name) {
|
|
808
|
+
const entry = this.cache.get(level);
|
|
848
809
|
const meta = entry?.routes[name];
|
|
849
810
|
if (meta) {
|
|
850
811
|
return { ...meta, level };
|
|
851
812
|
}
|
|
852
813
|
return void 0;
|
|
853
814
|
}
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
ctrl.abort(abortReason);
|
|
862
|
-
}
|
|
863
|
-
await autoDiscoveryPromise;
|
|
864
|
-
await load(level);
|
|
865
|
-
const meta = findRouteMeta(level, name);
|
|
866
|
-
if (!meta) {
|
|
867
|
-
throw new UnknownRouteError(name, level);
|
|
815
|
+
getRoutes(level) {
|
|
816
|
+
if (level !== void 0) {
|
|
817
|
+
const entry = this.cache.get(level);
|
|
818
|
+
const routes = entry?.routes ?? {};
|
|
819
|
+
const result2 = {};
|
|
820
|
+
for (const [k, v] of Object.entries(routes)) {
|
|
821
|
+
result2[k] = JSON.parse(JSON.stringify(v));
|
|
868
822
|
}
|
|
869
|
-
return
|
|
870
|
-
}
|
|
871
|
-
const
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
823
|
+
return result2;
|
|
824
|
+
}
|
|
825
|
+
const result = {};
|
|
826
|
+
for (const lvl of this.state.levels) {
|
|
827
|
+
const entry = this.cache.get(lvl);
|
|
828
|
+
if (entry) {
|
|
829
|
+
const levelRoutes = {};
|
|
830
|
+
for (const [k, v] of Object.entries(entry.routes)) {
|
|
831
|
+
levelRoutes[k] = JSON.parse(JSON.stringify(v));
|
|
832
|
+
}
|
|
833
|
+
result[lvl] = levelRoutes;
|
|
877
834
|
}
|
|
878
|
-
}
|
|
879
|
-
return
|
|
835
|
+
}
|
|
836
|
+
return result;
|
|
880
837
|
}
|
|
838
|
+
};
|
|
839
|
+
|
|
840
|
+
// src/http-runner.ts
|
|
841
|
+
function createHttpRunner(deps) {
|
|
842
|
+
const {
|
|
843
|
+
ensureAdapter,
|
|
844
|
+
requestInterceptors,
|
|
845
|
+
responseInterceptors,
|
|
846
|
+
load,
|
|
847
|
+
findRouteMeta,
|
|
848
|
+
baseURL,
|
|
849
|
+
state,
|
|
850
|
+
timeout,
|
|
851
|
+
loadingTracker,
|
|
852
|
+
autoDiscoveryPromise
|
|
853
|
+
} = deps;
|
|
881
854
|
async function doApiCall(meta, params, signal) {
|
|
882
855
|
const {
|
|
883
856
|
pathParams,
|
|
@@ -890,7 +863,10 @@ function createRouteForge(options) {
|
|
|
890
863
|
throw new RequestAbortedError(meta.name, meta.level, signal.reason);
|
|
891
864
|
}
|
|
892
865
|
const method = pickMethod(meta);
|
|
893
|
-
const urlWithQuery = appendQuery(
|
|
866
|
+
const urlWithQuery = appendQuery(
|
|
867
|
+
buildRequestUrl(meta, pathParams, { baseURL, urlPrefix: state.urlPrefix }),
|
|
868
|
+
query
|
|
869
|
+
);
|
|
894
870
|
const config = {
|
|
895
871
|
route: meta.name,
|
|
896
872
|
level: meta.level ?? "",
|
|
@@ -928,7 +904,7 @@ function createRouteForge(options) {
|
|
|
928
904
|
},
|
|
929
905
|
(err) => {
|
|
930
906
|
if (err instanceof ForgeError) throw err;
|
|
931
|
-
if (
|
|
907
|
+
if (isAbortError(err, signal)) {
|
|
932
908
|
throw new RequestAbortedError(meta.name, meta.level, err);
|
|
933
909
|
}
|
|
934
910
|
throw new NetworkError(
|
|
@@ -945,81 +921,77 @@ function createRouteForge(options) {
|
|
|
945
921
|
loadingTracker.stop();
|
|
946
922
|
}
|
|
947
923
|
}
|
|
948
|
-
function
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
for (const lvl of level) {
|
|
957
|
-
cache.del(lvl);
|
|
958
|
-
inflight.delete(lvl);
|
|
959
|
-
invalidationGens.set(lvl, (invalidationGens.get(lvl) ?? 0) + 1);
|
|
924
|
+
return function api(level, name, params = {}) {
|
|
925
|
+
let ctrl;
|
|
926
|
+
let abortedBeforeInit = false;
|
|
927
|
+
let abortReason;
|
|
928
|
+
const work = (async () => {
|
|
929
|
+
ctrl = new AbortController();
|
|
930
|
+
if (abortedBeforeInit) {
|
|
931
|
+
ctrl.abort(abortReason);
|
|
960
932
|
}
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
}
|
|
967
|
-
function isLoaded(level) {
|
|
968
|
-
if (level) return cache.get(level) !== void 0;
|
|
969
|
-
return effectiveLevels.length > 0 && effectiveLevels.every((lvl) => cache.get(lvl) !== void 0);
|
|
970
|
-
}
|
|
971
|
-
function hasRoute(level, name) {
|
|
972
|
-
assertDiscoveryReady();
|
|
973
|
-
return findRouteMeta(level, name) !== void 0;
|
|
974
|
-
}
|
|
975
|
-
function getRoutes(level) {
|
|
976
|
-
if (level !== void 0) {
|
|
977
|
-
const entry = cache.get(level);
|
|
978
|
-
const routes = entry?.routes ?? {};
|
|
979
|
-
const result2 = {};
|
|
980
|
-
for (const [k, v] of Object.entries(routes)) {
|
|
981
|
-
result2[k] = JSON.parse(JSON.stringify(v));
|
|
933
|
+
await autoDiscoveryPromise;
|
|
934
|
+
await load(level);
|
|
935
|
+
const meta = findRouteMeta(level, name);
|
|
936
|
+
if (!meta) {
|
|
937
|
+
throw new UnknownRouteError(name, level);
|
|
982
938
|
}
|
|
983
|
-
return
|
|
984
|
-
}
|
|
985
|
-
const
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
levelRoutes[k] = JSON.parse(JSON.stringify(v));
|
|
992
|
-
}
|
|
993
|
-
result[lvl] = levelRoutes;
|
|
939
|
+
return doApiCall(meta, params, ctrl.signal);
|
|
940
|
+
})();
|
|
941
|
+
const request = work;
|
|
942
|
+
request.abort = () => {
|
|
943
|
+
if (ctrl) {
|
|
944
|
+
ctrl.abort();
|
|
945
|
+
} else {
|
|
946
|
+
abortedBeforeInit = true;
|
|
994
947
|
}
|
|
995
|
-
}
|
|
996
|
-
return
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
948
|
+
};
|
|
949
|
+
return request;
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// src/resolveRouteName.ts
|
|
954
|
+
async function resolveRouteName(forge, level, prefix, suffix, separator = ".") {
|
|
955
|
+
if (!suffix) return prefix;
|
|
956
|
+
const joined = `${prefix}${separator}${suffix}`;
|
|
957
|
+
if (!suffix.startsWith(`${prefix}${separator}`)) return joined;
|
|
958
|
+
await forge.load(level);
|
|
959
|
+
if (forge.hasRoute(level, joined)) return joined;
|
|
960
|
+
if (forge.hasRoute(level, suffix)) return suffix;
|
|
961
|
+
throw new UnknownRouteError(joined, level);
|
|
962
|
+
}
|
|
963
|
+
function resolveRouteNameSync(forge, level, prefix, suffix, separator = ".") {
|
|
964
|
+
if (!suffix) return prefix;
|
|
965
|
+
const joined = `${prefix}${separator}${suffix}`;
|
|
966
|
+
if (!suffix.startsWith(`${prefix}${separator}`)) return joined;
|
|
967
|
+
if (forge.hasRoute(level, joined)) return joined;
|
|
968
|
+
if (forge.hasRoute(level, suffix)) return suffix;
|
|
969
|
+
throw new UnknownRouteError(joined, level);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// src/defineImmutableProps.ts
|
|
973
|
+
function defineImmutableProps(target, props) {
|
|
974
|
+
for (const key of Object.keys(props)) {
|
|
975
|
+
const val = props[key];
|
|
976
|
+
Object.defineProperty(target, key, {
|
|
977
|
+
value: val !== null && typeof val === "object" ? Object.freeze(val) : val,
|
|
978
|
+
writable: false,
|
|
979
|
+
enumerable: false,
|
|
980
|
+
configurable: false
|
|
981
|
+
});
|
|
1016
982
|
}
|
|
1017
|
-
|
|
983
|
+
return target;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// src/bound-forge.ts
|
|
987
|
+
function createBoundForgeFactory(deps) {
|
|
988
|
+
const { load, api, route, hasRoute, getRoutes, invalidate, isLoaded, loadingTracker } = deps;
|
|
989
|
+
const resolver = { load, hasRoute };
|
|
1018
990
|
function createBoundForge(level, prefix) {
|
|
1019
991
|
const levelLoadedPromise = load(level);
|
|
1020
992
|
levelLoadedPromise.catch(() => {
|
|
1021
993
|
});
|
|
1022
|
-
const apiFn = prefix ? (name, params) => resolveRouteName(
|
|
994
|
+
const apiFn = prefix ? (name, params) => resolveRouteName(resolver, level, prefix, name).then(
|
|
1023
995
|
(resolved) => api(level, resolved, params)
|
|
1024
996
|
) : (name, params) => api(level, name, params);
|
|
1025
997
|
const callable = apiFn;
|
|
@@ -1027,8 +999,8 @@ function createRouteForge(options) {
|
|
|
1027
999
|
level,
|
|
1028
1000
|
...prefix !== void 0 ? { prefix } : {},
|
|
1029
1001
|
api: apiFn,
|
|
1030
|
-
route: prefix ? (name, params) => route(level, resolveRouteNameSync(
|
|
1031
|
-
url: prefix ? (name, params) => route(level, resolveRouteNameSync(
|
|
1002
|
+
route: prefix ? (name, params) => route(level, resolveRouteNameSync(resolver, level, prefix, name), params) : (name, params) => route(level, name, params),
|
|
1003
|
+
url: prefix ? (name, params) => route(level, resolveRouteNameSync(resolver, level, prefix, name), params) : (name, params) => route(level, name, params),
|
|
1032
1004
|
hasRoute: (name) => hasRoute(level, name),
|
|
1033
1005
|
getRoutes: () => getRoutes(level),
|
|
1034
1006
|
load: () => load(level),
|
|
@@ -1072,6 +1044,208 @@ function createRouteForge(options) {
|
|
|
1072
1044
|
attachBoundMethods(bound, levelLoadedPromise, level);
|
|
1073
1045
|
return bound;
|
|
1074
1046
|
}
|
|
1047
|
+
return createBoundForgeWithMethods;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
// src/forge.ts
|
|
1051
|
+
var DEFAULT_TIMEOUT = 3e4;
|
|
1052
|
+
var DEFAULT_CACHE_TTL = 3600;
|
|
1053
|
+
function createRouteForge(options) {
|
|
1054
|
+
if (!options.endpoint) throw new TypeError("options.endpoint is required");
|
|
1055
|
+
const {
|
|
1056
|
+
adapter = "auto",
|
|
1057
|
+
timeout = DEFAULT_TIMEOUT,
|
|
1058
|
+
baseURL = "",
|
|
1059
|
+
interceptors: declarativeInterceptors,
|
|
1060
|
+
cache: cacheOpts = {}
|
|
1061
|
+
} = options;
|
|
1062
|
+
const loadingTracker = new LoadingTracker();
|
|
1063
|
+
const explicitLevels = options.levels;
|
|
1064
|
+
const explicitEager = options.eager;
|
|
1065
|
+
const explicitEndpoint = options.endpoint;
|
|
1066
|
+
const discoveryState = {
|
|
1067
|
+
levels: explicitLevels ?? [],
|
|
1068
|
+
eager: explicitEager ?? [],
|
|
1069
|
+
endpoint: explicitEndpoint,
|
|
1070
|
+
urlPrefix: "",
|
|
1071
|
+
summaryUnassigned: void 0,
|
|
1072
|
+
backendHasUnassignedLevel: false
|
|
1073
|
+
};
|
|
1074
|
+
const discoveryInputs = { explicitLevels, explicitEager, explicitEndpoint };
|
|
1075
|
+
let autoDiscoveryCompleted = false;
|
|
1076
|
+
let resolveReady;
|
|
1077
|
+
let rejectReady;
|
|
1078
|
+
const readyPromise = new Promise((resolve, reject) => {
|
|
1079
|
+
resolveReady = resolve;
|
|
1080
|
+
rejectReady = reject;
|
|
1081
|
+
});
|
|
1082
|
+
readyPromise.catch(() => {
|
|
1083
|
+
});
|
|
1084
|
+
const cacheTtl = cacheOpts.ttl ?? DEFAULT_CACHE_TTL;
|
|
1085
|
+
const cacheStorage = cacheOpts.storage ?? "memory";
|
|
1086
|
+
const cache = new RouteCache({ storage: cacheStorage, ttl: cacheTtl });
|
|
1087
|
+
const requestInterceptors = new InterceptorManagerImpl();
|
|
1088
|
+
const responseInterceptors = new InterceptorManagerImpl();
|
|
1089
|
+
if (declarativeInterceptors?.request) {
|
|
1090
|
+
for (const entry of declarativeInterceptors.request) {
|
|
1091
|
+
if (typeof entry === "function") {
|
|
1092
|
+
requestInterceptors.use(entry);
|
|
1093
|
+
} else {
|
|
1094
|
+
const [onFulfilled, onRejected] = entry;
|
|
1095
|
+
requestInterceptors.use(onFulfilled, onRejected);
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
if (declarativeInterceptors?.response) {
|
|
1100
|
+
for (const entry of declarativeInterceptors.response) {
|
|
1101
|
+
if (typeof entry === "function") {
|
|
1102
|
+
responseInterceptors.use(entry);
|
|
1103
|
+
} else {
|
|
1104
|
+
const [onFulfilled, onRejected] = entry;
|
|
1105
|
+
responseInterceptors.use(onFulfilled, onRejected);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
const adapterPromise = resolveAdapter({
|
|
1110
|
+
adapter,
|
|
1111
|
+
forgeInterceptors: { request: requestInterceptors, response: responseInterceptors }
|
|
1112
|
+
});
|
|
1113
|
+
let adapterResolved = false;
|
|
1114
|
+
let adapterObj = null;
|
|
1115
|
+
async function ensureAdapter() {
|
|
1116
|
+
if (!adapterResolved) {
|
|
1117
|
+
adapterObj = await adapterPromise.catch((e) => {
|
|
1118
|
+
if (e instanceof AdapterNotFoundError) throw e;
|
|
1119
|
+
return resolveAdapter({
|
|
1120
|
+
adapter: "builtin",
|
|
1121
|
+
forgeInterceptors: { request: requestInterceptors, response: responseInterceptors }
|
|
1122
|
+
});
|
|
1123
|
+
});
|
|
1124
|
+
adapterResolved = true;
|
|
1125
|
+
}
|
|
1126
|
+
return adapterObj;
|
|
1127
|
+
}
|
|
1128
|
+
function assertDiscoveryReady() {
|
|
1129
|
+
if (!autoDiscoveryCompleted && !explicitLevels?.length) {
|
|
1130
|
+
throw new ForgeError(
|
|
1131
|
+
"Route data not available. Auto-discovery has not completed. Use forge.ready() or forge.use(level) first.",
|
|
1132
|
+
{ code: "RF_FE_010" }
|
|
1133
|
+
);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
async function fetchMeta(routeTag, url, level = "") {
|
|
1137
|
+
const adp = await ensureAdapter();
|
|
1138
|
+
const config = {
|
|
1139
|
+
route: routeTag,
|
|
1140
|
+
level,
|
|
1141
|
+
method: "GET",
|
|
1142
|
+
url,
|
|
1143
|
+
headers: { Accept: "application/json" },
|
|
1144
|
+
params: {},
|
|
1145
|
+
timeout,
|
|
1146
|
+
meta: {
|
|
1147
|
+
name: routeTag,
|
|
1148
|
+
uri: url,
|
|
1149
|
+
methods: ["GET"],
|
|
1150
|
+
parameters: [],
|
|
1151
|
+
level
|
|
1152
|
+
}
|
|
1153
|
+
};
|
|
1154
|
+
const doRawRequest = adp.requestRaw ?? adp.request;
|
|
1155
|
+
const resp = await doRawRequest(config);
|
|
1156
|
+
if (!resp || resp.status < 200 || resp.status >= 300) {
|
|
1157
|
+
throw new HTTPError(
|
|
1158
|
+
`Failed to fetch "${routeTag}": HTTP ${resp?.status}`,
|
|
1159
|
+
{ level, status: resp?.status, url, method: "GET" }
|
|
1160
|
+
);
|
|
1161
|
+
}
|
|
1162
|
+
return resp.data;
|
|
1163
|
+
}
|
|
1164
|
+
const autoDiscoveryPromise = fetchSummary(discoveryInputs, baseURL, fetchMeta).then((summary) => {
|
|
1165
|
+
if (summary === null) {
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
applySummaryToState(summary, discoveryState, discoveryInputs);
|
|
1169
|
+
});
|
|
1170
|
+
let autoDiscoveryError = null;
|
|
1171
|
+
autoDiscoveryPromise.catch((e) => {
|
|
1172
|
+
autoDiscoveryError = e;
|
|
1173
|
+
});
|
|
1174
|
+
const store = new RouteStore({
|
|
1175
|
+
cache,
|
|
1176
|
+
state: discoveryState,
|
|
1177
|
+
baseURL,
|
|
1178
|
+
fetchMeta,
|
|
1179
|
+
autoDiscoveryPromise,
|
|
1180
|
+
getAutoDiscoveryError: () => autoDiscoveryError
|
|
1181
|
+
});
|
|
1182
|
+
const load = (level) => store.load(level);
|
|
1183
|
+
const findRouteMeta = (level, name) => store.findRouteMeta(level, name);
|
|
1184
|
+
const invalidate = (level) => store.invalidate(level);
|
|
1185
|
+
const isLoaded = (level) => store.isLoaded(level);
|
|
1186
|
+
function getRoutes(level) {
|
|
1187
|
+
return level === void 0 ? store.getRoutes() : store.getRoutes(level);
|
|
1188
|
+
}
|
|
1189
|
+
function route(level, name, params) {
|
|
1190
|
+
assertDiscoveryReady();
|
|
1191
|
+
const meta = findRouteMeta(level, name);
|
|
1192
|
+
if (!meta) {
|
|
1193
|
+
throw new UnknownRouteError(name, level);
|
|
1194
|
+
}
|
|
1195
|
+
return buildRequestUrl(meta, params ?? {}, { baseURL, urlPrefix: discoveryState.urlPrefix });
|
|
1196
|
+
}
|
|
1197
|
+
const api = createHttpRunner({
|
|
1198
|
+
ensureAdapter,
|
|
1199
|
+
requestInterceptors,
|
|
1200
|
+
responseInterceptors,
|
|
1201
|
+
load,
|
|
1202
|
+
findRouteMeta,
|
|
1203
|
+
baseURL,
|
|
1204
|
+
state: discoveryState,
|
|
1205
|
+
timeout,
|
|
1206
|
+
loadingTracker,
|
|
1207
|
+
autoDiscoveryPromise
|
|
1208
|
+
});
|
|
1209
|
+
function hasRoute(level, name) {
|
|
1210
|
+
assertDiscoveryReady();
|
|
1211
|
+
return findRouteMeta(level, name) !== void 0;
|
|
1212
|
+
}
|
|
1213
|
+
void autoDiscoveryPromise.then(() => {
|
|
1214
|
+
autoDiscoveryCompleted = true;
|
|
1215
|
+
if (discoveryState.eager.length > 0) {
|
|
1216
|
+
return Promise.allSettled(discoveryState.eager.map((lvl) => load(lvl))).then((results) => {
|
|
1217
|
+
results.forEach((r, i) => {
|
|
1218
|
+
if (r.status === "rejected") {
|
|
1219
|
+
console.error(
|
|
1220
|
+
`[route-forge] eager load failed for level "${discoveryState.eager[i]}":`,
|
|
1221
|
+
r.reason
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1224
|
+
});
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
}).then(() => {
|
|
1228
|
+
resolveReady(forgeInstance);
|
|
1229
|
+
}).catch((e) => {
|
|
1230
|
+
rejectReady(e);
|
|
1231
|
+
});
|
|
1232
|
+
function ready(onFulfilled, onRejected) {
|
|
1233
|
+
if (onFulfilled) {
|
|
1234
|
+
const p = readyPromise.then(onFulfilled, onRejected);
|
|
1235
|
+
return p.then(() => forgeInstance);
|
|
1236
|
+
}
|
|
1237
|
+
return readyPromise;
|
|
1238
|
+
}
|
|
1239
|
+
const createBoundForgeWithMethods = createBoundForgeFactory({
|
|
1240
|
+
load,
|
|
1241
|
+
api,
|
|
1242
|
+
route,
|
|
1243
|
+
hasRoute,
|
|
1244
|
+
getRoutes,
|
|
1245
|
+
invalidate,
|
|
1246
|
+
isLoaded,
|
|
1247
|
+
loadingTracker
|
|
1248
|
+
});
|
|
1075
1249
|
const forgeInstance = {
|
|
1076
1250
|
api,
|
|
1077
1251
|
load,
|
|
@@ -1095,70 +1269,6 @@ function createRouteForge(options) {
|
|
|
1095
1269
|
};
|
|
1096
1270
|
return forgeInstance;
|
|
1097
1271
|
}
|
|
1098
|
-
function pickMethod(meta) {
|
|
1099
|
-
const m = meta.methods.find((x) => x.toUpperCase() !== "HEAD");
|
|
1100
|
-
return (m ?? meta.methods[0] ?? "GET").toUpperCase();
|
|
1101
|
-
}
|
|
1102
|
-
function appendQuery(url, query) {
|
|
1103
|
-
if (!query) return url;
|
|
1104
|
-
const usp = new URLSearchParams();
|
|
1105
|
-
for (const [k, v] of Object.entries(query)) {
|
|
1106
|
-
if (v === void 0 || v === null) continue;
|
|
1107
|
-
usp.append(k, String(v));
|
|
1108
|
-
}
|
|
1109
|
-
const qs = usp.toString();
|
|
1110
|
-
if (!qs) return url;
|
|
1111
|
-
return url.includes("?") ? `${url}&${qs}` : `${url}?${qs}`;
|
|
1112
|
-
}
|
|
1113
|
-
function resolveApiParams(input) {
|
|
1114
|
-
const {
|
|
1115
|
-
params: explicitParams,
|
|
1116
|
-
query: rawQuery,
|
|
1117
|
-
body: rawBody,
|
|
1118
|
-
headers: rawHeaders,
|
|
1119
|
-
timeout: perCallTimeout,
|
|
1120
|
-
...flatRest
|
|
1121
|
-
} = input;
|
|
1122
|
-
const pathParams = explicitParams ? { ...explicitParams } : {};
|
|
1123
|
-
for (const [k, v] of Object.entries(flatRest)) {
|
|
1124
|
-
if (!(k in pathParams)) {
|
|
1125
|
-
pathParams[k] = v;
|
|
1126
|
-
}
|
|
1127
|
-
}
|
|
1128
|
-
let query;
|
|
1129
|
-
let body;
|
|
1130
|
-
let headers;
|
|
1131
|
-
if (rawQuery !== void 0) {
|
|
1132
|
-
if (typeof rawQuery === "object" && rawQuery !== null) {
|
|
1133
|
-
query = rawQuery;
|
|
1134
|
-
} else if (!("query" in pathParams)) {
|
|
1135
|
-
pathParams.query = rawQuery;
|
|
1136
|
-
}
|
|
1137
|
-
}
|
|
1138
|
-
if (rawBody !== void 0) {
|
|
1139
|
-
if (typeof rawBody !== "string" && typeof rawBody !== "number") {
|
|
1140
|
-
body = rawBody;
|
|
1141
|
-
} else if (!("body" in pathParams)) {
|
|
1142
|
-
pathParams.body = rawBody;
|
|
1143
|
-
}
|
|
1144
|
-
}
|
|
1145
|
-
if (rawHeaders !== void 0) {
|
|
1146
|
-
if (typeof rawHeaders === "object" && rawHeaders !== null) {
|
|
1147
|
-
headers = rawHeaders;
|
|
1148
|
-
} else if (!("headers" in pathParams)) {
|
|
1149
|
-
pathParams.headers = rawHeaders;
|
|
1150
|
-
}
|
|
1151
|
-
}
|
|
1152
|
-
return { pathParams, query, body, headers, timeout: perCallTimeout };
|
|
1153
|
-
}
|
|
1154
|
-
function isAbortError2(err, signal) {
|
|
1155
|
-
if (signal?.aborted) return true;
|
|
1156
|
-
const e = err;
|
|
1157
|
-
if (!e) return false;
|
|
1158
|
-
if (e.name === "AbortError") return true;
|
|
1159
|
-
if (e.code === "ERR_CANCELED") return true;
|
|
1160
|
-
return false;
|
|
1161
|
-
}
|
|
1162
1272
|
|
|
1163
1273
|
exports.AdapterNotFoundError = AdapterNotFoundError;
|
|
1164
1274
|
exports.ForgeError = ForgeError;
|