@ryuzaki13/react-foundation-lib 1.8.3 → 2.0.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.
Files changed (43) hide show
  1. package/README.md +5 -3
  2. package/dist/error-report/breadcrumbs.d.ts.map +1 -1
  3. package/dist/error-report/capture.d.ts.map +1 -1
  4. package/dist/error-report/diagnostics.d.ts.map +1 -1
  5. package/dist/error-report/drafts.d.ts +2 -11
  6. package/dist/error-report/drafts.d.ts.map +1 -1
  7. package/dist/error-report/errorInfo.d.ts.map +1 -1
  8. package/dist/error-report/index.d.ts +5 -0
  9. package/dist/error-report/index.d.ts.map +1 -1
  10. package/dist/error-report/index.js +920 -210
  11. package/dist/error-report/index.js.map +1 -1
  12. package/dist/error-report/payload.d.ts +13 -0
  13. package/dist/error-report/payload.d.ts.map +1 -0
  14. package/dist/error-report/queue.d.ts +15 -0
  15. package/dist/error-report/queue.d.ts.map +1 -0
  16. package/dist/error-report/queueContract.d.ts +62 -0
  17. package/dist/error-report/queueContract.d.ts.map +1 -0
  18. package/dist/error-report/queueRecord.d.ts +9 -0
  19. package/dist/error-report/queueRecord.d.ts.map +1 -0
  20. package/dist/error-report/safeValue.d.ts +26 -2
  21. package/dist/error-report/safeValue.d.ts.map +1 -1
  22. package/dist/error-report/schema.d.ts +14 -0
  23. package/dist/error-report/schema.d.ts.map +1 -0
  24. package/dist/error-report/transport.d.ts +0 -2
  25. package/dist/error-report/transport.d.ts.map +1 -1
  26. package/dist/error-report/types.d.ts +26 -1
  27. package/dist/error-report/types.d.ts.map +1 -1
  28. package/dist/pwa/index.d.ts +1 -0
  29. package/dist/pwa/index.d.ts.map +1 -1
  30. package/dist/pwa/index.js +75 -59
  31. package/dist/pwa/index.js.map +1 -1
  32. package/dist/pwa/useBrowserOnlineStatus.d.ts +3 -0
  33. package/dist/pwa/useBrowserOnlineStatus.d.ts.map +1 -0
  34. package/dist/query-client/index.js +137 -43
  35. package/dist/query-client/index.js.map +1 -1
  36. package/dist/query-client/persistence.d.ts +21 -3
  37. package/dist/query-client/persistence.d.ts.map +1 -1
  38. package/dist/query-client/queryMeta.d.ts +21 -2
  39. package/dist/query-client/queryMeta.d.ts.map +1 -1
  40. package/package.json +1 -1
  41. package/src/error-report/README.mdx +133 -27
  42. package/src/pwa/README.mdx +4 -0
  43. package/src/query-client/README.mdx +48 -7
@@ -4,7 +4,7 @@ import { Meta } from "@storybook/addon-docs/blocks";
4
4
 
5
5
  # Error Report
6
6
 
7
- Модуль `@ryuzaki13/react-foundation-lib/error-report` собирает клиентскую диагностику ошибки в структурированный draft и временно хранит последние drafts в `sessionStorage`.
7
+ Модуль `@ryuzaki13/react-foundation-lib/error-report` собирает клиентскую диагностику ошибки в versioned bounded draft, временно хранит последние drafts в `sessionStorage` и предоставляет durable IndexedDB queue для отложенной доставки.
8
8
 
9
9
  Он умеет объединить:
10
10
 
@@ -15,9 +15,10 @@ import { Meta } from "@storybook/addon-docs/blocks";
15
15
  - состояние TanStack Query без полного query data;
16
16
  - технические сведения о persisted queries;
17
17
  - контекст React/runtime/transport;
18
- - lifecycle доставки: `pending`, `sending`, `sent`, `failed`.
18
+ - локальный draft lifecycle: `pending`, `sending`, `sent`, `failed`;
19
+ - durable queue с TTL, bounded capacity и атомарным межвкладочным lease.
19
20
 
