@ryuzaki13/react-foundation-api 1.1.16 → 1.1.18

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.
Files changed (36) hide show
  1. package/README.md +32 -43
  2. package/dist/chunks/{odataFetchFn-vnAXC-c0.js → odataFetchFn-B9wSQpUS.js} +11 -11
  3. package/dist/chunks/{odataFetchFn-vnAXC-c0.js.map → odataFetchFn-B9wSQpUS.js.map} +1 -1
  4. package/dist/odata/fetchCollectionData.d.ts +1 -1
  5. package/dist/odata/fetchCollectionData.d.ts.map +1 -1
  6. package/dist/odata/index.js +111 -112
  7. package/dist/odata/index.js.map +1 -1
  8. package/dist/odata/projectODataCollectionSort.d.ts +1 -1
  9. package/dist/odata/projectODataCollectionSort.d.ts.map +1 -1
  10. package/dist/odata/types.d.ts +1 -2
  11. package/dist/odata/types.d.ts.map +1 -1
  12. package/dist/odata/useODataCollection.d.ts +1 -1
  13. package/dist/odata/useODataCollection.d.ts.map +1 -1
  14. package/dist/odata/useODataCollectionQuery.d.ts +1 -1
  15. package/dist/odata/useODataCollectionQuery.d.ts.map +1 -1
  16. package/dist/odata/useODataEntity.d.ts +1 -1
  17. package/dist/odata/useODataEntity.d.ts.map +1 -1
  18. package/dist/persisted/index.js +1 -1
  19. package/package.json +2 -2
  20. package/src/adt/README.mdx +461 -0
  21. package/src/async/README.mdx +628 -0
  22. package/src/error-report/README.mdx +471 -0
  23. package/src/foundationApi.mdx +123 -0
  24. package/src/http/README.mdx +570 -0
  25. package/src/odata/README.mdx +5142 -0
  26. package/src/persisted/README.mdx +1080 -0
  27. package/src/resource/README.mdx +820 -0
  28. package/src/server-fn/README.mdx +596 -0
  29. package/src/transport/README.mdx +528 -0
  30. package/src/README.md +0 -937
  31. package/src/async/README.md +0 -623
  32. package/src/async/async.mdx +0 -6
  33. package/src/odata/README.md +0 -761
  34. package/src/odata/odataFetchFn.mdx +0 -6
  35. package/src/persisted/README.md +0 -598
  36. package/src/persisted/persisted.mdx +0 -6
