Пример и краткая документация по ObservableStore
Основная идея и архитектура
1.1. Прозрачный Proxy
1.2. Поддержка типизированных строковых ключей
1.3. Подписки с «гранулярностью» путей
1.4. Кэш-ключи (cacheKeys)
1.5. Middleware-подход
1.6. Batching-обёртка
1.7. Асинхронные обновления
1.8. История изменений (undo/redo)
1.9. Интеграция с любым фреймворкомПример создания основного store (с middleware)
2.1 Что делаетDepthPath
и зачем он нужен
2.2 Что такоеAccessor
и зачем он нуженAPI createObservableStore
3.1.store.state
/store.$
3.2.store.subscribe(callback, cacheKeys?)
3.3.store.subscribeToPath(pathOrAccessor, callback, options?)
3.4.store.invalidate(cacheKey)
3.5.store.get(pathOrAccessor)
3.6.store.update(pathOrAccessor, valueOrFn)
3.7.store.resolveValue(pathOrAccessor, valueOrFn)
Асинхронные обновления
4.1.store.asyncUpdate(pathOrAccessor, asyncUpdater, options?)
4.2.store.cancelAsyncUpdates(pathOrAccessor?)
История изменений (undo/redo)
6.1.store.undo(pathOrAccessor)
6.2.store.redo(pathOrAccessor)
Статистика и очистка
7.1.store.getMemoryStats()
7.2.store.clearStore()
Промежуточная обработка (Middleware)
8.1. Когда срабатывает middleware
8.2. Изменениеvalue
внутри middleware
8.3. Блокировка изменения
8.4. Последовательность нескольких middleware
Ниже приведён пример реализации универсального реактивного стора (ObservableStore), написанного на TypeScript без привязки к конкретному фреймворку. Такой стор можно легко «подключить» в любом фронтенд-фреймворке (React, Vue, Svelte, Solid и т. д.) путём написания нескольких обёрток (адаптеров) поверх базового API.
- Обертка под react: @qtpy/state-management-react
Основная идея и архитектура
Прозрачный Proxy
Все обращения к состоянию (
state
) проходят через JavaScript Proxy, позволяющий автоматически отслеживать чтения и записи.- При чтении Proxy «собирает» зависимости: какие участки состояния используются внутри разных Accessor’ов или подписок.
- При записи Proxy перехватывает изменение и в конце вызывает цепочку middleware и нотификации подписчиков.
Поддержка типизированных строковых ключей
В ObservableStore реализована строго типизированная система строковых путей, которая позволяет:
безопасно указывать строки путей вроде "user.settings.theme" без риска ошибок типов;
получать автодополнение путей при использовании TypeScript;
обеспечивать валидацию пути и значения: если путь некорректен — TypeScript подскажет об ошибке на этапе компиляции;
поддерживать доступ к кортежам и массивам по индексам, например: "items.0" или "list.2.name".
пример: ссылка на картинку
Подписки с «гранулярностью» путей
- Можно подписаться на любое конкретное поле вложенного объекта, используя либо строку-путь (
"user.settings.theme"
), либо Accessor:(t) => store.state.user.settings.theme
. - При изменении именно этого поля подписчики получат уведомление. Изменения в других полях не затронут эту подписку.
- Можно подписаться на любое конкретное поле вложенного объекта, используя либо строку-путь (
Кэш-ключи (cacheKeys)
- Позволяют группировать «логические зависимости» (например, вычисляемые свойства).
- При вызове
store.invalidate(key)
все подписчики, передавшие этотcacheKey
при подписке, будут уведомлены, даже если напрямую поле остался тем же.
Middleware-подход
- При каждом
update
(или прямой записи через Proxy) можно выполнить цепочку middleware, чтобы логировать, валидировать или блокировать изменения. - Каждый middleware видит путь (
string
) и новое значение, может модифицировать или остановить дальнейшее распространение.
- При каждом
Batching-обёртка
- Позволяет «склеивать» несколько изменений в одно уведомление. Это важно, чтобы не вызывать повторный ререндер UI при последовательных взаимосвязанных изменениях.
Асинхронные обновления
- Метод
asyncUpdate(pathOrAccessor, asyncFn, options?)
позволяет выполнять асинхронные операции (fetch, таймеры, запросы) и автоматически отменять предыдущие, если они всё ещё в процессе (опцияabortPrevious
).
- Метод
История изменений (undo/redo)
- Для каждого пути автоматически поддерживается стек исторических значений (до
maxHistoryLength
). - Методы
undo(pathOrAccessor)
иredo(pathOrAccessor)
позволяют откатиться на предыдущие/следующие значения.
- Для каждого пути автоматически поддерживается стек исторических значений (до
Интеграция с любым фреймворком
- Базовое API стора не зависит от React/Vue и т. д.
- Для React достаточно написать хук
useObservableStore
, который внутри используетstore.subscribeToPath
с Accessor’ом, и диспатчит обновление компонента. Аналогично для Vue/Svelte/Solid достаточно написать адаптер, который на изменение Proxy вызывает реактивное обновление.
Пример создания основного store
(с middleware)
// ObservableStore.ts
import {
createObservableStore,
Middleware,
Accessor,
SubscriptionCallback,
} from "./index";
// 1) Определяем начальный interface :
export interface StoreState {
user: {
name: string;
age: number;
settings: {
theme: string;
locale: string;
};
};
items: number[];
counter: number;
}
// 2) Определяем начальный стейт:
const initialState = {
user: {
name: "Alice",
age: 30,
settings: {
theme: "light",
locale: "ru",
},
},
items: [1, 2, 3],
counter: 0,
};
// 3) Глубина тип поиска строковых путей
type DepthPath = 14;
// 3) Пример middleware: простой логгер перед и после update
const loggerMiddleware: Middleware<StoreState, DepthPath> = (store, next) => {
return (path, value) => {
console.log(
`[Logger] До обновления: путь="${path}", старое значение=`,
store.get(path)
);
next(path, value);
console.log(
`[Logger] После обновления: путь="${path}", новое значение=`,
store.get(path)
);
};
};
// 3) Создаём стор с middleware и ограничением истории:
export const store = createObservableStore<State, DepthPath>(
initialState,
[loggerMiddleware], // цепочка middleware
{ maxHistoryLength: 50 }
);
// Теперь при вызове store.update(...) или при прямой записи в store.state
// сработают middleware и, при изменении, уведомятся подписчики.
Что делает DepthPath
и зачем он нужен
DepthPath
управляет тем, насколько глубоко TypeScript будет "раскрывать" вложенные свойства объекта, чтобы сгенерировать возможные строковые пути вида "user.settings.locale"
, "items.0"
и т. д.
По умолчанию стоит `DepthPath=0`.
✅ DepthPath = 0
- Типы путей не вычисляются вообще.
- Все проверки типов путей (
SafePaths
,PathExtract
,PathOrAccessor
) становятся заглушками. Значения принимаются по произвольным строкам, но:
- ❌ автокомплита нет,
- ❌ типовая проверка путей и значений отключена,
- ✅ Это может быть удобно для моков, тестов, или свободной работы без ограничений.
⚠️ DepthPath = 14
и выше
- ⚠️ Потенциально медленно: генерация union-типов путей становится экспоненциальной.
- ✅ Позволяет обращаться к глубоко вложенным путям, если они есть.
❌ Но может:
- привести к замедлению или зависанию TypeScript/IDE (VSCode),
- вызвать ошибки "Type instantiation is excessively deep..." при сложных типах.
Рекомендуется:
Не использовать
DepthPath > 10
, если только это не оправдано реально вложенными структурами.
🧠 Итог
DepthPath |
Поведение |
---|---|
0 |
Заглушка: любые строки, нет проверки и автокомплита |
1–7 |
Оптимально: безопасные пути, разумная глубина |
8–13 |
Допустимо, но уже может тормозить IDE |
14+ |
Риск перегрузки компилятора, не рекомендуется без крайней необходимости |
Что такое Accessor
и зачем он нужен
Accessor<R>
— это магическая вспомогательная функция, которая позволяет безопасно обращаться к значениям в состоянии store
, особенно когда путь к данным содержит динамическую часть (например, индекс массива или ID).
🔧 Сигнатура
export type Accessor<R> = (t: <K>(arg: K) => K) => R;
На первый взгляд выглядит странно, но суть очень простая:
Ты пишешь обычный доступ к данным, используя
store.$
, например:store.$.items[2];
А теперь представь, что
2
— это переменнаяindex
, которая может меняться:const accessor = (t) => store.$.items[t(index)];
🔮 t(...)
— просто обёртка, которая говорит системе:
«Эта часть пути — динамическая, её нужно сохранить как выражение, а потом превратить в строку.»
🧙 Что происходит под капотом
Accessor
никак не исполняется напрямую.Вместо этого
store
вызываетtoString()
наAccessor
, и получает строку вроде:"items.5";
Это делается через анализ тела функции и регулярки:
- Внутри
t(index)
→5
store.$.items[t(index)]
превращается в"items.5"
- Внутри
💡 Это очень лёгкий способ выразить динамические пути без генерации миллионов типов и union'ов, что делает работу в VSCode быстрой и безопасной.
✅ Примеры
const index = 1;
store.update((t) => store.$.items[t(index)], 999);
// Аналогично: store.update("items.1", 999)
store.get((t) => store.$.user.settings[t("locale")]);
// Аналогично: store.get("user.settings.locale")
store.subscribeToPath(
(t) => store.$.items[t(dynamicIndex)],
(val) => {
console.log("Изменился элемент массива:", val);
}
);
🧩 Почему это лучше, чем store.update("items." + index, ...)
- ✅ Работает с автодополнением
- ✅ Проверяется типами (
PathExtract
,AssertValidPath
) - ✅ Не требует ручной склейки строк
- ✅ Не нагружает IDE (в отличие от большого количества вложенных
union
-типов)
⚠️ На заметку
- Функция
Accessor
используется только как сигнатура и для парсинга, она не вызываетstore.$
реально. - Не стоит использовать сложные условия внутри неё — только прямой доступ через
t(...)
.
API createObservableStore
store.state
/ store.$
Что это: реактивный Proxy-объект с текущим состоянием (readonly снаружи).
Как пользоваться:
// Чтение: console.log(store.state.user.name); // "Alice" console.log(store.$.items.length); // 3 // Прямая запись (через Proxy) «автоматом» вызывает middleware и нотификации подписчиков: store.state.user.name = "Bob"; store.state.items.push(4);
Примечание:
store.$
— это просто синонимstore.state
. Удобно для внутрянки.
store.subscribe(callback, cacheKeys?)
Что делает: подписывает на любой апдейт всего состояния (глобальная подписка).
Параметры:
callback: (newState: typeof initialState) => void
— вызывается после каждого «батча» изменений.cacheKeys?: string[]
— массив строковых ключей (cacheKey). Если указан, уведомление придёт только тогда, когда:- изменился любой кусок стейта, и при этом один из этих cacheKeys был инвалидирован (
store.invalidate
), либо - напрямую было вызвано
store.invalidate(cacheKey)
.
- изменился любой кусок стейта, и при этом один из этих cacheKeys был инвалидирован (
Пример:
// Подписываемся на все апдейты: const unsubAll = store.subscribe((fullState) => { console.log("Весь стейт изменился:", fullState); }); // Подписка, опирающаяся на cacheKey "user.settings.theme": const unsubFiltered = store.subscribe( (fullState) => console.log("Тема пользователя:", fullState.user.settings.theme), ["user.settings.theme"] ); // Отписка: unsubAll(); unsubFiltered();
store.subscribeToPath(pathOrAccessor, callback, options?)
Что делает: подписывается на изменения конкретного поля (пути) в стейте, используя либо строку-путь, либо Accessor<T>.
Параметры:
pathOrAccessor: string | Accessor<any>
string
: например,"user.age"
или"items.0"
Accessor<any>
: функция(t?) => store.state.some.nested[t(dynamicIndex), …]
— если вам нужно подписаться, но индекс вычисляется динамически, можно передать Accessor.
callback: (newValue: any) => void
— вызывается при изменении указанного пути.options?: { immediate?: boolean; cacheKeys?: string[] }
—immediate: true
— сразу вызываем callback с текущим значением, даже до первого изменения.cacheKeys: string[]
— список cacheKeys; колбэк будет вызываться при этом событии, даже если путь не менялся напрямую (см.store.invalidate
).
Примеры:
// 1) Подписка на изменение user.name по строковому пути: const unsubName = store.subscribeToPath( "user.name", (newName) => console.log("Имя пользователя:", newName), { immediate: true } ); // 2) Подписка на первый элемент массива items: // Здесь index может меняться динамически внутри Accessor через t(index) let idx = 0; const unsubFirstItem = store.subscribeToPath( (t) => store.state.items[t(idx)], // Accessor<any> (val) => console.log("Первый элемент массива:", val), { cacheKeys: ["counter"] } ); // 3) Отписка: unsubName(); unsubFirstItem();
store.invalidate(cacheKey)
Что делает: инвалидирует указанный строковый ключ (
cacheKey
).- Всем глобальным подписчикам, которые при подписке передали этот ключ в
cacheKeys
, придёт уведомление (даже если напрямую значение по их пути не менялось).
- Всем глобальным подписчикам, которые при подписке передали этот ключ в
Пример:
// Если где-то в логике нужно форсировать оповещение по подписчикам, полагающимся на cacheKey: store.invalidate("user.settings.theme");
store.get(pathOrAccessor)
Что делает: возвращает текущее значение по указанному
pathOrAccessor
.- Если передан
string
→ возвращаетstore.state[path]
(илиundefined
, если путь не найден). - Если передан
Accessor<any>
→ внутри создаётся временная «заглушка»t
(не обязательна), запускается Accessor, и возвращается результат.
- Если передан
Пример:
const age = store.get("user.age"); // 30 console.log("Возраст:", age); // Пример с Accessor: читаем элемент массива по динамическому индексу let idx = 1; const firstItem = store.get((t) => store.state.items[t(idx)]); console.log("Второй элемент массива:", firstItem); // 2
store.update(pathOrAccessor, valueOrFn)
Что делает: синхронно обновляет значение по заданному
pathOrAccessor
.pathOrAccessor: string | Accessor<any>
- Если
string
→ обновляем конкретный ключ. - Если
Accessor<any>
→ внутри Accessor использует функциюt(...)
для вычисления пути, затем обновляет это конкретное свойство.
- Если
valueOrFn
может быть:Прямым значением:
store.update("user.age", 35);
Функцией
(cur) => next
: вычисляет новое значение на основе текущего:store.update("user.age", (cur) => cur + 1);
Если Accessor: например,
let idx = 2; store.update( (t) => store.state.items[t(idx)], (cur) => cur * 10 );
— тут
t(idx)
возвращает число2
, и обновитсяitems[2]
.
При записи:
- Сначала сохраняется старое значение в историю (до
maxHistoryLength
). - Запускаются middleware (в порядке регистрации).
- Применяется фактическое обновление.
- В конце уведомляются подписчики.
- Сначала сохраняется старое значение в историю (до
Примеры:
// 1) Обновление через строковый путь: store.update("user.age", 35); store.update("user.age", (cur) => cur + 1); // 2) Обновление через Accessor + динамический индекс: let dynamicIdx = 0; store.update((t) => store.state.items[t(dynamicIdx)], 42); // После этого items[0] станет 42. // 3) Прямые присваивания через Proxy: // Proxy автоматически делегирует на store.update store.state.user.name = "Charlie"; store.state.items[1] = 100; // → Подписчики на "user.name" и "items.1" получат нотификацию.
store.resolveValue(pathOrAccessor, valueOrFn)
Что делает: вычисляет, какое значение получится при применении
valueOrFn
, но без фактической записи в стор.- Удобно, когда нужно только узнать, как изменится значение, но ещё не применять это обновление.
Пример:
const nextCounter = store.resolveValue("counter", (cur) => cur + 5); console.log("Будет следующий counter:", nextCounter); // Но store.get("counter") остаётся прежним.
Асинхронные обновления
store.asyncUpdate(pathOrAccessor, asyncUpdater, options?)
Что делает: выполняет асинхронную функцию, передающую текущее значение и
AbortSignal
, а затем записывает результат в указанный путь.- Если указан
options.abortPrevious: true
, предыдущий незавершённый запрос по тому же пути будет отменён при помощиAbortController
.
- Если указан
Параметры:
pathOrAccessor: string | Accessor<any>
asyncUpdater: (currentValue: any, signal: AbortSignal) => Promise<any>
options?: { abortPrevious?: boolean }
Пример:
// Загрузим список с сервера и запишем в state.items: await store.asyncUpdate( "items", async (currentItems, signal) => { const response = await fetch("/api/items", { signal }); const data = await response.json(); return data.list; }, { abortPrevious: true } );
store.cancelAsyncUpdates(pathOrAccessor?)
Что делает: отменяет все «висящие» (in-flight)
asyncUpdate
вызовы.- Если указан
pathOrAccessor
, то отменяет только для этого пути, иначе для всех.
- Если указан
Пример:
// Отменить все асинхронные обновления: store.cancelAsyncUpdates(); // Отменить только для пути "items": store.cancelAsyncUpdates("items");
Батчинг (store.batch
)
store.batch(callback)
Что делает: группирует несколько изменений внутри одного блока, откладывая уведомления подписчикам до конца.
- Внутри
callback
можно использовать какstore.update(...)
, так и прямые присваивания черезstore.state
(Proxy). - После выхода из
callback
уведомления отправляются единовременно.
- Внутри
Примеры:
// 1) Через метод update: await store.batch(() => { store.update("user.name", "Charlie"); store.update("user.age", (cur) => cur + 2); store.update("items.0", 100); }); // Подписчики получат одно уведомление после всех изменений. // 2) С прямыми присваиваниями: await store.batch(() => { store.state.user.name = "Charlie"; store.state.user.age = 23; store.state.items[0] = 100; store.state.items[2] = 2323; // всё в рамках одной батчи }); // Подписчики увидят изменения по "user.name", "user.age" и "items.0", "items.2" одним колбэком.
История изменений (undo/redo)
store.undo(pathOrAccessor)
Что делает: откатывает (undo) последнее изменение по указанному пути (или Accessor).
- Если есть предыдущая запись, возвращает
true
и восстанавливает предыдущее значение. Иначе возвращаетfalse
.
- Если есть предыдущая запись, возвращает
Пример:
store.update("counter", 10); store.update("counter", 20); console.log(store.get("counter")); // 20 store.undo("counter"); console.log(store.get("counter")); // 10
store.redo(pathOrAccessor)
Что делает: повторяет (redo) последнее откатное изменение по указанному пути.
- Если есть «отменённое» значение, возвращает
true
и применяет его. Иначеfalse
.
- Если есть «отменённое» значение, возвращает
Пример:
// Продолжение предыдущего примера: store.undo("counter"); // возвращает к 10 store.redo("counter"); console.log(store.get("counter")); // 20
Статистика и очистка
store.getMemoryStats()
Что делает: возвращает объект с текущими статистическими данными:
subscribersCount
— число глобальных подписчиков.pathSubscribersCount
— число подписок по конкретным путям/Accessor’ам.historyEntries
— список всех путей и длина их истории.activePathsCount
— число активных путей (за которыми кто-то следит).
Пример:
const stats = store.getMemoryStats(); console.log("Глобальных подписчиков:", stats.subscribersCount); console.log("Подписок по путям:", stats.pathSubscribersCount); console.log("История:", stats.historyEntries);
store.clearStore()
Что делает: полностью очищает хранилище:
- Удаляет все подписки (глобальные и по путям).
- Отменяет все «висящие» асинхронные обновления.
- Очищает внутренние таймеры (если есть) и освобождает память.
Пример:
// Когда стор больше не нужен: store.clearStore();
Промежуточная обработка (Middleware)
Middleware
— это функции, которые «оборачивают» вызовы store.update(...)
и дают возможность перехватывать (модифицировать, логировать, блокировать) запросы на изменение состояния.
1. Когда срабатывает middleware
Middleware вызываются только при:
- вызове
store.update(...)
, или - прямой записи через Proxy (
store.state.some.key = newValue
).
- вызове
Если обновление обойти Proxy (например, напрямую поменять внутренний «сырой» объект вне Proxy), middleware не запустятся.
// Гарантированная активация middleware:
store.update("user.name", "Dmitry");
store.state.user.name = "Dmitry"; // Proxy перехватывает и идёт через middleware
// НЕ активирует middleware (не рекомендуется):
// (внутренний «сырый» объект здесь не трогает Proxy)
(store as any).rawState.user.name = "Eve";
2. Изменение value
внутри middleware
Внутри middleware есть доступ к исходному path
и value
. Можно изменить value
перед тем, как передать его дальше по цепочке, вызвав next(path, newValue)
.
const clampAgeMiddleware: Middleware<typeof initialState> = (store, next) => {
return (path, value) => {
if (path === "user.age") {
// Ограничиваем возраст от 0 до 99:
const clamped = Math.max(0, Math.min(99, value as number));
next(path, clamped);
} else {
next(path, value);
}
};
};
// Теперь при store.update("user.age", 150) реально попадёт 99.
3. Блокировка изменения
Если внутри middleware не вызвать next(path, value)
, весь дальнейший вызов метода update
«глохнет» — изменения не применяются, а последующие middleware не вызываются.
const blockAgeMiddleware: Middleware<typeof initialState> = (store, next) => {
return (path, value) => {
if (path === "user.age") {
console.warn("[Middleware] Изменение user.age заблокировано");
// Не вызываем next → изменение не произойдёт
return;
}
next(path, value);
};
};
// Пробуем:
store.update("user.age", 40);
// Лог: [Middleware] Изменение user.age заблокировано
// Возраст остаётся прежним
store.update("user.name", "Bob");
// Проходит нормально, потому что для "user.name" вызывается next.
4. Последовательность нескольких middleware
При создании стора можно передать массив middleware, например: [mw1, mw2, mw3]
. Порядок вызова (после реверса) таков:
- mw1 → вызывает mw2
- mw2 → вызывает mw3
- mw3 → вызывает «ядро» update
Если на каком-то этапе next
не вызывается, дальнейшие middleware и само ядро не получат управление, и стор не обновится.
const mw1: Middleware<typeof initialState> = (store, next) => {
return (path, value) => {
console.log("[MW1] До", path, value);
next(path, value);
console.log("[MW1] После", path, store.get(path));
};
};
const mw2: Middleware<typeof initialState> = (store, next) => {
return (path, value) => {
console.log("[MW2] Проверяем", path);
if (path === "items.0") {
console.log("[MW2] Блокируем изменение items.0");
return; // mw3 и ядро не выполнятся
}
next(path, value);
};
};
const mw3: Middleware<typeof initialState> = (store, next) => {
return (path, value) => {
console.log("[MW3] Логика MW3");
next(path, value);
};
};
const store = createObservableStore(initialState, [mw1, mw2, mw3]);
// Пример:
store.update("items.0", 999);
// Лог:
// [MW1] До items.0 999
// [MW2] Проверяем items.0
// [MW2] Блокируем изменение items.0
// → mw1 не продолжит после next, mw3 не вызовется, update не применится.
store.update("user.name", "Dmitry");
// Лог:
// [MW1] До user.name Dmitry
// [MW2] Проверяем user.name
// [MW3] Логика MW3
// [MW1] После user.name Dmitry
// → Значение применено.
Основные преимущества такого подхода
Фреймворк-агностичность
- Ядро стора написано «чисто» на TypeScript, без зависимостей от React/Vue/Svelte.
- Для каждого фреймворка достаточно написать адаптер (хук или плагин), который будет цепляться к
store.subscribeToPath
и диспатчить обновления UI.
Точная гранулярность подписок
- Подписки могут работать по строковому пути или через Accessor<T>, где внутри Accessor можно использовать функцию
t(…)
для динамических индексов. - Подписчики получают уведомления только по тем полям, на которые они подписаны.
- Подписки могут работать по строковому пути или через Accessor<T>, где внутри Accessor можно использовать функцию
Middleware и валидаторы
- Можно централизованно описать проверки/блокировки/трансформации значений до их записи.
- Каждый middleware может модифицировать
value
или полностью отменить обновление.
Асинхронная логика ввода-вывода
asyncUpdate
с опциейabortPrevious
позволяет элегантно обрабатывать взаимодействие с сетью, отменяя прежние запросы, если они больше не актуальны.
История, undo/redo
- Автоматический стек изменений для каждого пути. Удобно в UI для кнопок «отменить»/«вернуть».
Batching
- Позволяет сгруппировать сразу несколько взаимосвязанных обновлений, чтобы подписчики получили единое уведомление, и UI не перерендеривался по каждому мелкому изменению.
Полная поддержка TypeScript
- Тип
Accessor<T> = (t: (arg: any) => any) => T
обеспечивает автодополнение и статическую проверку при работе с вложенными путями. - Вызовы
store.get
иstore.update
с Accessor’ом позволяют точно указывать нужное свойство без хардкода строк.
- Тип
Вывод
- ObservableStore — это универсальный реактивный стор, построенный на основе JavaScript Proxy, Accessor<T> для динамических путей, granular подписок и цепочек middleware.
- Благодаря «чистому» ядру, написанному на TypeScript, его можно без изменений подключать в React, Vue 3, Svelte, Solid и другие среды: достаточно написать лёгкие адаптеры для подписки и рендеринга.
Ключевые возможности:
- Поддержка динамических путей через
Accessor<T>
, где внутри можно вызватьt(index)
для вычисления индекса. - Гранулярные подписки по точечному пути или Accessor’у.
- Middleware для валидации и логирования.
- Асинхронные обновления с отменой прошлых запросов (
asyncUpdate
). - История изменений (undo/redo) для каждого пути.
- Бэчинг (
batch
) для группировки изменений.
- Поддержка динамических путей через
Если вам нужен лёгкий, быстро работающий, максимально гибкий реактивный стор с поддержкой динамических Accessor’ов, изложенный ObservableStore предоставит все механизмы «из коробки».