20
- Важная граница: **lib-пакет не отправляет отчёт на сервер**. Он формирует draft и предоставляет точки подключения. Авторизация, HTTP/API delivery, retry, пользовательское согласие и UI принадлежат приложению либо API-пакету.
21
+ Важная граница: **lib-пакет не отправляет отчёт на сервер**. Он формирует draft, хранит queue и предоставляет атомарные переходы. Авторизация, HTTP/API delivery, retry, пользовательское согласие и UI принадлежат приложению либо API-пакету.
21
22
 
22
23
  ## Установка
23
24
 
@@ -26,15 +27,18 @@ import { Meta } from "@storybook/addon-docs/blocks";
26
27
  ```bash
27
28
  npm install @ryuzaki13/react-foundation-lib \
28
29
  @tanstack/react-query \
29
- @tanstack/query-persist-client-core
30
+ @tanstack/query-persist-client-core \
31
+ zod
30
32
  ```
31
33
 
32
34
  ```ts
33
35
  import {
34
36
  captureRuntimeErrorReport,
37
+ createErrorReportQueue,
35
38
  createErrorInfo,
36
39
  installErrorReportBrowserBreadcrumbs,
37
40
  setErrorReportRuntimeErrorReporter,
41
+ setErrorReportSanitizer,
38
42
  type ErrorReportDraft
39
43
  } from "@ryuzaki13/react-foundation-lib/error-report";
40
44
  ```
