Предыстория

Все началось с рабочей задачи по реализации весьма специфичной web-панели мониторинга и управления различным оборудованием, в которой нужно было получать данные и отправлять команды по разнообразным сценариям (периодический опрос, подписка на websocket-события, https-запросы и т.д.), а также выводить состояние и графики в реальном времени, динамически комбинируя различные источники данных в разных виджетах.

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

В первой итерации с использованием RxJS получилось много неструктурированного и сложно поддерживаемого кода, так как архитектура на RxJS вынуждала императивно описывать перестроение топологии при изменении правил маршрутизации «на лету». В нашем специфичном кейсе с динамическими виджетами это приводило к сайд-эффектам и сложностям в отладке. Кроме того, местами размывалась строгая типизация и мы лишались compile-time гарантий. Так появилась идея разработать собственное решение на основе альтернативной концепции — модели графа потоков данных.

В результате получившаяся модель продемонстрировала предсказуемое поведение и низкую связность компонентов, а кодовая база сократилась на ~30% и приобрела более декларативный вид. Убедившись в эффективности решения, мы решили оформить его в виде отдельной библиотеки с открытым исходным кодом — Transferum.

И что, получилась просто еще одна реактивная библиотека?

Не совсем. Классические FRP-библиотеки (RxJS, Bacon, Most) построены вокруг единственного примитива (Observable). Transferum же основан на композиции различных типов узлов с явно определенным поведением.

Каждый узел в графе потоков распространения данных явно декларирует свои способности: может ли он принимать данные через push, отдавать через pull, распространять полученный сигнал подписчикам, опрашивать источник, фильтровать, блокировать поток и т.д.

Объявленные узлом возможности являются одновременно флагами для использования в runtime и compile-time гарантиями наличия соответствующих методов, определяющих его поведение.

Ключевая идея: поведение системы описывается как композиция независимых возможностей, которые одновременно определяют тип, реализацию и правила взаимодействия.

Transferum предоставляет четыре слоя абстракции:

  1. Трансферы — узлы графа (каналы, поллеры, мапперы, буферы, разветвители и концентраторы, реализации debounce, throttle, switchMap и т.д.).

  2. Мосты — ребра графа — управляемые вентили между узлами с динамической маршрутизацией и гейтингом.

  3. Операторы — stateless-трансформаторы и фильтры данных (используются трансферами, отвечающими за конвертацию данных).

  4. Билдеры — fluent-конструкторы композитных трансферов из цепочек трансферов-примитивов.

Концептуальная и архитектурная основа — capability flags system. Каждый трансфер реализует CommunicationContractInterface — набор булевых флагов, определяющих его возможности.

Флаги isPushable, isPullable, isSubscribable, isGate и другие — это не просто свойства объекта. Это метаданные, которые:

  • Определяют TypeScript-интерфейс трансфера на этапе компиляции.

  • Управляют стратегией связывания с другими трансферами в рантайме (с помощью функции linkTransfers()).

  • Обеспечивают совместимость в билдерах без приведений типов.

Один набор флагов — три потребителя. Это единый источник истины для всей системы.

Когда флаг равен true, соответствующий метод входит в TypeScript-интерфейс трансфера. Это позволяет предоставлять трансфер пользователю вот так:

Transfer<T, [Pushable, Pullable, Subscribable, Triggerable]>
// гомогенный трансфер: T -> [ Transfer ] -> T

Или вот так:

Transfer<TInput, TOutput, [Pushable, Pullable, Subscribable, Triggerable]>
// гетерогенный трансфер: TInput -> [ Transfer ] -> TOutput

Именно в таком формате типов фабрики в библиотеке возвращают трансферы. Например:

const pushChannel:  Transfer<number, [Pushable, Subscribable]>;
const converter:    Transfer<number, string, [Pushable, Subscribable]>;
// содержат методы push(), subscribe()

