@route-forge/core 1.1.0 → 1.2.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 +55 -3
- package/dist/codegen.d.cts +1 -1
- package/dist/codegen.d.ts +1 -1
- package/dist/defineImmutableProps.cjs +19 -0
- package/dist/defineImmutableProps.cjs.map +1 -0
- package/dist/defineImmutableProps.d.cts +10 -0
- package/dist/defineImmutableProps.d.ts +10 -0
- package/dist/defineImmutableProps.js +17 -0
- package/dist/defineImmutableProps.js.map +1 -0
- package/dist/index.cjs +87 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +34 -3
- package/dist/index.d.ts +34 -3
- package/dist/index.js +86 -10
- package/dist/index.js.map +1 -1
- package/dist/route-forge.global.js +1094 -0
- package/dist/route-forge.global.js.map +1 -0
- package/dist/route-forge.global.min.js +2 -0
- package/dist/{types-pHQLqpYF.d.cts → types-CMouq7zD.d.cts} +104 -1
- package/dist/{types-pHQLqpYF.d.ts → types-CMouq7zD.d.ts} +104 -1
- package/package.json +13 -4
|
@@ -0,0 +1,1094 @@
|
|
|
1
|
+
var RouteForge = (function (exports) {
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// src/cache.ts
|
|
5
|
+
var KEY_PREFIX = "route-forge:";
|
|
6
|
+
function pickStorage(storage) {
|
|
7
|
+
if (storage === "memory") return null;
|
|
8
|
+
if (typeof globalThis === "undefined") return null;
|
|
9
|
+
if (storage === "sessionStorage") {
|
|
10
|
+
return globalThis.sessionStorage ?? null;
|
|
11
|
+
}
|
|
12
|
+
if (storage === "localStorage") {
|
|
13
|
+
return globalThis.localStorage ?? null;
|
|
14
|
+
}
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
var RouteCache = class {
|
|
18
|
+
constructor(opts) {
|
|
19
|
+
this.memory = /* @__PURE__ */ new Map();
|
|
20
|
+
this.storage = opts.storage;
|
|
21
|
+
this.fallbackTtl = opts.ttl;
|
|
22
|
+
this.backend = pickStorage(opts.storage);
|
|
23
|
+
}
|
|
24
|
+
key(level) {
|
|
25
|
+
return `${KEY_PREFIX}${level}`;
|
|
26
|
+
}
|
|
27
|
+
get(level) {
|
|
28
|
+
if (this.storage === "memory" || !this.backend) {
|
|
29
|
+
return this.getFromMemory(level);
|
|
30
|
+
}
|
|
31
|
+
const raw = this.backend.getItem(this.key(level));
|
|
32
|
+
if (raw) {
|
|
33
|
+
try {
|
|
34
|
+
const entry = JSON.parse(raw);
|
|
35
|
+
if (this.isExpired(entry)) {
|
|
36
|
+
this.del(level);
|
|
37
|
+
return void 0;
|
|
38
|
+
}
|
|
39
|
+
return entry;
|
|
40
|
+
} catch {
|
|
41
|
+
return void 0;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return this.getFromMemory(level);
|
|
45
|
+
}
|
|
46
|
+
getFromMemory(level) {
|
|
47
|
+
const entry = this.memory.get(level);
|
|
48
|
+
if (!entry) return void 0;
|
|
49
|
+
if (this.isExpired(entry)) {
|
|
50
|
+
this.memory.delete(level);
|
|
51
|
+
return void 0;
|
|
52
|
+
}
|
|
53
|
+
return entry;
|
|
54
|
+
}
|
|
55
|
+
set(resp) {
|
|
56
|
+
let ttl;
|
|
57
|
+
if (resp.cache !== void 0 && resp.cache !== null) {
|
|
58
|
+
ttl = resp.cache > 0 ? Math.min(resp.cache, this.fallbackTtl) : resp.cache;
|
|
59
|
+
} else {
|
|
60
|
+
ttl = this.fallbackTtl;
|
|
61
|
+
}
|
|
62
|
+
const entry = {
|
|
63
|
+
level: resp.level,
|
|
64
|
+
routes: resp.routes,
|
|
65
|
+
ttl,
|
|
66
|
+
cachedAt: Date.now()
|
|
67
|
+
};
|
|
68
|
+
if (this.storage === "memory" || !this.backend) {
|
|
69
|
+
this.memory.set(resp.level, entry);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
this.backend.setItem(this.key(resp.level), JSON.stringify(entry));
|
|
74
|
+
} catch {
|
|
75
|
+
this.memory.set(resp.level, entry);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
del(level) {
|
|
79
|
+
this.memory.delete(level);
|
|
80
|
+
if (this.backend) {
|
|
81
|
+
try {
|
|
82
|
+
this.backend.removeItem(this.key(level));
|
|
83
|
+
} catch {
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
clear() {
|
|
88
|
+
this.memory.clear();
|
|
89
|
+
if (this.backend) {
|
|
90
|
+
try {
|
|
91
|
+
const toRemove = [];
|
|
92
|
+
for (let i = 0; i < this.backend.length; i++) {
|
|
93
|
+
const k = this.backend.key(i);
|
|
94
|
+
if (k && k.startsWith(KEY_PREFIX)) toRemove.push(k);
|
|
95
|
+
}
|
|
96
|
+
for (const k of toRemove) this.backend.removeItem(k);
|
|
97
|
+
} catch {
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
isExpired(entry) {
|
|
102
|
+
if (entry.ttl === null) return false;
|
|
103
|
+
if (entry.ttl === 0) return false;
|
|
104
|
+
return Date.now() - entry.cachedAt > entry.ttl * 1e3;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// src/errors.ts
|
|
109
|
+
var ForgeError = class extends Error {
|
|
110
|
+
constructor(message, opts) {
|
|
111
|
+
super(message);
|
|
112
|
+
this.name = this.constructor.name;
|
|
113
|
+
this.code = opts.code;
|
|
114
|
+
if (opts.route !== void 0) this.route = opts.route;
|
|
115
|
+
if (opts.level !== void 0) this.level = opts.level;
|
|
116
|
+
if (opts.context !== void 0) this.context = opts.context;
|
|
117
|
+
if (opts.cause !== void 0) this.cause = opts.cause;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
var UnknownRouteError = class extends ForgeError {
|
|
121
|
+
constructor(route, level) {
|
|
122
|
+
super(`Route "${route}" not found${level ? ` in level "${level}"` : ""}`, {
|
|
123
|
+
code: "RF_FE_001",
|
|
124
|
+
route,
|
|
125
|
+
level
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
var UnknownLevelError = class extends ForgeError {
|
|
130
|
+
constructor(level) {
|
|
131
|
+
super(`Level "${level}" not declared in options.levels`, {
|
|
132
|
+
code: "RF_FE_002",
|
|
133
|
+
level
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
var MissingRouteParamError = class extends ForgeError {
|
|
138
|
+
constructor(route, missingParams) {
|
|
139
|
+
super(`Missing path parameter(s) ${missingParams.join(", ")} for route "${route}"`, {
|
|
140
|
+
code: "RF_FE_003",
|
|
141
|
+
route,
|
|
142
|
+
context: { missingParams }
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
var AdapterNotFoundError = class extends ForgeError {
|
|
147
|
+
constructor(adapter) {
|
|
148
|
+
super(`Adapter "${adapter}" not available; install axios or use 'builtin'`, {
|
|
149
|
+
code: "RF_FE_005",
|
|
150
|
+
context: { adapter }
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
var InvalidInterceptorReturnError = class extends ForgeError {
|
|
155
|
+
constructor(route) {
|
|
156
|
+
super(`Request interceptor must return a RequestConfig object`, {
|
|
157
|
+
code: "RF_FE_006",
|
|
158
|
+
route
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
var NetworkError = class extends ForgeError {
|
|
163
|
+
constructor(message, route, level, cause) {
|
|
164
|
+
super(message, { code: "RF_FE_007", route, level, cause });
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
var HTTPError = class extends ForgeError {
|
|
168
|
+
constructor(message, opts) {
|
|
169
|
+
super(message, {
|
|
170
|
+
code: "RF_FE_008",
|
|
171
|
+
route: opts.route,
|
|
172
|
+
level: opts.level,
|
|
173
|
+
context: { status: opts.status, url: opts.url, method: opts.method },
|
|
174
|
+
cause: opts.cause
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
var RequestAbortedError = class extends ForgeError {
|
|
179
|
+
constructor(route, level, cause) {
|
|
180
|
+
super(`Request aborted${route ? ` for route "${route}"` : ""}`, {
|
|
181
|
+
code: "RF_FE_009",
|
|
182
|
+
route,
|
|
183
|
+
level,
|
|
184
|
+
cause
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
// src/interceptors.ts
|
|
190
|
+
var InterceptorManagerImpl = class {
|
|
191
|
+
constructor() {
|
|
192
|
+
this.handlers = [];
|
|
193
|
+
this.nextId = 0;
|
|
194
|
+
}
|
|
195
|
+
use(onFulfilled, onRejected) {
|
|
196
|
+
const id = this.nextId++;
|
|
197
|
+
this.handlers.push({ id, onFulfilled, onRejected });
|
|
198
|
+
return id;
|
|
199
|
+
}
|
|
200
|
+
eject(id) {
|
|
201
|
+
const idx = this.handlers.findIndex((h) => h.id === id);
|
|
202
|
+
if (idx >= 0) this.handlers.splice(idx, 1);
|
|
203
|
+
}
|
|
204
|
+
clear() {
|
|
205
|
+
this.handlers = [];
|
|
206
|
+
}
|
|
207
|
+
forEach(fn) {
|
|
208
|
+
for (const h of this.handlers) fn(h);
|
|
209
|
+
}
|
|
210
|
+
/** 测试用:当前注册数量 */
|
|
211
|
+
get size() {
|
|
212
|
+
return this.handlers.length;
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
async function runRequestInterceptors(manager, initial) {
|
|
216
|
+
const handlers = [];
|
|
217
|
+
manager.forEach((h) => handlers.push(h));
|
|
218
|
+
handlers.reverse();
|
|
219
|
+
let p = Promise.resolve(initial);
|
|
220
|
+
for (const h of handlers) {
|
|
221
|
+
const onF = h.onFulfilled;
|
|
222
|
+
const onR = h.onRejected;
|
|
223
|
+
p = p.then(
|
|
224
|
+
async (v) => {
|
|
225
|
+
if (!onF) return v;
|
|
226
|
+
const result = await onF(v);
|
|
227
|
+
if (result === null || typeof result !== "object") {
|
|
228
|
+
throw new InvalidInterceptorReturnError();
|
|
229
|
+
}
|
|
230
|
+
return result;
|
|
231
|
+
},
|
|
232
|
+
(e) => onR ? onR(e) : Promise.reject(e)
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
return p;
|
|
236
|
+
}
|
|
237
|
+
async function runResponseInterceptors(manager, source) {
|
|
238
|
+
const handlers = [];
|
|
239
|
+
manager.forEach((h) => handlers.push(h));
|
|
240
|
+
let p = source;
|
|
241
|
+
for (const h of handlers) {
|
|
242
|
+
const onF = h.onFulfilled;
|
|
243
|
+
const onR = h.onRejected;
|
|
244
|
+
p = p.then(
|
|
245
|
+
(v) => onF ? onF(v) : v,
|
|
246
|
+
(e) => onR ? onR(e) : Promise.reject(e)
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
return p;
|
|
250
|
+
}
|
|
251
|
+
function createInterceptorManager() {
|
|
252
|
+
return new InterceptorManagerImpl();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// src/adapters/builtin-http.ts
|
|
256
|
+
function isPassthroughBody(body) {
|
|
257
|
+
if (body === null) return false;
|
|
258
|
+
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;
|
|
259
|
+
}
|
|
260
|
+
function isAbortError(err) {
|
|
261
|
+
const e = err;
|
|
262
|
+
return !!e && e.name === "AbortError";
|
|
263
|
+
}
|
|
264
|
+
function combineSignals(...signals) {
|
|
265
|
+
const valid = signals.filter((s) => !!s);
|
|
266
|
+
if (valid.length === 0) return void 0;
|
|
267
|
+
if (valid.length === 1) return valid[0];
|
|
268
|
+
if (typeof AbortSignal !== "undefined" && typeof AbortSignal.any === "function") {
|
|
269
|
+
return AbortSignal.any(valid);
|
|
270
|
+
}
|
|
271
|
+
const controller = new AbortController();
|
|
272
|
+
for (const s of valid) {
|
|
273
|
+
if (s.aborted) {
|
|
274
|
+
controller.abort(s.reason);
|
|
275
|
+
return controller.signal;
|
|
276
|
+
}
|
|
277
|
+
s.addEventListener("abort", () => controller.abort(s.reason), { once: true });
|
|
278
|
+
}
|
|
279
|
+
return controller.signal;
|
|
280
|
+
}
|
|
281
|
+
function createBuiltinHttp(forgeInterceptors) {
|
|
282
|
+
const requestMgr = forgeInterceptors?.request ?? createInterceptorManager();
|
|
283
|
+
const responseMgr = forgeInterceptors?.response ?? createInterceptorManager();
|
|
284
|
+
async function request(config) {
|
|
285
|
+
const finalConfig = await runRequestInterceptors(requestMgr, config);
|
|
286
|
+
const signal = combineSignals(
|
|
287
|
+
finalConfig.signal,
|
|
288
|
+
finalConfig.timeout && finalConfig.timeout > 0 ? AbortSignal.timeout(finalConfig.timeout) : void 0
|
|
289
|
+
);
|
|
290
|
+
let url = finalConfig.url;
|
|
291
|
+
if (finalConfig.paramsSerializer && finalConfig.params) {
|
|
292
|
+
const qs = finalConfig.paramsSerializer(finalConfig.params);
|
|
293
|
+
if (qs) {
|
|
294
|
+
url = url.includes("?") ? `${url}&${qs}` : `${url}?${qs}`;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
const headers = new Headers(finalConfig.headers);
|
|
298
|
+
const fetchInit = {
|
|
299
|
+
method: finalConfig.method,
|
|
300
|
+
headers
|
|
301
|
+
};
|
|
302
|
+
if (signal) fetchInit.signal = signal;
|
|
303
|
+
if (finalConfig.body !== void 0 && !["GET", "HEAD"].includes(finalConfig.method.toUpperCase())) {
|
|
304
|
+
if (typeof finalConfig.body === "string" || isPassthroughBody(finalConfig.body)) {
|
|
305
|
+
fetchInit.body = finalConfig.body;
|
|
306
|
+
} else {
|
|
307
|
+
fetchInit.body = JSON.stringify(finalConfig.body);
|
|
308
|
+
if (!headers.has("Content-Type")) {
|
|
309
|
+
headers.set("Content-Type", "application/json");
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
let res;
|
|
314
|
+
try {
|
|
315
|
+
res = await fetch(url, fetchInit);
|
|
316
|
+
} catch (e) {
|
|
317
|
+
if (isAbortError(e)) throw e;
|
|
318
|
+
throw new NetworkError(
|
|
319
|
+
e instanceof Error ? e.message : String(e),
|
|
320
|
+
finalConfig.route,
|
|
321
|
+
finalConfig.level,
|
|
322
|
+
e
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
const text = await res.text();
|
|
326
|
+
let data = text;
|
|
327
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
328
|
+
if (contentType.includes("application/json")) {
|
|
329
|
+
try {
|
|
330
|
+
data = JSON.parse(text);
|
|
331
|
+
} catch {
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
const responseData = {
|
|
335
|
+
route: finalConfig.route,
|
|
336
|
+
level: finalConfig.level,
|
|
337
|
+
method: finalConfig.method,
|
|
338
|
+
url,
|
|
339
|
+
status: res.status,
|
|
340
|
+
headers: res.headers,
|
|
341
|
+
data,
|
|
342
|
+
config: finalConfig
|
|
343
|
+
};
|
|
344
|
+
const source = res.status >= 200 && res.status < 300 ? Promise.resolve(responseData) : Promise.reject(
|
|
345
|
+
new HTTPError(
|
|
346
|
+
`HTTP ${res.status} for route "${finalConfig.route}" (${finalConfig.method} ${url})`,
|
|
347
|
+
{
|
|
348
|
+
route: finalConfig.route,
|
|
349
|
+
level: finalConfig.level,
|
|
350
|
+
status: res.status,
|
|
351
|
+
url,
|
|
352
|
+
method: finalConfig.method
|
|
353
|
+
}
|
|
354
|
+
)
|
|
355
|
+
);
|
|
356
|
+
return runResponseInterceptors(
|
|
357
|
+
responseMgr,
|
|
358
|
+
source
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
const get = (url, config) => request({ ...config, url, method: "GET" });
|
|
362
|
+
const post = (url, config) => request({ ...config, url, method: "POST" });
|
|
363
|
+
const put = (url, config) => request({ ...config, url, method: "PUT" });
|
|
364
|
+
const patch = (url, config) => request({ ...config, url, method: "PATCH" });
|
|
365
|
+
const del = (url, config) => request({ ...config, url, method: "DELETE" });
|
|
366
|
+
return {
|
|
367
|
+
request,
|
|
368
|
+
interceptors: {
|
|
369
|
+
request: requestMgr,
|
|
370
|
+
response: responseMgr
|
|
371
|
+
},
|
|
372
|
+
runsInterceptors: true,
|
|
373
|
+
get,
|
|
374
|
+
post,
|
|
375
|
+
put,
|
|
376
|
+
patch,
|
|
377
|
+
delete: del
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// src/adapters/axios.ts
|
|
382
|
+
function toHeaders(raw) {
|
|
383
|
+
if (!raw) return new Headers();
|
|
384
|
+
const init = typeof raw.toJSON === "function" ? raw.toJSON() : raw;
|
|
385
|
+
try {
|
|
386
|
+
return new Headers(init);
|
|
387
|
+
} catch {
|
|
388
|
+
return new Headers();
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
async function wrapAxiosAdapter() {
|
|
392
|
+
let axios;
|
|
393
|
+
const axiosModule = "axios";
|
|
394
|
+
try {
|
|
395
|
+
const mod = await import(
|
|
396
|
+
/* @vite-ignore */
|
|
397
|
+
axiosModule
|
|
398
|
+
);
|
|
399
|
+
axios = mod.default ?? mod;
|
|
400
|
+
} catch {
|
|
401
|
+
if (typeof globalThis !== "undefined" && globalThis.axios) {
|
|
402
|
+
axios = globalThis.axios;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if (!axios || typeof axios.request !== "function") return null;
|
|
406
|
+
async function request(config) {
|
|
407
|
+
const headers = { ...config.headers };
|
|
408
|
+
try {
|
|
409
|
+
const res = await axios.request({
|
|
410
|
+
url: config.url,
|
|
411
|
+
method: config.method,
|
|
412
|
+
headers,
|
|
413
|
+
data: config.body,
|
|
414
|
+
// axios 会处理 baseURL/transformRequest 等 defaults;timeout 透传保证超时语义与 builtin 一致
|
|
415
|
+
timeout: config.timeout,
|
|
416
|
+
signal: config.signal
|
|
417
|
+
// 请求取消信号(AbortSignal)
|
|
418
|
+
});
|
|
419
|
+
return {
|
|
420
|
+
route: config.route,
|
|
421
|
+
level: config.level,
|
|
422
|
+
method: config.method,
|
|
423
|
+
url: config.url,
|
|
424
|
+
status: res.status,
|
|
425
|
+
headers: toHeaders(res.headers),
|
|
426
|
+
data: res.data,
|
|
427
|
+
config
|
|
428
|
+
};
|
|
429
|
+
} catch (e) {
|
|
430
|
+
if (e && e.response) {
|
|
431
|
+
const resp = e.response;
|
|
432
|
+
return {
|
|
433
|
+
route: config.route,
|
|
434
|
+
level: config.level,
|
|
435
|
+
method: config.method,
|
|
436
|
+
url: config.url,
|
|
437
|
+
status: resp.status,
|
|
438
|
+
headers: toHeaders(resp.headers),
|
|
439
|
+
data: resp.data,
|
|
440
|
+
config
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
throw e;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return { request, interceptors: void 0 };
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// src/adapters/index.ts
|
|
450
|
+
async function resolveAdapter(opts) {
|
|
451
|
+
const { adapter } = opts;
|
|
452
|
+
if (adapter && typeof adapter === "object") {
|
|
453
|
+
return {
|
|
454
|
+
request: (config) => adapter.request(config),
|
|
455
|
+
interceptors: adapter.interceptors
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
if (adapter === "builtin") {
|
|
459
|
+
return createBuiltinHttp(opts.forgeInterceptors);
|
|
460
|
+
}
|
|
461
|
+
if (adapter === "axios") {
|
|
462
|
+
const wrapped2 = await wrapAxiosAdapter();
|
|
463
|
+
if (!wrapped2) throw new AdapterNotFoundError("axios");
|
|
464
|
+
return wrapped2;
|
|
465
|
+
}
|
|
466
|
+
const wrapped = await wrapAxiosAdapter().catch(() => null);
|
|
467
|
+
if (wrapped) return wrapped;
|
|
468
|
+
return createBuiltinHttp(opts.forgeInterceptors);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// src/loading.ts
|
|
472
|
+
var LoadingTracker = class {
|
|
473
|
+
constructor() {
|
|
474
|
+
/** 当前并发请求计数 */
|
|
475
|
+
this.count = 0;
|
|
476
|
+
/** 订阅者集合 */
|
|
477
|
+
this.subscribers = /* @__PURE__ */ new Set();
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* 开始一次加载(计数器 +1)
|
|
481
|
+
*/
|
|
482
|
+
start() {
|
|
483
|
+
this.count++;
|
|
484
|
+
this.notify();
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* 结束一次加载(计数器 -1)
|
|
488
|
+
*/
|
|
489
|
+
stop() {
|
|
490
|
+
this.count = Math.max(0, this.count - 1);
|
|
491
|
+
this.notify();
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* 查询当前是否处于加载中
|
|
495
|
+
*/
|
|
496
|
+
isLoading() {
|
|
497
|
+
return this.count > 0;
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* 获取当前并发计数
|
|
501
|
+
*/
|
|
502
|
+
getCount() {
|
|
503
|
+
return this.count;
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* 订阅加载状态变更
|
|
507
|
+
* @returns 取消订阅函数
|
|
508
|
+
*/
|
|
509
|
+
subscribe(cb) {
|
|
510
|
+
this.subscribers.add(cb);
|
|
511
|
+
return () => {
|
|
512
|
+
this.subscribers.delete(cb);
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
/** 通知所有订阅者 */
|
|
516
|
+
notify() {
|
|
517
|
+
const event = {
|
|
518
|
+
loading: this.count > 0,
|
|
519
|
+
count: this.count
|
|
520
|
+
};
|
|
521
|
+
for (const cb of this.subscribers) {
|
|
522
|
+
try {
|
|
523
|
+
cb(event);
|
|
524
|
+
} catch {
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
// src/forge.ts
|
|
531
|
+
var DEFAULT_TIMEOUT = 3e4;
|
|
532
|
+
var DEFAULT_CACHE_TTL = 3600;
|
|
533
|
+
var UNASSIGNED_LEVEL = "unassigned";
|
|
534
|
+
function createRouteForge(options) {
|
|
535
|
+
if (!options.endpoint) throw new TypeError("options.endpoint is required");
|
|
536
|
+
const {
|
|
537
|
+
adapter = "auto",
|
|
538
|
+
timeout = DEFAULT_TIMEOUT,
|
|
539
|
+
baseURL = "",
|
|
540
|
+
interceptors: declarativeInterceptors,
|
|
541
|
+
cache: cacheOpts = {}
|
|
542
|
+
} = options;
|
|
543
|
+
const loadingTracker = new LoadingTracker();
|
|
544
|
+
const explicitLevels = options.levels;
|
|
545
|
+
const explicitEager = options.eager;
|
|
546
|
+
const explicitStrict = options.strict ?? false;
|
|
547
|
+
const explicitEndpoint = options.endpoint;
|
|
548
|
+
let effectiveLevels = explicitLevels ?? [];
|
|
549
|
+
let effectiveEager = explicitEager ?? [];
|
|
550
|
+
let effectiveEndpoint = explicitEndpoint;
|
|
551
|
+
let effectiveUrlPrefix = "";
|
|
552
|
+
let summaryUnassigned;
|
|
553
|
+
let backendHasUnassignedLevel = false;
|
|
554
|
+
const summaryPromise = (async () => {
|
|
555
|
+
try {
|
|
556
|
+
const summaryUrl = explicitEndpoint;
|
|
557
|
+
const resp = await fetch(summaryUrl, { method: "GET" });
|
|
558
|
+
if (!resp.ok) {
|
|
559
|
+
console.warn(
|
|
560
|
+
`[route-forge] summary endpoint ${summaryUrl} unreachable (HTTP ${resp.status}); falling back to explicit options`
|
|
561
|
+
);
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
return await resp.json();
|
|
565
|
+
} catch (e) {
|
|
566
|
+
if (explicitLevels && explicitLevels.length > 0) {
|
|
567
|
+
console.warn(
|
|
568
|
+
`[route-forge] summary endpoint unreachable: ${e.message}; using explicit levels`
|
|
569
|
+
);
|
|
570
|
+
return null;
|
|
571
|
+
}
|
|
572
|
+
throw new UnknownLevelError("(auto-discovery)");
|
|
573
|
+
}
|
|
574
|
+
})();
|
|
575
|
+
const autoDiscoveryPromise = summaryPromise.then((summary) => {
|
|
576
|
+
if (summary === null) {
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
const schemaVersion = summary.schemaVersion ?? 1;
|
|
580
|
+
if (schemaVersion > 1) {
|
|
581
|
+
console.warn(
|
|
582
|
+
`[route-forge] backend schemaVersion=${schemaVersion} > client supported 1; some features may be unavailable`
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
if (summary.config.endpoint_prefix && summary.config.endpoint_prefix !== explicitEndpoint) {
|
|
586
|
+
console.warn(
|
|
587
|
+
`[route-forge] backend endpoint_prefix "${summary.config.endpoint_prefix}" overrides frontend endpoint "${explicitEndpoint}"`
|
|
588
|
+
);
|
|
589
|
+
effectiveEndpoint = summary.config.endpoint_prefix;
|
|
590
|
+
}
|
|
591
|
+
if (summary.config.url_prefix) {
|
|
592
|
+
effectiveUrlPrefix = summary.config.url_prefix.endsWith("/") ? summary.config.url_prefix.slice(0, -1) : summary.config.url_prefix;
|
|
593
|
+
}
|
|
594
|
+
if (summary.config.strict_mode && !explicitStrict) {
|
|
595
|
+
console.warn(
|
|
596
|
+
"[route-forge] backend strict_mode=true overrides frontend strict=false; forcing strict=true"
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
const backendLevels = Object.keys(summary.levels);
|
|
600
|
+
backendHasUnassignedLevel = backendLevels.includes(UNASSIGNED_LEVEL);
|
|
601
|
+
if (Array.isArray(summary.unassigned) && summary.unassigned.length > 0 && !backendHasUnassignedLevel) {
|
|
602
|
+
summaryUnassigned = summary.unassigned;
|
|
603
|
+
}
|
|
604
|
+
const availableLevels = backendLevels.slice();
|
|
605
|
+
if (summaryUnassigned && !backendHasUnassignedLevel) {
|
|
606
|
+
availableLevels.push(UNASSIGNED_LEVEL);
|
|
607
|
+
}
|
|
608
|
+
if (explicitLevels && explicitLevels.length > 0) {
|
|
609
|
+
const intersection = explicitLevels.filter((l) => availableLevels.includes(l));
|
|
610
|
+
const removed = explicitLevels.filter((l) => !availableLevels.includes(l));
|
|
611
|
+
if (removed.length > 0) {
|
|
612
|
+
console.warn(
|
|
613
|
+
`[route-forge] levels not in backend summary and dropped: ${removed.join(", ")}`
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
effectiveLevels = intersection;
|
|
617
|
+
} else {
|
|
618
|
+
effectiveLevels = availableLevels;
|
|
619
|
+
}
|
|
620
|
+
const backendEager = backendLevels.filter((lvl) => summary.levels[lvl]?.load === "eager");
|
|
621
|
+
if (!explicitEager) {
|
|
622
|
+
effectiveEager = backendEager;
|
|
623
|
+
} else {
|
|
624
|
+
const union = /* @__PURE__ */ new Set([...backendEager, ...explicitEager]);
|
|
625
|
+
effectiveEager = [...union];
|
|
626
|
+
}
|
|
627
|
+
});
|
|
628
|
+
let autoDiscoveryError = null;
|
|
629
|
+
autoDiscoveryPromise.catch((e) => {
|
|
630
|
+
autoDiscoveryError = e;
|
|
631
|
+
});
|
|
632
|
+
let autoDiscoveryCompleted = false;
|
|
633
|
+
let resolveReady;
|
|
634
|
+
const readyPromise = new Promise((resolve) => {
|
|
635
|
+
resolveReady = resolve;
|
|
636
|
+
});
|
|
637
|
+
const levelLoadedListeners = /* @__PURE__ */ new Map();
|
|
638
|
+
const cacheTtl = cacheOpts.ttl ?? DEFAULT_CACHE_TTL;
|
|
639
|
+
const cacheStorage = cacheOpts.storage ?? "memory";
|
|
640
|
+
const cache = new RouteCache({ storage: cacheStorage, ttl: cacheTtl });
|
|
641
|
+
const requestInterceptors = new InterceptorManagerImpl();
|
|
642
|
+
const responseInterceptors = new InterceptorManagerImpl();
|
|
643
|
+
if (declarativeInterceptors?.request) {
|
|
644
|
+
for (const entry of declarativeInterceptors.request) {
|
|
645
|
+
if (typeof entry === "function") {
|
|
646
|
+
requestInterceptors.use(entry);
|
|
647
|
+
} else {
|
|
648
|
+
const [onFulfilled, onRejected] = entry;
|
|
649
|
+
requestInterceptors.use(onFulfilled, onRejected);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
if (declarativeInterceptors?.response) {
|
|
654
|
+
for (const entry of declarativeInterceptors.response) {
|
|
655
|
+
if (typeof entry === "function") {
|
|
656
|
+
responseInterceptors.use(entry);
|
|
657
|
+
} else {
|
|
658
|
+
const [onFulfilled, onRejected] = entry;
|
|
659
|
+
responseInterceptors.use(onFulfilled, onRejected);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
const adapterPromise = resolveAdapter({
|
|
664
|
+
adapter,
|
|
665
|
+
forgeInterceptors: { request: requestInterceptors, response: responseInterceptors }
|
|
666
|
+
});
|
|
667
|
+
let adapterResolved = false;
|
|
668
|
+
let adapterObj = null;
|
|
669
|
+
async function ensureAdapter() {
|
|
670
|
+
if (!adapterResolved) {
|
|
671
|
+
adapterObj = await adapterPromise.catch((e) => {
|
|
672
|
+
if (e instanceof AdapterNotFoundError) throw e;
|
|
673
|
+
return resolveAdapter({ adapter: "builtin" });
|
|
674
|
+
});
|
|
675
|
+
adapterResolved = true;
|
|
676
|
+
}
|
|
677
|
+
return adapterObj;
|
|
678
|
+
}
|
|
679
|
+
const inflight = /* @__PURE__ */ new Map();
|
|
680
|
+
const invalidationGens = /* @__PURE__ */ new Map();
|
|
681
|
+
function assertLevelDeclared(level) {
|
|
682
|
+
if (!effectiveLevels.includes(level)) {
|
|
683
|
+
throw new UnknownLevelError(level);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
function assertDiscoveryReady() {
|
|
687
|
+
if (!autoDiscoveryCompleted && !explicitLevels?.length) {
|
|
688
|
+
throw new ForgeError(
|
|
689
|
+
"Route data not available. Auto-discovery has not completed. Use onSummaryReady callback to mount app, or await forge.ready / forge.load(level) first.",
|
|
690
|
+
{ code: "RF_FE_010" }
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
function buildUrl(level) {
|
|
695
|
+
const base = baseURL.endsWith("/") ? baseURL.slice(0, -1) : baseURL;
|
|
696
|
+
const ep = effectiveEndpoint.startsWith("/") ? effectiveEndpoint : `/${effectiveEndpoint}`;
|
|
697
|
+
return `${base}${ep}/${encodeURIComponent(level)}`;
|
|
698
|
+
}
|
|
699
|
+
async function fetchLevel(level) {
|
|
700
|
+
const adp = await ensureAdapter();
|
|
701
|
+
const config = {
|
|
702
|
+
route: `__forge__.load.${level}`,
|
|
703
|
+
level,
|
|
704
|
+
method: "GET",
|
|
705
|
+
url: buildUrl(level),
|
|
706
|
+
headers: { Accept: "application/json" },
|
|
707
|
+
params: {},
|
|
708
|
+
timeout,
|
|
709
|
+
meta: {
|
|
710
|
+
name: `__forge__.load.${level}`,
|
|
711
|
+
uri: buildUrl(level),
|
|
712
|
+
methods: ["GET"],
|
|
713
|
+
parameters: [],
|
|
714
|
+
level
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
const resp = await adp.request(config);
|
|
718
|
+
if (!resp || resp.status < 200 || resp.status >= 300) {
|
|
719
|
+
throw new HTTPError(
|
|
720
|
+
`Failed to load level "${level}": HTTP ${resp?.status}`,
|
|
721
|
+
{ level, status: resp?.status, url: buildUrl(level), method: "GET" }
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
const data = resp.data;
|
|
725
|
+
return data;
|
|
726
|
+
}
|
|
727
|
+
async function loadOne(level) {
|
|
728
|
+
if (autoDiscoveryError) throw autoDiscoveryError;
|
|
729
|
+
assertLevelDeclared(level);
|
|
730
|
+
if (cache.get(level)) return;
|
|
731
|
+
const existing = inflight.get(level);
|
|
732
|
+
if (existing) return existing;
|
|
733
|
+
const gen = invalidationGens.get(level) ?? 0;
|
|
734
|
+
const p = (async () => {
|
|
735
|
+
try {
|
|
736
|
+
if (level === UNASSIGNED_LEVEL && !backendHasUnassignedLevel && summaryUnassigned) {
|
|
737
|
+
const routes = {};
|
|
738
|
+
for (const r of summaryUnassigned) {
|
|
739
|
+
routes[r.name] = { ...r, level: UNASSIGNED_LEVEL };
|
|
740
|
+
}
|
|
741
|
+
cache.set({ level: UNASSIGNED_LEVEL, routes, cache: null });
|
|
742
|
+
const listeners = levelLoadedListeners.get(level);
|
|
743
|
+
if (listeners) listeners.forEach((cb) => cb());
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
const resp = await fetchLevel(level);
|
|
747
|
+
if ((invalidationGens.get(level) ?? 0) === gen) {
|
|
748
|
+
cache.set(resp);
|
|
749
|
+
const listeners = levelLoadedListeners.get(level);
|
|
750
|
+
if (listeners) listeners.forEach((cb) => cb());
|
|
751
|
+
}
|
|
752
|
+
} finally {
|
|
753
|
+
inflight.delete(level);
|
|
754
|
+
}
|
|
755
|
+
})();
|
|
756
|
+
inflight.set(level, p);
|
|
757
|
+
return p;
|
|
758
|
+
}
|
|
759
|
+
async function load(level) {
|
|
760
|
+
await autoDiscoveryPromise;
|
|
761
|
+
const list = Array.isArray(level) ? level : [level];
|
|
762
|
+
await Promise.all(list.map(loadOne));
|
|
763
|
+
}
|
|
764
|
+
function route(level, name, params) {
|
|
765
|
+
assertDiscoveryReady();
|
|
766
|
+
const meta = findRouteMeta(level, name);
|
|
767
|
+
if (!meta) {
|
|
768
|
+
throw new UnknownRouteError(name, level);
|
|
769
|
+
}
|
|
770
|
+
return buildRequestUrl(meta, params ?? {});
|
|
771
|
+
}
|
|
772
|
+
function buildRequestUrl(meta, params) {
|
|
773
|
+
const defaults = meta.parameter_defaults ?? {};
|
|
774
|
+
const missingRequired = [];
|
|
775
|
+
const values = {};
|
|
776
|
+
for (const p of meta.parameters) {
|
|
777
|
+
let v = params[p];
|
|
778
|
+
if ((v === void 0 || v === null) && p in defaults) {
|
|
779
|
+
v = defaults[p];
|
|
780
|
+
}
|
|
781
|
+
if (v === void 0 || v === null) {
|
|
782
|
+
if (!meta.uri.includes(`{${p}?}`)) {
|
|
783
|
+
missingRequired.push(p);
|
|
784
|
+
}
|
|
785
|
+
} else {
|
|
786
|
+
values[p] = v;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
if (missingRequired.length > 0) {
|
|
790
|
+
throw new MissingRouteParamError(meta.name, missingRequired);
|
|
791
|
+
}
|
|
792
|
+
let uri = meta.uri.replace(/\{([^{}]+)\}/g, (match, raw) => {
|
|
793
|
+
const optional = raw.endsWith("?");
|
|
794
|
+
const name = optional ? raw.slice(0, -1) : raw;
|
|
795
|
+
if (values[name] !== void 0) {
|
|
796
|
+
const val = values[name];
|
|
797
|
+
if (typeof val === "object") {
|
|
798
|
+
throw new ForgeError(
|
|
799
|
+
`Path parameter "${name}" must be a primitive value (string, number, boolean), got ${typeof val}`,
|
|
800
|
+
{ code: "RF_FE_003", route: meta.name, context: { param: name, value: val } }
|
|
801
|
+
);
|
|
802
|
+
}
|
|
803
|
+
return encodeURIComponent(String(val));
|
|
804
|
+
}
|
|
805
|
+
return optional ? "" : match;
|
|
806
|
+
});
|
|
807
|
+
uri = uri.replace(/\/+/g, "/").replace(/\/$/, "");
|
|
808
|
+
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(effectiveUrlPrefix)) {
|
|
809
|
+
const prefix2 = effectiveUrlPrefix.endsWith("/") ? effectiveUrlPrefix.slice(0, -1) : effectiveUrlPrefix;
|
|
810
|
+
return uri.startsWith("/") ? `${prefix2}${uri}` : `${prefix2}/${uri}`;
|
|
811
|
+
}
|
|
812
|
+
const base = baseURL.endsWith("/") ? baseURL.slice(0, -1) : baseURL;
|
|
813
|
+
const prefix = effectiveUrlPrefix;
|
|
814
|
+
return uri.startsWith("/") ? `${base}${prefix}${uri}` : `${base}${prefix}/${uri}`;
|
|
815
|
+
}
|
|
816
|
+
function findRouteMeta(level, name) {
|
|
817
|
+
const entry = cache.get(level);
|
|
818
|
+
const meta = entry?.routes[name];
|
|
819
|
+
if (meta) {
|
|
820
|
+
return { ...meta, level };
|
|
821
|
+
}
|
|
822
|
+
return void 0;
|
|
823
|
+
}
|
|
824
|
+
async function api(level, name, params = {}) {
|
|
825
|
+
await autoDiscoveryPromise;
|
|
826
|
+
await load(level);
|
|
827
|
+
const meta = findRouteMeta(level, name);
|
|
828
|
+
if (!meta) {
|
|
829
|
+
throw new UnknownRouteError(name, level);
|
|
830
|
+
}
|
|
831
|
+
return doApiCall(meta, params);
|
|
832
|
+
}
|
|
833
|
+
async function doApiCall(meta, params) {
|
|
834
|
+
const {
|
|
835
|
+
pathParams,
|
|
836
|
+
query,
|
|
837
|
+
body,
|
|
838
|
+
headers,
|
|
839
|
+
timeout: perCallTimeout,
|
|
840
|
+
signal
|
|
841
|
+
} = resolveApiParams(params);
|
|
842
|
+
if (signal?.aborted) {
|
|
843
|
+
throw new RequestAbortedError(meta.name, meta.level, signal.reason);
|
|
844
|
+
}
|
|
845
|
+
const method = pickMethod(meta);
|
|
846
|
+
const urlWithQuery = appendQuery(buildRequestUrl(meta, pathParams), query);
|
|
847
|
+
const config = {
|
|
848
|
+
route: meta.name,
|
|
849
|
+
level: meta.level ?? "",
|
|
850
|
+
method,
|
|
851
|
+
url: urlWithQuery,
|
|
852
|
+
headers: { Accept: "application/json", ...headers ?? {} },
|
|
853
|
+
body,
|
|
854
|
+
params: pathParams,
|
|
855
|
+
timeout: perCallTimeout ?? timeout,
|
|
856
|
+
signal,
|
|
857
|
+
meta
|
|
858
|
+
};
|
|
859
|
+
const adp = await ensureAdapter();
|
|
860
|
+
const finalConfig = adp.runsInterceptors ? config : await runRequestInterceptors(requestInterceptors, config);
|
|
861
|
+
if (finalConfig.signal?.aborted) {
|
|
862
|
+
throw new RequestAbortedError(meta.name, meta.level, finalConfig.signal.reason);
|
|
863
|
+
}
|
|
864
|
+
loadingTracker.start();
|
|
865
|
+
try {
|
|
866
|
+
const source = adp.request(finalConfig).then(
|
|
867
|
+
(resp) => {
|
|
868
|
+
if (resp.status < 200 || resp.status >= 300) {
|
|
869
|
+
throw new HTTPError(
|
|
870
|
+
`HTTP ${resp.status} for route "${resp.route}" (${resp.method} ${resp.url})`,
|
|
871
|
+
{
|
|
872
|
+
route: resp.route,
|
|
873
|
+
level: resp.level,
|
|
874
|
+
status: resp.status,
|
|
875
|
+
url: resp.url,
|
|
876
|
+
method: resp.method
|
|
877
|
+
}
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
return resp;
|
|
881
|
+
},
|
|
882
|
+
(err) => {
|
|
883
|
+
if (err instanceof ForgeError) throw err;
|
|
884
|
+
if (isAbortError2(err, finalConfig.signal)) {
|
|
885
|
+
throw new RequestAbortedError(meta.name, meta.level, err);
|
|
886
|
+
}
|
|
887
|
+
throw new NetworkError(
|
|
888
|
+
err instanceof Error ? err.message : String(err),
|
|
889
|
+
meta.name,
|
|
890
|
+
meta.level,
|
|
891
|
+
err
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
);
|
|
895
|
+
const result = adp.runsInterceptors ? await source : await runResponseInterceptors(responseInterceptors, source);
|
|
896
|
+
return result;
|
|
897
|
+
} finally {
|
|
898
|
+
loadingTracker.stop();
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
function invalidate(level) {
|
|
902
|
+
if (level === void 0) {
|
|
903
|
+
cache.clear();
|
|
904
|
+
inflight.clear();
|
|
905
|
+
for (const lvl of effectiveLevels) {
|
|
906
|
+
invalidationGens.set(lvl, (invalidationGens.get(lvl) ?? 0) + 1);
|
|
907
|
+
}
|
|
908
|
+
} else if (Array.isArray(level)) {
|
|
909
|
+
for (const lvl of level) {
|
|
910
|
+
cache.del(lvl);
|
|
911
|
+
inflight.delete(lvl);
|
|
912
|
+
invalidationGens.set(lvl, (invalidationGens.get(lvl) ?? 0) + 1);
|
|
913
|
+
}
|
|
914
|
+
} else {
|
|
915
|
+
cache.del(level);
|
|
916
|
+
inflight.delete(level);
|
|
917
|
+
invalidationGens.set(level, (invalidationGens.get(level) ?? 0) + 1);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
function isLoaded(level) {
|
|
921
|
+
if (level) return cache.get(level) !== void 0;
|
|
922
|
+
return effectiveLevels.length > 0 && effectiveLevels.every((lvl) => cache.get(lvl) !== void 0);
|
|
923
|
+
}
|
|
924
|
+
function hasRoute(level, name) {
|
|
925
|
+
assertDiscoveryReady();
|
|
926
|
+
return findRouteMeta(level, name) !== void 0;
|
|
927
|
+
}
|
|
928
|
+
function getRoutes(level) {
|
|
929
|
+
if (level !== void 0) {
|
|
930
|
+
const entry = cache.get(level);
|
|
931
|
+
const routes = entry?.routes ?? {};
|
|
932
|
+
const result2 = {};
|
|
933
|
+
for (const [k, v] of Object.entries(routes)) {
|
|
934
|
+
result2[k] = JSON.parse(JSON.stringify(v));
|
|
935
|
+
}
|
|
936
|
+
return result2;
|
|
937
|
+
}
|
|
938
|
+
const result = {};
|
|
939
|
+
for (const lvl of effectiveLevels) {
|
|
940
|
+
const entry = cache.get(lvl);
|
|
941
|
+
if (entry) {
|
|
942
|
+
const levelRoutes = {};
|
|
943
|
+
for (const [k, v] of Object.entries(entry.routes)) {
|
|
944
|
+
levelRoutes[k] = JSON.parse(JSON.stringify(v));
|
|
945
|
+
}
|
|
946
|
+
result[lvl] = levelRoutes;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
return result;
|
|
950
|
+
}
|
|
951
|
+
void autoDiscoveryPromise.then(() => {
|
|
952
|
+
options.onSummaryReady?.();
|
|
953
|
+
if (effectiveEager.length > 0) {
|
|
954
|
+
return Promise.all(effectiveEager.map((lvl) => load(lvl))).catch((e) => {
|
|
955
|
+
console.warn(`[route-forge] eager load failed: ${e.message}`);
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
}).then(() => {
|
|
959
|
+
autoDiscoveryCompleted = true;
|
|
960
|
+
resolveReady();
|
|
961
|
+
}).catch(() => {
|
|
962
|
+
});
|
|
963
|
+
return {
|
|
964
|
+
api,
|
|
965
|
+
load,
|
|
966
|
+
route,
|
|
967
|
+
url: route,
|
|
968
|
+
invalidate,
|
|
969
|
+
isLoaded,
|
|
970
|
+
hasRoute,
|
|
971
|
+
getRoutes,
|
|
972
|
+
isLoading: () => loadingTracker.isLoading(),
|
|
973
|
+
onLoadingChange: (cb) => loadingTracker.subscribe(cb),
|
|
974
|
+
interceptors: {
|
|
975
|
+
request: requestInterceptors,
|
|
976
|
+
response: responseInterceptors
|
|
977
|
+
},
|
|
978
|
+
ready: readyPromise,
|
|
979
|
+
onLevelLoaded(level, cb) {
|
|
980
|
+
if (!levelLoadedListeners.has(level)) {
|
|
981
|
+
levelLoadedListeners.set(level, /* @__PURE__ */ new Set());
|
|
982
|
+
}
|
|
983
|
+
levelLoadedListeners.get(level).add(cb);
|
|
984
|
+
return () => levelLoadedListeners.get(level)?.delete(cb);
|
|
985
|
+
}
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
function pickMethod(meta) {
|
|
989
|
+
const m = meta.methods.find((x) => x.toUpperCase() !== "HEAD");
|
|
990
|
+
return (m ?? meta.methods[0] ?? "GET").toUpperCase();
|
|
991
|
+
}
|
|
992
|
+
function appendQuery(url, query) {
|
|
993
|
+
if (!query) return url;
|
|
994
|
+
const usp = new URLSearchParams();
|
|
995
|
+
for (const [k, v] of Object.entries(query)) {
|
|
996
|
+
if (v === void 0 || v === null) continue;
|
|
997
|
+
usp.append(k, String(v));
|
|
998
|
+
}
|
|
999
|
+
const qs = usp.toString();
|
|
1000
|
+
if (!qs) return url;
|
|
1001
|
+
return url.includes("?") ? `${url}&${qs}` : `${url}?${qs}`;
|
|
1002
|
+
}
|
|
1003
|
+
function resolveApiParams(input) {
|
|
1004
|
+
const {
|
|
1005
|
+
params: explicitParams,
|
|
1006
|
+
query: rawQuery,
|
|
1007
|
+
body: rawBody,
|
|
1008
|
+
headers: rawHeaders,
|
|
1009
|
+
timeout: perCallTimeout,
|
|
1010
|
+
signal,
|
|
1011
|
+
...flatRest
|
|
1012
|
+
} = input;
|
|
1013
|
+
const pathParams = explicitParams ? { ...explicitParams } : {};
|
|
1014
|
+
for (const [k, v] of Object.entries(flatRest)) {
|
|
1015
|
+
if (!(k in pathParams)) {
|
|
1016
|
+
pathParams[k] = v;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
let query;
|
|
1020
|
+
let body;
|
|
1021
|
+
let headers;
|
|
1022
|
+
if (rawQuery !== void 0) {
|
|
1023
|
+
if (typeof rawQuery === "object" && rawQuery !== null) {
|
|
1024
|
+
query = rawQuery;
|
|
1025
|
+
} else if (!("query" in pathParams)) {
|
|
1026
|
+
pathParams.query = rawQuery;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
if (rawBody !== void 0) {
|
|
1030
|
+
if (typeof rawBody !== "string" && typeof rawBody !== "number") {
|
|
1031
|
+
body = rawBody;
|
|
1032
|
+
} else if (!("body" in pathParams)) {
|
|
1033
|
+
pathParams.body = rawBody;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
if (rawHeaders !== void 0) {
|
|
1037
|
+
if (typeof rawHeaders === "object" && rawHeaders !== null) {
|
|
1038
|
+
headers = rawHeaders;
|
|
1039
|
+
} else if (!("headers" in pathParams)) {
|
|
1040
|
+
pathParams.headers = rawHeaders;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
return { pathParams, query, body, headers, timeout: perCallTimeout, signal };
|
|
1044
|
+
}
|
|
1045
|
+
function isAbortError2(err, signal) {
|
|
1046
|
+
if (signal?.aborted) return true;
|
|
1047
|
+
const e = err;
|
|
1048
|
+
if (!e) return false;
|
|
1049
|
+
if (e.name === "AbortError") return true;
|
|
1050
|
+
if (e.code === "ERR_CANCELED") return true;
|
|
1051
|
+
return false;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// src/resolveRouteName.ts
|
|
1055
|
+
async function resolveRouteName(forge, level, prefix, suffix, separator = ".") {
|
|
1056
|
+
if (!suffix) return prefix;
|
|
1057
|
+
const joined = `${prefix}${separator}${suffix}`;
|
|
1058
|
+
if (!suffix.startsWith(`${prefix}${separator}`)) return joined;
|
|
1059
|
+
await forge.load(level);
|
|
1060
|
+
if (forge.hasRoute(level, joined)) return joined;
|
|
1061
|
+
if (forge.hasRoute(level, suffix)) return suffix;
|
|
1062
|
+
throw new UnknownRouteError(joined, level);
|
|
1063
|
+
}
|
|
1064
|
+
function resolveRouteNameSync(forge, level, prefix, suffix, separator = ".") {
|
|
1065
|
+
if (!suffix) return prefix;
|
|
1066
|
+
const joined = `${prefix}${separator}${suffix}`;
|
|
1067
|
+
if (!suffix.startsWith(`${prefix}${separator}`)) return joined;
|
|
1068
|
+
if (forge.hasRoute(level, joined)) return joined;
|
|
1069
|
+
if (forge.hasRoute(level, suffix)) return suffix;
|
|
1070
|
+
throw new UnknownRouteError(joined, level);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
exports.AdapterNotFoundError = AdapterNotFoundError;
|
|
1074
|
+
exports.ForgeError = ForgeError;
|
|
1075
|
+
exports.HTTPError = HTTPError;
|
|
1076
|
+
exports.InterceptorManagerImpl = InterceptorManagerImpl;
|
|
1077
|
+
exports.InvalidInterceptorReturnError = InvalidInterceptorReturnError;
|
|
1078
|
+
exports.LoadingTracker = LoadingTracker;
|
|
1079
|
+
exports.MissingRouteParamError = MissingRouteParamError;
|
|
1080
|
+
exports.NetworkError = NetworkError;
|
|
1081
|
+
exports.RequestAbortedError = RequestAbortedError;
|
|
1082
|
+
exports.RouteCache = RouteCache;
|
|
1083
|
+
exports.UnknownLevelError = UnknownLevelError;
|
|
1084
|
+
exports.UnknownRouteError = UnknownRouteError;
|
|
1085
|
+
exports.createInterceptorManager = createInterceptorManager;
|
|
1086
|
+
exports.createRouteForge = createRouteForge;
|
|
1087
|
+
exports.resolveRouteName = resolveRouteName;
|
|
1088
|
+
exports.resolveRouteNameSync = resolveRouteNameSync;
|
|
1089
|
+
|
|
1090
|
+
return exports;
|
|
1091
|
+
|
|
1092
|
+
})({});
|
|
1093
|
+
//# sourceMappingURL=route-forge.global.js.map
|
|
1094
|
+
//# sourceMappingURL=route-forge.global.js.map
|