ebely 0.0.6 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +67 -3
- package/dist/index.js +136 -53
- package/dist/index.mjs +136 -53
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -110,6 +110,24 @@ type AfterHookArgs<Ctx, Body = unknown, ResBody = unknown> = {
|
|
|
110
110
|
type BeforeHook<Ctx, Body = unknown> = (args: BeforeHookArgs<Ctx, Body>) => void | Promise<void>;
|
|
111
111
|
/** `after`-хук: вызывается ПОСЛЕ ответа; `response` — только на чтение. */
|
|
112
112
|
type AfterHook<Ctx, Body = unknown, ResBody = unknown> = (args: AfterHookArgs<Ctx, Body, ResBody>) => void | Promise<void>;
|
|
113
|
+
/**
|
|
114
|
+
* `retry`-хук: вызывается ПОСЛЕ ответа (и после всех `after`-хуков) и
|
|
115
|
+
* РЕШАЕТ, нужно ли переиграть тот же запрос. Возврат `true` → ebely
|
|
116
|
+
* выполняет запрос заново (с нуля: снова прогоняет `before`-хуки и шлёт
|
|
117
|
+
* fetch), поэтому правки, сделанные внутри хука (напр. свежий токен в
|
|
118
|
+
* `ctx`), автоматически подхватятся повторным `before`. Любой
|
|
119
|
+
* не-истинный возврат (`false` / `undefined`) → ретрая нет, отдаём ответ
|
|
120
|
+
* как есть.
|
|
121
|
+
*
|
|
122
|
+
* Аргумент — тот же, что у `after` (`request` / `response` / `ctx`):
|
|
123
|
+
* `response` тут на чтение (его смотрят, чтобы решить про ретрай), а
|
|
124
|
+
* подготовку к повтору (refresh токена и т.п.) хук делает через `ctx`.
|
|
125
|
+
*
|
|
126
|
+
* Число повторов ограничено ядром (`EbelyConfig.maxRetries`, по
|
|
127
|
+
* умолчанию 3) — даже если хук упрямо возвращает `true`, бесконечного
|
|
128
|
+
* цикла не будет.
|
|
129
|
+
*/
|
|
130
|
+
type RetryHook<Ctx, Body = unknown, ResBody = unknown> = (args: AfterHookArgs<Ctx, Body, ResBody>) => boolean | void | Promise<boolean | void>;
|
|
113
131
|
/**
|
|
114
132
|
* Регистратор хуков, который пользователь передаёт в конфиг одной
|
|
115
133
|
* переменной (`EbelyConfig.hooks`). Точную типизированную форму аргумента
|
|
@@ -137,6 +155,9 @@ type HooksRegistrar = (registrar: any) => void;
|
|
|
137
155
|
declare class HookRegistry {
|
|
138
156
|
private beforeMap;
|
|
139
157
|
private afterMap;
|
|
158
|
+
private globalBeforeHooks;
|
|
159
|
+
private globalAfterHooks;
|
|
160
|
+
private globalRetryHooks;
|
|
140
161
|
/** Зарегистрировать `before`-хук на ключ `"<группа>.<метод>"`. */
|
|
141
162
|
before(args: {
|
|
142
163
|
key: string;
|
|
@@ -147,19 +168,52 @@ declare class HookRegistry {
|
|
|
147
168
|
key: string;
|
|
148
169
|
fn: AfterHook<any>;
|
|
149
170
|
}): void;
|
|
150
|
-
/**
|
|
171
|
+
/** Зарегистрировать ГЛОБАЛЬНЫЙ `before`-хук (на все операции). */
|
|
172
|
+
globalBefore(args: {
|
|
173
|
+
fn: BeforeHook<any>;
|
|
174
|
+
}): void;
|
|
175
|
+
/** Зарегистрировать ГЛОБАЛЬНЫЙ `after`-хук (на все операции). */
|
|
176
|
+
globalAfter(args: {
|
|
177
|
+
fn: AfterHook<any>;
|
|
178
|
+
}): void;
|
|
179
|
+
/** Зарегистрировать ГЛОБАЛЬНЫЙ `retry`-хук (решает про повтор запроса). */
|
|
180
|
+
globalRetry(args: {
|
|
181
|
+
fn: RetryHook<any>;
|
|
182
|
+
}): void;
|
|
183
|
+
/**
|
|
184
|
+
* Прогнать `before`-хуки по очереди (await на async): сначала глобальные,
|
|
185
|
+
* затем поименные для ключа — так общая подготовка (напр. заголовок
|
|
186
|
+
* авторизации) ложится раньше, а точечный хук может её переопределить.
|
|
187
|
+
*/
|
|
151
188
|
runBefore(args: {
|
|
152
189
|
key: string;
|
|
153
190
|
request: HookRequest;
|
|
154
191
|
ctx: unknown;
|
|
155
192
|
}): Promise<void>;
|
|
156
|
-
/**
|
|
193
|
+
/**
|
|
194
|
+
* Прогнать `after`-хуки по очереди (await на async): сначала поименные
|
|
195
|
+
* для ключа, затем глобальные — так глобальный «оборачивает» точечные
|
|
196
|
+
* (видит итоговый эффект, удобно для логирования/обработки статуса).
|
|
197
|
+
*/
|
|
157
198
|
runAfter(args: {
|
|
158
199
|
key: string;
|
|
159
200
|
request: HookRequest;
|
|
160
201
|
response: HookResponse;
|
|
161
202
|
ctx: unknown;
|
|
162
203
|
}): Promise<void>;
|
|
204
|
+
/**
|
|
205
|
+
* Спросить retry-хуки, нужно ли переиграть запрос. Прогон по очереди
|
|
206
|
+
* регистрации; ПЕРВЫЙ хук, вернувший истину, выигрывает (он уже сделал
|
|
207
|
+
* подготовку — напр. refresh — поэтому остальные не дёргаем). Если ни
|
|
208
|
+
* один не вернул истину — `false` (повтора нет). Цикл и лимит повторов
|
|
209
|
+
* — на стороне вызывающего (сгенерированный `request`).
|
|
210
|
+
*/
|
|
211
|
+
runRetry(args: {
|
|
212
|
+
key: string;
|
|
213
|
+
request: HookRequest;
|
|
214
|
+
response: HookResponse;
|
|
215
|
+
ctx: unknown;
|
|
216
|
+
}): Promise<boolean>;
|
|
163
217
|
}
|
|
164
218
|
|
|
165
219
|
/**
|
|
@@ -218,6 +272,16 @@ type EbelyConfig = {
|
|
|
218
272
|
* @default 'test'
|
|
219
273
|
*/
|
|
220
274
|
mode?: ClientMode;
|
|
275
|
+
/**
|
|
276
|
+
* Максимальное число ПОВТОРОВ запроса, которые ebely сделает, если
|
|
277
|
+
* глобальный `retry`-хук (`h.globalRetry`) вернул `true`. Это потолок
|
|
278
|
+
* на повторы СВЕРХ первой попытки: при `maxRetries: 3` запрос уйдёт
|
|
279
|
+
* максимум 4 раза. Защита от бесконечного цикла, если хук упрямо просит
|
|
280
|
+
* повтор (напр. сервер стабильно отвечает 401). Без `globalRetry`-хуков
|
|
281
|
+
* не влияет ни на что.
|
|
282
|
+
* @default 3
|
|
283
|
+
*/
|
|
284
|
+
maxRetries?: number;
|
|
221
285
|
/**
|
|
222
286
|
* Регистратор хуков `before` / `after`. Объявляется в ОТДЕЛЬНОМ
|
|
223
287
|
* типизированном файле (тип `Hooks` экспортирует сгенерированный
|
|
@@ -270,4 +334,4 @@ declare function generateClient(args: EbelyConfig): Promise<{
|
|
|
270
334
|
operations: number;
|
|
271
335
|
}>;
|
|
272
336
|
|
|
273
|
-
export { AfterHook, AfterHookArgs, ApiResponse, BaseStore, BeforeHook, BeforeHookArgs, ClientMode, DeepPartial, EbelyAssertionError, EbelyConfig, HookRegistry, HookRequest, HookResponse, HooksRegistrar, SwaggerSource, generateClient };
|
|
337
|
+
export { AfterHook, AfterHookArgs, ApiResponse, BaseStore, BeforeHook, BeforeHookArgs, ClientMode, DeepPartial, EbelyAssertionError, EbelyConfig, HookRegistry, HookRequest, HookResponse, HooksRegistrar, RetryHook, SwaggerSource, generateClient };
|
package/dist/index.js
CHANGED
|
@@ -163,6 +163,15 @@ var ApiResponse = class {
|
|
|
163
163
|
var HookRegistry = class {
|
|
164
164
|
beforeMap = /* @__PURE__ */ new Map();
|
|
165
165
|
afterMap = /* @__PURE__ */ new Map();
|
|
166
|
+
// Глобальные хуки — срабатывают на КАЖДУЮ операцию (вне зависимости от
|
|
167
|
+
// ключа). Удобны для сквозных задач: подстановка `Authorization` из
|
|
168
|
+
// `ctx` во все запросы, логирование, обработка 401 и т.п.
|
|
169
|
+
globalBeforeHooks = [];
|
|
170
|
+
globalAfterHooks = [];
|
|
171
|
+
// Глобальные retry-хуки — решают, переигрывать ли запрос (напр. на 401
|
|
172
|
+
// сходить за refresh и вернуть `true`). Цикл повторов и его лимит живут
|
|
173
|
+
// в сгенерированном `request` (см. render.ts) — сам реестр сети не знает.
|
|
174
|
+
globalRetryHooks = [];
|
|
166
175
|
/** Зарегистрировать `before`-хук на ключ `"<группа>.<метод>"`. */
|
|
167
176
|
before(args) {
|
|
168
177
|
const list = this.beforeMap.get(args.key) ?? [];
|
|
@@ -175,18 +184,59 @@ var HookRegistry = class {
|
|
|
175
184
|
list.push(args.fn);
|
|
176
185
|
this.afterMap.set(args.key, list);
|
|
177
186
|
}
|
|
178
|
-
/**
|
|
187
|
+
/** Зарегистрировать ГЛОБАЛЬНЫЙ `before`-хук (на все операции). */
|
|
188
|
+
globalBefore(args) {
|
|
189
|
+
this.globalBeforeHooks.push(args.fn);
|
|
190
|
+
}
|
|
191
|
+
/** Зарегистрировать ГЛОБАЛЬНЫЙ `after`-хук (на все операции). */
|
|
192
|
+
globalAfter(args) {
|
|
193
|
+
this.globalAfterHooks.push(args.fn);
|
|
194
|
+
}
|
|
195
|
+
/** Зарегистрировать ГЛОБАЛЬНЫЙ `retry`-хук (решает про повтор запроса). */
|
|
196
|
+
globalRetry(args) {
|
|
197
|
+
this.globalRetryHooks.push(args.fn);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Прогнать `before`-хуки по очереди (await на async): сначала глобальные,
|
|
201
|
+
* затем поименные для ключа — так общая подготовка (напр. заголовок
|
|
202
|
+
* авторизации) ложится раньше, а точечный хук может её переопределить.
|
|
203
|
+
*/
|
|
179
204
|
async runBefore(args) {
|
|
180
|
-
|
|
205
|
+
const hooks = [...this.globalBeforeHooks, ...this.beforeMap.get(args.key) ?? []];
|
|
206
|
+
for (const fn of hooks) {
|
|
181
207
|
await fn({ request: args.request, ctx: args.ctx });
|
|
182
208
|
}
|
|
183
209
|
}
|
|
184
|
-
/**
|
|
210
|
+
/**
|
|
211
|
+
* Прогнать `after`-хуки по очереди (await на async): сначала поименные
|
|
212
|
+
* для ключа, затем глобальные — так глобальный «оборачивает» точечные
|
|
213
|
+
* (видит итоговый эффект, удобно для логирования/обработки статуса).
|
|
214
|
+
*/
|
|
185
215
|
async runAfter(args) {
|
|
186
|
-
|
|
216
|
+
const hooks = [...this.afterMap.get(args.key) ?? [], ...this.globalAfterHooks];
|
|
217
|
+
for (const fn of hooks) {
|
|
187
218
|
await fn({ request: args.request, response: args.response, ctx: args.ctx });
|
|
188
219
|
}
|
|
189
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* Спросить retry-хуки, нужно ли переиграть запрос. Прогон по очереди
|
|
223
|
+
* регистрации; ПЕРВЫЙ хук, вернувший истину, выигрывает (он уже сделал
|
|
224
|
+
* подготовку — напр. refresh — поэтому остальные не дёргаем). Если ни
|
|
225
|
+
* один не вернул истину — `false` (повтора нет). Цикл и лимит повторов
|
|
226
|
+
* — на стороне вызывающего (сгенерированный `request`).
|
|
227
|
+
*/
|
|
228
|
+
async runRetry(args) {
|
|
229
|
+
for (const fn of this.globalRetryHooks) {
|
|
230
|
+
const decision = await fn({
|
|
231
|
+
request: args.request,
|
|
232
|
+
response: args.response,
|
|
233
|
+
ctx: args.ctx
|
|
234
|
+
});
|
|
235
|
+
if (decision)
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
190
240
|
};
|
|
191
241
|
|
|
192
242
|
// src/generate-client.ts
|
|
@@ -410,7 +460,7 @@ function renderImports(args) {
|
|
|
410
460
|
const { mode, userStoreImport, configImport } = args;
|
|
411
461
|
const values = mode === "frontend" ? "BaseStore, HookRegistry" : "BaseStore, ApiResponse, HookRegistry";
|
|
412
462
|
return `import { ${values} } from ${JSON.stringify(userStoreImport)}
|
|
413
|
-
import type { BeforeHook, AfterHook } from ${JSON.stringify(userStoreImport)}
|
|
463
|
+
import type { BeforeHook, AfterHook, RetryHook } from ${JSON.stringify(userStoreImport)}
|
|
414
464
|
import { ebely } from ${JSON.stringify(configImport)}`;
|
|
415
465
|
}
|
|
416
466
|
function renderRequestTail(mode) {
|
|
@@ -469,7 +519,11 @@ function renderHookTreeType(groups) {
|
|
|
469
519
|
${leaves.join("\n")}
|
|
470
520
|
}`;
|
|
471
521
|
});
|
|
522
|
+
const global = ` globalBefore(fn: BeforeHook<Store>): void
|
|
523
|
+
globalAfter(fn: AfterHook<Store>): void
|
|
524
|
+
globalRetry(fn: RetryHook<Store>): void`;
|
|
472
525
|
return `type EbelyHookTree<Store extends BaseStore> = {
|
|
526
|
+
${global}
|
|
473
527
|
${blocks.join("\n")}
|
|
474
528
|
}`;
|
|
475
529
|
}
|
|
@@ -489,6 +543,9 @@ ${leaves.join("\n")}
|
|
|
489
543
|
return ` private buildHookTree(): EbelyHookTree<Store> {
|
|
490
544
|
const r = this.hookRegistry
|
|
491
545
|
return {
|
|
546
|
+
globalBefore: (fn: BeforeHook<Store>) => r.globalBefore({ fn }),
|
|
547
|
+
globalAfter: (fn: AfterHook<Store>) => r.globalAfter({ fn }),
|
|
548
|
+
globalRetry: (fn: RetryHook<Store>) => r.globalRetry({ fn }),
|
|
492
549
|
${blocks.join("\n")}
|
|
493
550
|
} as unknown as EbelyHookTree<Store>
|
|
494
551
|
}`;
|
|
@@ -513,6 +570,7 @@ function renderClient(args) {
|
|
|
513
570
|
const basePath = spec.servers?.[0]?.url ?? "";
|
|
514
571
|
const groups = groupOperations(operations);
|
|
515
572
|
const tail = renderRequestTail(mode);
|
|
573
|
+
const tailRet = tail.ret.split("\n").map((l) => l.trim() ? ` ${l}` : l).join("\n");
|
|
516
574
|
return `// AUTO-GENERATED by ebely (generateClient) \u2014 \u041D\u0415 \u0440\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0432\u0440\u0443\u0447\u043D\u0443\u044E.
|
|
517
575
|
// \u0418\u0441\u0442\u043E\u0447\u043D\u0438\u043A: ${spec.info?.title ?? "OpenAPI spec"} v${spec.info?.version ?? "?"}
|
|
518
576
|
// \u0420\u0435\u0436\u0438\u043C \u043A\u043B\u0438\u0435\u043D\u0442\u0430: ${mode}
|
|
@@ -618,59 +676,84 @@ ${renderHookTreeBuilder(groups)}
|
|
|
618
676
|
const baseUrl = this.baseUrl()
|
|
619
677
|
const registry = this.hookRegistry
|
|
620
678
|
const { headers: baseHeaders, store } = cfg
|
|
679
|
+
// \u041F\u043E\u0442\u043E\u043B\u043E\u043A \u041F\u041E\u0412\u0422\u041E\u0420\u041E\u0412 (\u0441\u0432\u0435\u0440\u0445 \u043F\u0435\u0440\u0432\u043E\u0439 \u043F\u043E\u043F\u044B\u0442\u043A\u0438) \u0434\u043B\u044F globalRetry-\u0445\u0443\u043A\u043E\u0432 \u2014
|
|
680
|
+
// \u0437\u0430\u0449\u0438\u0442\u0430 \u043E\u0442 \u0431\u0435\u0441\u043A\u043E\u043D\u0435\u0447\u043D\u043E\u0433\u043E \u0446\u0438\u043A\u043B\u0430, \u0435\u0441\u043B\u0438 \u0445\u0443\u043A \u0443\u043F\u0440\u044F\u043C\u043E \u043F\u0440\u043E\u0441\u0438\u0442 \u043F\u043E\u0432\u0442\u043E\u0440.
|
|
681
|
+
const maxRetries = (ebely as { maxRetries?: number }).maxRetries ?? 3
|
|
621
682
|
|
|
622
683
|
return async (req): Promise<${tail.returnType}> => {
|
|
623
684
|
const { method, path, opKey, input } = req
|
|
624
685
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
686
|
+
// Retry-\u0446\u0438\u043A\u043B: \u043A\u0430\u0436\u0434\u0443\u044E \u043F\u043E\u043F\u044B\u0442\u043A\u0443 hookReq \u0441\u043E\u0431\u0438\u0440\u0430\u0435\u0442\u0441\u044F \u0417\u0410\u041D\u041E\u0412\u041E \u0438 \u0437\u0430\u043D\u043E\u0432\u043E
|
|
687
|
+
// \u043F\u0440\u043E\u0433\u043E\u043D\u044F\u044E\u0442\u0441\u044F before-\u0445\u0443\u043A\u0438, \u043F\u043E\u044D\u0442\u043E\u043C\u0443 \u043F\u0440\u0430\u0432\u043A\u0438 \u0438\u0437 retry-\u0445\u0443\u043A\u0430 (\u043D\u0430\u043F\u0440.
|
|
688
|
+
// \u0441\u0432\u0435\u0436\u0438\u0439 \u0442\u043E\u043A\u0435\u043D \u0432 ctx) \u043F\u043E\u0434\u0445\u0432\u0430\u0442\u044B\u0432\u0430\u044E\u0442\u0441\u044F \u043F\u043E\u0432\u0442\u043E\u0440\u043D\u044B\u043C before. \`for (;;)\`
|
|
689
|
+
// \u043A\u0440\u0443\u0442\u0438\u0442\u0441\u044F, \u043F\u043E\u043A\u0430 \u043D\u0435 \u0441\u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 return/throw \u0432 \u0445\u0432\u043E\u0441\u0442\u0435 \u0438\u043B\u0438 \`continue\`.
|
|
690
|
+
let attempt = 0
|
|
691
|
+
for (;;) {
|
|
692
|
+
const hookReq = {
|
|
693
|
+
method,
|
|
694
|
+
path,
|
|
695
|
+
pathParams: { ...(input?.path ?? {}) },
|
|
696
|
+
query: { ...(input?.query ?? {}) },
|
|
697
|
+
body: input?.body,
|
|
698
|
+
headers: { ...baseHeaders },
|
|
699
|
+
}
|
|
700
|
+
await registry.runBefore({ key: opKey, request: hookReq, ctx: store })
|
|
701
|
+
|
|
702
|
+
let resolvedPath = path
|
|
703
|
+
for (const [k, v] of Object.entries(hookReq.pathParams)) {
|
|
704
|
+
resolvedPath = resolvedPath.replace(
|
|
705
|
+
\`{\${k}}\`,
|
|
706
|
+
encodeURIComponent(String(v)),
|
|
707
|
+
)
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const url = new URL(baseUrl + resolvedPath)
|
|
711
|
+
for (const [k, v] of Object.entries(hookReq.query)) {
|
|
712
|
+
if (v !== undefined) url.searchParams.set(k, String(v))
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const hasBody = hookReq.body !== undefined
|
|
716
|
+
const response = await fetch(url, {
|
|
717
|
+
method,
|
|
718
|
+
headers: {
|
|
719
|
+
...(hasBody ? { 'content-type': 'application/json' } : {}),
|
|
720
|
+
...hookReq.headers,
|
|
721
|
+
},
|
|
722
|
+
body: hasBody ? JSON.stringify(hookReq.body) : undefined,
|
|
723
|
+
})
|
|
724
|
+
|
|
725
|
+
const text = await response.text()
|
|
726
|
+
let data: unknown
|
|
727
|
+
try {
|
|
728
|
+
data = text ? JSON.parse(text) : undefined
|
|
729
|
+
} catch {
|
|
730
|
+
data = text
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
await registry.runAfter({
|
|
734
|
+
key: opKey,
|
|
735
|
+
request: hookReq,
|
|
736
|
+
response: { status: response.status, body: data },
|
|
737
|
+
ctx: store,
|
|
738
|
+
})
|
|
739
|
+
|
|
740
|
+
// \u041F\u043E\u043A\u0430 \u0435\u0441\u0442\u044C \u0431\u044E\u0434\u0436\u0435\u0442 \u043F\u043E\u0432\u0442\u043E\u0440\u043E\u0432 \u2014 \u0441\u043F\u0440\u0430\u0448\u0438\u0432\u0430\u0435\u043C retry-\u0445\u0443\u043A\u0438. \u0412\u0435\u0440\u043D\u0443\u043B\u0438
|
|
741
|
+
// true \u2192 \u043D\u043E\u0432\u044B\u0439 \u0432\u0438\u0442\u043E\u043A (\u0437\u0430\u043D\u043E\u0432\u043E before \u2192 fetch); \u0438\u043D\u0430\u0447\u0435 \u043E\u0442\u0434\u0430\u0451\u043C \u043E\u0442\u0432\u0435\u0442.
|
|
742
|
+
if (attempt < maxRetries) {
|
|
743
|
+
const shouldRetry = await registry.runRetry({
|
|
744
|
+
key: opKey,
|
|
745
|
+
request: hookReq,
|
|
746
|
+
response: { status: response.status, body: data },
|
|
747
|
+
ctx: store,
|
|
748
|
+
})
|
|
749
|
+
if (shouldRetry) {
|
|
750
|
+
attempt++
|
|
751
|
+
continue
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
${tailRet}
|
|
664
756
|
}
|
|
665
|
-
|
|
666
|
-
await registry.runAfter({
|
|
667
|
-
key: opKey,
|
|
668
|
-
request: hookReq,
|
|
669
|
-
response: { status: response.status, body: data },
|
|
670
|
-
ctx: store,
|
|
671
|
-
})
|
|
672
|
-
|
|
673
|
-
${tail.ret}
|
|
674
757
|
}
|
|
675
758
|
}
|
|
676
759
|
|
package/dist/index.mjs
CHANGED
|
@@ -133,6 +133,15 @@ var ApiResponse = class {
|
|
|
133
133
|
var HookRegistry = class {
|
|
134
134
|
beforeMap = /* @__PURE__ */ new Map();
|
|
135
135
|
afterMap = /* @__PURE__ */ new Map();
|
|
136
|
+
// Глобальные хуки — срабатывают на КАЖДУЮ операцию (вне зависимости от
|
|
137
|
+
// ключа). Удобны для сквозных задач: подстановка `Authorization` из
|
|
138
|
+
// `ctx` во все запросы, логирование, обработка 401 и т.п.
|
|
139
|
+
globalBeforeHooks = [];
|
|
140
|
+
globalAfterHooks = [];
|
|
141
|
+
// Глобальные retry-хуки — решают, переигрывать ли запрос (напр. на 401
|
|
142
|
+
// сходить за refresh и вернуть `true`). Цикл повторов и его лимит живут
|
|
143
|
+
// в сгенерированном `request` (см. render.ts) — сам реестр сети не знает.
|
|
144
|
+
globalRetryHooks = [];
|
|
136
145
|
/** Зарегистрировать `before`-хук на ключ `"<группа>.<метод>"`. */
|
|
137
146
|
before(args) {
|
|
138
147
|
const list = this.beforeMap.get(args.key) ?? [];
|
|
@@ -145,18 +154,59 @@ var HookRegistry = class {
|
|
|
145
154
|
list.push(args.fn);
|
|
146
155
|
this.afterMap.set(args.key, list);
|
|
147
156
|
}
|
|
148
|
-
/**
|
|
157
|
+
/** Зарегистрировать ГЛОБАЛЬНЫЙ `before`-хук (на все операции). */
|
|
158
|
+
globalBefore(args) {
|
|
159
|
+
this.globalBeforeHooks.push(args.fn);
|
|
160
|
+
}
|
|
161
|
+
/** Зарегистрировать ГЛОБАЛЬНЫЙ `after`-хук (на все операции). */
|
|
162
|
+
globalAfter(args) {
|
|
163
|
+
this.globalAfterHooks.push(args.fn);
|
|
164
|
+
}
|
|
165
|
+
/** Зарегистрировать ГЛОБАЛЬНЫЙ `retry`-хук (решает про повтор запроса). */
|
|
166
|
+
globalRetry(args) {
|
|
167
|
+
this.globalRetryHooks.push(args.fn);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Прогнать `before`-хуки по очереди (await на async): сначала глобальные,
|
|
171
|
+
* затем поименные для ключа — так общая подготовка (напр. заголовок
|
|
172
|
+
* авторизации) ложится раньше, а точечный хук может её переопределить.
|
|
173
|
+
*/
|
|
149
174
|
async runBefore(args) {
|
|
150
|
-
|
|
175
|
+
const hooks = [...this.globalBeforeHooks, ...this.beforeMap.get(args.key) ?? []];
|
|
176
|
+
for (const fn of hooks) {
|
|
151
177
|
await fn({ request: args.request, ctx: args.ctx });
|
|
152
178
|
}
|
|
153
179
|
}
|
|
154
|
-
/**
|
|
180
|
+
/**
|
|
181
|
+
* Прогнать `after`-хуки по очереди (await на async): сначала поименные
|
|
182
|
+
* для ключа, затем глобальные — так глобальный «оборачивает» точечные
|
|
183
|
+
* (видит итоговый эффект, удобно для логирования/обработки статуса).
|
|
184
|
+
*/
|
|
155
185
|
async runAfter(args) {
|
|
156
|
-
|
|
186
|
+
const hooks = [...this.afterMap.get(args.key) ?? [], ...this.globalAfterHooks];
|
|
187
|
+
for (const fn of hooks) {
|
|
157
188
|
await fn({ request: args.request, response: args.response, ctx: args.ctx });
|
|
158
189
|
}
|
|
159
190
|
}
|
|
191
|
+
/**
|
|
192
|
+
* Спросить retry-хуки, нужно ли переиграть запрос. Прогон по очереди
|
|
193
|
+
* регистрации; ПЕРВЫЙ хук, вернувший истину, выигрывает (он уже сделал
|
|
194
|
+
* подготовку — напр. refresh — поэтому остальные не дёргаем). Если ни
|
|
195
|
+
* один не вернул истину — `false` (повтора нет). Цикл и лимит повторов
|
|
196
|
+
* — на стороне вызывающего (сгенерированный `request`).
|
|
197
|
+
*/
|
|
198
|
+
async runRetry(args) {
|
|
199
|
+
for (const fn of this.globalRetryHooks) {
|
|
200
|
+
const decision = await fn({
|
|
201
|
+
request: args.request,
|
|
202
|
+
response: args.response,
|
|
203
|
+
ctx: args.ctx
|
|
204
|
+
});
|
|
205
|
+
if (decision)
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
160
210
|
};
|
|
161
211
|
|
|
162
212
|
// src/generate-client.ts
|
|
@@ -380,7 +430,7 @@ function renderImports(args) {
|
|
|
380
430
|
const { mode, userStoreImport, configImport } = args;
|
|
381
431
|
const values = mode === "frontend" ? "BaseStore, HookRegistry" : "BaseStore, ApiResponse, HookRegistry";
|
|
382
432
|
return `import { ${values} } from ${JSON.stringify(userStoreImport)}
|
|
383
|
-
import type { BeforeHook, AfterHook } from ${JSON.stringify(userStoreImport)}
|
|
433
|
+
import type { BeforeHook, AfterHook, RetryHook } from ${JSON.stringify(userStoreImport)}
|
|
384
434
|
import { ebely } from ${JSON.stringify(configImport)}`;
|
|
385
435
|
}
|
|
386
436
|
function renderRequestTail(mode) {
|
|
@@ -439,7 +489,11 @@ function renderHookTreeType(groups) {
|
|
|
439
489
|
${leaves.join("\n")}
|
|
440
490
|
}`;
|
|
441
491
|
});
|
|
492
|
+
const global = ` globalBefore(fn: BeforeHook<Store>): void
|
|
493
|
+
globalAfter(fn: AfterHook<Store>): void
|
|
494
|
+
globalRetry(fn: RetryHook<Store>): void`;
|
|
442
495
|
return `type EbelyHookTree<Store extends BaseStore> = {
|
|
496
|
+
${global}
|
|
443
497
|
${blocks.join("\n")}
|
|
444
498
|
}`;
|
|
445
499
|
}
|
|
@@ -459,6 +513,9 @@ ${leaves.join("\n")}
|
|
|
459
513
|
return ` private buildHookTree(): EbelyHookTree<Store> {
|
|
460
514
|
const r = this.hookRegistry
|
|
461
515
|
return {
|
|
516
|
+
globalBefore: (fn: BeforeHook<Store>) => r.globalBefore({ fn }),
|
|
517
|
+
globalAfter: (fn: AfterHook<Store>) => r.globalAfter({ fn }),
|
|
518
|
+
globalRetry: (fn: RetryHook<Store>) => r.globalRetry({ fn }),
|
|
462
519
|
${blocks.join("\n")}
|
|
463
520
|
} as unknown as EbelyHookTree<Store>
|
|
464
521
|
}`;
|
|
@@ -483,6 +540,7 @@ function renderClient(args) {
|
|
|
483
540
|
const basePath = spec.servers?.[0]?.url ?? "";
|
|
484
541
|
const groups = groupOperations(operations);
|
|
485
542
|
const tail = renderRequestTail(mode);
|
|
543
|
+
const tailRet = tail.ret.split("\n").map((l) => l.trim() ? ` ${l}` : l).join("\n");
|
|
486
544
|
return `// AUTO-GENERATED by ebely (generateClient) \u2014 \u041D\u0415 \u0440\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0432\u0440\u0443\u0447\u043D\u0443\u044E.
|
|
487
545
|
// \u0418\u0441\u0442\u043E\u0447\u043D\u0438\u043A: ${spec.info?.title ?? "OpenAPI spec"} v${spec.info?.version ?? "?"}
|
|
488
546
|
// \u0420\u0435\u0436\u0438\u043C \u043A\u043B\u0438\u0435\u043D\u0442\u0430: ${mode}
|
|
@@ -588,59 +646,84 @@ ${renderHookTreeBuilder(groups)}
|
|
|
588
646
|
const baseUrl = this.baseUrl()
|
|
589
647
|
const registry = this.hookRegistry
|
|
590
648
|
const { headers: baseHeaders, store } = cfg
|
|
649
|
+
// \u041F\u043E\u0442\u043E\u043B\u043E\u043A \u041F\u041E\u0412\u0422\u041E\u0420\u041E\u0412 (\u0441\u0432\u0435\u0440\u0445 \u043F\u0435\u0440\u0432\u043E\u0439 \u043F\u043E\u043F\u044B\u0442\u043A\u0438) \u0434\u043B\u044F globalRetry-\u0445\u0443\u043A\u043E\u0432 \u2014
|
|
650
|
+
// \u0437\u0430\u0449\u0438\u0442\u0430 \u043E\u0442 \u0431\u0435\u0441\u043A\u043E\u043D\u0435\u0447\u043D\u043E\u0433\u043E \u0446\u0438\u043A\u043B\u0430, \u0435\u0441\u043B\u0438 \u0445\u0443\u043A \u0443\u043F\u0440\u044F\u043C\u043E \u043F\u0440\u043E\u0441\u0438\u0442 \u043F\u043E\u0432\u0442\u043E\u0440.
|
|
651
|
+
const maxRetries = (ebely as { maxRetries?: number }).maxRetries ?? 3
|
|
591
652
|
|
|
592
653
|
return async (req): Promise<${tail.returnType}> => {
|
|
593
654
|
const { method, path, opKey, input } = req
|
|
594
655
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
656
|
+
// Retry-\u0446\u0438\u043A\u043B: \u043A\u0430\u0436\u0434\u0443\u044E \u043F\u043E\u043F\u044B\u0442\u043A\u0443 hookReq \u0441\u043E\u0431\u0438\u0440\u0430\u0435\u0442\u0441\u044F \u0417\u0410\u041D\u041E\u0412\u041E \u0438 \u0437\u0430\u043D\u043E\u0432\u043E
|
|
657
|
+
// \u043F\u0440\u043E\u0433\u043E\u043D\u044F\u044E\u0442\u0441\u044F before-\u0445\u0443\u043A\u0438, \u043F\u043E\u044D\u0442\u043E\u043C\u0443 \u043F\u0440\u0430\u0432\u043A\u0438 \u0438\u0437 retry-\u0445\u0443\u043A\u0430 (\u043D\u0430\u043F\u0440.
|
|
658
|
+
// \u0441\u0432\u0435\u0436\u0438\u0439 \u0442\u043E\u043A\u0435\u043D \u0432 ctx) \u043F\u043E\u0434\u0445\u0432\u0430\u0442\u044B\u0432\u0430\u044E\u0442\u0441\u044F \u043F\u043E\u0432\u0442\u043E\u0440\u043D\u044B\u043C before. \`for (;;)\`
|
|
659
|
+
// \u043A\u0440\u0443\u0442\u0438\u0442\u0441\u044F, \u043F\u043E\u043A\u0430 \u043D\u0435 \u0441\u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 return/throw \u0432 \u0445\u0432\u043E\u0441\u0442\u0435 \u0438\u043B\u0438 \`continue\`.
|
|
660
|
+
let attempt = 0
|
|
661
|
+
for (;;) {
|
|
662
|
+
const hookReq = {
|
|
663
|
+
method,
|
|
664
|
+
path,
|
|
665
|
+
pathParams: { ...(input?.path ?? {}) },
|
|
666
|
+
query: { ...(input?.query ?? {}) },
|
|
667
|
+
body: input?.body,
|
|
668
|
+
headers: { ...baseHeaders },
|
|
669
|
+
}
|
|
670
|
+
await registry.runBefore({ key: opKey, request: hookReq, ctx: store })
|
|
671
|
+
|
|
672
|
+
let resolvedPath = path
|
|
673
|
+
for (const [k, v] of Object.entries(hookReq.pathParams)) {
|
|
674
|
+
resolvedPath = resolvedPath.replace(
|
|
675
|
+
\`{\${k}}\`,
|
|
676
|
+
encodeURIComponent(String(v)),
|
|
677
|
+
)
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
const url = new URL(baseUrl + resolvedPath)
|
|
681
|
+
for (const [k, v] of Object.entries(hookReq.query)) {
|
|
682
|
+
if (v !== undefined) url.searchParams.set(k, String(v))
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const hasBody = hookReq.body !== undefined
|
|
686
|
+
const response = await fetch(url, {
|
|
687
|
+
method,
|
|
688
|
+
headers: {
|
|
689
|
+
...(hasBody ? { 'content-type': 'application/json' } : {}),
|
|
690
|
+
...hookReq.headers,
|
|
691
|
+
},
|
|
692
|
+
body: hasBody ? JSON.stringify(hookReq.body) : undefined,
|
|
693
|
+
})
|
|
694
|
+
|
|
695
|
+
const text = await response.text()
|
|
696
|
+
let data: unknown
|
|
697
|
+
try {
|
|
698
|
+
data = text ? JSON.parse(text) : undefined
|
|
699
|
+
} catch {
|
|
700
|
+
data = text
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
await registry.runAfter({
|
|
704
|
+
key: opKey,
|
|
705
|
+
request: hookReq,
|
|
706
|
+
response: { status: response.status, body: data },
|
|
707
|
+
ctx: store,
|
|
708
|
+
})
|
|
709
|
+
|
|
710
|
+
// \u041F\u043E\u043A\u0430 \u0435\u0441\u0442\u044C \u0431\u044E\u0434\u0436\u0435\u0442 \u043F\u043E\u0432\u0442\u043E\u0440\u043E\u0432 \u2014 \u0441\u043F\u0440\u0430\u0448\u0438\u0432\u0430\u0435\u043C retry-\u0445\u0443\u043A\u0438. \u0412\u0435\u0440\u043D\u0443\u043B\u0438
|
|
711
|
+
// true \u2192 \u043D\u043E\u0432\u044B\u0439 \u0432\u0438\u0442\u043E\u043A (\u0437\u0430\u043D\u043E\u0432\u043E before \u2192 fetch); \u0438\u043D\u0430\u0447\u0435 \u043E\u0442\u0434\u0430\u0451\u043C \u043E\u0442\u0432\u0435\u0442.
|
|
712
|
+
if (attempt < maxRetries) {
|
|
713
|
+
const shouldRetry = await registry.runRetry({
|
|
714
|
+
key: opKey,
|
|
715
|
+
request: hookReq,
|
|
716
|
+
response: { status: response.status, body: data },
|
|
717
|
+
ctx: store,
|
|
718
|
+
})
|
|
719
|
+
if (shouldRetry) {
|
|
720
|
+
attempt++
|
|
721
|
+
continue
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
${tailRet}
|
|
634
726
|
}
|
|
635
|
-
|
|
636
|
-
await registry.runAfter({
|
|
637
|
-
key: opKey,
|
|
638
|
-
request: hookReq,
|
|
639
|
-
response: { status: response.status, body: data },
|
|
640
|
-
ctx: store,
|
|
641
|
-
})
|
|
642
|
-
|
|
643
|
-
${tail.ret}
|
|
644
727
|
}
|
|
645
728
|
}
|
|
646
729
|
|