const manualBuffer: Transfer<number, [Pushable, Pullable, Triggerable]>;
// содержит методы push(), pull(), trigger()
// trigger() в данном случае нужен, чтобы сделать запушенное значение 
// доступным для чтения через pull()

const pollingProxy: Transfer<T, [PollingProxy, Pullable, Subscribable, Triggerable, Gate]>
// содержит методы pull(), subscribe(), trigger(), activate(), deactivate(), toggle()
// может быть связан с pullable-источником благодаря поддержке поллинга
Эта «магия» работает в compile-time благодаря несколько замысловатой системе вычислимых типов
type FeatureMap<TIn, TOut, F> =
  F extends { readonly isPushable: true } ? Pushable<TIn> & { readonly isInput: true } :
  F extends { readonly isPollingProxy: true } ? PollingProxy<TIn> & { readonly isInput: true, readonly isOutput: true } :
  F extends { readonly isPullable: true } ? Pullable<TOut> & { readonly isOutput: true } :
  F extends { readonly isSubscribable: true } ? Subscribable<TOut> & { readonly isOutput: true } :
  F extends { readonly isTriggerable: true } ? Triggerable :
  F extends { readonly isGate: true } ? Gate :
  F extends { readonly isAsyncPushable: true } ? AsyncPushable<TIn> & { readonly isInput: true } :
  F extends { readonly isAsyncPollingProxy: true } ? AsyncPollingProxy<TIn> & { readonly isInput: true, readonly isOutput: true } :
  F extends { readonly isAsyncPullable: true } ? AsyncPullable<TOut> & { readonly isOutput: true } :
  F extends { readonly isAsyncTriggerable: true } ? AsyncTriggerable :
  unknown;

type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;

type ResolveFeatures<TIn, TOut, Features extends any[]> = UnionToIntersection<{
  [K in keyof Features]: FeatureMap<TIn, TOut, Features[K]>
}[number]>;

type IsDuplexFeatures<Features extends any[]> =
  ResolveFeatures<any, any, Features> extends { readonly isInput: true, readonly isOutput: true }
    ? { readonly isDuplex: true }
    : unknown;

type ExplicitTransfer<TIn, TOut, Features extends any[]> = BaseTransferInterface
  & ResolveFeatures<TIn, TOut, Features>
  & IsDuplexFeatures<Features>;

export type Transfer<
  TInOrAll,
  TOutOrFeatures extends any[] | any,
  TFeaturesOrUndefined extends any[] | undefined = undefined,
> = TFeaturesOrUndefined extends any[]
  ? ExplicitTransfer<TInOrAll, TOutOrFeatures, TFeaturesOrUndefined>
  : TOutOrFeatures extends any[]
    ? ExplicitTransfer<TInOrAll, TInOrAll, TOutOrFeatures>
    : never;

Архитектурные инварианты

1. Трансферы не знают своих соседей

Трансфер определяет своё поведение (push, pull, subscribe и др.), но никогда не ссылается и не проверяет класс другого трансфера. Он не знает, что является upstream или downstream — лишь выполняет свой контракт. Пользователь может создать свой трансфер, объявить и реализовать его возможности — и он органично и бесшовно впишется в экосистему.

2. Мосты не знают конкретных реализаций

Мост инспектирует capability flags, а не имена классов. Нет цепочки instanceof, нет переключения по имени класса. Любой output-трансфер может быть соединен с любым input-трансфером — при условии совместимости их флагов, о чем мы поговорим чуть ниже. Это применимо и к тем узлам, которые еще не существуют и будут созданы пользователем.

3. Значение undefined никогда не распространяется

В Transferum undefined означает «нет данных», а не «пустое значение». Оно подавляется на уровне внутренней реализации менеджера подписок — подписчики никогда не уведомляются с undefined. При этом для явных маркеров пустых значений можно использовать null. Мы сознательно пошли на этот компромисс, чтобы избежать runtime-оверхеда и сохранить нативную скорость работы на плотных потоках данных.

Связывание трансферов

