@ryuzaki13/react-foundation-lib 2.13.1 → 2.14.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/README.md CHANGED
@@ -104,6 +104,7 @@ npm run pack:dry-run
104
104
  | Публичный import path | Подробная документация |
105
105
  | ---------------------- | ------------------------------------------------------------------------------ |
106
106
  | `/array` | [`array/README.mdx`](./src/array/README.mdx) |
107
+ | `/async-task-runner` | [`async-task-runner/README.mdx`](./src/async-task-runner/README.mdx) |
107
108
  | `/binary` | [`binary/README.mdx`](./src/binary/README.mdx) |
108
109
  | `/bounded-copy-stack` | [`bounded-copy-stack/README.mdx`](./src/bounded-copy-stack/README.mdx) |
109
110
  | `/context-menu` | [`context-menu/README.mdx`](./src/context-menu/README.mdx) |
@@ -0,0 +1,18 @@
1
+ /** Настройки одной последовательной задачи, без знания transport, БД или предметного состава. */
2
+ export type CoalescingTaskRunnerOptions = Readonly<{
3
+ run: () => Promise<void>;
4
+ /** Обработчик ожидается; его собственный отказ закрывает поколение до явного stop. */
5
+ onError: (error: unknown) => void | Promise<void>;
6
+ /** Явная задержка повтора, без навязанной библиотекой политики backoff. */
7
+ retryDelayMs: number;
8
+ /** Необязательное пробуждение; отсутствие не заменяется периодом по умолчанию. */
9
+ pollIntervalMs?: number;
10
+ }>;
11
+ /** Promise start/wake относится текущей попытке, а не объединённому будущему хвосту. */
12
+ export type CoalescingTaskRunner = Readonly<{
13
+ start: () => Promise<void>;
14
+ wake: () => Promise<void>;
15
+ /** Отзывает дальнейший допуск и ждёт фактического завершения задачи и onError. */
16
+ stop: () => Promise<void>;
17
+ }>;
18
+ //# sourceMappingURL=asyncTaskRunnerTypes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"asyncTaskRunnerTypes.d.ts","sourceRoot":"","sources":["../../src/async-task-runner/asyncTaskRunnerTypes.ts"],"names":[],"mappings":"AAAA,iGAAiG;AACjG,MAAM,MAAM,2BAA2B,GAAG,QAAQ,CAAC;IAClD,GAAG,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACzB,sFAAsF;IACtF,OAAO,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClD,2EAA2E;IAC3E,YAAY,EAAE,MAAM,CAAC;IACrB,kFAAkF;IAClF,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC,CAAC;AAEH,wFAAwF;AACxF,MAAM,MAAM,oBAAoB,GAAG,QAAQ,CAAC;IAC3C,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,kFAAkF;IAClF,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B,CAAC,CAAC"}
@@ -0,0 +1,8 @@
1
+ import { type CoalescingTaskRunner, type CoalescingTaskRunnerOptions } from "./asyncTaskRunnerTypes";
2
+ /**
3
+ * Создаёт неактивный runner. Поколение защищает от поздних таймеров, single-flight
4
+ * объединяет сигналы в один следующий проход, stop не имитирует отмену уже
5
+ * выполняющейся атомарной задачи. Предметная запись и диагностика принадлежат caller.
6
+ */
7
+ export declare function createCoalescingTaskRunner(options: CoalescingTaskRunnerOptions): CoalescingTaskRunner;
8
+ //# sourceMappingURL=createCoalescingTaskRunner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createCoalescingTaskRunner.d.ts","sourceRoot":"","sources":["../../src/async-task-runner/createCoalescingTaskRunner.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,oBAAoB,EAAE,KAAK,2BAA2B,EAAE,MAAM,wBAAwB,CAAC;AAWrG;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,2BAA2B,GAAG,oBAAoB,CAgHrG"}
@@ -0,0 +1,3 @@
1
+ export { createCoalescingTaskRunner } from "./createCoalescingTaskRunner";
2
+ export { type CoalescingTaskRunner, type CoalescingTaskRunnerOptions } from "./asyncTaskRunnerTypes";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,65 @@
1
+ //#region src/async-task-runner/createCoalescingTaskRunner.ts
2
+ function e(e) {
3
+ for (let t of [e.retryDelayMs, ...e.pollIntervalMs === void 0 ? [] : [e.pollIntervalMs]]) if (!Number.isInteger(t) || t < 1 || t > 2147483647) throw RangeError("Задержки runner должны быть целыми числами от 1 до 2147483647 ms.");
4
+ let t = null, n = null, r = 0, i = (e) => {
5
+ e.scheduledWake && clearTimeout(e.scheduledWake.timer), e.scheduledWake = null;
6
+ }, a = (e) => {
7
+ i(e), e.pollTimer !== null && clearInterval(e.pollTimer), e.pollTimer = null;
8
+ }, o = (e) => {
9
+ t !== e || e.reportingFailure || e.scheduledWake?.kind === "retry" || c().catch(() => {});
10
+ }, s = (e, n, r) => {
11
+ if (t !== e || e.reportingFailure || e.scheduledWake) return;
12
+ let i = setTimeout(() => {
13
+ t === e && e.scheduledWake?.timer === i && (e.scheduledWake = null, o(e));
14
+ }, r);
15
+ e.scheduledWake = {
16
+ kind: n,
17
+ timer: i
18
+ }, i.unref?.();
19
+ }, c = () => {
20
+ let n = t;
21
+ return n ? n.reportingFailure ? Promise.reject(n.reportingFailure.error) : n.pending ? (n.rerun = !0, n.pending) : (i(n), n.rerun = !1, n.pending = Promise.resolve().then(async () => {
22
+ t === n && await e.run();
23
+ }).catch(async (t) => {
24
+ try {
25
+ await e.onError(t);
26
+ } catch (e) {
27
+ n.reportingFailure = { error: e }, n.rerun = !1, a(n);
28
+ }
29
+ throw s(n, "retry", e.retryDelayMs), t;
30
+ }).finally(() => {
31
+ n.pending = null, n.rerun && s(n, "tail", 0);
32
+ }), n.pending) : Promise.resolve();
33
+ }, l = (n) => {
34
+ if (n !== r) return Promise.reject(/* @__PURE__ */ Error("Запуск runner отменён более поздней остановкой."));
35
+ if (t) return t.reportingFailure ? Promise.reject(t.reportingFailure.error) : t.pending ?? Promise.resolve();
36
+ let i = {
37
+ pending: null,
38
+ rerun: !1,
39
+ pollTimer: null,
40
+ scheduledWake: null,
41
+ reportingFailure: null
42
+ };
43
+ return t = i, e.pollIntervalMs !== void 0 && (i.pollTimer = setInterval(() => o(i), e.pollIntervalMs), i.pollTimer.unref?.()), c();
44
+ };
45
+ return {
46
+ start() {
47
+ let e = r;
48
+ return n ? n.then(() => l(e)) : l(e);
49
+ },
50
+ wake: c,
51
+ stop() {
52
+ if (r++, n) return n;
53
+ let e = t;
54
+ return e ? (t = null, e.rerun = !1, a(e), n = (async () => {
55
+ if (await e.pending?.catch(() => {}), e.reportingFailure) throw e.reportingFailure.error;
56
+ })().finally(() => {
57
+ n = null;
58
+ }), n) : Promise.resolve();
59
+ }
60
+ };
61
+ }
62
+ //#endregion
63
+ export { e as createCoalescingTaskRunner };
64
+
65
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ryuzaki13/react-foundation-lib",
3
- "version": "2.13.1",
3
+ "version": "2.14.0",
4
4
  "description": "Reusable React and TypeScript foundation utilities, hooks, and primitives.",