@@ -47,7 +51,8 @@ import {
47
51
  → нормализация + diagnostics + breadcrumbs
48
52
  → ErrorReportDraft(status = pending)
49
53
  → sessionStorage, максимум 10 drafts
50
- → app/API delivery
54
+ → enqueue payload в IndexedDB, максимум 50 записей / 7 суток
55
+ → app/API delivery через lease
51
56
  → updateErrorReportDraft(status = sending/sent/failed)
52
57
  ```
53
58
 
@@ -80,15 +85,15 @@ declare const __APP_ID__: string;
80
85
  declare const __APP_BUILD_ID__: string;
81
86
  ```
82
87
 
83
- `__APP_ID__` входит в ключи `sessionStorage`, а `__APP_BUILD_ID__` помогает связать ошибку с деплоем.
88
+ `__APP_ID__` входит в payload как `application` и в ключи `sessionStorage`, а `__APP_BUILD_ID__` помогает связать ошибку с деплоем. Приложение обязано использовать устойчивый deployment identifier, а не display name. Например, KTK-приложения используют `ru.education-system` и `ru.ktk-website`.
84
89
 
85
90
  Пример Vite:
86
91
 
87
92
  ```ts
88
93
  define: {
89
94
  __DEV__: JSON.stringify(mode !== "production"),
90
- __APP_ID__: JSON.stringify("education-arm"),
91
- __APP_BUILD_ID__: JSON.stringify(process.env.BUILD_ID ?? "local"),
95
+ __APP_ID__: JSON.stringify("ru.education-system"),
96
+ __APP_BUILD_ID__: JSON.stringify(process.env.GIT_COMMIT_SHA ?? "local"),
92
97
  }
93
98
  ```
94
99
 
@@ -165,7 +170,7 @@ type ErrorReportErrorInfo = {
165
170
  };
166
171
  ```
167
172
 
168
- Для обычного `Error` сохраняются `name`, непустой `message` и полный `stack`.
173
+ Для обычного `Error` сохраняются `name`, непустой `message` и `stack` до 64 KiB.
169
174
 
170
175
  ```ts
171
176
  createErrorInfo(new Error("Не удалось сохранить"));
@@ -189,16 +194,34 @@ createErrorInfo(new Error("Не удалось сохранить"));
189
194
  - максимальная глубина — 4;
190
195
  - максимум 20 элементов массива;
191
196
  - максимум 40 ключей объекта;
197
+ - строка — не более 4096 символов;
192
198
  - `undefined`, function, symbol и усечённая ветка получают специальные объекты `{ type: ... }`;
199
+ - циклическая ссылка получает `{ type: "circular" }`;
193
200
  - `bigint` превращается в строку;
194
201
  - `Date` — в ISO-строку.
202
+ - известные secret-like keys (`authorization`, `cookie`, `password`, `token`, `secret`, CSRF/API keys) получают `[REDACTED]`;
203
+ - email в свободной строке заменяется на `[REDACTED_EMAIL]`;
204
+ - значения полей с именем `*Url` сохраняются без search/hash.
195
205
 
196
206
  ```ts
197
207
  createDiagnosticValue({ retry: 2, handler: () => undefined });
198
208
  // { retry: 2, handler: { type: "function" } }
199
209
  ```
200
210
 
201
- Критически важно: функция **не скрывает значения по имени ключа**. Поля `token`, `password`, `authorization`, email и другие секреты сохранятся, если их передать. Это ограничитель размера и типа, а не security sanitizer.
211
+ Built-in redaction является страховочной сеткой и не распознаёт ФИО, телефоны и все предметные идентификаторы. Он не заменяет app-level allow-list.
212
+
213
+ ### `setErrorReportSanitizer(sanitizer?)`
214
+
215
+ Регистрирует единственный sanitizer приложения для error message/stack, delivery failure reason, query/mutation keys, meta, context, data shape и breadcrumb detail. Callback получает уже bounded `ErrorReportSafeValue` и scope, возвращает разрешённое значение либо `undefined`, чтобы удалить секцию. Результат повторно нормализуется foundation-ограничителями.
216
+
217
+ ```ts
218
+ setErrorReportSanitizer((value, context) => {
219
+ if (context.scope !== "context" || !value || Array.isArray(value) || typeof value !== "object") return value;
220
+ return "operation" in value ? { operation: value.operation } : undefined;
221
+ });
222
+ ```
223
+
224
+ Callback должен быть чистым и идемпотентным: capture может повторно применить его к уже подготовленной секции перед storage write. Если callback бросает исключение, рискованная секция удаляется. При teardown/hot reload вызовите `setErrorReportSanitizer(undefined)`.
202
225
 
203
226
  ### `sanitizeDetail(detail)`
204
227
 
@@ -260,7 +283,7 @@ getErrorReportBreadcrumbs();
260
283
  clearErrorReportBreadcrumbs();
261
284
  ```
262
285
 
263
- Возвращаемый массив является текущим внутренним массивом, а не глубокой копией. Считайте его readonly и не меняйте элементы вручную.
286
+ Возвращаемый массив является shallow copy. Вложенные элементы считайте readonly и не меняйте вручную.
264
287
 
265
288
  ### Автоматические browser breadcrumbs
266
289
 
@@ -276,9 +299,9 @@ uninstallBreadcrumbs();
276
299
  - click в capture phase;
277
300
  - `document.visibilitychange`.
278
301
 
279
- Для click сохраняются координаты/модификаторы и ограниченное описание meaningful target: tag, стабильные атрибуты, короткий текст и цепочка родителей. Глубина DOM-цепочки ограничена 20, текст — 80 символами, атрибут — 120.
302
+ Для click сохраняются координаты/модификаторы и ограниченное техническое описание meaningful target: tag, role, input type и цепочка родителей. Приложение может явно добавить slug-like `data-error-report-target`, например `student-save`. Глубина DOM-цепочки ограничена 20, атрибут — 120 символами.
280
303
 
281
- Код не читает `input.value` и не копирует CSS class list, но текст/ARIA/title/data-атрибуты тоже могут содержать чувствительные данные. Не помещайте секреты в DOM-атрибуты и UI-текст.
304
+ Код не читает `input.value`, DOM text, `id`, `title`, `aria-*`, CSS classes, `data-ui`, `data-action` и произвольные `data-*`. Значение `data-error-report-target` принимается только из латинских букв, цифр и символов `._:/-`; свободный текст в него помещать нельзя.
282
305
 
283
306
  На сервере установщик возвращает пустую cleanup-функцию.
284
307
 
@@ -288,7 +311,7 @@ uninstallBreadcrumbs();
288
311
 
289
312
  Собирает:
290
313
 
291
- - `queryHash` и диагностическое представление `queryKey`;
314
+ - безопасный hash уже очищенного query key и его диагностическое представление;
292
315
  - status/fetchStatus;
293
316
  - timestamps обновления data/error;
294
317
  - failure count, invalidation и observer count;
@@ -296,7 +319,7 @@ uninstallBreadcrumbs();
296
319
  - `dataShape` вместо data;
297
320
  - нормализованную query error.
298
321
 
299
- Значения `queryKey` и `meta` сохраняются. Если туда попал token или персональный идентификатор, он попадёт в отчёт. Проектируйте query keys как диагностически безопасный контракт.
322
+ Значения `queryKey` и `meta` проходят built-in redaction и app sanitizer. Предметные персональные идентификаторы всё равно должны удаляться policy приложения.
300
323
 
301
324
  ### `collectMutationDiagnostics(mutation)`
302
325
 
@@ -310,7 +333,7 @@ uninstallBreadcrumbs();
310
333
 
311
334
  Асинхронно читает IndexedDB persistence из `/query-client` и возвращает максимум 40 записей.
312
335
 
313
- Сохраняются storage key, buster, query hash/key, statuses, timestamps, failure count и error. `state.data` намеренно исключён полностью.
336
+ Сохраняются buster, безопасный hash/key, statuses, timestamps, failure count и error. IndexedDB storage key и `state.data` намеренно исключены полностью.
314
337
 
315
338
  Если IndexedDB/persistence недоступны или чтение бросает ошибку, возвращается пустой массив — сбор отчёта не падает.
316
339
 
@@ -349,7 +372,7 @@ const draft = await captureQueryErrorReport(error, query, queryClient, {
349
372
  const draft = await captureMutationErrorReport(error, mutation, queryClient, { source: "save-order", detail: { form: "order-edit" } });
350
373
  ```
351
374
 
352
- Текущая реализация включает `detail` в breadcrumb, но не переносит его в `payload.context` mutation draft. Если этот контекст критичен для доставки, добавьте безопасные данные в app-level envelope либо используйте явную согласованную модель до capture.
375
+ Текущая реализация включает очищенный `detail` и в breadcrumb, и в `payload.context` mutation draft.
353
376
 
354
377
  ## Capture runtime/React-ошибки
355
378
 
@@ -395,14 +418,96 @@ type ErrorReportDraft = {
395
418
 
396
419
  Payload содержит category/source/error, build environment, client, location, viewport, breadcrumbs и optional секции query/mutation/react/context.
397
420
 
421
+ `ErrorReportPayload` содержит обязательные `payloadVersion: 1` и `application`. Runtime parser принимает только текущую версию; unversioned и будущие формы не интерпретируются эвристически.
422
+
423
+ ### Runtime parsing
424
+
425
+ ```ts
426
+ parseErrorReportPayload(value);
427
+ parseErrorReportDraft(value);
428
+ parseErrorReportDrafts(value);
429
+ ```
430
+
431
+ Первые две функции возвращают текущий typed contract либо `undefined`. Parser массива независимо отбрасывает повреждённые элементы, сохраняя соседние валидные drafts. Дополнительно проверяется совпадение identity полей draft и payload и фактический размер payload.
432
+
433
+ ### Bounded payload
434
+
435
+ `limitErrorReportPayload(payload)` сначала повторно применяет sanitization, затем проверяет UTF-8 размер `JSON.stringify`. Предел — `ERROR_REPORT_PAYLOAD_MAX_BYTES` (256 KiB). При превышении optional diagnostics удаляются в фиксированном порядке, а `payload.truncation` фиксирует исходный размер, limit и список удалённых секций. `getErrorReportPayloadSize(payload)` возвращает фактический сериализованный размер в байтах.
436
+
398
437
  ### Хранение
399
438
 
400
- Drafts сохраняются под ключом `${__APP_ID__}.errorReport.drafts.v1`. В одной вкладке остаются последние 10 элементов. Старые удаляются с начала массива.
439
+ Versioned drafts сохраняются под ключом `${__APP_ID__}.errorReport.drafts.v2`. В одной вкладке остаются последние 10 элементов. Старые удаляются с начала массива. Предыдущая unversioned storage-форма не загружается.
401
440
 
402
441
  Storage errors и некорректный JSON подавляются. При переполнении sessionStorage текущее in-memory состояние обновится, но запись на диск может не сохраниться.
403
442
 
404
443
  Модуль загружает drafts при первом импорте. Это модульное состояние, общее для всех consumers одного bundle.
405
444
 
445
+ ## Durable IndexedDB queue
446
+
447
+ `sessionStorage` draft остаётся кратким диагностическим lifecycle текущей вкладки. Надёжную отложенную доставку обеспечивает отдельная queue:
448
+
449
+ ```ts
450
+ const queue = createErrorReportQueue();
451
+ if (!queue) {
452
+ // IndexedDB отсутствует: app фиксирует безопасную деградацию без рекурсии report flow.
453
+ return;
454
+ }
455
+
456
+ const draft = await captureRuntimeErrorReport(error, context, queryClient);
457
+ if (draft) {
458
+ await queue.enqueue(draft.payload);
459
+ }
460
+ ```
461
+
462
+ Создание возвращает `undefined`, когда IndexedDB недоступен. Ошибки открывания, quota и транзакций не поглощаются: coordinator обязан отделить storage failure от исходной пользовательской ошибки и применить безопасную recovery policy.
463
+
464
+ ### Storage schema и limits
465
+
466
+ - база: `${__APP_ID__}.error-report.queue.v1`;
467
+ - object store: `records`;
468
+ - атомарная envelope-запись: `queue.v1`;
469
+ - queue schema: `queueVersion: 1`;
470
+ - максимум: 50 записей;
471
+ - TTL каждой записи: 7 суток от `enqueue`, независимо от client `createdUtc`;
472
+ - lease по умолчанию: 30 секунд.
473
+
474
+ Одна bounded envelope используется намеренно: `createIndexedDbQueryStorage.updateItem` изменяет её в одной `readwrite` транзакции, поэтому две вкладки не могут одновременно получить один report. Payload уже ограничен 256 KiB, а число записей — 50; отдельная база не смешивается с persisted Query cache.
475
+
476
+ При заполнении удаляется самый старый отчёт без активного lease, чтобы сохранить более свежую диагностику. Активные leases не вытесняются; если все 50 записей leased, `enqueue` возвращает `capacity-exceeded`. Повторный `reportId` возвращает `duplicate` и не заменяет сохранённый payload.
477
+
478
+ ### Lease flow
479
+
480
+ ```ts
481
+ const lease = await queue.leaseNext({ ownerId: tabId });
482
+ if (!lease) return;
483
+
484
+ try {
485
+ await deliver(lease.payload);
486
+ await queue.acknowledge(lease.reportId, tabId);
487
+ } catch {
488
+ await queue.reschedule(lease.reportId, tabId, {
489
+ nextAttemptUtc,
490
+ failure: { utc: new Date().toISOString(), kind: "network" }
491
+ });
492
+ }
493
+ ```
494
+
495
+ `acknowledge`, `reschedule` и `discard` изменяют запись только при совпадении `ownerId`. После crash/закрытия вкладки lease автоматически снова становится доступным по `expiresUtc`. Если server принял отчёт, а acknowledgement потерялся, клиент повторит тот же `reportId`; backend обязан быть идемпотентным.
496
+
497
+ `lastFailure` хранит только технический класс, UTC и optional HTTP status. Текст transport error не попадает в durable storage.
498
+
499
+ ### Queue API
500
+
501
+ - `enqueue(payload, now?)` — runtime-валидирует payload, удаляет expired и добавляет запись;
502
+ - `leaseNext({ ownerId, now?, leaseDurationMs? })` — атомарно выбирает ближайшую due-запись;
503
+ - `acknowledge(reportId, ownerId)` — удаляет подтверждённую запись;
504
+ - `reschedule(reportId, ownerId, retry)` — снимает lease и задаёт следующую попытку;
505
+ - `discard(reportId, ownerId)` — удаляет terminal-запись;
506
+ - `getRecords(now?)` — bounded диагностический snapshot;
507
+ - `getNextWakeUtc(now?)` — ближайшее due/lease-expiry время для scheduler-а.
508
+
509
+ Queue не использует `navigator.onLine` как источник истины и не владеет timer/event lifecycle. Этим занимается delivery coordinator из `@ryuzaki13/react-foundation-api/error-report`.
510
+
406
511
  ## Низкоуровневый `captureErrorReportDraft(args)`
407
512
 
408
513
  Используйте, когда все diagnostic sections уже подготовлены вручную:
@@ -445,7 +550,7 @@ try {
445
550
  }
446
551
  ```
447
552
 
448
- Для неизвестного ID update возвращает `undefined`. Обновление поверхностное и принимает `Partial<ErrorReportDraft>`: библиотека не проверяет разрешённые переходы статусов. App delivery обязан не менять `reportId`, payload и другие identity-поля без необходимости.
553
+ Для неизвестного ID update возвращает `undefined`. Функция принимает только lifecycle-поля `status`, `sentUtc`, `failedReason`; identity и payload изменить через неё нельзя. Runtime parser повторно проверяет draft, а свободный текст причины ограничивается и очищается перед storage write. Семантику разрешённых переходов статусов задаст delivery coordinator.
449
554
 
450
555
  ## Environment API
451
556
 
@@ -486,7 +591,7 @@ setErrorReportTransportErrorReporter(async (error, context) => {
486
591
 
487
592
  ### `reportTransportError(error, context)`
488
593
 
489
- Context имеет source `fetch.parseResponse` или `fetch.buildHttpError` и может содержать URL, method, SAP client, status/content type, redirect/response type и безопасное summary HTML-response.
594
+ Context имеет source `fetch.parseResponse` или `fetch.buildHttpError` и может содержать URL, method, SAP client, status/content type, redirect/response type и только техническое summary HTML-response. HTML title и text preview больше не входят в контракт, потому что являются содержимым response body.
490
595
 
491
596
  Вызов планирует reporter через 250 мс. Задержка позволяет TanStack Query перехватить тот же `Error` и создать более полезный query/mutation report.
492
597
 
@@ -550,11 +655,10 @@ export function installErrorReporting(queryClient: QueryClient) {
550
655
  - queryKey и mutationKey;
551
656
  - query/mutation meta;
552
657
  - `context.detail`;
553
- - URL и pathname;
658
+ - pathname без search/hash;
554
659
  - error message/stack;
555
- - UI text, ARIA/title/data attributes breadcrumbs;
660
+ - app-provided context, если policy ошибочно разрешила предметные данные;
556
661
  - user agent, language, timezone;
557
- - имена persisted storage keys.
558
662
 
559
663
  Поэтому:
560
664
 
@@ -570,11 +674,11 @@ export function installErrorReporting(queryClient: QueryClient) {
570
674
 
571
675
  ### Ожидать автоматическую отправку
572
676
 
573
- Capture только создаёт draft. Delivery нужно явно подключить.
677
+ Capture только создаёт draft. Приложение должно enqueue-ить его payload и один раз запустить API coordinator в client lifecycle.
574
678
 
575
- ### Использовать `createDiagnosticValue` как redactor
679
+ ### Считать built-in redaction полной privacy policy
576
680
 
577
- Он ограничивает размер, но сохраняет значения независимо от имени поля.
681
+ Foundation удаляет известные secret-like keys и email, но не умеет надёжно распознавать ФИО, телефоны и предметные IDs. Настройте app sanitizer по allow-list.
578
682
 
579
683
  ### Вызывать production capture в development и ждать draft
580
684
 
@@ -602,7 +706,9 @@ Capture только создаёт draft. Delivery нужно явно подк
602
706
  | ---------------- | --------------------------------------------------------------------------------------------------------------------------------- |
603
707
  | Capture | `captureQueryErrorReport`, `captureMutationErrorReport`, `captureRuntimeErrorReport`, `captureErrorReportDraft` |
604
708
  | Errors | `createErrorInfo` |
605
- | Safe values | `createDiagnosticValue`, `createDataShape`, `sanitizeDetail` |
709
+ | Safe values | `createDiagnosticValue`, `createDataShape`, `sanitizeDetail`, `sanitizeErrorReportValue`, `setErrorReportSanitizer` |
710
+ | Payload contract | `ERROR_REPORT_PAYLOAD_VERSION`, `ERROR_REPORT_PAYLOAD_MAX_BYTES`, `limitErrorReportPayload`, `getErrorReportPayloadSize`, parsers |
711
+ | Durable queue | `createErrorReportQueue`, queue limits, record/parser и atomic lease transitions |
606
712
  | Breadcrumbs | `addErrorReportBreadcrumb`, `getErrorReportBreadcrumbs`, `clearErrorReportBreadcrumbs`, `installErrorReportBrowserBreadcrumbs` |
607
713
  | Diagnostics | `collectQueryDiagnostics`, `collectMutationDiagnostics`, `collectQueryClientDiagnostics`, `collectPersistedQueryDiagnostics` |
608
714
  | Drafts | `getErrorReportDraft`, `getErrorReportDrafts`, `updateErrorReportDraft` |
@@ -13,6 +13,9 @@ import { Meta } from "@storybook/addon-docs/blocks";
13
13
 
14
14
  Используйте точный subpath. Внутренний файл `serviceWorker` не является самостоятельным публичным export, даже если отдельные функции используются самим hook.
15
15
 
16
+ `useBrowserOnlineStatus()` возвращает browser hint `navigator.onLine` через `useSyncExternalStore` и подписывается на события
17
+ `online`/`offline`. Это сигнал для UX и запуска revalidation, а не доказательство доступности конкретного backend.
18
+
16
19
  ## Часть 1. Обновление Service Worker
17
20
 
18
21
  ### Задача hook
@@ -378,6 +381,7 @@ Waiting worker остаётся установленным. Следующая
378
381
  | Экспорт | Публичный path | Назначение |
379
382
  | ----------------------------------- | -------------- | -------------------------------------------------- |
380
383
  | `useServiceWorkerUpdate()` | `/pwa` | Регистрация, проверка и UI-состояние обновления SW |
384
+ | `useBrowserOnlineStatus()` | `/pwa` | Реактивный browser online/offline hint |
381
385
  | `parseSwCachePolicy()` | оба | Разобрать текстовую политику |
382
386
  | `buildSwCachePolicyValue()` | оба | Собрать каноническую строку из draft |
383
387
  | `normalizeSwCacheName()` | оба | Подготовить безопасную часть cache name |
@@ -274,12 +274,14 @@ Stale не означает «удалена». Data может продолжа
274
274
  | Нужны данные только пока открыта вкладка? | Обычная query без `persistedQueryMeta`. |
275
275
  | Данные публичные, тяжёлые и могут пережить reload? | Рассмотреть `persistedQueryMeta`. |
276
276
  | Данные зависят от пользователя, роли или server session? | `sessionScopedQueryMeta`. |
277
+ | Session snapshot явно разрешено читать offline? | `createSessionQueryPersistenceMeta` + active scope. |
277
278
  | Нужно передать обновление другой открытой вкладке? | Broadcast installation и подходящее cache event. |
278
279
  | UI должен измениться до ответа mutation? | Optimistic update с обязательным rollback policy. |
279
280
  | Server response авторитетен и уже содержит новое состояние? | `setQueryData`/`onSuccessOptimistic`. |
280
281
  | После mutation нужен свежий server snapshot? | Scoped `invalidateQueries` с выбранным `refetchType`. |
281
282
 
282
- `persistedQueryMeta` и `sessionScopedQueryMeta` взаимоисключающие. Session data не должна переживать смену пользователя.
283
+ `persistedQueryMeta` и `sessionScopedQueryMeta` взаимоисключающие. Для узкого offline allowlist session data используется
284
+ отдельный `sessionPersistence` contract: он требует активную server-session identity и абсолютный `expiresAt`.
283
285
 
284
286
  ## Быстрый старт
285
287
 
@@ -489,7 +491,8 @@ queryOptions({
489
491
  });
490
492
  ```
491
493
 
492
- `Record<string, unknown>` разрешает consumer-проекту добавлять собственную metadata. Infrastructure этого модуля читает только `persist` и `sessionScoped`.
494
+ `Record<string, unknown>` разрешает consumer-проекту добавлять собственную metadata. Infrastructure этого модуля читает
495
+ `persist`, `sessionScoped` и явный `sessionPersistence` contract.
493
496
 
494
497
  ### `persistedQueryMeta`
495
498
 
@@ -533,6 +536,36 @@ queryOptions({
533
536
 
534
537
  При смене сессии host обязан вызвать `resetSessionScopedQueries`.
535
538
 
539
+ ### Session-scoped offline persistence
540
+
541
+ Только для явно согласованного offline allowlist создайте bounded metadata:
542
+
543
+ ```ts
544
+ import {
545
+ createSessionQueryPersistenceMeta,
546
+ setSessionQueryPersistenceScope
547
+ } from "@ryuzaki13/react-foundation-lib/query-client";
548
+
549
+ await setSessionQueryPersistenceScope({
550
+ id: session.id,
551
+ expiresAt: session.expiresAt
552
+ });
553
+
554
+ queryOptions({
555
+ queryKey: ["opened-week", weekStart],
556
+ queryFn: fetchWeek,
557
+ meta: createSessionQueryPersistenceMeta({
558
+ cacheName: "opened-weeks",
559
+ maxEntries: 24
560
+ }),
561
+ networkMode: "offlineFirst"
562
+ });
563
+ ```
564
+
565
+ Scope устанавливается отдельно от query contract, не содержит cookie secret/access token и меняет in-memory границу до очистки IndexedDB.
566
+ При `null`, истёкшем `expiresAt` или смене `id` старые session snapshots удаляются. `networkMode: "offlineFirst"` нужен, чтобы
567
+ TanStack вызвал persister до network pause. Cached data остаётся неавторитетным: write после reconnect обязан повторно получить серверный state.
568
+
536
569
  ### Meta нельзя совмещать
537
570
 
538
571
  Неверно:
@@ -550,13 +583,14 @@ meta: {
550
583
 
551
584
  - публичный/безопасный долгоживущий справочник → `persistedQueryMeta`;
552
585
  - auth/session dependent data → `sessionScopedQueryMeta`;
586
+ - разрешённый bounded offline snapshot → `createSessionQueryPersistenceMeta`;
553
587
  - обычные transient data → meta не нужна.
554
588
 
555
589
  ## Настройка project adapter
556
590
 
557
591
  ### Зачем нужен adapter
558
592
 
559
- Foundation-пакет не знает имя SAP system, client, tenant или окружение конкретного приложения. `QueryPersistenceProjectAdapter` позволяет host передать устойчивый namespace и собственный buster:
593
+ Foundation-пакет не знает имя SAP system, client, tenant или окружение конкретного приложения. `QueryPersistenceProjectAdapter` позволяет host передать устойчивый namespace и собственный buster. Текущая session identity задаётся отдельным runtime API и не входит в adapter:
560
594
 
561
595
  ```ts
562
596
  type QueryPersistenceProjectAdapter = {
@@ -778,7 +812,8 @@ Storage не скрывает ошибки. Promise отклоняется, ес
778
812
  const persister = createReactQueryPersister();
779
813
  ```
780
814
 
781
- Возвращает результат `experimental_createQueryPersister` либо `undefined`, если IndexedDB недоступен.
815
+ Возвращает routing-обёртку над `experimental_createQueryPersister` либо `undefined`, если IndexedDB недоступен. Обычные public query
816
+ идут в прежний persister, а query с `sessionPersistence` — в отдельный cache активной сессии с независимым `maxEntries`.
782
817
 
783
818
  Настройки foundation wrapper:
784
819
 
@@ -2356,9 +2391,11 @@ Profile tests подтверждают:
2356
2391
 
2357
2392
  | Symbol | Вид | Назначение |
2358
2393
  | ------------------------ | ----- | ----------------------------------------------- |
2359
- | `AppQueryMeta` | type | `persist`, `sessionScoped` и consumer metadata. |
2360
- | `persistedQueryMeta` | const | Opt-in public persistence marker. |
2361
- | `sessionScopedQueryMeta` | const | Session reset marker. |
2394
+ | `AppQueryMeta` | type | Public/session persistence и consumer metadata. |
2395
+ | `SessionQueryPersistencePolicy` | type | Имя и bounded limit session cache. |
2396
+ | `persistedQueryMeta` | const | Opt-in public persistence marker. |
2397
+ | `sessionScopedQueryMeta` | const | Session reset marker без browser persistence. |
2398
+ | `createSessionQueryPersistenceMeta` | function | Явный opt-in bounded persistence текущей server session. |
2362
2399
 
2363
2400
  ### Persistence project adapter
2364
2401
 
@@ -2377,6 +2414,10 @@ Profile tests подтверждают:
2377
2414
  | `shouldPersistQuery` | function | Opt-in predicate с session safety. |
2378
2415
  | `createIndexedDbQueryStorage` | function | AsyncStorage поверх IndexedDB. |
2379
2416
  | `createReactQueryPersister` | function | Настроенный per-query TanStack persister. |
2417
+ | `SessionQueryPersistenceScope` | type | Public session id и абсолютный expiresAt. |
2418
+ | `getSessionQueryPersistenceScope` | function | Читает валидный активный browser scope. |
2419
+ | `setSessionQueryPersistenceScope` | function | Меняет scope и очищает чужие snapshots. |
2420
+ | `clearSessionQueryPersistence` | function | Удаляет один или все session snapshots. |
2380
2421
 
2381
2422
  Options type `createIndexedDbQueryStorage` не экспортируется отдельным symbol; используйте структурный object или `Parameters<typeof createIndexedDbQueryStorage>[0]`.
2382
2423