@pravosleva/reactive-engine 1.5.7-beta → 1.5.9-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.
Files changed (43) hide show
  1. package/README.md +2 -0
  2. package/dist/decorators/index.d.ts +1 -0
  3. package/dist/decorators/index.d.ts.map +1 -1
  4. package/dist/decorators/withCache/withCache.cjs.map +1 -1
  5. package/dist/decorators/withCache/withCache.d.ts +57 -4
  6. package/dist/decorators/withCache/withCache.d.ts.map +1 -1
  7. package/dist/decorators/withCache/withCache.mjs.map +1 -1
  8. package/dist/decorators/withDebounce/withDebounce.cjs.map +1 -1
  9. package/dist/decorators/withDebounce/withDebounce.d.ts +54 -3
  10. package/dist/decorators/withDebounce/withDebounce.d.ts.map +1 -1
  11. package/dist/decorators/withDebounce/withDebounce.mjs.map +1 -1
  12. package/dist/decorators/withLongPolling/withLongPolling.cjs.map +1 -1
  13. package/dist/decorators/withLongPolling/withLongPolling.d.ts +69 -2
  14. package/dist/decorators/withLongPolling/withLongPolling.d.ts.map +1 -1
  15. package/dist/decorators/withLongPolling/withLongPolling.mjs.map +1 -1
  16. package/dist/decorators/withStaleWhileRevalidate/index.d.ts +2 -0
  17. package/dist/decorators/withStaleWhileRevalidate/index.d.ts.map +1 -0
  18. package/dist/decorators/withStaleWhileRevalidate/withStaleWhileRevalidate.cjs +2 -0
  19. package/dist/decorators/withStaleWhileRevalidate/withStaleWhileRevalidate.cjs.map +1 -0
  20. package/dist/decorators/withStaleWhileRevalidate/withStaleWhileRevalidate.d.ts +34 -0
  21. package/dist/decorators/withStaleWhileRevalidate/withStaleWhileRevalidate.d.ts.map +1 -0
  22. package/dist/decorators/withStaleWhileRevalidate/withStaleWhileRevalidate.mjs +22 -0
  23. package/dist/decorators/withStaleWhileRevalidate/withStaleWhileRevalidate.mjs.map +1 -0
  24. package/dist/decorators/withStaleWhileRevalidate/withStaleWhileRevalidate.test.d.ts +2 -0
  25. package/dist/decorators/withStaleWhileRevalidate/withStaleWhileRevalidate.test.d.ts.map +1 -0
  26. package/dist/decorators/withThrottle/withThrottle.cjs.map +1 -1
  27. package/dist/decorators/withThrottle/withThrottle.d.ts +51 -3
  28. package/dist/decorators/withThrottle/withThrottle.d.ts.map +1 -1
  29. package/dist/decorators/withThrottle/withThrottle.mjs.map +1 -1
  30. package/dist/decorators/withThrottleAndCache/withThrottleAndCache.cjs.map +1 -1
  31. package/dist/decorators/withThrottleAndCache/withThrottleAndCache.d.ts +58 -2
  32. package/dist/decorators/withThrottleAndCache/withThrottleAndCache.d.ts.map +1 -1
  33. package/dist/decorators/withThrottleAndCache/withThrottleAndCache.mjs.map +1 -1
  34. package/dist/decorators/withThrottleComputed/withThrottleComputed.cjs.map +1 -1
  35. package/dist/decorators/withThrottleComputed/withThrottleComputed.d.ts +54 -0
  36. package/dist/decorators/withThrottleComputed/withThrottleComputed.d.ts.map +1 -1
  37. package/dist/decorators/withThrottleComputed/withThrottleComputed.mjs.map +1 -1
  38. package/dist/index.cjs +1 -1
  39. package/dist/index.mjs +2 -1
  40. package/dist/reactive-engine.umd.js +1 -1
  41. package/dist/rocket-thruster.svg +1 -0
  42. package/package.json +1 -1
  43. package/dist/vite.svg +0 -1
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # 🚀 ReactiveEngine Core Framework
2
2
  A lightweight, type-safe reactive engine built with TypeScript, featuring Dependency Injection and seamless React integration.
3
3
 
4
+ ![logo](/public/reactive-engine.svg)
5
+
4
6
  ## Instruction
