Тесты на семантику графа (graph semantics tests) для реактивного движка проверяют, насколько правильно и предсказуемо движок обрабатывает сложные связи, зависимости и логику обновления данных внутри графа.

Если говорить простыми словами, @pravosleva/reactive-engine строит «карту» (граф) зависимостей, где переменные и функции связаны друг с другом. Когда меняется один элемент, движок должен автоматически и в правильном порядке обновить все остальные. Тесты на семантику проверяют, что эта система работает логически безупречно в самых запутанных ситуациях.

Такой результат выводит движок на уровень высокой production-готовности. Оставшиеся 2 невыполненных теста — это редкие пограничные сценарии (edge cases), которые уже локализованы и находятся в бэклоге разработки. Открытая демонстрация этих метрик подчеркивает прозрачность проекта и честность перед open-source сообществом, давая разработчикам уверенность в предсказуемости поведения системы при решении сложных задач.

Бескомпромиссная надежность: 176 из 178

Важнейшим этапом в развитии @pravosleva/reactive-engine стало тестирование на семантику графа зависимостей. Движок успешно проходит 176 из 178 специализированных тестов (более 98% стабильности). Это доказывает, что архитектура библиотеки изначально спроектирована верно: она эффективно решает классические проблемы реактивного программирования, такие как «проблема ромба» (diamond problem), корректно обрабатывает динамическое изменение связей в реальном времени и защищает приложение от избыточных вычислений.

Архитектура библиотеки блестяще справляется с ключевыми вызовами реактивного программирования: она полностью изолирована от «проблемы ромба» (diamond problem), гарантирует строгий топологический порядок выполнения, корректно управляет динамическими зависимостями и оптимизирует вычисления за счет интеллектуального кэширования. По общему числу успешно пройденных тестов семантики движок занял второе место из 16, обогнав такие зрелые экосистемные решения, как @preact/signals-core, @reatom/core, @vue/reactivity и solid-js. Такое достижение уверенно выводит @pravosleva/reactive-engine на уровень высокой production-готовности для построения сложных интерфейсов и систем управления состоянием.

Проблема ромба

Проблема ромба (Diamond Problem) в реактивном программировании — это ситуация, когда обновление одной переменной приводит к тому, что зависимая от неё функция пересчитывается дважды вместо одного раза, причем на первом шаге она получает некорректное (устаревшее) промежуточное значение.

      [ A ]  (изменился)
     /     \
  [ Б ]   [ В ]  (зависят от А)
     \     /
      [ Г ]  (зависит от Б и В)
  1. А — базовый сигнал (например, число).
  2. Б зависит от А (например, A × 2).
  3. В зависит от А (например, A + 10).
  4. Г зависит и от Б, и от В (например, Б + В).

В чем именно заключается «глюк» (Glitch)?

Пусть изначально A = 1. Тогда:

  • Б = 1 × 2 = 2
  • В = 1 + 10 = 11
  • Г = 2 + 11 = 13

Теперь мы меняем значение А на 2.

❌ Поведение наивного (плохого) движка:

  1. А изменилось на 2. Движок видит, что от А зависят Б и В, и начинает их обновлять.
  2. Движок обновляет Б. Теперь Б = 4.
  3. Движок сразу же видит, что Б изменился, а от Б зависит Г. Он тут же пересчитывает Г:
Г = Б (новое 4) + В (старое 11) = 15

Это первый рендер/расчет Г (значение неверное!). 4. Движок возвращается к исходной очереди и обновляет В. Теперь В = 12. 5. Движок видит, что В изменился, и снова пересчитывает Г:

Г = Б (новое 4) + В (новое 12) = 16

Это второй расчет Г (значение правильное).

Итог: Узел Г обновился два раза. На шаге №3 в системе на мгновение появилось «глючное» значение 15, которого в природе существовать не должно. Если к узлу Г привязан тяжелый сетевой запрос или рендер интерфейса, пользователь увидит мигание некорректных данных, а приложение выполнит лишнюю холостую работу.

Как эту проблему решает @pravosleva/reactive-engine?

Правильные реактивные движки используют топологическую сортировку графа (Topological Sort) и механизм батчинга (batching/транзакций).

✅ Поведение правильного движка:

  1. Когда А меняется, движок строит правильный порядок обновления элементов на основе их "глубины" в графе: [А] -> [Б, В] -> [Г].
  2. Движок обновляет Б (4) и В (12), но замораживает (маркирует как грязный / dirty) узел Г.
  3. Только когда все родители (Б и В) полностью завершили свои обновления, движок один-единственный раз выполняет расчет для Г, который сразу же получает актуальные данные: 4 + 12 = 16.

В результатах тестов, о которых мы говорили ранее, успешное прохождение бенчмарков доказывает, что @pravosleva/reactive-engine гарантирует 100% защиту от проблемы ромба. Граф вычисляется строго в топологическом порядке, без «мигания» промежуточных состояний и без лишних рендеров компонентов.

Анализ пограничных сценариев (Edge Cases)

Два невыполненных теста локализованы в категории Inner Write (запись в сигналы изнутри реактивного контекста). Они представляют собой специфические сценарии, деструктурированные в таблице ниже:

ID ТестаНазвание и описание сценарияПоведение на практике / Риски
#179computed self-increment: intra-run read-after-write values correct

Вычисляемое свойство (computed) при каждом чтении инкрементирует свой собственный базовый сигнал и должно сразу же вернуть актуализированное состояние.
Антипаттерн в архитектуре.

В реальной разработке мутации внутри computed строго не рекомендуются, так как вычисляемые свойства по стандарту должны оставаться чистыми функциями без побочных эффектов.
#186effect observes computed side-channel write during propagation

