@ryuzaki13/react-foundation-lib 2.14.1 → 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.
- package/dist/async-task-runner/createDeadlineTimer.d.ts +8 -0
- package/dist/async-task-runner/createDeadlineTimer.d.ts.map +1 -0
- package/dist/async-task-runner/deadlineTimerTypes.d.ts +12 -0
- package/dist/async-task-runner/deadlineTimerTypes.d.ts.map +1 -0
- package/dist/async-task-runner/index.d.ts +2 -0
- package/dist/async-task-runner/index.d.ts.map +1 -1
- package/dist/async-task-runner/index.js +35 -1
- package/dist/async-task-runner/index.js.map +1 -1
- package/dist/virtualizer/index.d.ts +2 -1
- package/dist/virtualizer/index.d.ts.map +1 -1
- package/dist/virtualizer/index.js +18 -6
- package/dist/virtualizer/index.js.map +1 -1
- package/dist/virtualizer/useFetchNextPageEffect.d.ts +6 -7
- package/dist/virtualizer/useFetchNextPageEffect.d.ts.map +1 -1
- package/dist/virtualizer/useFetchNextPageEffectTypes.d.ts +11 -0
- package/dist/virtualizer/useFetchNextPageEffectTypes.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/async-task-runner/README.mdx +53 -1
- package/src/virtualizer/README.mdx +39 -16
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type DeadlineTimer, type DeadlineTimerOptions } from "./deadlineTimerTypes";
|
|
2
|
+
/**
|
|
3
|
+
* Ожидает один абсолютный срок. Длинное ожидание разбивается только на native
|
|
4
|
+
* timers: промежуточные части не вызывают предметный callback. Создание не
|
|
5
|
+
* запускает таймер, а отменённое поколение не оживает от позднего callback.
|
|
6
|
+
*/
|
|
7
|
+
export declare function createDeadlineTimer({ onDue }: DeadlineTimerOptions): DeadlineTimer;
|
|
8
|
+
//# sourceMappingURL=createDeadlineTimer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createDeadlineTimer.d.ts","sourceRoot":"","sources":["../../src/async-task-runner/createDeadlineTimer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,aAAa,EAAE,KAAK,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AASrF;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,EAAE,KAAK,EAAE,EAAE,oBAAoB,GAAG,aAAa,CAgDlF"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Одноразовое пробуждение: предметную работу и обработку её отказа выполняет caller. */
|
|
2
|
+
export type DeadlineTimerOptions = Readonly<{
|
|
3
|
+
onDue: () => void;
|
|
4
|
+
}>;
|
|
5
|
+
/** Абсолютный срок в Unix milliseconds не зависит от длительности предыдущей задачи. */
|
|
6
|
+
export type DeadlineTimer = Readonly<{
|
|
7
|
+
/** Заменяет срок; null отменяет его. Некорректный срок не отменяет уже назначенный. */
|
|
8
|
+
schedule: (deadlineAtMs: number | null) => void;
|
|
9
|
+
/** Отзывает только ещё не начавшееся пробуждение, а не работу внутри onDue. */
|
|
10
|
+
cancel: () => void;
|
|
11
|
+
}>;
|
|
12
|
+
//# sourceMappingURL=deadlineTimerTypes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deadlineTimerTypes.d.ts","sourceRoot":"","sources":["../../src/async-task-runner/deadlineTimerTypes.ts"],"names":[],"mappings":"AAAA,yFAAyF;AACzF,MAAM,MAAM,oBAAoB,GAAG,QAAQ,CAAC;IAC3C,KAAK,EAAE,MAAM,IAAI,CAAC;CAClB,CAAC,CAAC;AAEH,wFAAwF;AACxF,MAAM,MAAM,aAAa,GAAG,QAAQ,CAAC;IACpC,uFAAuF;IACvF,QAAQ,EAAE,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAChD,+EAA+E;IAC/E,MAAM,EAAE,MAAM,IAAI,CAAC;CACnB,CAAC,CAAC"}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { createCoalescingTaskRunner } from "./createCoalescingTaskRunner";
|
|
2
2
|
export { type CoalescingTaskRunner, type CoalescingTaskRunnerOptions } from "./asyncTaskRunnerTypes";
|
|
3
|
+
export { createDeadlineTimer } from "./createDeadlineTimer";
|
|
4
|
+
export { type DeadlineTimer, type DeadlineTimerOptions } from "./deadlineTimerTypes";
|
|
3
5
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/async-task-runner/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,0BAA0B,EAAE,MAAM,8BAA8B,CAAC;AAC1E,OAAO,EAAE,KAAK,oBAAoB,EAAE,KAAK,2BAA2B,EAAE,MAAM,wBAAwB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/async-task-runner/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,0BAA0B,EAAE,MAAM,8BAA8B,CAAC;AAC1E,OAAO,EAAE,KAAK,oBAAoB,EAAE,KAAK,2BAA2B,EAAE,MAAM,wBAAwB,CAAC;AACrG,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,KAAK,aAAa,EAAE,KAAK,oBAAoB,EAAE,MAAM,sBAAsB,CAAC"}
|
|
@@ -59,7 +59,41 @@ function e(e) {
|
|
|
59
59
|
}
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
|
+
function t({ onDue: e }) {
|
|
63
|
+
let t = null, n = () => {
|
|
64
|
+
let e = t;
|
|
65
|
+
t = null, e && e.timer !== null && (clearTimeout(e.timer), e.timer = null);
|
|
66
|
+
}, r = (n) => {
|
|
67
|
+
let i = Math.max(1, Math.min(2147483647, n.deadlineAtMs - Date.now())), a = setTimeout(() => {
|
|
68
|
+
if (t === n && n.timer === a) {
|
|
69
|
+
if (n.timer = null, Date.now() < n.deadlineAtMs) {
|
|
70
|
+
r(n);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
t = null, e();
|
|
74
|
+
}
|
|
75
|
+
}, i);
|
|
76
|
+
n.timer = a, a.unref?.();
|
|
77
|
+
};
|
|
78
|
+
return {
|
|
79
|
+
schedule(e) {
|
|
80
|
+
if (e === null) {
|
|
81
|
+
n();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (!Number.isSafeInteger(e)) throw RangeError("Абсолютный срок должен быть безопасным целым числом Unix milliseconds.");
|
|
85
|
+
if (t?.deadlineAtMs === e) return;
|
|
86
|
+
n();
|
|
87
|
+
let i = {
|
|
88
|
+
deadlineAtMs: e,
|
|
89
|
+
timer: null
|
|
90
|
+
};
|
|
91
|
+
t = i, r(i);
|
|
92
|
+
},
|
|
93
|
+
cancel: n
|
|
94
|
+
};
|
|
95
|
+
}
|
|
62
96
|
//#endregion
|
|
63
|
-
export { e as createCoalescingTaskRunner };
|
|
97
|
+
export { e as createCoalescingTaskRunner, t as createDeadlineTimer };
|
|
64
98
|
|
|
65
99
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/async-task-runner/createCoalescingTaskRunner.ts"],"sourcesContent":["import { type CoalescingTaskRunner, type CoalescingTaskRunnerOptions } from \"./asyncTaskRunnerTypes\";\n\ntype ScheduledWake = { kind: \"retry\" | \"tail\"; timer: ReturnType<typeof setTimeout> };\ntype RunnerGeneration = {\n\tpending: Promise<void> | null;\n\trerun: boolean;\n\tpollTimer: ReturnType<typeof setInterval> | null;\n\tscheduledWake: ScheduledWake | null;\n\treportingFailure: Readonly<{ error: unknown }> | null;\n};\n\n/**\n * Создаёт неактивный runner. Поколение защищает от поздних таймеров, single-flight\n * объединяет сигналы в один следующий проход, stop не имитирует отмену уже\n * выполняющейся атомарной задачи. Предметная запись и диагностика принадлежат caller.\n */\nexport function createCoalescingTaskRunner(options: CoalescingTaskRunnerOptions): CoalescingTaskRunner {\n\tfor (const delay of [options.retryDelayMs, ...(options.pollIntervalMs === undefined ? [] : [options.pollIntervalMs])]) {\n\t\t// Более длинную задержку Node превращает в 1 ms: молчаливое ограничение здесь опасно.\n\t\tif (!Number.isInteger(delay) || delay < 1 || delay > 2_147_483_647) {\n\t\t\tthrow new RangeError(\"Задержки runner должны быть целыми числами от 1 до 2147483647 ms.\");\n\t\t}\n\t}\n\tlet active: RunnerGeneration | null = null;\n\tlet stopping: Promise<void> | null = null;\n\tlet stopRevision = 0;\n\n\tconst clearScheduledWake = (generation: RunnerGeneration) => {\n\t\tif (generation.scheduledWake) clearTimeout(generation.scheduledWake.timer);\n\t\tgeneration.scheduledWake = null;\n\t};\n\tconst clearTimers = (generation: RunnerGeneration) => {\n\t\tclearScheduledWake(generation);\n\t\tif (generation.pollTimer !== null) clearInterval(generation.pollTimer);\n\t\tgeneration.pollTimer = null;\n\t};\n\tconst backgroundWake = (generation: RunnerGeneration) => {\n\t\tif (active !== generation || generation.reportingFailure || generation.scheduledWake?.kind === \"retry\") return;\n\t\t// Обычный отказ уже передан onError; отказ самого onError сохранён для\n\t\t// admission/stop. Timer не создаёт unhandled rejection и не теряет reporter error.\n\t\tvoid wake().catch(() => {});\n\t};\n\tconst schedule = (generation: RunnerGeneration, kind: ScheduledWake[\"kind\"], delay: number) => {\n\t\tif (active !== generation || generation.reportingFailure || generation.scheduledWake) return;\n\t\tconst timer = setTimeout(() => {\n\t\t\tif (active !== generation || generation.scheduledWake?.timer !== timer) return;\n\t\t\tgeneration.scheduledWake = null;\n\t\t\tbackgroundWake(generation);\n\t\t}, delay);\n\t\tgeneration.scheduledWake = { kind, timer };\n\t\ttimer.unref?.();\n\t};\n\tconst wake = (): Promise<void> => {\n\t\tconst generation = active;\n\t\tif (!generation) return Promise.resolve();\n\t\tif (generation.reportingFailure) return Promise.reject(generation.reportingFailure.error);\n\t\tif (generation.pending) {\n\t\t\tgeneration.rerun = true;\n\t\t\treturn generation.pending;\n\t\t}\n\t\t// Явный wake ускоряет retry. Только caller, не poll, может принять это решение.\n\t\tclearScheduledWake(generation);\n\t\tgeneration.rerun = false;\n\t\tgeneration.pending = Promise.resolve()\n\t\t\t.then(async () => {\n\t\t\t\t// stop до первой microtask не разрешает позднее выполнение callback.\n\t\t\t\tif (active === generation) await options.run();\n\t\t\t})\n\t\t\t.catch(async (error: unknown) => {\n\t\t\t\t// Уже начатый run сообщает отказ даже после stop; shutdown не скрывает ошибку.\n\t\t\t\ttry {\n\t\t\t\t\tawait options.onError(error);\n\t\t\t\t} catch (reporterError: unknown) {\n\t\t\t\t\t// Wrapper различает отсутствие отказа и реальные throw undefined/null.\n\t\t\t\t\tgeneration.reportingFailure = { error: reporterError };\n\t\t\t\t\tgeneration.rerun = false;\n\t\t\t\t\tclearTimers(generation);\n\t\t\t\t}\n\t\t\t\tschedule(generation, \"retry\", options.retryDelayMs);\n\t\t\t\tthrow error;\n\t\t\t})\n\t\t\t.finally(() => {\n\t\t\t\tgeneration.pending = null;\n\t\t\t\t// При отказе retry timer уже существует и не заменяется немедленным хвостом.\n\t\t\t\tif (generation.rerun) schedule(generation, \"tail\", 0);\n\t\t\t});\n\t\treturn generation.pending;\n\t};\n\tconst startGeneration = (requestedBeforeStop: number): Promise<void> => {\n\t\tif (requestedBeforeStop !== stopRevision) return Promise.reject(new Error(\"Запуск runner отменён более поздней остановкой.\"));\n\t\tif (active) {\n\t\t\tif (active.reportingFailure) return Promise.reject(active.reportingFailure.error);\n\t\t\t// Повторный start не является wake и не создаёт лишний хвост или обход retry.\n\t\t\treturn active.pending ?? Promise.resolve();\n\t\t}\n\t\tconst generation: RunnerGeneration = { pending: null, rerun: false, pollTimer: null, scheduledWake: null, reportingFailure: null };\n\t\tactive = generation;\n\t\tif (options.pollIntervalMs !== undefined) {\n\t\t\tgeneration.pollTimer = setInterval(() => backgroundWake(generation), options.pollIntervalMs);\n\t\t\tgeneration.pollTimer.unref?.();\n\t\t}\n\t\treturn wake();\n\t};\n\treturn {\n\t\tstart() {\n\t\t\tconst requestedBeforeStop = stopRevision;\n\t\t\treturn stopping ? stopping.then(() => startGeneration(requestedBeforeStop)) : startGeneration(requestedBeforeStop);\n\t\t},\n\t\twake,\n\t\tstop() {\n\t\t\tstopRevision++;\n\t\t\tif (stopping) return stopping;\n\t\t\tconst generation = active;\n\t\t\tif (!generation) return Promise.resolve();\n\t\t\tactive = null;\n\t\t\tgeneration.rerun = false;\n\t\t\tclearTimers(generation);\n\t\t\tstopping = (async () => {\n\t\t\t\t// Task failure уже обработан onError и остаётся у start/wake caller;\n\t\t\t\t// drain отдельно передаёт точный отказ reporter, если тот не сработал.\n\t\t\t\tawait generation.pending?.catch(() => {});\n\t\t\t\tif (generation.reportingFailure) throw generation.reportingFailure.error;\n\t\t\t})().finally(() => {\n\t\t\t\tstopping = null;\n\t\t\t});\n\t\t\treturn stopping;\n\t\t}\n\t};\n}\n"],"mappings":";AAgBA,SAAgB,EAA2B,GAA4D;CACtG,KAAK,IAAM,KAAS,CAAC,EAAQ,cAAc,GAAI,EAAQ,mBAAmB,KAAA,IAAY,CAAC,IAAI,CAAC,EAAQ,cAAc,CAAE,GAEnH,IAAI,CAAC,OAAO,UAAU,CAAK,KAAK,IAAQ,KAAK,IAAQ,YACpD,MAAU,WAAW,mEAAmE;CAG1F,IAAI,IAAkC,MAClC,IAAiC,MACjC,IAAe,GAEb,KAAsB,MAAiC;EAE5D,AADI,EAAW,iBAAe,aAAa,EAAW,cAAc,KAAK,GACzE,EAAW,gBAAgB;CAC5B,GACM,KAAe,MAAiC;EAGrD,AAFA,EAAmB,CAAU,GACzB,EAAW,cAAc,QAAM,cAAc,EAAW,SAAS,GACrE,EAAW,YAAY;CACxB,GACM,KAAkB,MAAiC;EACpD,MAAW,KAAc,EAAW,oBAAoB,EAAW,eAAe,SAAS,WAG/F,EAAU,CAAC,CAAC,YAAY,CAAC,CAAC;CAC3B,GACM,KAAY,GAA8B,GAA6B,MAAkB;EAC9F,IAAI,MAAW,KAAc,EAAW,oBAAoB,EAAW,eAAe;EACtF,IAAM,IAAQ,iBAAiB;GAC1B,MAAW,KAAc,EAAW,eAAe,UAAU,MACjE,EAAW,gBAAgB,MAC3B,EAAe,CAAU;EAC1B,GAAG,CAAK;EAER,AADA,EAAW,gBAAgB;GAAE;GAAM;EAAM,GACzC,EAAM,QAAQ;CACf,GACM,UAA4B;EACjC,IAAM,IAAa;EAiCnB,OAhCK,IACD,EAAW,mBAAyB,QAAQ,OAAO,EAAW,iBAAiB,KAAK,IACpF,EAAW,WACd,EAAW,QAAQ,IACZ,EAAW,YAGnB,EAAmB,CAAU,GAC7B,EAAW,QAAQ,IACnB,EAAW,UAAU,QAAQ,QAAQ,CAAC,CACpC,KAAK,YAAY;GAEjB,AAAI,MAAW,KAAY,MAAM,EAAQ,IAAI;EAC9C,CAAC,CAAC,CACD,MAAM,OAAO,MAAmB;GAEhC,IAAI;IACH,MAAM,EAAQ,QAAQ,CAAK;GAC5B,SAAS,GAAwB;IAIhC,AAFA,EAAW,mBAAmB,EAAE,OAAO,EAAc,GACrD,EAAW,QAAQ,IACnB,EAAY,CAAU;GACvB;GAEA,MADA,EAAS,GAAY,SAAS,EAAQ,YAAY,GAC5C;EACP,CAAC,CAAC,CACD,cAAc;GAGd,AAFA,EAAW,UAAU,MAEjB,EAAW,SAAO,EAAS,GAAY,QAAQ,CAAC;EACrD,CAAC,GACK,EAAW,WAhCM,QAAQ,QAAQ;CAiCzC,GACM,KAAmB,MAA+C;EACvE,IAAI,MAAwB,GAAc,OAAO,QAAQ,OAAO,gBAAI,MAAM,iDAAiD,CAAC;EAC5H,IAAI,GAGH,OAFI,EAAO,mBAAyB,QAAQ,OAAO,EAAO,iBAAiB,KAAK,IAEzE,EAAO,WAAW,QAAQ,QAAQ;EAE1C,IAAM,IAA+B;GAAE,SAAS;GAAM,OAAO;GAAO,WAAW;GAAM,eAAe;GAAM,kBAAkB;EAAK;EAMjI,OALA,IAAS,GACL,EAAQ,mBAAmB,KAAA,MAC9B,EAAW,YAAY,kBAAkB,EAAe,CAAU,GAAG,EAAQ,cAAc,GAC3F,EAAW,UAAU,QAAQ,IAEvB,EAAK;CACb;CACA,OAAO;EACN,QAAQ;GACP,IAAM,IAAsB;GAC5B,OAAO,IAAW,EAAS,WAAW,EAAgB,CAAmB,CAAC,IAAI,EAAgB,CAAmB;EAClH;EACA;EACA,OAAO;GAEN,IADA,KACI,GAAU,OAAO;GACrB,IAAM,IAAa;GAanB,OAZK,KACL,IAAS,MACT,EAAW,QAAQ,IACnB,EAAY,CAAU,GACtB,KAAY,YAAY;IAIvB,IADA,MAAM,EAAW,SAAS,YAAY,CAAC,CAAC,GACpC,EAAW,kBAAkB,MAAM,EAAW,iBAAiB;GACpE,EAAA,CAAG,CAAC,CAAC,cAAc;IAClB,IAAW;GACZ,CAAC,GACM,KAZiB,QAAQ,QAAQ;EAazC;CACD;AACD"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/async-task-runner/createCoalescingTaskRunner.ts","../../src/async-task-runner/createDeadlineTimer.ts"],"sourcesContent":["import { type CoalescingTaskRunner, type CoalescingTaskRunnerOptions } from \"./asyncTaskRunnerTypes\";\n\ntype ScheduledWake = { kind: \"retry\" | \"tail\"; timer: ReturnType<typeof setTimeout> };\ntype RunnerGeneration = {\n\tpending: Promise<void> | null;\n\trerun: boolean;\n\tpollTimer: ReturnType<typeof setInterval> | null;\n\tscheduledWake: ScheduledWake | null;\n\treportingFailure: Readonly<{ error: unknown }> | null;\n};\n\n/**\n * Создаёт неактивный runner. Поколение защищает от поздних таймеров, single-flight\n * объединяет сигналы в один следующий проход, stop не имитирует отмену уже\n * выполняющейся атомарной задачи. Предметная запись и диагностика принадлежат caller.\n */\nexport function createCoalescingTaskRunner(options: CoalescingTaskRunnerOptions): CoalescingTaskRunner {\n\tfor (const delay of [options.retryDelayMs, ...(options.pollIntervalMs === undefined ? [] : [options.pollIntervalMs])]) {\n\t\t// Более длинную задержку Node превращает в 1 ms: молчаливое ограничение здесь опасно.\n\t\tif (!Number.isInteger(delay) || delay < 1 || delay > 2_147_483_647) {\n\t\t\tthrow new RangeError(\"Задержки runner должны быть целыми числами от 1 до 2147483647 ms.\");\n\t\t}\n\t}\n\tlet active: RunnerGeneration | null = null;\n\tlet stopping: Promise<void> | null = null;\n\tlet stopRevision = 0;\n\n\tconst clearScheduledWake = (generation: RunnerGeneration) => {\n\t\tif (generation.scheduledWake) clearTimeout(generation.scheduledWake.timer);\n\t\tgeneration.scheduledWake = null;\n\t};\n\tconst clearTimers = (generation: RunnerGeneration) => {\n\t\tclearScheduledWake(generation);\n\t\tif (generation.pollTimer !== null) clearInterval(generation.pollTimer);\n\t\tgeneration.pollTimer = null;\n\t};\n\tconst backgroundWake = (generation: RunnerGeneration) => {\n\t\tif (active !== generation || generation.reportingFailure || generation.scheduledWake?.kind === \"retry\") return;\n\t\t// Обычный отказ уже передан onError; отказ самого onError сохранён для\n\t\t// admission/stop. Timer не создаёт unhandled rejection и не теряет reporter error.\n\t\tvoid wake().catch(() => {});\n\t};\n\tconst schedule = (generation: RunnerGeneration, kind: ScheduledWake[\"kind\"], delay: number) => {\n\t\tif (active !== generation || generation.reportingFailure || generation.scheduledWake) return;\n\t\tconst timer = setTimeout(() => {\n\t\t\tif (active !== generation || generation.scheduledWake?.timer !== timer) return;\n\t\t\tgeneration.scheduledWake = null;\n\t\t\tbackgroundWake(generation);\n\t\t}, delay);\n\t\tgeneration.scheduledWake = { kind, timer };\n\t\ttimer.unref?.();\n\t};\n\tconst wake = (): Promise<void> => {\n\t\tconst generation = active;\n\t\tif (!generation) return Promise.resolve();\n\t\tif (generation.reportingFailure) return Promise.reject(generation.reportingFailure.error);\n\t\tif (generation.pending) {\n\t\t\tgeneration.rerun = true;\n\t\t\treturn generation.pending;\n\t\t}\n\t\t// Явный wake ускоряет retry. Только caller, не poll, может принять это решение.\n\t\tclearScheduledWake(generation);\n\t\tgeneration.rerun = false;\n\t\tgeneration.pending = Promise.resolve()\n\t\t\t.then(async () => {\n\t\t\t\t// stop до первой microtask не разрешает позднее выполнение callback.\n\t\t\t\tif (active === generation) await options.run();\n\t\t\t})\n\t\t\t.catch(async (error: unknown) => {\n\t\t\t\t// Уже начатый run сообщает отказ даже после stop; shutdown не скрывает ошибку.\n\t\t\t\ttry {\n\t\t\t\t\tawait options.onError(error);\n\t\t\t\t} catch (reporterError: unknown) {\n\t\t\t\t\t// Wrapper различает отсутствие отказа и реальные throw undefined/null.\n\t\t\t\t\tgeneration.reportingFailure = { error: reporterError };\n\t\t\t\t\tgeneration.rerun = false;\n\t\t\t\t\tclearTimers(generation);\n\t\t\t\t}\n\t\t\t\tschedule(generation, \"retry\", options.retryDelayMs);\n\t\t\t\tthrow error;\n\t\t\t})\n\t\t\t.finally(() => {\n\t\t\t\tgeneration.pending = null;\n\t\t\t\t// При отказе retry timer уже существует и не заменяется немедленным хвостом.\n\t\t\t\tif (generation.rerun) schedule(generation, \"tail\", 0);\n\t\t\t});\n\t\treturn generation.pending;\n\t};\n\tconst startGeneration = (requestedBeforeStop: number): Promise<void> => {\n\t\tif (requestedBeforeStop !== stopRevision) return Promise.reject(new Error(\"Запуск runner отменён более поздней остановкой.\"));\n\t\tif (active) {\n\t\t\tif (active.reportingFailure) return Promise.reject(active.reportingFailure.error);\n\t\t\t// Повторный start не является wake и не создаёт лишний хвост или обход retry.\n\t\t\treturn active.pending ?? Promise.resolve();\n\t\t}\n\t\tconst generation: RunnerGeneration = { pending: null, rerun: false, pollTimer: null, scheduledWake: null, reportingFailure: null };\n\t\tactive = generation;\n\t\tif (options.pollIntervalMs !== undefined) {\n\t\t\tgeneration.pollTimer = setInterval(() => backgroundWake(generation), options.pollIntervalMs);\n\t\t\tgeneration.pollTimer.unref?.();\n\t\t}\n\t\treturn wake();\n\t};\n\treturn {\n\t\tstart() {\n\t\t\tconst requestedBeforeStop = stopRevision;\n\t\t\treturn stopping ? stopping.then(() => startGeneration(requestedBeforeStop)) : startGeneration(requestedBeforeStop);\n\t\t},\n\t\twake,\n\t\tstop() {\n\t\t\tstopRevision++;\n\t\t\tif (stopping) return stopping;\n\t\t\tconst generation = active;\n\t\t\tif (!generation) return Promise.resolve();\n\t\t\tactive = null;\n\t\t\tgeneration.rerun = false;\n\t\t\tclearTimers(generation);\n\t\t\tstopping = (async () => {\n\t\t\t\t// Task failure уже обработан onError и остаётся у start/wake caller;\n\t\t\t\t// drain отдельно передаёт точный отказ reporter, если тот не сработал.\n\t\t\t\tawait generation.pending?.catch(() => {});\n\t\t\t\tif (generation.reportingFailure) throw generation.reportingFailure.error;\n\t\t\t})().finally(() => {\n\t\t\t\tstopping = null;\n\t\t\t});\n\t\t\treturn stopping;\n\t\t}\n\t};\n}\n","import { type DeadlineTimer, type DeadlineTimerOptions } from \"./deadlineTimerTypes\";\n\nconst MAX_NATIVE_DELAY_MS = 2_147_483_647;\n\ntype DeadlineGeneration = {\n\tdeadlineAtMs: number;\n\ttimer: ReturnType<typeof setTimeout> | null;\n};\n\n/**\n * Ожидает один абсолютный срок. Длинное ожидание разбивается только на native\n * timers: промежуточные части не вызывают предметный callback. Создание не\n * запускает таймер, а отменённое поколение не оживает от позднего callback.\n */\nexport function createDeadlineTimer({ onDue }: DeadlineTimerOptions): DeadlineTimer {\n\tlet active: DeadlineGeneration | null = null;\n\n\tconst cancel = (): void => {\n\t\tconst generation = active;\n\t\tactive = null;\n\t\tif (generation && generation.timer !== null) {\n\t\t\tclearTimeout(generation.timer);\n\t\t\tgeneration.timer = null;\n\t\t}\n\t};\n\tconst arm = (generation: DeadlineGeneration): void => {\n\t\t// Node превращает переполнение delay в 1 ms. Даже годовой deadline нельзя\n\t\t// передавать напрямую; оставшийся срок перепроверяется после каждой части.\n\t\tconst delay = Math.max(1, Math.min(MAX_NATIVE_DELAY_MS, generation.deadlineAtMs - Date.now()));\n\t\tconst timer = setTimeout(() => {\n\t\t\tif (active !== generation || generation.timer !== timer) return;\n\t\t\tgeneration.timer = null;\n\t\t\tif (Date.now() < generation.deadlineAtMs) {\n\t\t\t\tarm(generation);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Срок отзывается до callback: onDue может безопасно назначить следующий.\n\t\t\t// Ошибки и async rejection принадлежат caller, а не timer primitive.\n\t\t\tactive = null;\n\t\t\tonDue();\n\t\t}, delay);\n\t\tgeneration.timer = timer;\n\t\ttimer.unref?.();\n\t};\n\n\treturn {\n\t\tschedule(deadlineAtMs) {\n\t\t\tif (deadlineAtMs === null) {\n\t\t\t\tcancel();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (!Number.isSafeInteger(deadlineAtMs)) {\n\t\t\t\tthrow new RangeError(\"Абсолютный срок должен быть безопасным целым числом Unix milliseconds.\");\n\t\t\t}\n\t\t\tif (active?.deadlineAtMs === deadlineAtMs) return;\n\t\t\tcancel();\n\t\t\tconst generation: DeadlineGeneration = { deadlineAtMs, timer: null };\n\t\t\tactive = generation;\n\t\t\tarm(generation);\n\t\t},\n\t\tcancel\n\t};\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/virtualizer/index.ts"],"names":[],"mappings":"AAAA,
|
|
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
|
|
5
|
-
e(() => {
|
|
6
|
-
|
|
7
|
-
|
|
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 {
|
|
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 {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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,
|
|
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,10 +1,14 @@
|
|
|
1
|
-
# Последовательный async runner
|
|
1
|
+
# Последовательный async runner и абсолютный deadline
|
|
2
2
|
|
|
3
3
|
Публичный импорт: `@ryuzaki13/react-foundation-lib/async-task-runner`.
|
|
4
4
|
Runner объединяет повторные сигналы, повторяет отказавшую задачу и корректно
|
|
5
5
|
завершает lifecycle. Он не знает о HTTP, БД, React или предметном составе.
|
|
6
6
|
Создание и импорт не вызывают `run` и не создают таймеры.
|
|
7
7
|
|
|
8
|
+
`createDeadlineTimer` дополняет runner независимым одноразовым пробуждением в
|
|
9
|
+
абсолютный момент. Он не задаёт polling или retry policy и не вызывает
|
|
10
|
+
предметную работу при промежуточных частях длинного ожидания.
|
|
11
|
+
|
|
8
12
|
```ts
|
|
9
13
|
import { createCoalescingTaskRunner } from "@ryuzaki13/react-foundation-lib/async-task-runner";
|
|
10
14
|
|
|
@@ -77,3 +81,51 @@ stop отменяет такой отложенный start, не позволя
|
|
|
77
81
|
создаёт цикл самодренирования. Остановка принадлежит внешнему lifecycle.
|
|
78
82
|
Callback, который никогда не завершается, удерживает drain; timeout и реальная
|
|
79
83
|
отмена внешней операции остаются ответственностью приложения.
|
|
84
|
+
|
|
85
|
+
## Абсолютный одноразовый срок
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
import { createDeadlineTimer } from "@ryuzaki13/react-foundation-lib/async-task-runner";
|
|
89
|
+
|
|
90
|
+
const deadline = createDeadlineTimer({
|
|
91
|
+
onDue: () => {
|
|
92
|
+
// Async callback имеет собственную обработку отказа у владельца работы.
|
|
93
|
+
void runner.wake().catch(reportTaskFailure);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
deadline.schedule(nextEffectiveAtMs);
|
|
97
|
+
// При остановке сначала отменяется допуск timer, затем ожидается работа runner.
|
|
98
|
+
deadline.cancel();
|
|
99
|
+
await runner.stop();
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Создание timer не запускает callback и не создаёт native timer.
|
|
103
|
+
`schedule(deadlineAtMs: number | null): void` заменяет прежний срок, а `null`
|
|
104
|
+
отменяет его. Срок — безопасное целое число абсолютных Unix milliseconds;
|
|
105
|
+
`NaN`, бесконечность, дробное или небезопасное число дают `RangeError` до
|
|
106
|
+
побочных эффектов и не отменяют уже назначенный корректный срок. Повтор того
|
|
107
|
+
же pending срока идемпотентен. Просроченный срок, включая отрицательное число,
|
|
108
|
+
выполняется отдельной timer-задачей, а не синхронно внутри `schedule`.
|
|
109
|
+
|
|
110
|
+
Задержка каждого native timer не превышает 2147483647 ms. Если абсолютный срок
|
|
111
|
+
дальше, ожидание разбивается на части с повторной проверкой `Date.now()`.
|
|
112
|
+
Промежуточные части не вызывают `onDue`, не читают внешние источники и не
|
|
113
|
+
выполняют фоновые предметные проходы. Одновременно существует не более одного
|
|
114
|
+
native timer. В Node используется `unref`: само ожидание не удерживает процесс.
|
|
115
|
+
|
|
116
|
+
`onDue` вызывается один раз за назначенный срок. До вызова срок снимается, поэтому
|
|
117
|
+
callback может назначить следующий срок. `cancel(): void` удаляет pending timer
|
|
118
|
+
и отзывает его поколение; поздние callbacks отменённого или заменённого срока
|
|
119
|
+
игнорируются. Cancel не отменяет уже начавшуюся работу внутри `onDue`. Новая
|
|
120
|
+
`schedule` после cancel разрешена; окончательный lifecycle принадлежит caller.
|
|
121
|
+
|
|
122
|
+
Timer не знает timezone, календарных правил, БД, SQL, transport или React.
|
|
123
|
+
Вычисление абсолютного срока, обработка синхронных ошибок и async rejection
|
|
124
|
+
в callback, retry и остановка атомарной работы остаются у caller. Он не ожидает
|
|
125
|
+
Promise, возвращённый callback, и не добавляет собственной диагностики.
|
|
126
|
+
|
|
127
|
+
Точность зависит от event loop и системных часов. После перевода часов назад
|
|
128
|
+
ранний callback перевзводит оставшееся ожидание; после перевода часов вперёд
|
|
129
|
+
просроченность обнаруживается при ближайшем уже назначенном timer callback,
|
|
130
|
+
не через скрытый interval. Этот primitive не гарантирует немедленную реакцию
|
|
131
|
+
на изменение часов и не заменяет внешний startup/reconnect catch-up.
|
|
@@ -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
|
-
Внутренний
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
153
|
+
onError?: (error: unknown) => void | Promise<void>;
|
|
154
|
+
}>;
|
|
155
|
+
|
|
156
|
+
function useFetchNextPageEffect(options: UseFetchNextPageEffectOptions): void;
|
|
134
157
|
```
|