@ryuzaki13/react-foundation-lib 2.4.0 → 2.6.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.
@@ -1 +1 @@
1
- {"version":3,"file":"persistence.d.ts","sourceRoot":"","sources":["../../src/query-client/persistence.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqC,KAAK,YAAY,EAAE,KAAK,cAAc,EAAE,MAAM,qCAAqC,CAAC;AAChI,OAAO,EAAE,KAAK,KAAK,EAAE,KAAK,aAAa,EAAE,KAAK,oBAAoB,EAAE,KAAK,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAOjH,eAAO,MAAM,8BAA8B,QAAqC,CAAC;AACjF,eAAO,MAAM,+BAA+B,QAA2B,CAAC;AAExE,KAAK,4BAA4B,GAAG;IACnC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,UAAU,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,4BAA4B,GAAG;IAC1C,6FAA6F;IAC7F,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,4FAA4F;IAC5F,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC3B,CAAC;AAIF;;;GAGG;AACH,MAAM,MAAM,6BAA6B,CAAC,aAAa,EAAE,OAAO,IAC7D;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;CAAE,GACrD;IAAE,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;CAAE,GACnF;IAAE,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;CAAE,CAAC;AAE3D,MAAM,MAAM,qBAAqB,CAAC,aAAa,IAAI,YAAY,CAAC,aAAa,CAAC,GAAG;IAChF,oFAAoF;IACpF,QAAQ,CAAC,OAAO,EAAE,MAAM,OAAO,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,CAAC;IAC5E;;;;;OAKG;IACH,QAAQ,CAAC,UAAU,EAAE,CAAC,OAAO,EAC5B,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,CAAC,KAAK,EAAE,aAAa,GAAG,SAAS,KAAK,6BAA6B,CAAC,aAAa,EAAE,OAAO,CAAC,KAC/F,OAAO,CAAC,OAAO,CAAC,CAAC;CACtB,CAAC;AAsDF,uGAAuG;AACvG,wBAAgB,+BAA+B,IAAI,4BAA4B,GAAG,IAAI,CAErF;AAUD,gFAAgF;AAChF,wBAAsB,4BAA4B,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAGlF;AAED;;;;GAIG;AACH,wBAAsB,+BAA+B,CAAC,KAAK,EAAE,4BAA4B,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAW/G;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,WAE5D;AAWD,wBAAgB,2BAA2B,CAAC,aAAa,GAAG,OAAO,EAClE,OAAO,GAAE,4BAAiC,GACxC,qBAAqB,CAAC,aAAa,CAAC,GAAG,SAAS,CA4HlD;AAED,wBAAgB,yBAAyB;gBA8DrB,CAAC,EAAE,SAAS,SAAS,QAAQ,WACrC,aAAa,CAAC,CAAC,EAAE,SAAS,CAAC,WAC3B,oBAAoB,CAAC,SAAS,CAAC,SACjC,KAAK,GACV,OAAO,CAAC,CAAC,CAAC;;;+DAlT2B,CAAC;;qFAGzC,CAAF;2BAGoC,CAAC;cAsTpC"}
1
+ {"version":3,"file":"persistence.d.ts","sourceRoot":"","sources":["../../src/query-client/persistence.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqC,KAAK,YAAY,EAAE,KAAK,cAAc,EAAE,MAAM,qCAAqC,CAAC;AAChI,OAAO,EAAE,KAAK,KAAK,EAAE,KAAK,aAAa,EAAE,KAAK,oBAAoB,EAAE,KAAK,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAOjH,eAAO,MAAM,8BAA8B,QAAqC,CAAC;AACjF,eAAO,MAAM,+BAA+B,QAA2B,CAAC;AAExE,KAAK,4BAA4B,GAAG;IACnC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,UAAU,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,4BAA4B,GAAG;IAC1C,6FAA6F;IAC7F,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,4FAA4F;IAC5F,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC3B,CAAC;AAIF;;;GAGG;AACH,MAAM,MAAM,6BAA6B,CAAC,aAAa,EAAE,OAAO,IAC7D;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;CAAE,GACrD;IAAE,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;CAAE,GACnF;IAAE,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;CAAE,CAAC;AAE3D,MAAM,MAAM,qBAAqB,CAAC,aAAa,IAAI,YAAY,CAAC,aAAa,CAAC,GAAG;IAChF,oFAAoF;IACpF,QAAQ,CAAC,OAAO,EAAE,MAAM,OAAO,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,CAAC;IAC5E;;;;;OAKG;IACH,QAAQ,CAAC,UAAU,EAAE,CAAC,OAAO,EAC5B,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,CAAC,KAAK,EAAE,aAAa,GAAG,SAAS,KAAK,6BAA6B,CAAC,aAAa,EAAE,OAAO,CAAC,KAC/F,OAAO,CAAC,OAAO,CAAC,CAAC;CACtB,CAAC;AAsDF,uGAAuG;AACvG,wBAAgB,+BAA+B,IAAI,4BAA4B,GAAG,IAAI,CAErF;AAUD,gFAAgF;AAChF,wBAAsB,4BAA4B,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAGlF;AAED;;;;GAIG;AACH,wBAAsB,+BAA+B,CAAC,KAAK,EAAE,4BAA4B,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAW/G;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,WAE5D;AAkCD,wBAAgB,2BAA2B,CAAC,aAAa,GAAG,OAAO,EAClE,OAAO,GAAE,4BAAiC,GACxC,qBAAqB,CAAC,aAAa,CAAC,GAAG,SAAS,CA4HlD;AAED,wBAAgB,yBAAyB;gBAgFrB,CAAC,EAAE,SAAS,SAAS,QAAQ,WACrC,aAAa,CAAC,CAAC,EAAE,SAAS,CAAC,WAC3B,oBAAoB,CAAC,SAAS,CAAC,SACjC,KAAK,GACV,OAAO,CAAC,CAAC,CAAC;;;+DA3V2B,CAAC;;qFAGzC,CAAF;2BAGoC,CAAC;cAqWpC"}
@@ -3,6 +3,17 @@ export type SessionQueryPersistencePolicy = {
3
3
  readonly cacheName: string;
4
4
  /** Максимальное число последних успешно полученных query в этом кэше. */
5
5
  readonly maxEntries: number;
6
+ /**
7
+ * По умолчанию восстановленный snapshot сразу перепроверяется полным queryFn.
8
+ * `false` допустим только при наличии отдельной лёгкой проверки актуальности.
9
+ */
10
+ readonly refetchOnRestore?: boolean;
11
+ /**
12
+ * Поле верхнего уровня query data с неотрицательной целочисленной ревизией.
13
+ * При наличии поля IndexedDB атомарно отклоняет запоздавшую запись меньшей
14
+ * ревизии, поэтому параллельная вкладка не может откатить сохранённый snapshot.
15
+ */
16
+ readonly monotonicRevisionField?: string;
6
17
  };
7
18
  /**
8
19
  * Общие признаки query, которые управляют инфраструктурой клиентского кеша.
@@ -38,6 +49,8 @@ export declare const sessionScopedQueryMeta: {
38
49
  export declare function createSessionQueryPersistenceMeta(policy: SessionQueryPersistencePolicy): {
39
50
  readonly sessionScoped: true;
40
51
  readonly sessionPersistence: {
52
+ readonly monotonicRevisionField?: string | undefined;
53
+ readonly refetchOnRestore?: boolean | undefined;
41
54
  readonly cacheName: string;
42
55
  readonly maxEntries: number;
43
56
  };
@@ -1 +1 @@
1
- {"version":3,"file":"queryMeta.d.ts","sourceRoot":"","sources":["../../src/query-client/queryMeta.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,6BAA6B,GAAG;IAC3C,+EAA+E;IAC/E,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,yEAAyE;IACzE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,MAAM,YAAY,GAAG;IAC1B,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;IACjC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,6BAA6B,CAAC;CAC5D,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAE5B,OAAO,QAAQ,uBAAuB,CAAC;IACtC,UAAU,QAAQ;QACjB,SAAS,EAAE,YAAY,CAAC;KACxB;CACD;AAED,sEAAsE;AACtE,eAAO,MAAM,kBAAkB;;CAAiD,CAAC;AAEjF,gEAAgE;AAChE,eAAO,MAAM,sBAAsB;;CAAuD,CAAC;AAE3F;;;;GAIG;AACH,wBAAgB,iCAAiC,CAAC,MAAM,EAAE,6BAA6B;;;;;;EAatF"}
1
+ {"version":3,"file":"queryMeta.d.ts","sourceRoot":"","sources":["../../src/query-client/queryMeta.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,6BAA6B,GAAG;IAC3C,+EAA+E;IAC/E,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,yEAAyE;IACzE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B;;;OAGG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,OAAO,CAAC;IACpC;;;;OAIG;IACH,QAAQ,CAAC,sBAAsB,CAAC,EAAE,MAAM,CAAC;CACzC,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,MAAM,YAAY,GAAG;IAC1B,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;IACjC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,6BAA6B,CAAC;CAC5D,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAE5B,OAAO,QAAQ,uBAAuB,CAAC;IACtC,UAAU,QAAQ;QACjB,SAAS,EAAE,YAAY,CAAC;KACxB;CACD;AAED,sEAAsE;AACtE,eAAO,MAAM,kBAAkB;;CAAiD,CAAC;AAEjF,gEAAgE;AAChE,eAAO,MAAM,sBAAsB;;CAAuD,CAAC;AAE3F;;;;GAIG;AACH,wBAAgB,iCAAiC,CAAC,MAAM,EAAE,6BAA6B;;;;;;;;EAyBtF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ryuzaki13/react-foundation-lib",
3
- "version": "2.4.0",
3
+ "version": "2.6.0",
4
4
  "description": "Reusable React and TypeScript foundation utilities, hooks, and primitives.",
5
5
  "license": "MIT",
6
6
  "author": "Ryuzaki13",
@@ -13,6 +13,7 @@ import { Meta } from "@storybook/addon-docs/blocks";
13
13
  - установить начальный фокус, зациклить `Tab` и восстановить прежний фокус;
14
14
  - узнать высоту элемента через `ResizeObserver`;
15
15
  - узнать, пересекается ли элемент с viewport или другим root;
16
+ - сохранить позицию чтения при вставке элементов выше текущего;
16
17
  - определить наличие touch/coarse-pointer возможностей.
17
18
 
18
19
  Модуль является низкоуровневым browser foundation-слоем. Он не создаёт готовые кнопки, dialog, modal, popover, tooltip или download UI. Визуальный компонент по-прежнему отвечает за семантический HTML, ARIA, стили, анимацию, стек overlay, сообщения об ошибках и пользовательский сценарий.
@@ -27,6 +28,7 @@ import { Meta } from "@storybook/addon-docs/blocks";
27
28
  - [Быстрый старт](#быстрый-старт)
28
29
  - [Скачивание файлов](#скачивание-файлов)
29
30
  - [`getOrCreatePortalRoot`](#getorcreateportalroot)
31
+ - [`captureScrollAnchor`](#capturescrollanchor)
30
32
  - [`useClickOutside`](#useclickoutside)
31
33
  - [`useEscapeDismiss`](#useescapedismiss)
32
34
  - [`useElementHeightObserver`](#useelementheightobserver)
@@ -103,6 +105,7 @@ npm install react react-dom
103
105
 
104
106
  ```ts
105
107
  import {
108
+ captureScrollAnchor,
106
109
  downloadFileFromBlob,
107
110
  downloadFileFromJson,
108
111
  downloadFileFromObjectURL,
@@ -272,6 +275,7 @@ Focus определяет, какой элемент получает keyboard
272
275
  | Скачать готовый `Blob` | `downloadFileFromBlob` |
273
276
  | Сериализовать значение в readable JSON и скачать | `downloadFileFromJson` |
274
277
  | Получить общий container для `createPortal` | `getOrCreatePortalRoot` |
278
+ | Сохранить позицию при вставке элементов сверху | `captureScrollAnchor` |
275
279
  | Закрыть UI по `mousedown` вне одного элемента | `useClickOutside` |
276
280
  | Считать trigger и portal content одной «внутренней» областью | `useClickOutside` с массивом refs |
277
281
  | Закрыть активный overlay по `Escape` | `useEscapeDismiss` |
@@ -668,6 +672,81 @@ getOrCreatePortalRoot("dialog-root"); // null, если document отсутст
668
672
 
669
673
  Не кешируйте этот `null` в server-loaded module и не ожидайте, что он автоматически станет HTMLElement на client. Portal target должен разрешаться на корректной browser lifecycle boundary.
670
674
 
675
+ ## `captureScrollAnchor`
676
+
677
+ Одноразовая компенсация сдвига сохраняемого DOM-элемента в обычном
678
+ вертикальном scroll-контейнере. Например, при добавлении более ранних
679
+ элементов в начало списка. Не запускает запросы и не зависит от React,
680
+ Query, cursor или предметной модели.
681
+
682
+ ```ts
683
+ declare function captureScrollAnchor(viewport: HTMLElement | null, anchor: HTMLElement | null): CapturedScrollAnchor | null;
684
+
685
+ type CapturedScrollAnchor = Readonly<{
686
+ restore: () => boolean;
687
+ release: () => void;
688
+ }>;
689
+ ```
690
+
691
+ Host выбирает сохраняемый элемент, захватывает его **до изменения DOM**,
692
+ вызывает `restore()` **после DOM commit**, а при отмене, ошибке или unmount —
693
+ `release()`. Разрешение Promise загрузки не гарантирует DOM commit: React-host
694
+ применяет восстановление в layout effect соответствующего обновления, а не
695
+ сразу после `await fetchNextPage()`.
696
+
697
+ ```ts
698
+ const pendingAnchor = captureScrollAnchor(viewportElement, retainedElement);
699
+ try {
700
+ // Здесь пример синхронного DOM commit, не React render.
701
+ listElement.prepend(olderElementsFragment);
702
+ pendingAnchor?.restore();
703
+ } finally {
704
+ pendingAnchor?.release();
705
+ }
706
+ ```
707
+
708
+ Операция сохраняет положение anchor в координатах содержимого контейнера.
709
+ Разницу после commit прибавляет к **текущему** `scrollTop`: ручная прокрутка
710
+ во время ожидания не отменяется. Компенсация мгновенная даже при CSS
711
+ `scroll-behavior: smooth`; горизонтальная позиция не меняется. При нулевом
712
+ сдвиге запись прокрутки не выполняется. Допустимый диапазон ограничивает
713
+ сам браузер.
714
+
715
+ На время операции `overflow-anchor` контейнера устанавливается в
716
+ `none!important`, чтобы браузер не выполнил вторую компенсацию. При завершении
717
+ возвращаются исходные inline-значение и priority либо удаляется отсутствовавшее
718
+ свойство. Внешнее изменение этого свойства отменяет компенсацию и не
719
+ перезаписывается: native anchoring уже мог возобновиться. Браузер без
720
+ поддержки `overflow-anchor` не требует отключения native anchoring.
721
+
722
+ На контейнер допускается один активный захват: новый корректный вызов
723
+ отменяет предыдущий без прокрутки. Его старый handle становится неактивным
724
+ и не может освободить стиль нового захвата. Разные контейнеры независимы.
725
+
726
+ `null` возвращается для отсутствующих или отключённых от документа элементов,
727
+ самого viewport в качестве anchor и элемента вне viewport. `restore()`
728
+ возвращает `true`, если anchor сохранился и операция выполнена, включая нулевой
729
+ сдвиг; `false` — если операция уже завершена либо DOM больше не подходит.
730
+ `release()` и повторный `restore()` безопасны. Временный стиль освобождается
731
+ и при ошибке browser API; ошибка не подавляется.
732
+ После завершения host отбрасывает handle: пока он хранится, его closure
733
+ содержит ссылки на захваченные DOM-элементы.
734
+
735
+ Границы: anchor должен сохранять DOM identity и обычное положение в потоке
736
+ без промежуточного независимо прокручиваемого контейнера. Не использовать
737
+ для window-scroll, горизонтальной или reverse-flex ленты, sticky/fixed anchor,
738
+ CSS scroll-snap и преобразования масштаба/поворота. Изменения layout после
739
+ завершения операции, например поздние изображения, не отслеживаются. API не
740
+ создаёт listeners, observers, timers или React state. Его импорт SSR-safe;
741
+ во время render вызов с реальными DOM-элементами запрещён.
742
+
743
+ Реальная геометрия, native anchoring, CSS smooth и browser clamp проверяются
744
+ в [browser fixture](./scroll-anchor/scroll-anchor.browser.html). Для повторения
745
+ запустить `npx vite dev --host 127.0.0.1 --port 6011 --strictPort`, открыть
746
+ `http://127.0.0.1:6011/src/dom/scroll-anchor/scroll-anchor.browser.html` и нажать
747
+ «Запустить проверки». Результаты и измерения выводятся на странице; этот
748
+ dev-only сценарий дополняет unit-тесты, а не заменяет проверку React-host.
749
+
671
750
  ## `useClickOutside`
672
751
 
673
752
  Вызывает handler на `mousedown`, если target находится вне одного ref или вне всех refs из массива.
@@ -2055,6 +2134,7 @@ Touch-capable laptop не обязательно mobile. Не связывайт
2055
2134
  | `downloadFileFromBlob` | filename, `Blob` | `void` | Сам создаёт/revokes URL | Browser-only, ошибки синхронные. |
2056
2135
  | `downloadFileFromJson` | filename, `unknown` | `void` | JSON с 2 spaces, UTF-8 MIME | Наследует ошибки и потери `JSON.stringify`. |
2057
2136
  | `getOrCreatePortalRoot` | `id: string` | `HTMLElement \| null` | Создаёт `div` в body | DOM mutation; root не удаляется. |
2137
+ | `captureScrollAnchor` | viewport, сохраняемый элемент | `CapturedScrollAnchor \| null` | Один активный захват на viewport | Обычный вертикальный DOM flow; restore после commit. |
2058
2138
  | `useClickOutside` | один ref/массив refs, handler | `void` | Global bubbling `mousedown` | Нет enabled/event arg/custom document. |
2059
2139
  | `useEscapeDismiss` | options object | `void` | enabled=true, capture `keydown` | Не управляет stack/focus; defaultPrevented игнорируется. |
2060
2140
  | `useElementHeightObserver` | element ref | `number` | Initial `0` | ContentRect, требует ResizeObserver. |
@@ -541,10 +541,7 @@ queryOptions({
541
541
  Только для явно согласованного offline allowlist создайте bounded metadata:
542
542
 
543
543
  ```ts
544
- import {
545
- createSessionQueryPersistenceMeta,
546
- setSessionQueryPersistenceScope
547
- } from "@ryuzaki13/react-foundation-lib/query-client";
544
+ import { createSessionQueryPersistenceMeta, setSessionQueryPersistenceScope } from "@ryuzaki13/react-foundation-lib/query-client";
548
545
 
549
546
  await setSessionQueryPersistenceScope({
550
547
  id: session.id,
@@ -556,7 +553,8 @@ queryOptions({
556
553
  queryFn: fetchWeek,
557
554
  meta: createSessionQueryPersistenceMeta({
558
555
  cacheName: "opened-weeks",
559
- maxEntries: 24
556
+ maxEntries: 24,
557
+ refetchOnRestore: true
560
558
  }),
561
559
  networkMode: "offlineFirst"
562
560
  });
@@ -566,6 +564,25 @@ Scope устанавливается отдельно от query contract, не
566
564
  При `null`, истёкшем `expiresAt` или смене `id` старые session snapshots удаляются. `networkMode: "offlineFirst"` нужен, чтобы
567
565
  TanStack вызвал persister до network pause. Cached data остаётся неавторитетным: write после reconnect обязан повторно получить серверный state.
568
566
 
567
+ Если consumer имеет отдельный лёгкий checker актуальности, полный запрос после
568
+ restore можно отключить. Для revisioned snapshot передайте поле верхнего уровня
569
+ с decimal string revision:
570
+
571
+ ```ts
572
+ meta: createSessionQueryPersistenceMeta({
573
+ cacheName: "reference-data",
574
+ maxEntries: 16,
575
+ refetchOnRestore: false,
576
+ monotonicRevisionField: "revision"
577
+ });
578
+ ```
579
+
580
+ `refetchOnRestore: false` без внешнего checker оставляет consumer ответственным
581
+ за бесконечно устаревшие данные. `monotonicRevisionField` не создаёт новые
582
+ storage keys: запись того же query key обновляется атомарно, а меньшая decimal
583
+ revision из запоздавшей вкладки не заменяет уже сохранённую большую. Если поле
584
+ отсутствует или не является строкой из цифр, запись использует обычную policy.
585
+
569
586
  ### Meta нельзя совмещать
570
587
 
571
588
  Неверно:
@@ -824,7 +841,7 @@ const persister = createReactQueryPersister();
824
841
  prefix: systemIdentifier,
825
842
  buster: adapterBuster ?? buildTimeBuster,
826
843
  maxAge: 90 * 24 * 60 * 60 * 1000,
827
- refetchOnRestore: true,
844
+ refetchOnRestore: true, // либо session policy конкретного cache
828
845
  filters: { predicate: shouldPersistQuery }
829
846
  }
830
847
  ```
@@ -894,7 +911,8 @@ deserialize: (query) => query;
894
911
  3. По `prefix-queryHash` читается IndexedDB.
895
912
  4. Проверяются buster и возраст записи.
896
913
  5. Подходящее state восстанавливается в Query cache.
897
- 6. Если восстановленная query stale, `refetchOnRestore: true` запускает обновление.
914
+ 6. Если восстановленная query stale, effective `refetchOnRestore` запускает
915
+ обновление; session cache может отключить его при наличии внешнего checker.
898
916
  7. Если записи нет/она протухла/не совпал buster, выполняется обычный `queryFn`.
899
917
  8. Успешный результат планируется к записи в IndexedDB.
900
918
 
@@ -2389,12 +2407,12 @@ Profile tests подтверждают:
2389
2407
 
2390
2408
  ### Query metadata
2391
2409
 
2392
- | Symbol | Вид | Назначение |
2393
- | ------------------------ | ----- | ----------------------------------------------- |
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. |
2410
+ | Symbol | Вид | Назначение |
2411
+ | ----------------------------------- | -------- | -------------------------------------------------------- |
2412
+ | `AppQueryMeta` | type | Public/session persistence и consumer metadata. |
2413
+ | `SessionQueryPersistencePolicy` | type | Имя и bounded limit session cache. |
2414
+ | `persistedQueryMeta` | const | Opt-in public persistence marker. |
2415
+ | `sessionScopedQueryMeta` | const | Session reset marker без browser persistence. |
2398
2416
  | `createSessionQueryPersistenceMeta` | function | Явный opt-in bounded persistence текущей server session. |
2399
2417
 
2400
2418
  ### Persistence project adapter
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/dom/downloadFile.ts","../../src/dom/getOrCreatePortalRoot.ts","../../src/dom/useClickOutside.ts","../../src/dom/useElementHeightObserver.ts","../../src/dom/useEscapeDismiss.ts","../../src/dom/useOverlayFocus.ts","../../src/dom/useFocusTrap.ts","../../src/dom/useIntersectionObserver.ts","../../src/dom/useIsTouchDevice.ts"],"sourcesContent":["export function downloadFileFromObjectURL(filename: string, objectUrl: string) {\n\tconst link = document.createElement(\"a\");\n\n\tlink.href = objectUrl;\n\tlink.download = filename;\n\tlink.click();\n\n\twindow.setTimeout(() => {\n\t\twindow.URL.revokeObjectURL(objectUrl);\n\t}, 0);\n}\n\nexport function downloadFileFromBlob(filename: string, blob: Blob) {\n\tconst objectUrl = window.URL.createObjectURL(blob);\n\tdownloadFileFromObjectURL(filename, objectUrl);\n}\n\nexport function downloadFileFromJson(filename: string, payload: unknown) {\n\tconst blob = new Blob([JSON.stringify(payload, null, 2)], { type: \"application/json;charset=utf-8\" });\n\tdownloadFileFromBlob(filename, blob);\n}\n","export function getOrCreatePortalRoot(id: string): HTMLElement | null {\n\tif (typeof document === \"undefined\") {\n\t\treturn null;\n\t}\n\n\tconst existingElement = document.getElementById(id);\n\tif (existingElement instanceof HTMLElement) {\n\t\treturn existingElement;\n\t}\n\n\tconst portalRoot = document.createElement(\"div\");\n\tportalRoot.id = id;\n\tdocument.body.append(portalRoot);\n\n\treturn portalRoot;\n}\n","import { RefObject, useEffect } from \"react\";\n\nexport function useClickOutside(ref: RefObject<HTMLElement | null> | RefObject<HTMLElement | null>[], handler: () => void) {\n\tuseEffect(() => {\n\t\tconst handleClickOutside = (event: MouseEvent) => {\n\t\t\tif (Array.isArray(ref)) {\n\t\t\t\tlet isOutside = true;\n\t\t\t\tfor (const r of ref) {\n\t\t\t\t\tif (r.current && r.current.contains(event.target as Node)) {\n\t\t\t\t\t\tisOutside = false;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (isOutside) {\n\t\t\t\t\thandler();\n\t\t\t\t}\n\t\t\t} else if (ref.current && !ref.current.contains(event.target as Node)) {\n\t\t\t\thandler();\n\t\t\t}\n\t\t};\n\n\t\tdocument.addEventListener(\"mousedown\", handleClickOutside);\n\t\treturn () => document.removeEventListener(\"mousedown\", handleClickOutside);\n\t}, [ref, handler]);\n}\n","import { type RefObject, useLayoutEffect, useState } from \"react\";\n\nexport function useElementHeightObserver<TElement extends HTMLElement>(elementRef: RefObject<TElement | null>) {\n\tconst [height, setHeight] = useState(0);\n\n\tuseLayoutEffect(() => {\n\t\tconst element = elementRef.current;\n\t\tif (!element) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst observer = new ResizeObserver((entries) => {\n\t\t\tconst nextHeight = entries[0]?.contentRect.height ?? element.getBoundingClientRect().height;\n\t\t\tsetHeight(nextHeight);\n\t\t});\n\t\tobserver.observe(element);\n\n\t\treturn () => observer.disconnect();\n\t\t// eslint-disable-next-line react-hooks/exhaustive-deps, react-hooks/refs\n\t}, [elementRef.current]);\n\n\treturn height;\n}\n","import { RefObject, useEffect, useEffectEvent } from \"react\";\n\ninterface UseEscapeDismissOptions {\n\tactive: boolean;\n\tonDismiss: () => void;\n\tenabled?: boolean;\n\tdocumentTarget?: Document | null;\n\tcontainerRef?: RefObject<HTMLElement | null>;\n}\n\n/**\n * Подписывает overlay на закрытие по клавише Escape.\n */\nexport function useEscapeDismiss({ active, onDismiss, enabled = true, documentTarget, containerRef }: UseEscapeDismissOptions) {\n\tconst dismiss = useEffectEvent(() => onDismiss());\n\n\tuseEffect(() => {\n\t\tif (!active || !enabled) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst targetDocument = documentTarget ?? (typeof document === \"undefined\" ? null : document);\n\t\tif (!targetDocument) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst handleKeyDown = (event: KeyboardEvent) => {\n\t\t\tif (event.key !== \"Escape\" /*|| event.defaultPrevented*/) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst activeElement = targetDocument.activeElement;\n\t\t\tconst container = containerRef?.current;\n\t\t\tif (container && activeElement instanceof Node && activeElement !== container && !container.contains(activeElement)) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tevent.preventDefault();\n\t\t\tdismiss();\n\t\t};\n\n\t\ttargetDocument.addEventListener(\"keydown\", handleKeyDown, true);\n\t\treturn () => targetDocument.removeEventListener(\"keydown\", handleKeyDown, true);\n\t}, [active, enabled, documentTarget, containerRef]);\n}\n","import { RefObject, useEffect, useEffectEvent, useRef } from \"react\";\n\nconst focusableElementsSelector =\n\t'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"]):not([disabled])';\n\nexport interface UseOverlayFocusOptions<T extends HTMLElement> {\n\tactive: boolean;\n\ttrapFocus?: boolean;\n\trestoreFocus?: boolean;\n\tinitialFocus?: \"auto\" | \"container\" | ((container: T) => HTMLElement | null | undefined);\n\trestoreFocusTarget?: () => HTMLElement | null | undefined;\n\tcontainerRef?: RefObject<T | null>;\n}\n\n/**\n * Возвращает ref overlay-контейнера и управляет начальными/возвратными переходами фокуса.\n * Открытие и закрытие не прокручивают документ: портал может получить фокус\n * до завершения позиционирования. Пользовательский Tab по-прежнему раскрывает\n * выбранный элемент в прокручиваемом содержимом overlay.\n */\nexport function useOverlayFocus<T extends HTMLElement>({\n\tactive,\n\ttrapFocus = false,\n\trestoreFocus = true,\n\tinitialFocus = \"auto\",\n\trestoreFocusTarget,\n\tcontainerRef\n}: UseOverlayFocusOptions<T>) {\n\tconst internalOverlayRef = useRef<T | null>(null);\n\tconst overlayRef = containerRef ?? internalOverlayRef;\n\tconst previousActiveElementRef = useRef<HTMLElement | null>(null);\n\n\t/**\n\t * Собирает фокусируемые элементы overlay в актуальном DOM-порядке.\n\t */\n\tconst getFocusableElements = useEffectEvent((container: T) =>\n\t\tArray.from(container.querySelectorAll<HTMLElement>(focusableElementsSelector)).filter(\n\t\t\t(element) => !element.hasAttribute(\"aria-hidden\")\n\t\t)\n\t);\n\n\t/**\n\t * Вычисляет стартовый фокус при открытии overlay.\n\t */\n\tconst resolveInitialFocus = useEffectEvent((container: T) => {\n\t\tif (initialFocus === \"container\") {\n\t\t\treturn container;\n\t\t}\n\n\t\tif (typeof initialFocus === \"function\") {\n\t\t\treturn initialFocus(container) ?? container;\n\t\t}\n\n\t\tconst focusableElements = getFocusableElements(container);\n\t\treturn (\n\t\t\tfocusableElements.find((element) => element.hasAttribute(\"autofocus\") || element.hasAttribute(\"autoFocus\")) ??\n\t\t\tfocusableElements[0] ??\n\t\t\tcontainer\n\t\t);\n\t});\n\n\t/**\n\t * Вычисляет элемент, которому нужно вернуть фокус после закрытия overlay.\n\t */\n\tconst resolveRestoreFocusTarget = useEffectEvent(() => restoreFocusTarget?.() ?? previousActiveElementRef.current);\n\n\tuseEffect(() => {\n\t\tif (!active) {\n\t\t\treturn;\n\t\t}\n\n\t\tpreviousActiveElementRef.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;\n\t\tlet overlayElement: T | null = null;\n\t\tlet rafId = 0;\n\n\t\t/**\n\t\t * Цикл откладывает инициализацию, пока ref контейнера не будет привязан к DOM.\n\t\t */\n\t\tconst attachOverlay = () => {\n\t\t\toverlayElement = overlayRef.current;\n\t\t\tif (!overlayElement) {\n\t\t\t\trafId = window.requestAnimationFrame(attachOverlay);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst focusTarget = resolveInitialFocus(overlayElement);\n\t\t\tif (focusTarget instanceof HTMLElement && focusTarget.isConnected) {\n\t\t\t\tfocusTarget.focus({ preventScroll: true });\n\t\t\t}\n\n\t\t\tif (trapFocus) {\n\t\t\t\toverlayElement.addEventListener(\"keydown\", handleTabKey);\n\t\t\t}\n\t\t};\n\n\t\t/**\n\t\t * Зацикливает Tab внутри overlay, пока он открыт как модальный слой.\n\t\t */\n\t\tconst handleTabKey = (event: KeyboardEvent) => {\n\t\t\tif (!trapFocus || event.key !== \"Tab\" || !overlayElement) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst focusableElements = getFocusableElements(overlayElement);\n\t\t\tif (focusableElements.length === 0) {\n\t\t\t\tevent.preventDefault();\n\t\t\t\toverlayElement.focus();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst firstElement = focusableElements[0];\n\t\t\tconst lastElement = focusableElements[focusableElements.length - 1];\n\t\t\tconst activeElement = document.activeElement;\n\n\t\t\tif (event.shiftKey) {\n\t\t\t\tif (activeElement === firstElement || activeElement === overlayElement) {\n\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\tlastElement.focus();\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (activeElement === lastElement) {\n\t\t\t\tevent.preventDefault();\n\t\t\t\tfirstElement.focus();\n\t\t\t}\n\t\t};\n\n\t\tattachOverlay();\n\n\t\treturn () => {\n\t\t\twindow.cancelAnimationFrame(rafId);\n\t\t\tif (trapFocus && overlayElement) {\n\t\t\t\toverlayElement.removeEventListener(\"keydown\", handleTabKey);\n\t\t\t}\n\n\t\t\tif (!restoreFocus) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst restoreTarget = resolveRestoreFocusTarget();\n\t\t\tif (restoreTarget?.isConnected) {\n\t\t\t\trestoreTarget.focus({ preventScroll: true });\n\t\t\t}\n\t\t};\n\t}, [active, trapFocus, restoreFocus, overlayRef]);\n\n\treturn overlayRef;\n}\n","import { useOverlayFocus } from \"./useOverlayFocus\";\n\n/**\n * Совместимый wrapper для overlay с фокус-ловушкой.\n */\nexport const useFocusTrap = (isOpen: boolean) => {\n\treturn useOverlayFocus<HTMLDivElement>({ active: isOpen, trapFocus: true, restoreFocus: true, initialFocus: \"auto\" });\n};\n","import { useEffect, useState } from \"react\";\n\nexport function useIntersectionObserver(ref: React.RefObject<Element | null>, options?: IntersectionObserverInit) {\n\tconst [isIntersecting, setIsIntersecting] = useState(false);\n\n\tuseEffect(() => {\n\t\tconst el = ref.current;\n\t\tif (!el) return;\n\n\t\tconst observer = new IntersectionObserver(([entry]) => {\n\t\t\tsetIsIntersecting(entry.isIntersecting);\n\t\t}, options);\n\n\t\tobserver.observe(el);\n\n\t\treturn () => {\n\t\t\tif (el) {\n\t\t\t\tobserver.unobserve(el);\n\t\t\t}\n\t\t\tobserver.disconnect();\n\t\t};\n\t}, [options, ref]); // не забыть, что ref.current может меняться\n\n\treturn { isIntersecting };\n}\n","import { useEffect, useState } from \"react\";\n\nexport const useIsTouchDevice = () => {\n\tconst [isTouchDevice, setIsTouchDevice] = useState(false);\n\n\tuseEffect(() => {\n\t\tconst checkTouchDevice = () => {\n\t\t\tconst isTouch = window.matchMedia(\"(pointer: coarse)\").matches || \"ontouchstart\" in window || navigator.maxTouchPoints > 0;\n\t\t\tsetIsTouchDevice(isTouch);\n\t\t};\n\n\t\tcheckTouchDevice();\n\n\t\t// Обработчик для изменений в типе устройства\n\t\twindow.addEventListener(\"resize\", checkTouchDevice);\n\n\t\treturn () => {\n\t\t\twindow.removeEventListener(\"resize\", checkTouchDevice);\n\t\t};\n\t}, []);\n\n\treturn isTouchDevice;\n};\n"],"mappings":"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"}