@qtpy/state-management-observable
Version:
1. [Основная идея и архитектура](#основная-идея-и-архитектура) 1.1. [Прозрачная реактивность](#прозрачная-реактивность) 1.2. [Поддержка типизированных строковых ключей](#поддержка-типизированных-строковых-ключей) 1.3. [Подписки с «гранул
305 lines (304 loc) • 21.7 kB
TypeScript
/**
* Примитивные типы, не участвующие в рекурсивной генерации путей.
*/
export type MaxDepth = 0;
/**a
* Генерация строковых путей к значениям в массиве с поддержкой кортежей
*/
type Range<N extends number, Acc extends number[] = []> = Acc['length'] extends N ? Acc[number] : Range<N, [...Acc, Acc['length']]>;
type NumberToString<N extends number> = `${N}`;
export type LiteralIndices<N extends number = 1> = NumberToString<Range<N>>;
/**
* Генерирует объединение всех кортежей длиной от 0 до N включительно.
* Например: TupleUpTo<2, number> → [] | [number] | [number, number]
*/
export type TupleUpTo<T, N extends number, R extends unknown[] = [], U = never> = R['length'] extends N ? U | R : TupleUpTo<T, N, [T, ...R], U | R>;
/**
* Утилита для извлечения типа элемента массива/кортежа
*/
export type IsTuple<T> = T extends readonly any[] ? (number extends T['length'] ? false : true) : false;
export type ArrayPaths<T, Depth extends number> = Depth extends 0 ? TypeError<'Maximum depth exceeded in array path'> : T extends readonly (infer U)[] ? IsTuple<T> extends true ? TupleArrayPaths<T, U, Depth> : RegularArrayPaths<U, Depth> : TypeError<'ArrayPaths used on a non-array type'>;
type TupleArrayPaths<T extends readonly any[], U, Depth extends number> = LiteralIndices<T['length']> | (Paths<U, Decrement<Depth>> extends infer Sub ? Sub extends string ? `${LiteralIndices<T['length']>}.${Sub}` : TypeError<'Invalid subpath inside tuple array'> : TypeError<'Failed to infer subpaths from tuple array'>);
type RegularArrayPaths<U, Depth extends number> = LiteralIndices | (Paths<U, Decrement<Depth>> extends infer Sub ? Sub extends string ? `${LiteralIndices}.${Sub}` : TypeError<'Invalid subpath inside regular array'> : TypeError<'Failed to infer subpaths from regular array'>);
/**
* Рекурсивная генерация строковых путей к полям объекта с улучшенной поддержкой массивов
*/
type IsTypeError<T> = T extends TypeError<any> ? true : false;
type IfValid<T, Fallback = never> = IsTypeError<T> extends true ? Fallback : T;
export type ObjectPaths<T, Depth extends number> = Depth extends 0 ? TypeError<'Maximum depth exceeded in object path'> : {
[K in keyof T & string]: K | (T[K] extends Primitive ? never : T[K] extends readonly any[] ? IfValid<ArrayPaths<T[K], Decrement<Depth>>, TypeError<`Invalid nested array path at key '${K}'`>> extends infer Sub ? Sub extends string ? `${K}.${Sub}` : TypeError<`Invalid array subpath at '${K}'`> : never : IfValid<Paths<T[K], Decrement<Depth>>, TypeError<`Invalid nested object path at key '${K}'`>> extends infer Sub ? Sub extends string ? `${K}.${Sub}` : TypeError<`Invalid object subpath at '${K}'`> : never);
}[keyof T & string];
export type Paths<T, Depth extends number = MaxDepth> = Depth extends 0 ? never : T extends Primitive ? never : T extends readonly any[] ? ArrayPaths<T, Depth> : ObjectPaths<T, Depth>;
type TypeError<Message extends string> = {
__error__: Message;
};
export type ValidUpdateValue<T, P extends string, D extends number> = ExtractPathType<T, P, D> extends TypeError<any> ? TypeError<'Invalid value type for this path'> : SafeExtract<T, P, D> | SafeUpdateFn<T, P, D>;
export type PathDepth<P extends string, Acc extends any[] = []> = P extends `${string}.${infer Rest}` ? PathDepth<Rest, [1, ...Acc]> : Acc['length'] extends 0 ? 1 : Acc['length'];
export type PathTooDeep<P extends string, D extends number> = PathDepth<P> extends infer PD extends number ? PD extends Decrement<D> | D ? false : true : true;
export type FilteredPaths<T, D extends number> = SafePaths<T, D> extends infer P ? P extends string ? PathOrError<T, P, D> extends string ? P : never : never : never;
export type PathOrError<T, P, D extends number> = D extends 0 ? string : P extends SafePaths<T, D> ? P : P extends `${string}.${infer Rest}` ? PathTooDeep<P, D> extends true ? TypeError<`Invalid path "${P}": unknown key or index "${Rest}"`> : string : SafePaths<T, D>;
type IsPrimitive<T> = T extends Primitive ? true : false;
export type AssertValueAssignable<T, P extends string, D extends number, V> = ExtractPathType<T, P, D> extends infer Extracted ? Extracted extends TypeError<any> ? Extracted : IsPrimitive<Extracted> extends true ? V extends Extracted ? V : TypeError<`Invalid value type for path "${P}"`> : IsPrimitive<V> extends true ? TypeError<`Cannot assign primitive to object path "${P}"`> : V extends Extracted ? V : TypeError<`Invalid value type for path "${P}"`> : TypeError<`Failed to infer type for path "${P}"`>;
export type PathExtract<T, D extends number, P extends string, V = undefined> = D extends 0 ? any : P extends SafePaths<T, D> ? V extends undefined ? ExtractPathType<T, P, D> : AssertValueAssignable<T, P, D, V> : TypeError<`Invalid path "${P}". This path does not exist in the state object.`>;
/**
* Улучшенное извлечение типа по пути с поддержкой кортежей
*/
export type ExtractPathType<T, P extends string, Depth extends number> = Depth extends 0 ? TypeError<'Maximum depth exceeded'> : P extends `${infer K}.${infer Rest}` ? T extends readonly [...infer Elements] ? K extends keyof Elements ? ExtractPathType<Elements[K], Rest, Decrement<Depth>> : K extends `${number}` ? Elements extends readonly (infer U)[] ? ExtractPathType<U, Rest, Decrement<Depth>> : TypeError<`Index "${K}" is invalid`> : TypeError<`Index "${K}" out of bounds in tuple`> : T extends readonly any[] ? K extends `${number}` ? T[number] extends infer U ? ExtractPathType<U, Rest, Decrement<Depth>> : never : TypeError<`Invalid array index "${K}"`> : K extends keyof T ? ExtractPathType<T[K], Rest, Decrement<Depth>> : TypeError<`Key "${K}" does not exist in object`> : T extends readonly [...infer Elements] ? P extends keyof Elements ? Elements[P] : P extends `${number}` ? Elements extends readonly (infer U)[] ? U : TypeError<`Index "${P}" is invalid`> : TypeError<`Index "${P}" out of bounds in tuple`> : T extends readonly any[] ? P extends `${number}` ? T[number] : TypeError<`Invalid array index "${P}"`> : P extends keyof T ? T[P] : TypeError<`Key "${P}" does not exist in object`>;
export type Decrement<N extends number> = N extends 12 ? 11 : N extends 11 ? 10 : N extends 10 ? 9 : N extends 9 ? 8 : N extends 8 ? 7 : N extends 7 ? 6 : N extends 6 ? 5 : N extends 5 ? 4 : N extends 4 ? 3 : N extends 3 ? 2 : N extends 2 ? 1 : N extends 1 ? 0 : never;
export type SafePaths<T, D extends number> = D extends 0 ? string : Paths<T, D>;
export type SafeExtract<T, P, D extends number> = D extends 0 ? any : ExtractPathType<T, P & string, D>;
export type SafeUpdateFn<T, P, D extends number> = D extends 0 ? (prev: any) => any : (prev: SafeExtract<T, P, D>) => SafeExtract<T, P, D>;
export type AccessorSafeExtract<T> = T extends Accessor<T, infer R> ? R : any;
/**
* Тип функции-доступа (Accessor), возвращающей значение из состояния.
* Например: (t) => state.user.board[0][t(dynamicValue)]
*/
export type Accessor<T, R = any> = ($: T, t: <K>(arg: K) => K) => R;
/**
* Примитивные типы значений, поддерживаемые хранилищем.
*/
export type Primitive = string | number | boolean | symbol | null | undefined;
export type ValueOrFn<V> = ((value: V) => V) | V;
export type ExtractPathReturn<T, P extends PathOrAccessor<T, D>, D extends number = MaxDepth> = P extends Accessor<T, infer V> ? V : P extends PathOrError<T, infer S, D> ? S extends string ? PathExtract<T, D, S> : any : any;
export type CacheKeys<T, P extends readonly PathOrAccessor<T, D>[], D extends number = MaxDepth> = {
[K in keyof P]: P[K] extends Accessor<T, infer V> ? V : P[K] extends PathOrError<T, infer S, D> ? S extends string ? PathExtract<T, D, S> : never : never;
};
export type PathOrAccessor<T, D extends number = MaxDepth> = PathOrError<T, string, D> | Accessor<T, any>;
/**
* Колбэк для подписки на изменения.
* @template T - Тип данных, передаваемых в колбэк.
*/
export type Subscriber<T> = (value: T) => void;
/**
* Функция для отписки.
*/
export type Unsubscribe = () => void;
/**
* Сигнатура функции обновления хранилища.
* Принимает путь (строка или Accessor) и новое значение.
*/
export type UpdateFunction<T, D extends number = MaxDepth> = <const P extends PathOrAccessor<T, D>>(path: P, value: ExtractPathReturn<T, P, D>) => void;
/**
* Интерфейс middleware (промежуточной обёртки) для update-функции.
* Позволяет перехватывать вызовы store.update.
*
* @template T - Тип корневого состояния.
*/
export type Middleware<T, D extends number = MaxDepth> = (store: ObservableStore<T, D>, next: UpdateFunction<T, D>) => UpdateFunction<T, D>;
/**
* Статистика использования памяти и подписок в хранилище.
*/
export type MemoryStats = {
/** Количество глобальных подписчиков (subscribe). */
subscribersCount: number;
/** Количество подписок на конкретные пути (subscribeToPath). */
pathSubscribersCount: number;
/** Список записей истории: для каждого пути длина истории. */
historyEntries: Array<{
path: string;
length: number;
}>;
/** Количество активных путей, на которые подписались. */
activePathsCount: number;
};
/**
* Мета-данные для подписки.
*/
export type SubscriptionMeta = {
/** Активна ли подписка. */
active: boolean;
/** Сет строковых путей, отслеживаемых подпиской. */
trackedPaths: Set<string>;
/** Необязательный набор нормализованных cacheKeys для фильтрации. */
cacheKeys?: Set<string>;
};
export type MetaWeakMap = WeakMap<object, MetaData>;
export type MetaData = {
_prevSignature?: any;
[key: string]: any;
};
export type PathLimitEntry<T, D extends number> = [PathOrAccessor<T, D>, number];
/**
* Интерфейс реактивного хранилища состояния.
*
* @template T - Тип состояния
*/
export interface ObservableStore<T, D extends number = MaxDepth> {
/** Прокси-объект состояния. */
readonly $: T;
/**
* Вернуть "сырой" state без Proxy.
*/
getRawStore(): T;
/**
* Форсировать обновление всех подписчиков, как будто изменилось всё состояние.
*/
invalidateAll(): void;
/**
* Полностью заменить "сырой" state.
*
* @param {T} newState - Новый объект состояния.
* @param options.keepQuiet - Если true, не триггерить подписчиков.
*/
setRawStore(newState: T, options?: {
keepQuiet?: boolean;
}): void;
/**
* Очищает историю изменений (undo/redo) для указанного пути в состоянии.
*
* @param {P} pathOrAccessor - Accessor-функция или строка, указывающая на свойство, историю которого нужно очистить.
* @param {'undo' | 'redo'} [mode] - Определяет, какую часть истории очистить: только 'undo', только 'redo' или обе если указать 'all').
* @param spliceIndices - Опционально: индексы [start, deleteCount] для удаления элементов из указанного стека.
*/
clearHistoryPath<const P extends PathOrAccessor<T, D>>(pathOrAccessor: P, mode?: 'redo' | 'undo' | 'all', spliceIndices?: [number, number]): void;
/**
* Полностью очищает всю историю изменений (undo/redo) для всех путей в состоянии.
*/
clearAllHistory(): void;
/**
* Подписка на изменения состояния (глобально).
* @param callback - Колбэк, вызываемый при любом изменении состояния.
* @param cacheKeys - Ключи кэша для фильтрации уведомлений (необязательно).
*/
subscribe(callback: Subscriber<T>, cacheKeys?: readonly PathOrAccessor<T, D>[]): Unsubscribe;
/**
* Подписка на изменения по строковому пути.
* @param path - Accessor-функция или строка.
* @param callback - Колбэк, вызываемый при изменении значения по этому пути.
* @param options - Опции подписки (immediate вызов, cacheKeys).
*/
subscribeToPath<const P extends PathOrAccessor<T, D>>(path: P, callback: (value: ExtractPathReturn<T, P, D>) => void, options?: {
immediate?: boolean;
cacheKeys?: readonly PathOrAccessor<T, D>[];
}): Unsubscribe;
/**
* Получить значение по строковому пути.
* @param path - Accessor-функция или строка, представляющая путь в объекте состояния.
* @returns Значение по указанному пути.
*/
get<const P extends PathOrAccessor<T, D>>(path: P): ExtractPathReturn<T, P, D>;
update: {
/**
* Обновить значение.
* @param path - Accessor-функция или путь к значению.
* @param valueOrFn - Новое значение или функция обновления.
* @param options - Дополнительные параметры.
* @param option.keepQuiet - Если true, обновление не будет уведомлять подписчиков.
*/
<const P extends PathOrAccessor<T, D>>(path: P, valueOrFn: ValueOrFn<ExtractPathReturn<T, P, D>>, options?: {
keepQuiet?: boolean;
}): void;
/**
* Обновить значение без уведомлений подписок.
* @param path - Accessor-функция или путь к значению.
* @param valueOrFn - Новое значение или функция обновления.
*/
quiet: <const P extends PathOrAccessor<T, D>>(path: P, valueOrFn: ValueOrFn<ExtractPathReturn<T, P, D>>) => void;
};
/**
* Создаёт отложенный вызов функции с debounce.
* @param callback - Функция, которая будет вызвана с задержкой, принимающая произвольные аргументы.
* @param delay - Задержка в миллисекундах перед выполнением callback.
* @returns Функция, которая принимает аргументы для callback и выполняет его с задержкой.
* Содержит метод `cancel` для отмены отложенного вызова.
*/
debounced<T extends any[]>(callback: (...args: T) => void, delay: number): ((...args: T) => void) & {
cancel: () => void;
};
/**
* Вычислить новое значение без его установки по строковому пути.
* @param path - Accessor-функция или путь к значению.
* @param valueOrFn - Новое значение или функция вычисления.
* @returns Предполагаемое значение.
*/
resolveValue<const P extends PathOrAccessor<T, D>>(path: P, valueOrFn: ValueOrFn<P>): ExtractPathReturn<T, P, D>;
/**
* Преобразует путь или Accessor-функцию в строковый путь для работы с хранилищем.
* @param path - Accessor-функция или путь к значению.
*/
resolvePath<const P extends PathOrAccessor<T, D>>(path: P): string;
/**
* Отменить все отложенные (асинхронные) обновления.
*/
cancelAsyncUpdates(): void;
/**
* Отменить отложенные обновления по указанному пути.
* @param path - Строковый путь или Accessor.
*/
cancelAsyncUpdates<const P extends PathOrAccessor<T, D>>(path: P): void;
/**
* Асинхронно обновить значение по строковому пути.
* @param path - Accessor-функция, Путь к значению.
* @param asyncUpdater - Функция, возвращающая промис нового значения.
* @param options - Опции (например, отмена предыдущих).
*/
asyncUpdate: {
<const P extends PathOrAccessor<T, D>, E = ExtractPathReturn<T, P, D>>(path: P, asyncUpdater: (current: E, signal: AbortSignal) => Promise<E>, options?: {
abortPrevious?: boolean;
keepQuiet?: boolean;
}): Promise<void>;
quiet: <const P extends PathOrAccessor<T, D>, E = ExtractPathReturn<T, P, D>>(path: P, asyncUpdater: (current: E, signal: AbortSignal) => Promise<E>, options?: {
abortPrevious?: boolean;
}) => Promise<void>;
};
/**
* Выполняет откат изменений по указанному пути.
* @param path - Путь или Accessor.
* @param spliceIndices - Опционально: индексы [start, deleteCount] для удаления элементов из undo-стека.
* @returns Значение после отката или undefined, если откат невозможен.
*/
undo<const P extends PathOrAccessor<T, D>>(path: P, spliceIndices?: [number, number]): boolean;
/**
* Выполняет откат изменений по указанному пути.
* @param path - Путь или Accessor.
* @param spliceIndices - Опционально: индексы [start, deleteCount] для удаления элементов из undo-стека.
* @returns Значение после отката или undefined, если откат невозможен.
*/
redo<const P extends PathOrAccessor<T, D>>(path: P, spliceIndices?: [number, number]): boolean;
/**
* Получить значение из undo-истории на указанное количество шагов назад.
*
* @param path - Строковый путь или Accessor-функция к значению.
* @param step - Количество шагов назад (0 — текущее значение, 1 — предыдущее и т.д.).
* @returns Значение на указанном шаге в истории или undefined, если шаг вне границ.
*/
getUndo<const P extends PathOrAccessor<T, D>>(path: P, step: number): ExtractPathReturn<T, P, D> | undefined;
/**
* Получить значение из redo-истории на указанное количество шагов вперёд.
*
* @param path - Строковый путь или Accessor-функция к значению.
* @param step - Количество шагов вперёд (0 — следующий откат, 1 — через один и т.д.).
* @returns Значение на указанном шаге в redo или undefined, если шаг вне границ.
*/
getRedo<const P extends PathOrAccessor<T, D>>(path: P, step: number): ExtractPathReturn<T, P, D> | undefined;
/**
* Получить полную историю изменений (undo и redo) по пути.
*
* @param path - Строковый путь или Accessor-функция к значению.
* @returns Объект с массивами undo и redo.
*/
getHistory<const P extends PathOrAccessor<T, D>>(path: P): {
undo: ExtractPathReturn<T, P, D>[];
redo: ExtractPathReturn<T, P, D>[];
};
/**
* Принудительно вызвать обновления по cacheKey.
* @param cacheKey - Ключ или набор ключей.
*/
invalidate<const P extends PathOrAccessor<T, D>>(cacheKey: P): void;
/**
* Выполнить пакетное обновление состояния.
* @param callback - Функция с обновлениями.
*/
batch(callback: () => void): Promise<void> | void;
/**
* Получить статистику использования хранилища.
* @returns Объект со статистикой.
*/
getMemoryStats(): MemoryStats;
/**
* Полностью очистить хранилище: подписки, таймеры и т.д.
*/
clearStore(): void;
}
export {};