@ryuzaki13/react-foundation-lib 2.15.0 → 2.16.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,2 +1,3 @@
1
- export * from "./useFetchNextPageEffect";
1
+ export { useFetchNextPageEffect } from "./useFetchNextPageEffect";
2
+ export { type UseFetchNextPageEffectOptions } from "./useFetchNextPageEffectTypes";
2
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/virtualizer/index.ts"],"names":[],"mappings":"AAAA,cAAc,0BAA0B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/virtualizer/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,EAAE,KAAK,6BAA6B,EAAE,MAAM,+BAA+B,CAAC"}
@@ -1,16 +1,28 @@
1
1
  import { useEffect as e, useRef as t } from "react";
2
2
  //#region src/virtualizer/useFetchNextPageEffect.ts
3
- function n({ virtualItems: n, currentItemsCount: r, fetchNextPage: i, hasNextPage: a }) {
4
- let o = t(!1);
5
- e(() => {
6
- !n.length || !r || n[n.length - 1].index >= r - 1 && a && !o.current && (o.current = !0, i?.().finally(() => {
7
- o.current = !1;
3
+ function n({ virtualItems: n, currentItemsCount: r, fetchNextPage: i, hasNextPage: a, onError: o }) {
4
+ let s = t(!1), c = t(null), l = t(!1);
5
+ e(() => (l.current = !0, () => {
6
+ l.current = !1;
7
+ }), []), e(() => {
8
+ (!a || c.current !== r) && (c.current = null), !(!n.length || !r || !a || s.current || c.current === r) && n[n.length - 1].index >= r - 1 && (s.current = !0, Promise.resolve().then(i).then(() => {
9
+ s.current = !1;
10
+ }, async (e) => {
11
+ c.current = r;
12
+ try {
13
+ l.current && o ? await o(e) : console.error("Не удалось загрузить следующую страницу виртуализированного списка.", e);
14
+ } catch (t) {
15
+ console.error("Не удалось обработать ошибку дозагрузки виртуализированного списка.", e, t);
16
+ } finally {
17
+ s.current = !1;
18
+ }
8
19
  }));
9
20
  }, [
10
21
  n,
11
22
  r,
12
23
  a,
13
- i
24
+ i,
25
+ o
14
26
  ]);
15
27
  }
16
28
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/virtualizer/useFetchNextPageEffect.ts"],"sourcesContent":["import { useEffect, useRef } from \"react\";\n\nimport { VirtualItem } from \"@tanstack/react-virtual\";\n\nexport function useFetchNextPageEffect({\n\tvirtualItems,\n\tcurrentItemsCount,\n\tfetchNextPage,\n\thasNextPage\n}: {\n\tvirtualItems: VirtualItem[];\n\tcurrentItemsCount: number;\n\thasNextPage: boolean;\n\tfetchNextPage: () => Promise<unknown>;\n}) {\n\tconst fetchingRef = useRef(false);\n\n\tuseEffect(() => {\n\t\tif (!virtualItems.length || !currentItemsCount) return;\n\t\tconst lastVirtual = virtualItems[virtualItems.length - 1];\n\n\t\tif (lastVirtual.index >= currentItemsCount - 1 && hasNextPage && !fetchingRef.current) {\n\t\t\tfetchingRef.current = true;\n\t\t\tfetchNextPage?.().finally(() => {\n\t\t\t\tfetchingRef.current = false;\n\t\t\t});\n\t\t}\n\t}, [virtualItems, currentItemsCount, hasNextPage, fetchNextPage]);\n}\n"],"mappings":";;AAIA,SAAgB,EAAuB,EACtC,iBACA,sBACA,kBACA,kBAME;CACF,IAAM,IAAc,EAAO,EAAK;CAEhC,QAAgB;EACX,CAAC,EAAa,UAAU,CAAC,KACT,EAAa,EAAa,SAAS,EAEnD,CAAY,SAAS,IAAoB,KAAK,KAAe,CAAC,EAAY,YAC7E,EAAY,UAAU,IACtB,IAAgB,CAAC,CAAC,cAAc;GAC/B,EAAY,UAAU;EACvB,CAAC;CAEH,GAAG;EAAC;EAAc;EAAmB;EAAa;CAAa,CAAC;AACjE"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/virtualizer/useFetchNextPageEffect.ts"],"sourcesContent":["import { useEffect, useRef } from \"react\";\n\nimport { type UseFetchNextPageEffectOptions } from \"./useFetchNextPageEffectTypes\";\n\n/**\n * Достигнутый край запускает одну дозагрузку. Отказ не превращается в detached\n * rejection и не повторяется из-за новых virtual items при каждом scroll/render.\n */\nexport function useFetchNextPageEffect({\n\tvirtualItems,\n\tcurrentItemsCount,\n\tfetchNextPage,\n\thasNextPage,\n\tonError\n}: UseFetchNextPageEffectOptions): void {\n\tconst fetchingRef = useRef(false);\n\tconst failedItemsCountRef = useRef<number | null>(null);\n\tconst mountedRef = useRef(false);\n\n\tuseEffect(() => {\n\t\tmountedRef.current = true;\n\t\treturn () => {\n\t\t\t// Transport принадлежит caller и не отменяется; поздний отказ остаётся\n\t\t\t// диагностикой, но уже не вызывает callback размонтированного UI.\n\t\t\tmountedRef.current = false;\n\t\t};\n\t}, []);\n\n\tuseEffect(() => {\n\t\t// Новые данные либо явное выключение pagination разрешают следующую\n\t\t// попытку. Identity callback и видимого диапазона не являются retry.\n\t\tif (!hasNextPage || failedItemsCountRef.current !== currentItemsCount) failedItemsCountRef.current = null;\n\t\tif (\n\t\t\t!virtualItems.length ||\n\t\t\t!currentItemsCount ||\n\t\t\t!hasNextPage ||\n\t\t\tfetchingRef.current ||\n\t\t\tfailedItemsCountRef.current === currentItemsCount\n\t\t)\n\t\t\treturn;\n\t\tconst lastVirtual = virtualItems[virtualItems.length - 1];\n\n\t\tif (lastVirtual.index >= currentItemsCount - 1) {\n\t\t\tfetchingRef.current = true;\n\t\t\t// Начало через Promise охватывает и синхронный throw transport callback.\n\t\t\t// Обе ветки завершения обработаны: finally не создаёт второй rejected Promise.\n\t\t\tvoid Promise.resolve()\n\t\t\t\t.then(fetchNextPage)\n\t\t\t\t.then(\n\t\t\t\t\t() => {\n\t\t\t\t\t\tfetchingRef.current = false;\n\t\t\t\t\t},\n\t\t\t\t\tasync (error: unknown) => {\n\t\t\t\t\t\tfailedItemsCountRef.current = currentItemsCount;\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tif (mountedRef.current && onError) await onError(error);\n\t\t\t\t\t\t\telse console.error(\"Не удалось загрузить следующую страницу виртуализированного списка.\", error);\n\t\t\t\t\t\t} catch (reporterError: unknown) {\n\t\t\t\t\t\t\tconsole.error(\"Не удалось обработать ошибку дозагрузки виртуализированного списка.\", error, reporterError);\n\t\t\t\t\t\t} finally {\n\t\t\t\t\t\t\tfetchingRef.current = false;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t);\n\t\t}\n\t}, [virtualItems, currentItemsCount, hasNextPage, fetchNextPage, onError]);\n}\n"],"mappings":";;AAQA,SAAgB,EAAuB,EACtC,iBACA,sBACA,kBACA,gBACA,cACuC;CACvC,IAAM,IAAc,EAAO,EAAK,GAC1B,IAAsB,EAAsB,IAAI,GAChD,IAAa,EAAO,EAAK;CAW/B,AATA,SACC,EAAW,UAAU,UACR;EAGZ,EAAW,UAAU;CACtB,IACE,CAAC,CAAC,GAEL,QAAgB;EAGf,CAAI,CAAC,KAAe,EAAoB,YAAY,OAAmB,EAAoB,UAAU,OAEpG,GAAC,EAAa,UACd,CAAC,KACD,CAAC,KACD,EAAY,WACZ,EAAoB,YAAY,MAGb,EAAa,EAAa,SAAS,EAEnD,CAAY,SAAS,IAAoB,MAC5C,EAAY,UAAU,IAGtB,QAAa,QAAQ,CAAC,CACpB,KAAK,CAAa,CAAC,CACnB,WACM;GACL,EAAY,UAAU;EACvB,GACA,OAAO,MAAmB;GACzB,EAAoB,UAAU;GAC9B,IAAI;IACH,AAAI,EAAW,WAAW,IAAS,MAAM,EAAQ,CAAK,IACjD,QAAQ,MAAM,uEAAuE,CAAK;GAChG,SAAS,GAAwB;IAChC,QAAQ,MAAM,uEAAuE,GAAO,CAAa;GAC1G,UAAU;IACT,EAAY,UAAU;GACvB;EACD,CACD;CAEH,GAAG;EAAC;EAAc;EAAmB;EAAa;EAAe;CAAO,CAAC;AAC1E"}
@@ -1,8 +1,7 @@
1
- import { VirtualItem } from "@tanstack/react-virtual";
2
- export declare function useFetchNextPageEffect({ virtualItems, currentItemsCount, fetchNextPage, hasNextPage }: {
3
- virtualItems: VirtualItem[];
4
- currentItemsCount: number;
5
- hasNextPage: boolean;
6
- fetchNextPage: () => Promise<unknown>;
7
- }): void;
1
+ import { type UseFetchNextPageEffectOptions } from "./useFetchNextPageEffectTypes";
2
+ /**
3
+ * Достигнутый край запускает одну дозагрузку. Отказ не превращается в detached
4
+ * rejection и не повторяется из-за новых virtual items при каждом scroll/render.
5
+ */
6
+ export declare function useFetchNextPageEffect({ virtualItems, currentItemsCount, fetchNextPage, hasNextPage, onError }: UseFetchNextPageEffectOptions): void;
8
7
  //# sourceMappingURL=useFetchNextPageEffect.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"useFetchNextPageEffect.d.ts","sourceRoot":"","sources":["../../src/virtualizer/useFetchNextPageEffect.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAEtD,wBAAgB,sBAAsB,CAAC,EACtC,YAAY,EACZ,iBAAiB,EACjB,aAAa,EACb,WAAW,EACX,EAAE;IACF,YAAY,EAAE,WAAW,EAAE,CAAC;IAC5B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,WAAW,EAAE,OAAO,CAAC;IACrB,aAAa,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;CACtC,QAcA"}
1
+ {"version":3,"file":"useFetchNextPageEffect.d.ts","sourceRoot":"","sources":["../../src/virtualizer/useFetchNextPageEffect.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,6BAA6B,EAAE,MAAM,+BAA+B,CAAC;AAEnF;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,EACtC,YAAY,EACZ,iBAAiB,EACjB,aAAa,EACb,WAAW,EACX,OAAO,EACP,EAAE,6BAA6B,GAAG,IAAI,CAoDtC"}
@@ -0,0 +1,11 @@
1
+ import { type VirtualItem } from "@tanstack/react-virtual";
2
+ /** Дозагрузка не владеет данными и retry: caller передаёт состояние своей пагинации. */
3
+ export type UseFetchNextPageEffectOptions = Readonly<{
4
+ virtualItems: readonly VirtualItem[];
5
+ currentItemsCount: number;
6
+ hasNextPage: boolean;
7
+ fetchNextPage: () => Promise<unknown>;
8
+ /** Ошибка передаётся один раз; без callback остаётся явная console-диагностика. */
9
+ onError?: (error: unknown) => void | Promise<void>;
10
+ }>;
11
+ //# sourceMappingURL=useFetchNextPageEffectTypes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useFetchNextPageEffectTypes.d.ts","sourceRoot":"","sources":["../../src/virtualizer/useFetchNextPageEffectTypes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE3D,wFAAwF;AACxF,MAAM,MAAM,6BAA6B,GAAG,QAAQ,CAAC;IACpD,YAAY,EAAE,SAAS,WAAW,EAAE,CAAC;IACrC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,WAAW,EAAE,OAAO,CAAC;IACrB,aAAa,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IACtC,mFAAmF;IACnF,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnD,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ryuzaki13/react-foundation-lib",
3
- "version": "2.15.0",
3
+ "version": "2.16.0",
4
4
  "description": "Reusable React and TypeScript foundation utilities, hooks, and primitives.",
5
5
  "license": "MIT",
6
6
  "author": "Ryuzaki13",
@@ -36,16 +36,18 @@ useFetchNextPageEffect({
36
36
  virtualItems,
37
37
  currentItemsCount,
38
38
  fetchNextPage,
39
- hasNextPage
39
+ hasNextPage,
40
+ onError
40
41
  });
41
42
  ```
42
43
 
43
- | Поле | Значение |
44
- | ------------------- | ------------------------------------ |
45
- | `virtualItems` | Текущий массив `VirtualItem[]` |
46
- | `currentItemsCount` | Число реально загруженных data items |
47
- | `hasNextPage` | Есть ли следующая server page |
48
- | `fetchNextPage` | Stable функция, возвращающая Promise |
44
+ | Поле | Значение |
45
+ | ------------------- | ------------------------------------------------------------------- |
46
+ | `virtualItems` | Текущий массив `readonly VirtualItem[]` |
47
+ | `currentItemsCount` | Число реально загруженных data items |
48
+ | `hasNextPage` | Есть ли следующая server page |
49
+ | `fetchNextPage` | Stable функция, возвращающая Promise |
50
+ | `onError` | Необязательный callback `(error: unknown) => void \| Promise<void>` |
49
51
 
50
52
  Hook ничего не возвращает.
51
53
 
@@ -81,25 +83,41 @@ Trigger происходит строго на последнем загруже
81
83
 
82
84
  ## Защита от параллельного fetch
83
85
 
84
- Внутренний `useRef(false)` устанавливается до вызова и сбрасывается в `.finally()`:
86
+ Внутренний single-flight ref устанавливается до вызова и сбрасывается после
87
+ успеха либо завершения обработки ошибки:
85
88
 
86
89
  ```text
87
90
  idle ── trigger ──► fetchingRef=true
88
91
  │
89
92
  Promise pending
90
93
  │
91
- resolve или reject
94
+ resolve или завершение onError
92
95
  ▼
93
96
  fetchingRef=false
94
97
  ```
95
98
 
96
99
  Защита действует только внутри одного mounted hook instance. TanStack Query также должен владеть дедупликацией/network state.
97
100
 
98
- Если `fetchNextPage` отклоняет Promise, hook не добавляет `.catch`; rejection может стать unhandled. Передавайте функцию с принятой error policy или wrapper, который корректно обрабатывает ожидаемую ошибку.
101
+ ## Ошибки и повторная попытка
102
+
103
+ Promise rejection и синхронный throw `fetchNextPage` передаются в `onError`
104
+ один раз. Hook ожидает async callback; его собственный отказ также обрабатывается.
105
+ Без `onError`, при отказе самого callback либо после unmount исходная ошибка
106
+ остаётся в явной `console.error`-диагностике с контекстом дозагрузки. Hook не
107
+ создаёт дополнительный unhandled Promise rejection и не решает, как показывать
108
+ ошибку пользователю: это остаётся у владельца Query и UI.
109
+
110
+ После отказа hook не пытается загрузить ту же страницу при каждом scroll/render.
111
+ Неудачная попытка привязана к `currentItemsCount`: новое количество загруженных
112
+ строк либо явное выключение `hasNextPage` и последующее включение разрешают новую
113
+ попытку. Смена identity `fetchNextPage`, `onError` или `virtualItems` не является
114
+ retry. Изменение данных без изменения количества само по себе latch не снимает;
115
+ явная ручная retry-команда остаётся у владельца Query. Интервалов и собственной
116
+ retry-policy в hook нет.
99
117
 
100
118
  ## Referential stability
101
119
 
102
- Effect зависит от `virtualItems`, `currentItemsCount`, `hasNextPage`, `fetchNextPage`.
120
+ Effect зависит от `virtualItems`, `currentItemsCount`, `hasNextPage`, `fetchNextPage`, `onError`.
103
121
 
104
122
  - Используйте значение virtualizer как предусмотрено его API.
105
123
  - Не создавайте лишний async callback с новой identity без причины.
@@ -110,7 +128,9 @@ Effect зависит от `virtualItems`, `currentItemsCount`, `hasNextPage`, `
110
128
 
111
129
  Сам hook не читает `window`, но является React effect и не выполняет fetch во время server render. В browser effect запускается после commit.
112
130
 
113
- При unmount Promise не отменяется. Abort/cancel должен быть реализован query/transport owner. `fetchingRef` больше не влияет после удаления hook instance.
131
+ При unmount Promise не отменяется. Abort/cancel должен быть реализован query/transport owner.
132
+ Поздний отказ не вызывает `onError` размонтированного UI, но остаётся в явной
133
+ console-диагностике. Уже начатый async `onError` не отменяется, его отказ не теряется.
114
134
 
115
135
  ## Типичные ошибки
116
136
 
@@ -118,17 +138,20 @@ Effect зависит от `virtualItems`, `currentItemsCount`, `hasNextPage`, `
118
138
  - Включить client-side sorting/filtering одной страницы при server-side модели.
119
139
  - Добавить фиктивный loader row в `currentItemsCount`.
120
140
  - Ожидать preload заранее — trigger находится на самом конце.
121
- - Передать rejecting Promise без error handling.
141
+ - Ожидать, что hook сам покажет Query-ошибку или автоматически повторит failed page.
122
142
  - Выполнять fetch прямо в row render вместо effect.
123
143
  - Использовать изменяемый index как domain identity строки.
124
144
 
125
145
  ## API
126
146
 
127
147
  ```ts
128
- function useFetchNextPageEffect(args: {
129
- virtualItems: VirtualItem[];
148
+ type UseFetchNextPageEffectOptions = Readonly<{
149
+ virtualItems: readonly VirtualItem[];
130
150
  currentItemsCount: number;
131
151
  hasNextPage: boolean;
132
152
  fetchNextPage: () => Promise<unknown>;
133
- }): void;
153
+ onError?: (error: unknown) => void | Promise<void>;
154
+ }>;
155
+
156
+ function useFetchNextPageEffect(options: UseFetchNextPageEffectOptions): void;
134
157
  ```