@pravosleva/reactive-engine 1.5.13 → 1.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.
Files changed (36) hide show
  1. package/dist/core/core.batch.test.d.ts +2 -0
  2. package/dist/core/core.batch.test.d.ts.map +1 -0
  3. package/dist/core/core.computed.test.d.ts +2 -0
  4. package/dist/core/core.computed.test.d.ts.map +1 -0
  5. package/dist/core/core.d.ts.map +1 -1
  6. package/dist/core/core.di.test.d.ts +2 -0
  7. package/dist/core/core.di.test.d.ts.map +1 -0
  8. package/dist/core/core.gc.test.d.ts +2 -0
  9. package/dist/core/core.gc.test.d.ts.map +1 -0
  10. package/dist/core/core.reactive.test.d.ts +2 -0
  11. package/dist/core/core.reactive.test.d.ts.map +1 -0
  12. package/dist/core/core.resource.test.d.ts +2 -0
  13. package/dist/core/core.resource.test.d.ts.map +1 -0
  14. package/dist/core/core.signals.test.d.ts +2 -0
  15. package/dist/core/core.signals.test.d.ts.map +1 -0
  16. package/dist/core.cjs +1 -1
  17. package/dist/core.cjs.map +1 -1
  18. package/dist/core.mjs +34 -19
  19. package/dist/core.mjs.map +1 -1
  20. package/dist/decorators/withCache/withCache.cjs.map +1 -1
  21. package/dist/decorators/withCache/withCache.d.ts +2 -2
  22. package/dist/decorators/withCache/withCache.mjs.map +1 -1
  23. package/dist/decorators/withDebounce/withDebounce.cjs.map +1 -1
  24. package/dist/decorators/withDebounce/withDebounce.d.ts +2 -2
  25. package/dist/decorators/withDebounce/withDebounce.mjs.map +1 -1
  26. package/dist/decorators/withLongPolling/withLongPolling.cjs.map +1 -1
  27. package/dist/decorators/withLongPolling/withLongPolling.d.ts +2 -2
  28. package/dist/decorators/withLongPolling/withLongPolling.mjs.map +1 -1
  29. package/dist/decorators/withThrottle/withThrottle.cjs.map +1 -1
  30. package/dist/decorators/withThrottle/withThrottle.d.ts +2 -2
  31. package/dist/decorators/withThrottle/withThrottle.mjs.map +1 -1
  32. package/dist/reactive-engine.umd.js +2 -2
  33. package/dist/reactive-engine.umd.js.map +1 -1
  34. package/package.json +1 -1
  35. package/dist/core/core.test.d.ts +0 -2
  36. package/dist/core/core.test.d.ts.map +0 -1