Функция linkTransfers(lhs, rhs) соединяет output-трансфер (lhs) с input-трансфером (rhs) с автоматическим выбором стратегии связывания на основе возможностей этих трансферов:

  • isSubscribableisPushable (реактивная подписка);

  • isPullableisPollingProxy (активный опрос);

  • isSubscribableisAsyncPushable (реактивная подписка + асинхронный push);

  • isAsyncPullableisAsyncPollingProxy (активный асинхронный опрос асинхронного pull-источника);

  • isPullableisAsyncPollingProxy (активный асинхронный опрос синхронного pull-источника).

Protocol-oriented design: механизм не спрашивает «какой это класс?» — он выясняет, какие у него есть возможности. Любая пара трансферов с совместимыми возможностями является linkable. Добавление нового класса трансфера требует только объявления его флагов и реализации соответствующих методов — как связать его с другим трансфером, связующий алгоритм разберется сам.

import { createPushChannelTransfer, createSinkTransfer, linkTransfers } from 'transferum';
 
const source = createPushChannelTransfer<number>();
const target = createSinkTransfer<number>({ callback: (x) => console.log(x) });
 
const link = linkTransfers(source, target);
source.push(22);
 
link.unsubscribe(); // разорвать связь

Sync и async в одной экосистеме

Синхронные и асинхронные трансферы сосуществуют и могут быть связаны между собой. linkTransfers() предпочитает sync-связывание, когда это возможно, а async-стратегии применяет только когда sync неприменим. Нет отдельного «асинхронного мира».

Поддержка backpressure

Ряд асинхронных трансферов (AsyncSinkTransfer, AsyncWriteTransfer, AsyncConvertTransfer, AsyncConditionTransfer) поддерживают необязательные поля в конфигурации: maxConcurrency, bufferSize и onBufferOverflow — для ограничения параллельных async-операций, очереди избыточных данных и graceful-обработки переполнения. По умолчанию — неограниченная обработка, без буферизации.

Локальная обработка ошибок

Transferum использует единую модель обработки ошибок для всех трансферов. Каждый трансфер, который может столкнуться с runtime-ошибкой, принимает опциональный onError-хэндлер в своей конфигурации.

А теперь — к примерам использования

Вот так можно просто и декларативно описать опрос и агрегирование данных из нескольких источников:

import { 
  createMergeTransfer, 
  createConditionTransfer, 
  createAsyncWriteTransfer,
  createAsyncSinkTransfer,
  createAsyncPollingSourceTransfer, 
  OutputPipelineBuilder, 
} from 'transferum';

const sensor1 = createAsyncPollingSourceTransfer<SensorData>({
  fetcher: () => Promise.resolve({ sensorId: 1, temperature: 25, humidity: 50 }),
  interval: 50,
  activated: true,
});

const sensor2 = createAsyncPollingSourceTransfer<SensorData>({
  fetcher: () => Promise.resolve({ sensorId: 2, temperature: 26, humidity: 55 }),
  interval: 50,
  activated: true,
});

const sensorsAggregator = createMergeTransfer<SensorData>({
  sources: [sensor1, sensor2],
});

const alertPipeline = OutputPipelineBuilder
  .start(sensorsAggregator)
  .to(createConditionTransfer<SensorData>({
    shouldAccept: (d) => d.temperature > 95,
  }))
  .finish(createAsyncSinkTransfer<SensorData>((sensorData) => console.warn(`Alert! Sensor ${sensorData.sensorId}: temperature is ${sensorData.temperature}`)));

sensorsAggregator.subscribe((sensorData) => console.log('Debugging', sensorData));

А вот как можно организовать динамический роутинг:

import { 
  createPushStoredChannelTransfer,
  createBridgeMultiSelector, 
  createBridgeSelector, 
  createPassBridge, 
} from 'transferum';