5
5
  "license": "MIT",
6
6
  "author": "Ryuzaki13",
@@ -37,6 +37,10 @@
37
37
  "types": "./dist/array/index.d.ts",
38
38
  "import": "./dist/array/index.js"
39
39
  },
40
+ "./async-task-runner": {
41
+ "types": "./dist/async-task-runner/index.d.ts",
42
+ "import": "./dist/async-task-runner/index.js"
43
+ },
40
44
  "./binary": {
41
45
  "types": "./dist/binary/index.d.ts",
42
46
  "import": "./dist/binary/index.js"
@@ -0,0 +1,79 @@
1
+ # Последовательный async runner
2
+
3
+ Публичный импорт: `@ryuzaki13/react-foundation-lib/async-task-runner`.
4
+ Runner объединяет повторные сигналы, повторяет отказавшую задачу и корректно
5
+ завершает lifecycle. Он не знает о HTTP, БД, React или предметном составе.
6
+ Создание и импорт не вызывают `run` и не создают таймеры.
7
+
8
+ ```ts
9
+ import { createCoalescingTaskRunner } from "@ryuzaki13/react-foundation-lib/async-task-runner";
10
+
11
+ const runner = createCoalescingTaskRunner({
12
+ run: synchronizeCurrentState,
13
+ onError: reportTaskFailure,
14
+ retryDelayMs: 5000
15
+ });
16
+ await runner.start();
17
+ // Внешний lifecycle явно запускает и завершает ресурс.
18
+ await runner.stop();
19
+ ```
20
+
21
+ `retryDelayMs` обязателен. `pollIntervalMs` необязателен и не имеет default:
22
+ периодичность, допустимую задержку синхронизации и нагрузку выбирает приложение.
23
+ Обе задержки должны быть целыми числами от 1 до 2147483647 ms включительно;
24
+ некорректная настройка даёт `RangeError` при создании, до любых побочных эффектов.
25
+
26
+ ## Допуск и объединение сигналов
27
+
28
+ `start(): Promise<void>` создаёт поколение и первую попытку. Повторный start
29
+ идемпотентен: присоединяется к текущей попытке, если она выполняется, либо
30
+ возвращает fulfilled promise без нового прохода. Это допуск lifecycle, а не
31
+ проверка readiness или подтверждение актуальности данных во время retry.
32
+
33
+ `wake(): Promise<void>` не запускает неактивный runner. В активном поколении он
34
+ запускает попытку либо возвращает promise уже выполняющейся попытки и запрашивает
35
+ ровно один следующий проход. Любое число таких сигналов не создаёт очередь.
36
+ Следующий проход выполняется отдельной timer-задачей после завершения текущего.
37
+
38
+ Promise start/wake относится **текущей попытке**, включая её обработчик ошибки,
39
+ но не подтверждает завершение объединённого будущего хвоста. Если stop отозвал
40
+ поколение до первой microtask, callback ещё не начавшейся попытки не выполняется.
41
+ Для гарантии завершения всего уже допущенного выполнения используется stop.
42
+
43
+ Явный wake отменяет запланированную задержку retry и немедленно допускает новую
44
+ попытку, если текущая уже завершилась. Poll этого не делает и не обходит backoff.
45
+ Сигналы во время отказавшей попытки не заменяют delayed retry немедленным хвостом.
46
+ Повторный start также не ускоряет retry.
47
+
48
+ ## Отказы задачи и reporter
49
+
50
+ При отказе run обработчик `onError(error): void | Promise<void>` вызывается один
51
+ раз и ожидается, после чего планируется один delayed retry. Promise текущей
52
+ попытки отклоняется исходной task error. Запланированные фоновые вызовы имеют
53
+ внутренний rejection boundary: они не создают unhandled rejection.
54
+ Reporter отвечает за безопасную диагностику; библиотека ничего не логирует.
55
+
56
+ Если onError сам бросил исключение либо отклонил promise, точная reporter error
57
+ сохраняется, таймеры и хвост удаляются, автоматические повторы прекращаются.
58
+ Следующие start/wake отклоняются reporter error; stop ожидает текущую попытку
59
+ и тоже отклоняется этой ошибкой. Значения `undefined` и `null` не теряются.
60
+ Текущая попытка по-прежнему отклоняется исходной task error, не подменяя её.
61
+ Восстановление требует явного stop/drain, обработки его отказа и нового start.
62
+
63
+ ## Остановка и повторный запуск
64
+
65
+ `stop(): Promise<void>` сразу закрывает поколение, удаляет таймеры и накопленный
66
+ хвост, затем ждёт уже выполняющиеся run/onError. Обычная task error уже передана
67
+ onError и caller текущей попытки: stop не повторяет её как собственный отказ.
68
+ Отказ admitted задачи обрабатывается и после начала stop, но не заводит таймер.
69
+
70
+ Stop не имитирует отмену атомарной операции: уже начатая задача может завершиться
71
+ успешно, в том числе выполнить COMMIT, прежде чем завершится drain. Повторные
72
+ stop разделяют тот же drain promise. Start во время drain ждёт его; более поздний
73
+ stop отменяет такой отложенный start, не позволяя старому запросу оживить ресурс.
74
+
75
+ Нельзя ожидать собственные start, wake или stop внутри run/onError: start/wake
76
+ возвращают promise текущей попытки, а stop ждёт этот callback. Такое ожидание
77
+ создаёт цикл самодренирования. Остановка принадлежит внешнему lifecycle.
78
+ Callback, который никогда не завершается, удерживает drain; timeout и реальная
79
+ отмена внешней операции остаются ответственностью приложения.