@@ -42,10 +42,10 @@ interface DebounceOptions {
42
42
  * const debouncedFetch = withDebounce(searchProducts, { delay: 400 });
43
43
  *
44
44
  * // Интеграция с фабрикой ресурсов вашего реактивного ядра
45
- * const productSearchResource = engine.resource({
45
+ * const productSearchResource = engine.resource(
46
46
  * fetcher: debouncedFetch,
47
47
  * source: () => searchInputValue.value // Следит за сигналом строки ввода
48
- * });
48
+ * );
49
49
  * ```
50
50
  *
51
51
  * @abstract
@@ -1 +1 @@
1
- {"version":3,"file":"withDebounce.mjs","names":[],"sources":["../../../src/decorators/withDebounce/withDebounce.ts"],"sourcesContent":["interface DebounceOptions {\n /** Задержка в миллисекундах. По умолчанию 300 мс */\n delay?: number;\n}\n\n/**\n * Декоратор для создания дебаунсящего (отложенного) загрузчика данных, специально\n * адаптированный для использования совместно с `engine.resource`.\n *\n * Откладывает выполнение асинхронной функции `fetcher` на заданную задержку `options.delay`.\n * Если в течение этого интервала происходит новый вызов (например, пользователь продолжает вводить текст),\n * таймер сбрасывается, а предыдущий ожидающий промис принудительно отклоняется. В итоге выполняется\n * только один запрос — после того, как поток вызовов затихнет.\n *\n * ### 🧠 Механика управления промисами (Контур Debounce):\n * 1. **Сброс дребезга:** При каждом вызове функции проверяется наличие активного таймера. Если он есть,\n * `clearTimeout` мгновенно останавливает его.\n * 2. **Каскадное отклонение:** Ссылка на метод `reject` каждого создаваемого промиса сохраняется в `rejectPrevious`.\n * При поступлении нового запроса предыдущий промис не остается «зависшим», а принудительно переходит\n * в состояние `rejected` с ошибкой `AbortError`. Это сигнализирует ядру, что данные устарели.\n * 3. **Чистый запуск:** Оригинальный `fetcher` вызывается только тогда, когда таймер успешно дотикал до конца,\n * не будучи прерванным.\n *\n * @template S Тип входных данных (аргументов) для функции запроса.\n * @template T Тип данных, возвращаемых асинхронным `fetcher`-ом (разрешенное значение промиса).\n *\n * @param {(source: S, signal: AbortSignal) => Promise<T>} fetcher Оригинальная асинхронная функция запроса, принимающая параметры и сигнал отмены.\n * @param {DebounceOptions} [options={}] Параметры конфигурации дебаунса.\n * @param {number} [options.delay=300] Время ожидания в миллисекундах с момента последнего вызова до фактического старта запроса.\n *\n * @returns {(source: S, signal: AbortSignal) => Promise<T>} Возвращает обернутую дебаунс-функцию, возвращающую `Promise<T>` и полностью сохраняющую исходную сигнатуру типов.\n *\n * @example\n * ```typescript\n * import { withDebounce } from '@pravosleva/reactive-engine';\n *\n * const searchProducts = async (query: string, signal: AbortSignal) => {\n * const res = await fetch(`/api/products?search=${encodeURIComponent(query)}`, { signal });\n * return res.json();\n * };\n *\n * // Запрос уйдет только через 400мс после того, как пользователь перестанет нажимать клавиши\n * const debouncedFetch = withDebounce(searchProducts, { delay: 400 });\n *\n * // Интеграция с фабрикой ресурсов вашего реактивного ядра\n * const productSearchResource = engine.resource({\n * fetcher: debouncedFetch,\n * source: () => searchInputValue.value // Следит за сигналом строки ввода\n * });\n * ```\n *\n * @abstract\n * ### 🚨 Сквозная интеграция с AbortSignal и менеджмент памяти\n * Декоратор непрерывно отслеживает состояние нативного `AbortSignal`, передаваемого движком (например, при размонтировании UI-виджета):\n * - Если сигнал переходит в состояние `aborted` во время отсчета таймаута дебаунса, таймер сбрасывается,\n * а промис мгновенно отклоняется.\n * - При успешном выполнении таймера слушатель `abort` своевременно удаляется через `removeEventListener`,\n * исключая накопление холостых подписок.\n * - Все внутренние ссылки на методы разрешения промисов (`rejectPrevious`, `timeoutId`) принудительно зануляются (`null`),\n * очищая замыкания от застрявших в памяти объектов и защищая Node.js/браузер от утечек.\n */\nexport const withDebounce = <S, T>(\n fetcher: (source: S, signal: AbortSignal) => Promise<T>,\n options: DebounceOptions = {}\n) => {\n const delay = options.delay ?? 300\n\n // Храним ссылку на активный таймер текущего ожидания\n let timeoutId: ReturnType<typeof setTimeout> | null = null\n // Храним функцию отмены для предыдущего неоконченного промиса ожидания\n let rejectPrevious: ((reason: any) => void) | null = null\n\n return (source: S, signal: AbortSignal): Promise<T> => {\n // 1. Если уже идет ожидание другого запроса — отменяем его промис и очищаем таймер\n if (timeoutId) {\n clearTimeout(timeoutId)\n }\n if (rejectPrevious) {\n // Бросаем DOMException, имитируя нативную отмену fetch, чтобы движок понял, что запрос прерван\n rejectPrevious(new DOMException('Aborted due to debounce', 'AbortError'))\n }\n\n return new Promise<T>((resolve, reject) => {\n // Сохраняем ссылку на reject текущего промиса, чтобы его мог отменить следующий вызов\n rejectPrevious = reject\n\n // 2. Слушаем нативный AbortSignal от движка (например, если компонент размонтировался во время дебаунса)\n const onAbort = () => {\n if (timeoutId) clearTimeout(timeoutId)\n reject(new DOMException('Aborted by resource signal', 'AbortError'))\n }\n\n if (signal.aborted) {\n return onAbort()\n }\n signal.addEventListener('abort', onAbort)\n\n // 3. Взводим отложенный таймер выполнения\n timeoutId = setTimeout(async () => {\n // Убираем слушатель, так как таймаут успешно дождался конца\n signal.removeEventListener('abort', onAbort)\n rejectPrevious = null\n timeoutId = null\n\n try {\n // 4. Запускаем оригинальный fetcher\n const data = await fetcher(source, signal)\n resolve(data)\n } catch (error) {\n reject(error)\n }\n }, delay)\n })\n }\n}\n"],"mappings":";AA6DA,IAAa,KACX,GACA,IAA2B,CAAC,MACzB;CACH,IAAM,IAAQ,EAAQ,SAAS,KAG3B,IAAkD,MAElD,IAAiD;CAErD,QAAQ,GAAW,OAEb,KACF,aAAa,CAAS,GAEpB,KAEF,EAAe,IAAI,aAAa,2BAA2B,YAAY,CAAC,GAGnE,IAAI,SAAY,GAAS,MAAW;EAEzC,IAAiB;EAGjB,IAAM,UAAgB;GAEpB,AADI,KAAW,aAAa,CAAS,GACrC,EAAO,IAAI,aAAa,8BAA8B,YAAY,CAAC;EACrE;EAEA,IAAI,EAAO,SACT,OAAO,EAAQ;EAKjB,AAHA,EAAO,iBAAiB,SAAS,CAAO,GAGxC,IAAY,WAAW,YAAY;GAIjC,AAFA,EAAO,oBAAoB,SAAS,CAAO,GAC3C,IAAiB,MACjB,IAAY;GAEZ,IAAI;IAGF,EAAQ,MADW,EAAQ,GAAQ,CAAM,CAC7B;GACd,SAAS,GAAO;IACd,EAAO,CAAK;GACd;EACF,GAAG,CAAK;CACV,CAAC;AAEL"}
1
+ {"version":3,"file":"withDebounce.mjs","names":[],"sources":["../../../src/decorators/withDebounce/withDebounce.ts"],"sourcesContent":["interface DebounceOptions {\n /** Задержка в миллисекундах. По умолчанию 300 мс */\n delay?: number;\n}\n\n/**\n * Декоратор для создания дебаунсящего (отложенного) загрузчика данных, специально\n * адаптированный для использования совместно с `engine.resource`.\n *\n * Откладывает выполнение асинхронной функции `fetcher` на заданную задержку `options.delay`.\n * Если в течение этого интервала происходит новый вызов (например, пользователь продолжает вводить текст),\n * таймер сбрасывается, а предыдущий ожидающий промис принудительно отклоняется. В итоге выполняется\n * только один запрос — после того, как поток вызовов затихнет.\n *\n * ### 🧠 Механика управления промисами (Контур Debounce):\n * 1. **Сброс дребезга:** При каждом вызове функции проверяется наличие активного таймера. Если он есть,\n * `clearTimeout` мгновенно останавливает его.\n * 2. **Каскадное отклонение:** Ссылка на метод `reject` каждого создаваемого промиса сохраняется в `rejectPrevious`.\n * При поступлении нового запроса предыдущий промис не остается «зависшим», а принудительно переходит\n * в состояние `rejected` с ошибкой `AbortError`. Это сигнализирует ядру, что данные устарели.\n * 3. **Чистый запуск:** Оригинальный `fetcher` вызывается только тогда, когда таймер успешно дотикал до конца,\n * не будучи прерванным.\n *\n * @template S Тип входных данных (аргументов) для функции запроса.\n * @template T Тип данных, возвращаемых асинхронным `fetcher`-ом (разрешенное значение промиса).\n *\n * @param {(source: S, signal: AbortSignal) => Promise<T>} fetcher Оригинальная асинхронная функция запроса, принимающая параметры и сигнал отмены.\n * @param {DebounceOptions} [options={}] Параметры конфигурации дебаунса.\n * @param {number} [options.delay=300] Время ожидания в миллисекундах с момента последнего вызова до фактического старта запроса.\n *\n * @returns {(source: S, signal: AbortSignal) => Promise<T>} Возвращает обернутую дебаунс-функцию, возвращающую `Promise<T>` и полностью сохраняющую исходную сигнатуру типов.\n *\n * @example\n * ```typescript\n * import { withDebounce } from '@pravosleva/reactive-engine';\n *\n * const searchProducts = async (query: string, signal: AbortSignal) => {\n * const res = await fetch(`/api/products?search=${encodeURIComponent(query)}`, { signal });\n * return res.json();\n * };\n *\n * // Запрос уйдет только через 400мс после того, как пользователь перестанет нажимать клавиши\n * const debouncedFetch = withDebounce(searchProducts, { delay: 400 });\n *\n * // Интеграция с фабрикой ресурсов вашего реактивного ядра\n * const productSearchResource = engine.resource(\n * fetcher: debouncedFetch,\n * source: () => searchInputValue.value // Следит за сигналом строки ввода\n * );\n * ```\n *\n * @abstract\n * ### 🚨 Сквозная интеграция с AbortSignal и менеджмент памяти\n * Декоратор непрерывно отслеживает состояние нативного `AbortSignal`, передаваемого движком (например, при размонтировании UI-виджета):\n * - Если сигнал переходит в состояние `aborted` во время отсчета таймаута дебаунса, таймер сбрасывается,\n * а промис мгновенно отклоняется.\n * - При успешном выполнении таймера слушатель `abort` своевременно удаляется через `removeEventListener`,\n * исключая накопление холостых подписок.\n * - Все внутренние ссылки на методы разрешения промисов (`rejectPrevious`, `timeoutId`) принудительно зануляются (`null`),\n * очищая замыкания от застрявших в памяти объектов и защищая Node.js/браузер от утечек.\n */\nexport const withDebounce = <S, T>(\n fetcher: (source: S, signal: AbortSignal) => Promise<T>,\n options: DebounceOptions = {}\n) => {\n const delay = options.delay ?? 300\n\n // Храним ссылку на активный таймер текущего ожидания\n let timeoutId: ReturnType<typeof setTimeout> | null = null\n // Храним функцию отмены для предыдущего неоконченного промиса ожидания\n let rejectPrevious: ((reason: any) => void) | null = null\n\n return (source: S, signal: AbortSignal): Promise<T> => {\n // 1. Если уже идет ожидание другого запроса — отменяем его промис и очищаем таймер\n if (timeoutId) {\n clearTimeout(timeoutId)\n }\n if (rejectPrevious) {\n // Бросаем DOMException, имитируя нативную отмену fetch, чтобы движок понял, что запрос прерван\n rejectPrevious(new DOMException('Aborted due to debounce', 'AbortError'))\n }\n\n return new Promise<T>((resolve, reject) => {\n // Сохраняем ссылку на reject текущего промиса, чтобы его мог отменить следующий вызов\n rejectPrevious = reject\n\n // 2. Слушаем нативный AbortSignal от движка (например, если компонент размонтировался во время дебаунса)\n const onAbort = () => {\n if (timeoutId) clearTimeout(timeoutId)\n reject(new DOMException('Aborted by resource signal', 'AbortError'))\n }\n\n if (signal.aborted) {\n return onAbort()\n }\n signal.addEventListener('abort', onAbort)\n\n // 3. Взводим отложенный таймер выполнения\n timeoutId = setTimeout(async () => {\n // Убираем слушатель, так как таймаут успешно дождался конца\n signal.removeEventListener('abort', onAbort)\n rejectPrevious = null\n timeoutId = null\n\n try {\n // 4. Запускаем оригинальный fetcher\n const data = await fetcher(source, signal)\n resolve(data)\n } catch (error) {\n reject(error)\n }\n }, delay)\n })\n }\n}\n"],"mappings":";AA6DA,IAAa,KACX,GACA,IAA2B,CAAC,MACzB;CACH,IAAM,IAAQ,EAAQ,SAAS,KAG3B,IAAkD,MAElD,IAAiD;CAErD,QAAQ,GAAW,OAEb,KACF,aAAa,CAAS,GAEpB,KAEF,EAAe,IAAI,aAAa,2BAA2B,YAAY,CAAC,GAGnE,IAAI,SAAY,GAAS,MAAW;EAEzC,IAAiB;EAGjB,IAAM,UAAgB;GAEpB,AADI,KAAW,aAAa,CAAS,GACrC,EAAO,IAAI,aAAa,8BAA8B,YAAY,CAAC;EACrE;EAEA,IAAI,EAAO,SACT,OAAO,EAAQ;EAKjB,AAHA,EAAO,iBAAiB,SAAS,CAAO,GAGxC,IAAY,WAAW,YAAY;GAIjC,AAFA,EAAO,oBAAoB,SAAS,CAAO,GAC3C,IAAiB,MACjB,IAAY;GAEZ,IAAI;IAGF,EAAQ,MADW,EAAQ,GAAQ,CAAM,CAC7B;GACd,SAAS,GAAO;IACd,EAAO,CAAK;GACd;EACF,GAAG,CAAK;CACV,CAAC;AAEL"}
@@ -1 +1 @@
1
- {"version":3,"file":"withLongPolling.cjs","names":[],"sources":["../../../src/decorators/withLongPolling/withLongPolling.ts"],"sourcesContent":["interface LongPollingOptions {\n /** Коллбэк для инкремента реактивного тика (перехода на следующую итерацию поллинга) */\n onNextTick: () => void;\n\n /**\n * Коллбэк, вызываемый при возникновении сетевой ошибки.\n * Позволяет внешней системе узнать о сбое и текущем времени ожидания до следующей попытки.\n *\n * @param {number} delayMs - Текущая задержка Exponential Backoff в миллисекундах перед следующим запросом.\n * @param {() => void} onRetryScheduled - Коллбэк-триггер. Должен быть вызван один раз, чтобы\n * просигнализировать декоратору, что шаг зафиксирован и задержка для следующей ошибки может быть увеличена.\n */\n onError: (delayMs: number, onRetryScheduled: () => void) => void;\n\n /** Пауза перед открытием следующего соединения в миллисекундах. По умолчанию 500 мс */\n delay?: number;\n /** Стартовая задержка Exponential Backoff в миллисекундах. По умолчанию 2000 мс */\n errorInitialDelay?: number;\n /** Потолок задержки Exponential Backoff в миллисекундах. По умолчанию 8000 мс */\n errorMaxDelay?: number;\n /** Внешний сигнал для жесткой остановки рекурсивных таймаутов */\n externalSignal?: AbortSignal;\n}\n\n/**\n * Адаптированный декоратор для реализации паттерна Long Polling (длинные опросы),\n * спроектированный специально для интеграции с подсистемой `engine.resource`.\n *\n * Превращает разовый асинхронный запрос данных в непрерывный контролируемый конвейер\n * циклического опроса сервера с поддержкой экспоненциального отката при ошибках\n * (Exponential Backoff) и защитой от пересечения параллельных сессий.\n *\n * ### 🧠 Архитектура конвейера опроса:\n * 1. **Контроль сессионных токенов:** Каждый новый вызов функции инкрементирует `currentSessionToken`.\n * Если предыдущий запрос завершится позже, таймер следующего шага не взведется, так как\n * идентификаторы сессий (`activeSessionId === currentSessionToken`) не совпадут. Это полностью исключает утечки и гонки ответов.\n * 2. **Экспоненциальный откат (Exponential Backoff):** При успешном запросе интервал между циклами равен `options.delay`.\n * В случае падения сетевого запроса, время ожидания перед следующим опросом прогрессивно удваивается,\n * начиная с `errorInitialDelay` вплоть до достижения лимита `errorMaxDelay`, чтобы не спамить упавший бэкенд.\n * 3. **Двойной контур отмены:** Поддерживает как локальный `AbortSignal` от ресурса, так и глобальный `externalSignal`\n * (например, для остановки всех фоновых процессов при закрытии приложения).\n *\n * @template S Тип входных данных (аргументов) для функции запроса.\n * @template T Тип данных, возвращаемых асинхронным `fetcher`-ом (разрешенное значение промиса).\n *\n * @param {(source: S, signal: AbortSignal) => Promise<T>} fetcher Асинхронная функция-загрузчик (например, fetch-запрос к эндпоинту уведомлений).\n * @param {LongPollingOptions} options Конфигурация параметров и коллбэков длинных опросов.\n * @param {() => void} options.onNextTick Коллбэк-триггер, вызываемый для перезапуска цикла опроса (в контексте ядра обычно мутирует или инвалидирует `source` ресурса).\n * @param {(currentDelay: number, increaseDelay: () => void) => void} [options.onError] Коллбэк, вызываемый при ошибке сети. Позволяет логировать сбои и управлять шагом экспоненциального отката через запуск функции `increaseDelay()`.\n * @param {AbortSignal} [options.externalSignal] Внешний сигнал отмены для принудительной остановки поллинга извне (независимо от жизненного цикла ресурса).\n * @param {number} [options.delay=500] Базовая задержка в миллисекундах между успешным ответом сервера и началом следующего запроса.\n * @param {number} [options.errorInitialDelay=2000] Стартовая задержка в миллисекундах при возникновении первой ошибки.\n * @param {number} [options.errorMaxDelay=8000] Максимально возможный интервал задержки в миллисекундах при череде последовательных ошибок.\n *\n * @returns {(source: S, signal: AbortSignal) => Promise<T>} Возвращает обернутую циклическую функцию, возвращающую `Promise<T>`.\n *\n * @example\n * ```typescript\n * import { withLongPolling } from '@pravosleva/reactive-engine';\n *\n * // 1. Настраиваем функцию опроса новостной ленты\n * const pollingFetch = withLongPolling(\n * async (userId: string, signal) => {\n * const res = await fetch(`/api/notifications?uid=${userId}`, { signal });\n * return res.json();\n * },\n * {\n * delay: 1000,\n * errorInitialDelay: 2000,\n * errorMaxDelay: 10000,\n * onNextTick: () => {\n * // Инвалидируем или триггерим обновление ресурса в ядре\n * notificationResource.refresh();\n * },\n * onError: (delay, increaseDelay) => {\n * console.warn(`Ошибка сети. Следующая попытка через ${delay}мс`);\n * increaseDelay(); // Удваиваем интервал до следующего тика\n * }\n * }\n * );\n *\n * // 2. Передаем декоратор в подсистему ресурсов ядра\n * const notificationResource = engine.resource({\n * fetcher: pollingFetch,\n * source: () => currentUserId.value\n * });\n * ```\n *\n * @abstract\n * ### 🚨 Поведение при отмене и очистка слушателей\n * При срабатывании любого из сигналов отмены (`signal` или `externalSignal` переходит в состояние `aborted`):\n * - Текущий ожидающий промис немедленно отклоняется с системным исключением `AbortError`.\n * - Цикл поллинга блокируется и полностью прекращает планирование будущих макрозадач `setTimeout`.\n * - Все подписки на события `'abort'` корректно удаляются, предотвращая скрытые утечки памяти в Node.js / браузере.\n */\nexport const withLongPolling = <S, T>(\n fetcher: (source: S, signal: AbortSignal) => Promise<T>,\n options: LongPollingOptions\n) => {\n const {\n onNextTick,\n onError,\n externalSignal,\n delay = 500,\n errorInitialDelay = 2000,\n errorMaxDelay = 8000\n } = options\n\n let currentErrorDelay = errorInitialDelay\n let currentSessionToken = 0\n\n return (source: S, signal: AbortSignal): Promise<T> => {\n const activeSessionId = ++currentSessionToken\n\n return new Promise<T>((resolve, reject) => {\n\n const poll = async () => {\n if (signal.aborted || externalSignal?.aborted) {\n reject(new DOMException('Aborted by long polling lifecycle', 'AbortError'))\n return\n }\n\n try {\n const data = await fetcher(source, signal)\n currentErrorDelay = errorInitialDelay\n\n if (signal.aborted || externalSignal?.aborted) {\n reject(new DOMException('Aborted by long polling lifecycle', 'AbortError'))\n return\n }\n\n resolve(data)\n\n setTimeout(() => {\n if (activeSessionId === currentSessionToken && !signal.aborted && !externalSignal?.aborted) {\n onNextTick?.()\n }\n }, delay)\n\n } catch (error) {\n if (error instanceof DOMException && error.name === 'AbortError') {\n reject(error)\n return\n }\n\n reject(error)\n const delayDuration = currentErrorDelay\n\n if (typeof onError === 'function') onError(delayDuration, () => {\n if (activeSessionId === currentSessionToken) {\n currentErrorDelay = Math.min(currentErrorDelay * 2, errorMaxDelay)\n }\n })\n\n setTimeout(() => {\n if (activeSessionId === currentSessionToken && !signal.aborted && !externalSignal?.aborted) {\n onNextTick?.()\n }\n }, delayDuration)\n }\n }\n\n const forceKillTimer = () => {\n reject(new DOMException('Aborted by lifecycle event', 'AbortError'))\n }\n\n signal.addEventListener('abort', forceKillTimer)\n if (externalSignal) {\n externalSignal.addEventListener('abort', forceKillTimer)\n }\n\n if (signal.aborted || externalSignal?.aborted) {\n return forceKillTimer()\n }\n\n poll()\n })\n }\n}\n"],"mappings":"AA+FA,IAAa,GACX,EACA,IACG,CACH,GAAM,CACJ,aACA,UACA,iBACA,QAAQ,IACR,oBAAoB,IACpB,gBAAgB,KACd,EAEA,EAAoB,EACpB,EAAsB,EAE1B,OAAQ,EAAW,IAAoC,CACrD,IAAM,EAAkB,EAAE,EAE1B,OAAO,IAAI,SAAY,EAAS,IAAW,CAEzC,IAAM,EAAO,SAAY,CACvB,GAAI,EAAO,SAAW,GAAgB,QAAS,CAC7C,EAAO,IAAI,aAAa,oCAAqC,YAAY,CAAC,EAC1E,MACF,CAEA,GAAI,CACF,IAAM,EAAO,MAAM,EAAQ,EAAQ,CAAM,EAGzC,GAFA,EAAoB,EAEhB,EAAO,SAAW,GAAgB,QAAS,CAC7C,EAAO,IAAI,aAAa,oCAAqC,YAAY,CAAC,EAC1E,MACF,CAEA,EAAQ,CAAI,EAEZ,eAAiB,CACX,IAAoB,GAAuB,CAAC,EAAO,SAAW,CAAC,GAAgB,SACjF,IAAa,CAEjB,EAAG,CAAK,CAEV,OAAS,EAAO,CACd,GAAI,aAAiB,cAAgB,EAAM,OAAS,aAAc,CAChE,EAAO,CAAK,EACZ,MACF,CAEA,EAAO,CAAK,EACZ,IAAM,EAAgB,EAElB,OAAO,GAAY,YAAY,EAAQ,MAAqB,CAC1D,IAAoB,IACtB,EAAoB,KAAK,IAAI,EAAoB,EAAG,CAAa,EAErE,CAAC,EAED,eAAiB,CACX,IAAoB,GAAuB,CAAC,EAAO,SAAW,CAAC,GAAgB,SACjF,IAAa,CAEjB,EAAG,CAAa,CAClB,CACF,EAEM,MAAuB,CAC3B,EAAO,IAAI,aAAa,6BAA8B,YAAY,CAAC,CACrE,EAOA,GALA,EAAO,iBAAiB,QAAS,CAAc,EAC3C,GACF,EAAe,iBAAiB,QAAS,CAAc,EAGrD,EAAO,SAAW,GAAgB,QACpC,OAAO,EAAe,EAGxB,EAAK,CACP,CAAC,CACH,CACF"}
1
+ {"version":3,"file":"withLongPolling.cjs","names":[],"sources":["../../../src/decorators/withLongPolling/withLongPolling.ts"],"sourcesContent":["interface LongPollingOptions {\n /** Коллбэк для инкремента реактивного тика (перехода на следующую итерацию поллинга) */\n onNextTick: () => void;\n\n /**\n * Коллбэк, вызываемый при возникновении сетевой ошибки.\n * Позволяет внешней системе узнать о сбое и текущем времени ожидания до следующей попытки.\n *\n * @param {number} delayMs - Текущая задержка Exponential Backoff в миллисекундах перед следующим запросом.\n * @param {() => void} onRetryScheduled - Коллбэк-триггер. Должен быть вызван один раз, чтобы\n * просигнализировать декоратору, что шаг зафиксирован и задержка для следующей ошибки может быть увеличена.\n */\n onError: (delayMs: number, onRetryScheduled: () => void) => void;\n\n /** Пауза перед открытием следующего соединения в миллисекундах. По умолчанию 500 мс */\n delay?: number;\n /** Стартовая задержка Exponential Backoff в миллисекундах. По умолчанию 2000 мс */\n errorInitialDelay?: number;\n /** Потолок задержки Exponential Backoff в миллисекундах. По умолчанию 8000 мс */\n errorMaxDelay?: number;\n /** Внешний сигнал для жесткой остановки рекурсивных таймаутов */\n externalSignal?: AbortSignal;\n}\n\n/**\n * Адаптированный декоратор для реализации паттерна Long Polling (длинные опросы),\n * спроектированный специально для интеграции с подсистемой `engine.resource`.\n *\n * Превращает разовый асинхронный запрос данных в непрерывный контролируемый конвейер\n * циклического опроса сервера с поддержкой экспоненциального отката при ошибках\n * (Exponential Backoff) и защитой от пересечения параллельных сессий.\n *\n * ### 🧠 Архитектура конвейера опроса:\n * 1. **Контроль сессионных токенов:** Каждый новый вызов функции инкрементирует `currentSessionToken`.\n * Если предыдущий запрос завершится позже, таймер следующего шага не взведется, так как\n * идентификаторы сессий (`activeSessionId === currentSessionToken`) не совпадут. Это полностью исключает утечки и гонки ответов.\n * 2. **Экспоненциальный откат (Exponential Backoff):** При успешном запросе интервал между циклами равен `options.delay`.\n * В случае падения сетевого запроса, время ожидания перед следующим опросом прогрессивно удваивается,\n * начиная с `errorInitialDelay` вплоть до достижения лимита `errorMaxDelay`, чтобы не спамить упавший бэкенд.\n * 3. **Двойной контур отмены:** Поддерживает как локальный `AbortSignal` от ресурса, так и глобальный `externalSignal`\n * (например, для остановки всех фоновых процессов при закрытии приложения).\n *\n * @template S Тип входных данных (аргументов) для функции запроса.\n * @template T Тип данных, возвращаемых асинхронным `fetcher`-ом (разрешенное значение промиса).\n *\n * @param {(source: S, signal: AbortSignal) => Promise<T>} fetcher Асинхронная функция-загрузчик (например, fetch-запрос к эндпоинту уведомлений).\n * @param {LongPollingOptions} options Конфигурация параметров и коллбэков длинных опросов.\n * @param {() => void} options.onNextTick Коллбэк-триггер, вызываемый для перезапуска цикла опроса (в контексте ядра обычно мутирует или инвалидирует `source` ресурса).\n * @param {(currentDelay: number, increaseDelay: () => void) => void} [options.onError] Коллбэк, вызываемый при ошибке сети. Позволяет логировать сбои и управлять шагом экспоненциального отката через запуск функции `increaseDelay()`.\n * @param {AbortSignal} [options.externalSignal] Внешний сигнал отмены для принудительной остановки поллинга извне (независимо от жизненного цикла ресурса).\n * @param {number} [options.delay=500] Базовая задержка в миллисекундах между успешным ответом сервера и началом следующего запроса.\n * @param {number} [options.errorInitialDelay=2000] Стартовая задержка в миллисекундах при возникновении первой ошибки.\n * @param {number} [options.errorMaxDelay=8000] Максимально возможный интервал задержки в миллисекундах при череде последовательных ошибок.\n *\n * @returns {(source: S, signal: AbortSignal) => Promise<T>} Возвращает обернутую циклическую функцию, возвращающую `Promise<T>`.\n *\n * @example\n * ```typescript\n * import { withLongPolling } from '@pravosleva/reactive-engine';\n *\n * // 1. Настраиваем функцию опроса новостной ленты\n * const pollingFetch = withLongPolling(\n * async (userId: string, signal) => {\n * const res = await fetch(`/api/notifications?uid=${userId}`, { signal });\n * return res.json();\n * },\n * {\n * delay: 1000,\n * errorInitialDelay: 2000,\n * errorMaxDelay: 10000,\n * onNextTick: () => {\n * // Инвалидируем или триггерим обновление ресурса в ядре\n * notificationResource.refresh();\n * },\n * onError: (delay, increaseDelay) => {\n * console.warn(`Ошибка сети. Следующая попытка через ${delay}мс`);\n * increaseDelay(); // Удваиваем интервал до следующего тика\n * }\n * }\n * );\n *\n * // 2. Передаем декоратор в подсистему ресурсов ядра\n * const notificationResource = engine.resource(\n * fetcher: pollingFetch,\n * source: () => currentUserId.value\n * );\n * ```\n *\n * @abstract\n * ### 🚨 Поведение при отмене и очистка слушателей\n * При срабатывании любого из сигналов отмены (`signal` или `externalSignal` переходит в состояние `aborted`):\n * - Текущий ожидающий промис немедленно отклоняется с системным исключением `AbortError`.\n * - Цикл поллинга блокируется и полностью прекращает планирование будущих макрозадач `setTimeout`.\n * - Все подписки на события `'abort'` корректно удаляются, предотвращая скрытые утечки памяти в Node.js / браузере.\n */\nexport const withLongPolling = <S, T>(\n fetcher: (source: S, signal: AbortSignal) => Promise<T>,\n options: LongPollingOptions\n) => {\n const {\n onNextTick,\n onError,\n externalSignal,\n delay = 500,\n errorInitialDelay = 2000,\n errorMaxDelay = 8000\n } = options\n\n let currentErrorDelay = errorInitialDelay\n let currentSessionToken = 0\n\n return (source: S, signal: AbortSignal): Promise<T> => {\n const activeSessionId = ++currentSessionToken\n\n return new Promise<T>((resolve, reject) => {\n\n const poll = async () => {\n if (signal.aborted || externalSignal?.aborted) {\n reject(new DOMException('Aborted by long polling lifecycle', 'AbortError'))\n return\n }\n\n try {\n const data = await fetcher(source, signal)\n currentErrorDelay = errorInitialDelay\n\n if (signal.aborted || externalSignal?.aborted) {\n reject(new DOMException('Aborted by long polling lifecycle', 'AbortError'))\n return\n }\n\n resolve(data)\n\n setTimeout(() => {\n if (activeSessionId === currentSessionToken && !signal.aborted && !externalSignal?.aborted) {\n onNextTick?.()\n }\n }, delay)\n\n } catch (error) {\n if (error instanceof DOMException && error.name === 'AbortError') {\n reject(error)\n return\n }\n\n reject(error)\n const delayDuration = currentErrorDelay\n\n if (typeof onError === 'function') onError(delayDuration, () => {\n if (activeSessionId === currentSessionToken) {\n currentErrorDelay = Math.min(currentErrorDelay * 2, errorMaxDelay)\n }\n })\n\n setTimeout(() => {\n if (activeSessionId === currentSessionToken && !signal.aborted && !externalSignal?.aborted) {\n onNextTick?.()\n }\n }, delayDuration)\n }\n }\n\n const forceKillTimer = () => {\n reject(new DOMException('Aborted by lifecycle event', 'AbortError'))\n }\n\n signal.addEventListener('abort', forceKillTimer)\n if (externalSignal) {\n externalSignal.addEventListener('abort', forceKillTimer)\n }\n\n if (signal.aborted || externalSignal?.aborted) {\n return forceKillTimer()\n }\n\n poll()\n })\n }\n}\n"],"mappings":"AA+FA,IAAa,GACX,EACA,IACG,CACH,GAAM,CACJ,aACA,UACA,iBACA,QAAQ,IACR,oBAAoB,IACpB,gBAAgB,KACd,EAEA,EAAoB,EACpB,EAAsB,EAE1B,OAAQ,EAAW,IAAoC,CACrD,IAAM,EAAkB,EAAE,EAE1B,OAAO,IAAI,SAAY,EAAS,IAAW,CAEzC,IAAM,EAAO,SAAY,CACvB,GAAI,EAAO,SAAW,GAAgB,QAAS,CAC7C,EAAO,IAAI,aAAa,oCAAqC,YAAY,CAAC,EAC1E,MACF,CAEA,GAAI,CACF,IAAM,EAAO,MAAM,EAAQ,EAAQ,CAAM,EAGzC,GAFA,EAAoB,EAEhB,EAAO,SAAW,GAAgB,QAAS,CAC7C,EAAO,IAAI,aAAa,oCAAqC,YAAY,CAAC,EAC1E,MACF,CAEA,EAAQ,CAAI,EAEZ,eAAiB,CACX,IAAoB,GAAuB,CAAC,EAAO,SAAW,CAAC,GAAgB,SACjF,IAAa,CAEjB,EAAG,CAAK,CAEV,OAAS,EAAO,CACd,GAAI,aAAiB,cAAgB,EAAM,OAAS,aAAc,CAChE,EAAO,CAAK,EACZ,MACF,CAEA,EAAO,CAAK,EACZ,IAAM,EAAgB,EAElB,OAAO,GAAY,YAAY,EAAQ,MAAqB,CAC1D,IAAoB,IACtB,EAAoB,KAAK,IAAI,EAAoB,EAAG,CAAa,EAErE,CAAC,EAED,eAAiB,CACX,IAAoB,GAAuB,CAAC,EAAO,SAAW,CAAC,GAAgB,SACjF,IAAa,CAEjB,EAAG,CAAa,CAClB,CACF,EAEM,MAAuB,CAC3B,EAAO,IAAI,aAAa,6BAA8B,YAAY,CAAC,CACrE,EAOA,GALA,EAAO,iBAAiB,QAAS,CAAc,EAC3C,GACF,EAAe,iBAAiB,QAAS,CAAc,EAGrD,EAAO,SAAW,GAAgB,QACpC,OAAO,EAAe,EAGxB,EAAK,CACP,CAAC,CACH,CACF"}
@@ -77,10 +77,10 @@ interface LongPollingOptions {
77
77
  * );
78
78
  *
79
79
  * // 2. Передаем декоратор в подсистему ресурсов ядра
80
- * const notificationResource = engine.resource({
80
+ * const notificationResource = engine.resource(
81
81
  * fetcher: pollingFetch,
82
82
  * source: () => currentUserId.value
83
- * });
83
+ * );
84
84
  * ```
85
85
  *
86
86
  * @abstract
@@ -1 +1 @@
1
- {"version":3,"file":"withLongPolling.mjs","names":[],"sources":["../../../src/decorators/withLongPolling/withLongPolling.ts"],"sourcesContent":["interface LongPollingOptions {\n /** Коллбэк для инкремента реактивного тика (перехода на следующую итерацию поллинга) */\n onNextTick: () => void;\n\n /**\n * Коллбэк, вызываемый при возникновении сетевой ошибки.\n * Позволяет внешней системе узнать о сбое и текущем времени ожидания до следующей попытки.\n *\n * @param {number} delayMs - Текущая задержка Exponential Backoff в миллисекундах перед следующим запросом.\n * @param {() => void} onRetryScheduled - Коллбэк-триггер. Должен быть вызван один раз, чтобы\n * просигнализировать декоратору, что шаг зафиксирован и задержка для следующей ошибки может быть увеличена.\n */\n onError: (delayMs: number, onRetryScheduled: () => void) => void;\n\n /** Пауза перед открытием следующего соединения в миллисекундах. По умолчанию 500 мс */\n delay?: number;\n /** Стартовая задержка Exponential Backoff в миллисекундах. По умолчанию 2000 мс */\n errorInitialDelay?: number;\n /** Потолок задержки Exponential Backoff в миллисекундах. По умолчанию 8000 мс */\n errorMaxDelay?: number;\n /** Внешний сигнал для жесткой остановки рекурсивных таймаутов */\n externalSignal?: AbortSignal;\n}\n\n/**\n * Адаптированный декоратор для реализации паттерна Long Polling (длинные опросы),\n * спроектированный специально для интеграции с подсистемой `engine.resource`.\n *\n * Превращает разовый асинхронный запрос данных в непрерывный контролируемый конвейер\n * циклического опроса сервера с поддержкой экспоненциального отката при ошибках\n * (Exponential Backoff) и защитой от пересечения параллельных сессий.\n *\n * ### 🧠 Архитектура конвейера опроса:\n * 1. **Контроль сессионных токенов:** Каждый новый вызов функции инкрементирует `currentSessionToken`.\n * Если предыдущий запрос завершится позже, таймер следующего шага не взведется, так как\n * идентификаторы сессий (`activeSessionId === currentSessionToken`) не совпадут. Это полностью исключает утечки и гонки ответов.\n * 2. **Экспоненциальный откат (Exponential Backoff):** При успешном запросе интервал между циклами равен `options.delay`.\n * В случае падения сетевого запроса, время ожидания перед следующим опросом прогрессивно удваивается,\n * начиная с `errorInitialDelay` вплоть до достижения лимита `errorMaxDelay`, чтобы не спамить упавший бэкенд.\n * 3. **Двойной контур отмены:** Поддерживает как локальный `AbortSignal` от ресурса, так и глобальный `externalSignal`\n * (например, для остановки всех фоновых процессов при закрытии приложения).\n *\n * @template S Тип входных данных (аргументов) для функции запроса.\n * @template T Тип данных, возвращаемых асинхронным `fetcher`-ом (разрешенное значение промиса).\n *\n * @param {(source: S, signal: AbortSignal) => Promise<T>} fetcher Асинхронная функция-загрузчик (например, fetch-запрос к эндпоинту уведомлений).\n * @param {LongPollingOptions} options Конфигурация параметров и коллбэков длинных опросов.\n * @param {() => void} options.onNextTick Коллбэк-триггер, вызываемый для перезапуска цикла опроса (в контексте ядра обычно мутирует или инвалидирует `source` ресурса).\n * @param {(currentDelay: number, increaseDelay: () => void) => void} [options.onError] Коллбэк, вызываемый при ошибке сети. Позволяет логировать сбои и управлять шагом экспоненциального отката через запуск функции `increaseDelay()`.\n * @param {AbortSignal} [options.externalSignal] Внешний сигнал отмены для принудительной остановки поллинга извне (независимо от жизненного цикла ресурса).\n * @param {number} [options.delay=500] Базовая задержка в миллисекундах между успешным ответом сервера и началом следующего запроса.\n * @param {number} [options.errorInitialDelay=2000] Стартовая задержка в миллисекундах при возникновении первой ошибки.\n * @param {number} [options.errorMaxDelay=8000] Максимально возможный интервал задержки в миллисекундах при череде последовательных ошибок.\n *\n * @returns {(source: S, signal: AbortSignal) => Promise<T>} Возвращает обернутую циклическую функцию, возвращающую `Promise<T>`.\n *\n * @example\n * ```typescript\n * import { withLongPolling } from '@pravosleva/reactive-engine';\n *\n * // 1. Настраиваем функцию опроса новостной ленты\n * const pollingFetch = withLongPolling(\n * async (userId: string, signal) => {\n * const res = await fetch(`/api/notifications?uid=${userId}`, { signal });\n * return res.json();\n * },\n * {\n * delay: 1000,\n * errorInitialDelay: 2000,\n * errorMaxDelay: 10000,\n * onNextTick: () => {\n * // Инвалидируем или триггерим обновление ресурса в ядре\n * notificationResource.refresh();\n * },\n * onError: (delay, increaseDelay) => {\n * console.warn(`Ошибка сети. Следующая попытка через ${delay}мс`);\n * increaseDelay(); // Удваиваем интервал до следующего тика\n * }\n * }\n * );\n *\n * // 2. Передаем декоратор в подсистему ресурсов ядра\n * const notificationResource = engine.resource({\n * fetcher: pollingFetch,\n * source: () => currentUserId.value\n * });\n * ```\n *\n * @abstract\n * ### 🚨 Поведение при отмене и очистка слушателей\n * При срабатывании любого из сигналов отмены (`signal` или `externalSignal` переходит в состояние `aborted`):\n * - Текущий ожидающий промис немедленно отклоняется с системным исключением `AbortError`.\n * - Цикл поллинга блокируется и полностью прекращает планирование будущих макрозадач `setTimeout`.\n * - Все подписки на события `'abort'` корректно удаляются, предотвращая скрытые утечки памяти в Node.js / браузере.\n */\nexport const withLongPolling = <S, T>(\n fetcher: (source: S, signal: AbortSignal) => Promise<T>,\n options: LongPollingOptions\n) => {\n const {\n onNextTick,\n onError,\n externalSignal,\n delay = 500,\n errorInitialDelay = 2000,\n errorMaxDelay = 8000\n } = options\n\n let currentErrorDelay = errorInitialDelay\n let currentSessionToken = 0\n\n return (source: S, signal: AbortSignal): Promise<T> => {\n const activeSessionId = ++currentSessionToken\n\n return new Promise<T>((resolve, reject) => {\n\n const poll = async () => {\n if (signal.aborted || externalSignal?.aborted) {\n reject(new DOMException('Aborted by long polling lifecycle', 'AbortError'))\n return\n }\n\n try {\n const data = await fetcher(source, signal)\n currentErrorDelay = errorInitialDelay\n\n if (signal.aborted || externalSignal?.aborted) {\n reject(new DOMException('Aborted by long polling lifecycle', 'AbortError'))\n return\n }\n\n resolve(data)\n\n setTimeout(() => {\n if (activeSessionId === currentSessionToken && !signal.aborted && !externalSignal?.aborted) {\n onNextTick?.()\n }\n }, delay)\n\n } catch (error) {\n if (error instanceof DOMException && error.name === 'AbortError') {\n reject(error)\n return\n }\n\n reject(error)\n const delayDuration = currentErrorDelay\n\n if (typeof onError === 'function') onError(delayDuration, () => {\n if (activeSessionId === currentSessionToken) {\n currentErrorDelay = Math.min(currentErrorDelay * 2, errorMaxDelay)\n }\n })\n\n setTimeout(() => {\n if (activeSessionId === currentSessionToken && !signal.aborted && !externalSignal?.aborted) {\n onNextTick?.()\n }\n }, delayDuration)\n }\n }\n\n const forceKillTimer = () => {\n reject(new DOMException('Aborted by lifecycle event', 'AbortError'))\n }\n\n signal.addEventListener('abort', forceKillTimer)\n if (externalSignal) {\n externalSignal.addEventListener('abort', forceKillTimer)\n }\n\n if (signal.aborted || externalSignal?.aborted) {\n return forceKillTimer()\n }\n\n poll()\n })\n }\n}\n"],"mappings":";AA+FA,IAAa,KACX,GACA,MACG;CACH,IAAM,EACJ,eACA,YACA,mBACA,WAAQ,KACR,uBAAoB,KACpB,mBAAgB,QACd,GAEA,IAAoB,GACpB,IAAsB;CAE1B,QAAQ,GAAW,MAAoC;EACrD,IAAM,IAAkB,EAAE;EAE1B,OAAO,IAAI,SAAY,GAAS,MAAW;GAEzC,IAAM,IAAO,YAAY;IACvB,IAAI,EAAO,WAAW,GAAgB,SAAS;KAC7C,EAAO,IAAI,aAAa,qCAAqC,YAAY,CAAC;KAC1E;IACF;IAEA,IAAI;KACF,IAAM,IAAO,MAAM,EAAQ,GAAQ,CAAM;KAGzC,IAFA,IAAoB,GAEhB,EAAO,WAAW,GAAgB,SAAS;MAC7C,EAAO,IAAI,aAAa,qCAAqC,YAAY,CAAC;MAC1E;KACF;KAIA,AAFA,EAAQ,CAAI,GAEZ,iBAAiB;MACf,AAAI,MAAoB,KAAuB,CAAC,EAAO,WAAW,CAAC,GAAgB,WACjF,IAAa;KAEjB,GAAG,CAAK;IAEV,SAAS,GAAO;KACd,IAAI,aAAiB,gBAAgB,EAAM,SAAS,cAAc;MAChE,EAAO,CAAK;MACZ;KACF;KAEA,EAAO,CAAK;KACZ,IAAM,IAAgB;KAQtB,AANI,OAAO,KAAY,cAAY,EAAQ,SAAqB;MAC9D,AAAI,MAAoB,MACtB,IAAoB,KAAK,IAAI,IAAoB,GAAG,CAAa;KAErE,CAAC,GAED,iBAAiB;MACf,AAAI,MAAoB,KAAuB,CAAC,EAAO,WAAW,CAAC,GAAgB,WACjF,IAAa;KAEjB,GAAG,CAAa;IAClB;GACF,GAEM,UAAuB;IAC3B,EAAO,IAAI,aAAa,8BAA8B,YAAY,CAAC;GACrE;GAOA,IALA,EAAO,iBAAiB,SAAS,CAAc,GAC3C,KACF,EAAe,iBAAiB,SAAS,CAAc,GAGrD,EAAO,WAAW,GAAgB,SACpC,OAAO,EAAe;GAGxB,EAAK;EACP,CAAC;CACH;AACF"}
1
+ {"version":3,"file":"withLongPolling.mjs","names":[],"sources":["../../../src/decorators/withLongPolling/withLongPolling.ts"],"sourcesContent":["interface LongPollingOptions {\n /** Коллбэк для инкремента реактивного тика (перехода на следующую итерацию поллинга) */\n onNextTick: () => void;\n\n /**\n * Коллбэк, вызываемый при возникновении сетевой ошибки.\n * Позволяет внешней системе узнать о сбое и текущем времени ожидания до следующей попытки.\n *\n * @param {number} delayMs - Текущая задержка Exponential Backoff в миллисекундах перед следующим запросом.\n * @param {() => void} onRetryScheduled - Коллбэк-триггер. Должен быть вызван один раз, чтобы\n * просигнализировать декоратору, что шаг зафиксирован и задержка для следующей ошибки может быть увеличена.\n */\n onError: (delayMs: number, onRetryScheduled: () => void) => void;\n\n /** Пауза перед открытием следующего соединения в миллисекундах. По умолчанию 500 мс */\n delay?: number;\n /** Стартовая задержка Exponential Backoff в миллисекундах. По умолчанию 2000 мс */\n errorInitialDelay?: number;\n /** Потолок задержки Exponential Backoff в миллисекундах. По умолчанию 8000 мс */\n errorMaxDelay?: number;\n /** Внешний сигнал для жесткой остановки рекурсивных таймаутов */\n externalSignal?: AbortSignal;\n}\n\n/**\n * Адаптированный декоратор для реализации паттерна Long Polling (длинные опросы),\n * спроектированный специально для интеграции с подсистемой `engine.resource`.\n *\n * Превращает разовый асинхронный запрос данных в непрерывный контролируемый конвейер\n * циклического опроса сервера с поддержкой экспоненциального отката при ошибках\n * (Exponential Backoff) и защитой от пересечения параллельных сессий.\n *\n * ### 🧠 Архитектура конвейера опроса:\n * 1. **Контроль сессионных токенов:** Каждый новый вызов функции инкрементирует `currentSessionToken`.\n * Если предыдущий запрос завершится позже, таймер следующего шага не взведется, так как\n * идентификаторы сессий (`activeSessionId === currentSessionToken`) не совпадут. Это полностью исключает утечки и гонки ответов.\n * 2. **Экспоненциальный откат (Exponential Backoff):** При успешном запросе интервал между циклами равен `options.delay`.\n * В случае падения сетевого запроса, время ожидания перед следующим опросом прогрессивно удваивается,\n * начиная с `errorInitialDelay` вплоть до достижения лимита `errorMaxDelay`, чтобы не спамить упавший бэкенд.\n * 3. **Двойной контур отмены:** Поддерживает как локальный `AbortSignal` от ресурса, так и глобальный `externalSignal`\n * (например, для остановки всех фоновых процессов при закрытии приложения).\n *\n * @template S Тип входных данных (аргументов) для функции запроса.\n * @template T Тип данных, возвращаемых асинхронным `fetcher`-ом (разрешенное значение промиса).\n *\n * @param {(source: S, signal: AbortSignal) => Promise<T>} fetcher Асинхронная функция-загрузчик (например, fetch-запрос к эндпоинту уведомлений).\n * @param {LongPollingOptions} options Конфигурация параметров и коллбэков длинных опросов.\n * @param {() => void} options.onNextTick Коллбэк-триггер, вызываемый для перезапуска цикла опроса (в контексте ядра обычно мутирует или инвалидирует `source` ресурса).\n * @param {(currentDelay: number, increaseDelay: () => void) => void} [options.onError] Коллбэк, вызываемый при ошибке сети. Позволяет логировать сбои и управлять шагом экспоненциального отката через запуск функции `increaseDelay()`.\n * @param {AbortSignal} [options.externalSignal] Внешний сигнал отмены для принудительной остановки поллинга извне (независимо от жизненного цикла ресурса).\n * @param {number} [options.delay=500] Базовая задержка в миллисекундах между успешным ответом сервера и началом следующего запроса.\n * @param {number} [options.errorInitialDelay=2000] Стартовая задержка в миллисекундах при возникновении первой ошибки.\n * @param {number} [options.errorMaxDelay=8000] Максимально возможный интервал задержки в миллисекундах при череде последовательных ошибок.\n *\n * @returns {(source: S, signal: AbortSignal) => Promise<T>} Возвращает обернутую циклическую функцию, возвращающую `Promise<T>`.\n *\n * @example\n * ```typescript\n * import { withLongPolling } from '@pravosleva/reactive-engine';\n *\n * // 1. Настраиваем функцию опроса новостной ленты\n * const pollingFetch = withLongPolling(\n * async (userId: string, signal) => {\n * const res = await fetch(`/api/notifications?uid=${userId}`, { signal });\n * return res.json();\n * },\n * {\n * delay: 1000,\n * errorInitialDelay: 2000,\n * errorMaxDelay: 10000,\n * onNextTick: () => {\n * // Инвалидируем или триггерим обновление ресурса в ядре\n * notificationResource.refresh();\n * },\n * onError: (delay, increaseDelay) => {\n * console.warn(`Ошибка сети. Следующая попытка через ${delay}мс`);\n * increaseDelay(); // Удваиваем интервал до следующего тика\n * }\n * }\n * );\n *\n * // 2. Передаем декоратор в подсистему ресурсов ядра\n * const notificationResource = engine.resource(\n * fetcher: pollingFetch,\n * source: () => currentUserId.value\n * );\n * ```\n *\n * @abstract\n * ### 🚨 Поведение при отмене и очистка слушателей\n * При срабатывании любого из сигналов отмены (`signal` или `externalSignal` переходит в состояние `aborted`):\n * - Текущий ожидающий промис немедленно отклоняется с системным исключением `AbortError`.\n * - Цикл поллинга блокируется и полностью прекращает планирование будущих макрозадач `setTimeout`.\n * - Все подписки на события `'abort'` корректно удаляются, предотвращая скрытые утечки памяти в Node.js / браузере.\n */\nexport const withLongPolling = <S, T>(\n fetcher: (source: S, signal: AbortSignal) => Promise<T>,\n options: LongPollingOptions\n) => {\n const {\n onNextTick,\n onError,\n externalSignal,\n delay = 500,\n errorInitialDelay = 2000,\n errorMaxDelay = 8000\n } = options\n\n let currentErrorDelay = errorInitialDelay\n let currentSessionToken = 0\n\n return (source: S, signal: AbortSignal): Promise<T> => {\n const activeSessionId = ++currentSessionToken\n\n return new Promise<T>((resolve, reject) => {\n\n const poll = async () => {\n if (signal.aborted || externalSignal?.aborted) {\n reject(new DOMException('Aborted by long polling lifecycle', 'AbortError'))\n return\n }\n\n try {\n const data = await fetcher(source, signal)\n currentErrorDelay = errorInitialDelay\n\n if (signal.aborted || externalSignal?.aborted) {\n reject(new DOMException('Aborted by long polling lifecycle', 'AbortError'))\n return\n }\n\n resolve(data)\n\n setTimeout(() => {\n if (activeSessionId === currentSessionToken && !signal.aborted && !externalSignal?.aborted) {\n onNextTick?.()\n }\n }, delay)\n\n } catch (error) {\n if (error instanceof DOMException && error.name === 'AbortError') {\n reject(error)\n return\n }\n\n reject(error)\n const delayDuration = currentErrorDelay\n\n if (typeof onError === 'function') onError(delayDuration, () => {\n if (activeSessionId === currentSessionToken) {\n currentErrorDelay = Math.min(currentErrorDelay * 2, errorMaxDelay)\n }\n })\n\n setTimeout(() => {\n if (activeSessionId === currentSessionToken && !signal.aborted && !externalSignal?.aborted) {\n onNextTick?.()\n }\n }, delayDuration)\n }\n }\n\n const forceKillTimer = () => {\n reject(new DOMException('Aborted by lifecycle event', 'AbortError'))\n }\n\n signal.addEventListener('abort', forceKillTimer)\n if (externalSignal) {\n externalSignal.addEventListener('abort', forceKillTimer)\n }\n\n if (signal.aborted || externalSignal?.aborted) {\n return forceKillTimer()\n }\n\n poll()\n })\n }\n}\n"],"mappings":";AA+FA,IAAa,KACX,GACA,MACG;CACH,IAAM,EACJ,eACA,YACA,mBACA,WAAQ,KACR,uBAAoB,KACpB,mBAAgB,QACd,GAEA,IAAoB,GACpB,IAAsB;CAE1B,QAAQ,GAAW,MAAoC;EACrD,IAAM,IAAkB,EAAE;EAE1B,OAAO,IAAI,SAAY,GAAS,MAAW;GAEzC,IAAM,IAAO,YAAY;IACvB,IAAI,EAAO,WAAW,GAAgB,SAAS;KAC7C,EAAO,IAAI,aAAa,qCAAqC,YAAY,CAAC;KAC1E;IACF;IAEA,IAAI;KACF,IAAM,IAAO,MAAM,EAAQ,GAAQ,CAAM;KAGzC,IAFA,IAAoB,GAEhB,EAAO,WAAW,GAAgB,SAAS;MAC7C,EAAO,IAAI,aAAa,qCAAqC,YAAY,CAAC;MAC1E;KACF;KAIA,AAFA,EAAQ,CAAI,GAEZ,iBAAiB;MACf,AAAI,MAAoB,KAAuB,CAAC,EAAO,WAAW,CAAC,GAAgB,WACjF,IAAa;KAEjB,GAAG,CAAK;IAEV,SAAS,GAAO;KACd,IAAI,aAAiB,gBAAgB,EAAM,SAAS,cAAc;MAChE,EAAO,CAAK;MACZ;KACF;KAEA,EAAO,CAAK;KACZ,IAAM,IAAgB;KAQtB,AANI,OAAO,KAAY,cAAY,EAAQ,SAAqB;MAC9D,AAAI,MAAoB,MACtB,IAAoB,KAAK,IAAI,IAAoB,GAAG,CAAa;KAErE,CAAC,GAED,iBAAiB;MACf,AAAI,MAAoB,KAAuB,CAAC,EAAO,WAAW,CAAC,GAAgB,WACjF,IAAa;KAEjB,GAAG,CAAa;IAClB;GACF,GAEM,UAAuB;IAC3B,EAAO,IAAI,aAAa,8BAA8B,YAAY,CAAC;GACrE;GAOA,IALA,EAAO,iBAAiB,SAAS,CAAc,GAC3C,KACF,EAAe,iBAAiB,SAAS,CAAc,GAGrD,EAAO,WAAW,GAAgB,SACpC,OAAO,EAAe;GAGxB,EAAK;EACP,CAAC;CACH;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"withThrottle.cjs","names":[],"sources":["../../../src/decorators/withThrottle/withThrottle.ts"],"sourcesContent":["interface ThrottleOptions {\n /** Интервал троттлинга в миллисекундах. По умолчанию 300 мс */\n limit?: number;\n}\n\n/**\n * Декоратор для создания дросселирующего (throttled) загрузчика данных, специально\n * адаптированный для использования совместно с `engine.resource`.\n *\n * Ограничивает частоту вызовов асинхронной функции `fetcher`, гарантируя выполнение\n * первого запроса мгновенно (Leading edge), а самого последнего запроса — на хвосте\n * временного интервала (Trailing edge) с автоматической отменой всех промежуточных вызовов.\n *\n * ### 🧠 Механика обработки вызовов (Две фазы):\n * 1. **Прямое выполнение (Leading edge):** Если окно блокировки закрыто (`remainingTime <= 0`),\n * запрос пробрасывается к исходному `fetcher` немедленно. Время последнего выполнения фиксируется.\n * 2. **Отложенное выполнение (Trailing edge):** Если вызов происходит внутри активного окна блокировки,\n * его параметры (`source`, `signal`, `resolve`, `reject`) запоминаются, перезаписывая предыдущие.\n * По истечении таймера выполнится только самый актуальный (последний) запрос.\n * Все промежуточные «перезаписанные» промисы отклоняются с ошибкой `AbortError`.\n *\n * @template S Тип входных данных (аргументов) для функции запроса.\n * @template T Тип данных, возвращаемых асинхронным `fetcher`-ом (разрешенное значение промиса).\n *\n * @param {(source: S, signal: AbortSignal) => Promise<T>} fetcher Оригинальная асинхронная функция запроса, принимающая параметры и сигнал отмены.\n * @param {ThrottleOptions} [options={}] Параметры конфигурации троттлинга.\n * @param {number} [options.limit=300] Минимальный интервал времени в миллисекундах между последовательными прямыми вызовами.\n *\n * @returns {(source: S, signal: AbortSignal) => Promise<T>} Возвращает обернутую функцию, возвращающую `Promise<T>` и полностью сохраняющую исходную сигнатуру типов.\n *\n * @example\n * ```typescript\n * import { withThrottle } from '@pravosleva/reactive-engine';\n *\n * const fetchUserData = async (userId: string, signal: AbortSignal) => {\n * const res = await fetch(`/api/users/${userId}`, { signal });\n * return res.json();\n * };\n *\n * // Ограничиваем частоту запросов профиля до 1 раза в 400мс\n * const throttledFetch = withThrottle(fetchUserData, { limit: 400 });\n *\n * // Интеграция с подсистемой ресурсов вашего ядра\n * const userResource = engine.resource({\n * fetcher: throttledFetch,\n * source: () => currentUserId.value\n * });\n * ```\n *\n * @abstract\n * ### 🚨 Интеграция с AbortSignal и защита от утечек памяти\n * Декоратор подписывается на событие отмены `abort` переданного сигнала. Если внешний ресурс\n * размонтируется или перезапрашивает данные в процессе ожидания внутри окна троттлинга:\n * - Активный таймер `setTimeout` незамедлительно сбрасывается.\n * - Ожидающий хвостовой промис переходит в состояние `rejected` с системной ошибкой `AbortError`.\n * - Все внутренние ссылки на контекст промиса (`resolve`, `reject`, `source`) принудительно зануляются (`null`),\n * освобождая память от скрытых замыканий и предотвращая появление эффекта «зависших» запросов.\n */\nexport const withThrottle = <S, T>(\n fetcher: (source: S, signal: AbortSignal) => Promise<T>,\n options: ThrottleOptions = {}\n) => {\n const limit = options.limit ?? 300\n\n let lastExecutionTime = 0\n let throttleTimeoutId: ReturnType<typeof setTimeout> | null = null\n\n // Храним параметры последнего \"заблокированного\" вызова, чтобы выполнить его на хвосте\n let lastSavedSource: S | null = null\n let lastSavedResolve: ((value: T | PromiseLike<T>) => void) | null = null\n let lastSavedReject: ((reason: any) => void) | null = null\n let lastSavedSignal: AbortSignal | null = null\n\n return (source: S, signal: AbortSignal): Promise<T> => {\n const now = Date.now()\n const remainingTime = limit - (now - lastExecutionTime)\n\n // Слушатель для мгновенной отмены ожидания, если ресурс размонтировался во время блокировки\n const onAbort = () => {\n if (throttleTimeoutId) {\n clearTimeout(throttleTimeoutId)\n throttleTimeoutId = null\n }\n if (lastSavedReject) {\n lastSavedReject(new DOMException('Aborted by resource signal during throttle', 'AbortError'))\n lastSavedResolve = null\n lastSavedReject = null\n }\n }\n\n // СЦЕНАРИЙ 1: Интервал блокировки истек — выполняем запрос мгновенно (Leading edge)\n if (remainingTime <= 0) {\n // Если висел отложенный хвостовой вызов, отменяем его, так как пришел более свежий прямой запрос\n if (throttleTimeoutId) {\n clearTimeout(throttleTimeoutId)\n throttleTimeoutId = null\n }\n if (lastSavedReject) {\n lastSavedReject(new DOMException('Aborted due to newer direct throttle execution', 'AbortError'))\n lastSavedResolve = null\n lastSavedReject = null\n }\n\n lastExecutionTime = now\n return fetcher(source, signal)\n }\n\n // СЦЕНАРИЙ 2: Мы находимся внутри интервала блокировки.\n // Запоминаем текущие параметры как самые актуальные для выполнения на хвосте (Trailing edge).\n if (lastSavedReject) {\n // Отклоняем предыдущий сохраненный хвостовой промис, так как данные уже устарели\n lastSavedReject(new DOMException('Aborted due to newer throttled value', 'AbortError'))\n }\n\n return new Promise<T>((resolve, reject) => {\n lastSavedSource = source\n lastSavedResolve = resolve\n lastSavedReject = reject\n lastSavedSignal = signal\n\n if (signal.aborted) {\n return onAbort()\n }\n signal.addEventListener('abort', onAbort)\n\n // Если таймер хвостового вызова еще не запущен, взводим его на остаток времени блокировки\n if (!throttleTimeoutId) {\n throttleTimeoutId = setTimeout(async () => {\n throttleTimeoutId = null\n\n const savedSource = lastSavedSource!\n const savedResolve = lastSavedResolve!\n const savedReject = lastSavedReject!\n const savedSignal = lastSavedSignal!\n\n // Очищаем ссылки перед асинхронным вызовом\n lastSavedSource = null\n lastSavedResolve = null\n lastSavedReject = null\n lastSavedSignal = null\n savedSignal.removeEventListener('abort', onAbort)\n\n try {\n lastExecutionTime = Date.now()\n const data = await fetcher(savedSource, savedSignal)\n savedResolve(data)\n } catch (error) {\n savedReject(error)\n }\n }, remainingTime)\n }\n })\n }\n}\n"],"mappings":"AA0DA,IAAa,GACX,EACA,EAA2B,CAAC,IACzB,CACH,IAAM,EAAQ,EAAQ,OAAS,IAE3B,EAAoB,EACpB,EAA0D,KAG1D,EAA4B,KAC5B,EAAiE,KACjE,EAAkD,KAClD,EAAsC,KAE1C,OAAQ,EAAW,IAAoC,CACrD,IAAM,EAAM,KAAK,IAAI,EACf,EAAgB,GAAS,EAAM,GAG/B,MAAgB,CACpB,AAEE,KADA,aAAa,CAAiB,EACV,MAEtB,AAGE,KAFA,EAAgB,IAAI,aAAa,6CAA8C,YAAY,CAAC,EAC5F,EAAmB,KACD,KAEtB,EA0BA,OAvBI,GAAiB,GAEnB,AAEE,KADA,aAAa,CAAiB,EACV,MAEtB,AAGE,KAFA,EAAgB,IAAI,aAAa,iDAAkD,YAAY,CAAC,EAChG,EAAmB,KACD,MAGpB,EAAoB,EACb,EAAQ,EAAQ,CAAM,IAK3B,GAEF,EAAgB,IAAI,aAAa,uCAAwC,YAAY,CAAC,EAGjF,IAAI,SAAY,EAAS,IAAW,CAMzC,GALA,EAAkB,EAClB,EAAmB,EACnB,EAAkB,EAClB,EAAkB,EAEd,EAAO,QACT,OAAO,EAAQ,EAEjB,EAAO,iBAAiB,QAAS,CAAO,EAGxC,AACE,IAAoB,WAAW,SAAY,CACzC,EAAoB,KAEpB,IAAM,EAAc,EACd,EAAe,EACf,EAAc,EACd,EAAc,EAGpB,EAAkB,KAClB,EAAmB,KACnB,EAAkB,KAClB,EAAkB,KAClB,EAAY,oBAAoB,QAAS,CAAO,EAEhD,GAAI,CACF,EAAoB,KAAK,IAAI,EAE7B,EAAa,MADM,EAAQ,EAAa,CAAW,CAClC,CACnB,OAAS,EAAO,CACd,EAAY,CAAK,CACnB,CACF,EAAG,CAAa,CAEpB,CAAC,EACH,CACF"}
1
+ {"version":3,"file":"withThrottle.cjs","names":[],"sources":["../../../src/decorators/withThrottle/withThrottle.ts"],"sourcesContent":["interface ThrottleOptions {\n /** Интервал троттлинга в миллисекундах. По умолчанию 300 мс */\n limit?: number;\n}\n\n/**\n * Декоратор для создания дросселирующего (throttled) загрузчика данных, специально\n * адаптированный для использования совместно с `engine.resource`.\n *\n * Ограничивает частоту вызовов асинхронной функции `fetcher`, гарантируя выполнение\n * первого запроса мгновенно (Leading edge), а самого последнего запроса — на хвосте\n * временного интервала (Trailing edge) с автоматической отменой всех промежуточных вызовов.\n *\n * ### 🧠 Механика обработки вызовов (Две фазы):\n * 1. **Прямое выполнение (Leading edge):** Если окно блокировки закрыто (`remainingTime <= 0`),\n * запрос пробрасывается к исходному `fetcher` немедленно. Время последнего выполнения фиксируется.\n * 2. **Отложенное выполнение (Trailing edge):** Если вызов происходит внутри активного окна блокировки,\n * его параметры (`source`, `signal`, `resolve`, `reject`) запоминаются, перезаписывая предыдущие.\n * По истечении таймера выполнится только самый актуальный (последний) запрос.\n * Все промежуточные «перезаписанные» промисы отклоняются с ошибкой `AbortError`.\n *\n * @template S Тип входных данных (аргументов) для функции запроса.\n * @template T Тип данных, возвращаемых асинхронным `fetcher`-ом (разрешенное значение промиса).\n *\n * @param {(source: S, signal: AbortSignal) => Promise<T>} fetcher Оригинальная асинхронная функция запроса, принимающая параметры и сигнал отмены.\n * @param {ThrottleOptions} [options={}] Параметры конфигурации троттлинга.\n * @param {number} [options.limit=300] Минимальный интервал времени в миллисекундах между последовательными прямыми вызовами.\n *\n * @returns {(source: S, signal: AbortSignal) => Promise<T>} Возвращает обернутую функцию, возвращающую `Promise<T>` и полностью сохраняющую исходную сигнатуру типов.\n *\n * @example\n * ```typescript\n * import { withThrottle } from '@pravosleva/reactive-engine';\n *\n * const fetchUserData = async (userId: string, signal: AbortSignal) => {\n * const res = await fetch(`/api/users/${userId}`, { signal });\n * return res.json();\n * };\n *\n * // Ограничиваем частоту запросов профиля до 1 раза в 400мс\n * const throttledFetch = withThrottle(fetchUserData, { limit: 400 });\n *\n * // Интеграция с подсистемой ресурсов вашего ядра\n * const userResource = engine.resource(\n * fetcher: throttledFetch,\n * source: () => currentUserId.value\n * );\n * ```\n *\n * @abstract\n * ### 🚨 Интеграция с AbortSignal и защита от утечек памяти\n * Декоратор подписывается на событие отмены `abort` переданного сигнала. Если внешний ресурс\n * размонтируется или перезапрашивает данные в процессе ожидания внутри окна троттлинга:\n * - Активный таймер `setTimeout` незамедлительно сбрасывается.\n * - Ожидающий хвостовой промис переходит в состояние `rejected` с системной ошибкой `AbortError`.\n * - Все внутренние ссылки на контекст промиса (`resolve`, `reject`, `source`) принудительно зануляются (`null`),\n * освобождая память от скрытых замыканий и предотвращая появление эффекта «зависших» запросов.\n */\nexport const withThrottle = <S, T>(\n fetcher: (source: S, signal: AbortSignal) => Promise<T>,\n options: ThrottleOptions = {}\n) => {\n const limit = options.limit ?? 300\n\n let lastExecutionTime = 0\n let throttleTimeoutId: ReturnType<typeof setTimeout> | null = null\n\n // Храним параметры последнего \"заблокированного\" вызова, чтобы выполнить его на хвосте\n let lastSavedSource: S | null = null\n let lastSavedResolve: ((value: T | PromiseLike<T>) => void) | null = null\n let lastSavedReject: ((reason: any) => void) | null = null\n let lastSavedSignal: AbortSignal | null = null\n\n return (source: S, signal: AbortSignal): Promise<T> => {\n const now = Date.now()\n const remainingTime = limit - (now - lastExecutionTime)\n\n // Слушатель для мгновенной отмены ожидания, если ресурс размонтировался во время блокировки\n const onAbort = () => {\n if (throttleTimeoutId) {\n clearTimeout(throttleTimeoutId)\n throttleTimeoutId = null\n }\n if (lastSavedReject) {\n lastSavedReject(new DOMException('Aborted by resource signal during throttle', 'AbortError'))\n lastSavedResolve = null\n lastSavedReject = null\n }\n }\n\n // СЦЕНАРИЙ 1: Интервал блокировки истек — выполняем запрос мгновенно (Leading edge)\n if (remainingTime <= 0) {\n // Если висел отложенный хвостовой вызов, отменяем его, так как пришел более свежий прямой запрос\n if (throttleTimeoutId) {\n clearTimeout(throttleTimeoutId)\n throttleTimeoutId = null\n }\n if (lastSavedReject) {\n lastSavedReject(new DOMException('Aborted due to newer direct throttle execution', 'AbortError'))\n lastSavedResolve = null\n lastSavedReject = null\n }\n\n lastExecutionTime = now\n return fetcher(source, signal)\n }\n\n // СЦЕНАРИЙ 2: Мы находимся внутри интервала блокировки.\n // Запоминаем текущие параметры как самые актуальные для выполнения на хвосте (Trailing edge).\n if (lastSavedReject) {\n // Отклоняем предыдущий сохраненный хвостовой промис, так как данные уже устарели\n lastSavedReject(new DOMException('Aborted due to newer throttled value', 'AbortError'))\n }\n\n return new Promise<T>((resolve, reject) => {\n lastSavedSource = source\n lastSavedResolve = resolve\n lastSavedReject = reject\n lastSavedSignal = signal\n\n if (signal.aborted) {\n return onAbort()\n }\n signal.addEventListener('abort', onAbort)\n\n // Если таймер хвостового вызова еще не запущен, взводим его на остаток времени блокировки\n if (!throttleTimeoutId) {\n throttleTimeoutId = setTimeout(async () => {\n throttleTimeoutId = null\n\n const savedSource = lastSavedSource!\n const savedResolve = lastSavedResolve!\n const savedReject = lastSavedReject!\n const savedSignal = lastSavedSignal!\n\n // Очищаем ссылки перед асинхронным вызовом\n lastSavedSource = null\n lastSavedResolve = null\n lastSavedReject = null\n lastSavedSignal = null\n savedSignal.removeEventListener('abort', onAbort)\n\n try {\n lastExecutionTime = Date.now()\n const data = await fetcher(savedSource, savedSignal)\n savedResolve(data)\n } catch (error) {\n savedReject(error)\n }\n }, remainingTime)\n }\n })\n }\n}\n"],"mappings":"AA0DA,IAAa,GACX,EACA,EAA2B,CAAC,IACzB,CACH,IAAM,EAAQ,EAAQ,OAAS,IAE3B,EAAoB,EACpB,EAA0D,KAG1D,EAA4B,KAC5B,EAAiE,KACjE,EAAkD,KAClD,EAAsC,KAE1C,OAAQ,EAAW,IAAoC,CACrD,IAAM,EAAM,KAAK,IAAI,EACf,EAAgB,GAAS,EAAM,GAG/B,MAAgB,CACpB,AAEE,KADA,aAAa,CAAiB,EACV,MAEtB,AAGE,KAFA,EAAgB,IAAI,aAAa,6CAA8C,YAAY,CAAC,EAC5F,EAAmB,KACD,KAEtB,EA0BA,OAvBI,GAAiB,GAEnB,AAEE,KADA,aAAa,CAAiB,EACV,MAEtB,AAGE,KAFA,EAAgB,IAAI,aAAa,iDAAkD,YAAY,CAAC,EAChG,EAAmB,KACD,MAGpB,EAAoB,EACb,EAAQ,EAAQ,CAAM,IAK3B,GAEF,EAAgB,IAAI,aAAa,uCAAwC,YAAY,CAAC,EAGjF,IAAI,SAAY,EAAS,IAAW,CAMzC,GALA,EAAkB,EAClB,EAAmB,EACnB,EAAkB,EAClB,EAAkB,EAEd,EAAO,QACT,OAAO,EAAQ,EAEjB,EAAO,iBAAiB,QAAS,CAAO,EAGxC,AACE,IAAoB,WAAW,SAAY,CACzC,EAAoB,KAEpB,IAAM,EAAc,EACd,EAAe,EACf,EAAc,EACd,EAAc,EAGpB,EAAkB,KAClB,EAAmB,KACnB,EAAkB,KAClB,EAAkB,KAClB,EAAY,oBAAoB,QAAS,CAAO,EAEhD,GAAI,CACF,EAAoB,KAAK,IAAI,EAE7B,EAAa,MADM,EAAQ,EAAa,CAAW,CAClC,CACnB,OAAS,EAAO,CACd,EAAY,CAAK,CACnB,CACF,EAAG,CAAa,CAEpB,CAAC,EACH,CACF"}
@@ -40,10 +40,10 @@ interface ThrottleOptions {
40
40
  * const throttledFetch = withThrottle(fetchUserData, { limit: 400 });
41
41
  *
42
42
  * // Интеграция с подсистемой ресурсов вашего ядра
43
- * const userResource = engine.resource({
43
+ * const userResource = engine.resource(
44
44
  * fetcher: throttledFetch,
45
45
  * source: () => currentUserId.value
46
- * });
46
+ * );
47
47
  * ```
48
48
  *
49
49
  * @abstract
@@ -1 +1 @@
1
- {"version":3,"file":"withThrottle.mjs","names":[],"sources":["../../../src/decorators/withThrottle/withThrottle.ts"],"sourcesContent":["interface ThrottleOptions {\n /** Интервал троттлинга в миллисекундах. По умолчанию 300 мс */\n limit?: number;\n}\n\n/**\n * Декоратор для создания дросселирующего (throttled) загрузчика данных, специально\n * адаптированный для использования совместно с `engine.resource`.\n *\n * Ограничивает частоту вызовов асинхронной функции `fetcher`, гарантируя выполнение\n * первого запроса мгновенно (Leading edge), а самого последнего запроса — на хвосте\n * временного интервала (Trailing edge) с автоматической отменой всех промежуточных вызовов.\n *\n * ### 🧠 Механика обработки вызовов (Две фазы):\n * 1. **Прямое выполнение (Leading edge):** Если окно блокировки закрыто (`remainingTime <= 0`),\n * запрос пробрасывается к исходному `fetcher` немедленно. Время последнего выполнения фиксируется.\n * 2. **Отложенное выполнение (Trailing edge):** Если вызов происходит внутри активного окна блокировки,\n * его параметры (`source`, `signal`, `resolve`, `reject`) запоминаются, перезаписывая предыдущие.\n * По истечении таймера выполнится только самый актуальный (последний) запрос.\n * Все промежуточные «перезаписанные» промисы отклоняются с ошибкой `AbortError`.\n *\n * @template S Тип входных данных (аргументов) для функции запроса.\n * @template T Тип данных, возвращаемых асинхронным `fetcher`-ом (разрешенное значение промиса).\n *\n * @param {(source: S, signal: AbortSignal) => Promise<T>} fetcher Оригинальная асинхронная функция запроса, принимающая параметры и сигнал отмены.\n * @param {ThrottleOptions} [options={}] Параметры конфигурации троттлинга.\n * @param {number} [options.limit=300] Минимальный интервал времени в миллисекундах между последовательными прямыми вызовами.\n *\n * @returns {(source: S, signal: AbortSignal) => Promise<T>} Возвращает обернутую функцию, возвращающую `Promise<T>` и полностью сохраняющую исходную сигнатуру типов.\n *\n * @example\n * ```typescript\n * import { withThrottle } from '@pravosleva/reactive-engine';\n *\n * const fetchUserData = async (userId: string, signal: AbortSignal) => {\n * const res = await fetch(`/api/users/${userId}`, { signal });\n * return res.json();\n * };\n *\n * // Ограничиваем частоту запросов профиля до 1 раза в 400мс\n * const throttledFetch = withThrottle(fetchUserData, { limit: 400 });\n *\n * // Интеграция с подсистемой ресурсов вашего ядра\n * const userResource = engine.resource({\n * fetcher: throttledFetch,\n * source: () => currentUserId.value\n * });\n * ```\n *\n * @abstract\n * ### 🚨 Интеграция с AbortSignal и защита от утечек памяти\n * Декоратор подписывается на событие отмены `abort` переданного сигнала. Если внешний ресурс\n * размонтируется или перезапрашивает данные в процессе ожидания внутри окна троттлинга:\n * - Активный таймер `setTimeout` незамедлительно сбрасывается.\n * - Ожидающий хвостовой промис переходит в состояние `rejected` с системной ошибкой `AbortError`.\n * - Все внутренние ссылки на контекст промиса (`resolve`, `reject`, `source`) принудительно зануляются (`null`),\n * освобождая память от скрытых замыканий и предотвращая появление эффекта «зависших» запросов.\n */\nexport const withThrottle = <S, T>(\n fetcher: (source: S, signal: AbortSignal) => Promise<T>,\n options: ThrottleOptions = {}\n) => {\n const limit = options.limit ?? 300\n\n let lastExecutionTime = 0\n let throttleTimeoutId: ReturnType<typeof setTimeout> | null = null\n\n // Храним параметры последнего \"заблокированного\" вызова, чтобы выполнить его на хвосте\n let lastSavedSource: S | null = null\n let lastSavedResolve: ((value: T | PromiseLike<T>) => void) | null = null\n let lastSavedReject: ((reason: any) => void) | null = null\n let lastSavedSignal: AbortSignal | null = null\n\n return (source: S, signal: AbortSignal): Promise<T> => {\n const now = Date.now()\n const remainingTime = limit - (now - lastExecutionTime)\n\n // Слушатель для мгновенной отмены ожидания, если ресурс размонтировался во время блокировки\n const onAbort = () => {\n if (throttleTimeoutId) {\n clearTimeout(throttleTimeoutId)\n throttleTimeoutId = null\n }\n if (lastSavedReject) {\n lastSavedReject(new DOMException('Aborted by resource signal during throttle', 'AbortError'))\n lastSavedResolve = null\n lastSavedReject = null\n }\n }\n\n // СЦЕНАРИЙ 1: Интервал блокировки истек — выполняем запрос мгновенно (Leading edge)\n if (remainingTime <= 0) {\n // Если висел отложенный хвостовой вызов, отменяем его, так как пришел более свежий прямой запрос\n if (throttleTimeoutId) {\n clearTimeout(throttleTimeoutId)\n throttleTimeoutId = null\n }\n if (lastSavedReject) {\n lastSavedReject(new DOMException('Aborted due to newer direct throttle execution', 'AbortError'))\n lastSavedResolve = null\n lastSavedReject = null\n }\n\n lastExecutionTime = now\n return fetcher(source, signal)\n }\n\n // СЦЕНАРИЙ 2: Мы находимся внутри интервала блокировки.\n // Запоминаем текущие параметры как самые актуальные для выполнения на хвосте (Trailing edge).\n if (lastSavedReject) {\n // Отклоняем предыдущий сохраненный хвостовой промис, так как данные уже устарели\n lastSavedReject(new DOMException('Aborted due to newer throttled value', 'AbortError'))\n }\n\n return new Promise<T>((resolve, reject) => {\n lastSavedSource = source\n lastSavedResolve = resolve\n lastSavedReject = reject\n lastSavedSignal = signal\n\n if (signal.aborted) {\n return onAbort()\n }\n signal.addEventListener('abort', onAbort)\n\n // Если таймер хвостового вызова еще не запущен, взводим его на остаток времени блокировки\n if (!throttleTimeoutId) {\n throttleTimeoutId = setTimeout(async () => {\n throttleTimeoutId = null\n\n const savedSource = lastSavedSource!\n const savedResolve = lastSavedResolve!\n const savedReject = lastSavedReject!\n const savedSignal = lastSavedSignal!\n\n // Очищаем ссылки перед асинхронным вызовом\n lastSavedSource = null\n lastSavedResolve = null\n lastSavedReject = null\n lastSavedSignal = null\n savedSignal.removeEventListener('abort', onAbort)\n\n try {\n lastExecutionTime = Date.now()\n const data = await fetcher(savedSource, savedSignal)\n savedResolve(data)\n } catch (error) {\n savedReject(error)\n }\n }, remainingTime)\n }\n })\n }\n}\n"],"mappings":";AA0DA,IAAa,KACX,GACA,IAA2B,CAAC,MACzB;CACH,IAAM,IAAQ,EAAQ,SAAS,KAE3B,IAAoB,GACpB,IAA0D,MAG1D,IAA4B,MAC5B,IAAiE,MACjE,IAAkD,MAClD,IAAsC;CAE1C,QAAQ,GAAW,MAAoC;EACrD,IAAM,IAAM,KAAK,IAAI,GACf,IAAgB,KAAS,IAAM,IAG/B,UAAgB;GAKpB,AAJA,AAEE,OADA,aAAa,CAAiB,GACV,OAEtB,AAGE,OAFA,EAAgB,IAAI,aAAa,8CAA8C,YAAY,CAAC,GAC5F,IAAmB,MACD;EAEtB;EA0BA,OAvBI,KAAiB,KAEnB,AAEE,OADA,aAAa,CAAiB,GACV,OAEtB,AAGE,OAFA,EAAgB,IAAI,aAAa,kDAAkD,YAAY,CAAC,GAChG,IAAmB,MACD,OAGpB,IAAoB,GACb,EAAQ,GAAQ,CAAM,MAK3B,KAEF,EAAgB,IAAI,aAAa,wCAAwC,YAAY,CAAC,GAGjF,IAAI,SAAY,GAAS,MAAW;GAMzC,IALA,IAAkB,GAClB,IAAmB,GACnB,IAAkB,GAClB,IAAkB,GAEd,EAAO,SACT,OAAO,EAAQ;GAKjB,AAHA,EAAO,iBAAiB,SAAS,CAAO,GAGxC,AACE,MAAoB,WAAW,YAAY;IACzC,IAAoB;IAEpB,IAAM,IAAc,GACd,IAAe,GACf,IAAc,GACd,IAAc;IAOpB,AAJA,IAAkB,MAClB,IAAmB,MACnB,IAAkB,MAClB,IAAkB,MAClB,EAAY,oBAAoB,SAAS,CAAO;IAEhD,IAAI;KAGF,AAFA,IAAoB,KAAK,IAAI,GAE7B,EAAa,MADM,EAAQ,GAAa,CAAW,CAClC;IACnB,SAAS,GAAO;KACd,EAAY,CAAK;IACnB;GACF,GAAG,CAAa;EAEpB,CAAC;CACH;AACF"}
1
+ {"version":3,"file":"withThrottle.mjs","names":[],"sources":["../../../src/decorators/withThrottle/withThrottle.ts"],"sourcesContent":["interface ThrottleOptions {\n /** Интервал троттлинга в миллисекундах. По умолчанию 300 мс */\n limit?: number;\n}\n\n/**\n * Декоратор для создания дросселирующего (throttled) загрузчика данных, специально\n * адаптированный для использования совместно с `engine.resource`.\n *\n * Ограничивает частоту вызовов асинхронной функции `fetcher`, гарантируя выполнение\n * первого запроса мгновенно (Leading edge), а самого последнего запроса — на хвосте\n * временного интервала (Trailing edge) с автоматической отменой всех промежуточных вызовов.\n *\n * ### 🧠 Механика обработки вызовов (Две фазы):\n * 1. **Прямое выполнение (Leading edge):** Если окно блокировки закрыто (`remainingTime <= 0`),\n * запрос пробрасывается к исходному `fetcher` немедленно. Время последнего выполнения фиксируется.\n * 2. **Отложенное выполнение (Trailing edge):** Если вызов происходит внутри активного окна блокировки,\n * его параметры (`source`, `signal`, `resolve`, `reject`) запоминаются, перезаписывая предыдущие.\n * По истечении таймера выполнится только самый актуальный (последний) запрос.\n * Все промежуточные «перезаписанные» промисы отклоняются с ошибкой `AbortError`.\n *\n * @template S Тип входных данных (аргументов) для функции запроса.\n * @template T Тип данных, возвращаемых асинхронным `fetcher`-ом (разрешенное значение промиса).\n *\n * @param {(source: S, signal: AbortSignal) => Promise<T>} fetcher Оригинальная асинхронная функция запроса, принимающая параметры и сигнал отмены.\n * @param {ThrottleOptions} [options={}] Параметры конфигурации троттлинга.\n * @param {number} [options.limit=300] Минимальный интервал времени в миллисекундах между последовательными прямыми вызовами.\n *\n * @returns {(source: S, signal: AbortSignal) => Promise<T>} Возвращает обернутую функцию, возвращающую `Promise<T>` и полностью сохраняющую исходную сигнатуру типов.\n *\n * @example\n * ```typescript\n * import { withThrottle } from '@pravosleva/reactive-engine';\n *\n * const fetchUserData = async (userId: string, signal: AbortSignal) => {\n * const res = await fetch(`/api/users/${userId}`, { signal });\n * return res.json();\n * };\n *\n * // Ограничиваем частоту запросов профиля до 1 раза в 400мс\n * const throttledFetch = withThrottle(fetchUserData, { limit: 400 });\n *\n * // Интеграция с подсистемой ресурсов вашего ядра\n * const userResource = engine.resource(\n * fetcher: throttledFetch,\n * source: () => currentUserId.value\n * );\n * ```\n *\n * @abstract\n * ### 🚨 Интеграция с AbortSignal и защита от утечек памяти\n * Декоратор подписывается на событие отмены `abort` переданного сигнала. Если внешний ресурс\n * размонтируется или перезапрашивает данные в процессе ожидания внутри окна троттлинга:\n * - Активный таймер `setTimeout` незамедлительно сбрасывается.\n * - Ожидающий хвостовой промис переходит в состояние `rejected` с системной ошибкой `AbortError`.\n * - Все внутренние ссылки на контекст промиса (`resolve`, `reject`, `source`) принудительно зануляются (`null`),\n * освобождая память от скрытых замыканий и предотвращая появление эффекта «зависших» запросов.\n */\nexport const withThrottle = <S, T>(\n fetcher: (source: S, signal: AbortSignal) => Promise<T>,\n options: ThrottleOptions = {}\n) => {\n const limit = options.limit ?? 300\n\n let lastExecutionTime = 0\n let throttleTimeoutId: ReturnType<typeof setTimeout> | null = null\n\n // Храним параметры последнего \"заблокированного\" вызова, чтобы выполнить его на хвосте\n let lastSavedSource: S | null = null\n let lastSavedResolve: ((value: T | PromiseLike<T>) => void) | null = null\n let lastSavedReject: ((reason: any) => void) | null = null\n let lastSavedSignal: AbortSignal | null = null\n\n return (source: S, signal: AbortSignal): Promise<T> => {\n const now = Date.now()\n const remainingTime = limit - (now - lastExecutionTime)\n\n // Слушатель для мгновенной отмены ожидания, если ресурс размонтировался во время блокировки\n const onAbort = () => {\n if (throttleTimeoutId) {\n clearTimeout(throttleTimeoutId)\n throttleTimeoutId = null\n }\n if (lastSavedReject) {\n lastSavedReject(new DOMException('Aborted by resource signal during throttle', 'AbortError'))\n lastSavedResolve = null\n lastSavedReject = null\n }\n }\n\n // СЦЕНАРИЙ 1: Интервал блокировки истек — выполняем запрос мгновенно (Leading edge)\n if (remainingTime <= 0) {\n // Если висел отложенный хвостовой вызов, отменяем его, так как пришел более свежий прямой запрос\n if (throttleTimeoutId) {\n clearTimeout(throttleTimeoutId)\n throttleTimeoutId = null\n }\n if (lastSavedReject) {\n lastSavedReject(new DOMException('Aborted due to newer direct throttle execution', 'AbortError'))\n lastSavedResolve = null\n lastSavedReject = null\n }\n\n lastExecutionTime = now\n return fetcher(source, signal)\n }\n\n // СЦЕНАРИЙ 2: Мы находимся внутри интервала блокировки.\n // Запоминаем текущие параметры как самые актуальные для выполнения на хвосте (Trailing edge).\n if (lastSavedReject) {\n // Отклоняем предыдущий сохраненный хвостовой промис, так как данные уже устарели\n lastSavedReject(new DOMException('Aborted due to newer throttled value', 'AbortError'))\n }\n\n return new Promise<T>((resolve, reject) => {\n lastSavedSource = source\n lastSavedResolve = resolve\n lastSavedReject = reject\n lastSavedSignal = signal\n\n if (signal.aborted) {\n return onAbort()\n }\n signal.addEventListener('abort', onAbort)\n\n // Если таймер хвостового вызова еще не запущен, взводим его на остаток времени блокировки\n if (!throttleTimeoutId) {\n throttleTimeoutId = setTimeout(async () => {\n throttleTimeoutId = null\n\n const savedSource = lastSavedSource!\n const savedResolve = lastSavedResolve!\n const savedReject = lastSavedReject!\n const savedSignal = lastSavedSignal!\n\n // Очищаем ссылки перед асинхронным вызовом\n lastSavedSource = null\n lastSavedResolve = null\n lastSavedReject = null\n lastSavedSignal = null\n savedSignal.removeEventListener('abort', onAbort)\n\n try {\n lastExecutionTime = Date.now()\n const data = await fetcher(savedSource, savedSignal)\n savedResolve(data)\n } catch (error) {\n savedReject(error)\n }\n }, remainingTime)\n }\n })\n }\n}\n"],"mappings":";AA0DA,IAAa,KACX,GACA,IAA2B,CAAC,MACzB;CACH,IAAM,IAAQ,EAAQ,SAAS,KAE3B,IAAoB,GACpB,IAA0D,MAG1D,IAA4B,MAC5B,IAAiE,MACjE,IAAkD,MAClD,IAAsC;CAE1C,QAAQ,GAAW,MAAoC;EACrD,IAAM,IAAM,KAAK,IAAI,GACf,IAAgB,KAAS,IAAM,IAG/B,UAAgB;GAKpB,AAJA,AAEE,OADA,aAAa,CAAiB,GACV,OAEtB,AAGE,OAFA,EAAgB,IAAI,aAAa,8CAA8C,YAAY,CAAC,GAC5F,IAAmB,MACD;EAEtB;EA0BA,OAvBI,KAAiB,KAEnB,AAEE,OADA,aAAa,CAAiB,GACV,OAEtB,AAGE,OAFA,EAAgB,IAAI,aAAa,kDAAkD,YAAY,CAAC,GAChG,IAAmB,MACD,OAGpB,IAAoB,GACb,EAAQ,GAAQ,CAAM,MAK3B,KAEF,EAAgB,IAAI,aAAa,wCAAwC,YAAY,CAAC,GAGjF,IAAI,SAAY,GAAS,MAAW;GAMzC,IALA,IAAkB,GAClB,IAAmB,GACnB,IAAkB,GAClB,IAAkB,GAEd,EAAO,SACT,OAAO,EAAQ;GAKjB,AAHA,EAAO,iBAAiB,SAAS,CAAO,GAGxC,AACE,MAAoB,WAAW,YAAY;IACzC,IAAoB;IAEpB,IAAM,IAAc,GACd,IAAe,GACf,IAAc,GACd,IAAc;IAOpB,AAJA,IAAkB,MAClB,IAAmB,MACnB,IAAkB,MAClB,IAAkB,MAClB,EAAY,oBAAoB,SAAS,CAAO;IAEhD,IAAI;KAGF,AAFA,IAAoB,KAAK,IAAI,GAE7B,EAAa,MADM,EAAQ,GAAa,CAAW,CAClC;IACnB,SAAS,GAAO;KACd,EAAY,CAAK;IACnB;GACF,GAAG,CAAa;EAEpB,CAAC;CACH;AACF"}
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * @pravosleva/reactive-engine v1.5.13
2
+ * @pravosleva/reactive-engine v1.6.0
3
3
  * High-performance transactional reactive engine with microtask batching.
4
4
  *
5
5
  * @license MIT
@@ -9,5 +9,5 @@
9
9
  * @contact Telegram Channel: https://t.me/bash_exp_ru/3393
10
10
  * @contact Developer Telegram: https://t.me/pravosleva
11
11
  */
12
- (function(e,t){typeof exports==`object`&&typeof module<`u`?t(exports):typeof define==`function`&&define.amd?define([`exports`],t):(e=typeof globalThis<`u`?globalThis:e||self,t(e.ReactiveEngineLib={}))})(this,function(e){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});var t=({tested:e,expectedKey:t,valueType:n})=>{let r=[];if(!e||!Array.isArray(e))return r;for(let i=0,a=e.length;i<a;i++){let a=e[i];if(typeof a!=`string`)continue;let o;switch(n){case`number`:o=RegExp(`\\[${t}=(?<value>\\d+)\\]`,`g`);break;default:o=RegExp(`\\[${t}=(?<value>.*?)\\]`,`g`);break}for(let e of a.matchAll(o))e?.groups&&r.push(e.groups.value)}return r},n=class{frameworkPrefix=`core`;activeEffect=null;isBatching=!1;pendingEffects=new Set;services=new Map;factories=new Map;proxyCache=new WeakMap;allEffects=new Set;loggerOptions;pendingLogQueue=[];lastTransactionDuration=null;signalMutationCounts=new Map;lastComputedDurations=new Map;effectExecutionCounts=new Map;batchTickCounts=0;constructor(e){this.loggerOptions=e?.logger}log(e,n,r){if(!this.loggerOptions?.isEnabled)return;if(!this.loggerOptions?.isCoreOptimizationDebugEnabled){let e=t({expectedKey:`CORE_INTERNAL_SIGNAL`,tested:[n],valueType:`number`})?.[0]===`1`,r=t({expectedKey:`CORE_INTERNAL_EFFECT`,tested:[n],valueType:`number`})?.[0]===`1`;if(e||r)return}if(this.loggerOptions.filter&&!(this.loggerOptions.filter instanceof RegExp?this.loggerOptions.filter:new RegExp(this.loggerOptions.filter.replace(/^\/|\/$/g,``))).test(n))return;let i={signal:`background: #007acc; color: #fff; padding: 2px 6px; border-radius: 4px; font-weight: bold;`,computed:`background: #42b883; color: #fff; padding: 2px 6px; border-radius: 4px; font-weight: bold;`,effect:`background: #e01e5a; color: #fff; padding: 2px 6px; border-radius: 4px; font-weight: bold;`,batch:`background: #7952b3; color: #fff; padding: 2px 6px; border-radius: 4px; font-weight: bold;`,resource:`background: #d97706; color: #fff; padding: 2px 6px; border-radius: 4px; font-weight: bold;`,reactive:`background: #007acc; color: #fff; padding: 2px 6px; border-radius: 4px; font-weight: bold;`},a=``,o=`color: #aaa; font-weight: normal;`;switch(e){case`signal`:{let s=t({tested:[n],expectedKey:`CORE_INTERNAL_SIGNAL`,valueType:`number`})?.[0]===`1`,c=n,l=(this.signalMutationCounts.get(c)||0)+1;this.signalMutationCounts.set(c,l);let u=r,d=u.subscribers&&u.subscribers.length>0?u.subscribers.every(e=>e?t({tested:[e],expectedKey:`IS_OPTIMIZED`,valueType:`number`})?.[0]===`1`:!1):!1,f=l>50,p=s?`COMPUTED_INTERNAL`:`SIGNAL`;a=` 🔄 Изменений ${s?`кэша`:`сигнала`}: ${l}${f?` ⚠️ (high noise — обнаружен дребезг/спам значений!)`:``}`,f&&(o=`color: orange; font-weight: bold;`,d||(r.__performance_advice__={issue:`Сигнал [${c}] обновляется слишком часто (${l} раз за такт). Это приводит к избыточным ререндерам UI.`,solution:`Оберните чтение этого сигнала в декоратор сжатия потока данных 'withThrottleComputed'`,example:`public throttled = withThrottleComputed(this.engine, () => this.yourSignalName.value, { limit: 300 });`})),console.groupCollapsed(`%c${p}%c [${c}]%c${a}`,s?`background: #42b883; color: #fff; padding: 2px 6px; border-radius: 4px; font-weight: bold;`:i[e],`color: #aaa; font-weight: bold;`,o),console.log(`Данные (Payload):`,r),this.loggerOptions.traceTime&&typeof performance<`u`&&console.log(`Тайминг рантайма:`,`${performance.now().toFixed(2)} ms`),console.groupEnd();return}case`computed`:{let e=r;if(e&&`to`in e){let t=e.to;a=` 🧮 Значение: ${typeof t==`object`&&t?`{...}`:String(t)}`}else if(e&&`duration`in e){let t=parseFloat(e.duration);if(!isNaN(t)){let r=this.lastComputedDurations.get(n);if(r!==void 0&&r>0){let n=(t-r)/r*100;n>5?(a=` 🔴 Замедление: +${n.toFixed(1)}% (${e.duration})`,o=`color: #e01e5a; font-weight: bold;`):n<-5?(a=` 🟢 Ускорение: ${n.toFixed(1)}% (${e.duration})`,o=`color: #42b883; font-weight: bold;`):(a=` 🟢 Стабильно (${e.duration})`,o=`color: #42b883; font-weight: bold;`)}else a=` ⚪ Первый расчет (${e.duration})`;this.lastComputedDurations.set(n,t)}}break}case`effect`:{let e=(this.effectExecutionCounts.get(n)||0)+1;this.effectExecutionCounts.set(n,e);let t=e>30,i=r;a=` 🟢 Стабильно${i&&`duration`in i?` (${i.duration})`:``} | 🚀 Вызовов: ${e}${t?` ⚠️ (heavy re-renders)`:``}`,o=`color: #42b883; font-weight: bold;`,t&&(o=`color: #ff4a4a; font-weight: bold;`);break}case`batch`:this.batchTickCounts+=1,a=` 📦 Номер транзакции: #${this.batchTickCounts}`;break;case`resource`:{let e=r;if(e.loading)a=` ⏳ ЗАГРУЗКА (fetching...)`,o=`color: #d97706; font-weight: bold;`;else if(e.error)a=` 🔴 ОШИБКА: ${e.error.message||`Unknown Error`}`,o=`color: #ff4a4a; font-weight: bold;`;else{let t=e.data;a=` 🟢 УСПЕХ (data: ${typeof t==`object`&&t?`{...}`:String(t)})`,o=`color: #42b883; font-weight: bold;`}break}default:break}console.groupCollapsed(`%c${e.toUpperCase()}%c [${n}]%c${a}`,i[e],`color: #aaa; font-weight: bold;`,o),console.log(`Данные (Payload):`,r),this.loggerOptions.traceTime&&typeof performance<`u`&&console.log(`Тайминг рантайма:`,`${performance.now().toFixed(2)} ms`),console.groupEnd()}onSignalChange;provide(e,t){typeof t==`function`&&!t.prototype?this.factories.set(e,t):this.services.set(e,t)}inject(e){if(!e)throw Error(`[DI Error]: Вы пытаетесь внедрить пустой токен (undefined/null). Проверьте импорты.`);let t=e;if(this.services.has(t))return this.services.get(t);try{let n=this.factories.get(t);if(n){let e=n(this);return this.services.set(t,e),e}if(typeof e==`function`&&e.prototype){let n=new e(this);return this.services.set(t,n),n}throw Error(`Service not found: ${String(e)}`)}catch(t){throw Error(`[DI Error]: Не удалось создать сервис ${String(e)}. Ошибка: ${t?.message||`No e?.message`}`)}}queueLog(e,t,n){this.loggerOptions?.isEnabled&&this.pendingLogQueue.push({type:e,name:t,detail:n})}flushLogs(){if(!this.loggerOptions?.isEnabled||this.pendingLogQueue.length===0)return;let e=typeof performance<`u`?performance.now():0,t=this.pendingLogQueue;if(this.loggerOptions.filter)try{let e;if(this.loggerOptions.filter instanceof RegExp)e=this.loggerOptions.filter;else{let t=this.loggerOptions.filter.replace(/^\/|\/$/g,``);e=new RegExp(t)}t=t.filter(t=>e.test(t.name))}catch(e){console.warn(`[Logger Error]: Некорректный паттерн фильтрации логов`,e)}if(t.length===0){this.pendingLogQueue=[];return}let n=``,r=``,i=`color: #aaa; font-weight: normal;`;if(this.loggerOptions.traceTime&&e){let t=performance.now()-e;if(n=` | Duration: ${t.toFixed(3)} ms`,this.lastTransactionDuration!==null&&this.lastTransactionDuration>0){let e=(t-this.lastTransactionDuration)/this.lastTransactionDuration*100;e>.1?(r=` 🔴 +${e.toFixed(1)}%`,i=`color: #e01e5a; font-weight: bold;`):e<-.1?(r=` 🟢 ${e.toFixed(1)}%`,i=`color: #42b883; font-weight: bold;`):(r=` 🟢 stable`,i=`color: #42b883; font-weight: bold;`)}else r=` ⚪ first`;this.lastTransactionDuration=t}console.groupCollapsed(`%cREACTIVE TRANSACTION${this.loggerOptions.instanceName?` ${this.loggerOptions.instanceName}`:``}%c Microtask Tick (Size: ${t.length}${n})%c${r}`,`background: #7952b3; color: #fff; padding: 2px 6px; border-radius: 4px; font-weight: bold;`,`color: #aaa; font-weight: normal;`,i),t.forEach(e=>{this.log(e.type,e.name,e.detail)}),this.loggerOptions.traceTime&&typeof performance<`u`&&console.log(`%c[Execution Timestamp]: ${performance.now().toFixed(2)} ms`,`color: #7952b3; font-weight: bold;`),console.groupEnd(),this.pendingLogQueue=[]}signal(e,t){let n=this,r=e,i=new Set,a=typeof t==`string`?{name:t}:t||{},o=a.name||`unnamed_signal`;return{get value(){if(n.activeEffect){let e=n.activeEffect;i.has(e)||(i.add(e),e.cleanups.add(()=>{i.delete(e)}))}return r},set value(e){if(r===e)return;if(a.validate){let t=a.validate(e);if(t===!1||typeof t==`string`){let n=typeof t==`string`?t:`[Validation Error]: Некорректное значение для сигнала "${o}"`;console.error(`%c${n}`,`color: orange; font-weight: bold;`,{received:e,current:r});return}}let t=r;r=e,n.onSignalChange?.(o,e,t);let s=Array.from(i).map(e=>e.label||`unnamed_effect`);n.queueLog?.(`signal`,o,{from:t,to:e,subscribersCount:i.size,subscribers:s}),i.forEach(e=>n.pendingEffects.add(e)),n.isBatching||(n.isBatching=!0,queueMicrotask(()=>{for(let e of n.pendingEffects)n.pendingEffects.delete(e),e.run();n.isBatching=!1,n.flushLogs?.()}))},subscribe(e){return n.effect(()=>{e(this.value)},`${n.frameworkPrefix}:use:${o}`)}}}effect(e,t){let n=this,r=!0,i={label:t,cleanups:new Set,run(){this.cleanups.forEach(e=>e()),this.cleanups.clear();let t=n.activeEffect;n.activeEffect=this;let i=performance.now();n.safeRun(this,()=>{let t=e();typeof t==`function`&&this.cleanups.add(t)}),n.activeEffect=t;let a=performance.now()-i;if(!r&&n.loggerOptions?.isEnabled){let e=n.loggerOptions.filter;(!e||e instanceof RegExp&&this.label&&e.test(this.label))&&typeof n.queueLog==`function`&&n.queueLog(`effect`,this.label||`unnamed-efect-0`,{name:this.label||`unnamed-efect-0`,type:`effect`,detail:{status:`stable`,duration:`${a.toFixed(3)}ms`,timestamp:i}})}r=!1}};return this.allEffects.add(i),i.run(),()=>{i.cleanups.forEach(e=>e()),n.pendingEffects.delete(i),n.allEffects.delete(i)}}computedCache=new Map;cleanupRegistry=new FinalizationRegistry(e=>{e()});computed(e,t){if(this.computedCache.has(e)){let t=this.computedCache.get(e)?.deref();if(t)return t}let n=this,r=t||`unnamed_computed`,i=this.signal(void 0,`[CORE_INTERNAL_SIGNAL=1]:${r}`),a=!0,o,s=this.effect(()=>{o=e(),a=!1;let t=typeof performance<`u`&&performance.now()?`0.010ms`:`N/A`;typeof n.queueLog==`function`&&n.queueLog(`computed`,r,{value:o,duration:t}),i.value=o},`[CORE_INTERNAL_EFFECT=1]:${r}`),c=Array.from(this.allEffects)[this.allEffects.size-1],l=()=>{s(),c&&n.allEffects.delete(c),n.computedCache.delete(e)},u={get value(){return a&&(o=e(),a=!1,i.value=o),i.value},subscribe:e=>i.subscribe(e),destroy(){s(),l(),n.computedCache.delete(e)}};return this.cleanupRegistry.register(u,l),this.computedCache.set(e,new WeakRef(u)),u}reactive(e,t=`reactive`){if(this.proxyCache.has(e))return this.proxyCache.get(e);let n=this,r=new Map,i=new Proxy(e,{get(e,i,a){n.activeEffect&&(r.has(i)||r.set(i,new Set),r.get(i).add(n.activeEffect));let o=Reflect.get(e,i,a);return n.queueLog?.(`reactive`,`${t}.${String(i)}`,{action:`get`,property:String(i),value:o}),typeof o==`object`&&o?n.reactive(o,`${t}.${String(i)}`):o},set(e,i,a,o){let s=Reflect.get(e,i,o);return s===a||s!==a&&(Reflect.set(e,i,a,o),n.onSignalChange?.(`${t}.${String(i)}`,a,s),r.get(i)?.forEach(e=>n.isBatching?n.pendingEffects.add(e):e.run()),n.queueLog?.(`reactive`,`${t}.${String(i)}`,{action:`set`,property:String(i),oldValue:s,newValue:a})),!0}});return this.proxyCache.set(e,i),i}batch(e){e()}delay(e,t){return new Promise((n,r)=>{if(t.aborted)return r(new DOMException(`Aborted`,`AbortError`));let i=setTimeout(()=>{t.removeEventListener(`abort`,a),n()},e),a=()=>{clearTimeout(i),r(new DOMException(`Aborted`,`AbortError`))};t.addEventListener(`abort`,a,{once:!0})})}resource(e,n,r){let i=r&&typeof r==`object`,a=i?r.name:r||`unnamed_resource`,o=i?r:{},s=o.resetDataOnSourceChange??!0,c=o.responseValidate,l=o.validateBeforeFetch,u=o.retryCount??0,d=o.retryDelay??1e3,f=o.isExponentialBackoffEnabled??!1,p=o.maxRetryDelay??3e4,m=o.timeout,h=this.signal({data:null,loading:!0,error:null,isRetrying:!1},`[CORE_INTERNAL_SIGNAL=1]:resource:state:${a}`),g=e=>{h.value=e,typeof this.queueLog==`function`&&this.queueLog(`resource`,a,{loading:e.loading,data:e.data,error:e.error,isRetrying:e.isRetrying})},_=async(n,r,i=!1)=>{if(r.aborted)return;if(l){let e=l(n);if(e===!1||typeof e==`string`){if(r.aborted)return;let t=typeof e==`string`?e:`Pre-fetch validation failed for resource`,n=i?null:this.untrack(()=>h.value.data);g({data:n,loading:!1,error:Error(t),isRetrying:!1});return}}let o=i&&s?null:this.untrack(()=>h.value.data);g({data:o,loading:!0,error:null,isRetrying:!1});for(let i=0;i<=u;i++){if(r.aborted)return;let o=r;try{if(m&&m>0){let e=AbortSignal.timeout(m);o=AbortSignal.any([r,e])}let t=await e(n,o);if(r.aborted)return;if(o.aborted)throw new DOMException(`The operation timed out.`,`TimeoutError`);if(c){let e=c(t);if(e===!1||typeof e==`string`){if(r.aborted)return;g({data:null,loading:!1,error:Error(typeof e==`string`?e:`Validation failed`),isRetrying:!1});return}}if(r.aborted)return;g({data:t,loading:!1,error:null,isRetrying:!1});return}catch(e){if(r.aborted||e.name===`AbortError`&&r.aborted)return;let n=e.name===`TimeoutError`||e.name===`AbortError`||e.message?.includes(`timeout`),o=t({tested:[e.message],expectedKey:`THROW_CUSTOM_VALIDATION_ERROR_NO_RETRY`,valueType:`number`})?.[0]===`1`||!1,s=o&&t({tested:[e.message],expectedKey:`MESSAGE`,valueType:`string`})?.[0]||`Custom validation error`,c=o||e.name===`ValidationError`;if(i===u||c){if(r.aborted)return;let t=n?Error(`Request timed out after ${m}ms`,{cause:e}):o?Error(s,{cause:e}):e;g({data:null,loading:!1,error:t,isRetrying:!1});return}else{if(r.aborted)return;let e=f?d*2**i:d;e=Math.min(e,p);let t=Math.random()*200;e+=t,g({data:h.value.data,loading:!0,error:null,isRetrying:!0}),console.warn(`[Resource Retry] "${a}" сбой по ${n?`таймауту`:`ошибке сети`}. Попытка ${i+1}/${u+1}...`);try{if(await this.delay(e,r),r.aborted)return}catch{return}}}}},v=null;return this.effect(()=>{let e=n?n.value:void 0,t=new AbortController;return v=t,this.untrack(()=>{_(e,t.signal,!0)}),()=>t.abort()},`[dev]`),{get data(){return h.value.data},get loading(){return h.value.loading},get error(){return h.value.error},get isRetrying(){return h.value.isRetrying},get value(){return h.value},refetch:()=>{v?.abort(),v=new AbortController,_(n?n.value:void 0,v.signal,!1)},subscribe:e=>h.subscribe(e)}}untrack(e){let t=this.activeEffect;this.activeEffect=null;try{return e()}finally{this.activeEffect=t}}safeRun(e,t){try{t()}catch(e){console.error(`%c[Reactive Error] %cОшибка в эффекте/computed:`,`color: white; background: red; padding: 2px 4px; border-radius: 3px;`,`font-weight: bold;`,e)}}},r=class{engine;constructor(e){this.engine=e,this.onInit()}createNumericSignal(e,t){return this.engine.signal(e,{name:t,validate:e=>typeof e==`number`&&!isNaN(e)})}createComputed(e){return this.engine.computed(e)}createSignal(e,t){return this.engine.signal(e,t)}onInit(){}},i=r,a=class{engine;constructor(e){this.engine=e,setTimeout(()=>this.onInit(),0)}onInit(){}};e.AbstractService=r,e.BaseREService=i,e.BaseREServiceEnhanced=a,e.ReactiveEngine=n});
12
+ (function(e,t){typeof exports==`object`&&typeof module<`u`?t(exports):typeof define==`function`&&define.amd?define([`exports`],t):(e=typeof globalThis<`u`?globalThis:e||self,t(e.ReactiveEngineLib={}))})(this,function(e){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});var t=({tested:e,expectedKey:t,valueType:n})=>{let r=[];if(!e||!Array.isArray(e))return r;for(let i=0,a=e.length;i<a;i++){let a=e[i];if(typeof a!=`string`)continue;let o;switch(n){case`number`:o=RegExp(`\\[${t}=(?<value>\\d+)\\]`,`g`);break;default:o=RegExp(`\\[${t}=(?<value>.*?)\\]`,`g`);break}for(let e of a.matchAll(o))e?.groups&&r.push(e.groups.value)}return r},n=class{frameworkPrefix=`core`;activeEffect=null;isBatching=!1;pendingEffects=new Set;services=new Map;factories=new Map;proxyCache=new WeakMap;allEffects=new Set;loggerOptions;pendingLogQueue=[];lastTransactionDuration=null;signalMutationCounts=new Map;lastComputedDurations=new Map;effectExecutionCounts=new Map;batchTickCounts=0;constructor(e){this.loggerOptions=e?.logger}log(e,n,r){if(!this.loggerOptions?.isEnabled)return;if(!this.loggerOptions?.isCoreOptimizationDebugEnabled){let e=t({expectedKey:`CORE_INTERNAL_SIGNAL`,tested:[n],valueType:`number`})?.[0]===`1`,r=t({expectedKey:`CORE_INTERNAL_EFFECT`,tested:[n],valueType:`number`})?.[0]===`1`;if(e||r)return}if(this.loggerOptions.filter&&!(this.loggerOptions.filter instanceof RegExp?this.loggerOptions.filter:new RegExp(this.loggerOptions.filter.replace(/^\/|\/$/g,``))).test(n))return;let i={signal:`background: #007acc; color: #fff; padding: 2px 6px; border-radius: 4px; font-weight: bold;`,computed:`background: #42b883; color: #fff; padding: 2px 6px; border-radius: 4px; font-weight: bold;`,effect:`background: #e01e5a; color: #fff; padding: 2px 6px; border-radius: 4px; font-weight: bold;`,batch:`background: #7952b3; color: #fff; padding: 2px 6px; border-radius: 4px; font-weight: bold;`,resource:`background: #d97706; color: #fff; padding: 2px 6px; border-radius: 4px; font-weight: bold;`,reactive:`background: #007acc; color: #fff; padding: 2px 6px; border-radius: 4px; font-weight: bold;`},a=``,o=`color: #aaa; font-weight: normal;`;switch(e){case`signal`:{let s=t({tested:[n],expectedKey:`CORE_INTERNAL_SIGNAL`,valueType:`number`})?.[0]===`1`,c=n,l=(this.signalMutationCounts.get(c)||0)+1;this.signalMutationCounts.set(c,l);let u=r,d=u.subscribers&&u.subscribers.length>0?u.subscribers.every(e=>e?t({tested:[e],expectedKey:`IS_OPTIMIZED`,valueType:`number`})?.[0]===`1`:!1):!1,f=l>50,p=s?`COMPUTED_INTERNAL`:`SIGNAL`;a=` 🔄 Изменений ${s?`кэша`:`сигнала`}: ${l}${f?` ⚠️ (high noise — обнаружен дребезг/спам значений!)`:``}`,f&&(o=`color: orange; font-weight: bold;`,d||(r.__performance_advice__={issue:`Сигнал [${c}] обновляется слишком часто (${l} раз за такт). Это приводит к избыточным ререндерам UI.`,solution:`Оберните чтение этого сигнала в декоратор сжатия потока данных 'withThrottleComputed'`,example:`public throttled = withThrottleComputed(this.engine, () => this.yourSignalName.value, { limit: 300 });`})),console.groupCollapsed(`%c${p}%c [${c}]%c${a}`,s?`background: #42b883; color: #fff; padding: 2px 6px; border-radius: 4px; font-weight: bold;`:i[e],`color: #aaa; font-weight: bold;`,o),console.log(`Данные (Payload):`,r),this.loggerOptions.traceTime&&typeof performance<`u`&&console.log(`Тайминг рантайма:`,`${performance.now().toFixed(2)} ms`),console.groupEnd();return}case`computed`:{let e=r;if(e&&`to`in e){let t=e.to;a=` 🧮 Значение: ${typeof t==`object`&&t?`{...}`:String(t)}`}else if(e&&`duration`in e){let t=parseFloat(e.duration);if(!isNaN(t)){let r=this.lastComputedDurations.get(n);if(r!==void 0&&r>0){let n=(t-r)/r*100;n>5?(a=` 🔴 Замедление: +${n.toFixed(1)}% (${e.duration})`,o=`color: #e01e5a; font-weight: bold;`):n<-5?(a=` 🟢 Ускорение: ${n.toFixed(1)}% (${e.duration})`,o=`color: #42b883; font-weight: bold;`):(a=` 🟢 Стабильно (${e.duration})`,o=`color: #42b883; font-weight: bold;`)}else a=` ⚪ Первый расчет (${e.duration})`;this.lastComputedDurations.set(n,t)}}break}case`effect`:{let e=(this.effectExecutionCounts.get(n)||0)+1;this.effectExecutionCounts.set(n,e);let t=e>30,i=r;a=` 🟢 Стабильно${i&&`duration`in i?` (${i.duration})`:``} | 🚀 Вызовов: ${e}${t?` ⚠️ (heavy re-renders)`:``}`,o=`color: #42b883; font-weight: bold;`,t&&(o=`color: #ff4a4a; font-weight: bold;`);break}case`batch`:this.batchTickCounts+=1,a=` 📦 Номер транзакции: #${this.batchTickCounts}`;break;case`resource`:{let e=r;if(e.loading)a=` ⏳ ЗАГРУЗКА (fetching...)`,o=`color: #d97706; font-weight: bold;`;else if(e.error)a=` 🔴 ОШИБКА: ${e.error.message||`Unknown Error`}`,o=`color: #ff4a4a; font-weight: bold;`;else{let t=e.data;a=` 🟢 УСПЕХ (data: ${typeof t==`object`&&t?`{...}`:String(t)})`,o=`color: #42b883; font-weight: bold;`}break}default:break}console.groupCollapsed(`%c${e.toUpperCase()}%c [${n}]%c${a}`,i[e],`color: #aaa; font-weight: bold;`,o),console.log(`Данные (Payload):`,r),this.loggerOptions.traceTime&&typeof performance<`u`&&console.log(`Тайминг рантайма:`,`${performance.now().toFixed(2)} ms`),console.groupEnd()}onSignalChange;provide(e,t){typeof t==`function`&&!t.prototype?this.factories.set(e,t):this.services.set(e,t)}inject(e){if(!e)throw Error(`[DI Error]: Вы пытаетесь внедрить пустой токен (undefined/null). Проверьте импорты.`);let t=e;if(this.services.has(t))return this.services.get(t);try{let n=this.factories.get(t);if(n){let e=n(this);return this.services.set(t,e),e}if(typeof e==`function`&&e.prototype){let n=new e(this);return this.services.set(t,n),n}throw Error(`Service not found: ${String(e)}`)}catch(t){throw Error(`[DI Error]: Не удалось создать сервис ${String(e)}. Ошибка: ${t?.message||`No e?.message`}`)}}queueLog(e,t,n){this.loggerOptions?.isEnabled&&this.pendingLogQueue.push({type:e,name:t,detail:n})}flushLogs(){if(!this.loggerOptions?.isEnabled||this.pendingLogQueue.length===0)return;let e=typeof performance<`u`?performance.now():0,t=this.pendingLogQueue;if(this.loggerOptions.filter)try{let e;if(this.loggerOptions.filter instanceof RegExp)e=this.loggerOptions.filter;else{let t=this.loggerOptions.filter.replace(/^\/|\/$/g,``);e=new RegExp(t)}t=t.filter(t=>e.test(t.name))}catch(e){console.warn(`[Logger Error]: Некорректный паттерн фильтрации логов`,e)}if(t.length===0){this.pendingLogQueue=[];return}let n=``,r=``,i=`color: #aaa; font-weight: normal;`;if(this.loggerOptions.traceTime&&e){let t=performance.now()-e;if(n=` | Duration: ${t.toFixed(3)} ms`,this.lastTransactionDuration!==null&&this.lastTransactionDuration>0){let e=(t-this.lastTransactionDuration)/this.lastTransactionDuration*100;e>.1?(r=` 🔴 +${e.toFixed(1)}%`,i=`color: #e01e5a; font-weight: bold;`):e<-.1?(r=` 🟢 ${e.toFixed(1)}%`,i=`color: #42b883; font-weight: bold;`):(r=` 🟢 stable`,i=`color: #42b883; font-weight: bold;`)}else r=` ⚪ first`;this.lastTransactionDuration=t}console.groupCollapsed(`%cREACTIVE TRANSACTION${this.loggerOptions.instanceName?` ${this.loggerOptions.instanceName}`:``}%c Microtask Tick (Size: ${t.length}${n})%c${r}`,`background: #7952b3; color: #fff; padding: 2px 6px; border-radius: 4px; font-weight: bold;`,`color: #aaa; font-weight: normal;`,i),t.forEach(e=>{this.log(e.type,e.name,e.detail)}),this.loggerOptions.traceTime&&typeof performance<`u`&&console.log(`%c[Execution Timestamp]: ${performance.now().toFixed(2)} ms`,`color: #7952b3; font-weight: bold;`),console.groupEnd(),this.pendingLogQueue=[]}signal(e,t){let n=this,r=e,i=new Set,a=typeof t==`string`?{name:t}:t||{},o=a.name||`unnamed_signal`;return{get value(){if(n.activeEffect){let e=n.activeEffect;i.has(e)||(i.add(e),e.cleanups.add(()=>{i.delete(e)}))}return r},set value(e){if(r===e)return;if(a.validate){let t=a.validate(e);if(t===!1||typeof t==`string`){let n=typeof t==`string`?t:`[Validation Error]: Некорректное значение для сигнала "${o}"`;console.error(`%c${n}`,`color: orange; font-weight: bold;`,{received:e,current:r});return}}let t=r;r=e,n.onSignalChange?.(o,e,t);let s=Array.from(i).map(e=>e.label||`unnamed_effect`);n.queueLog?.(`signal`,o,{from:t,to:e,subscribersCount:i.size,subscribers:s}),i.forEach(e=>n.pendingEffects.add(e)),n.isBatching||(n.isBatching=!0,queueMicrotask(()=>{for(let e of n.pendingEffects)n.pendingEffects.delete(e),e.run();n.isBatching=!1,n.flushLogs?.()}))},subscribe(e){return n.effect(()=>{e(this.value)},`${n.frameworkPrefix}:use:${o}`)}}}effect(e,t){let n=this,r=!0,i={label:t,cleanups:new Set,run(){let t=Array.from(this.cleanups);this.cleanups.clear(),t.forEach(e=>e());let i=n.activeEffect;n.activeEffect=this;let a=performance.now();n.safeRun(this,()=>{let t=e();typeof t==`function`&&this.cleanups.add(t)}),n.activeEffect=i;let o=performance.now()-a;if(!r&&n.loggerOptions?.isEnabled){let e=n.loggerOptions.filter;(!e||e instanceof RegExp&&this.label&&e.test(this.label))&&typeof n.queueLog==`function`&&n.queueLog(`effect`,this.label||`unnamed-efect-0`,{name:this.label||`unnamed-efect-0`,type:`effect`,detail:{status:`stable`,duration:`${o.toFixed(3)}ms`,timestamp:a}})}r=!1}};return this.allEffects.add(i),i.run(),()=>{i.cleanups.forEach(e=>e()),n.pendingEffects.delete(i),n.allEffects.delete(i)}}computedCache=new Map;cleanupRegistry=new FinalizationRegistry(e=>{e()});computed(e,t){if(this.computedCache.has(e)){let t=this.computedCache.get(e)?.deref();if(t)return t}let n=this,r=t||`unnamed_computed`,i=this.signal(void 0,`[CORE_INTERNAL_SIGNAL=1]:${r}`),a=!0,o,s=this.effect(()=>{o=e(),a=!1;let t=typeof performance<`u`&&performance.now()?`0.010ms`:`N/A`;typeof n.queueLog==`function`&&n.queueLog(`computed`,r,{value:o,duration:t}),i.value=o},`[CORE_INTERNAL_EFFECT=1]:${r}`),c=Array.from(this.allEffects)[this.allEffects.size-1],l=()=>{s(),c&&n.allEffects.delete(c),n.computedCache.delete(e)},u={get value(){return a&&(o=e(),a=!1,i.value=o),i.value},subscribe:e=>i.subscribe(e),destroy(){s(),l(),n.computedCache.delete(e)}};return this.cleanupRegistry.register(u,l),this.computedCache.set(e,new WeakRef(u)),u}reactive(e,t=`reactive`){if(this.proxyCache.has(e))return this.proxyCache.get(e);let n=this,r=new Map,i=new Proxy(e,{get(e,i,a){if(n.activeEffect){let e=n.activeEffect;r.has(i)||r.set(i,new Set);let t=r.get(i);t.has(e)||(t.add(e),e.cleanups.add(()=>{t.delete(e)}))}let o=Reflect.get(e,i,a);return typeof o==`object`&&o?n.reactive(o,`${t}.${String(i)}`):o},set(e,i,a,o){let s=Reflect.get(e,i,o);if(s===a)return!0;if(s!==a){Reflect.set(e,i,a,o),n.onSignalChange?.(`${t}.${String(i)}`,a,s);let c=r.get(i);(c?Array.from(c):[]).forEach(e=>{n.isBatching?n.pendingEffects.add(e):(n.pendingEffects.add(e),n.isBatching||(n.isBatching=!0,queueMicrotask(()=>{for(let e of n.pendingEffects)n.pendingEffects.delete(e),e.run();n.isBatching=!1,n.flushLogs?.()})))}),n.queueLog?.(`reactive`,`${t}.${String(i)}`,{action:`set`,property:String(i),oldValue:s,newValue:a})}return!0}});return this.proxyCache.set(e,i),i}batch(e){e()}delay(e,t){return new Promise((n,r)=>{if(t.aborted)return r(new DOMException(`Aborted`,`AbortError`));let i=setTimeout(()=>{t.removeEventListener(`abort`,a),n()},e),a=()=>{clearTimeout(i),r(new DOMException(`Aborted`,`AbortError`))};t.addEventListener(`abort`,a,{once:!0})})}resource(e,n,r){let i=r&&typeof r==`object`,a=i?r.name:r||`unnamed_resource`,o=i?r:{},s=o.resetDataOnSourceChange??!0,c=o.responseValidate,l=o.validateBeforeFetch,u=o.retryCount??0,d=o.retryDelay??1e3,f=o.isExponentialBackoffEnabled??!1,p=o.maxRetryDelay??3e4,m=o.timeout,h=this.signal({data:null,loading:!0,error:null,isRetrying:!1},`[CORE_INTERNAL_SIGNAL=1]:resource:state:${a}`),g=e=>{h.value=e,typeof this.queueLog==`function`&&this.queueLog(`resource`,a,{loading:e.loading,data:e.data,error:e.error,isRetrying:e.isRetrying})},_=async(n,r,i=!1)=>{if(r.aborted)return;if(l){let e=l(n);if(e===!1||typeof e==`string`){if(r.aborted)return;let t=typeof e==`string`?e:`Pre-fetch validation failed for resource`,n=i?null:this.untrack(()=>h.value.data);g({data:n,loading:!1,error:Error(t),isRetrying:!1});return}}let o=i&&s?null:this.untrack(()=>h.value.data);g({data:o,loading:!0,error:null,isRetrying:!1});for(let i=0;i<=u;i++){if(r.aborted)return;let o=r;try{if(m&&m>0){let e=AbortSignal.timeout(m);o=AbortSignal.any([r,e])}let t=await e(n,o);if(r.aborted)return;if(o.aborted)throw new DOMException(`The operation timed out.`,`TimeoutError`);if(c){let e=c(t);if(e===!1||typeof e==`string`){if(r.aborted)return;g({data:null,loading:!1,error:Error(typeof e==`string`?e:`Validation failed`),isRetrying:!1});return}}if(r.aborted)return;g({data:t,loading:!1,error:null,isRetrying:!1});return}catch(e){if(r.aborted||e.name===`AbortError`&&r.aborted)return;let n=e.name===`TimeoutError`||e.name===`AbortError`||e.message?.includes(`timeout`),o=t({tested:[e.message],expectedKey:`THROW_CUSTOM_VALIDATION_ERROR_NO_RETRY`,valueType:`number`})?.[0]===`1`||!1,s=o&&t({tested:[e.message],expectedKey:`MESSAGE`,valueType:`string`})?.[0]||`Custom validation error`,c=o||e.name===`ValidationError`;if(i===u||c){if(r.aborted)return;let t=n?Error(`Request timed out after ${m}ms`,{cause:e}):o?Error(s,{cause:e}):e;g({data:null,loading:!1,error:t,isRetrying:!1});return}else{if(r.aborted)return;let e=f?d*2**i:d;e=Math.min(e,p);let t=Math.random()*200;e+=t,g({data:h.value.data,loading:!0,error:null,isRetrying:!0}),console.warn(`[Resource Retry] "${a}" сбой по ${n?`таймауту`:`ошибке сети`}. Попытка ${i+1}/${u+1}...`);try{if(await this.delay(e,r),r.aborted)return}catch{return}}}}},v=null;return this.effect(()=>{let e=n?n.value:void 0,t=new AbortController;return v=t,this.untrack(()=>{_(e,t.signal,!0)}),()=>t.abort()},`[dev]`),{get data(){return h.value.data},get loading(){return h.value.loading},get error(){return h.value.error},get isRetrying(){return h.value.isRetrying},get value(){return h.value},refetch:()=>{v?.abort(),v=new AbortController,_(n?n.value:void 0,v.signal,!1)},subscribe:e=>h.subscribe(e)}}untrack(e){let t=this.activeEffect;this.activeEffect=null;try{return e()}finally{this.activeEffect=t}}safeRun(e,t){try{t()}catch(e){console.error(`%c[Reactive Error] %cОшибка в эффекте/computed:`,`color: white; background: red; padding: 2px 4px; border-radius: 3px;`,`font-weight: bold;`,e)}}},r=class{engine;constructor(e){this.engine=e,this.onInit()}createNumericSignal(e,t){return this.engine.signal(e,{name:t,validate:e=>typeof e==`number`&&!isNaN(e)})}createComputed(e){return this.engine.computed(e)}createSignal(e,t){return this.engine.signal(e,t)}onInit(){}},i=r,a=class{engine;constructor(e){this.engine=e,setTimeout(()=>this.onInit(),0)}onInit(){}};e.AbstractService=r,e.BaseREService=i,e.BaseREServiceEnhanced=a,e.ReactiveEngine=n});
13
13
  //# sourceMappingURL=reactive-engine.umd.js.map