@@ -0,0 +1,471 @@
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
+ ### Что такое draft и delivery body
89
+
90
+ Для начинающего разработчика важно не смешивать две формы:
91
+
92
+ ```text
93
+ ErrorReportDraft
94
+ ├── локальный lifecycle status
95
+ ├── retry information
96
+ ├── collected/sanitized payload
97
+ └── identifiers
98
+
99
+ ErrorReportDeliveryBody
100
+ ├── плоские обязательные backend fields
101
+ └── payload: JSON string
102
+ ```
103
+
104
+ Draft принадлежит локальному хранилищу `foundation-lib`. Delivery body — одноразовый snapshot для transport adapter-а.
105
+
106
+ Нельзя отправлять draft целиком только потому, что он уже похож на body: backend contract намеренно отделён от local store contract.
107
+
108
+ ### Соответствие полей
109
+
110
+ | Delivery field | Источник |
111
+ | --- | --- |
112
+ | `reportId` | `draft.reportId` |
113
+ | `sessionId` | `draft.sessionId` |
114
+ | `createdUtc` | `draft.createdUtc` |
115
+ | `category` | `draft.category` |
116
+ | `errorClass` | `draft.payload.error.name` |
117
+ | `errorMessage` | `draft.payload.error.message` или fallback |
118
+ | `stackTrace` | `draft.payload.error.stackTrace` |
119
+ | `payload` | `JSON.stringify(normalized payload)` |
120
+
121
+ Если message содержит непустой текст с пробелами по краям, проверка считает его валидным, но в delivery сохраняется исходная строка. Функция не выполняет общий trim draft-а.
122
+
123
+ ```ts
124
+ draft.payload.error.message = " Ошибка сохранения ";
125
+
126
+ const body = createErrorReportDeliveryBody(draft);
127
+ body.errorMessage;
128
+ // " Ошибка сохранения "
129
+ ```
130
+
131
+ Если message пустой или whitespace-only:
132
+
133
+ ```ts
134
+ draft.payload.error.message = " ";
135
+
136
+ const body = createErrorReportDeliveryBody(draft);
137
+ body.errorMessage;
138
+ // "Неизвестная ошибка"
139
+
140
+ JSON.parse(body.payload).error.message;
141
+ // "Неизвестная ошибка"
142
+ ```
143
+
144
+ Функция создаёт новые wrapper objects и не записывает fallback обратно в draft.
145
+
146
+ ## `sendErrorReport`
147
+
148
+ Lifecycle:
149
+
150
+ ```text
151
+ draft отсутствует ─────────► undefined
152
+ status sent/sending ───────► текущий draft, adapter не вызывается
153
+ остальной draft ───────────► sending
154
+
155
+ adapter success / error
156
+ │ │
157
+ ▼ ▼
158
+ sent failed + failedReason
159
+ ```
160
+
161
+ Пошагово функция:
162
+
163
+ 1. Ищет draft по `reportId` в store из `foundation-lib`.
164
+ 2. Возвращает `undefined`, если draft не найден.
165
+ 3. Не отправляет повторно draft со статусом `sent` или `sending`.
166
+ 4. Устанавливает `status: "sending"` и очищает `failedReason`.
167
+ 5. Создаёт delivery body.
168
+ 6. Вызывает `adapter(body, { draft, queryClient })`.
169
+ 7. При успехе устанавливает `status: "sent"` и `sentUtc`.
170
+ 8. При ошибке устанавливает `status: "failed"`, сохраняет нормализованное сообщение и повторно бросает исходную ошибку.
171
+
172
+ Adapter получает исходный snapshot `draft`, прочитанный до перевода store в `sending`. Используйте `body` как delivery payload, а context — для query invalidation или transport, которому нужен `QueryClient`.
173
+
174
+ ### Status table
175
+
176
+ | Status draft до вызова | Вызывается adapter | Результат |
177
+ | --- | --- | --- |
178
+ | draft отсутствует | нет | `undefined` |
179
+ | `sent` | нет | текущий draft |
180
+ | `sending` | нет | текущий draft |
181
+ | `pending` или эквивалентный начальный | да | `sent` либо exception + `failed` |
182
+ | `failed` | да | повторная попытка |
183
+
184
+ Функция не создаёт draft. Если `reportId` ошибочный, это не exception: result равен `undefined`.
185
+
186
+ ### Время отправки
187
+
188
+ `sentUtc` создаётся на client после успешного завершения adapter-а:
189
+
190
+ ```ts
191
+ new Date().toISOString()
192
+ ```
193
+
194
+ Это client timestamp, а не server acknowledgement time. Для аудита используйте server time из backend record.
195
+
196
+ ### Ошибка построения body
197
+
198
+ `createErrorReportDeliveryBody` вызывается внутри `try`. Если `JSON.stringify` выбросил error:
199
+
200
+ - adapter не вызывается;
201
+ - draft становится `failed`;
202
+ - `failedReason` формируется через `createErrorInfo`;
203
+ - исходный error снова выбрасывается caller-у.
204
+
205
+ ### UI flow
206
+
207
+ ```tsx
208
+ import { useState } from "react";
209
+ import { useQueryClient } from "@tanstack/react-query";
210
+ import { sendErrorReport } from "@ryuzaki13/react-foundation-api/error-report";
211
+
212
+ function SendReportButton({ reportId }: { reportId: string }) {
213
+ const queryClient = useQueryClient();
214
+ const [isSending, setIsSending] = useState(false);
215
+ const [errorMessage, setErrorMessage] = useState<string>();
216
+
217
+ const handleSend = async () => {
218
+ setIsSending(true);
219
+ setErrorMessage(undefined);
220
+
221
+ try {
222
+ const result = await sendErrorReport(reportId, {
223
+ adapter: deliveryAdapter,
224
+ queryClient
225
+ });
226
+
227
+ if (!result) {
228
+ setErrorMessage("Черновик отчёта не найден");
229
+ }
230
+ } catch (error) {
231
+ setErrorMessage(
232
+ error instanceof Error ? error.message : "Не удалось отправить отчёт"
233
+ );
234
+ } finally {
235
+ setIsSending(false);
236
+ }
237
+ };
238
+
239
+ return (
240
+ <>
241
+ <button type="button" disabled={isSending} onClick={handleSend}>
242
+ {isSending ? "Отправка…" : "Отправить отчёт"}
243
+ </button>
244
+ {errorMessage ? <p role="alert">{errorMessage}</p> : null}
245
+ </>
246
+ );
247
+ }
248
+ ```
249
+
250
+ Local `isSending` нужен UI для немедленной блокировки повторного click. Draft status остаётся источником долгоживущего lifecycle, но component может не быть подписан на store.
251
+
252
+ ## Варианты delivery adapter-а
253
+
254
+ ### Обычный HTTP
255
+
256
+ ```ts
257
+ import { httpFetchPayload } from "@ryuzaki13/react-foundation-api/http";
258
+ import type { ErrorReportDeliveryAdapter } from "@ryuzaki13/react-foundation-api/error-report";
259
+
260
+ export const deliveryAdapter: ErrorReportDeliveryAdapter = async (body) => {
261
+ await httpFetchPayload("/api/error-reports", {
262
+ init: {
263
+ method: "POST",
264
+ headers: { "Content-Type": "application/json" },
265
+ body: JSON.stringify(body)
266
+ }
267
+ });
268
+ };
269
+ ```
270
+
271
+ ### OData FunctionImport
272
+
273
+ Если backend contract опубликован как FunctionImport, operation определяется metadata:
274
+
275
+ ```ts
276
+ import { odataFunctionImportFn } from "@ryuzaki13/react-foundation-api/odata";
277
+ import { wrapODataParams } from "@ryuzaki13/react-foundation-lib/odata-service";
278
+
279
+ const deliveryAdapter: ErrorReportDeliveryAdapter = async (body, { queryClient }) => {
280
+ await odataFunctionImportFn({
281
+ odata: {
282
+ service: "ZERROR_REPORT_SRV",
283
+ target: "SendErrorReport"
284
+ },
285
+ params: wrapODataParams({
286
+ ReportId: body.reportId,
287
+ Payload: body.payload
288
+ })
289
+ })({ client: queryClient });
290
+ };
291
+ ```
292
+
293
+ Это только пример mapping: точные parameter names должны соответствовать metadata вашего сервиса.
294
+
295
+ ### Server function
296
+
297
+ Adapter может вызвать client-safe serverFn operation. Модуль error-report не знает о transport и не ограничивает его, пока adapter возвращает `Promise<void>`.
298
+
299
+ ### Почему adapter ничего не возвращает
300
+
301
+ `sendErrorReport` управляет local draft lifecycle, а не server entity. Если backend вернул receipt, adapter может обработать его внутри: сохранить в отдельный Query cache, telemetry или domain store. Public delivery contract считает успехом fulfilled promise.
302
+
303
+ ## Adapter с cache invalidation
304
+
305
+ ```ts
306
+ const adapter: ErrorReportDeliveryAdapter = async (body, { queryClient }) => {
307
+ await postReport(body);
308
+ await queryClient.invalidateQueries({ queryKey: ["error-reports"] });
309
+ };
310
+ ```
311
+
312
+ Если invalidation бросит ошибку, вся доставка считается неуспешной и draft станет `failed`, даже если backend уже принял отчёт. Для non-critical cache update обработайте ошибку внутри adapter-а.
313
+
314
+ Безопасный вариант для необязательного refresh:
315
+
316
+ ```ts
317
+ const adapter: ErrorReportDeliveryAdapter = async (body, { queryClient }) => {
318
+ await postReport(body);
319
+
320
+ try {
321
+ await queryClient.invalidateQueries({ queryKey: ["error-reports"] });
322
+ } catch (error) {
323
+ console.warn("Отчёт отправлен, но cache не обновлён", error);
324
+ }
325
+ };
326
+ ```
327
+
328
+ Решение зависит от contract: если fresh cache является частью успешной операции, не поглощайте ошибку.
329
+
330
+ ## Повторная отправка и concurrency
331
+
332
+ После `failed` функцию можно вызвать повторно. Проверка `sending` защищает от обычного повторного нажатия, но это не распределённая блокировка: два практически одновременных вызова могут успеть прочитать старый статус до его обновления. Backend желательно сделать идемпотентным по `reportId`.
333
+
334
+ ### Idempotency backend-а
335
+
336
+ Рекомендуемый server contract:
337
+
338
+ ```text
339
+ UNIQUE(reportId)
340
+ ```
341
+
342
+ Повтор с тем же `reportId` должен либо вернуть прежний успешный result, либо завершиться предсказуемым «already accepted», которое adapter считает успехом.
343
+
344
+ Это защищает сценарий:
345
+
346
+ 1. Backend принял report.
347
+ 2. Network response потерялся.
348
+ 3. Client пометил draft как `failed`.
349
+ 4. Пользователь повторил отправку.
350
+
351
+ Без idempotency server создаст duplicate.
352
+
353
+ ### Несколько tabs
354
+
355
+ Status `sending` в local store не является cross-tab distributed lock, если storage implementation явно не гарантирует это. Idempotency по `reportId` остаётся обязательной защитой.
356
+
357
+ ## Тестирование
358
+
359
+ ### Body builder
360
+
361
+ ```ts
362
+ import { expect, it } from "vitest";
363
+
364
+ it("подставляет fallback message в оба места", () => {
365
+ const body = createErrorReportDeliveryBody({
366
+ ...draftFixture,
367
+ payload: {
368
+ ...draftFixture.payload,
369
+ error: {
370
+ ...draftFixture.payload.error,
371
+ message: " "
372
+ }
373
+ }
374
+ });
375
+
376
+ expect(body.errorMessage).toBe("Неизвестная ошибка");
377
+ expect(JSON.parse(body.payload).error.message).toBe("Неизвестная ошибка");
378
+ });
379
+ ```
380
+
381
+ ### Delivery lifecycle
382
+
383
+ В package/feature test проверяйте:
384
+
385
+ - отсутствующий id не вызывает adapter;
386
+ - `sent` не отправляется повторно;
387
+ - `sending` не отправляется повторно;
388
+ - success переводит status в `sent` и задаёт ISO `sentUtc`;
389
+ - adapter error переводит status в `failed`;
390
+ - исходный error сохраняет identity и выбрасывается повторно;
391
+ - retry после `failed` снова вызывает adapter.
392
+
393
+ Adapter удобно делать mock-функцией:
394
+
395
+ ```ts
396
+ const adapter = vi.fn<ErrorReportDeliveryAdapter>().mockResolvedValue(undefined);
397
+ ```
398
+
399
+ Не помещайте реальные stack traces, cookies, tokens или пользовательские данные в test snapshots.
400
+
401
+ ## Частые ошибки
402
+
403
+ ### Передавать draft вместо body
404
+
405
+ Adapter contract уже получает подготовленный `body`. Не сериализуйте `context.draft` вместо него.
406
+
407
+ ### Дважды сериализовать `payload`
408
+
409
+ `body.payload` уже string:
410
+
411
+ ```ts
412
+ JSON.stringify(body)
413
+ ```
414
+
415
+ правильно сериализует outer HTTP body. Не делайте `body.payload = JSON.stringify(body.payload)`.
416
+
417
+ ### Считать `sending` надёжной блокировкой
418
+
419
+ Это local lifecycle guard. Server idempotency всё равно нужна.
420
+
421
+ ### Поглощать adapter error в UI
422
+
423
+ Функция намеренно rethrow-ит error. Покажите feedback и оставьте возможность retry.
424
+
425
+ ### Добавлять transport в shared delivery module
426
+
427
+ Endpoint и auth policy специфичны проекту. Они должны оставаться adapter-ом host application.
428
+
429
+ ## Ошибки и безопасность
430
+
431
+ - Ошибка adapter-а не поглощается: вызывающий код обязан её обработать.
432
+ - Не добавляйте secrets в draft. Delivery layer не выполняет дополнительную redaction всего payload.
433
+ - `queryClient` передаётся adapter-у, но модуль сам не инвалидирует queries.
434
+ - Функция управляет локальным draft store; не вызывайте её в среде без подходящего storage/runtime, если draft store там недоступен.
435
+ - Статус `sent` означает успешное завершение adapter-а, а не обязательное подтверждение бизнес-обработки backend-ом.
436
+ - Delivery body содержит stack trace и diagnostic payload: передавайте его только по защищённому каналу авторизованному endpoint.
437
+ - Не логируйте `body.payload` целиком в production.
438
+ - Redaction должна происходить при сборе draft в `foundation-lib`; delivery boundary не может безопасно угадать все domain secrets.
439
+
440
+ ## FAQ
441
+
442
+ ### Почему функции нужен `QueryClient`, даже если adapter его не использует?
443
+
444
+ Это единый adapter context. Он позволяет OData operations и cache synchronization без глобального QueryClient.
445
+
446
+ ### Можно ли отправить report, которого нет в store?
447
+
448
+ Нет. `sendErrorReport` принимает id существующего draft и вернёт `undefined`.
449
+
450
+ ### Можно ли изменить body перед отправкой?
451
+
452
+ Да, внутри project adapter можно построить backend-specific envelope. Не мутируйте входной body; создайте новый object.
453
+
454
+ ### Когда разрешён retry?
455
+
456
+ После status `failed`. `sent` и `sending` являются no-op states.
457
+
458
+ ### Кто отвечает за offline queue?
459
+
460
+ Не этот модуль. Он хранит lifecycle одного draft и вызывает adapter. Background sync/retry scheduler должен быть отдельной явно спроектированной инфраструктурой.
461
+
462
+ ## Полный API
463
+
464
+ | Export | Назначение |
465
+ | --- | --- |
466
+ | `createErrorReportDeliveryBody` | Строит сериализуемый backend body |
467
+ | `sendErrorReport` | Управляет status и вызывает adapter |
468
+ | `ErrorReportDeliveryBody` | Поля delivery payload |
469
+ | `ErrorReportDeliveryContext` | `draft` и `queryClient` для adapter-а |
470
+ | `ErrorReportDeliveryAdapter` | Контракт функции доставки |
471
+ | `SendErrorReportOptions` | `adapter` и `queryClient` |
@@ -0,0 +1,123 @@
1
+ import { Meta } from "@storybook/addon-docs/blocks";
2
+
3
+ <Meta title="Foundation API/Обзор" />
4
+
5
+ # `@ryuzaki13/react-foundation-api`
6
+
7
+ Пакет соединяет приложение с внешними API. Он содержит transport, интеграцию с TanStack Query, SAP/OData V2 infrastructure и небольшие orchestration helpers. Документ рассчитан на разработчика, который видит только опубликованный пакет и его типы.
8
+
9
+ ## Подключение
10
+
11
+ ```bash
12
+ npm install @ryuzaki13/react-foundation-api @ryuzaki13/react-foundation-lib
13
+ ```
14
+
15
+ Корневого импорта нет. Всегда указывайте точный subpath:
16
+
17
+ ```ts
18
+ import { httpFetchPayload } from "@ryuzaki13/react-foundation-api/http";
19
+ import { useResourceQuery } from "@ryuzaki13/react-foundation-api/resource";
20
+ import { useODataMetadataQuery } from "@ryuzaki13/react-foundation-api/odata";
21
+ ```
22
+
23
+ ## Где находится пакет в архитектуре
24
+
25
+ ```text
26
+ host application: entities / features / widgets / pages
27
+
28
+
29
+ @ryuzaki13/react-foundation-api
30
+ transport + query orchestration
31
+
32
+
33
+ @ryuzaki13/react-foundation-lib
34
+ pure helpers + metadata + query policies
35
+ ```
36
+
37
+ `foundation-api` не должен знать конкретную бизнес-сущность, экран или UI-компонент. DTO/domain mapping конкретного проекта остаётся у host-приложения. UI primitives находятся в `@ryuzaki13/react-foundation-ui`.
38
+
39
+ ## Как выбрать entrypoint
40
+
41
+ | Нужно сделать | Entrypoint | Важная граница |
42
+ | --- | --- | --- |
43
+ | Вызвать обычный JSON REST endpoint | `http` | Нет SAP/SSO/X-CSRF политики |
44
+ | Вызвать SAP Gateway/OData V2 | `odata` | Metadata, envelope, SSO и X-CSRF принадлежат этому слою |
45
+ | Описать произвольные queries/mutations | `resource` | Transport передаётся операциями |
46
+ | Описать сохранённые records с фиксированными capability | `persisted` | Только list/latest/history/save/create/delete |
47
+ | Подключить функцию вида `fn({ data })` | `server-fn` | Это adapter операции, не сервер и не security boundary |
48
+ | Выполнить пакет независимых задач | `async` | Выберите all-at-once или bounded concurrency |
49
+ | Прочитать ADT transport XML | `adt` | Endpoint `/sap/bc/adt`, XML |
50
+ | Прочитать UI2 workbench/customizing requests | `transport` | Это SAP transport requests, а не generic HTTP transport |
51
+ | Отправить error-report draft | `error-report` | Сбор/хранение draft принадлежит `foundation-lib` |
52
+
53
+ ## Самая частая ошибка: одинаковые слова, разные уровни
54
+
55
+ ### `http` и `odata`
56
+
57
+ `http` подходит для нейтрального REST. Он не получает CSRF token, не распознаёт SAML form и не снимает OData V2 envelope. Для SAP Gateway используйте `odata`, даже если технически запрос тоже выполняется через `fetch`.
58
+
59
+ ### `resource` и `persisted`
60
+
61
+ `resource` позволяет задавать любые имена операций: например `search`, `details`, `archive`. `persisted` — специализированный facade для сохранённых записей с именами `list`, `latest`, `history`, `save`, `create`, `delete`.
62
+
63
+ ### `adt` и `transport`
64
+
65
+ Оба entrypoint читают SAP transports, но из разных API:
66
+
67
+ | Entrypoint | Endpoint/format | Результат |
68
+ | --- | --- | --- |
69
+ | `adt` | `/sap/bc/adt/cts/transports`, XML | Общий `UserTransport` из ADT header |
70
+ | `transport` | UI2 OData endpoints, JSON | Нормализованные workbench/customizing requests |
71
+
72
+ ## TanStack Query: кто чем владеет
73
+
74
+ `resource`, `persisted` и часть `odata` работают внутри `QueryClientProvider`. Query key определяет identity кеша; `staleTime` — свежесть; `gcTime` — время хранения неиспользуемого query; `AbortSignal` позволяет отменять чтение. Mutation не обновляет все связанные query автоматически: это делает явно выбранная cache strategy.
75
+
76
+ ```tsx
77
+ import { QueryClientProvider } from "@tanstack/react-query";
78
+ import { createQueryClient } from "@ryuzaki13/react-foundation-lib/query-client";
79
+
80
+ const queryClient = createQueryClient();
81
+
82
+ export function App() {
83
+ return <QueryClientProvider client={queryClient}>{/* приложение */}</QueryClientProvider>;
84
+ }
85
+ ```
86
+
87
+ Не создавайте новый `QueryClient` при каждом render.
88
+
89
+ ## Runtime
90
+
91
+ | API | Browser | Server runtime |
92
+ | --- | :---: | :---: |
93
+ | `async` и pure normalization | Да | Да |
94
+ | `http` | Да | Да, если доступны Web Fetch API |
95
+ | pure `resource` factories | Да | Да |
96
+ | React hooks | Да | Только в поддерживаемом React runtime/provider |
97
+ | OData SSO recovery | Да | Нет: использует `window`, `document`, iframe |
98
+ | SAP transport fetchers | Да | Только при корректном fetch/proxy/cookie окружении |
99
+
100
+ OData transport использует compile-time constants и проектный adapter. Host-сборка должна предоставлять ожидаемую конфигурацию; подробности приведены на странице OData.
101
+
102
+ ## Service Worker cache
103
+
104
+ Некоторые query factories передают строку политики через header `x-sw-cache`. Сам header ничего не кеширует: Service Worker host-приложения должен понимать тот же protocol.
105
+
106
+ ```text
107
+ off
108
+ ttl=24h
109
+ ttl=10m;max=200;name=ui
110
+ bust=24h;name=ref
111
+ ```
112
+
113
+ `bust` означает принудительное обновление сетевого snapshot в соответствующей cache policy. Не используйте SW cache для персональных/секретных payload без отдельной модели изоляции.
114
+
115
+ ## Каталог страниц
116
+
117
+ | Область | Документы |
118
+ | --- | --- |
119
+ | Transport | [HTTP](./http/README.mdx), [OData](./odata/README.mdx), [ADT](./adt/README.mdx), [SAP Transport Requests](./transport/README.mdx) |
120
+ | Query infrastructure | [Resource](./resource/README.mdx), [Persisted](./persisted/README.mdx), [Server Function](./server-fn/README.mdx) |
121
+ | Orchestration и diagnostics | [Async](./async/README.mdx), [Error Report](./error-report/README.mdx) |
122
+
123
+ Каждая страница содержит точный import, пошаговый guide, runtime/lifecycle, ошибки и таблицу публичных exports. Не импортируйте файл глубоким путём, если его символ не опубликован соответствующим entrypoint.