// Вариант 1. Выбор одного реципиента
const commandRouter = createBridgeSelector({
  bridges: {
    light: createPassBridge({ source: commandChannel, target: lightController, activated: false }),
    thermostat: createPassBridge({ source: commandChannel, target: thermostatController, activated: false }),
    lock: createPassBridge({ source: commandChannel, target: lockController, activated: false }),
  },
  initialKey: 'thermostat', // выбор по умолчанию
  activated: true,
  owned: true,
});

commandRouter.select('lock'); // выберем другой вариант

// Вариант 2. Выбор нескольких реципиентов
const loggersRouter = createBridgeMultiSelector({
  bridges: {
    prometheus: createPassBridge({ source: metricsChannel, target: prometheusWriter, activated: false }),
    elk: createPassBridge({ source: metricsChannel, target: elkWriter, activated: false }),
    sentry: createPassBridge({ source: metricsChannel, target: sentryWriter, activated: false }),
  },
  initialKeys: ['prometheus', 'elk'], // будут активированы по умолчанию
  activated: true,
  owned: true,
});

loggersRouter.check('elk');                     // теперь выбраны все три
loggersRouter.uncheck('prometheus');            // выбраны elk, sentry
loggersRouter.select(['prometheus', 'sentry']); // выбраны prometheus, sentry
Еще один более длинный пример c организацией игровой механики
import {
  createDebounceTransfer, 
  createConvertTransfer, 
  createMapOperator,
  createBridgeSelector, 
  createPassBridge, 
  linkTransfers,
} from 'transferum';

// Источник: debounce нажатий клавиш (16ms ~ 1 кадр при 60 FPS)
const rawInputSource = createDebounceTransfer<KeyboardEvent>({ delay: 16 });

// Конвертер: KeyboardEvent -> код клавиши
const inputConverter = createConvertTransfer<KeyboardEvent, string>({
  operator: createMapOperator((event) => event.code),
});

// Целевые системы-потребители пользовательских событий
const carSystem = { push: (cmd: string) => console.log(`? Car executing: ${cmd}`) };
const planeSystem = { push: (cmd: string) => console.log(`✈️ Plane executing: ${cmd}`) };
const menuSystem = { push: (cmd: string) => console.log(`? Menu processing: ${cmd}`) };

// Роутер: динамическое переключение между игровыми контекстами
const gameplayRouter = createBridgeSelector({
  bridges: {
    driving: createPassBridge({ source: inputConverter, target: carSystem }),
    flying: createPassBridge({ source: inputConverter, target: planeSystem }),
    ui: createPassBridge({ source: inputConverter, target: menuSystem }),
  },
  initialKey: 'driving', // игрок начинает в машине
  activated: true,
});

// Связывание источника с конвертером с автовыбором стратегии
linkTransfers(rawInputSource, inputConverter);

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

// Игрок в машине нажимает "W"
rawInputSource.push(new KeyboardEvent('keydown', { code: 'KeyW' }));
// Через 16ms: ? Car: KeyW

// ==================

// Переключаемся на самолёт
gameplayRouter.select('flying');

// Та же клавиша теперь управляет самолётом
rawInputSource.push(new KeyboardEvent('keydown', { code: 'KeyW' }));
// Через 16ms: ✈️ Plane: KeyW

Когда имеет смысл попробовать Transferum

Библиотека подойдет для:

  • TypeScript-first проектов — благодаря максимально строгой типизации и compile-time вычислению доступных методов любого трансфера на основе объявленных у него флагов возможностей.

  • Работы с pull-based источниками данных — polling API, датчиков, хранилищ с PollingProxy.

  • Смешанных sync/async пайплайнов — в единой модели без ручного преобразования.

  • Явного flow control — gates, bridges, selectors для runtime-маршрутизации.

  • Game development / IoT — frame-aligned tickers, idle polling, sensor aggregation.

  • Устойчивой обработки ошибок — локальная, non-fatal обработка: одна стадия не убивает пайплайн при условии переданного в конфиге обработчика ошибок, ничего не подавляется молча.

