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