UNPKG

@mobx-sentinel/core

Version:

MobX library for non-intrusive class-based model enhancement. Acting as a sentinel, it provides change detection, reactive validation, and form integration capabilities without contamination.

567 lines (559 loc) 20.5 kB
import { IEqualsComparer } from 'mobx'; declare namespace Decorator202112 { type ClassDecorator<TFunction extends Function> = (target: TFunction) => TFunction | void; type PropertyDecorator<TObject extends Object> = (target: TObject, propertyKey: string | symbol) => void; type MethodDecorator<TObject extends Object, T> = (target: TObject, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void; type ParameterDecorator<TObject extends Object> = (target: TObject, propertyKey: string | symbol | undefined, parameterIndex: number) => void; } declare namespace Decorator202203 { type ClassDecorator<Value extends abstract new (...args: any) => any = abstract new (...args: any) => any> = (value: Value, context: ClassDecoratorContext<Value>) => Value | void; type ClassAccessorDecorator<This = any, Value = any> = (value: ClassAccessorDecoratorTarget<This, Value>, context: ClassAccessorDecoratorContext) => ClassAccessorDecoratorResult<This, Value> | void; type ClassGetterDecorator<This = any, Value = any> = (value: (this: This) => Value, context: ClassGetterDecoratorContext) => ((this: This) => Value) | void; type ClassSetterDecorator<This = any, Value = any> = (value: (this: This, value: Value) => void, context: ClassSetterDecoratorContext) => ((this: This, value: Value) => void) | void; type ClassMethodDecorator<This = any, Value extends (...p: any[]) => any = any> = (value: Value, context: ClassMethodDecoratorContext<This, Value>) => Value | void; type ClassFieldDecorator<This = any, Value extends (...p: any[]) => any = any> = (value: undefined, context: ClassFieldDecoratorContext<This, Value>) => Value | void; } /** * Key paths represent paths to access nested properties in an object * * Key paths can be either: * - A dot-notation string representing nested properties (e.g. "user.address.street") * - A special Self symbol representing the current object */ type KeyPath = KeyPath.Component | KeyPath.Self; declare namespace KeyPath { /** Branded type for key path components */ type Component = string & { __brand: "KeyPath.Component"; }; /** Branded type for a self-referencing key path */ type Self = symbol & { __brand: "KeyPath.Self"; }; /** Self path symbol */ const Self: Self; /** * Whether a key path is a self path * * Empty strings are also considered self paths. */ function isSelf(keyPath: KeyPath): keyPath is Self; /** * Build a key path from an array of keys * * @remarks * - Joins keys with dots * - Ignores null values * - Ignores empty strings * - Handles {@link KeyPath.Self} * * @returns The constructed key path */ function build(...keys: (KeyPath | string | number | null)[]): KeyPath; /** * Get the relative key path from a prefix key path * * @returns * - `null` if the key path is not a child of the prefix * - {@link KeyPath.Self} if the paths are identical * - The original path if the prefix is {@link KeyPath.Self} */ function getRelative(keyPath: KeyPath, prefixKeyPath: KeyPath): KeyPath | null; /** * Get the parent key of a key path * * @returns The parent key or a self path if the key path is a self path */ function getParentKey(keyPath: KeyPath): KeyPath; /** * Get all ancestors of a key path * * @param includeSelf Whether to include the path itself * * @returns * - {@link KeyPath.Self} for single-level paths when `includeSelf` is false * - Key paths starting from the closest ancestor and moving up to the root */ function getAncestors(keyPath: KeyPath, includeSelf?: boolean): Generator<KeyPath>; } /** * Read-only interface for {@link KeyPathMultiMap}. */ interface ReadonlyKeyPathMultiMap<T> extends Iterable<[KeyPath, T]> { /** The number of key paths */ readonly size: number; /** Whether the map contains a key path */ has(keyPath: KeyPath): boolean; /** Find exact matches for a key path */ findExact(keyPath: KeyPath): Generator<T>; /** Find prefix matches for a key path */ findPrefix(keyPath: KeyPath): Generator<T>; /** Get values for a key path */ get(keyPath: KeyPath, prefixMatch?: boolean): Set<T>; } /** * Annotation for nested objects * * @remarks * - Mixed nested modes for the same key are not allowed * * @function */ declare const nested: Decorator202112.PropertyDecorator<object> & Decorator202203.ClassGetterDecorator<object, any> & Decorator202203.ClassAccessorDecorator<object, any> & Decorator202203.ClassFieldDecorator<object, any> & { /** * Annotation for nested objects that should be hoisted to the parent object * * When using hoist mode: * - Nested objects are treated as part of the parent object * - Changes and errors from nested objects appear on the parent * * @remarks * - Multiple hoisted keys within the same inheritance chain are not allowed * * @example * ```typescript * @nested.hoist @observable private list: Sample[] = []; * // Key path for "list.0.value" becomes "0.value" * ``` * * @function */ hoist: Decorator202112.PropertyDecorator<object> & Decorator202203.ClassGetterDecorator<object, any> & Decorator202203.ClassAccessorDecorator<object, any> & Decorator202203.ClassFieldDecorator<object, any>; }; /** * A fetcher that returns all nested entries * * Key features: * - Tracks nested objects in properties, arrays, sets, and maps * - Provides access to nested objects by key path * - Supports iteration over all nested objects * - Maintains parent-child relationships * * @remarks * Symbol keys are not supported */ declare class StandardNestedFetcher<T extends object> implements Iterable<StandardNestedFetcher.Entry<T>> { #private; /** * @param target - The target object * @param transform - A function that transforms the entry to the desired type.\ * If the function returns `null`, the entry is ignored. */ constructor(target: object, transform: (entry: StandardNestedFetcher.Entry<any>) => T | null); /** Iterate over all entries */ [Symbol.iterator](): Generator<StandardNestedFetcher.Entry<T>, void, unknown>; /** Iterate over all entries for the given key path */ getForKey(keyPath: KeyPath): Generator<StandardNestedFetcher.Entry<T>, void, unknown>; /** Map of key paths to data */ get dataMap(): ReadonlyMap<KeyPath, T>; } declare namespace StandardNestedFetcher { /** * Entry representing a nested object * * @remarks * Used when iterating over nested objects to provide context about their location */ type Entry<T extends object> = { /** Name of the field containing the nested object */ readonly key: KeyPath; /** Full path to the nested object */ readonly keyPath: KeyPath; /** Data */ readonly data: T; }; } declare const createUnwatch: Decorator202112.PropertyDecorator<object> & Decorator202203.ClassGetterDecorator<object, any> & Decorator202203.ClassAccessorDecorator<object, any> & Decorator202203.ClassFieldDecorator<object, any>; declare function runInUnwatch(action: () => void): void; /** * Annotation for watching changes to a property and a getter * * `@watch` by default unwraps boxed observables, arrays, sets, and maps — meaning it uses shallow comparison. * If you don't want this behavior, use `@watch.ref` instead. * * - `@observable` and `@computed` (and their variants) are automatically assumed to be `@watched`,\ * unless `@unwatch` or `@unwatch.ref` is specified. * - `@nested` (and its variants) are considered `@watched` unless `@unwatch` is specified. * - If `@watch` and `@watch.ref` are specified for the same key (in the same inheritance chain),\ * the last annotation prevails. * * @function */ declare const watch: Decorator202112.PropertyDecorator<object> & Decorator202203.ClassGetterDecorator<object, any> & Decorator202203.ClassAccessorDecorator<object, any> & Decorator202203.ClassFieldDecorator<object, any> & { /** * Annotation for watching values with identity comparison * * It has no effect when combined with `@nested`. * * @function */ ref: Decorator202112.PropertyDecorator<object> & Decorator202203.ClassGetterDecorator<object, any> & Decorator202203.ClassAccessorDecorator<object, any> & Decorator202203.ClassFieldDecorator<object, any>; }; /** * Annotation for unwatching changes to a property and a getter * * When used as an annotation: * - Combine with `@observable`, `@computed` or `@nested` (and their variants) to stop watching changes. * - You cannot re-enable watching once `@unwatch` is specified. * * When used as a function: * - Runs a piece of code without changes being detected by Watcher. * ```typescript * unwatch(() => (model.field = "value")); * ``` * - Warning: When used inside a transaction, it only becomes 'watching' when the outermost transaction completes. * ```typescript * runInAction(() => { * unwatch(() => { * // isWatching === false * }); * // isWatching === FALSE <-- already in a transaction * unwatch(() => { * // isWatching === false * }); * }); * // isWatching === TRUE * ``` * * @function */ declare const unwatch: typeof runInUnwatch & typeof createUnwatch; declare const internalToken: unique symbol; /** * Watcher for tracking changes to observable properties * * - Automatically tracks `@observable` and `@computed` properties * - Supports `@watch` and `@watch.ref` annotations * - Can track nested objects * - Provides change detection at both property and path levels * - Can be temporarily disabled via `unwatch()` */ declare class Watcher { #private; readonly id: string; /** * Get a watcher instance for the target object. * * @remarks * - Returns existing instance if one exists for the target * - Creates new instance if none exists * - Instances are cached and garbage collected with their targets * * @throws `TypeError` if the target is not an object. */ static get<T extends object>(target: T): Watcher; /** * Get a watcher instance for the target object. * * Same as {@link Watcher.get} but returns null instead of throwing an error. */ static getSafe(target: any): Watcher | null; /** Whether Watcher is enabled in the current transaction */ static get isWatching(): boolean; private constructor(); /** * The total number of changes processed * * Observe this value to react to changes. * * @remarks * - Incremented for each change and each affected key * - Not affected by assumeChanged() * - Reset to 0 when reset() is called */ get changedTick(): bigint; /** Whether changes have been made */ get changed(): boolean; /** * The keys that have changed * * @remarks * - Does not include keys of nested objects * - Cleared when reset() is called * - Updated when properties are modified */ get changedKeys(): ReadonlySet<KeyPath>; /** * The key paths that have changed * * Keys of nested objects are included. */ get changedKeyPaths(): ReadonlySet<KeyPath>; /** Nested watchers */ get nested(): ReadonlyMap<KeyPath, Watcher>; /** * Reset the changed state * * @remarks * - Clears all changed keys * - Resets changedTick to 0 * - Clears assumeChanged flag * - Resets all nested watchers */ reset(): void; /** * Assume some changes have been made * * It only changes {@link changed} to true and does not increment {@link changedTick}. */ assumeChanged(): void; /** @internal @ignore */ [internalToken](): { didChange: (key: KeyPath) => void; }; } /** * Represents a validation error for a specific key path */ declare class ValidationError extends Error { readonly key: KeyPath; readonly keyPath: KeyPath; readonly message: string; readonly cause?: Error; /** * Create a validation error * * @param args.keyPath Path to the invalid field * @param args.reason Error message or Error object */ constructor(args: { keyPath: KeyPath; reason: string | Error; }); } /** * Builder for collecting validation errors * * Key features: * - Collects errors for multiple keys * - Can invalidate the target object itself * * @hideconstructor */ declare class ValidationErrorMapBuilder<T> { #private; /** * Add an error for a key * * @remarks * Multiple reasons can be added for the same key. * * @param key Key to invalidate * @param reason Error message or Error object */ invalidate(key: keyof T & string, reason: string | Error): void; /** * Invalidate the target object itself * * @remarks * This is useful with `@nested.hoist` to invalidate the parent object. */ invalidateSelf(reason: string | Error): void; /** Whether the builder has any errors */ get hasError(): boolean; /** * Build an immutable error map * * @internal @ignore */ static build(instance: ValidationErrorMapBuilder<any>): ReadonlyKeyPathMultiMap<ValidationError>; } /** * Make a target object validatable * * It's just a shorthand of: * ```typescript * Validator.get(target).addAsyncHandler(expr, handler, opt) * ``` * * @remarks If you're using `make(Auto)Observable`, make sure to call `makeValidatable` * after `make(Auto)Observable`. * * @param target The target object * @param expr The expression to observe * @param handler The async handler to call when the expression changes * @param opt The handler options * * @returns A function to remove the handler */ declare function makeValidatable<T extends object, Expr>(target: T, expr: () => Expr, handler: Validator.AsyncHandler<T, NoInfer<Expr>>, opt?: Validator.HandlerOptions<NoInfer<Expr>>): () => void; /** * Make a target object validatable * * It's just a shorthand of: * ```typescript * Validator.get(target).addSyncHandler(handler, opt) * ``` * * @remarks If you're using `make(Auto)Observable`, make sure to call `makeValidatable` * after `make(Auto)Observable`. * * @param target The target object * @param handler The sync handler containing observable expressions * @param opt The handler options * * @returns A function to remove the handler */ declare function makeValidatable<T extends object>(target: T, handler: Validator.SyncHandler<T>, opt?: Validator.HandlerOptions): () => void; /** * Validator for handling synchronous and asynchronous validations * * - Supports both sync and async validation handlers * - Tracks validation state (isValidating) * - Provides error access by key path * - Supports nested validators */ declare class Validator<T> { #private; static defaultDelayMs: number; readonly id: string; /** * Get a validator instance for the target object. * * @remarks * - Returns existing instance if one exists for the target * - Creates new instance if none exists * - Instances are cached and garbage collected with their targets * * @throws `TypeError` if the target is not an object. */ static get<T extends object>(target: T): Validator<T>; /** * Get a validator instance for the target object. * * Same as {@link Validator.get} but returns null instead of throwing an error. */ static getSafe<T>(target: T): Validator<T> | null; private constructor(); /** Whether no errors are found */ get isValid(): boolean; /** The number of invalid keys */ get invalidKeyCount(): number; /** * The keys that have errors * * Keys of nested objects are NOT included. */ get invalidKeys(): ReadonlySet<KeyPath>; /** The number of invalid key paths */ get invalidKeyPathCount(): number; /** * The key paths that have errors * * Keys of nested objects are included. */ get invalidKeyPaths(): ReadonlySet<KeyPath>; /** Get the first error message (including nested objects) */ get firstErrorMessage(): string | null; /** Get error messages for the key path */ getErrorMessages(keyPath: KeyPath, prefixMatch?: boolean): Set<string>; /** Check if the validator has errors for the key path */ hasErrors(keyPath: KeyPath, prefixMatch?: boolean): boolean; /** * Find errors for the key path * * - Can do exact or prefix matching * - Returns all errors that match the key path * - Includes errors from nested validators when using prefix match */ findErrors(searchKeyPath: KeyPath, prefixMatch?: boolean): Generator<[keyPath: KeyPath, error: ValidationError], void, unknown>; /** * The number of pending/running reactions. */ get reactionState(): number; /** * The number of pending/running async jobs. */ get asyncState(): number; /** Whether the validator is computing errors */ get isValidating(): boolean; /** Nested validators */ get nested(): ReadonlyMap<KeyPath, Validator<any>>; /** * Reset the validator * * Use with caution.\ * Since validation is reactive, errors won't reappear until you make some changes. */ reset(): void; /** * Update the errors immediately * * @returns A function to remove the errors */ updateErrors(key: symbol, handler: Validator.InstantHandler<T>): () => void; /** * Add a sync handler * * @param handler The sync handler containing observable expressions * * @returns A function to remove the handler * * @remarks * - Handler runs immediately when added for initial validation * - Handler is called when observable expressions within it change * - Changes are throttled by default delay */ addSyncHandler(handler: Validator.SyncHandler<T>, opt?: Validator.HandlerOptions): () => void; /** * Add an async handler * * @param expr The expression to observe * @param handler The async handler to call when the expression changes * @param opt The handler options * * @returns A function to remove the handler * * @remarks * - Handler runs immediately when added for initial validation * - Handler is called when the watched expression changes * - Changes are throttled by default delay * - Provides abort signal for cancellation * - Previous validations are aborted when new ones start */ addAsyncHandler<Expr>(expr: () => Expr, handler: Validator.AsyncHandler<T, NoInfer<Expr>>, opt?: Validator.HandlerOptions<NoInfer<Expr>>): () => void; } declare namespace Validator { /** * Async handler * * @param expr The expression observed * @param builder The builder to build the errors * @param abortSignal The abort signal */ type AsyncHandler<T, Expr> = (expr: Expr, builder: ValidationErrorMapBuilder<T>, abortSignal: AbortSignal) => Promise<void>; /** * Sync handler * * @param builder The builder to build the errors */ type SyncHandler<T> = (builder: ValidationErrorMapBuilder<T>) => void; /** * Instant handler * * @param builder The builder to build the errors */ type InstantHandler<T> = (builder: ValidationErrorMapBuilder<T>) => void; /** Handler options */ type HandlerOptions<Expr = unknown> = { /** * Whether to run the handler immediately * * @default true */ initialRun?: boolean; /** * Throttle reaction. [milliseconds] * * @default 100 */ delayMs?: number; /** * The equality comparer for the expression. * * Only effective for async handlers. */ equals?: IEqualsComparer<Expr>; }; } export { KeyPath, StandardNestedFetcher, ValidationError, ValidationErrorMapBuilder, Validator, Watcher, makeValidatable, nested, unwatch, watch };