Результаты и планы

  • Библиотека уже используется в двух наших внутренних проектах и показывает свою эффективность. Код доступен на GitHub под лицензией MIT, библиотека не имеет внешних зависимостей и поставляется с подробной документацией (README + API Reference).

  • В одном из проектов граф состоит из ~80 узлов и стабильно обрабатывает несколько сотен событий в секунду без деградации. На основе этих данных в том числе рендерится 3D-сцена в Babylon.js со стабильным фреймрейтом ~60 FPS без микрофризов.

  • Тесты библиотеки покрывают не только отдельные трансферы, но и поведение системы в динамике: переподключение мостов, обработку ошибок в длинных асинхронных цепочках, а также разнообразные граничные случаи. Покрытие — 100%.

  • В дальнейшем планируем реализовать хуки и утилиты для более удобного и нативного использования Transferum с Vue и React. Если они окажутся в достаточной мере переиспользуемыми, оформим в отдельные пакеты-адаптеры.

Буду рад, если вы заглянете в репозиторий, попробуете библиотеку в деле и поделитесь замечаниями — обратная связь поможет сделать Transferum лучше.

P. S. Если вы сталкивались с похожими задачами и решили их как-то иначе — буду рад прочитать о вашем опыте в комментариях.

Комментарии (11)


  1. cmyser
    23.07.2026 22:04

    ООО ! Надо бы с полоской сравнить, сравню и вернусь попозже


    1. Smoren Автор
      23.07.2026 22:04

      Благодарю за комментарий! Буду ждать обратную связь.


      1. cmyser
        23.07.2026 22:04

        для начала вводные
        вот тут есть большое сравнение рективных систем https://page.hyoo.ru/?#!=3ia3ll_rcpl7b/View’3ia3ll_rcpl7b’ ( rxjs не только сложен но еще и показывает невалидное поведение )
        а тут создание "идеальной" системы реактивности https://page.hyoo.ru/?#!=yj0h42_ixzv4p по курсу https://page.hyoo.ru/?#!=mt9ynz_nj5p16




      1. cmyser
        23.07.2026 22:04

        Transferum vs $mol_wire

        Сравнение статьи «Разработка TypeScript-библиотеки для построения реактивных графов распространения и обработки данных» (Smoren, библиотека transferum-ts) с тем, как те же задачи решены в $mol_wire — по статье «Проектируем идеальную систему реактивности» (черновик: HabHub#48) и по исходникам mol/wire в этом репо. Разбор Transferum — по коду v1.3.0, не по пересказу.

        В двух словах

        Обе библиотеки решают один список проблем: push против pull, смешение sync/async, динамическая топология, управление подписками и ресурсами. Но относятся к разным категориям инструментов. Transferum описывает, как данные движутся между компонентами: программист руками создаёт узлы (трансферы) и рисует рёбра (мосты). $mol_wire описывает, что от чего зависит: граф строится сам по факту чтения состояний внутри вычислений, а рантайм решает, когда и что пересчитывать.

        Из этой развилки следует почти всё остальное: где в Transferum отдельный класс узла и фабрика, в $mol_wire — обычный код с декоратором.

        Концепты Transferum → как в $mol_wire

        Узлы-трансферы (31 класс, 52 фабрики create*)

        • Transferum: каждый вид узла — отдельный класс: PushChannelTransfer, DebounceTransfer, AsyncPollingSourceTransfer

        • $mol_wire: узел возникает неявно, любой метод под @$mol_mem — атом с кешем, подписками и деструктором. Один декоратор вместо выбора из 31 класса.

        Мосты / linkTransfers

        • Transferum: if-каскад по capability-флагам (utils.ts:52), 10 стратегий проводки; реактивность = lhs.subscribe(d => rhs.push(d)), каждое ребро рисуется руками.

        • $mol_wire: рёбра создаются автоматически — publisher при чтении зовёт promote(), текущий подписчик линкуется (mol/wire/pub/pub.ts:81). Дифф по курсору переиспользует старые рёбра и отписывает лишние (sub.ts:44-160).

        Capability-флаги (isPushable, isPullable, isSubscribable… ~14 шт)

        • Transferum: runtime — boolean-поля на BaseTransfer; compile-time — 64 интерфейса с сужением флага до литерала true.

        • $mol_wire: флагов нет. «Способности» канала выражаются телом функции: read-only, write-only или полный геттер-сеттер. Подписываемость даёт пара Pub/Sub (promote()/emit()).

        Push

        • Transferum: синхронный каскад в глубину: pushsendStateforEach по listener’ам → push следующего. Значение течёт по графу немедленно.

        • $mol_wire: push распространяет только статус, не значение: прямые зависимости помечаются stale, транзитивные doubt (sub.ts:188). Пересчёт откладывается до чтения.

        Pull

        • Transferum: pull() = вызвать фетчер прямо сейчас (transfers.ts:1112), без кеша и инвалидации.

        • $mol_wire: pull = ленивое вычисление с кешем: fresh — вернуть кеш; doubt — опросить зависимости и пересчитать только если что-то реально изменилось (fiber.ts:147-241).

        Polling (тикеры RAF/interval)

        • Transferum: активный опрос по расписанию, тикает даже без подписчиков.

        • $mol_wire: инварианты пересчитываются по факту чтения; периодика при нужде — атом от $mol_state_time. Без потребителей ничего не вычисляется.

        Динамический роутинг (BridgeSelector, createPassBridge, select/check/uncheck)

        • Transferum: отдельная подсистема: заранее создать все мосты, активировать нужный по ключу.

        • $mol_wire: обычный switch/if внутри вычисления. Поменялось условие — трекинг сам перецепил рёбра на следующем пересчёте, отдельного механизма нет.

        Операторы (map/filter/reduce, 9 шт)

        • Transferum: stateless-обёртки, подключаются через ConvertTransfer.

        • $mol_wire: обычные выражения в теле метода.

        Билдеры (1060 строк)

        • Transferum: fluent-конструкторы пайплайнов, отслеживают owned-ресурсы.

        • $mol_wire: не нужны, композиция = вызовы методов.

        Ошибки (onError, onAcceptError… по узлу)

        • Transferum: локальные хендлеры; без хендлера — проброс; поллер без onError останавливает тикер.

        • $mol_wire: ошибка — значение: кешируется как результат (fiber.ts:200), при чтении ре-throw ($mol_fail_hidden), ловится обычным try-catch у потребителя. Не пересчитывается до инвалидации.

        Backpressure (maxConcurrency, bufferSize, onBufferOverflow)

        • Transferum: очереди и лимит параллелизма на 4 async-классах.

        • $mol_wire: прямого аналога нет. Стратегия другая: новое вычисление отменяет устаревшее ($mol_wire_async убивает прошлый fiber, async.ts:19), очередь не копится.

        Владение / destroy()

        • Transferum: явная модель: unsubscribe/destroy руками, owned-ресурсы в билдерах. Узел без подписчиков живёт (объект, буфер, тикер), пока не убьёшь.

        • $mol_wire: автоматическая: ноль подписчиков → reap() → деструктор (pub.ts:69, atom.ts:106). Владеемые объекты уничтожаются детерминированно через $mol_owning, без опоры на GC.

        Аспекты реактивности из issue #48 → что в Transferum

        Origin: pull для инвариантов, push для событий

        • $mol_wire: гибрид в одной абстракции канала: push помечает статусы, pull лениво пересчитывает.

        • Transferum: гибрид тоже заявлен, но push = немедленный каскад значений, pull = немедленный фетч. Ленивых инвариантов нет.

        Multiplexing / Keys (@$mol_mem_key, $mol_key)

        • $mol_wire: словарь атомов по сериализованному ключу, общая логика на неограниченный набор каналов.

        • Transferum: аналога нет, однотипные узлы создаются по одному.

        Factory / жизненный цикл

        • $mol_wire: фабрика-канал создаёт и мемоизирует объект, деструктор зовётся сам, когда объект не нужен по графу.

        • Transferum: создание руками, уничтожение руками (destroy()).

        Dupes: дедупликация значений

        • $mol_wire: $mol_compare_deep в atom.put(): структурно равное значение не будит подписчиков (atom.ts:155).

        • Transferum: нет: sendState не сравнивает old/new, шлёт всегда (глушится только undefined). Нет и аналога distinctUntilChanged в операторах.

        Мемоизация вычислений

        • $mol_wire: кеш в каждом fiber, пересчёт только по инвалидации.

        • Transferum: нет, ConvertTransfer гоняет оператор на каждый push.

        Tonus: ленивость

        • $mol_wire: без подписчиков ничего не вычисляется; невидимый рендер замирает.

        • Transferum: рассылка без подписчиков не идёт, но поллеры тикают и фетчат независимо от потребителей.

        Order: glitch-free

        • $mol_wire: 4 статуса (stale/doubt/fresh/final), пересчёт при чтении после актуализации всех входов, промежуточное состояние ненаблюдаемо.

        • Transferum: не решён, синхронный DFS-обход. На алмазе A→{B,C}→D узел D срабатывает дважды, первый раз с промежуточным состоянием. Топосортировки и батчинга нет.

        Error: ошибки как значения

        • $mol_wire: Result/Error/Promise в одном кеше, нормализация throw/return.

        • Transferum: иначе, но продуманно: локальные хендлеры по узлам, защита от зомби-тикеров. Известный косяк: async-push без onError → unhandled rejection (задокументирован в коде).

        Extern: sync↔async без цветных функций

        • $mol_wire: SuspenseAPI (throw Promise + идемпотентный перезапуск), $mol_wire_sync/$mol_wire_async.

        • Transferum: sync и async — параллельные наборы классов (Async-дубли), смешение через приоритеты в linkTransfers.

        Concurrency / Abort

        • $mol_wire: новый запуск отменяет предыдущий, abort внешних запросов через destructor владеемого объекта.

        • Transferum: отмены нет: AbortController в коде не используется, протухший запрос доедет и опубликуется.

        Cycle: циклические зависимости

        • $mol_wire: мгновенный Error: Circular subscription вместо зависания.

        • Transferum: отдельно не проверялось; защиты в просмотренном коде не встретилось.

        Economy

        • $mol_wire: fiber 64 байта, 3 аллокации, связи в одном плоском массиве, O(1) отписка. Ядро ≈1000 строк, mol_wire_lib 7 КБ min+gz.

        • Transferum: объект + буфер + тикер на узел; 8393 строки src, 64 интерфейса. Зато 0 зависимостей и tree-shaking.

        Debug

        • $mol_wire: семантичные имена объектов (app.account(1).task(456)), custom formatters в DevTools.

        • Transferum: специальных средств не встретилось.

        Что в Transferum сделано хорошо

        Для честного сравнения — сильные стороны, которых у ручной модели не отнять:

        • Капабилити-типизация работает по-настоящему: вызвать push() на узле без isPushable — ошибка компиляции, не рантайма. Мосты диспетчеризуются по флагам, не по instanceof, новый узел не трогает чужой код.

        • Backpressure из коробки (maxConcurrency/bufferSize) — этого нет ни в $mol_wire, ни в большинстве реактивных либ. В wire вместо очередей отмена устаревшего, это другой трейд-офф, а не строгое превосходство.

        • Обработка ошибок гранулярная и с защитой от зомби-тикеров.

        • Ноль зависимостей, MIT, полное покрытие тестами.

        Цена — площадь API: 31 класс, 52 фабрики, 64 интерфейса, 8393 строки. То, что в wire выражается одним @$mol_mem-методом, здесь требует выбрать правильный класс, обернуть фабрикой и связать мостом.

        Контраст на примере роутинга из статьи

        Transferum (пример из статьи, все мосты создаются заранее):

        const commandRouter = createBridgeSelector({
          bridges: {
            light: createPassBridge({ source: commandChannel, target: lightController }),
            thermostat: createPassBridge({ source: commandChannel, target: thermostatController }),
            lock: createPassBridge({ source: commandChannel, target: lockController }),
          },
          initialKey: 'thermostat',
        })
        commandRouter.select('lock')
        

        $mol_wire — роутинг это просто код, рёбра перецепляет трекинг:

        class Home extends $mol_object2 {
        
          @ $mol_mem
          mode( next = 'thermostat' ) { return next }
        
          controller() {
            switch( this.mode() ) {
              case 'light': return this.light()
              case 'thermostat': return this.thermostat()
              default: return this.lock()
            }
          }
        
        }
        

        Бонус: находка для автора

        AsyncConvertTransfer._process (transfers.ts:3146) при maxConcurrency > 1 пишет результаты параллельных операций в общий this._state.value и сразу рассылает. Порядок эмиссии не гарантирован: результат более старого запроса может опубликоваться после более нового. Sequence-guard’а нет, в README не описано. Можно оформить доброжелательный issue — полезнее любого спора в комментах.

        Источники


        1. Smoren Автор
          23.07.2026 22:04

          Спасибо огромное за столь детальный сравнительный обзор! Получить в комментарии подробный аудит кодовой базы — это огромная ценность и редкое удовольствие для опенсорс-разработчика.

          Ранее я не слышал о проекте $mol, поэтому сразу оговорюсь, что сейчас, после короткого изучения его основных концепций, могу не понимать всей глубины идеи, равно как и не видеть подводных камней, которые она могла за собой принести.

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

          Вы абсолютно правы в корневом разделении наших подходов: Explicit Dataflow против Transparent FRP (Pull-реактивности).

          Пересчет графа “на лету” в теле методов в $mol_wire — это красивое рантайм-решение для управления состояниями. Но Transferum создавался для решения немного другого класса задач, где явный контроль над топологией важнее автоматизации. Фокус Transferum — это жесткие, изолированные конвейеры обработки данных, где разработчику нужно четко видеть каждый узел и ребро графа и контролировать потоки.

          Из этой развилки вытекают сильные стороны Transferum, которые при автоматическом Pull-подходе получить трудно: максимально строгая compile-time типизация и предсказуемый backpressure.

          Что касается проблемы глитчей — вы абсолютно правы, в текущей реализации используется синхронный DFS-обход. В сложных UI-графах это могло бы стать проблемой, но в рамках наших кейсов маршрутизации потоков данных эта специфика полностью нивелируется явным flow-control (гейтами и бриджами).

          И отдельное спасибо за находку в AsyncConvertTransfer._process! Вы правы: если тяжелый первый запрос завершится позже легкого второго, он перезапишет _state.value устаревшими данными. Это реальное пограничное состояние, которое до этого момента не всплывало, так как в критичных местах мы использовали последовательную обработку (maxConcurrency: 1). Это надо будет, как минимум, задокументировать, а в идеале — подумать про Sequence guard.

          Еще раз благодарю за анализ!


          1. cmyser
            23.07.2026 22:04

            Да не за что, задачу конечно всё таки одну и ту же решают, но по разному

            Мне ближе архитекрный подход с уменьшением кода


            1. Smoren Автор
              23.07.2026 22:04

              Добавил себе issue с описанием найденной Вам проблемы, упомянул Вас. Еще раз спасибо!

              https://github.com/Smoren/transferum-ts/issues/7


              1. cmyser
                23.07.2026 22:04

                да не за что)


                1. Smoren Автор
                  23.07.2026 22:04

                  Есть за что :) Найденный Вами баг исправлен в релизе:

                  https://github.com/Smoren/transferum-ts/releases/tag/v1.4.0


  1. Sherbet
    23.07.2026 22:04

    Интересное решение. Попробуем библиотеку в своем проекте.


    1. Smoren Автор
      23.07.2026 22:04

      Благодарю за отзыв! Буду рад, если потом поделитесь опытом использования и укажете на сложности и недостатки, с которыми столкнулись.