Эффект одновременно наблюдает за computed и за сторонним сигналом, в который это же вычисляемое свойство осуществляет скрытую запись во время расчета графа.
Скрытые побочные каналы данных.

Сценарий воспроизводится, если разработчик пытается реализовать сложную двустороннюю синхронизацию «в обход» стандартного потока. Это может приводить к рассинхронизации шагов обновления, если движок жестко не изолирует транзакции в момент распространения изменений.

Открытая публикация результатов тестирования подчеркивает прозрачность разработки движка. Оба случая уже зафиксированы в бэклоге проекта, а их последующее исправление отточит реактивное поведение системы в экстремальных условиях девелоперских экспериментов, не влияя на стабильность классических паттернов проектирования.

Демонстрация

import { signal, computed, effect } from '@pravosleva/reactive-engine'

// 1. Создаем корневой сигнал (Вершина А)
const signalA = signal(1, 'Signal_A')

// 2. Создаем промежуточные вычисляемые свойства (Стороны Б и В)
const computedB = computed(() => {
  const a = signalA.value
  return a * 2
}, 'Computed_B')

const computedC = computed(() => {
  const a = signalA.value
  return a + 10
}, 'Computed_C')

// 3. Создаем результирующее вычисляемое свойство (Основание Г)
const computedD = computed(() => {
  const b = computedB.value
  const c = computedC.value
  return b + c
}, 'Computed_D')

// Переменная для подсчета реального количества запусков эффекта
let effectTriggerCount = 0

// 4. Подписываемся на финальный узел графа через эффект
effect(() => {
  effectTriggerCount++
  console.log(`[Effect Trigger #${effectTriggerCount}] Текущее значение Г = ${computedD.value}`)
}, 'Final_Effect')

console.log('--- Инициализация графа завершена ---')
console.log(`Стартовые значения -> А: ${signalA.value}, Б: ${computedB.value}, В: ${computedC.value}, Г: ${computedD.value}\n`)

// 5. Изменяем корневой сигнал А и проверяем отсутствие "глюка ромба"
console.log('=== Обновление значения: А = 2 ===')
signalA.value = 2

console.log('\n--- Проверка результатов топологической сортировки ---')
console.log(`Финальные значения -> А: ${signalA.value}, Б: ${computedB.value}, В: ${computedC.value}, Г: ${computedD.value}`)
console.log(`Всего вызовов эффекта: ${effectTriggerCount} (Ожидается ровно 2: 1 при старте + 1 при обновлении)`)

Что вы увидите в консоли при запуске:

[Effect Trigger #1] Текущее значение Г = 13
--- Инициализация графа завершена ---
Стартовые значения -> А: 1, Б: 2, В: 11, Г: 13

=== Обновление значения: А = 2 ===
[Effect Trigger #2] Текущее значение Г = 16

--- Проверка результатов топологической сортировки ---
Финальные значения -> А: 2, Б: 4, В: 12, Г: 16
Всего вызовов эффекта: 2 (Ожидается ровно 2: 1 при старте + 1 при обновлении)

Как этот код доказывает защиту от проблемы ромба:

  • Если бы движок был наивным и страдал от проблемы ромба, в процессе обновления signalA.value = 2 эффект выполнился бы лишний раз посередине, выведя промежуточное неверное значение 15 (новое Б(4) + старое В(11)), а счетчик effectTriggerCount стал бы равен 3.
  • Архитектура @pravosleva/reactive-engine благодаря ранней dirty-маркировке и распределению приоритетов по уровням глубины (depth) заморозила выполнение эффекта до тех пор, пока Computed_B и Computed_C не обновили свои версии. Эффект сработал строго один раз и сразу вывел корректный результат 16.

Автоматический Vitest/Jest тест-кейс для проверки "проблемы ромба"

Ниже представлен готовый изолированный тест-кейс, который можно напрямую добавить в тестовую среду проекта (например, в файл diamond.test.ts). Тест программно верифицирует топологический порядок расчёта графа и гарантирует защиту от лишних вызовов и промежуточных глитчей.

import { describe, it, expect } from 'vitest' // или 'jest'
import { signal, computed, effect, resetGlobalReactiveEngine } from '@pravosleva/reactive-engine'

describe('Reactive Engine: Diamond Problem Mitigation', () => {
  it('should resolve diamond dependency graph strictly in topological order with 0 glitches', () => {
    // Гарантируем чистоту эпохи и графа перед выполнением тест-кейса
    resetGlobalReactiveEngine()

    // 1. Инициализируем граф в форме ромба (A -> B/C -> D)
    const signalA = signal(1, 'A')
    
    const computedB = computed(() => signalA.value * 2, 'B')
    const computedC = computed(() => signalA.value + 10, 'C')
    
    const computedD = computed(() => computedB.value + computedC.value, 'D')

    let effectTriggerCount = 0
    const capturedValues: number[] = []

    // 2. Регистрируем эффект-потребитель основания ромба
    effect(() => {
      effectTriggerCount++
      capturedValues.push(computedD.value)
    }, 'Consumer_Effect')

    // Проверяем состояние графа после первоначального pull-запуска
    expect(effectTriggerCount).toBe(1)
    expect(computedD.value).toBe(13)
    expect(capturedValues).toEqual([13])

    // 3. Изменяем корневой источник изменений
    signalA.value = 2

    // 4. Проверяем отсутствие промежуточных глитчей и дублирующих запусков
    // Если бы движок был наивным, эффект сработал бы дважды (сначала записав глитч 15)
    expect(effectTriggerCount).toBe(2) 
    expect(computedD.value).toBe(16)
    
    // Проверяем, что в истории изменений не зафиксировано промежуточного значения 15
    expect(capturedValues).not.toContain(15)
    expect(capturedValues).toEqual([13, 16])
  })
})