Skip to content

withThrottleComputed (для синхронных вычислений)

Применяется на входе в реактивный граф для изоляции сырого спама данных.

  • Когда использовать: Когда входной сигнал генерирует сотни событий в секунду (координаты мыши, пиксели скролла), и вам нужно «сжать» этот поток до дозированных тиков (например, раз в 300 мс) перед тем, как пустить его дальше в другие сервисы, computed-свойства или тяжелые UI-компоненты.
  • Как это работает: Он перехватывает чтение сырого сигнала, подписывается на него, но обновляет результирующий стейт строго по таймеру, отсекая промежуточный дребезг.
typescript
import { AbstractService, withThrottleComputed } from '@pravosleva/reactive-engine'

export class Throttle2DLogic extends AbstractService {
  // ...

  // Пример: Троттлинг мышиного трекинга перед расчетом тяжелой гео-модели
  public rawCoords = this.engine.signal({ x: 0, y: 0 }, 'signal:raw-coords')

  // Зажимаем спам координат на этапе вычислений
  public throttledCoords = withThrottleComputed(
    this.engine,
    () => this.rawCoords.value,
    { limit: 300 },
    'computed:throttled-coords'
  )
}

Пример 212

Ниже приведен готовый пример использования сервиса:

ts
import { AbstractService, withThrottleComputed } from '@pravosleva/reactive-engine'

export class Throttle2DLogic extends AbstractService {
  // Сигнал, куда записываются сырые координаты X и Y при движении мыши
  public coordsSignal = this.createSignal<{ x: number; y: number }>({ x: 0, y: 0 }, 'example-212:signal:coords')

  // 1. Сырой сигнал мыши (спамит на каждый пиксель)
  public rawCoords = this.engine.signal({ x: 0, y: 0 }, 'example-212:signal:raw-coords')

  /**
   * 2.1 🚫 Альтернативный вариант (он тоже будет работать)
   * ☝️ НО НЕ для этого случая, когда оригинальный синал генерирует сотни изменений в секунду (спам),
   * т.к. чтение сырого сигнала требует неоправдано больше ресурсов для этого случая.
   * Реактивный ресурс, обёрнутый в декоратор withThrottle.
   * Он считывает координаты и выполняет "тяжёлый" фейковый расчёт зон.
   * Троттлинг гарантирует частоту выполнения не чаще 1 раза в 300 мс.
   */
  // public analyticsResource = this.engine.resource(
  //   withThrottle(
  //     async (coords, _abortSignal) => {
  //       // Имитируем небольшую задержку расчёта (например, обращение к гео-модели)
  //       await new Promise((resolve) => setTimeout(resolve, 100))
  //       const sector = coords.x < 200 ? 'Левый сектор' : 'Правый сектор'
  //       return `Аналитика GPU: [${sector}] для точки X: ${coords.x}, Y: ${coords.y}`
  //     },
  //     { limit: 300 } // Лимит троттлинга 300 мс
  //   ),
  //   this.coordsSignal,
  //   'example-212:resource:analytics'
  // )

  /**
   * 2.2 ✅ Ресурс слушает уже ЗАТРOТТЛЕННЫЙ сигнал.
   * Он перейдет в статус loading ровно 1 раз в 300мс! Счетчик изменений будет равен 1!
   */
  // Вычисляемый затроттленный сигнал (тикает строго 1 раз в 300мс)
  public throttledCoords = withThrottleComputed(
    this.engine,
    () => this.coordsSignal.value,
    { limit: 300 },
    '[IS_OPTIMIZED=1]:example-212:computed:throttled-coords'
  )
  public analyticsResource = this.engine.resource(
    async (coords) => {
      await new Promise((resolve) => setTimeout(resolve, 100))
      const sector = coords.x < 200 ? 'Левый сектор' : 'Правый сектор'
      return `Аналитика GPU: [${sector}] для точки X: ${coords.x}, Y: ${coords.y}`
    },
    this.throttledCoords,
    'example-212:resource:analytics'
  )

  /**
   * Экшен обновления координат из UI
   */
  public updateCoords(x: number, y: number) {
    this.coordsSignal.value = { x, y }
  }
}
tsx
import { MouseEvent } from 'react'
import { ReactiveEngine, useReactiveValue } from '@pravosleva/reactive-engine/react'
import { Throttle2DLogic } from './service.Throttle2DLogic'
import baseClasses from '~/ui.common.module.scss'
import clsx from 'clsx'

const engine = new ReactiveEngine({
  logger: {
    isEnabled: true, // Включаем логгер
    traceTime: true, // Добавляем вывод таймингов по желанию
    filter: /^example-212:*/ // Можно фильтровать только нужные логи
  }
})

export const Throttle2DExample = () => {
  const logic = engine.inject(Throttle2DLogic)

  // Подписываемся на сырой сигнал координат и обработанный ресурс аналитики
  // -- Будьте внимательны: Оцените разницу на что подписаться!
  // ⛔ const coords = engine.use(logic.coordsSignal)
  const coords = engine.use(logic.throttledCoords)
  // --
  const { loading, data: analyticsResult } = useReactiveValue(logic.analyticsResource)

  // Перехват движения мыши внутри зоны
  const handleMouseMove = (e: MouseEvent<HTMLDivElement>) => {
    const rect = e.currentTarget.getBoundingClientRect()
    const x = Math.round(e.clientX - rect.left)
    const y = Math.round(e.clientY - rect.top)

    // Спамим изменения в сигнал на каждый пиксель движения
    logic.updateCoords(x, y)
  }

  return (
    <div
      className={clsx(baseClasses.unit, baseClasses.stack2)}
      style={{ fontFamily: 'system-ui', width: '600px' }}
    >
      <div className={baseClasses.absoluteUnitLabel}>Throttle Computed Mouse Tracking Demo</div>

      <div className={baseClasses.stack1}>
        {/* Индикаторы текущего состояния */}
        <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 'small' }}>
          <span>Сырые координаты из сигнала:</span>
          <span style={{ fontFamily: 'monospace', color: '#00b4d8' }}>X: {coords.x}, Y: {coords.y}</span>
        </div>

        {/* Интерактивная зона для вождения мышкой */}
        <div
          onMouseMove={handleMouseMove}
          style={{ width: '100%', height: '180px', background: '#15151a', borderRadius: '8px', cursor: 'crosshair', display: 'flex', alignItems: 'center', justifyContent: 'center', userSelect: 'none' }}
        >
          <span style={{ color: '#aaa', fontSize: '13px' }}>Двигайте курсор внутри этой зоны</span>
        </div>
      </div>

      {/* Терминал вывода затроттленной аналитики */}
      <div className={baseClasses.stack1}>
        <div style={{ fontSize: 'small', display: 'flex', justifyContent: 'space-between', gap: '8px' }}>
          <span>Результат обработки (не чаще 1 раза в 300мс):</span>
          {loading && <span style={{ color: '#e6af2e' }}>⏳ Расчёт...</span>}
        </div>
        <div style={{ background: '#111', borderRadius: '8px', padding: '12px', minHeight: '44px', display: 'flex', alignItems: 'center', fontSize: '13px', fontFamily: 'monospace', color: '#4caf50' }}>
          {analyticsResult || <span style={{ color: '#aaa' }}>Запустите движение мыши...</span>}
        </div>
      </div>
    </div>
  )
}