@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
TypeScript
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 };