@ryuzaki13/react-foundation-api 1.1.15 → 1.1.17
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 +32 -43
- package/dist/chunks/{odataFetchFn-vnAXC-c0.js → odataFetchFn-B9wSQpUS.js} +11 -11
- package/dist/chunks/{odataFetchFn-vnAXC-c0.js.map → odataFetchFn-B9wSQpUS.js.map} +1 -1
- package/dist/odata/fetchCollectionData.d.ts +1 -1
- package/dist/odata/fetchCollectionData.d.ts.map +1 -1
- package/dist/odata/index.js +111 -112
- package/dist/odata/index.js.map +1 -1
- package/dist/odata/projectODataCollectionSort.d.ts +1 -1
- package/dist/odata/projectODataCollectionSort.d.ts.map +1 -1
- package/dist/odata/types.d.ts +1 -2
- package/dist/odata/types.d.ts.map +1 -1
- package/dist/odata/useODataCollection.d.ts +1 -1
- package/dist/odata/useODataCollection.d.ts.map +1 -1
- package/dist/odata/useODataCollectionQuery.d.ts +1 -1
- package/dist/odata/useODataCollectionQuery.d.ts.map +1 -1
- package/dist/odata/useODataEntity.d.ts +1 -1
- package/dist/odata/useODataEntity.d.ts.map +1 -1
- package/dist/persisted/index.js +1 -1
- package/package.json +8 -3
- package/src/adt/README.mdx +164 -0
- package/src/async/README.mdx +253 -0
- package/src/error-report/README.mdx +148 -0
- package/src/foundationApi.mdx +123 -0
- package/src/http/README.mdx +221 -0
- package/src/odata/README.mdx +790 -0
- package/src/persisted/README.mdx +454 -0
- package/src/resource/README.mdx +358 -0
- package/src/server-fn/README.mdx +194 -0
- package/src/transport/README.mdx +183 -0
- package/src/README.md +0 -937
- package/src/async/README.md +0 -623
- package/src/async/async.mdx +0 -6
- package/src/odata/README.md +0 -761
- package/src/odata/odataFetchFn.mdx +0 -6
- package/src/persisted/README.md +0 -598
- package/src/persisted/persisted.mdx +0 -6
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { Meta } from "@storybook/addon-docs/blocks";
|
|
2
|
+
|
|
3
|
+
<Meta title="Foundation API/SAP/ADT Transports" />
|
|
4
|
+
|
|
5
|
+
# SAP ADT transports через `@ryuzaki13/react-foundation-api/adt`
|
|
6
|
+
|
|
7
|
+
Модуль получает transport requests текущего пользователя из SAP ADT endpoint и преобразует XML в простой массив объектов.
|
|
8
|
+
|
|
9
|
+
## Содержание
|
|
10
|
+
|
|
11
|
+
- [Когда использовать](#когда-использовать)
|
|
12
|
+
- [Установка и импорт](#установка-и-импорт)
|
|
13
|
+
- [Быстрый старт](#быстрый-старт)
|
|
14
|
+
- [`fetchUserTransports`](#fetchusertransports)
|
|
15
|
+
- [`parseUserTransportsXml`](#parseusertransportsxml)
|
|
16
|
+
- [Формат результата](#формат-результата)
|
|
17
|
+
- [Ошибки и ограничения](#ошибки-и-ограничения)
|
|
18
|
+
- [Полный API](#полный-api)
|
|
19
|
+
|
|
20
|
+
## Когда использовать
|
|
21
|
+
|
|
22
|
+
Используйте `adt`, если backend доступен по `/sap/bc/adt/cts/transports` и возвращает ADT XML. Если нужны отдельные UI2 списки workbench/customizing в JSON, используйте [`@ryuzaki13/react-foundation-api/transport`](../transport/README.mdx).
|
|
23
|
+
|
|
24
|
+
```text
|
|
25
|
+
SAP ADT XML
|
|
26
|
+
│
|
|
27
|
+
▼
|
|
28
|
+
parseUserTransportsXml
|
|
29
|
+
│
|
|
30
|
+
▼
|
|
31
|
+
UserTransport[]
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Установка и импорт
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm install @ryuzaki13/react-foundation-api @ryuzaki13/react-foundation-lib fast-xml-parser
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { fetchUserTransports, parseUserTransportsXml } from "@ryuzaki13/react-foundation-api/adt";
|
|
42
|
+
|
|
43
|
+
import type { UserTransport } from "@ryuzaki13/react-foundation-api/adt";
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Быстрый старт
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
async function loadTransports() {
|
|
50
|
+
try {
|
|
51
|
+
const transports = await fetchUserTransports();
|
|
52
|
+
return transports;
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error("Не удалось получить transports", error);
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`fetchUserTransports` возвращает Promise. Пока он выполняется, данных ещё нет; ошибку нужно обработать через `try/catch`, TanStack Query или error boundary владельца сценария.
|
|
61
|
+
|
|
62
|
+
## `fetchUserTransports`
|
|
63
|
+
|
|
64
|
+
Функция выполняет GET:
|
|
65
|
+
|
|
66
|
+
```text
|
|
67
|
+
/sap/bc/adt/cts/transports?_action=FIND&trfunction=K
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Запрос отправляется через SAP/OData transport пакета с:
|
|
71
|
+
|
|
72
|
+
- `Accept: application/xml, text/xml, */*`;
|
|
73
|
+
- cookies через `credentials: include`;
|
|
74
|
+
- SAP language/client и SSO policy общего OData transport;
|
|
75
|
+
- пустым base URL, потому что ADT endpoint уже содержит `/sap/...`.
|
|
76
|
+
|
|
77
|
+
Ответ читается как text и передаётся `parseUserTransportsXml`.
|
|
78
|
+
|
|
79
|
+
Если сервер вместо XML вернул HTML login/error page, функция выбросит ошибку. Это защищает UI от ситуации, когда HTML ошибочно принимается за пустой список transports.
|
|
80
|
+
|
|
81
|
+
## `parseUserTransportsXml`
|
|
82
|
+
|
|
83
|
+
Parser можно использовать отдельно, например в тесте или при получении XML другим transport:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
const xml = `
|
|
87
|
+
<root>
|
|
88
|
+
<CTS_REQ_HEADER>
|
|
89
|
+
<TRKORR>DEVK900001</TRKORR>
|
|
90
|
+
<AS4TEXT>Исправление отчёта</AS4TEXT>
|
|
91
|
+
<AS4USER>IVANOV</AS4USER>
|
|
92
|
+
<TARSYSTEM>QAS</TARSYSTEM>
|
|
93
|
+
<TRFUNCTION>K</TRFUNCTION>
|
|
94
|
+
<TRSTATUS>D</TRSTATUS>
|
|
95
|
+
</CTS_REQ_HEADER>
|
|
96
|
+
</root>`;
|
|
97
|
+
|
|
98
|
+
const transports = parseUserTransportsXml(xml);
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Алгоритм:
|
|
102
|
+
|
|
103
|
+
1. Пустая или состоящая из пробелов строка превращается в `[]`.
|
|
104
|
+
2. XML разбирается `fast-xml-parser`.
|
|
105
|
+
3. Во вложенной структуре рекурсивно ищутся узлы `CTS_REQ_HEADER`.
|
|
106
|
+
4. Поля преобразуются в `UserTransport`.
|
|
107
|
+
5. Записи без непустого `TRKORR` отбрасываются.
|
|
108
|
+
6. Дубликаты `transportNo` удаляются; сохраняется первая запись.
|
|
109
|
+
7. Если header-узлы не найдены, parser ищет вложенные `TRKORR` и создаёт минимальные записи.
|
|
110
|
+
|
|
111
|
+
Parser делает `trim` строковых полей, но не декодирует бизнес-значения статуса/function и не проверяет их по справочнику SAP.
|
|
112
|
+
|
|
113
|
+
Некорректный XML может привести к исключению parser-а. Функция не заменяет его пустым массивом: повреждённый ответ должен быть виден как ошибка.
|
|
114
|
+
|
|
115
|
+
## Формат результата
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
interface UserTransport {
|
|
119
|
+
transportNo: string;
|
|
120
|
+
description?: string;
|
|
121
|
+
owner?: string;
|
|
122
|
+
targetSystem?: string;
|
|
123
|
+
function?: string;
|
|
124
|
+
status?: string;
|
|
125
|
+
parentTransportNo?: string;
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Соответствие XML:
|
|
130
|
+
|
|
131
|
+
| XML | Поле | Значение |
|
|
132
|
+
| --- | --- | --- |
|
|
133
|
+
| `TRKORR` | `transportNo` | Обязательный номер transport |
|
|
134
|
+
| `AS4TEXT` | `description` | Описание |
|
|
135
|
+
| `AS4USER` | `owner` | Владелец |
|
|
136
|
+
| `TARSYSTEM` | `targetSystem` | Целевая система |
|
|
137
|
+
| `TRFUNCTION` | `function` | SAP transport function |
|
|
138
|
+
| `TRSTATUS` | `status` | SAP status |
|
|
139
|
+
| `STRKORR` | `parentTransportNo` | Родительский request/task |
|
|
140
|
+
|
|
141
|
+
Optional-поле может быть `undefined`. Проверяйте его перед отображением:
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
const label = transport.description
|
|
145
|
+
? `${transport.transportNo} — ${transport.description}`
|
|
146
|
+
: transport.transportNo;
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Ошибки и ограничения
|
|
150
|
+
|
|
151
|
+
- `fast-xml-parser` должен быть установлен в host, использующем этот entrypoint.
|
|
152
|
+
- Это не generic ADT client: endpoint и query parameters зафиксированы.
|
|
153
|
+
- Функция не создаёт/не освобождает transport и не проверяет authorization.
|
|
154
|
+
- SSO/cookies требуют browser/proxy окружения SAP. На server runtime поведение зависит от переданного глобального `fetch` и cookie boundary.
|
|
155
|
+
- HTML-response, HTTP error и XML parse error не превращаются в `[]`.
|
|
156
|
+
- Дедупликация выполняется только по `transportNo` и сохраняет первую найденную запись.
|
|
157
|
+
|
|
158
|
+
## Полный API
|
|
159
|
+
|
|
160
|
+
| Export | Назначение |
|
|
161
|
+
| --- | --- |
|
|
162
|
+
| `fetchUserTransports` | Загружает и разбирает ADT XML |
|
|
163
|
+
| `parseUserTransportsXml` | Преобразует XML string в массив |
|
|
164
|
+
| `UserTransport` | Контракт результата |
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { Meta } from "@storybook/addon-docs/blocks";
|
|
2
|
+
|
|
3
|
+
<Meta title="Foundation API/Orchestration/Async Tasks" />
|
|
4
|
+
|
|
5
|
+
# Асинхронные задачи через `@ryuzaki13/react-foundation-api/async`
|
|
6
|
+
|
|
7
|
+
Модуль запускает независимые Promise-задачи, сохраняет связь результата с исходным элементом и формирует удобную сводку для UI/логов.
|
|
8
|
+
|
|
9
|
+
## Какой runner выбрать
|
|
10
|
+
|
|
11
|
+
| Сценарий | Функция |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
| Задач немного, можно начать все одновременно, любая ошибка должна завершить batch исключением | `runAsyncTasks` |
|
|
14
|
+
| Нужно ограничить число одновременных запросов, показать progress или поддержать abort | `runConcurrentTasks` |
|
|
15
|
+
|
|
16
|
+
Обе функции выполняют задачи параллельно. Они не создают web workers и не ускоряют CPU-bound JavaScript: Promise-concurrency полезна прежде всего для I/O — HTTP, чтения storage и других async операций.
|
|
17
|
+
|
|
18
|
+
## Импорт
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import {
|
|
22
|
+
AsyncTasksError,
|
|
23
|
+
ConcurrencyPartialError,
|
|
24
|
+
runAsyncTasks,
|
|
25
|
+
runConcurrentTasks
|
|
26
|
+
} from "@ryuzaki13/react-foundation-api/async";
|
|
27
|
+
|
|
28
|
+
import type {
|
|
29
|
+
AsyncTask,
|
|
30
|
+
RunAsyncTasksResult,
|
|
31
|
+
RunConcurrentTasksOptions,
|
|
32
|
+
RunConcurrentTasksResult
|
|
33
|
+
} from "@ryuzaki13/react-foundation-api/async";
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Модуль не требует React или TanStack Query.
|
|
37
|
+
|
|
38
|
+
## База: `async`, `Promise` и `await`
|
|
39
|
+
|
|
40
|
+
`async`-функция всегда возвращает `Promise`. `await` приостанавливает только текущую async-функцию, а не весь browser thread:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
async function loadName() {
|
|
44
|
+
const response = await fetch("/api/me");
|
|
45
|
+
return response.text();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const namePromise = loadName(); // Promise<string>.
|
|
49
|
+
const name = await namePromise; // string.
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Ошибку Promise обрабатывают через `try/catch` вокруг `await`.
|
|
53
|
+
|
|
54
|
+
## `runAsyncTasks`
|
|
55
|
+
|
|
56
|
+
### Быстрый старт
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
const result = await runAsyncTasks([
|
|
60
|
+
{ name: "profile", run: () => fetchProfile() },
|
|
61
|
+
{ name: "settings", run: () => fetchSettings() }
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
console.log(result.summary);
|
|
65
|
+
// { total: 2, succeeded: 2, failed: 0 }
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Все `run` запускаются сразу через `Promise.all`. Важное отличие от обычного `Promise.all`: ошибка одной задачи перехватывается внутри runner-а, поэтому остальные задачи успевают завершиться. После завершения всего batch функция либо возвращает полный результат, либо бросает `AsyncTasksError` с тем же полным результатом.
|
|
69
|
+
|
|
70
|
+
### Обработка частичного успеха
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
try {
|
|
74
|
+
const result = await runAsyncTasks(tasks, {
|
|
75
|
+
errorMessage: "Не все стартовые данные загрузились"
|
|
76
|
+
});
|
|
77
|
+
useData(result.successes);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
if (error instanceof AsyncTasksError) {
|
|
80
|
+
usePartialData(error.result.successes);
|
|
81
|
+
showErrors(error.result.errors);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`AsyncTasksError.result` содержит:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
type RunAsyncTasksResult<TResult> = {
|
|
93
|
+
settled: readonly AsyncTaskSettled<TResult>[];
|
|
94
|
+
successes: readonly AsyncTaskSuccess<TResult>[];
|
|
95
|
+
errors: readonly AsyncTaskFailure[];
|
|
96
|
+
summary: {
|
|
97
|
+
total: number;
|
|
98
|
+
succeeded: number;
|
|
99
|
+
failed: number;
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Элементы `settled`, `successes` и `errors` сохраняют порядок исходного массива. Каждый элемент содержит `index` и `name`.
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
if (item.status === "fulfilled") {
|
|
108
|
+
console.log(item.data);
|
|
109
|
+
} else {
|
|
110
|
+
console.error(item.error);
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
TypeScript сужает union по `status`.
|
|
115
|
+
|
|
116
|
+
Пустой список возвращает успешный пустой результат. `runAsyncTasks` не имеет встроенного concurrency limit и AbortSignal.
|
|
117
|
+
|
|
118
|
+
## `runConcurrentTasks`
|
|
119
|
+
|
|
120
|
+
### Ограничение одновременных запросов
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
const result = await runConcurrentTasks(
|
|
124
|
+
userIds,
|
|
125
|
+
async (userId, { signal }) => {
|
|
126
|
+
const response = await fetch(`/api/users/${userId}`, { signal });
|
|
127
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
128
|
+
return response.json();
|
|
129
|
+
},
|
|
130
|
+
{ concurrency: 4 }
|
|
131
|
+
);
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Не более четырёх задач будут находиться в работе одновременно. Значение по умолчанию — `4`. Если элементов меньше, workers создаются только по числу элементов.
|
|
135
|
+
|
|
136
|
+
`concurrency` обязан быть целым числом не меньше `1`:
|
|
137
|
+
|
|
138
|
+
| Значение | Результат |
|
|
139
|
+
| --- | --- |
|
|
140
|
+
| `4` | Работает |
|
|
141
|
+
| `1` | Последовательное выполнение |
|
|
142
|
+
| `0`, `-1` | `RangeError` |
|
|
143
|
+
| `1.5`, `NaN` | `TypeError` |
|
|
144
|
+
|
|
145
|
+
### Ошибки отдельных элементов
|
|
146
|
+
|
|
147
|
+
Runner не бросает ошибки отдельных задач. Он помещает их в `result.errors` и продолжает обработку:
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
const result = await runConcurrentTasks(inputs, task);
|
|
151
|
+
|
|
152
|
+
if (result.errors.length > 0) {
|
|
153
|
+
for (const failure of result.errors) {
|
|
154
|
+
console.error(failure.index, failure.input, failure.error);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Исключение самой функции возможно при невалидном `concurrency` или если callback `onProgress`/`onItemSuccess`/`onItemError` сам бросил ошибку. Callbacks должны быть безопасными и быстрыми.
|
|
160
|
+
|
|
161
|
+
### Progress
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
await runConcurrentTasks(files, uploadFile, {
|
|
165
|
+
concurrency: 3,
|
|
166
|
+
onProgress(progress) {
|
|
167
|
+
setProgress(progress.percentage);
|
|
168
|
+
},
|
|
169
|
+
onItemSuccess(item) {
|
|
170
|
+
console.log("Загружен", item.input.name);
|
|
171
|
+
},
|
|
172
|
+
onItemError(item) {
|
|
173
|
+
console.error("Ошибка", item.input.name, item.error);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
`onProgress` вызывается после каждой завершённой задачи. В первом вызове процент уже больше нуля; отдельного начального события `0%` нет.
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
type ConcurrentTaskProgress<TInput, TResult> = {
|
|
182
|
+
total: number;
|
|
183
|
+
completed: number;
|
|
184
|
+
succeeded: number;
|
|
185
|
+
failed: number;
|
|
186
|
+
running: number;
|
|
187
|
+
pending: number;
|
|
188
|
+
percentage: number;
|
|
189
|
+
aborted: boolean;
|
|
190
|
+
lastSettled?: ConcurrentTaskSettled<TInput, TResult>;
|
|
191
|
+
};
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
`percentage` округляется через `Math.round`.
|
|
195
|
+
|
|
196
|
+
### Abort
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
const controller = new AbortController();
|
|
200
|
+
|
|
201
|
+
const promise = runConcurrentTasks(urls, async (url, { signal }) => {
|
|
202
|
+
const response = await fetch(url, { signal });
|
|
203
|
+
return response.text();
|
|
204
|
+
}, {
|
|
205
|
+
concurrency: 2,
|
|
206
|
+
signal: controller.signal
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
controller.abort();
|
|
210
|
+
const result = await promise;
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
После abort новые задачи не стартуют. Уже начатая операция прервётся только если сама использует переданный `signal`. Runner ждёт завершения начатых tasks и возвращает:
|
|
214
|
+
|
|
215
|
+
- `aborted: true`;
|
|
216
|
+
- обработанные элементы в `settled`;
|
|
217
|
+
- не начатые элементы в `unprocessed`.
|
|
218
|
+
|
|
219
|
+
Abort не является ошибкой runner-а и сам по себе не приводит к `throw`.
|
|
220
|
+
|
|
221
|
+
### Порядок результата
|
|
222
|
+
|
|
223
|
+
Tasks могут завершаться в любом порядке, callbacks идут в порядке фактического завершения. Итоговые `settled`, `successes`, `errors` и `unprocessed` собираются по исходным индексам, поэтому результат детерминирован относительно input.
|
|
224
|
+
|
|
225
|
+
## `ConcurrencyPartialError`
|
|
226
|
+
|
|
227
|
+
`runConcurrentTasks` сам не создаёт этот error. Класс нужен, если слой приложения хочет преобразовать частичный результат в исключение:
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
const result = await runConcurrentTasks(inputs, task);
|
|
231
|
+
|
|
232
|
+
if (result.errors.length > 0 || result.aborted) {
|
|
233
|
+
throw new ConcurrencyPartialError("Batch выполнен частично", result);
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Не путайте его с `AsyncTasksError`: первый создаёт consumer по своей policy, второй автоматически бросает `runAsyncTasks`.
|
|
238
|
+
|
|
239
|
+
## Практические рекомендации
|
|
240
|
+
|
|
241
|
+
- Не ставьте слишком большой concurrency: browser, proxy и backend имеют свои лимиты.
|
|
242
|
+
- Для write-операций продумайте идемпотентность: retry может повторить уже выполненное действие.
|
|
243
|
+
- Не меняйте input objects внутри task, если те же значения использует UI.
|
|
244
|
+
- Обрабатывайте `unknown` error через безопасный normalizer.
|
|
245
|
+
- Не используйте runner вместо TanStack Query cache для одного и того же resource.
|
|
246
|
+
- Если задача зависит от результата предыдущей, это не независимый batch: выполните зависимые шаги последовательно.
|
|
247
|
+
|
|
248
|
+
## Полный API
|
|
249
|
+
|
|
250
|
+
| Группа | Exports |
|
|
251
|
+
| --- | --- |
|
|
252
|
+
| All-at-once | `runAsyncTasks`, `AsyncTasksError`, `AsyncTask`, `AsyncTaskSuccess`, `AsyncTaskFailure`, `AsyncTaskSettled`, `RunAsyncTasksResult`, `RunAsyncTasksOptions` |
|
|
253
|
+
| Bounded concurrency | `runConcurrentTasks`, `ConcurrencyPartialError`, `ConcurrentTaskContext`, `ConcurrentTaskFn`, `ConcurrentTaskSuccess`, `ConcurrentTaskError`, `ConcurrentTaskSettled`, `ConcurrentTaskUnprocessed`, `ConcurrentTaskProgress`, `RunConcurrentTasksOptions`, `RunConcurrentTasksResult` |
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { Meta } from "@storybook/addon-docs/blocks";
|
|
2
|
+
|
|
3
|
+
<Meta title="Foundation API/Errors/Error Report Delivery" />
|
|
4
|
+
|
|
5
|
+
# Доставка error reports через `@ryuzaki13/react-foundation-api/error-report`
|
|
6
|
+
|
|
7
|
+
Модуль превращает сохранённый черновик отчёта из `foundation-lib` в delivery body, вызывает adapter приложения и обновляет статус черновика.
|
|
8
|
+
|
|
9
|
+
## Граница ответственности
|
|
10
|
+
|
|
11
|
+
```text
|
|
12
|
+
foundation-lib/error-report foundation-api/error-report host backend adapter
|
|
13
|
+
collect + sanitize + draft store ──► lifecycle + delivery body ─────► HTTP/OData/serverFn
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Модуль не собирает browser diagnostics, не выбирает endpoint и не отправляет запрос самостоятельно. Transport явно передаёт приложение через `adapter`.
|
|
17
|
+
|
|
18
|
+
## Установка и импорт
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import {
|
|
22
|
+
createErrorReportDeliveryBody,
|
|
23
|
+
sendErrorReport
|
|
24
|
+
} from "@ryuzaki13/react-foundation-api/error-report";
|
|
25
|
+
|
|
26
|
+
import type {
|
|
27
|
+
ErrorReportDeliveryAdapter,
|
|
28
|
+
ErrorReportDeliveryBody,
|
|
29
|
+
ErrorReportDeliveryContext,
|
|
30
|
+
SendErrorReportOptions
|
|
31
|
+
} from "@ryuzaki13/react-foundation-api/error-report";
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Нужны `@ryuzaki13/react-foundation-lib` и `@tanstack/react-query`.
|
|
35
|
+
|
|
36
|
+
## Полный сценарий
|
|
37
|
+
|
|
38
|
+
Сначала другой слой создаёт и сохраняет draft средствами `@ryuzaki13/react-foundation-lib/error-report`. Затем API-слой получает только его `reportId`:
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { httpFetchPayload } from "@ryuzaki13/react-foundation-api/http";
|
|
42
|
+
import { sendErrorReport } from "@ryuzaki13/react-foundation-api/error-report";
|
|
43
|
+
import type { ErrorReportDeliveryAdapter } from "@ryuzaki13/react-foundation-api/error-report";
|
|
44
|
+
|
|
45
|
+
const deliveryAdapter: ErrorReportDeliveryAdapter = async (body) => {
|
|
46
|
+
await httpFetchPayload("/api/error-reports", {
|
|
47
|
+
init: {
|
|
48
|
+
method: "POST",
|
|
49
|
+
headers: { "Content-Type": "application/json" },
|
|
50
|
+
body: JSON.stringify(body)
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
await sendErrorReport(reportId, {
|
|
56
|
+
adapter: deliveryAdapter,
|
|
57
|
+
queryClient
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## `createErrorReportDeliveryBody`
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
const body = createErrorReportDeliveryBody(draft);
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Результат:
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
type ErrorReportDeliveryBody = {
|
|
71
|
+
reportId: string;
|
|
72
|
+
sessionId: string;
|
|
73
|
+
createdUtc: string;
|
|
74
|
+
category: ErrorReportCategory;
|
|
75
|
+
errorClass: string;
|
|
76
|
+
errorMessage: string;
|
|
77
|
+
stackTrace?: string;
|
|
78
|
+
payload: string;
|
|
79
|
+
};
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`payload` — JSON string всего payload черновика, а не вложенный object. Это важно для backend contract.
|
|
83
|
+
|
|
84
|
+
Если `draft.payload.error.message` пуст, обязательные `errorMessage` и message внутри сериализованного payload получают значение `"Неизвестная ошибка"`. Остальной draft не нормализуется.
|
|
85
|
+
|
|
86
|
+
`JSON.stringify` может выбросить ошибку при циклических или неподдерживаемых данных. Draft должен соответствовать сериализуемому контракту `foundation-lib`.
|
|
87
|
+
|
|
88
|
+
## `sendErrorReport`
|
|
89
|
+
|
|
90
|
+
Lifecycle:
|
|
91
|
+
|
|
92
|
+
```text
|
|
93
|
+
draft отсутствует ─────────► undefined
|
|
94
|
+
status sent/sending ───────► текущий draft, adapter не вызывается
|
|
95
|
+
остальной draft ───────────► sending
|
|
96
|
+
│
|
|
97
|
+
adapter success / error
|
|
98
|
+
│ │
|
|
99
|
+
▼ ▼
|
|
100
|
+
sent failed + failedReason
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Пошагово функция:
|
|
104
|
+
|
|
105
|
+
1. Ищет draft по `reportId` в store из `foundation-lib`.
|
|
106
|
+
2. Возвращает `undefined`, если draft не найден.
|
|
107
|
+
3. Не отправляет повторно draft со статусом `sent` или `sending`.
|
|
108
|
+
4. Устанавливает `status: "sending"` и очищает `failedReason`.
|
|
109
|
+
5. Создаёт delivery body.
|
|
110
|
+
6. Вызывает `adapter(body, { draft, queryClient })`.
|
|
111
|
+
7. При успехе устанавливает `status: "sent"` и `sentUtc`.
|
|
112
|
+
8. При ошибке устанавливает `status: "failed"`, сохраняет нормализованное сообщение и повторно бросает исходную ошибку.
|
|
113
|
+
|
|
114
|
+
Adapter получает исходный snapshot `draft`, прочитанный до перевода store в `sending`. Используйте `body` как delivery payload, а context — для query invalidation или transport, которому нужен `QueryClient`.
|
|
115
|
+
|
|
116
|
+
## Adapter с cache invalidation
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
const adapter: ErrorReportDeliveryAdapter = async (body, { queryClient }) => {
|
|
120
|
+
await postReport(body);
|
|
121
|
+
await queryClient.invalidateQueries({ queryKey: ["error-reports"] });
|
|
122
|
+
};
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Если invalidation бросит ошибку, вся доставка считается неуспешной и draft станет `failed`, даже если backend уже принял отчёт. Для non-critical cache update обработайте ошибку внутри adapter-а.
|
|
126
|
+
|
|
127
|
+
## Повторная отправка и concurrency
|
|
128
|
+
|
|
129
|
+
После `failed` функцию можно вызвать повторно. Проверка `sending` защищает от обычного повторного нажатия, но это не распределённая блокировка: два практически одновременных вызова могут успеть прочитать старый статус до его обновления. Backend желательно сделать идемпотентным по `reportId`.
|
|
130
|
+
|
|
131
|
+
## Ошибки и безопасность
|
|
132
|
+
|
|
133
|
+
- Ошибка adapter-а не поглощается: вызывающий код обязан её обработать.
|
|
134
|
+
- Не добавляйте secrets в draft. Delivery layer не выполняет дополнительную redaction всего payload.
|
|
135
|
+
- `queryClient` передаётся adapter-у, но модуль сам не инвалидирует queries.
|
|
136
|
+
- Функция управляет локальным draft store; не вызывайте её в среде без подходящего storage/runtime, если draft store там недоступен.
|
|
137
|
+
- Статус `sent` означает успешное завершение adapter-а, а не обязательное подтверждение бизнес-обработки backend-ом.
|
|
138
|
+
|
|
139
|
+
## Полный API
|
|
140
|
+
|
|
141
|
+
| Export | Назначение |
|
|
142
|
+
| --- | --- |
|
|
143
|
+
| `createErrorReportDeliveryBody` | Строит сериализуемый backend body |
|
|
144
|
+
| `sendErrorReport` | Управляет status и вызывает adapter |
|
|
145
|
+
| `ErrorReportDeliveryBody` | Поля delivery payload |
|
|
146
|
+
| `ErrorReportDeliveryContext` | `draft` и `queryClient` для adapter-а |
|
|
147
|
+
| `ErrorReportDeliveryAdapter` | Контракт функции доставки |
|
|
148
|
+
| `SendErrorReportOptions` | `adapter` и `queryClient` |
|