@pravosleva/reactive-engine 1.5.7-beta → 1.5.8-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/decorators/withCache/withCache.cjs.map +1 -1
- package/dist/decorators/withCache/withCache.d.ts +57 -4
- package/dist/decorators/withCache/withCache.d.ts.map +1 -1
- package/dist/decorators/withCache/withCache.mjs.map +1 -1
- package/dist/decorators/withDebounce/withDebounce.cjs.map +1 -1
- package/dist/decorators/withDebounce/withDebounce.d.ts +54 -3
- package/dist/decorators/withDebounce/withDebounce.d.ts.map +1 -1
- package/dist/decorators/withDebounce/withDebounce.mjs.map +1 -1
- package/dist/decorators/withLongPolling/withLongPolling.cjs.map +1 -1
- package/dist/decorators/withLongPolling/withLongPolling.d.ts +69 -2
- package/dist/decorators/withLongPolling/withLongPolling.d.ts.map +1 -1
- package/dist/decorators/withLongPolling/withLongPolling.mjs.map +1 -1
- package/dist/decorators/withThrottle/withThrottle.cjs.map +1 -1
- package/dist/decorators/withThrottle/withThrottle.d.ts +51 -3
- package/dist/decorators/withThrottle/withThrottle.d.ts.map +1 -1
- package/dist/decorators/withThrottle/withThrottle.mjs.map +1 -1
- package/dist/decorators/withThrottleAndCache/withThrottleAndCache.cjs.map +1 -1
- package/dist/decorators/withThrottleAndCache/withThrottleAndCache.d.ts +58 -2
- package/dist/decorators/withThrottleAndCache/withThrottleAndCache.d.ts.map +1 -1
- package/dist/decorators/withThrottleAndCache/withThrottleAndCache.mjs.map +1 -1
- package/dist/decorators/withThrottleComputed/withThrottleComputed.cjs.map +1 -1
- package/dist/decorators/withThrottleComputed/withThrottleComputed.d.ts +54 -0
- package/dist/decorators/withThrottleComputed/withThrottleComputed.d.ts.map +1 -1
- package/dist/decorators/withThrottleComputed/withThrottleComputed.mjs.map +1 -1
- package/dist/reactive-engine.umd.js +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"withCache.cjs","names":[],"sources":["../../../src/decorators/withCache/withCache.ts"],"sourcesContent":["interface CacheEntry<T> {\n data: T;\n timestamp: number;\n}\n\ninterface CacheOptions {\n /** Время жизни кэша в миллисекундах. По умолчанию 5 минут (300000 мс) */\n ttl?: number;\n}\n\n/**\n * Декоратор для создания кэширующего
|
|
1
|
+
{"version":3,"file":"withCache.cjs","names":[],"sources":["../../../src/decorators/withCache/withCache.ts"],"sourcesContent":["interface CacheEntry<T> {\n data: T;\n timestamp: number;\n}\n\ninterface CacheOptions {\n /** Время жизни кэша в миллисекундах. По умолчанию 5 минут (300000 мс) */\n ttl?: number;\n}\n\n/**\n * Декоратор для создания кэширующего загрузчика данных, специально\n * адаптированный для использования совместно с `engine.resource`.\n *\n * Оборачивает асинхронную функцию `fetcher` в контур кэширования в оперативной памяти (In-Memory Cache).\n * При повторных вызовах с теми же входными параметрами `source` декоратор возвращает сохраненный результат\n * из памяти, если не истекло время жизни записи (TTL), полностью предотвращая избыточные сетевые или дисковые запросы.\n *\n * ### 🧠 Механика кэширования и дедупликации:\n * 1. **Динамическая генерация ключей:** Декоратор автоматически формирует строковый ключ кэша на основе аргумента `source`.\n * Если в качестве параметров передан сложный объект или массив, применяется `JSON.stringify`, что обеспечивает\n * глубокое сравнение параметров. Для примитивов используется явное приведение к `String`.\n * 2. **Контроль устаревания (TTL):** Каждая запись в кэше снабжается меткой времени (`timestamp`), которая фиксируется\n * *после* успешного завершения запроса. При каждом вызове проверяется условие `now - timestamp < ttl`.\n * Если кэш устарел, он прозрачно перезаписывается свежими данными.\n *\n * @template S Тип входных данных (аргументов) для функции запроса, используемых для генерации ключа кэша.\n * @template T Тип данных, возвращаемых асинхронным `fetcher`-ом (разрешенное значение промиса).\n *\n * @param {(source: S, signal: AbortSignal) => Promise<T>} fetcher Оригинальная асинхронная функция запроса, принимающая параметры и сигнал отмены.\n * @param {CacheOptions} [options={}] Параметры конфигурации кэширования.\n * @param {number} [options.ttl=300000] Время жизни кэша (Time-To-Live) в миллисекундах (по умолчанию 5 минут).\n *\n * @returns {(source: S, signal: AbortSignal) => Promise<T>} Возвращает обернутую асинхронную функцию, возвращающую `Promise<T>` и полностью сохраняющую исходную сигнатуру типов.\n *\n * @example\n * ```typescript\n * import { withCache } from '@pravosleva/reactive-engine';\n *\n * interface FilterParams { category: string; tags: string[]; }\n *\n * const fetchCategories = async (params: FilterParams, signal: AbortSignal) => {\n * const res = await fetch(`/api/items?cat=${params.category}&tags=${params.tags.join(',')}`, { signal });\n * return res.json();\n * };\n *\n * // Кэшируем результаты запросов на 1 минуту. Повторные вызовы с одинаковыми тегами не пойдут в сеть.\n * const cachedFetch = withCache(fetchCategories, { ttl: 60 * 1000 });\n *\n * // Интеграция с подсистемой ресурсов вашего реактивного ядра\n * const directoryResource = engine.resource({\n * fetcher: cachedFetch,\n * source: () => currentFilters.value // Следит за сигналом фильтров\n * });\n * ```\n *\n * @abstract\n * ### 🚨 Внимание при работе в SSR (Next.js / Nuxt 3)\n * Поскольку структура кэша `cache = new Map()` объявлена на уровне замыкания декоратора, в среде выполнения\n * на сервере Node.js этот экземпляр кэша будет **общим для всех HTTP-запросов**, если декоратор создан\n * как глобальный синглтон.\n * - Для предотвращения утечек данных между пользователями в SSR-среде создавайте обернутую функцию `withCache`\n * **строго внутри локального request-scoped контекста** (например, внутри плагинов или компонентов),\n * либо используйте её исключительно на стороне клиента (CSR).\n *\n * ### 🧪 Совместимость с Fake Timers\n * В коде используется нативный `Date.now()`, что обеспечивает 100% стабильность работы при тестировании\n * кэша через имитацию времени в Vitest / Jest с помощью функций `vi.useFakeTimers()` и `vi.advanceTimersByTime()`.\n */\nexport const withCache = <S, T>(\n fetcher: (source: S, signal: AbortSignal) => Promise<T>,\n options: CacheOptions = {}\n) => {\n const ttl = options.ttl ?? 5 * 60 * 1000 // Дефолтный TTL: 5 минут\n const cache = new Map<string, CacheEntry<T>>()\n\n return async (source: S, signal: AbortSignal): Promise<T> => {\n // 1. Генерируем уникальный ключ на основе зависимостей (массива или объекта)\n const cacheKey = typeof source === 'object' && source !== null\n ? JSON.stringify(source)\n : String(source)\n\n const now = Date.now()\n const cached = cache.get(cacheKey)\n\n // 2. Проверяем, есть ли валидный (не устаревший) кэш\n if (cached && (now - cached.timestamp < ttl)) {\n return cached.data\n }\n\n // 3. Если кэша нет или он устарел — делаем реальный сетевой запрос\n const freshData = await fetcher(source, signal)\n\n // 4. Сохраняем свежие данные и метку времени в кэш\n cache.set(cacheKey, {\n data: freshData,\n timestamp: Date.now() // берем актуальное время после завершения запроса\n })\n\n return freshData\n }\n}\n"],"mappings":"AAqEA,IAAa,GACX,EACA,EAAwB,CAAC,IACtB,CACH,IAAM,EAAM,EAAQ,KAAO,IAAS,IAC9B,EAAQ,IAAI,IAElB,OAAO,MAAO,EAAW,IAAoC,CAE3D,IAAM,EAAW,OAAO,GAAW,UAAY,EAC3C,KAAK,UAAU,CAAM,EACrB,OAAO,CAAM,EAEX,EAAM,KAAK,IAAI,EACf,EAAS,EAAM,IAAI,CAAQ,EAGjC,GAAI,GAAW,EAAM,EAAO,UAAY,EACtC,OAAO,EAAO,KAIhB,IAAM,EAAY,MAAM,EAAQ,EAAQ,CAAM,EAQ9C,OALA,EAAM,IAAI,EAAU,CAClB,KAAM,EACN,UAAW,KAAK,IAAI,CACtB,CAAC,EAEM,CACT,CACF"}
|
|
@@ -3,10 +3,63 @@ interface CacheOptions {
|
|
|
3
3
|
ttl?: number;
|
|
4
4
|
}
|
|
5
5
|
/**
|
|
6
|
-
* Декоратор для создания кэширующего
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* Декоратор для создания кэширующего загрузчика данных, специально
|
|
7
|
+
* адаптированный для использования совместно с `engine.resource`.
|
|
8
|
+
*
|
|
9
|
+
* Оборачивает асинхронную функцию `fetcher` в контур кэширования в оперативной памяти (In-Memory Cache).
|
|
10
|
+
* При повторных вызовах с теми же входными параметрами `source` декоратор возвращает сохраненный результат
|
|
11
|
+
* из памяти, если не истекло время жизни записи (TTL), полностью предотвращая избыточные сетевые или дисковые запросы.
|
|
12
|
+
*
|
|
13
|
+
* ### 🧠 Механика кэширования и дедупликации:
|
|
14
|
+
* 1. **Динамическая генерация ключей:** Декоратор автоматически формирует строковый ключ кэша на основе аргумента `source`.
|
|
15
|
+
* Если в качестве параметров передан сложный объект или массив, применяется `JSON.stringify`, что обеспечивает
|
|
16
|
+
* глубокое сравнение параметров. Для примитивов используется явное приведение к `String`.
|
|
17
|
+
* 2. **Контроль устаревания (TTL):** Каждая запись в кэше снабжается меткой времени (`timestamp`), которая фиксируется
|
|
18
|
+
* *после* успешного завершения запроса. При каждом вызове проверяется условие `now - timestamp < ttl`.
|
|
19
|
+
* Если кэш устарел, он прозрачно перезаписывается свежими данными.
|
|
20
|
+
*
|
|
21
|
+
* @template S Тип входных данных (аргументов) для функции запроса, используемых для генерации ключа кэша.
|
|
22
|
+
* @template T Тип данных, возвращаемых асинхронным `fetcher`-ом (разрешенное значение промиса).
|
|
23
|
+
*
|
|
24
|
+
* @param {(source: S, signal: AbortSignal) => Promise<T>} fetcher Оригинальная асинхронная функция запроса, принимающая параметры и сигнал отмены.
|
|
25
|
+
* @param {CacheOptions} [options={}] Параметры конфигурации кэширования.
|
|
26
|
+
* @param {number} [options.ttl=300000] Время жизни кэша (Time-To-Live) в миллисекундах (по умолчанию 5 минут).
|
|
27
|
+
*
|
|
28
|
+
* @returns {(source: S, signal: AbortSignal) => Promise<T>} Возвращает обернутую асинхронную функцию, возвращающую `Promise<T>` и полностью сохраняющую исходную сигнатуру типов.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```typescript
|
|
32
|
+
* import { withCache } from '@pravosleva/reactive-engine';
|
|
33
|
+
*
|
|
34
|
+
* interface FilterParams { category: string; tags: string[]; }
|
|
35
|
+
*
|
|
36
|
+
* const fetchCategories = async (params: FilterParams, signal: AbortSignal) => {
|
|
37
|
+
* const res = await fetch(`/api/items?cat=${params.category}&tags=${params.tags.join(',')}`, { signal });
|
|
38
|
+
* return res.json();
|
|
39
|
+
* };
|
|
40
|
+
*
|
|
41
|
+
* // Кэшируем результаты запросов на 1 минуту. Повторные вызовы с одинаковыми тегами не пойдут в сеть.
|
|
42
|
+
* const cachedFetch = withCache(fetchCategories, { ttl: 60 * 1000 });
|
|
43
|
+
*
|
|
44
|
+
* // Интеграция с подсистемой ресурсов вашего реактивного ядра
|
|
45
|
+
* const directoryResource = engine.resource({
|
|
46
|
+
* fetcher: cachedFetch,
|
|
47
|
+
* source: () => currentFilters.value // Следит за сигналом фильтров
|
|
48
|
+
* });
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* @abstract
|
|
52
|
+
* ### 🚨 Внимание при работе в SSR (Next.js / Nuxt 3)
|
|
53
|
+
* Поскольку структура кэша `cache = new Map()` объявлена на уровне замыкания декоратора, в среде выполнения
|
|
54
|
+
* на сервере Node.js этот экземпляр кэша будет **общим для всех HTTP-запросов**, если декоратор создан
|
|
55
|
+
* как глобальный синглтон.
|
|
56
|
+
* - Для предотвращения утечек данных между пользователями в SSR-среде создавайте обернутую функцию `withCache`
|
|
57
|
+
* **строго внутри локального request-scoped контекста** (например, внутри плагинов или компонентов),
|
|
58
|
+
* либо используйте её исключительно на стороне клиента (CSR).
|
|
59
|
+
*
|
|
60
|
+
* ### 🧪 Совместимость с Fake Timers
|
|
61
|
+
* В коде используется нативный `Date.now()`, что обеспечивает 100% стабильность работы при тестировании
|
|
62
|
+
* кэша через имитацию времени в Vitest / Jest с помощью функций `vi.useFakeTimers()` и `vi.advanceTimersByTime()`.
|
|
10
63
|
*/
|
|
11
64
|
export declare const withCache: <S, T>(fetcher: (source: S, signal: AbortSignal) => Promise<T>, options?: CacheOptions) => (source: S, signal: AbortSignal) => Promise<T>;
|
|
12
65
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"withCache.d.ts","sourceRoot":"","sources":["../../../src/decorators/withCache/withCache.ts"],"names":[],"mappings":"AAKA,UAAU,YAAY;IACpB,yEAAyE;IACzE,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED
|
|
1
|
+
{"version":3,"file":"withCache.d.ts","sourceRoot":"","sources":["../../../src/decorators/withCache/withCache.ts"],"names":[],"mappings":"AAKA,UAAU,YAAY;IACpB,yEAAyE;IACzE,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0DG;AACH,eAAO,MAAM,SAAS,GAAI,CAAC,EAAE,CAAC,EAC5B,SAAS,CAAC,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC,EACvD,UAAS,YAAiB,MAKZ,QAAQ,CAAC,EAAE,QAAQ,WAAW,KAAG,OAAO,CAAC,CAAC,CAyBzD,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"withCache.mjs","names":[],"sources":["../../../src/decorators/withCache/withCache.ts"],"sourcesContent":["interface CacheEntry<T> {\n data: T;\n timestamp: number;\n}\n\ninterface CacheOptions {\n /** Время жизни кэша в миллисекундах. По умолчанию 5 минут (300000 мс) */\n ttl?: number;\n}\n\n/**\n * Декоратор для создания кэширующего
|
|
1
|
+
{"version":3,"file":"withCache.mjs","names":[],"sources":["../../../src/decorators/withCache/withCache.ts"],"sourcesContent":["interface CacheEntry<T> {\n data: T;\n timestamp: number;\n}\n\ninterface CacheOptions {\n /** Время жизни кэша в миллисекундах. По умолчанию 5 минут (300000 мс) */\n ttl?: number;\n}\n\n/**\n * Декоратор для создания кэширующего загрузчика данных, специально\n * адаптированный для использования совместно с `engine.resource`.\n *\n * Оборачивает асинхронную функцию `fetcher` в контур кэширования в оперативной памяти (In-Memory Cache).\n * При повторных вызовах с теми же входными параметрами `source` декоратор возвращает сохраненный результат\n * из памяти, если не истекло время жизни записи (TTL), полностью предотвращая избыточные сетевые или дисковые запросы.\n *\n * ### 🧠 Механика кэширования и дедупликации:\n * 1. **Динамическая генерация ключей:** Декоратор автоматически формирует строковый ключ кэша на основе аргумента `source`.\n * Если в качестве параметров передан сложный объект или массив, применяется `JSON.stringify`, что обеспечивает\n * глубокое сравнение параметров. Для примитивов используется явное приведение к `String`.\n * 2. **Контроль устаревания (TTL):** Каждая запись в кэше снабжается меткой времени (`timestamp`), которая фиксируется\n * *после* успешного завершения запроса. При каждом вызове проверяется условие `now - timestamp < ttl`.\n * Если кэш устарел, он прозрачно перезаписывается свежими данными.\n *\n * @template S Тип входных данных (аргументов) для функции запроса, используемых для генерации ключа кэша.\n * @template T Тип данных, возвращаемых асинхронным `fetcher`-ом (разрешенное значение промиса).\n *\n * @param {(source: S, signal: AbortSignal) => Promise<T>} fetcher Оригинальная асинхронная функция запроса, принимающая параметры и сигнал отмены.\n * @param {CacheOptions} [options={}] Параметры конфигурации кэширования.\n * @param {number} [options.ttl=300000] Время жизни кэша (Time-To-Live) в миллисекундах (по умолчанию 5 минут).\n *\n * @returns {(source: S, signal: AbortSignal) => Promise<T>} Возвращает обернутую асинхронную функцию, возвращающую `Promise<T>` и полностью сохраняющую исходную сигнатуру типов.\n *\n * @example\n * ```typescript\n * import { withCache } from '@pravosleva/reactive-engine';\n *\n * interface FilterParams { category: string; tags: string[]; }\n *\n * const fetchCategories = async (params: FilterParams, signal: AbortSignal) => {\n * const res = await fetch(`/api/items?cat=${params.category}&tags=${params.tags.join(',')}`, { signal });\n * return res.json();\n * };\n *\n * // Кэшируем результаты запросов на 1 минуту. Повторные вызовы с одинаковыми тегами не пойдут в сеть.\n * const cachedFetch = withCache(fetchCategories, { ttl: 60 * 1000 });\n *\n * // Интеграция с подсистемой ресурсов вашего реактивного ядра\n * const directoryResource = engine.resource({\n * fetcher: cachedFetch,\n * source: () => currentFilters.value // Следит за сигналом фильтров\n * });\n * ```\n *\n * @abstract\n * ### 🚨 Внимание при работе в SSR (Next.js / Nuxt 3)\n * Поскольку структура кэша `cache = new Map()` объявлена на уровне замыкания декоратора, в среде выполнения\n * на сервере Node.js этот экземпляр кэша будет **общим для всех HTTP-запросов**, если декоратор создан\n * как глобальный синглтон.\n * - Для предотвращения утечек данных между пользователями в SSR-среде создавайте обернутую функцию `withCache`\n * **строго внутри локального request-scoped контекста** (например, внутри плагинов или компонентов),\n * либо используйте её исключительно на стороне клиента (CSR).\n *\n * ### 🧪 Совместимость с Fake Timers\n * В коде используется нативный `Date.now()`, что обеспечивает 100% стабильность работы при тестировании\n * кэша через имитацию времени в Vitest / Jest с помощью функций `vi.useFakeTimers()` и `vi.advanceTimersByTime()`.\n */\nexport const withCache = <S, T>(\n fetcher: (source: S, signal: AbortSignal) => Promise<T>,\n options: CacheOptions = {}\n) => {\n const ttl = options.ttl ?? 5 * 60 * 1000 // Дефолтный TTL: 5 минут\n const cache = new Map<string, CacheEntry<T>>()\n\n return async (source: S, signal: AbortSignal): Promise<T> => {\n // 1. Генерируем уникальный ключ на основе зависимостей (массива или объекта)\n const cacheKey = typeof source === 'object' && source !== null\n ? JSON.stringify(source)\n : String(source)\n\n const now = Date.now()\n const cached = cache.get(cacheKey)\n\n // 2. Проверяем, есть ли валидный (не устаревший) кэш\n if (cached && (now - cached.timestamp < ttl)) {\n return cached.data\n }\n\n // 3. Если кэша нет или он устарел — делаем реальный сетевой запрос\n const freshData = await fetcher(source, signal)\n\n // 4. Сохраняем свежие данные и метку времени в кэш\n cache.set(cacheKey, {\n data: freshData,\n timestamp: Date.now() // берем актуальное время после завершения запроса\n })\n\n return freshData\n }\n}\n"],"mappings":";AAqEA,IAAa,KACX,GACA,IAAwB,CAAC,MACtB;CACH,IAAM,IAAM,EAAQ,OAAO,MAAS,KAC9B,oBAAQ,IAAI,IAA2B;CAE7C,OAAO,OAAO,GAAW,MAAoC;EAE3D,IAAM,IAAW,OAAO,KAAW,YAAY,IAC3C,KAAK,UAAU,CAAM,IACrB,OAAO,CAAM,GAEX,IAAM,KAAK,IAAI,GACf,IAAS,EAAM,IAAI,CAAQ;EAGjC,IAAI,KAAW,IAAM,EAAO,YAAY,GACtC,OAAO,EAAO;EAIhB,IAAM,IAAY,MAAM,EAAQ,GAAQ,CAAM;EAQ9C,OALA,EAAM,IAAI,GAAU;GAClB,MAAM;GACN,WAAW,KAAK,IAAI;EACtB,CAAC,GAEM;CACT;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"withDebounce.cjs","names":[],"sources":["../../../src/decorators/withDebounce/withDebounce.ts"],"sourcesContent":["interface DebounceOptions {\n /** Задержка в миллисекундах. По умолчанию 300 мс */\n delay?: number;\n}\n\n/**\n * Декоратор для создания дебаунсящего
|
|
1
|
+
{"version":3,"file":"withDebounce.cjs","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,GACX,EACA,EAA2B,CAAC,IACzB,CACH,IAAM,EAAQ,EAAQ,OAAS,IAG3B,EAAkD,KAElD,EAAiD,KAErD,OAAQ,EAAW,KAEb,GACF,aAAa,CAAS,EAEpB,GAEF,EAAe,IAAI,aAAa,0BAA2B,YAAY,CAAC,EAGnE,IAAI,SAAY,EAAS,IAAW,CAEzC,EAAiB,EAGjB,IAAM,MAAgB,CAChB,GAAW,aAAa,CAAS,EACrC,EAAO,IAAI,aAAa,6BAA8B,YAAY,CAAC,CACrE,EAEA,GAAI,EAAO,QACT,OAAO,EAAQ,EAEjB,EAAO,iBAAiB,QAAS,CAAO,EAGxC,EAAY,WAAW,SAAY,CAEjC,EAAO,oBAAoB,QAAS,CAAO,EAC3C,EAAiB,KACjB,EAAY,KAEZ,GAAI,CAGF,EAAQ,MADW,EAAQ,EAAQ,CAAM,CAC7B,CACd,OAAS,EAAO,CACd,EAAO,CAAK,CACd,CACF,EAAG,CAAK,CACV,CAAC,EAEL"}
|
|
@@ -3,9 +3,60 @@ interface DebounceOptions {
|
|
|
3
3
|
delay?: number;
|
|
4
4
|
}
|
|
5
5
|
/**
|
|
6
|
-
* Декоратор для создания дебаунсящего
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* Декоратор для создания дебаунсящего (отложенного) загрузчика данных, специально
|
|
7
|
+
* адаптированный для использования совместно с `engine.resource`.
|
|
8
|
+
*
|
|
9
|
+
* Откладывает выполнение асинхронной функции `fetcher` на заданную задержку `options.delay`.
|
|
10
|
+
* Если в течение этого интервала происходит новый вызов (например, пользователь продолжает вводить текст),
|
|
11
|
+
* таймер сбрасывается, а предыдущий ожидающий промис принудительно отклоняется. В итоге выполняется
|
|
12
|
+
* только один запрос — после того, как поток вызовов затихнет.
|
|
13
|
+
*
|
|
14
|
+
* ### 🧠 Механика управления промисами (Контур Debounce):
|
|
15
|
+
* 1. **Сброс дребезга:** При каждом вызове функции проверяется наличие активного таймера. Если он есть,
|
|
16
|
+
* `clearTimeout` мгновенно останавливает его.
|
|
17
|
+
* 2. **Каскадное отклонение:** Ссылка на метод `reject` каждого создаваемого промиса сохраняется в `rejectPrevious`.
|
|
18
|
+
* При поступлении нового запроса предыдущий промис не остается «зависшим», а принудительно переходит
|
|
19
|
+
* в состояние `rejected` с ошибкой `AbortError`. Это сигнализирует ядру, что данные устарели.
|
|
20
|
+
* 3. **Чистый запуск:** Оригинальный `fetcher` вызывается только тогда, когда таймер успешно дотикал до конца,
|
|
21
|
+
* не будучи прерванным.
|
|
22
|
+
*
|
|
23
|
+
* @template S Тип входных данных (аргументов) для функции запроса.
|
|
24
|
+
* @template T Тип данных, возвращаемых асинхронным `fetcher`-ом (разрешенное значение промиса).
|
|
25
|
+
*
|
|
26
|
+
* @param {(source: S, signal: AbortSignal) => Promise<T>} fetcher Оригинальная асинхронная функция запроса, принимающая параметры и сигнал отмены.
|
|
27
|
+
* @param {DebounceOptions} [options={}] Параметры конфигурации дебаунса.
|
|
28
|
+
* @param {number} [options.delay=300] Время ожидания в миллисекундах с момента последнего вызова до фактического старта запроса.
|
|
29
|
+
*
|
|
30
|
+
* @returns {(source: S, signal: AbortSignal) => Promise<T>} Возвращает обернутую дебаунс-функцию, возвращающую `Promise<T>` и полностью сохраняющую исходную сигнатуру типов.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```typescript
|
|
34
|
+
* import { withDebounce } from '@pravosleva/reactive-engine';
|
|
35
|
+
*
|
|
36
|
+
* const searchProducts = async (query: string, signal: AbortSignal) => {
|
|
37
|
+
* const res = await fetch(`/api/products?search=${encodeURIComponent(query)}`, { signal });
|
|
38
|
+
* return res.json();
|
|
39
|
+
* };
|
|
40
|
+
*
|
|
41
|
+
* // Запрос уйдет только через 400мс после того, как пользователь перестанет нажимать клавиши
|
|
42
|
+
* const debouncedFetch = withDebounce(searchProducts, { delay: 400 });
|
|
43
|
+
*
|
|
44
|
+
* // Интеграция с фабрикой ресурсов вашего реактивного ядра
|
|
45
|
+
* const productSearchResource = engine.resource({
|
|
46
|
+
* fetcher: debouncedFetch,
|
|
47
|
+
* source: () => searchInputValue.value // Следит за сигналом строки ввода
|
|
48
|
+
* });
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* @abstract
|
|
52
|
+
* ### 🚨 Сквозная интеграция с AbortSignal и менеджмент памяти
|
|
53
|
+
* Декоратор непрерывно отслеживает состояние нативного `AbortSignal`, передаваемого движком (например, при размонтировании UI-виджета):
|
|
54
|
+
* - Если сигнал переходит в состояние `aborted` во время отсчета таймаута дебаунса, таймер сбрасывается,
|
|
55
|
+
* а промис мгновенно отклоняется.
|
|
56
|
+
* - При успешном выполнении таймера слушатель `abort` своевременно удаляется через `removeEventListener`,
|
|
57
|
+
* исключая накопление холостых подписок.
|
|
58
|
+
* - Все внутренние ссылки на методы разрешения промисов (`rejectPrevious`, `timeoutId`) принудительно зануляются (`null`),
|
|
59
|
+
* очищая замыкания от застрявших в памяти объектов и защищая Node.js/браузер от утечек.
|
|
9
60
|
*/
|
|
10
61
|
export declare const withDebounce: <S, T>(fetcher: (source: S, signal: AbortSignal) => Promise<T>, options?: DebounceOptions) => (source: S, signal: AbortSignal) => Promise<T>;
|
|
11
62
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"withDebounce.d.ts","sourceRoot":"","sources":["../../../src/decorators/withDebounce/withDebounce.ts"],"names":[],"mappings":"AAAA,UAAU,eAAe;IACvB,oDAAoD;IACpD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED
|
|
1
|
+
{"version":3,"file":"withDebounce.d.ts","sourceRoot":"","sources":["../../../src/decorators/withDebounce/withDebounce.ts"],"names":[],"mappings":"AAAA,UAAU,eAAe;IACvB,oDAAoD;IACpD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AACH,eAAO,MAAM,YAAY,GAAI,CAAC,EAAE,CAAC,EAC/B,SAAS,CAAC,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC,EACvD,UAAS,eAAoB,MASrB,QAAQ,CAAC,EAAE,QAAQ,WAAW,KAAG,OAAO,CAAC,CAAC,CA0CnD,CAAA"}
|
|
@@ -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 * Декоратор для создания дебаунсящего
|
|
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 * Адаптированный декоратор
|
|
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"}
|
|
@@ -20,8 +20,75 @@ interface LongPollingOptions {
|
|
|
20
20
|
externalSignal?: AbortSignal;
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
|
-
* Адаптированный декоратор
|
|
24
|
-
*
|
|
23
|
+
* Адаптированный декоратор для реализации паттерна Long Polling (длинные опросы),
|
|
24
|
+
* спроектированный специально для интеграции с подсистемой `engine.resource`.
|
|
25
|
+
*
|
|
26
|
+
* Превращает разовый асинхронный запрос данных в непрерывный контролируемый конвейер
|
|
27
|
+
* циклического опроса сервера с поддержкой экспоненциального отката при ошибках
|
|
28
|
+
* (Exponential Backoff) и защитой от пересечения параллельных сессий.
|
|
29
|
+
*
|
|
30
|
+
* ### 🧠 Архитектура конвейера опроса:
|
|
31
|
+
* 1. **Контроль сессионных токенов:** Каждый новый вызов функции инкрементирует `currentSessionToken`.
|
|
32
|
+
* Если предыдущий запрос завершится позже, таймер следующего шага не взведется, так как
|
|
33
|
+
* идентификаторы сессий (`activeSessionId === currentSessionToken`) не совпадут. Это полностью исключает утечки и гонки ответов.
|
|
34
|
+
* 2. **Экспоненциальный откат (Exponential Backoff):** При успешном запросе интервал между циклами равен `options.delay`.
|
|
35
|
+
* В случае падения сетевого запроса, время ожидания перед следующим опросом прогрессивно удваивается,
|
|
36
|
+
* начиная с `errorInitialDelay` вплоть до достижения лимита `errorMaxDelay`, чтобы не спамить упавший бэкенд.
|
|
37
|
+
* 3. **Двойной контур отмены:** Поддерживает как локальный `AbortSignal` от ресурса, так и глобальный `externalSignal`
|
|
38
|
+
* (например, для остановки всех фоновых процессов при закрытии приложения).
|
|
39
|
+
*
|
|
40
|
+
* @template S Тип входных данных (аргументов) для функции запроса.
|
|
41
|
+
* @template T Тип данных, возвращаемых асинхронным `fetcher`-ом (разрешенное значение промиса).
|
|
42
|
+
*
|
|
43
|
+
* @param {(source: S, signal: AbortSignal) => Promise<T>} fetcher Асинхронная функция-загрузчик (например, fetch-запрос к эндпоинту уведомлений).
|
|
44
|
+
* @param {LongPollingOptions} options Конфигурация параметров и коллбэков длинных опросов.
|
|
45
|
+
* @param {() => void} options.onNextTick Коллбэк-триггер, вызываемый для перезапуска цикла опроса (в контексте ядра обычно мутирует или инвалидирует `source` ресурса).
|
|
46
|
+
* @param {(currentDelay: number, increaseDelay: () => void) => void} [options.onError] Коллбэк, вызываемый при ошибке сети. Позволяет логировать сбои и управлять шагом экспоненциального отката через запуск функции `increaseDelay()`.
|
|
47
|
+
* @param {AbortSignal} [options.externalSignal] Внешний сигнал отмены для принудительной остановки поллинга извне (независимо от жизненного цикла ресурса).
|
|
48
|
+
* @param {number} [options.delay=500] Базовая задержка в миллисекундах между успешным ответом сервера и началом следующего запроса.
|
|
49
|
+
* @param {number} [options.errorInitialDelay=2000] Стартовая задержка в миллисекундах при возникновении первой ошибки.
|
|
50
|
+
* @param {number} [options.errorMaxDelay=8000] Максимально возможный интервал задержки в миллисекундах при череде последовательных ошибок.
|
|
51
|
+
*
|
|
52
|
+
* @returns {(source: S, signal: AbortSignal) => Promise<T>} Возвращает обернутую циклическую функцию, возвращающую `Promise<T>`.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```typescript
|
|
56
|
+
* import { withLongPolling } from '@pravosleva/reactive-engine';
|
|
57
|
+
*
|
|
58
|
+
* // 1. Настраиваем функцию опроса новостной ленты
|
|
59
|
+
* const pollingFetch = withLongPolling(
|
|
60
|
+
* async (userId: string, signal) => {
|
|
61
|
+
* const res = await fetch(`/api/notifications?uid=${userId}`, { signal });
|
|
62
|
+
* return res.json();
|
|
63
|
+
* },
|
|
64
|
+
* {
|
|
65
|
+
* delay: 1000,
|
|
66
|
+
* errorInitialDelay: 2000,
|
|
67
|
+
* errorMaxDelay: 10000,
|
|
68
|
+
* onNextTick: () => {
|
|
69
|
+
* // Инвалидируем или триггерим обновление ресурса в ядре
|
|
70
|
+
* notificationResource.refresh();
|
|
71
|
+
* },
|
|
72
|
+
* onError: (delay, increaseDelay) => {
|
|
73
|
+
* console.warn(`Ошибка сети. Следующая попытка через ${delay}мс`);
|
|
74
|
+
* increaseDelay(); // Удваиваем интервал до следующего тика
|
|
75
|
+
* }
|
|
76
|
+
* }
|
|
77
|
+
* );
|
|
78
|
+
*
|
|
79
|
+
* // 2. Передаем декоратор в подсистему ресурсов ядра
|
|
80
|
+
* const notificationResource = engine.resource({
|
|
81
|
+
* fetcher: pollingFetch,
|
|
82
|
+
* source: () => currentUserId.value
|
|
83
|
+
* });
|
|
84
|
+
* ```
|
|
85
|
+
*
|
|
86
|
+
* @abstract
|
|
87
|
+
* ### 🚨 Поведение при отмене и очистка слушателей
|
|
88
|
+
* При срабатывании любого из сигналов отмены (`signal` или `externalSignal` переходит в состояние `aborted`):
|
|
89
|
+
* - Текущий ожидающий промис немедленно отклоняется с системным исключением `AbortError`.
|
|
90
|
+
* - Цикл поллинга блокируется и полностью прекращает планирование будущих макрозадач `setTimeout`.
|
|
91
|
+
* - Все подписки на события `'abort'` корректно удаляются, предотвращая скрытые утечки памяти в Node.js / браузере.
|
|
25
92
|
*/
|
|
26
93
|
export declare const withLongPolling: <S, T>(fetcher: (source: S, signal: AbortSignal) => Promise<T>, options: LongPollingOptions) => (source: S, signal: AbortSignal) => Promise<T>;
|
|
27
94
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"withLongPolling.d.ts","sourceRoot":"","sources":["../../../src/decorators/withLongPolling/withLongPolling.ts"],"names":[],"mappings":"AAAA,UAAU,kBAAkB;IAC1B,wFAAwF;IACxF,UAAU,EAAE,MAAM,IAAI,CAAC;IAEvB;;;;;;;OAOG;IACH,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;IAEjE,uFAAuF;IACvF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mFAAmF;IACnF,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,iFAAiF;IACjF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iEAAiE;IACjE,cAAc,CAAC,EAAE,WAAW,CAAC;CAC9B;AAED
|
|
1
|
+
{"version":3,"file":"withLongPolling.d.ts","sourceRoot":"","sources":["../../../src/decorators/withLongPolling/withLongPolling.ts"],"names":[],"mappings":"AAAA,UAAU,kBAAkB;IAC1B,wFAAwF;IACxF,UAAU,EAAE,MAAM,IAAI,CAAC;IAEvB;;;;;;;OAOG;IACH,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;IAEjE,uFAAuF;IACvF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mFAAmF;IACnF,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,iFAAiF;IACjF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iEAAiE;IACjE,cAAc,CAAC,EAAE,WAAW,CAAC;CAC9B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsEG;AACH,eAAO,MAAM,eAAe,GAAI,CAAC,EAAE,CAAC,EAClC,SAAS,CAAC,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC,EACvD,SAAS,kBAAkB,MAcnB,QAAQ,CAAC,EAAE,QAAQ,WAAW,KAAG,OAAO,CAAC,CAAC,CAmEnD,CAAA"}
|
|
@@ -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 * Адаптированный декоратор
|
|
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 * Декоратор для создания
|
|
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"}
|
|
@@ -3,9 +3,57 @@ interface ThrottleOptions {
|
|
|
3
3
|
limit?: number;
|
|
4
4
|
}
|
|
5
5
|
/**
|
|
6
|
-
* Декоратор для создания
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* Декоратор для создания дросселирующего (throttled) загрузчика данных, специально
|
|
7
|
+
* адаптированный для использования совместно с `engine.resource`.
|
|
8
|
+
*
|
|
9
|
+
* Ограничивает частоту вызовов асинхронной функции `fetcher`, гарантируя выполнение
|
|
10
|
+
* первого запроса мгновенно (Leading edge), а самого последнего запроса — на хвосте
|
|
11
|
+
* временного интервала (Trailing edge) с автоматической отменой всех промежуточных вызовов.
|
|
12
|
+
*
|
|
13
|
+
* ### 🧠 Механика обработки вызовов (Две фазы):
|
|
14
|
+
* 1. **Прямое выполнение (Leading edge):** Если окно блокировки закрыто (`remainingTime <= 0`),
|
|
15
|
+
* запрос пробрасывается к исходному `fetcher` немедленно. Время последнего выполнения фиксируется.
|
|
16
|
+
* 2. **Отложенное выполнение (Trailing edge):** Если вызов происходит внутри активного окна блокировки,
|
|
17
|
+
* его параметры (`source`, `signal`, `resolve`, `reject`) запоминаются, перезаписывая предыдущие.
|
|
18
|
+
* По истечении таймера выполнится только самый актуальный (последний) запрос.
|
|
19
|
+
* Все промежуточные «перезаписанные» промисы отклоняются с ошибкой `AbortError`.
|
|
20
|
+
*
|
|
21
|
+
* @template S Тип входных данных (аргументов) для функции запроса.
|
|
22
|
+
* @template T Тип данных, возвращаемых асинхронным `fetcher`-ом (разрешенное значение промиса).
|
|
23
|
+
*
|
|
24
|
+
* @param {(source: S, signal: AbortSignal) => Promise<T>} fetcher Оригинальная асинхронная функция запроса, принимающая параметры и сигнал отмены.
|
|
25
|
+
* @param {ThrottleOptions} [options={}] Параметры конфигурации троттлинга.
|
|
26
|
+
* @param {number} [options.limit=300] Минимальный интервал времени в миллисекундах между последовательными прямыми вызовами.
|
|
27
|
+
*
|
|
28
|
+
* @returns {(source: S, signal: AbortSignal) => Promise<T>} Возвращает обернутую функцию, возвращающую `Promise<T>` и полностью сохраняющую исходную сигнатуру типов.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```typescript
|
|
32
|
+
* import { withThrottle } from '@pravosleva/reactive-engine';
|
|
33
|
+
*
|
|
34
|
+
* const fetchUserData = async (userId: string, signal: AbortSignal) => {
|
|
35
|
+
* const res = await fetch(`/api/users/${userId}`, { signal });
|
|
36
|
+
* return res.json();
|
|
37
|
+
* };
|
|
38
|
+
*
|
|
39
|
+
* // Ограничиваем частоту запросов профиля до 1 раза в 400мс
|
|
40
|
+
* const throttledFetch = withThrottle(fetchUserData, { limit: 400 });
|
|
41
|
+
*
|
|
42
|
+
* // Интеграция с подсистемой ресурсов вашего ядра
|
|
43
|
+
* const userResource = engine.resource({
|
|
44
|
+
* fetcher: throttledFetch,
|
|
45
|
+
* source: () => currentUserId.value
|
|
46
|
+
* });
|
|
47
|
+
* ```
|
|
48
|
+
*
|
|
49
|
+
* @abstract
|
|
50
|
+
* ### 🚨 Интеграция с AbortSignal и защита от утечек памяти
|
|
51
|
+
* Декоратор подписывается на событие отмены `abort` переданного сигнала. Если внешний ресурс
|
|
52
|
+
* размонтируется или перезапрашивает данные в процессе ожидания внутри окна троттлинга:
|
|
53
|
+
* - Активный таймер `setTimeout` незамедлительно сбрасывается.
|
|
54
|
+
* - Ожидающий хвостовой промис переходит в состояние `rejected` с системной ошибкой `AbortError`.
|
|
55
|
+
* - Все внутренние ссылки на контекст промиса (`resolve`, `reject`, `source`) принудительно зануляются (`null`),
|
|
56
|
+
* освобождая память от скрытых замыканий и предотвращая появление эффекта «зависших» запросов.
|
|
9
57
|
*/
|
|
10
58
|
export declare const withThrottle: <S, T>(fetcher: (source: S, signal: AbortSignal) => Promise<T>, options?: ThrottleOptions) => (source: S, signal: AbortSignal) => Promise<T>;
|
|
11
59
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"withThrottle.d.ts","sourceRoot":"","sources":["../../../src/decorators/withThrottle/withThrottle.ts"],"names":[],"mappings":"AAAA,UAAU,eAAe;IACvB,+DAA+D;IAC/D,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED
|
|
1
|
+
{"version":3,"file":"withThrottle.d.ts","sourceRoot":"","sources":["../../../src/decorators/withThrottle/withThrottle.ts"],"names":[],"mappings":"AAAA,UAAU,eAAe;IACvB,+DAA+D;IAC/D,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AACH,eAAO,MAAM,YAAY,GAAI,CAAC,EAAE,CAAC,EAC/B,SAAS,CAAC,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC,EACvD,UAAS,eAAoB,MAarB,QAAQ,CAAC,EAAE,QAAQ,WAAW,KAAG,OAAO,CAAC,CAAC,CAgFnD,CAAA"}
|
|
@@ -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 * Декоратор для создания
|
|
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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"withThrottleAndCache.cjs","names":[],"sources":["../../../src/decorators/withThrottleAndCache/withThrottleAndCache.ts"],"sourcesContent":["interface ThrottleAndCacheOptions {\n /** Интервал троттлинга в миллисекундах. По умолчанию 300 мс */\n limit?: number;\n /** Время жизни кэша в миллисекундах. По умолчанию 5 минут (300000 мс) */\n ttl?: number;\n}\n\ninterface CacheEntry<T> {\n data: T;\n timestamp: number;\n}\n\n/**\n * Комбинированный
|
|
1
|
+
{"version":3,"file":"withThrottleAndCache.cjs","names":[],"sources":["../../../src/decorators/withThrottleAndCache/withThrottleAndCache.ts"],"sourcesContent":["interface ThrottleAndCacheOptions {\n /** Интервал троттлинга в миллисекундах. По умолчанию 300 мс */\n limit?: number;\n /** Время жизни кэша в миллисекундах. По умолчанию 5 минут (300000 мс) */\n ttl?: number;\n}\n\ninterface CacheEntry<T> {\n data: T;\n timestamp: number;\n}\n\n/**\n * Комбинированный декоратор высокой производительности: применяет ограничение частоты вызовов (Throttling)\n * с сохранением последнего вызова (Trailing edge) и проверяет валидность кэша в оперативной памяти\n * перед отправкой реального сетевого запроса.\n *\n * Нативно поддерживает отмену операций через стандартный `AbortSignal` на всех этапах жизненного цикла.\n *\n * ### 🧠 Алгоритм работы (Два контура защиты):\n * 1. **Контур Троттлинга:** Если функция вызывается чаще чем раз в `limit` миллисекунд,\n * вызовы группируются. Первый вызов выполняется мгновенно (Leading edge), а все последующие\n * внутри окна блокировки перезаписывают друг друга. Выполнится только самый последний вызов (Trailing edge)\n * по истечении таймера. Все промежуточные отброшенные вызовы отклоняются с ошибкой `AbortError`.\n * 2. **Контур Кэширования:** Когда таймер троттлинга истекает и наступает время реального выполнения,\n * функция генерирует строковый ключ на основе аргумента `source` (через `JSON.stringify` для объектов).\n * Если в кэше есть свежий результат, чей возраст меньше `ttl`, он возвращается мгновенно **без**\n * повторного вызова исходного `fetcher`-запроса.\n *\n * @template S Тип входных данных (аргументов) для запроса. Используется для генерации ключа кэша.\n * @template T Тип данных, возвращаемых асинхронным `fetcher`-ом (разрешенное значение промиса).\n *\n * @param {(source: S, signal: AbortSignal) => Promise<T>} fetcher Асинхронная функция-загрузчик, выполняющая реальный сетевой или дисковый запрос.\n * @param {ThrottleAndCacheOptions} [options={}] Параметры конфигурации декоратора.\n * @param {number} [options.limit=300] Окно троттлинга в миллисекундах (минимальный интервал между прямыми вызовами).\n * @param {number} [options.ttl=300000] Время жизни кэша (Time-To-Live) в миллисекундах (по умолчанию 5 минут).\n *\n * @returns {(source: S, signal: AbortSignal) => Promise<T>} Возвращает обернутую функцию, которая возвращает `Promise<T>`.\n *\n * @example\n * ```typescript\n * import { withThrottleAndCache } from '@pravosleva/reactive-engine';\n *\n * interface SearchQuery { query: string; page: number; }\n * interface SearchResult { items: string[]; total: number; }\n *\n * const fetchApi = async (search: SearchQuery, signal: AbortSignal): Promise<SearchResult> => {\n * const response = await fetch(`/api/search?q=${search.query}&p=${search.page}`, { signal });\n * return response.json();\n * };\n *\n * // Создаем оптимизированную функцию поиска\n * const optimizedSearch = withThrottleAndCache(fetchApi, { limit: 500, ttl: 60 * 1000 });\n *\n * // Пример вызова внутри реактивного эффекта\n * const controller = new AbortController();\n *\n * optimizedSearch({ query: 'react', page: 1 }, controller.signal)\n * .then(data => updateUi(data))\n * .catch(err => {\n * if (err.name === 'AbortError') console.log('Запрос отменен троттлером или пользователем');\n * });\n * ```\n *\n * @abstract\n * ### 🚨 Интеграция с AbortSignal и управление памятью\n * Декоратор имеет встроенный обработчик события `abort`. Если пользователь отменяет операцию\n * (например, уходит со страницы) во время ожидания в окне троттлинга:\n * - Активный таймер `setTimeout` сбрасывается и зануляется.\n * - Ожидающий промис немедленно переходит в состояние `rejected` с ошибкой `AbortError`.\n * - Все внутренние ссылки на `source` и методы разрешения промиса очищаются (`null`), предотвращая утечки памяти в замыканиях.\n */\nexport const withThrottleAndCache = <S, T>(\n fetcher: (source: S, signal: AbortSignal) => Promise<T>,\n options: ThrottleAndCacheOptions = {}\n) => {\n const limit = options.limit ?? 300\n const ttl = options.ttl ?? 5 * 60 * 1000\n\n // Инфраструктура троттлинга\n let lastExecutionTime = 0\n let throttleTimeoutId: ReturnType<typeof setTimeout> | null = null\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 // Инфраструктура кэширования\n const cache = new Map<string, CacheEntry<T>>()\n\n // Внутренний хелпер для генерации ключа кэша\n const getCacheKey = (source: S): string => {\n return typeof source === 'object' && source !== null\n ? JSON.stringify(source)\n : String(source)\n }\n\n return (source: S, signal: AbortSignal): Promise<T> => {\n const now = Date.now()\n const remainingTime = limit - (now - lastExecutionTime)\n const cacheKey = getCacheKey(source)\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 // --- ФУНКЦИЯ ВЫПОЛНЕНИЯ ЗАПРОСА С УЧЕТОМ КЭША ---\n const executeWithCache = async (src: S, sig: AbortSignal): Promise<T> => {\n const currentNow = Date.now()\n const currentKey = getCacheKey(src)\n const cached = cache.get(currentKey)\n\n // Проверяем оперативную память на валидность кэша\n if (cached && currentNow - cached.timestamp < ttl) {\n return cached.data\n }\n\n // Если кэша нет — делаем реальный запрос\n const freshData = await fetcher(src, sig)\n\n // Сохраняем в кэш\n cache.set(currentKey, {\n data: freshData,\n timestamp: Date.now(),\n })\n\n return freshData\n }\n\n // --- ВЕТКА 1: Leading edge (Окно блокировки закрыто, выполняем сразу) ---\n if (remainingTime <= 0) {\n if (throttleTimeoutId) {\n clearTimeout(throttleTimeoutId)\n throttleTimeoutId = null\n }\n if (lastSavedReject) {\n lastSavedReject(new DOMException('Aborted due to newer direct execution', 'AbortError'))\n lastSavedResolve = null\n lastSavedReject = null\n }\n\n lastExecutionTime = now\n return executeWithCache(source, signal)\n }\n\n // --- ВЕТКА 2: Trailing edge (Внутри окна блокировки, перезаписываем хвост) ---\n if (lastSavedReject) {\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 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 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 executeWithCache(savedSource, savedSignal)\n savedResolve(data)\n } catch (error) {\n savedReject(error)\n }\n }, remainingTime)\n }\n })\n }\n}\n"],"mappings":"AAwEA,IAAa,GACX,EACA,EAAmC,CAAC,IACjC,CACH,IAAM,EAAQ,EAAQ,OAAS,IACzB,EAAM,EAAQ,KAAO,IAAS,IAGhC,EAAoB,EACpB,EAA0D,KAC1D,EAA4B,KAC5B,EAAiE,KACjE,EAAkD,KAClD,EAAsC,KAGpC,EAAQ,IAAI,IAGZ,EAAe,GACZ,OAAO,GAAW,UAAY,EACjC,KAAK,UAAU,CAAM,EACrB,OAAO,CAAM,EAGnB,OAAQ,EAAW,IAAoC,CACrD,IAAM,EAAM,KAAK,IAAI,EACf,EAAgB,GAAS,EAAM,GACpB,EAAY,CAAM,EAEnC,IAAM,MAAgB,CACpB,AAEE,KADA,aAAa,CAAiB,EACV,MAEtB,AAGE,KAFA,EAAgB,IAAI,aAAa,6CAA8C,YAAY,CAAC,EAC5F,EAAmB,KACD,KAEtB,EAGM,EAAmB,MAAO,EAAQ,IAAiC,CACvE,IAAM,EAAa,KAAK,IAAI,EACtB,EAAa,EAAY,CAAG,EAC5B,EAAS,EAAM,IAAI,CAAU,EAGnC,GAAI,GAAU,EAAa,EAAO,UAAY,EAC5C,OAAO,EAAO,KAIhB,IAAM,EAAY,MAAM,EAAQ,EAAK,CAAG,EAQxC,OALA,EAAM,IAAI,EAAY,CACpB,KAAM,EACN,UAAW,KAAK,IAAI,CACtB,CAAC,EAEM,CACT,EAuBA,OApBI,GAAiB,GACnB,AAEE,KADA,aAAa,CAAiB,EACV,MAEtB,AAGE,KAFA,EAAgB,IAAI,aAAa,wCAAyC,YAAY,CAAC,EACvF,EAAmB,KACD,MAGpB,EAAoB,EACb,EAAiB,EAAQ,CAAM,IAIpC,GACF,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,EAExC,AACE,IAAoB,WAAW,SAAY,CACzC,EAAoB,KAEpB,IAAM,EAAc,EACd,EAAe,EACf,EAAc,EACd,EAAc,EAEpB,EAAkB,KAClB,EAAmB,KACnB,EAAkB,KAClB,EAAkB,KAClB,EAAY,oBAAoB,QAAS,CAAO,EAEhD,GAAI,CACF,EAAoB,KAAK,IAAI,EAE7B,EAAa,MADM,EAAiB,EAAa,CAAW,CAC3C,CACnB,OAAS,EAAO,CACd,EAAY,CAAK,CACnB,CACF,EAAG,CAAa,CAEpB,CAAC,EACH,CACF"}
|
|
@@ -5,8 +5,64 @@ interface ThrottleAndCacheOptions {
|
|
|
5
5
|
ttl?: number;
|
|
6
6
|
}
|
|
7
7
|
/**
|
|
8
|
-
* Комбинированный
|
|
9
|
-
*
|
|
8
|
+
* Комбинированный декоратор высокой производительности: применяет ограничение частоты вызовов (Throttling)
|
|
9
|
+
* с сохранением последнего вызова (Trailing edge) и проверяет валидность кэша в оперативной памяти
|
|
10
|
+
* перед отправкой реального сетевого запроса.
|
|
11
|
+
*
|
|
12
|
+
* Нативно поддерживает отмену операций через стандартный `AbortSignal` на всех этапах жизненного цикла.
|
|
13
|
+
*
|
|
14
|
+
* ### 🧠 Алгоритм работы (Два контура защиты):
|
|
15
|
+
* 1. **Контур Троттлинга:** Если функция вызывается чаще чем раз в `limit` миллисекунд,
|
|
16
|
+
* вызовы группируются. Первый вызов выполняется мгновенно (Leading edge), а все последующие
|
|
17
|
+
* внутри окна блокировки перезаписывают друг друга. Выполнится только самый последний вызов (Trailing edge)
|
|
18
|
+
* по истечении таймера. Все промежуточные отброшенные вызовы отклоняются с ошибкой `AbortError`.
|
|
19
|
+
* 2. **Контур Кэширования:** Когда таймер троттлинга истекает и наступает время реального выполнения,
|
|
20
|
+
* функция генерирует строковый ключ на основе аргумента `source` (через `JSON.stringify` для объектов).
|
|
21
|
+
* Если в кэше есть свежий результат, чей возраст меньше `ttl`, он возвращается мгновенно **без**
|
|
22
|
+
* повторного вызова исходного `fetcher`-запроса.
|
|
23
|
+
*
|
|
24
|
+
* @template S Тип входных данных (аргументов) для запроса. Используется для генерации ключа кэша.
|
|
25
|
+
* @template T Тип данных, возвращаемых асинхронным `fetcher`-ом (разрешенное значение промиса).
|
|
26
|
+
*
|
|
27
|
+
* @param {(source: S, signal: AbortSignal) => Promise<T>} fetcher Асинхронная функция-загрузчик, выполняющая реальный сетевой или дисковый запрос.
|
|
28
|
+
* @param {ThrottleAndCacheOptions} [options={}] Параметры конфигурации декоратора.
|
|
29
|
+
* @param {number} [options.limit=300] Окно троттлинга в миллисекундах (минимальный интервал между прямыми вызовами).
|
|
30
|
+
* @param {number} [options.ttl=300000] Время жизни кэша (Time-To-Live) в миллисекундах (по умолчанию 5 минут).
|
|
31
|
+
*
|
|
32
|
+
* @returns {(source: S, signal: AbortSignal) => Promise<T>} Возвращает обернутую функцию, которая возвращает `Promise<T>`.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```typescript
|
|
36
|
+
* import { withThrottleAndCache } from '@pravosleva/reactive-engine';
|
|
37
|
+
*
|
|
38
|
+
* interface SearchQuery { query: string; page: number; }
|
|
39
|
+
* interface SearchResult { items: string[]; total: number; }
|
|
40
|
+
*
|
|
41
|
+
* const fetchApi = async (search: SearchQuery, signal: AbortSignal): Promise<SearchResult> => {
|
|
42
|
+
* const response = await fetch(`/api/search?q=${search.query}&p=${search.page}`, { signal });
|
|
43
|
+
* return response.json();
|
|
44
|
+
* };
|
|
45
|
+
*
|
|
46
|
+
* // Создаем оптимизированную функцию поиска
|
|
47
|
+
* const optimizedSearch = withThrottleAndCache(fetchApi, { limit: 500, ttl: 60 * 1000 });
|
|
48
|
+
*
|
|
49
|
+
* // Пример вызова внутри реактивного эффекта
|
|
50
|
+
* const controller = new AbortController();
|
|
51
|
+
*
|
|
52
|
+
* optimizedSearch({ query: 'react', page: 1 }, controller.signal)
|
|
53
|
+
* .then(data => updateUi(data))
|
|
54
|
+
* .catch(err => {
|
|
55
|
+
* if (err.name === 'AbortError') console.log('Запрос отменен троттлером или пользователем');
|
|
56
|
+
* });
|
|
57
|
+
* ```
|
|
58
|
+
*
|
|
59
|
+
* @abstract
|
|
60
|
+
* ### 🚨 Интеграция с AbortSignal и управление памятью
|
|
61
|
+
* Декоратор имеет встроенный обработчик события `abort`. Если пользователь отменяет операцию
|
|
62
|
+
* (например, уходит со страницы) во время ожидания в окне троттлинга:
|
|
63
|
+
* - Активный таймер `setTimeout` сбрасывается и зануляется.
|
|
64
|
+
* - Ожидающий промис немедленно переходит в состояние `rejected` с ошибкой `AbortError`.
|
|
65
|
+
* - Все внутренние ссылки на `source` и методы разрешения промиса очищаются (`null`), предотвращая утечки памяти в замыканиях.
|
|
10
66
|
*/
|
|
11
67
|
export declare const withThrottleAndCache: <S, T>(fetcher: (source: S, signal: AbortSignal) => Promise<T>, options?: ThrottleAndCacheOptions) => (source: S, signal: AbortSignal) => Promise<T>;
|
|
12
68
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"withThrottleAndCache.d.ts","sourceRoot":"","sources":["../../../src/decorators/withThrottleAndCache/withThrottleAndCache.ts"],"names":[],"mappings":"AAAA,UAAU,uBAAuB;IAC/B,+DAA+D;IAC/D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yEAAyE;IACzE,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAOD
|
|
1
|
+
{"version":3,"file":"withThrottleAndCache.d.ts","sourceRoot":"","sources":["../../../src/decorators/withThrottleAndCache/withThrottleAndCache.ts"],"names":[],"mappings":"AAAA,UAAU,uBAAuB;IAC/B,+DAA+D;IAC/D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yEAAyE;IACzE,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAOD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2DG;AACH,eAAO,MAAM,oBAAoB,GAAI,CAAC,EAAE,CAAC,EACvC,SAAS,CAAC,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC,EACvD,UAAS,uBAA4B,MAuB7B,QAAQ,CAAC,EAAE,QAAQ,WAAW,KAAG,OAAO,CAAC,CAAC,CAkGnD,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"withThrottleAndCache.mjs","names":[],"sources":["../../../src/decorators/withThrottleAndCache/withThrottleAndCache.ts"],"sourcesContent":["interface ThrottleAndCacheOptions {\n /** Интервал троттлинга в миллисекундах. По умолчанию 300 мс */\n limit?: number;\n /** Время жизни кэша в миллисекундах. По умолчанию 5 минут (300000 мс) */\n ttl?: number;\n}\n\ninterface CacheEntry<T> {\n data: T;\n timestamp: number;\n}\n\n/**\n * Комбинированный
|
|
1
|
+
{"version":3,"file":"withThrottleAndCache.mjs","names":[],"sources":["../../../src/decorators/withThrottleAndCache/withThrottleAndCache.ts"],"sourcesContent":["interface ThrottleAndCacheOptions {\n /** Интервал троттлинга в миллисекундах. По умолчанию 300 мс */\n limit?: number;\n /** Время жизни кэша в миллисекундах. По умолчанию 5 минут (300000 мс) */\n ttl?: number;\n}\n\ninterface CacheEntry<T> {\n data: T;\n timestamp: number;\n}\n\n/**\n * Комбинированный декоратор высокой производительности: применяет ограничение частоты вызовов (Throttling)\n * с сохранением последнего вызова (Trailing edge) и проверяет валидность кэша в оперативной памяти\n * перед отправкой реального сетевого запроса.\n *\n * Нативно поддерживает отмену операций через стандартный `AbortSignal` на всех этапах жизненного цикла.\n *\n * ### 🧠 Алгоритм работы (Два контура защиты):\n * 1. **Контур Троттлинга:** Если функция вызывается чаще чем раз в `limit` миллисекунд,\n * вызовы группируются. Первый вызов выполняется мгновенно (Leading edge), а все последующие\n * внутри окна блокировки перезаписывают друг друга. Выполнится только самый последний вызов (Trailing edge)\n * по истечении таймера. Все промежуточные отброшенные вызовы отклоняются с ошибкой `AbortError`.\n * 2. **Контур Кэширования:** Когда таймер троттлинга истекает и наступает время реального выполнения,\n * функция генерирует строковый ключ на основе аргумента `source` (через `JSON.stringify` для объектов).\n * Если в кэше есть свежий результат, чей возраст меньше `ttl`, он возвращается мгновенно **без**\n * повторного вызова исходного `fetcher`-запроса.\n *\n * @template S Тип входных данных (аргументов) для запроса. Используется для генерации ключа кэша.\n * @template T Тип данных, возвращаемых асинхронным `fetcher`-ом (разрешенное значение промиса).\n *\n * @param {(source: S, signal: AbortSignal) => Promise<T>} fetcher Асинхронная функция-загрузчик, выполняющая реальный сетевой или дисковый запрос.\n * @param {ThrottleAndCacheOptions} [options={}] Параметры конфигурации декоратора.\n * @param {number} [options.limit=300] Окно троттлинга в миллисекундах (минимальный интервал между прямыми вызовами).\n * @param {number} [options.ttl=300000] Время жизни кэша (Time-To-Live) в миллисекундах (по умолчанию 5 минут).\n *\n * @returns {(source: S, signal: AbortSignal) => Promise<T>} Возвращает обернутую функцию, которая возвращает `Promise<T>`.\n *\n * @example\n * ```typescript\n * import { withThrottleAndCache } from '@pravosleva/reactive-engine';\n *\n * interface SearchQuery { query: string; page: number; }\n * interface SearchResult { items: string[]; total: number; }\n *\n * const fetchApi = async (search: SearchQuery, signal: AbortSignal): Promise<SearchResult> => {\n * const response = await fetch(`/api/search?q=${search.query}&p=${search.page}`, { signal });\n * return response.json();\n * };\n *\n * // Создаем оптимизированную функцию поиска\n * const optimizedSearch = withThrottleAndCache(fetchApi, { limit: 500, ttl: 60 * 1000 });\n *\n * // Пример вызова внутри реактивного эффекта\n * const controller = new AbortController();\n *\n * optimizedSearch({ query: 'react', page: 1 }, controller.signal)\n * .then(data => updateUi(data))\n * .catch(err => {\n * if (err.name === 'AbortError') console.log('Запрос отменен троттлером или пользователем');\n * });\n * ```\n *\n * @abstract\n * ### 🚨 Интеграция с AbortSignal и управление памятью\n * Декоратор имеет встроенный обработчик события `abort`. Если пользователь отменяет операцию\n * (например, уходит со страницы) во время ожидания в окне троттлинга:\n * - Активный таймер `setTimeout` сбрасывается и зануляется.\n * - Ожидающий промис немедленно переходит в состояние `rejected` с ошибкой `AbortError`.\n * - Все внутренние ссылки на `source` и методы разрешения промиса очищаются (`null`), предотвращая утечки памяти в замыканиях.\n */\nexport const withThrottleAndCache = <S, T>(\n fetcher: (source: S, signal: AbortSignal) => Promise<T>,\n options: ThrottleAndCacheOptions = {}\n) => {\n const limit = options.limit ?? 300\n const ttl = options.ttl ?? 5 * 60 * 1000\n\n // Инфраструктура троттлинга\n let lastExecutionTime = 0\n let throttleTimeoutId: ReturnType<typeof setTimeout> | null = null\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 // Инфраструктура кэширования\n const cache = new Map<string, CacheEntry<T>>()\n\n // Внутренний хелпер для генерации ключа кэша\n const getCacheKey = (source: S): string => {\n return typeof source === 'object' && source !== null\n ? JSON.stringify(source)\n : String(source)\n }\n\n return (source: S, signal: AbortSignal): Promise<T> => {\n const now = Date.now()\n const remainingTime = limit - (now - lastExecutionTime)\n const cacheKey = getCacheKey(source)\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 // --- ФУНКЦИЯ ВЫПОЛНЕНИЯ ЗАПРОСА С УЧЕТОМ КЭША ---\n const executeWithCache = async (src: S, sig: AbortSignal): Promise<T> => {\n const currentNow = Date.now()\n const currentKey = getCacheKey(src)\n const cached = cache.get(currentKey)\n\n // Проверяем оперативную память на валидность кэша\n if (cached && currentNow - cached.timestamp < ttl) {\n return cached.data\n }\n\n // Если кэша нет — делаем реальный запрос\n const freshData = await fetcher(src, sig)\n\n // Сохраняем в кэш\n cache.set(currentKey, {\n data: freshData,\n timestamp: Date.now(),\n })\n\n return freshData\n }\n\n // --- ВЕТКА 1: Leading edge (Окно блокировки закрыто, выполняем сразу) ---\n if (remainingTime <= 0) {\n if (throttleTimeoutId) {\n clearTimeout(throttleTimeoutId)\n throttleTimeoutId = null\n }\n if (lastSavedReject) {\n lastSavedReject(new DOMException('Aborted due to newer direct execution', 'AbortError'))\n lastSavedResolve = null\n lastSavedReject = null\n }\n\n lastExecutionTime = now\n return executeWithCache(source, signal)\n }\n\n // --- ВЕТКА 2: Trailing edge (Внутри окна блокировки, перезаписываем хвост) ---\n if (lastSavedReject) {\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 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 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 executeWithCache(savedSource, savedSignal)\n savedResolve(data)\n } catch (error) {\n savedReject(error)\n }\n }, remainingTime)\n }\n })\n }\n}\n"],"mappings":";AAwEA,IAAa,KACX,GACA,IAAmC,CAAC,MACjC;CACH,IAAM,IAAQ,EAAQ,SAAS,KACzB,IAAM,EAAQ,OAAO,MAAS,KAGhC,IAAoB,GACpB,IAA0D,MAC1D,IAA4B,MAC5B,IAAiE,MACjE,IAAkD,MAClD,IAAsC,MAGpC,oBAAQ,IAAI,IAA2B,GAGvC,KAAe,MACZ,OAAO,KAAW,YAAY,IACjC,KAAK,UAAU,CAAM,IACrB,OAAO,CAAM;CAGnB,QAAQ,GAAW,MAAoC;EACrD,IAAM,IAAM,KAAK,IAAI,GACf,IAAgB,KAAS,IAAM;EACpB,EAAY,CAAM;EAEnC,IAAM,UAAgB;GAKpB,AAJA,AAEE,OADA,aAAa,CAAiB,GACV,OAEtB,AAGE,OAFA,EAAgB,IAAI,aAAa,8CAA8C,YAAY,CAAC,GAC5F,IAAmB,MACD;EAEtB,GAGM,IAAmB,OAAO,GAAQ,MAAiC;GACvE,IAAM,IAAa,KAAK,IAAI,GACtB,IAAa,EAAY,CAAG,GAC5B,IAAS,EAAM,IAAI,CAAU;GAGnC,IAAI,KAAU,IAAa,EAAO,YAAY,GAC5C,OAAO,EAAO;GAIhB,IAAM,IAAY,MAAM,EAAQ,GAAK,CAAG;GAQxC,OALA,EAAM,IAAI,GAAY;IACpB,MAAM;IACN,WAAW,KAAK,IAAI;GACtB,CAAC,GAEM;EACT;EAuBA,OApBI,KAAiB,KACnB,AAEE,OADA,aAAa,CAAiB,GACV,OAEtB,AAGE,OAFA,EAAgB,IAAI,aAAa,yCAAyC,YAAY,CAAC,GACvF,IAAmB,MACD,OAGpB,IAAoB,GACb,EAAiB,GAAQ,CAAM,MAIpC,KACF,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;GAIjB,AAFA,EAAO,iBAAiB,SAAS,CAAO,GAExC,AACE,MAAoB,WAAW,YAAY;IACzC,IAAoB;IAEpB,IAAM,IAAc,GACd,IAAe,GACf,IAAc,GACd,IAAc;IAMpB,AAJA,IAAkB,MAClB,IAAmB,MACnB,IAAkB,MAClB,IAAkB,MAClB,EAAY,oBAAoB,SAAS,CAAO;IAEhD,IAAI;KAGF,AAFA,IAAoB,KAAK,IAAI,GAE7B,EAAa,MADM,EAAiB,GAAa,CAAW,CAC3C;IACnB,SAAS,GAAO;KACd,EAAY,CAAK;IACnB;GACF,GAAG,CAAa;EAEpB,CAAC;CACH;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"withThrottleComputed.cjs","names":[],"sources":["../../../src/decorators/withThrottleComputed/withThrottleComputed.ts"],"sourcesContent":["import { ReactiveEngine } from '../../core/core'\n\ninterface ThrottleComputedOptions {\n limit: number;\n}\n\nexport const withThrottleComputed = <T>(\n engine: ReactiveEngine,\n getter: () => T,\n options: ThrottleComputedOptions,\n signalName?: string\n) => {\n const name = signalName || 'throttled_computed:unnamed'\n const limit = options.limit\n\n const initialValue = getter()\n const throttledSignal = engine.signal<T>(initialValue, `signal:internal:${name}`)\n\n // ИСПОЛЬЗУЕМ СТАНДАРТНЫЙ ТАЙМЕР ДЛЯ СИНХРOНИЗАЦИИ С ТЕСТАМИ\n let lastRunTime = 0\n let timeoutId: any = null\n let pendingValue: T | null = null\n let hasPending = false\n\n let isFirstEffectRun = true\n\n const updateSignal = (value: T) => {\n throttledSignal.value = value\n lastRunTime = Date.now() // Перешли на Date.now() для 100% поддержки FakeTimers\n hasPending = false\n timeoutId = null\n }\n\n engine.effect(() => {\n const freshValue = getter() // Собираем зависимости нативно\n\n if (isFirstEffectRun) {\n isFirstEffectRun = false\n return\n }\n\n const now = Date.now()\n\n // Если lastRunTime === 0 — это самое первое реальное изменение, пушим его сразу\n const timePassed = lastRunTime === 0 ? limit : (now - lastRunTime)\n\n if (timePassed >= limit) {\n if (timeoutId) {\n clearTimeout(timeoutId)\n timeoutId = null\n }\n updateSignal(freshValue)\n } else {\n pendingValue = freshValue\n hasPending = true\n\n if (!timeoutId) {\n timeoutId = setTimeout(() => {\n if (hasPending) {\n updateSignal(pendingValue as T)\n }\n }, limit - timePassed)\n }\n }\n }, `effect:throttle-scheduler:${name}`)\n\n const destroy = () => {\n if (timeoutId) {\n clearTimeout(timeoutId)\n }\n }\n\n return {\n get value(): T {\n return throttledSignal.value\n },\n subscribe: (cb: (val: T) => void) => throttledSignal.subscribe(cb),\n destroy\n }\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"withThrottleComputed.cjs","names":[],"sources":["../../../src/decorators/withThrottleComputed/withThrottleComputed.ts"],"sourcesContent":["import { ReactiveEngine } from '../../core/core'\n\ninterface ThrottleComputedOptions {\n limit: number;\n}\n\n/**\n * Обертка-декоратор для создания дросселируемых (throttled) вычисляемых значений.\n * Позволяет сгруппировать и ограничить частоту обновления тяжелых вычислений,\n * защищая граф зависимостей и UI от лавинообразных изменений (\"дребезга\" данных).\n *\n * ### 🧠 Как это работает под капотом:\n * 1. Функция синхронно считывает `getter()`, чтобы получить стартовое значение.\n * 2. Внутри создается скрытый сигнал `engine.signal<T>` для хранения задросселированного стейта.\n * 3. Регистрируется `engine.effect`, который автоматически собирает все реактивные зависимости\n * внутри вашего геттера. При изменении любой зависимости эффект запускает таймер на базе `setTimeout`.\n * 4. Значение выдается мгновенно на первом пассе, а последующие изменения зажимаются во временной интервал `options.limit`.\n *\n * @template T Тип вычисляемого значения.\n *\n * @param {ReactiveEngine} engine Экземпляр реактивного ядра, в контексте которого создаются примитивы.\n * @param {() => T} getter Функция-вычислитель, содержащая реактивные сигналы. Зависимости собираются автоматически.\n * @param {Object} options Конфигурация планировщика дросселирования.\n * @param {number} options.limit Временной интервал задержки в миллисекундах (минимальное время между обновлениями).\n * @param {string} [signalName] Необязательное имя для отладки. Будет впечено в системные маркеры логгера.\n *\n * @returns {Object} Возвращает объект интерфейса вычисляемого значения:\n * @returns {T} value Геттер для чтения текущего задросселированного значения (вызывает Pull-обновление).\n * @returns {(cb: (val: T) => void) => () => void} subscribe Метод подписки на изменения значения (для связи с UI слоем).\n * @returns {() => void} destroy Функция принудительной очистки активных таймеров. Обязательна к вызову при уничтожении контекста.\n *\n * @example\n * ```typescript\n * import { withThrottleComputed } from '@pravosleva/reactive-engine';\n *\n * const searchInput = engine.signal('reac', 'search');\n *\n * // Создаем задросселированный компут: будет обновляться не чаще чем раз в 300мс\n * const throttledSearch = withThrottleComputed(\n * engine,\n * () => executeHeavySearchFilter(searchInput.value),\n * { limit: 300 },\n * 'heavy-search'\n * );\n *\n * // Подписываемся на финальный результат\n * throttledSearch.subscribe((finalResult) => {\n * renderSearchResults(finalResult);\n * });\n *\n * // Не забываем вызвать destroy при анмаунте компонента/сервиса\n * onUnmounted(() => throttledSearch.destroy());\n * ```\n *\n * @abstract\n * ### 🧪 Тестирование с Fake Timers\n * Код функции переведен на нативный `Date.now()`, что обеспечивает 100% совместимость с ложными\n * таймерами в тестовых средах. При написании тестов используйте `vi.advanceTimersByTime(limit)`\n * для контролируемой перемотки времени планировщика.\n */\nexport const withThrottleComputed = <T>(\n engine: ReactiveEngine,\n getter: () => T,\n options: ThrottleComputedOptions,\n signalName?: string\n) => {\n const name = signalName || 'throttled_computed:unnamed'\n const limit = options.limit\n\n const initialValue = getter()\n const throttledSignal = engine.signal<T>(initialValue, `signal:internal:${name}`)\n\n // ИСПОЛЬЗУЕМ СТАНДАРТНЫЙ ТАЙМЕР ДЛЯ СИНХРOНИЗАЦИИ С ТЕСТАМИ\n let lastRunTime = 0\n let timeoutId: any = null\n let pendingValue: T | null = null\n let hasPending = false\n\n let isFirstEffectRun = true\n\n const updateSignal = (value: T) => {\n throttledSignal.value = value\n lastRunTime = Date.now() // Перешли на Date.now() для 100% поддержки FakeTimers\n hasPending = false\n timeoutId = null\n }\n\n engine.effect(() => {\n const freshValue = getter() // Собираем зависимости нативно\n\n if (isFirstEffectRun) {\n isFirstEffectRun = false\n return\n }\n\n const now = Date.now()\n\n // Если lastRunTime === 0 — это самое первое реальное изменение, пушим его сразу\n const timePassed = lastRunTime === 0 ? limit : (now - lastRunTime)\n\n if (timePassed >= limit) {\n if (timeoutId) {\n clearTimeout(timeoutId)\n timeoutId = null\n }\n updateSignal(freshValue)\n } else {\n pendingValue = freshValue\n hasPending = true\n\n if (!timeoutId) {\n timeoutId = setTimeout(() => {\n if (hasPending) {\n updateSignal(pendingValue as T)\n }\n }, limit - timePassed)\n }\n }\n }, `effect:throttle-scheduler:${name}`)\n\n const destroy = () => {\n if (timeoutId) {\n clearTimeout(timeoutId)\n }\n }\n\n return {\n get value(): T {\n return throttledSignal.value\n },\n subscribe: (cb: (val: T) => void) => throttledSignal.subscribe(cb),\n destroy\n }\n}\n"],"mappings":"AA4DA,IAAa,GACX,EACA,EACA,EACA,IACG,CACH,IAAM,EAAO,GAAc,6BACrB,EAAQ,EAAQ,MAEhB,EAAe,EAAO,EACtB,EAAkB,EAAO,OAAU,EAAc,mBAAmB,GAAM,EAG5E,EAAc,EACd,EAAiB,KACjB,EAAyB,KACzB,EAAa,GAEb,EAAmB,GAEjB,EAAgB,GAAa,CACjC,EAAgB,MAAQ,EACxB,EAAc,KAAK,IAAI,EACvB,EAAa,GACb,EAAY,IACd,EAyCA,OAvCA,EAAO,WAAa,CAClB,IAAM,EAAa,EAAO,EAE1B,GAAI,EAAkB,CACpB,EAAmB,GACnB,MACF,CAKA,IAAM,EAAa,IAAgB,EAAI,EAH3B,KAAK,IAG+B,EAAM,EAElD,GAAc,GAChB,AAEE,KADA,aAAa,CAAS,EACV,MAEd,EAAa,CAAU,IAEvB,EAAe,EACf,EAAa,GAEb,AACE,IAAY,eAAiB,CACvB,GACF,EAAa,CAAiB,CAElC,EAAG,EAAQ,CAAU,EAG3B,EAAG,6BAA6B,GAAM,EAQ/B,CACL,IAAI,OAAW,CACb,OAAO,EAAgB,KACzB,EACA,UAAY,GAAyB,EAAgB,UAAU,CAAE,EACjE,YAXoB,CAChB,GACF,aAAa,CAAS,CAE1B,CAQA,CACF"}
|
|
@@ -2,6 +2,60 @@ import { ReactiveEngine } from '../../core/core';
|
|
|
2
2
|
interface ThrottleComputedOptions {
|
|
3
3
|
limit: number;
|
|
4
4
|
}
|
|
5
|
+
/**
|
|
6
|
+
* Обертка-декоратор для создания дросселируемых (throttled) вычисляемых значений.
|
|
7
|
+
* Позволяет сгруппировать и ограничить частоту обновления тяжелых вычислений,
|
|
8
|
+
* защищая граф зависимостей и UI от лавинообразных изменений ("дребезга" данных).
|
|
9
|
+
*
|
|
10
|
+
* ### 🧠 Как это работает под капотом:
|
|
11
|
+
* 1. Функция синхронно считывает `getter()`, чтобы получить стартовое значение.
|
|
12
|
+
* 2. Внутри создается скрытый сигнал `engine.signal<T>` для хранения задросселированного стейта.
|
|
13
|
+
* 3. Регистрируется `engine.effect`, который автоматически собирает все реактивные зависимости
|
|
14
|
+
* внутри вашего геттера. При изменении любой зависимости эффект запускает таймер на базе `setTimeout`.
|
|
15
|
+
* 4. Значение выдается мгновенно на первом пассе, а последующие изменения зажимаются во временной интервал `options.limit`.
|
|
16
|
+
*
|
|
17
|
+
* @template T Тип вычисляемого значения.
|
|
18
|
+
*
|
|
19
|
+
* @param {ReactiveEngine} engine Экземпляр реактивного ядра, в контексте которого создаются примитивы.
|
|
20
|
+
* @param {() => T} getter Функция-вычислитель, содержащая реактивные сигналы. Зависимости собираются автоматически.
|
|
21
|
+
* @param {Object} options Конфигурация планировщика дросселирования.
|
|
22
|
+
* @param {number} options.limit Временной интервал задержки в миллисекундах (минимальное время между обновлениями).
|
|
23
|
+
* @param {string} [signalName] Необязательное имя для отладки. Будет впечено в системные маркеры логгера.
|
|
24
|
+
*
|
|
25
|
+
* @returns {Object} Возвращает объект интерфейса вычисляемого значения:
|
|
26
|
+
* @returns {T} value Геттер для чтения текущего задросселированного значения (вызывает Pull-обновление).
|
|
27
|
+
* @returns {(cb: (val: T) => void) => () => void} subscribe Метод подписки на изменения значения (для связи с UI слоем).
|
|
28
|
+
* @returns {() => void} destroy Функция принудительной очистки активных таймеров. Обязательна к вызову при уничтожении контекста.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```typescript
|
|
32
|
+
* import { withThrottleComputed } from '@pravosleva/reactive-engine';
|
|
33
|
+
*
|
|
34
|
+
* const searchInput = engine.signal('reac', 'search');
|
|
35
|
+
*
|
|
36
|
+
* // Создаем задросселированный компут: будет обновляться не чаще чем раз в 300мс
|
|
37
|
+
* const throttledSearch = withThrottleComputed(
|
|
38
|
+
* engine,
|
|
39
|
+
* () => executeHeavySearchFilter(searchInput.value),
|
|
40
|
+
* { limit: 300 },
|
|
41
|
+
* 'heavy-search'
|
|
42
|
+
* );
|
|
43
|
+
*
|
|
44
|
+
* // Подписываемся на финальный результат
|
|
45
|
+
* throttledSearch.subscribe((finalResult) => {
|
|
46
|
+
* renderSearchResults(finalResult);
|
|
47
|
+
* });
|
|
48
|
+
*
|
|
49
|
+
* // Не забываем вызвать destroy при анмаунте компонента/сервиса
|
|
50
|
+
* onUnmounted(() => throttledSearch.destroy());
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* @abstract
|
|
54
|
+
* ### 🧪 Тестирование с Fake Timers
|
|
55
|
+
* Код функции переведен на нативный `Date.now()`, что обеспечивает 100% совместимость с ложными
|
|
56
|
+
* таймерами в тестовых средах. При написании тестов используйте `vi.advanceTimersByTime(limit)`
|
|
57
|
+
* для контролируемой перемотки времени планировщика.
|
|
58
|
+
*/
|
|
5
59
|
export declare const withThrottleComputed: <T>(engine: ReactiveEngine, getter: () => T, options: ThrottleComputedOptions, signalName?: string) => {
|
|
6
60
|
readonly value: T;
|
|
7
61
|
subscribe: (cb: (val: T) => void) => import('../..').CleanupFn;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"withThrottleComputed.d.ts","sourceRoot":"","sources":["../../../src/decorators/withThrottleComputed/withThrottleComputed.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAEhD,UAAU,uBAAuB;IAC/B,KAAK,EAAE,MAAM,CAAC;CACf;AAED,eAAO,MAAM,oBAAoB,GAAI,CAAC,EACpC,QAAQ,cAAc,EACtB,QAAQ,MAAM,CAAC,EACf,SAAS,uBAAuB,EAChC,aAAa,MAAM;oBA+DJ,CAAC;oBAGE,CAAC,GAAG,EAAE,CAAC,KAAK,IAAI;;CAGnC,CAAA"}
|
|
1
|
+
{"version":3,"file":"withThrottleComputed.d.ts","sourceRoot":"","sources":["../../../src/decorators/withThrottleComputed/withThrottleComputed.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAEhD,UAAU,uBAAuB;IAC/B,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AACH,eAAO,MAAM,oBAAoB,GAAI,CAAC,EACpC,QAAQ,cAAc,EACtB,QAAQ,MAAM,CAAC,EACf,SAAS,uBAAuB,EAChC,aAAa,MAAM;oBA+DJ,CAAC;oBAGE,CAAC,GAAG,EAAE,CAAC,KAAK,IAAI;;CAGnC,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"withThrottleComputed.mjs","names":[],"sources":["../../../src/decorators/withThrottleComputed/withThrottleComputed.ts"],"sourcesContent":["import { ReactiveEngine } from '../../core/core'\n\ninterface ThrottleComputedOptions {\n limit: number;\n}\n\nexport const withThrottleComputed = <T>(\n engine: ReactiveEngine,\n getter: () => T,\n options: ThrottleComputedOptions,\n signalName?: string\n) => {\n const name = signalName || 'throttled_computed:unnamed'\n const limit = options.limit\n\n const initialValue = getter()\n const throttledSignal = engine.signal<T>(initialValue, `signal:internal:${name}`)\n\n // ИСПОЛЬЗУЕМ СТАНДАРТНЫЙ ТАЙМЕР ДЛЯ СИНХРOНИЗАЦИИ С ТЕСТАМИ\n let lastRunTime = 0\n let timeoutId: any = null\n let pendingValue: T | null = null\n let hasPending = false\n\n let isFirstEffectRun = true\n\n const updateSignal = (value: T) => {\n throttledSignal.value = value\n lastRunTime = Date.now() // Перешли на Date.now() для 100% поддержки FakeTimers\n hasPending = false\n timeoutId = null\n }\n\n engine.effect(() => {\n const freshValue = getter() // Собираем зависимости нативно\n\n if (isFirstEffectRun) {\n isFirstEffectRun = false\n return\n }\n\n const now = Date.now()\n\n // Если lastRunTime === 0 — это самое первое реальное изменение, пушим его сразу\n const timePassed = lastRunTime === 0 ? limit : (now - lastRunTime)\n\n if (timePassed >= limit) {\n if (timeoutId) {\n clearTimeout(timeoutId)\n timeoutId = null\n }\n updateSignal(freshValue)\n } else {\n pendingValue = freshValue\n hasPending = true\n\n if (!timeoutId) {\n timeoutId = setTimeout(() => {\n if (hasPending) {\n updateSignal(pendingValue as T)\n }\n }, limit - timePassed)\n }\n }\n }, `effect:throttle-scheduler:${name}`)\n\n const destroy = () => {\n if (timeoutId) {\n clearTimeout(timeoutId)\n }\n }\n\n return {\n get value(): T {\n return throttledSignal.value\n },\n subscribe: (cb: (val: T) => void) => throttledSignal.subscribe(cb),\n destroy\n }\n}\n"],"mappings":";
|
|
1
|
+
{"version":3,"file":"withThrottleComputed.mjs","names":[],"sources":["../../../src/decorators/withThrottleComputed/withThrottleComputed.ts"],"sourcesContent":["import { ReactiveEngine } from '../../core/core'\n\ninterface ThrottleComputedOptions {\n limit: number;\n}\n\n/**\n * Обертка-декоратор для создания дросселируемых (throttled) вычисляемых значений.\n * Позволяет сгруппировать и ограничить частоту обновления тяжелых вычислений,\n * защищая граф зависимостей и UI от лавинообразных изменений (\"дребезга\" данных).\n *\n * ### 🧠 Как это работает под капотом:\n * 1. Функция синхронно считывает `getter()`, чтобы получить стартовое значение.\n * 2. Внутри создается скрытый сигнал `engine.signal<T>` для хранения задросселированного стейта.\n * 3. Регистрируется `engine.effect`, который автоматически собирает все реактивные зависимости\n * внутри вашего геттера. При изменении любой зависимости эффект запускает таймер на базе `setTimeout`.\n * 4. Значение выдается мгновенно на первом пассе, а последующие изменения зажимаются во временной интервал `options.limit`.\n *\n * @template T Тип вычисляемого значения.\n *\n * @param {ReactiveEngine} engine Экземпляр реактивного ядра, в контексте которого создаются примитивы.\n * @param {() => T} getter Функция-вычислитель, содержащая реактивные сигналы. Зависимости собираются автоматически.\n * @param {Object} options Конфигурация планировщика дросселирования.\n * @param {number} options.limit Временной интервал задержки в миллисекундах (минимальное время между обновлениями).\n * @param {string} [signalName] Необязательное имя для отладки. Будет впечено в системные маркеры логгера.\n *\n * @returns {Object} Возвращает объект интерфейса вычисляемого значения:\n * @returns {T} value Геттер для чтения текущего задросселированного значения (вызывает Pull-обновление).\n * @returns {(cb: (val: T) => void) => () => void} subscribe Метод подписки на изменения значения (для связи с UI слоем).\n * @returns {() => void} destroy Функция принудительной очистки активных таймеров. Обязательна к вызову при уничтожении контекста.\n *\n * @example\n * ```typescript\n * import { withThrottleComputed } from '@pravosleva/reactive-engine';\n *\n * const searchInput = engine.signal('reac', 'search');\n *\n * // Создаем задросселированный компут: будет обновляться не чаще чем раз в 300мс\n * const throttledSearch = withThrottleComputed(\n * engine,\n * () => executeHeavySearchFilter(searchInput.value),\n * { limit: 300 },\n * 'heavy-search'\n * );\n *\n * // Подписываемся на финальный результат\n * throttledSearch.subscribe((finalResult) => {\n * renderSearchResults(finalResult);\n * });\n *\n * // Не забываем вызвать destroy при анмаунте компонента/сервиса\n * onUnmounted(() => throttledSearch.destroy());\n * ```\n *\n * @abstract\n * ### 🧪 Тестирование с Fake Timers\n * Код функции переведен на нативный `Date.now()`, что обеспечивает 100% совместимость с ложными\n * таймерами в тестовых средах. При написании тестов используйте `vi.advanceTimersByTime(limit)`\n * для контролируемой перемотки времени планировщика.\n */\nexport const withThrottleComputed = <T>(\n engine: ReactiveEngine,\n getter: () => T,\n options: ThrottleComputedOptions,\n signalName?: string\n) => {\n const name = signalName || 'throttled_computed:unnamed'\n const limit = options.limit\n\n const initialValue = getter()\n const throttledSignal = engine.signal<T>(initialValue, `signal:internal:${name}`)\n\n // ИСПОЛЬЗУЕМ СТАНДАРТНЫЙ ТАЙМЕР ДЛЯ СИНХРOНИЗАЦИИ С ТЕСТАМИ\n let lastRunTime = 0\n let timeoutId: any = null\n let pendingValue: T | null = null\n let hasPending = false\n\n let isFirstEffectRun = true\n\n const updateSignal = (value: T) => {\n throttledSignal.value = value\n lastRunTime = Date.now() // Перешли на Date.now() для 100% поддержки FakeTimers\n hasPending = false\n timeoutId = null\n }\n\n engine.effect(() => {\n const freshValue = getter() // Собираем зависимости нативно\n\n if (isFirstEffectRun) {\n isFirstEffectRun = false\n return\n }\n\n const now = Date.now()\n\n // Если lastRunTime === 0 — это самое первое реальное изменение, пушим его сразу\n const timePassed = lastRunTime === 0 ? limit : (now - lastRunTime)\n\n if (timePassed >= limit) {\n if (timeoutId) {\n clearTimeout(timeoutId)\n timeoutId = null\n }\n updateSignal(freshValue)\n } else {\n pendingValue = freshValue\n hasPending = true\n\n if (!timeoutId) {\n timeoutId = setTimeout(() => {\n if (hasPending) {\n updateSignal(pendingValue as T)\n }\n }, limit - timePassed)\n }\n }\n }, `effect:throttle-scheduler:${name}`)\n\n const destroy = () => {\n if (timeoutId) {\n clearTimeout(timeoutId)\n }\n }\n\n return {\n get value(): T {\n return throttledSignal.value\n },\n subscribe: (cb: (val: T) => void) => throttledSignal.subscribe(cb),\n destroy\n }\n}\n"],"mappings":";AA4DA,IAAa,KACX,GACA,GACA,GACA,MACG;CACH,IAAM,IAAO,KAAc,8BACrB,IAAQ,EAAQ,OAEhB,IAAe,EAAO,GACtB,IAAkB,EAAO,OAAU,GAAc,mBAAmB,GAAM,GAG5E,IAAc,GACd,IAAiB,MACjB,IAAyB,MACzB,IAAa,IAEb,IAAmB,IAEjB,KAAgB,MAAa;EAIjC,AAHA,EAAgB,QAAQ,GACxB,IAAc,KAAK,IAAI,GACvB,IAAa,IACb,IAAY;CACd;CAyCA,OAvCA,EAAO,aAAa;EAClB,IAAM,IAAa,EAAO;EAE1B,IAAI,GAAkB;GACpB,IAAmB;GACnB;EACF;EAKA,IAAM,IAAa,MAAgB,IAAI,IAH3B,KAAK,IAG+B,IAAM;EAEtD,AAAI,KAAc,KAChB,AAEE,OADA,aAAa,CAAS,GACV,OAEd,EAAa,CAAU,MAEvB,IAAe,GACf,IAAa,IAEb,AACE,MAAY,iBAAiB;GAC3B,AAAI,KACF,EAAa,CAAiB;EAElC,GAAG,IAAQ,CAAU;CAG3B,GAAG,6BAA6B,GAAM,GAQ/B;EACL,IAAI,QAAW;GACb,OAAO,EAAgB;EACzB;EACA,YAAY,MAAyB,EAAgB,UAAU,CAAE;EACjE,eAXoB;GACpB,AAAI,KACF,aAAa,CAAS;EAE1B;CAQA;AACF"}
|