5
7
  - 🇬🇧 [In English](https://pravosleva.pro/reactive-engine/en)
6
8
  - 🇷🇺 [In Russian](https://pravosleva.pro/reactive-engine)
@@ -4,4 +4,5 @@ export { withLongPolling } from './withLongPolling';
4
4
  export { withThrottle } from './withThrottle';
5
5
  export { withThrottleComputed } from './withThrottleComputed';
6
6
  export { withThrottleAndCache } from './withThrottleAndCache';
7
+ export { withStaleWhileRevalidate } from './withStaleWhileRevalidate';
7
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/decorators/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AACvC,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAC7C,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAC7C,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAA;AAC7D,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/decorators/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AACvC,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAC7C,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAC7C,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAA;AAC7D,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAA;AAC7D,OAAO,EAAE,wBAAwB,EAAE,MAAM,4BAA4B,CAAA"}
@@ -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 * Декоратор для создания кэширующего fetcher-а для engine.resource\n * @param fetcher Оригинальная асинхронная функция запроса\n * @param options Настройки кэширования\n * @source\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":"AAgBA,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"}
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
- * Декоратор для создания кэширующего fetcher-а для engine.resource
7
- * @param fetcher Оригинальная асинхронная функция запроса
8
- * @param options Настройки кэширования
9
- * @source
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;;;;;GAKG;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
+ {"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 * Декоратор для создания кэширующего fetcher-а для engine.resource\n * @param fetcher Оригинальная асинхронная функция запроса\n * @param options Настройки кэширования\n * @source\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":";AAgBA,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
+ {"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 * Декоратор для создания дебаунсящего fetcher-а для engine.resource\n * @param fetcher Оригинальная асинхронная функция запроса\n * @param options Настройки дебаунса\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":"AAUA,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"}
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
- * Декоратор для создания дебаунсящего fetcher-а для engine.resource
7
- * @param fetcher Оригинальная асинхронная функция запроса
8
- * @param options Настройки дебаунса
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;;;;GAIG;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
+ {"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 * Декоратор для создания дебаунсящего fetcher-а для engine.resource\n * @param fetcher Оригинальная асинхронная функция запроса\n * @param options Настройки дебаунса\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":";AAUA,IAAa,KACX,GACA,IAA2B,CAAC,MACzB;CACH,IAAM,IAAQ,EAAQ,SAAS,KAG3B,IAAkD,MAElD,IAAiD;CAErD,QAAQ,GAAW,OAEb,KACF,aAAa,CAAS,GAEpB,KAEF,EAAe,IAAI,aAAa,2BAA2B,YAAY,CAAC,GAGnE,IAAI,SAAY,GAAS,MAAW;EAEzC,IAAiB;EAGjB,IAAM,UAAgB;GAEpB,AADI,KAAW,aAAa,CAAS,GACrC,EAAO,IAAI,aAAa,8BAA8B,YAAY,CAAC;EACrE;EAEA,IAAI,EAAO,SACT,OAAO,EAAQ;EAKjB,AAHA,EAAO,iBAAiB,SAAS,CAAO,GAGxC,IAAY,WAAW,YAAY;GAIjC,AAFA,EAAO,oBAAoB,SAAS,CAAO,GAC3C,IAAiB,MACjB,IAAY;GAEZ,IAAI;IAGF,EAAQ,MADW,EAAQ,GAAQ,CAAM,CAC7B;GACd,SAAS,GAAO;IACd,EAAO,CAAK;GACd;EACF,GAAG,CAAK;CACV,CAAC;AAEL"}
1
+ {"version":3,"file":"withDebounce.mjs","names":[],"sources":["../../../src/decorators/withDebounce/withDebounce.ts"],"sourcesContent":["interface DebounceOptions {\n /** Задержка в миллисекундах. По умолчанию 300 мс */\n delay?: number;\n}\n\n/**\n * Декоратор для создания дебаунсящего (отложенного) загрузчика данных, специально\n * адаптированный для использования совместно с `engine.resource`.\n *\n * Откладывает выполнение асинхронной функции `fetcher` на заданную задержку `options.delay`.\n * Если в течение этого интервала происходит новый вызов (например, пользователь продолжает вводить текст),\n * таймер сбрасывается, а предыдущий ожидающий промис принудительно отклоняется. В итоге выполняется\n * только один запрос — после того, как поток вызовов затихнет.\n *\n * ### 🧠 Механика управления промисами (Контур Debounce):\n * 1. **Сброс дребезга:** При каждом вызове функции проверяется наличие активного таймера. Если он есть,\n * `clearTimeout` мгновенно останавливает его.\n * 2. **Каскадное отклонение:** Ссылка на метод `reject` каждого создаваемого промиса сохраняется в `rejectPrevious`.\n * При поступлении нового запроса предыдущий промис не остается «зависшим», а принудительно переходит\n * в состояние `rejected` с ошибкой `AbortError`. Это сигнализирует ядру, что данные устарели.\n * 3. **Чистый запуск:** Оригинальный `fetcher` вызывается только тогда, когда таймер успешно дотикал до конца,\n * не будучи прерванным.\n *\n * @template S Тип входных данных (аргументов) для функции запроса.\n * @template T Тип данных, возвращаемых асинхронным `fetcher`-ом (разрешенное значение промиса).\n *\n * @param {(source: S, signal: AbortSignal) => Promise<T>} fetcher Оригинальная асинхронная функция запроса, принимающая параметры и сигнал отмены.\n * @param {DebounceOptions} [options={}] Параметры конфигурации дебаунса.\n * @param {number} [options.delay=300] Время ожидания в миллисекундах с момента последнего вызова до фактического старта запроса.\n *\n * @returns {(source: S, signal: AbortSignal) => Promise<T>} Возвращает обернутую дебаунс-функцию, возвращающую `Promise<T>` и полностью сохраняющую исходную сигнатуру типов.\n *\n * @example\n * ```typescript\n * import { withDebounce } from '@pravosleva/reactive-engine';\n *\n * const searchProducts = async (query: string, signal: AbortSignal) => {\n * const res = await fetch(`/api/products?search=${encodeURIComponent(query)}`, { signal });\n * return res.json();\n * };\n *\n * // Запрос уйдет только через 400мс после того, как пользователь перестанет нажимать клавиши\n * const debouncedFetch = withDebounce(searchProducts, { delay: 400 });\n *\n * // Интеграция с фабрикой ресурсов вашего реактивного ядра\n * const productSearchResource = engine.resource({\n * fetcher: debouncedFetch,\n * source: () => searchInputValue.value // Следит за сигналом строки ввода\n * });\n * ```\n *\n * @abstract\n * ### 🚨 Сквозная интеграция с AbortSignal и менеджмент памяти\n * Декоратор непрерывно отслеживает состояние нативного `AbortSignal`, передаваемого движком (например, при размонтировании UI-виджета):\n * - Если сигнал переходит в состояние `aborted` во время отсчета таймаута дебаунса, таймер сбрасывается,\n * а промис мгновенно отклоняется.\n * - При успешном выполнении таймера слушатель `abort` своевременно удаляется через `removeEventListener`,\n * исключая накопление холостых подписок.\n * - Все внутренние ссылки на методы разрешения промисов (`rejectPrevious`, `timeoutId`) принудительно зануляются (`null`),\n * очищая замыкания от застрявших в памяти объектов и защищая Node.js/браузер от утечек.\n */\nexport const withDebounce = <S, T>(\n fetcher: (source: S, signal: AbortSignal) => Promise<T>,\n options: DebounceOptions = {}\n) => {\n const delay = options.delay ?? 300\n\n // Храним ссылку на активный таймер текущего ожидания\n let timeoutId: ReturnType<typeof setTimeout> | null = null\n // Храним функцию отмены для предыдущего неоконченного промиса ожидания\n let rejectPrevious: ((reason: any) => void) | null = null\n\n return (source: S, signal: AbortSignal): Promise<T> => {\n // 1. Если уже идет ожидание другого запроса — отменяем его промис и очищаем таймер\n if (timeoutId) {\n clearTimeout(timeoutId)\n }\n if (rejectPrevious) {\n // Бросаем DOMException, имитируя нативную отмену fetch, чтобы движок понял, что запрос прерван\n rejectPrevious(new DOMException('Aborted due to debounce', 'AbortError'))\n }\n\n return new Promise<T>((resolve, reject) => {\n // Сохраняем ссылку на reject текущего промиса, чтобы его мог отменить следующий вызов\n rejectPrevious = reject\n\n // 2. Слушаем нативный AbortSignal от движка (например, если компонент размонтировался во время дебаунса)\n const onAbort = () => {\n if (timeoutId) clearTimeout(timeoutId)\n reject(new DOMException('Aborted by resource signal', 'AbortError'))\n }\n\n if (signal.aborted) {\n return onAbort()\n }\n signal.addEventListener('abort', onAbort)\n\n // 3. Взводим отложенный таймер выполнения\n timeoutId = setTimeout(async () => {\n // Убираем слушатель, так как таймаут успешно дождался конца\n signal.removeEventListener('abort', onAbort)\n rejectPrevious = null\n timeoutId = null\n\n try {\n // 4. Запускаем оригинальный fetcher\n const data = await fetcher(source, signal)\n resolve(data)\n } catch (error) {\n reject(error)\n }\n }, delay)\n })\n }\n}\n"],"mappings":";AA6DA,IAAa,KACX,GACA,IAA2B,CAAC,MACzB;CACH,IAAM,IAAQ,EAAQ,SAAS,KAG3B,IAAkD,MAElD,IAAiD;CAErD,QAAQ,GAAW,OAEb,KACF,aAAa,CAAS,GAEpB,KAEF,EAAe,IAAI,aAAa,2BAA2B,YAAY,CAAC,GAGnE,IAAI,SAAY,GAAS,MAAW;EAEzC,IAAiB;EAGjB,IAAM,UAAgB;GAEpB,AADI,KAAW,aAAa,CAAS,GACrC,EAAO,IAAI,aAAa,8BAA8B,YAAY,CAAC;EACrE;EAEA,IAAI,EAAO,SACT,OAAO,EAAQ;EAKjB,AAHA,EAAO,iBAAiB,SAAS,CAAO,GAGxC,IAAY,WAAW,YAAY;GAIjC,AAFA,EAAO,oBAAoB,SAAS,CAAO,GAC3C,IAAiB,MACjB,IAAY;GAEZ,IAAI;IAGF,EAAQ,MADW,EAAQ,GAAQ,CAAM,CAC7B;GACd,SAAS,GAAO;IACd,EAAO,CAAK;GACd;EACF,GAAG,CAAK;CACV,CAAC;AAEL"}
@@ -1 +1 @@
1
- {"version":3,"file":"withLongPolling.cjs","names":[],"sources":["../../../src/decorators/withLongPolling/withLongPolling.ts"],"sourcesContent":["interface LongPollingOptions {\n /** Коллбэк для инкремента реактивного тика (перехода на следующую итерацию поллинга) */\n onNextTick: () => void;\n\n /**\n * Коллбэк, вызываемый при возникновении сетевой ошибки.\n * Позволяет внешней системе узнать о сбое и текущем времени ожидания до следующей попытки.\n *\n * @param {number} delayMs - Текущая задержка Exponential Backoff в миллисекундах перед следующим запросом.\n * @param {() => void} onRetryScheduled - Коллбэк-триггер. Должен быть вызван один раз, чтобы\n * просигнализировать декоратору, что шаг зафиксирован и задержка для следующей ошибки может быть увеличена.\n */\n onError: (delayMs: number, onRetryScheduled: () => void) => void;\n\n /** Пауза перед открытием следующего соединения в миллисекундах. По умолчанию 500 мс */\n delay?: number;\n /** Стартовая задержка Exponential Backoff в миллисекундах. По умолчанию 2000 мс */\n errorInitialDelay?: number;\n /** Потолок задержки Exponential Backoff в миллисекундах. По умолчанию 8000 мс */\n errorMaxDelay?: number;\n /** Внешний сигнал для жесткой остановки рекурсивных таймаутов */\n externalSignal?: AbortSignal;\n}\n\n/**\n * Адаптированный декоратор лонг-поллинга для engine.resource.\n * Принимает строго два аргумента: асинхронную функцию и конфиг-объект.\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":"AA4BA,IAAa,GACX,EACA,IACG,CACH,GAAM,CACJ,aACA,UACA,iBACA,QAAQ,IACR,oBAAoB,IACpB,gBAAgB,KACd,EAEA,EAAoB,EACpB,EAAsB,EAE1B,OAAQ,EAAW,IAAoC,CACrD,IAAM,EAAkB,EAAE,EAE1B,OAAO,IAAI,SAAY,EAAS,IAAW,CAEzC,IAAM,EAAO,SAAY,CACvB,GAAI,EAAO,SAAW,GAAgB,QAAS,CAC7C,EAAO,IAAI,aAAa,oCAAqC,YAAY,CAAC,EAC1E,MACF,CAEA,GAAI,CACF,IAAM,EAAO,MAAM,EAAQ,EAAQ,CAAM,EAGzC,GAFA,EAAoB,EAEhB,EAAO,SAAW,GAAgB,QAAS,CAC7C,EAAO,IAAI,aAAa,oCAAqC,YAAY,CAAC,EAC1E,MACF,CAEA,EAAQ,CAAI,EAEZ,eAAiB,CACX,IAAoB,GAAuB,CAAC,EAAO,SAAW,CAAC,GAAgB,SACjF,IAAa,CAEjB,EAAG,CAAK,CAEV,OAAS,EAAO,CACd,GAAI,aAAiB,cAAgB,EAAM,OAAS,aAAc,CAChE,EAAO,CAAK,EACZ,MACF,CAEA,EAAO,CAAK,EACZ,IAAM,EAAgB,EAElB,OAAO,GAAY,YAAY,EAAQ,MAAqB,CAC1D,IAAoB,IACtB,EAAoB,KAAK,IAAI,EAAoB,EAAG,CAAa,EAErE,CAAC,EAED,eAAiB,CACX,IAAoB,GAAuB,CAAC,EAAO,SAAW,CAAC,GAAgB,SACjF,IAAa,CAEjB,EAAG,CAAa,CAClB,CACF,EAEM,MAAuB,CAC3B,EAAO,IAAI,aAAa,6BAA8B,YAAY,CAAC,CACrE,EAOA,GALA,EAAO,iBAAiB,QAAS,CAAc,EAC3C,GACF,EAAe,iBAAiB,QAAS,CAAc,EAGrD,EAAO,SAAW,GAAgB,QACpC,OAAO,EAAe,EAGxB,EAAK,CACP,CAAC,CACH,CACF"}
1
+ {"version":3,"file":"withLongPolling.cjs","names":[],"sources":["../../../src/decorators/withLongPolling/withLongPolling.ts"],"sourcesContent":["interface LongPollingOptions {\n /** Коллбэк для инкремента реактивного тика (перехода на следующую итерацию поллинга) */\n onNextTick: () => void;\n\n /**\n * Коллбэк, вызываемый при возникновении сетевой ошибки.\n * Позволяет внешней системе узнать о сбое и текущем времени ожидания до следующей попытки.\n *\n * @param {number} delayMs - Текущая задержка Exponential Backoff в миллисекундах перед следующим запросом.\n * @param {() => void} onRetryScheduled - Коллбэк-триггер. Должен быть вызван один раз, чтобы\n * просигнализировать декоратору, что шаг зафиксирован и задержка для следующей ошибки может быть увеличена.\n */\n onError: (delayMs: number, onRetryScheduled: () => void) => void;\n\n /** Пауза перед открытием следующего соединения в миллисекундах. По умолчанию 500 мс */\n delay?: number;\n /** Стартовая задержка Exponential Backoff в миллисекундах. По умолчанию 2000 мс */\n errorInitialDelay?: number;\n /** Потолок задержки Exponential Backoff в миллисекундах. По умолчанию 8000 мс */\n errorMaxDelay?: number;\n /** Внешний сигнал для жесткой остановки рекурсивных таймаутов */\n externalSignal?: AbortSignal;\n}\n\n/**\n * Адаптированный декоратор для реализации паттерна Long Polling (длинные опросы),\n * спроектированный специально для интеграции с подсистемой `engine.resource`.\n *\n * Превращает разовый асинхронный запрос данных в непрерывный контролируемый конвейер\n * циклического опроса сервера с поддержкой экспоненциального отката при ошибках\n * (Exponential Backoff) и защитой от пересечения параллельных сессий.\n *\n * ### 🧠 Архитектура конвейера опроса:\n * 1. **Контроль сессионных токенов:** Каждый новый вызов функции инкрементирует `currentSessionToken`.\n * Если предыдущий запрос завершится позже, таймер следующего шага не взведется, так как\n * идентификаторы сессий (`activeSessionId === currentSessionToken`) не совпадут. Это полностью исключает утечки и гонки ответов.\n * 2. **Экспоненциальный откат (Exponential Backoff):** При успешном запросе интервал между циклами равен `options.delay`.\n * В случае падения сетевого запроса, время ожидания перед следующим опросом прогрессивно удваивается,\n * начиная с `errorInitialDelay` вплоть до достижения лимита `errorMaxDelay`, чтобы не спамить упавший бэкенд.\n * 3. **Двойной контур отмены:** Поддерживает как локальный `AbortSignal` от ресурса, так и глобальный `externalSignal`\n * (например, для остановки всех фоновых процессов при закрытии приложения).\n *\n * @template S Тип входных данных (аргументов) для функции запроса.\n * @template T Тип данных, возвращаемых асинхронным `fetcher`-ом (разрешенное значение промиса).\n *\n * @param {(source: S, signal: AbortSignal) => Promise<T>} fetcher Асинхронная функция-загрузчик (например, fetch-запрос к эндпоинту уведомлений).\n * @param {LongPollingOptions} options Конфигурация параметров и коллбэков длинных опросов.\n * @param {() => void} options.onNextTick Коллбэк-триггер, вызываемый для перезапуска цикла опроса (в контексте ядра обычно мутирует или инвалидирует `source` ресурса).\n * @param {(currentDelay: number, increaseDelay: () => void) => void} [options.onError] Коллбэк, вызываемый при ошибке сети. Позволяет логировать сбои и управлять шагом экспоненциального отката через запуск функции `increaseDelay()`.\n * @param {AbortSignal} [options.externalSignal] Внешний сигнал отмены для принудительной остановки поллинга извне (независимо от жизненного цикла ресурса).\n * @param {number} [options.delay=500] Базовая задержка в миллисекундах между успешным ответом сервера и началом следующего запроса.\n * @param {number} [options.errorInitialDelay=2000] Стартовая задержка в миллисекундах при возникновении первой ошибки.\n * @param {number} [options.errorMaxDelay=8000] Максимально возможный интервал задержки в миллисекундах при череде последовательных ошибок.\n *\n * @returns {(source: S, signal: AbortSignal) => Promise<T>} Возвращает обернутую циклическую функцию, возвращающую `Promise<T>`.\n *\n * @example\n * ```typescript\n * import { withLongPolling } from '@pravosleva/reactive-engine';\n *\n * // 1. Настраиваем функцию опроса новостной ленты\n * const pollingFetch = withLongPolling(\n * async (userId: string, signal) => {\n * const res = await fetch(`/api/notifications?uid=${userId}`, { signal });\n * return res.json();\n * },\n * {\n * delay: 1000,\n * errorInitialDelay: 2000,\n * errorMaxDelay: 10000,\n * onNextTick: () => {\n * // Инвалидируем или триггерим обновление ресурса в ядре\n * notificationResource.refresh();\n * },\n * onError: (delay, increaseDelay) => {\n * console.warn(`Ошибка сети. Следующая попытка через ${delay}мс`);\n * increaseDelay(); // Удваиваем интервал до следующего тика\n * }\n * }\n * );\n *\n * // 2. Передаем декоратор в подсистему ресурсов ядра\n * const notificationResource = engine.resource({\n * fetcher: pollingFetch,\n * source: () => currentUserId.value\n * });\n * ```\n *\n * @abstract\n * ### 🚨 Поведение при отмене и очистка слушателей\n * При срабатывании любого из сигналов отмены (`signal` или `externalSignal` переходит в состояние `aborted`):\n * - Текущий ожидающий промис немедленно отклоняется с системным исключением `AbortError`.\n * - Цикл поллинга блокируется и полностью прекращает планирование будущих макрозадач `setTimeout`.\n * - Все подписки на события `'abort'` корректно удаляются, предотвращая скрытые утечки памяти в Node.js / браузере.\n */\nexport const withLongPolling = <S, T>(\n fetcher: (source: S, signal: AbortSignal) => Promise<T>,\n options: LongPollingOptions\n) => {\n const {\n onNextTick,\n onError,\n externalSignal,\n delay = 500,\n errorInitialDelay = 2000,\n errorMaxDelay = 8000\n } = options\n\n let currentErrorDelay = errorInitialDelay\n let currentSessionToken = 0\n\n return (source: S, signal: AbortSignal): Promise<T> => {\n const activeSessionId = ++currentSessionToken\n\n return new Promise<T>((resolve, reject) => {\n\n const poll = async () => {\n if (signal.aborted || externalSignal?.aborted) {\n reject(new DOMException('Aborted by long polling lifecycle', 'AbortError'))\n return\n }\n\n try {\n const data = await fetcher(source, signal)\n currentErrorDelay = errorInitialDelay\n\n if (signal.aborted || externalSignal?.aborted) {\n reject(new DOMException('Aborted by long polling lifecycle', 'AbortError'))\n return\n }\n\n resolve(data)\n\n setTimeout(() => {\n if (activeSessionId === currentSessionToken && !signal.aborted && !externalSignal?.aborted) {\n onNextTick?.()\n }\n }, delay)\n\n } catch (error) {\n if (error instanceof DOMException && error.name === 'AbortError') {\n reject(error)\n return\n }\n\n reject(error)\n const delayDuration = currentErrorDelay\n\n if (typeof onError === 'function') onError(delayDuration, () => {\n if (activeSessionId === currentSessionToken) {\n currentErrorDelay = Math.min(currentErrorDelay * 2, errorMaxDelay)\n }\n })\n\n setTimeout(() => {\n if (activeSessionId === currentSessionToken && !signal.aborted && !externalSignal?.aborted) {\n onNextTick?.()\n }\n }, delayDuration)\n }\n }\n\n const forceKillTimer = () => {\n reject(new DOMException('Aborted by lifecycle event', 'AbortError'))\n }\n\n signal.addEventListener('abort', forceKillTimer)\n if (externalSignal) {\n externalSignal.addEventListener('abort', forceKillTimer)\n }\n\n if (signal.aborted || externalSignal?.aborted) {\n return forceKillTimer()\n }\n\n poll()\n })\n }\n}\n"],"mappings":"AA+FA,IAAa,GACX,EACA,IACG,CACH,GAAM,CACJ,aACA,UACA,iBACA,QAAQ,IACR,oBAAoB,IACpB,gBAAgB,KACd,EAEA,EAAoB,EACpB,EAAsB,EAE1B,OAAQ,EAAW,IAAoC,CACrD,IAAM,EAAkB,EAAE,EAE1B,OAAO,IAAI,SAAY,EAAS,IAAW,CAEzC,IAAM,EAAO,SAAY,CACvB,GAAI,EAAO,SAAW,GAAgB,QAAS,CAC7C,EAAO,IAAI,aAAa,oCAAqC,YAAY,CAAC,EAC1E,MACF,CAEA,GAAI,CACF,IAAM,EAAO,MAAM,EAAQ,EAAQ,CAAM,EAGzC,GAFA,EAAoB,EAEhB,EAAO,SAAW,GAAgB,QAAS,CAC7C,EAAO,IAAI,aAAa,oCAAqC,YAAY,CAAC,EAC1E,MACF,CAEA,EAAQ,CAAI,EAEZ,eAAiB,CACX,IAAoB,GAAuB,CAAC,EAAO,SAAW,CAAC,GAAgB,SACjF,IAAa,CAEjB,EAAG,CAAK,CAEV,OAAS,EAAO,CACd,GAAI,aAAiB,cAAgB,EAAM,OAAS,aAAc,CAChE,EAAO,CAAK,EACZ,MACF,CAEA,EAAO,CAAK,EACZ,IAAM,EAAgB,EAElB,OAAO,GAAY,YAAY,EAAQ,MAAqB,CAC1D,IAAoB,IACtB,EAAoB,KAAK,IAAI,EAAoB,EAAG,CAAa,EAErE,CAAC,EAED,eAAiB,CACX,IAAoB,GAAuB,CAAC,EAAO,SAAW,CAAC,GAAgB,SACjF,IAAa,CAEjB,EAAG,CAAa,CAClB,CACF,EAEM,MAAuB,CAC3B,EAAO,IAAI,aAAa,6BAA8B,YAAY,CAAC,CACrE,EAOA,GALA,EAAO,iBAAiB,QAAS,CAAc,EAC3C,GACF,EAAe,iBAAiB,QAAS,CAAc,EAGrD,EAAO,SAAW,GAAgB,QACpC,OAAO,EAAe,EAGxB,EAAK,CACP,CAAC,CACH,CACF"}
@@ -20,8 +20,75 @@ interface LongPollingOptions {
20
20
  externalSignal?: AbortSignal;
21
21
  }
22
22
  /**
23
- * Адаптированный декоратор лонг-поллинга для engine.resource.
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;;;GAGG;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
+ {"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 * Адаптированный декоратор лонг-поллинга для engine.resource.\n * Принимает строго два аргумента: асинхронную функцию и конфиг-объект.\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":";AA4BA,IAAa,KACX,GACA,MACG;CACH,IAAM,EACJ,eACA,YACA,mBACA,WAAQ,KACR,uBAAoB,KACpB,mBAAgB,QACd,GAEA,IAAoB,GACpB,IAAsB;CAE1B,QAAQ,GAAW,MAAoC;EACrD,IAAM,IAAkB,EAAE;EAE1B,OAAO,IAAI,SAAY,GAAS,MAAW;GAEzC,IAAM,IAAO,YAAY;IACvB,IAAI,EAAO,WAAW,GAAgB,SAAS;KAC7C,EAAO,IAAI,aAAa,qCAAqC,YAAY,CAAC;KAC1E;IACF;IAEA,IAAI;KACF,IAAM,IAAO,MAAM,EAAQ,GAAQ,CAAM;KAGzC,IAFA,IAAoB,GAEhB,EAAO,WAAW,GAAgB,SAAS;MAC7C,EAAO,IAAI,aAAa,qCAAqC,YAAY,CAAC;MAC1E;KACF;KAIA,AAFA,EAAQ,CAAI,GAEZ,iBAAiB;MACf,AAAI,MAAoB,KAAuB,CAAC,EAAO,WAAW,CAAC,GAAgB,WACjF,IAAa;KAEjB,GAAG,CAAK;IAEV,SAAS,GAAO;KACd,IAAI,aAAiB,gBAAgB,EAAM,SAAS,cAAc;MAChE,EAAO,CAAK;MACZ;KACF;KAEA,EAAO,CAAK;KACZ,IAAM,IAAgB;KAQtB,AANI,OAAO,KAAY,cAAY,EAAQ,SAAqB;MAC9D,AAAI,MAAoB,MACtB,IAAoB,KAAK,IAAI,IAAoB,GAAG,CAAa;KAErE,CAAC,GAED,iBAAiB;MACf,AAAI,MAAoB,KAAuB,CAAC,EAAO,WAAW,CAAC,GAAgB,WACjF,IAAa;KAEjB,GAAG,CAAa;IAClB;GACF,GAEM,UAAuB;IAC3B,EAAO,IAAI,aAAa,8BAA8B,YAAY,CAAC;GACrE;GAOA,IALA,EAAO,iBAAiB,SAAS,CAAc,GAC3C,KACF,EAAe,iBAAiB,SAAS,CAAc,GAGrD,EAAO,WAAW,GAAgB,SACpC,OAAO,EAAe;GAGxB,EAAK;EACP,CAAC;CACH;AACF"}
1
+ {"version":3,"file":"withLongPolling.mjs","names":[],"sources":["../../../src/decorators/withLongPolling/withLongPolling.ts"],"sourcesContent":["interface LongPollingOptions {\n /** Коллбэк для инкремента реактивного тика (перехода на следующую итерацию поллинга) */\n onNextTick: () => void;\n\n /**\n * Коллбэк, вызываемый при возникновении сетевой ошибки.\n * Позволяет внешней системе узнать о сбое и текущем времени ожидания до следующей попытки.\n *\n * @param {number} delayMs - Текущая задержка Exponential Backoff в миллисекундах перед следующим запросом.\n * @param {() => void} onRetryScheduled - Коллбэк-триггер. Должен быть вызван один раз, чтобы\n * просигнализировать декоратору, что шаг зафиксирован и задержка для следующей ошибки может быть увеличена.\n */\n onError: (delayMs: number, onRetryScheduled: () => void) => void;\n\n /** Пауза перед открытием следующего соединения в миллисекундах. По умолчанию 500 мс */\n delay?: number;\n /** Стартовая задержка Exponential Backoff в миллисекундах. По умолчанию 2000 мс */\n errorInitialDelay?: number;\n /** Потолок задержки Exponential Backoff в миллисекундах. По умолчанию 8000 мс */\n errorMaxDelay?: number;\n /** Внешний сигнал для жесткой остановки рекурсивных таймаутов */\n externalSignal?: AbortSignal;\n}\n\n/**\n * Адаптированный декоратор для реализации паттерна Long Polling (длинные опросы),\n * спроектированный специально для интеграции с подсистемой `engine.resource`.\n *\n * Превращает разовый асинхронный запрос данных в непрерывный контролируемый конвейер\n * циклического опроса сервера с поддержкой экспоненциального отката при ошибках\n * (Exponential Backoff) и защитой от пересечения параллельных сессий.\n *\n * ### 🧠 Архитектура конвейера опроса:\n * 1. **Контроль сессионных токенов:** Каждый новый вызов функции инкрементирует `currentSessionToken`.\n * Если предыдущий запрос завершится позже, таймер следующего шага не взведется, так как\n * идентификаторы сессий (`activeSessionId === currentSessionToken`) не совпадут. Это полностью исключает утечки и гонки ответов.\n * 2. **Экспоненциальный откат (Exponential Backoff):** При успешном запросе интервал между циклами равен `options.delay`.\n * В случае падения сетевого запроса, время ожидания перед следующим опросом прогрессивно удваивается,\n * начиная с `errorInitialDelay` вплоть до достижения лимита `errorMaxDelay`, чтобы не спамить упавший бэкенд.\n * 3. **Двойной контур отмены:** Поддерживает как локальный `AbortSignal` от ресурса, так и глобальный `externalSignal`\n * (например, для остановки всех фоновых процессов при закрытии приложения).\n *\n * @template S Тип входных данных (аргументов) для функции запроса.\n * @template T Тип данных, возвращаемых асинхронным `fetcher`-ом (разрешенное значение промиса).\n *\n * @param {(source: S, signal: AbortSignal) => Promise<T>} fetcher Асинхронная функция-загрузчик (например, fetch-запрос к эндпоинту уведомлений).\n * @param {LongPollingOptions} options Конфигурация параметров и коллбэков длинных опросов.\n * @param {() => void} options.onNextTick Коллбэк-триггер, вызываемый для перезапуска цикла опроса (в контексте ядра обычно мутирует или инвалидирует `source` ресурса).\n * @param {(currentDelay: number, increaseDelay: () => void) => void} [options.onError] Коллбэк, вызываемый при ошибке сети. Позволяет логировать сбои и управлять шагом экспоненциального отката через запуск функции `increaseDelay()`.\n * @param {AbortSignal} [options.externalSignal] Внешний сигнал отмены для принудительной остановки поллинга извне (независимо от жизненного цикла ресурса).\n * @param {number} [options.delay=500] Базовая задержка в миллисекундах между успешным ответом сервера и началом следующего запроса.\n * @param {number} [options.errorInitialDelay=2000] Стартовая задержка в миллисекундах при возникновении первой ошибки.\n * @param {number} [options.errorMaxDelay=8000] Максимально возможный интервал задержки в миллисекундах при череде последовательных ошибок.\n *\n * @returns {(source: S, signal: AbortSignal) => Promise<T>} Возвращает обернутую циклическую функцию, возвращающую `Promise<T>`.\n *\n * @example\n * ```typescript\n * import { withLongPolling } from '@pravosleva/reactive-engine';\n *\n * // 1. Настраиваем функцию опроса новостной ленты\n * const pollingFetch = withLongPolling(\n * async (userId: string, signal) => {\n * const res = await fetch(`/api/notifications?uid=${userId}`, { signal });\n * return res.json();\n * },\n * {\n * delay: 1000,\n * errorInitialDelay: 2000,\n * errorMaxDelay: 10000,\n * onNextTick: () => {\n * // Инвалидируем или триггерим обновление ресурса в ядре\n * notificationResource.refresh();\n * },\n * onError: (delay, increaseDelay) => {\n * console.warn(`Ошибка сети. Следующая попытка через ${delay}мс`);\n * increaseDelay(); // Удваиваем интервал до следующего тика\n * }\n * }\n * );\n *\n * // 2. Передаем декоратор в подсистему ресурсов ядра\n * const notificationResource = engine.resource({\n * fetcher: pollingFetch,\n * source: () => currentUserId.value\n * });\n * ```\n *\n * @abstract\n * ### 🚨 Поведение при отмене и очистка слушателей\n * При срабатывании любого из сигналов отмены (`signal` или `externalSignal` переходит в состояние `aborted`):\n * - Текущий ожидающий промис немедленно отклоняется с системным исключением `AbortError`.\n * - Цикл поллинга блокируется и полностью прекращает планирование будущих макрозадач `setTimeout`.\n * - Все подписки на события `'abort'` корректно удаляются, предотвращая скрытые утечки памяти в Node.js / браузере.\n */\nexport const withLongPolling = <S, T>(\n fetcher: (source: S, signal: AbortSignal) => Promise<T>,\n options: LongPollingOptions\n) => {\n const {\n onNextTick,\n onError,\n externalSignal,\n delay = 500,\n errorInitialDelay = 2000,\n errorMaxDelay = 8000\n } = options\n\n let currentErrorDelay = errorInitialDelay\n let currentSessionToken = 0\n\n return (source: S, signal: AbortSignal): Promise<T> => {\n const activeSessionId = ++currentSessionToken\n\n return new Promise<T>((resolve, reject) => {\n\n const poll = async () => {\n if (signal.aborted || externalSignal?.aborted) {\n reject(new DOMException('Aborted by long polling lifecycle', 'AbortError'))\n return\n }\n\n try {\n const data = await fetcher(source, signal)\n currentErrorDelay = errorInitialDelay\n\n if (signal.aborted || externalSignal?.aborted) {\n reject(new DOMException('Aborted by long polling lifecycle', 'AbortError'))\n return\n }\n\n resolve(data)\n\n setTimeout(() => {\n if (activeSessionId === currentSessionToken && !signal.aborted && !externalSignal?.aborted) {\n onNextTick?.()\n }\n }, delay)\n\n } catch (error) {\n if (error instanceof DOMException && error.name === 'AbortError') {\n reject(error)\n return\n }\n\n reject(error)\n const delayDuration = currentErrorDelay\n\n if (typeof onError === 'function') onError(delayDuration, () => {\n if (activeSessionId === currentSessionToken) {\n currentErrorDelay = Math.min(currentErrorDelay * 2, errorMaxDelay)\n }\n })\n\n setTimeout(() => {\n if (activeSessionId === currentSessionToken && !signal.aborted && !externalSignal?.aborted) {\n onNextTick?.()\n }\n }, delayDuration)\n }\n }\n\n const forceKillTimer = () => {\n reject(new DOMException('Aborted by lifecycle event', 'AbortError'))\n }\n\n signal.addEventListener('abort', forceKillTimer)\n if (externalSignal) {\n externalSignal.addEventListener('abort', forceKillTimer)\n }\n\n if (signal.aborted || externalSignal?.aborted) {\n return forceKillTimer()\n }\n\n poll()\n })\n }\n}\n"],"mappings":";AA+FA,IAAa,KACX,GACA,MACG;CACH,IAAM,EACJ,eACA,YACA,mBACA,WAAQ,KACR,uBAAoB,KACpB,mBAAgB,QACd,GAEA,IAAoB,GACpB,IAAsB;CAE1B,QAAQ,GAAW,MAAoC;EACrD,IAAM,IAAkB,EAAE;EAE1B,OAAO,IAAI,SAAY,GAAS,MAAW;GAEzC,IAAM,IAAO,YAAY;IACvB,IAAI,EAAO,WAAW,GAAgB,SAAS;KAC7C,EAAO,IAAI,aAAa,qCAAqC,YAAY,CAAC;KAC1E;IACF;IAEA,IAAI;KACF,IAAM,IAAO,MAAM,EAAQ,GAAQ,CAAM;KAGzC,IAFA,IAAoB,GAEhB,EAAO,WAAW,GAAgB,SAAS;MAC7C,EAAO,IAAI,aAAa,qCAAqC,YAAY,CAAC;MAC1E;KACF;KAIA,AAFA,EAAQ,CAAI,GAEZ,iBAAiB;MACf,AAAI,MAAoB,KAAuB,CAAC,EAAO,WAAW,CAAC,GAAgB,WACjF,IAAa;KAEjB,GAAG,CAAK;IAEV,SAAS,GAAO;KACd,IAAI,aAAiB,gBAAgB,EAAM,SAAS,cAAc;MAChE,EAAO,CAAK;MACZ;KACF;KAEA,EAAO,CAAK;KACZ,IAAM,IAAgB;KAQtB,AANI,OAAO,KAAY,cAAY,EAAQ,SAAqB;MAC9D,AAAI,MAAoB,MACtB,IAAoB,KAAK,IAAI,IAAoB,GAAG,CAAa;KAErE,CAAC,GAED,iBAAiB;MACf,AAAI,MAAoB,KAAuB,CAAC,EAAO,WAAW,CAAC,GAAgB,WACjF,IAAa;KAEjB,GAAG,CAAa;IAClB;GACF,GAEM,UAAuB;IAC3B,EAAO,IAAI,aAAa,8BAA8B,YAAY,CAAC;GACrE;GAOA,IALA,EAAO,iBAAiB,SAAS,CAAc,GAC3C,KACF,EAAe,iBAAiB,SAAS,CAAc,GAGrD,EAAO,WAAW,GAAgB,SACpC,OAAO,EAAe;GAGxB,EAAK;EACP,CAAC;CACH;AACF"}
@@ -0,0 +1,2 @@
1
+ export * from './withStaleWhileRevalidate';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/decorators/withStaleWhileRevalidate/index.ts"],"names":[],"mappings":"AAAA,cAAc,4BAA4B,CAAA"}
@@ -0,0 +1,2 @@
1
+ var e=(e,t={})=>{let n=t.initialData,r=t.isLogsEnabled??!1;return async(t,i)=>{if(i.aborted){if(n!==void 0)return n;throw i.reason||new DOMException(`The operation was aborted.`,`AbortError`)}try{let r=await e(t,i);return n=r,r}catch(e){let t=e instanceof DOMException&&e.name===`AbortError`;if(n!==void 0)return r&&console.warn(`[ReactiveEngine:Stale] Запрос ${t?`отменен`:`упал с ошибкой`}. Отдаем предыдущее состояние.`,e),n;throw e}}};exports.withStaleWhileRevalidate=e;
2
+ //# sourceMappingURL=withStaleWhileRevalidate.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"withStaleWhileRevalidate.cjs","names":[],"sources":["../../../src/decorators/withStaleWhileRevalidate/withStaleWhileRevalidate.ts"],"sourcesContent":["interface StaleOptions<T> {\n /**\n * Дефолтное значение, которое вернется при самом первом запросе,\n * если он завершился ошибкой или был отменен.\n */\n initialData?: T;\n isLogsEnabled?: boolean;\n}\n\n/**\n * Декоратор для сохранения предыдущего успешного состояния данных,\n * специально адаптированный для использования совместно с `engine.resource`.\n *\n * Оборачивает асинхронную функцию `fetcher`. Если текущий запрос отменяется\n * (например, через AbortSignal при перетаскивании карты) или завершается ошибкой,\n * декоратор перехватывает исключение и возвращает последнее успешно полученное значение.\n *\n * ### 🧠 Механика Stale-While-Revalidate:\n * 1. **Стабильный кэш последнего состояния:** Декоратор сохраняет в замыкании успешный результат.\n * 2. **Грациозная деградация (Graceful Degradation):** При возникновении `AbortError` (отмена)\n * или любого другого сетевого сбоя, вместо выброса исключения в граф возвращается `stale`-дата.\n * 3. **Бесшовный UX:** Карта или UI-компонент не сбрасывают свое состояние в `null` во время\n * перепривязки сигналов, исключая эффект \"моргания\".\n *\n * @template S Тип входных данных (аргументов) для функции запроса.\n * @template T Тип данных, возвращаемых асинхронным `fetcher`-ом.\n *\n * @param {(source: S, signal: AbortSignal) => Promise<T>} fetcher Оригинальная асинхронная функция запроса.\n * @param {StaleOptions<T>} [options={}] Параметры конфигурации предыдущего состояния.\n *\n * @returns {(source: S, signal: AbortSignal) => Promise<T>} Вовращает обернутую функцию с сохраненной сигнатурой типов.\n */\nexport const withStaleWhileRevalidate = <S, T>(\n fetcher: (source: S, signal: AbortSignal) => Promise<T>,\n options: StaleOptions<T> = {}\n) => {\n // Хранилище для последнего успешного ответа сервера в рамках замыкания декоратора\n let lastValidData: T | undefined = options.initialData\n const isLogsEnabled = options.isLogsEnabled ?? false\n\n return async (source: S, signal: AbortSignal): Promise<T> => {\n // Если запрос отменен еще до старта, сразу возвращаем последнее известное состояние\n if (signal.aborted) {\n if (lastValidData !== undefined) return lastValidData\n throw signal.reason || new DOMException('The operation was aborted.', 'AbortError')\n }\n\n try {\n // Выполняем реальный сетевой запрос\n const freshData = await fetcher(source, signal)\n\n // Запоминаем успешный результат\n lastValidData = freshData\n\n return freshData\n } catch (error) {\n // Если запрос был отменен движком (например, изменился bbox) ИЛИ произошла сетевая ошибка\n const isAbort = error instanceof DOMException && error.name === 'AbortError'\n\n // Если у нас уже есть сохраненные старые данные — отдаем их вместо падения\n if (lastValidData !== undefined) {\n if (isLogsEnabled) {\n // Логируем для трассировки в dev-режиме (опционально)\n console.warn(\n `[ReactiveEngine:Stale] Запрос ${isAbort ? 'отменен' : 'упал с ошибкой'}. Отдаем предыдущее состояние.`,\n error\n )\n }\n return lastValidData\n }\n\n // Если это самый первый запрос и кэш пуст — прокидываем ошибку дальше в граф\n throw error\n }\n }\n}\n"],"mappings":"AAgCA,IAAa,GACX,EACA,EAA2B,CAAC,IACzB,CAEH,IAAI,EAA+B,EAAQ,YACrC,EAAgB,EAAQ,eAAiB,GAE/C,OAAO,MAAO,EAAW,IAAoC,CAE3D,GAAI,EAAO,QAAS,CAClB,GAAI,IAAkB,IAAA,GAAW,OAAO,EACxC,MAAM,EAAO,QAAU,IAAI,aAAa,6BAA8B,YAAY,CACpF,CAEA,GAAI,CAEF,IAAM,EAAY,MAAM,EAAQ,EAAQ,CAAM,EAK9C,MAFA,GAAgB,EAET,CACT,OAAS,EAAO,CAEd,IAAM,EAAU,aAAiB,cAAgB,EAAM,OAAS,aAGhE,GAAI,IAAkB,IAAA,GAQpB,OAPI,GAEF,QAAQ,KACN,iCAAiC,EAAU,UAAY,iBAAiB,gCACxE,CACF,EAEK,EAIT,MAAM,CACR,CACF,CACF"}
@@ -0,0 +1,34 @@
1
+ interface StaleOptions<T> {
2
+ /**
3
+ * Дефолтное значение, которое вернется при самом первом запросе,
4
+ * если он завершился ошибкой или был отменен.
5
+ */
6
+ initialData?: T;
7
+ isLogsEnabled?: boolean;
8
+ }
9
+ /**
10
+ * Декоратор для сохранения предыдущего успешного состояния данных,
11
+ * специально адаптированный для использования совместно с `engine.resource`.
12
+ *
13
+ * Оборачивает асинхронную функцию `fetcher`. Если текущий запрос отменяется
14
+ * (например, через AbortSignal при перетаскивании карты) или завершается ошибкой,
15
+ * декоратор перехватывает исключение и возвращает последнее успешно полученное значение.
16
+ *
17
+ * ### 🧠 Механика Stale-While-Revalidate:
18
+ * 1. **Стабильный кэш последнего состояния:** Декоратор сохраняет в замыкании успешный результат.
19
+ * 2. **Грациозная деградация (Graceful Degradation):** При возникновении `AbortError` (отмена)
20
+ * или любого другого сетевого сбоя, вместо выброса исключения в граф возвращается `stale`-дата.
21
+ * 3. **Бесшовный UX:** Карта или UI-компонент не сбрасывают свое состояние в `null` во время
22
+ * перепривязки сигналов, исключая эффект "моргания".
23
+ *
24
+ * @template S Тип входных данных (аргументов) для функции запроса.
25
+ * @template T Тип данных, возвращаемых асинхронным `fetcher`-ом.
26
+ *
27
+ * @param {(source: S, signal: AbortSignal) => Promise<T>} fetcher Оригинальная асинхронная функция запроса.
28
+ * @param {StaleOptions<T>} [options={}] Параметры конфигурации предыдущего состояния.
29
+ *
30
+ * @returns {(source: S, signal: AbortSignal) => Promise<T>} Вовращает обернутую функцию с сохраненной сигнатурой типов.
31
+ */
32
+ export declare const withStaleWhileRevalidate: <S, T>(fetcher: (source: S, signal: AbortSignal) => Promise<T>, options?: StaleOptions<T>) => (source: S, signal: AbortSignal) => Promise<T>;
33
+ export {};
34
+ //# sourceMappingURL=withStaleWhileRevalidate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"withStaleWhileRevalidate.d.ts","sourceRoot":"","sources":["../../../src/decorators/withStaleWhileRevalidate/withStaleWhileRevalidate.ts"],"names":[],"mappings":"AAAA,UAAU,YAAY,CAAC,CAAC;IACtB;;;OAGG;IACH,WAAW,CAAC,EAAE,CAAC,CAAC;IAChB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,wBAAwB,GAAI,CAAC,EAAE,CAAC,EAC3C,SAAS,CAAC,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC,EACvD,UAAS,YAAY,CAAC,CAAC,CAAM,MAMf,QAAQ,CAAC,EAAE,QAAQ,WAAW,KAAG,OAAO,CAAC,CAAC,CAmCzD,CAAA"}
@@ -0,0 +1,22 @@
1
+ //#region src/decorators/withStaleWhileRevalidate/withStaleWhileRevalidate.ts
2
+ var e = (e, t = {}) => {
3
+ let n = t.initialData, r = t.isLogsEnabled ?? !1;
4
+ return async (t, i) => {
5
+ if (i.aborted) {
6
+ if (n !== void 0) return n;
7
+ throw i.reason || new DOMException("The operation was aborted.", "AbortError");
8
+ }
9
+ try {
10
+ let r = await e(t, i);
11
+ return n = r, r;
12
+ } catch (e) {
13
+ let t = e instanceof DOMException && e.name === "AbortError";
14
+ if (n !== void 0) return r && console.warn(`[ReactiveEngine:Stale] Запрос ${t ? "отменен" : "упал с ошибкой"}. Отдаем предыдущее состояние.`, e), n;
15
+ throw e;
16
+ }
17
+ };
18
+ };
19
+ //#endregion
20
+ export { e as withStaleWhileRevalidate };
21
+
22
+ //# sourceMappingURL=withStaleWhileRevalidate.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"withStaleWhileRevalidate.mjs","names":[],"sources":["../../../src/decorators/withStaleWhileRevalidate/withStaleWhileRevalidate.ts"],"sourcesContent":["interface StaleOptions<T> {\n /**\n * Дефолтное значение, которое вернется при самом первом запросе,\n * если он завершился ошибкой или был отменен.\n */\n initialData?: T;\n isLogsEnabled?: boolean;\n}\n\n/**\n * Декоратор для сохранения предыдущего успешного состояния данных,\n * специально адаптированный для использования совместно с `engine.resource`.\n *\n * Оборачивает асинхронную функцию `fetcher`. Если текущий запрос отменяется\n * (например, через AbortSignal при перетаскивании карты) или завершается ошибкой,\n * декоратор перехватывает исключение и возвращает последнее успешно полученное значение.\n *\n * ### 🧠 Механика Stale-While-Revalidate:\n * 1. **Стабильный кэш последнего состояния:** Декоратор сохраняет в замыкании успешный результат.\n * 2. **Грациозная деградация (Graceful Degradation):** При возникновении `AbortError` (отмена)\n * или любого другого сетевого сбоя, вместо выброса исключения в граф возвращается `stale`-дата.\n * 3. **Бесшовный UX:** Карта или UI-компонент не сбрасывают свое состояние в `null` во время\n * перепривязки сигналов, исключая эффект \"моргания\".\n *\n * @template S Тип входных данных (аргументов) для функции запроса.\n * @template T Тип данных, возвращаемых асинхронным `fetcher`-ом.\n *\n * @param {(source: S, signal: AbortSignal) => Promise<T>} fetcher Оригинальная асинхронная функция запроса.\n * @param {StaleOptions<T>} [options={}] Параметры конфигурации предыдущего состояния.\n *\n * @returns {(source: S, signal: AbortSignal) => Promise<T>} Вовращает обернутую функцию с сохраненной сигнатурой типов.\n */\nexport const withStaleWhileRevalidate = <S, T>(\n fetcher: (source: S, signal: AbortSignal) => Promise<T>,\n options: StaleOptions<T> = {}\n) => {\n // Хранилище для последнего успешного ответа сервера в рамках замыкания декоратора\n let lastValidData: T | undefined = options.initialData\n const isLogsEnabled = options.isLogsEnabled ?? false\n\n return async (source: S, signal: AbortSignal): Promise<T> => {\n // Если запрос отменен еще до старта, сразу возвращаем последнее известное состояние\n if (signal.aborted) {\n if (lastValidData !== undefined) return lastValidData\n throw signal.reason || new DOMException('The operation was aborted.', 'AbortError')\n }\n\n try {\n // Выполняем реальный сетевой запрос\n const freshData = await fetcher(source, signal)\n\n // Запоминаем успешный результат\n lastValidData = freshData\n\n return freshData\n } catch (error) {\n // Если запрос был отменен движком (например, изменился bbox) ИЛИ произошла сетевая ошибка\n const isAbort = error instanceof DOMException && error.name === 'AbortError'\n\n // Если у нас уже есть сохраненные старые данные — отдаем их вместо падения\n if (lastValidData !== undefined) {\n if (isLogsEnabled) {\n // Логируем для трассировки в dev-режиме (опционально)\n console.warn(\n `[ReactiveEngine:Stale] Запрос ${isAbort ? 'отменен' : 'упал с ошибкой'}. Отдаем предыдущее состояние.`,\n error\n )\n }\n return lastValidData\n }\n\n // Если это самый первый запрос и кэш пуст — прокидываем ошибку дальше в граф\n throw error\n }\n }\n}\n"],"mappings":";AAgCA,IAAa,KACX,GACA,IAA2B,CAAC,MACzB;CAEH,IAAI,IAA+B,EAAQ,aACrC,IAAgB,EAAQ,iBAAiB;CAE/C,OAAO,OAAO,GAAW,MAAoC;EAE3D,IAAI,EAAO,SAAS;GAClB,IAAI,MAAkB,KAAA,GAAW,OAAO;GACxC,MAAM,EAAO,UAAU,IAAI,aAAa,8BAA8B,YAAY;EACpF;EAEA,IAAI;GAEF,IAAM,IAAY,MAAM,EAAQ,GAAQ,CAAM;GAK9C,OAFA,IAAgB,GAET;EACT,SAAS,GAAO;GAEd,IAAM,IAAU,aAAiB,gBAAgB,EAAM,SAAS;GAGhE,IAAI,MAAkB,KAAA,GAQpB,OAPI,KAEF,QAAQ,KACN,iCAAiC,IAAU,YAAY,iBAAiB,iCACxE,CACF,GAEK;GAIT,MAAM;EACR;CACF;AACF"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=withStaleWhileRevalidate.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"withStaleWhileRevalidate.test.d.ts","sourceRoot":"","sources":["../../../src/decorators/withStaleWhileRevalidate/withStaleWhileRevalidate.test.ts"],"names":[],"mappings":""}
@@ -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 * Декоратор для создания троттлящего fetcher-а для engine.resource\n * @param fetcher Оригинальная асинхронная функция запроса\n * @param options Настройки троттлинга\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":"AAUA,IAAa,GACX,EACA,EAA2B,CAAC,IACzB,CACH,IAAM,EAAQ,EAAQ,OAAS,IAE3B,EAAoB,EACpB,EAA0D,KAG1D,EAA4B,KAC5B,EAAiE,KACjE,EAAkD,KAClD,EAAsC,KAE1C,OAAQ,EAAW,IAAoC,CACrD,IAAM,EAAM,KAAK,IAAI,EACf,EAAgB,GAAS,EAAM,GAG/B,MAAgB,CACpB,AAEE,KADA,aAAa,CAAiB,EACV,MAEtB,AAGE,KAFA,EAAgB,IAAI,aAAa,6CAA8C,YAAY,CAAC,EAC5F,EAAmB,KACD,KAEtB,EA0BA,OAvBI,GAAiB,GAEnB,AAEE,KADA,aAAa,CAAiB,EACV,MAEtB,AAGE,KAFA,EAAgB,IAAI,aAAa,iDAAkD,YAAY,CAAC,EAChG,EAAmB,KACD,MAGpB,EAAoB,EACb,EAAQ,EAAQ,CAAM,IAK3B,GAEF,EAAgB,IAAI,aAAa,uCAAwC,YAAY,CAAC,EAGjF,IAAI,SAAY,EAAS,IAAW,CAMzC,GALA,EAAkB,EAClB,EAAmB,EACnB,EAAkB,EAClB,EAAkB,EAEd,EAAO,QACT,OAAO,EAAQ,EAEjB,EAAO,iBAAiB,QAAS,CAAO,EAGxC,AACE,IAAoB,WAAW,SAAY,CACzC,EAAoB,KAEpB,IAAM,EAAc,EACd,EAAe,EACf,EAAc,EACd,EAAc,EAGpB,EAAkB,KAClB,EAAmB,KACnB,EAAkB,KAClB,EAAkB,KAClB,EAAY,oBAAoB,QAAS,CAAO,EAEhD,GAAI,CACF,EAAoB,KAAK,IAAI,EAE7B,EAAa,MADM,EAAQ,EAAa,CAAW,CAClC,CACnB,OAAS,EAAO,CACd,EAAY,CAAK,CACnB,CACF,EAAG,CAAa,CAEpB,CAAC,EACH,CACF"}
1
+ {"version":3,"file":"withThrottle.cjs","names":[],"sources":["../../../src/decorators/withThrottle/withThrottle.ts"],"sourcesContent":["interface ThrottleOptions {\n /** Интервал троттлинга в миллисекундах. По умолчанию 300 мс */\n limit?: number;\n}\n\n/**\n * Декоратор для создания дросселирующего (throttled) загрузчика данных, специально\n * адаптированный для использования совместно с `engine.resource`.\n *\n * Ограничивает частоту вызовов асинхронной функции `fetcher`, гарантируя выполнение\n * первого запроса мгновенно (Leading edge), а самого последнего запроса — на хвосте\n * временного интервала (Trailing edge) с автоматической отменой всех промежуточных вызовов.\n *\n * ### 🧠 Механика обработки вызовов (Две фазы):\n * 1. **Прямое выполнение (Leading edge):** Если окно блокировки закрыто (`remainingTime <= 0`),\n * запрос пробрасывается к исходному `fetcher` немедленно. Время последнего выполнения фиксируется.\n * 2. **Отложенное выполнение (Trailing edge):** Если вызов происходит внутри активного окна блокировки,\n * его параметры (`source`, `signal`, `resolve`, `reject`) запоминаются, перезаписывая предыдущие.\n * По истечении таймера выполнится только самый актуальный (последний) запрос.\n * Все промежуточные «перезаписанные» промисы отклоняются с ошибкой `AbortError`.\n *\n * @template S Тип входных данных (аргументов) для функции запроса.\n * @template T Тип данных, возвращаемых асинхронным `fetcher`-ом (разрешенное значение промиса).\n *\n * @param {(source: S, signal: AbortSignal) => Promise<T>} fetcher Оригинальная асинхронная функция запроса, принимающая параметры и сигнал отмены.\n * @param {ThrottleOptions} [options={}] Параметры конфигурации троттлинга.\n * @param {number} [options.limit=300] Минимальный интервал времени в миллисекундах между последовательными прямыми вызовами.\n *\n * @returns {(source: S, signal: AbortSignal) => Promise<T>} Возвращает обернутую функцию, возвращающую `Promise<T>` и полностью сохраняющую исходную сигнатуру типов.\n *\n * @example\n * ```typescript\n * import { withThrottle } from '@pravosleva/reactive-engine';\n *\n * const fetchUserData = async (userId: string, signal: AbortSignal) => {\n * const res = await fetch(`/api/users/${userId}`, { signal });\n * return res.json();\n * };\n *\n * // Ограничиваем частоту запросов профиля до 1 раза в 400мс\n * const throttledFetch = withThrottle(fetchUserData, { limit: 400 });\n *\n * // Интеграция с подсистемой ресурсов вашего ядра\n * const userResource = engine.resource({\n * fetcher: throttledFetch,\n * source: () => currentUserId.value\n * });\n * ```\n *\n * @abstract\n * ### 🚨 Интеграция с AbortSignal и защита от утечек памяти\n * Декоратор подписывается на событие отмены `abort` переданного сигнала. Если внешний ресурс\n * размонтируется или перезапрашивает данные в процессе ожидания внутри окна троттлинга:\n * - Активный таймер `setTimeout` незамедлительно сбрасывается.\n * - Ожидающий хвостовой промис переходит в состояние `rejected` с системной ошибкой `AbortError`.\n * - Все внутренние ссылки на контекст промиса (`resolve`, `reject`, `source`) принудительно зануляются (`null`),\n * освобождая память от скрытых замыканий и предотвращая появление эффекта «зависших» запросов.\n */\nexport const withThrottle = <S, T>(\n fetcher: (source: S, signal: AbortSignal) => Promise<T>,\n options: ThrottleOptions = {}\n) => {\n const limit = options.limit ?? 300\n\n let lastExecutionTime = 0\n let throttleTimeoutId: ReturnType<typeof setTimeout> | null = null\n\n // Храним параметры последнего \"заблокированного\" вызова, чтобы выполнить его на хвосте\n let lastSavedSource: S | null = null\n let lastSavedResolve: ((value: T | PromiseLike<T>) => void) | null = null\n let lastSavedReject: ((reason: any) => void) | null = null\n let lastSavedSignal: AbortSignal | null = null\n\n return (source: S, signal: AbortSignal): Promise<T> => {\n const now = Date.now()\n const remainingTime = limit - (now - lastExecutionTime)\n\n // Слушатель для мгновенной отмены ожидания, если ресурс размонтировался во время блокировки\n const onAbort = () => {\n if (throttleTimeoutId) {\n clearTimeout(throttleTimeoutId)\n throttleTimeoutId = null\n }\n if (lastSavedReject) {\n lastSavedReject(new DOMException('Aborted by resource signal during throttle', 'AbortError'))\n lastSavedResolve = null\n lastSavedReject = null\n }\n }\n\n // СЦЕНАРИЙ 1: Интервал блокировки истек — выполняем запрос мгновенно (Leading edge)\n if (remainingTime <= 0) {\n // Если висел отложенный хвостовой вызов, отменяем его, так как пришел более свежий прямой запрос\n if (throttleTimeoutId) {\n clearTimeout(throttleTimeoutId)\n throttleTimeoutId = null\n }\n if (lastSavedReject) {\n lastSavedReject(new DOMException('Aborted due to newer direct throttle execution', 'AbortError'))\n lastSavedResolve = null\n lastSavedReject = null\n }\n\n lastExecutionTime = now\n return fetcher(source, signal)\n }\n\n // СЦЕНАРИЙ 2: Мы находимся внутри интервала блокировки.\n // Запоминаем текущие параметры как самые актуальные для выполнения на хвосте (Trailing edge).\n if (lastSavedReject) {\n // Отклоняем предыдущий сохраненный хвостовой промис, так как данные уже устарели\n lastSavedReject(new DOMException('Aborted due to newer throttled value', 'AbortError'))\n }\n\n return new Promise<T>((resolve, reject) => {\n lastSavedSource = source\n lastSavedResolve = resolve\n lastSavedReject = reject\n lastSavedSignal = signal\n\n if (signal.aborted) {\n return onAbort()\n }\n signal.addEventListener('abort', onAbort)\n\n // Если таймер хвостового вызова еще не запущен, взводим его на остаток времени блокировки\n if (!throttleTimeoutId) {\n throttleTimeoutId = setTimeout(async () => {\n throttleTimeoutId = null\n\n const savedSource = lastSavedSource!\n const savedResolve = lastSavedResolve!\n const savedReject = lastSavedReject!\n const savedSignal = lastSavedSignal!\n\n // Очищаем ссылки перед асинхронным вызовом\n lastSavedSource = null\n lastSavedResolve = null\n lastSavedReject = null\n lastSavedSignal = null\n savedSignal.removeEventListener('abort', onAbort)\n\n try {\n lastExecutionTime = Date.now()\n const data = await fetcher(savedSource, savedSignal)\n savedResolve(data)\n } catch (error) {\n savedReject(error)\n }\n }, remainingTime)\n }\n })\n }\n}\n"],"mappings":"AA0DA,IAAa,GACX,EACA,EAA2B,CAAC,IACzB,CACH,IAAM,EAAQ,EAAQ,OAAS,IAE3B,EAAoB,EACpB,EAA0D,KAG1D,EAA4B,KAC5B,EAAiE,KACjE,EAAkD,KAClD,EAAsC,KAE1C,OAAQ,EAAW,IAAoC,CACrD,IAAM,EAAM,KAAK,IAAI,EACf,EAAgB,GAAS,EAAM,GAG/B,MAAgB,CACpB,AAEE,KADA,aAAa,CAAiB,EACV,MAEtB,AAGE,KAFA,EAAgB,IAAI,aAAa,6CAA8C,YAAY,CAAC,EAC5F,EAAmB,KACD,KAEtB,EA0BA,OAvBI,GAAiB,GAEnB,AAEE,KADA,aAAa,CAAiB,EACV,MAEtB,AAGE,KAFA,EAAgB,IAAI,aAAa,iDAAkD,YAAY,CAAC,EAChG,EAAmB,KACD,MAGpB,EAAoB,EACb,EAAQ,EAAQ,CAAM,IAK3B,GAEF,EAAgB,IAAI,aAAa,uCAAwC,YAAY,CAAC,EAGjF,IAAI,SAAY,EAAS,IAAW,CAMzC,GALA,EAAkB,EAClB,EAAmB,EACnB,EAAkB,EAClB,EAAkB,EAEd,EAAO,QACT,OAAO,EAAQ,EAEjB,EAAO,iBAAiB,QAAS,CAAO,EAGxC,AACE,IAAoB,WAAW,SAAY,CACzC,EAAoB,KAEpB,IAAM,EAAc,EACd,EAAe,EACf,EAAc,EACd,EAAc,EAGpB,EAAkB,KAClB,EAAmB,KACnB,EAAkB,KAClB,EAAkB,KAClB,EAAY,oBAAoB,QAAS,CAAO,EAEhD,GAAI,CACF,EAAoB,KAAK,IAAI,EAE7B,EAAa,MADM,EAAQ,EAAa,CAAW,CAClC,CACnB,OAAS,EAAO,CACd,EAAY,CAAK,CACnB,CACF,EAAG,CAAa,CAEpB,CAAC,EACH,CACF"}
@@ -3,9 +3,57 @@ interface ThrottleOptions {
3
3
  limit?: number;
4
4
  }
5
5
  /**
6
- * Декоратор для создания троттлящего fetcher-а для engine.resource
7
- * @param fetcher Оригинальная асинхронная функция запроса
8
- * @param options Настройки троттлинга
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;;;;GAIG;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
+ {"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 * Декоратор для создания троттлящего fetcher-а для engine.resource\n * @param fetcher Оригинальная асинхронная функция запроса\n * @param options Настройки троттлинга\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":";AAUA,IAAa,KACX,GACA,IAA2B,CAAC,MACzB;CACH,IAAM,IAAQ,EAAQ,SAAS,KAE3B,IAAoB,GACpB,IAA0D,MAG1D,IAA4B,MAC5B,IAAiE,MACjE,IAAkD,MAClD,IAAsC;CAE1C,QAAQ,GAAW,MAAoC;EACrD,IAAM,IAAM,KAAK,IAAI,GACf,IAAgB,KAAS,IAAM,IAG/B,UAAgB;GAKpB,AAJA,AAEE,OADA,aAAa,CAAiB,GACV,OAEtB,AAGE,OAFA,EAAgB,IAAI,aAAa,8CAA8C,YAAY,CAAC,GAC5F,IAAmB,MACD;EAEtB;EA0BA,OAvBI,KAAiB,KAEnB,AAEE,OADA,aAAa,CAAiB,GACV,OAEtB,AAGE,OAFA,EAAgB,IAAI,aAAa,kDAAkD,YAAY,CAAC,GAChG,IAAmB,MACD,OAGpB,IAAoB,GACb,EAAQ,GAAQ,CAAM,MAK3B,KAEF,EAAgB,IAAI,aAAa,wCAAwC,YAAY,CAAC,GAGjF,IAAI,SAAY,GAAS,MAAW;GAMzC,IALA,IAAkB,GAClB,IAAmB,GACnB,IAAkB,GAClB,IAAkB,GAEd,EAAO,SACT,OAAO,EAAQ;GAKjB,AAHA,EAAO,iBAAiB,SAAS,CAAO,GAGxC,AACE,MAAoB,WAAW,YAAY;IACzC,IAAoB;IAEpB,IAAM,IAAc,GACd,IAAe,GACf,IAAc,GACd,IAAc;IAOpB,AAJA,IAAkB,MAClB,IAAmB,MACnB,IAAkB,MAClB,IAAkB,MAClB,EAAY,oBAAoB,SAAS,CAAO;IAEhD,IAAI;KAGF,AAFA,IAAoB,KAAK,IAAI,GAE7B,EAAa,MADM,EAAQ,GAAa,CAAW,CAClC;IACnB,SAAS,GAAO;KACd,EAAY,CAAK;IACnB;GACF,GAAG,CAAa;EAEpB,CAAC;CACH;AACF"}
1
+ {"version":3,"file":"withThrottle.mjs","names":[],"sources":["../../../src/decorators/withThrottle/withThrottle.ts"],"sourcesContent":["interface ThrottleOptions {\n /** Интервал троттлинга в миллисекундах. По умолчанию 300 мс */\n limit?: number;\n}\n\n/**\n * Декоратор для создания дросселирующего (throttled) загрузчика данных, специально\n * адаптированный для использования совместно с `engine.resource`.\n *\n * Ограничивает частоту вызовов асинхронной функции `fetcher`, гарантируя выполнение\n * первого запроса мгновенно (Leading edge), а самого последнего запроса — на хвосте\n * временного интервала (Trailing edge) с автоматической отменой всех промежуточных вызовов.\n *\n * ### 🧠 Механика обработки вызовов (Две фазы):\n * 1. **Прямое выполнение (Leading edge):** Если окно блокировки закрыто (`remainingTime <= 0`),\n * запрос пробрасывается к исходному `fetcher` немедленно. Время последнего выполнения фиксируется.\n * 2. **Отложенное выполнение (Trailing edge):** Если вызов происходит внутри активного окна блокировки,\n * его параметры (`source`, `signal`, `resolve`, `reject`) запоминаются, перезаписывая предыдущие.\n * По истечении таймера выполнится только самый актуальный (последний) запрос.\n * Все промежуточные «перезаписанные» промисы отклоняются с ошибкой `AbortError`.\n *\n * @template S Тип входных данных (аргументов) для функции запроса.\n * @template T Тип данных, возвращаемых асинхронным `fetcher`-ом (разрешенное значение промиса).\n *\n * @param {(source: S, signal: AbortSignal) => Promise<T>} fetcher Оригинальная асинхронная функция запроса, принимающая параметры и сигнал отмены.\n * @param {ThrottleOptions} [options={}] Параметры конфигурации троттлинга.\n * @param {number} [options.limit=300] Минимальный интервал времени в миллисекундах между последовательными прямыми вызовами.\n *\n * @returns {(source: S, signal: AbortSignal) => Promise<T>} Возвращает обернутую функцию, возвращающую `Promise<T>` и полностью сохраняющую исходную сигнатуру типов.\n *\n * @example\n * ```typescript\n * import { withThrottle } from '@pravosleva/reactive-engine';\n *\n * const fetchUserData = async (userId: string, signal: AbortSignal) => {\n * const res = await fetch(`/api/users/${userId}`, { signal });\n * return res.json();\n * };\n *\n * // Ограничиваем частоту запросов профиля до 1 раза в 400мс\n * const throttledFetch = withThrottle(fetchUserData, { limit: 400 });\n *\n * // Интеграция с подсистемой ресурсов вашего ядра\n * const userResource = engine.resource({\n * fetcher: throttledFetch,\n * source: () => currentUserId.value\n * });\n * ```\n *\n * @abstract\n * ### 🚨 Интеграция с AbortSignal и защита от утечек памяти\n * Декоратор подписывается на событие отмены `abort` переданного сигнала. Если внешний ресурс\n * размонтируется или перезапрашивает данные в процессе ожидания внутри окна троттлинга:\n * - Активный таймер `setTimeout` незамедлительно сбрасывается.\n * - Ожидающий хвостовой промис переходит в состояние `rejected` с системной ошибкой `AbortError`.\n * - Все внутренние ссылки на контекст промиса (`resolve`, `reject`, `source`) принудительно зануляются (`null`),\n * освобождая память от скрытых замыканий и предотвращая появление эффекта «зависших» запросов.\n */\nexport const withThrottle = <S, T>(\n fetcher: (source: S, signal: AbortSignal) => Promise<T>,\n options: ThrottleOptions = {}\n) => {\n const limit = options.limit ?? 300\n\n let lastExecutionTime = 0\n let throttleTimeoutId: ReturnType<typeof setTimeout> | null = null\n\n // Храним параметры последнего \"заблокированного\" вызова, чтобы выполнить его на хвосте\n let lastSavedSource: S | null = null\n let lastSavedResolve: ((value: T | PromiseLike<T>) => void) | null = null\n let lastSavedReject: ((reason: any) => void) | null = null\n let lastSavedSignal: AbortSignal | null = null\n\n return (source: S, signal: AbortSignal): Promise<T> => {\n const now = Date.now()\n const remainingTime = limit - (now - lastExecutionTime)\n\n // Слушатель для мгновенной отмены ожидания, если ресурс размонтировался во время блокировки\n const onAbort = () => {\n if (throttleTimeoutId) {\n clearTimeout(throttleTimeoutId)\n throttleTimeoutId = null\n }\n if (lastSavedReject) {\n lastSavedReject(new DOMException('Aborted by resource signal during throttle', 'AbortError'))\n lastSavedResolve = null\n lastSavedReject = null\n }\n }\n\n // СЦЕНАРИЙ 1: Интервал блокировки истек — выполняем запрос мгновенно (Leading edge)\n if (remainingTime <= 0) {\n // Если висел отложенный хвостовой вызов, отменяем его, так как пришел более свежий прямой запрос\n if (throttleTimeoutId) {\n clearTimeout(throttleTimeoutId)\n throttleTimeoutId = null\n }\n if (lastSavedReject) {\n lastSavedReject(new DOMException('Aborted due to newer direct throttle execution', 'AbortError'))\n lastSavedResolve = null\n lastSavedReject = null\n }\n\n lastExecutionTime = now\n return fetcher(source, signal)\n }\n\n // СЦЕНАРИЙ 2: Мы находимся внутри интервала блокировки.\n // Запоминаем текущие параметры как самые актуальные для выполнения на хвосте (Trailing edge).\n if (lastSavedReject) {\n // Отклоняем предыдущий сохраненный хвостовой промис, так как данные уже устарели\n lastSavedReject(new DOMException('Aborted due to newer throttled value', 'AbortError'))\n }\n\n return new Promise<T>((resolve, reject) => {\n lastSavedSource = source\n lastSavedResolve = resolve\n lastSavedReject = reject\n lastSavedSignal = signal\n\n if (signal.aborted) {\n return onAbort()\n }\n signal.addEventListener('abort', onAbort)\n\n // Если таймер хвостового вызова еще не запущен, взводим его на остаток времени блокировки\n if (!throttleTimeoutId) {\n throttleTimeoutId = setTimeout(async () => {\n throttleTimeoutId = null\n\n const savedSource = lastSavedSource!\n const savedResolve = lastSavedResolve!\n const savedReject = lastSavedReject!\n const savedSignal = lastSavedSignal!\n\n // Очищаем ссылки перед асинхронным вызовом\n lastSavedSource = null\n lastSavedResolve = null\n lastSavedReject = null\n lastSavedSignal = null\n savedSignal.removeEventListener('abort', onAbort)\n\n try {\n lastExecutionTime = Date.now()\n const data = await fetcher(savedSource, savedSignal)\n savedResolve(data)\n } catch (error) {\n savedReject(error)\n }\n }, remainingTime)\n }\n })\n }\n}\n"],"mappings":";AA0DA,IAAa,KACX,GACA,IAA2B,CAAC,MACzB;CACH,IAAM,IAAQ,EAAQ,SAAS,KAE3B,IAAoB,GACpB,IAA0D,MAG1D,IAA4B,MAC5B,IAAiE,MACjE,IAAkD,MAClD,IAAsC;CAE1C,QAAQ,GAAW,MAAoC;EACrD,IAAM,IAAM,KAAK,IAAI,GACf,IAAgB,KAAS,IAAM,IAG/B,UAAgB;GAKpB,AAJA,AAEE,OADA,aAAa,CAAiB,GACV,OAEtB,AAGE,OAFA,EAAgB,IAAI,aAAa,8CAA8C,YAAY,CAAC,GAC5F,IAAmB,MACD;EAEtB;EA0BA,OAvBI,KAAiB,KAEnB,AAEE,OADA,aAAa,CAAiB,GACV,OAEtB,AAGE,OAFA,EAAgB,IAAI,aAAa,kDAAkD,YAAY,CAAC,GAChG,IAAmB,MACD,OAGpB,IAAoB,GACb,EAAQ,GAAQ,CAAM,MAK3B,KAEF,EAAgB,IAAI,aAAa,wCAAwC,YAAY,CAAC,GAGjF,IAAI,SAAY,GAAS,MAAW;GAMzC,IALA,IAAkB,GAClB,IAAmB,GACnB,IAAkB,GAClB,IAAkB,GAEd,EAAO,SACT,OAAO,EAAQ;GAKjB,AAHA,EAAO,iBAAiB,SAAS,CAAO,GAGxC,AACE,MAAoB,WAAW,YAAY;IACzC,IAAoB;IAEpB,IAAM,IAAc,GACd,IAAe,GACf,IAAc,GACd,IAAc;IAOpB,AAJA,IAAkB,MAClB,IAAmB,MACnB,IAAkB,MAClB,IAAkB,MAClB,EAAY,oBAAoB,SAAS,CAAO;IAEhD,IAAI;KAGF,AAFA,IAAoB,KAAK,IAAI,GAE7B,EAAa,MADM,EAAQ,GAAa,CAAW,CAClC;IACnB,SAAS,GAAO;KACd,EAAY,CAAK;IACnB;GACF,GAAG,CAAa;EAEpB,CAAC;CACH;AACF"}
@@ -1 +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 * Комбинированный декоратор: сначала применяет троттлинг к частоте вызовов,\n * а затем проверяет кэш перед отправкой реального fetcher-запроса.\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":"AAgBA,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"}
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
- * а затем проверяет кэш перед отправкой реального fetcher-запроса.
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;;;GAGG;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
+ {"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 * Комбинированный декоратор: сначала применяет троттлинг к частоте вызовов,\n * а затем проверяет кэш перед отправкой реального fetcher-запроса.\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":";AAgBA,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
+ {"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":"AAMA,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"}
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":";AAMA,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"}
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"}
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./core.cjs"),t=require("./AbstractService.cjs"),n=require("./BaseREServiceEnhanced.cjs"),r=require("./decorators/withCache/withCache.cjs"),i=require("./decorators/withDebounce/withDebounce.cjs"),a=require("./decorators/withLongPolling/withLongPolling.cjs"),o=require("./decorators/withThrottle/withThrottle.cjs"),s=require("./decorators/withThrottleComputed/withThrottleComputed.cjs"),c=require("./decorators/withThrottleAndCache/withThrottleAndCache.cjs");exports.AbstractService=t.AbstractService,exports.BaseREService=t.BaseREService,exports.BaseREServiceEnhanced=n.BaseREServiceEnhanced,exports.ReactiveEngine=e.ReactiveEngine,exports.withCache=r.withCache,exports.withDebounce=i.withDebounce,exports.withLongPolling=a.withLongPolling,exports.withThrottle=o.withThrottle,exports.withThrottleAndCache=c.withThrottleAndCache,exports.withThrottleComputed=s.withThrottleComputed;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./core.cjs"),t=require("./AbstractService.cjs"),n=require("./BaseREServiceEnhanced.cjs"),r=require("./decorators/withCache/withCache.cjs"),i=require("./decorators/withDebounce/withDebounce.cjs"),a=require("./decorators/withLongPolling/withLongPolling.cjs"),o=require("./decorators/withThrottle/withThrottle.cjs"),s=require("./decorators/withThrottleComputed/withThrottleComputed.cjs"),c=require("./decorators/withThrottleAndCache/withThrottleAndCache.cjs"),l=require("./decorators/withStaleWhileRevalidate/withStaleWhileRevalidate.cjs");exports.AbstractService=t.AbstractService,exports.BaseREService=t.BaseREService,exports.BaseREServiceEnhanced=n.BaseREServiceEnhanced,exports.ReactiveEngine=e.ReactiveEngine,exports.withCache=r.withCache,exports.withDebounce=i.withDebounce,exports.withLongPolling=a.withLongPolling,exports.withStaleWhileRevalidate=l.withStaleWhileRevalidate,exports.withThrottle=o.withThrottle,exports.withThrottleAndCache=c.withThrottleAndCache,exports.withThrottleComputed=s.withThrottleComputed;
package/dist/index.mjs CHANGED
@@ -7,4 +7,5 @@ import { withLongPolling as o } from "./decorators/withLongPolling/withLongPolli
7
7
  import { withThrottle as s } from "./decorators/withThrottle/withThrottle.mjs";
8
8
  import { withThrottleComputed as c } from "./decorators/withThrottleComputed/withThrottleComputed.mjs";
9
9
  import { withThrottleAndCache as l } from "./decorators/withThrottleAndCache/withThrottleAndCache.mjs";
10
- export { t as AbstractService, n as BaseREService, r as BaseREServiceEnhanced, e as ReactiveEngine, i as withCache, a as withDebounce, o as withLongPolling, s as withThrottle, l as withThrottleAndCache, c as withThrottleComputed };
10
+ import { withStaleWhileRevalidate as u } from "./decorators/withStaleWhileRevalidate/withStaleWhileRevalidate.mjs";
11
+ export { t as AbstractService, n as BaseREService, r as BaseREServiceEnhanced, e as ReactiveEngine, i as withCache, a as withDebounce, o as withLongPolling, u as withStaleWhileRevalidate, s as withThrottle, l as withThrottleAndCache, c as withThrottleComputed };
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * @pravosleva/reactive-engine v1.5.7-beta
2
+ * @pravosleva/reactive-engine v1.5.9-beta
3
3
  * High-performance transactional reactive engine with microtask batching.
4
4
  *
5
5
  * @license MIT
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" style="height: 512px; width: 512px;"><circle cx="256" cy="256" r="256" fill="#000" fill-opacity="1"></circle><g class="" transform="translate(0,0)" style=""><path d="M36.078 16 16 35.55v50.247L87.684 16H36.078zm121.83 0-97.834 95.26c8.838-.48 17.72-.946 27.207-1.367L183.71 16h-25.8zm107.768 0L217.72 62.693c11.73-7.19 24.684-14.537 37.987-21.455 17.973-9.346 36.334-17.656 52.807-23.023 2.466-.804 4.865-1.53 7.234-2.215h-50.072zm105.627 0-4.29 3.432c56.944 23.005 97.62 63.682 120.628 120.627l8.16-10.2C475.674 75.84 440.51 38.356 388.93 16H371.3zm55.894 0C455.86 34.214 478.884 58.318 496 88.027V63.582C484.442 45.14 469.57 29.234 451.488 16h-24.29zm52.176 0A192.31 192.31 0 0 1 496 33.447V16h-16.627zM341.316 30.266c-6.124-.108-15.98 1.4-27.226 5.064-14.994 4.886-32.696 12.84-50.078 21.88a519.558 519.558 0 0 0-18.948 10.384c27.085 5.188 54.39 17.866 79.737 35.3l69.516-49.654c-14.42-8.99-30.465-16.435-48.207-22.35-1.126-.374-2.752-.588-4.794-.624zm68.713 33.87-69.99 49.995a317.653 317.653 0 0 1 28.007 24.895 317.684 317.684 0 0 1 24.902 28.002l49.988-69.986a181.614 181.614 0 0 0-32.91-32.905zm-193.41 18.08c-19.394.162-36.698 5.932-49.39 18.626a58.492 58.492 0 0 0-6.992 8.447c7.737.345 15.336.865 22.698 1.606 8.6-7.025 20.286-10.66 34.26-10.71 9.396-.034 19.82 1.55 31.025 4.802 29.88 8.675 64.076 29.196 94.372 59.492 30.296 30.297 50.82 64.496 59.494 94.374 8.674 29.877 5.474 54.205-8.582 68.26a40.523 40.523 0 0 1-7.076 5.63 234.492 234.492 0 0 1-5.32 22.106c9.518-3 18.04-7.923 25.124-15.006 20.31-20.31 22.893-52.42 13.14-86.01-9.36-32.248-30.12-66.993-60.245-98.2l-.09-.093-.057-.056a307.882 307.882 0 0 0-3.66-3.73h-.002a308.138 308.138 0 0 0-3.726-3.66l-.06-.06a2.16 2.16 0 0 1-.1-.093c-31.205-30.12-65.946-50.88-98.192-60.24-12.596-3.658-24.985-5.58-36.62-5.483zM453.83 112.76l-49.652 69.515c17.437 25.354 30.1 52.67 35.285 79.762a519.383 519.383 0 0 0 10.4-18.974c9.04-17.382 16.992-35.084 21.877-50.078 4.886-14.993 5.94-27.516 4.44-32.017-5.914-17.742-13.36-33.785-22.35-48.207zm-315.766 14.306c-24.424-.103-49.52 1.074-72.78 2.217 7.69 2.09 14.93 3.963 22.872 6.343 29.69 8.9 59.69 19.512 74.75 40.217l12.465 17.137-20.988-2.93c-45.902-6.41-74.37 5.044-101.47 15.693 33.77 7.288 58.506 15.248 87.683 36.73l13.96 10.278-16.44 5.504C78.56 278.195 47.45 315.047 16 357.834v22.143c9.58-5.07 19.283-10.596 29.057-15.934 27.153-14.832 55.85-28.346 85.502-19.572l7.84 2.32-1.56 8.028c-6.803 35.01-28.027 91.445-48.008 141.182h15.455c44.07-50.768 93.33-113.807 108.994-146.596l7.16-14.984 8.644 14.178c5.784 9.484 3.667 19.766.973 31.724-2.694 11.958-7.28 25.69-12.178 39.88-6.224 18.024-12.536 36.71-16.552 52.19 12.538-12.48 25.598-29.23 36.842-46.912 16.8-26.42 29.886-55.1 33.586-72.515l5.015-23.613 11.668 21.134c12.356 22.385 10.967 52.3 6.996 84.723-2.37 19.346-5.82 39.638-8.854 59.653 14.87-22.31 32.57-48.36 48.307-77.36 24.843-45.79 43.262-96.883 34.79-143.876-2.723-15.107-13.146-34.003-28.327-52.83-21.165 6.466-42.44 20.892-62.875 40.822 17.09-29.95 23.5-49.964 37.183-65.225-24.578 10.682-57.176 29.572-91.263 62.765 19.654-25.258 40.095-64.07 53.935-91.478-20.372 15.64-51.45 31.377-71.21 43.81 9.8-15.11 30.47-43.68 36.517-66.957-10.983-4.976-21.758-8.688-31.87-10.765l-.148-.032-.15-.035c-20.223-4.873-46.176-6.523-73.404-6.64zM31.73 138.86 16 154.177V179.3l35.928-34.984c-7.012-1.842-13.946-3.663-20.2-5.455zm82.047 25.04-8.13 7.916a179.563 179.563 0 0 1 21.603-1.504c-4.143-2.217-8.685-4.355-13.473-6.412zM52.14 223.916 16 259.106v50.244l75.926-73.928C79.87 230.577 67.19 227.2 52.14 223.916zM496 287.72 346.97 432.83c-13.02 23.235-27.113 44.683-39.44 63.172h26.163L496 337.965V287.72zm-384.45 72.075c-3.92 0-7.91.426-11.972 1.203L16 442.378V467.5l97.834-95.258c1.202-4.276 2.285-8.36 3.193-12.162a51.858 51.858 0 0 0-5.476-.285zM496 368.81 365.373 496h25.8L496 393.934V368.81zm-295.277 45.924-27.32 26.602c-14.17 18.31-29.498 36.993-44.624 54.664h14.282l44.74-43.56c3.907-12.385 8.606-25.225 12.92-37.706z" fill="#fff" fill-opacity="1"></path></g></svg>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pravosleva/reactive-engine",
3
- "version": "1.5.7-beta",
3
+ "version": "1.5.9-beta",
4
4
  "engines": {
5
5
  "node": ">=22.20.0"
6
6
  },
package/dist/vite.svg DELETED
@@ -1 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>