UNPKG

mobx-keystone

Version:

A MobX powered state management solution based on data trees with first class support for TypeScript, snapshots, patches and much more

511 lines (448 loc) 13.8 kB
import type { SnapshotInOf, SnapshotOutOf } from "../snapshot/SnapshotOf" import type { LateTypeChecker, TypeChecker } from "../types/TypeChecker" import { getOrCreate } from "../utils/mapUtils" import type { Flatten, IsNeverType, IsOptionalValue } from "../utils/types" /** * @ignore */ export const noDefaultValue = Symbol("noDefaultValue") /** * A model property. */ export interface ModelProp< TPropValue, TPropCreationValue, TTransformedValue, TTransformedCreationValue, TIsRequired, TIsId extends boolean = false, THasSetter = never, TFromSnapshotOverride = never, TToSnapshotOverride = never, > { $valueType: TPropValue $creationValueType: TPropCreationValue $transformedValueType: TTransformedValue $transformedCreationValueType: TTransformedCreationValue $isRequired: TIsRequired $isId: TIsId $hasSetter: THasSetter $fromSnapshotOverride: TFromSnapshotOverride $toSnapshotOverride: TToSnapshotOverride _defaultFn: (() => TPropValue) | typeof noDefaultValue _defaultValue: TPropValue | typeof noDefaultValue _typeChecker: TypeChecker | LateTypeChecker | undefined _setter: boolean | "assign" _isId: boolean _transform: | { transform: ( original: unknown, model: object, propName: PropertyKey, setOriginalValue: (newOriginalValue: unknown) => void ) => unknown untransform: (transformed: unknown, model: object, propName: PropertyKey) => unknown } | undefined _fromSnapshotProcessor?: (sn: unknown) => unknown _toSnapshotProcessor?: (sn: unknown) => unknown withSetter(): ModelProp< TPropValue, TPropCreationValue, TTransformedValue, TTransformedCreationValue, TIsRequired, TIsId, string, TFromSnapshotOverride, TToSnapshotOverride > /** * @deprecated Setter methods are preferred. */ withSetter( mode: "assign" ): ModelProp< TPropValue, TPropCreationValue, TTransformedValue, TTransformedCreationValue, TIsRequired, TIsId, string, TFromSnapshotOverride, TToSnapshotOverride > /** * Sets a transform for the property instance value. * * @template TTV Transformed value type. * @param transform Transform to be used. * @returns */ withTransform<TTV>( transform: ModelPropTransform<NonNullable<TPropValue>, TTV> ): ModelProp< TPropValue, TPropCreationValue, TTV | Extract<TPropValue, null | undefined>, TTV | Extract<TPropCreationValue, null | undefined>, TIsRequired, TIsId, THasSetter, TFromSnapshotOverride, TToSnapshotOverride > withSnapshotProcessor< FS = TFromSnapshotOverride, TS = TToSnapshotOverride, This extends AnyModelProp = this, >(processor: { fromSnapshot?: (sn: FS) => ModelPropFromSnapshot<This> toSnapshot?: (sn: ModelPropToSnapshot<This>) => TS }): ModelProp< TPropValue, TPropCreationValue, TTransformedValue, TTransformedCreationValue, TIsRequired, TIsId, THasSetter, FS, TS > } /** * The snapshot in type of a model property. */ export type ModelPropFromSnapshot<MP extends AnyModelProp> = IsNeverType< MP["$fromSnapshotOverride"], SnapshotInOf<MP["$creationValueType"]>, MP["$fromSnapshotOverride"] > /** * The snapshot out type of a model property. */ export type ModelPropToSnapshot<MP extends AnyModelProp> = IsNeverType< MP["$toSnapshotOverride"], SnapshotOutOf<MP["$valueType"]>, MP["$toSnapshotOverride"] > /** * A model prop transform. */ export interface ModelPropTransform<TOriginal, TTransformed> { transform(params: { originalValue: TOriginal cachedTransformedValue: TTransformed | undefined setOriginalValue(value: TOriginal): void }): TTransformed untransform(params: { transformedValue: TTransformed cacheTransformedValue: () => void }): TOriginal } /** * Any model property. */ export type AnyModelProp = ModelProp<any, any, any, any, any, any, any, any, any> /** * Model properties. */ export interface ModelProps { [k: string]: AnyModelProp } export type RequiredModelProps<MP extends ModelProps> = { [K in keyof MP]: MP[K]["$isRequired"] & K }[keyof MP] export type ModelPropsToUntransformedData<MP extends ModelProps> = Flatten<{ [k in keyof MP]: MP[k]["$valueType"] }> export type ModelPropsToSnapshotData<MP extends ModelProps> = Flatten<{ [k in keyof MP]: ModelPropToSnapshot<MP[k]> extends infer R ? R : never }> // we don't use O.Optional anymore since it generates unions too heavy // also if we use Pick over the optional props we will loose the ability to infer generics // we also don't use Flatten because if we do some generics won't work export type ModelPropsToUntransformedCreationData<MP extends ModelProps> = { [k in keyof MP]?: MP[k]["$creationValueType"] } & { [k in RequiredModelProps<MP>]: MP[k]["$creationValueType"] } // we don't use O.Optional anymore since it generates unions too heavy // also if we use Pick over the optional props we will loose the ability to infer generics export type ModelPropsToSnapshotCreationData<MP extends ModelProps> = Flatten< { [k in keyof MP]?: ModelPropFromSnapshot<MP[k]> extends infer R ? R : never } & { [k in { [K in keyof MP]: IsNeverType< MP[K]["$fromSnapshotOverride"], MP[K]["$isRequired"] & K, // no override IsOptionalValue<MP[K]["$fromSnapshotOverride"], never, K> // with override > }[keyof MP]]: ModelPropFromSnapshot<MP[k]> extends infer R ? R : never } > export type ModelPropsToTransformedData<MP extends ModelProps> = Flatten<{ [k in keyof MP]: MP[k]["$transformedValueType"] }> // we don't use O.Optional anymore since it generates unions too heavy // also if we use Pick over the optional props we will loose the ability to infer generics // we also don't use Flatten because if we do some generics won't work // we also don't use Omit because if we do some generics won't work export type ModelPropsToTransformedCreationData<MP extends ModelProps> = { [k in keyof MP]?: MP[k]["$transformedCreationValueType"] } & { [k in RequiredModelProps<MP>]: MP[k]["$transformedCreationValueType"] } export type ModelPropsToSetter<MP extends ModelProps> = Flatten<{ [k in keyof MP as MP[k]["$hasSetter"] & `set${Capitalize<k & string>}`]: ( value: MP[k]["$transformedValueType"] ) => void }> export type ModelIdProp<T extends string = string> = ModelProp< T, T | undefined, T, T | undefined, never, // not required true > /** * A property that will be used as model id, accessible through $modelId. * Can only be used in models and there can be only one per model. */ export const idProp = { _setter: false, _isId: true, withSetter(mode?: boolean | "assign") { const obj: AnyModelProp = Object.create(this) obj._setter = mode ?? true return obj }, typedAs() { return idProp }, } as any as ModelIdProp & { /** * Same as `idProp`, except that it might have an specific TypeScript string template as type. * E.g. `typedIdProp<`custom-${string}`>()` */ typedAs<T extends string>(): ModelIdProp<T> } /** * @ignore */ export type OnlyPrimitives<T> = Exclude<T, object> /** * A model prop that maybe / maybe not is optional, depending on if the value can take undefined. */ export type MaybeOptionalModelProp<TPropValue> = ModelProp< TPropValue, TPropValue, TPropValue, TPropValue, IsOptionalValue<TPropValue, never, string> // calculate if required > /** * A model prop that is definitely optional. */ export type OptionalModelProp<TPropValue> = ModelProp< TPropValue, TPropValue | null | undefined, TPropValue, TPropValue | null | undefined, never // not required > const baseProp: AnyModelProp = { ...({} as Pick< AnyModelProp, | "$valueType" | "$creationValueType" | "$transformedValueType" | "$transformedCreationValueType" | "$isRequired" | "$isId" | "$hasSetter" | "$fromSnapshotOverride" | "$toSnapshotOverride" >), _defaultFn: noDefaultValue, _defaultValue: noDefaultValue, _typeChecker: undefined, _setter: false, _isId: false, _transform: undefined, _fromSnapshotProcessor: undefined, _toSnapshotProcessor: undefined, withSetter(mode?: boolean | "assign") { const obj: AnyModelProp = Object.create(this) obj._setter = mode ?? true return obj }, withTransform(transform: ModelPropTransform<unknown, unknown>) { const obj: AnyModelProp = Object.create(this) obj._transform = toFullTransform(transform) return obj }, withSnapshotProcessor({ fromSnapshot, toSnapshot }) { let newFromSnapshot: ((sn: any) => any) | undefined if (this._fromSnapshotProcessor && fromSnapshot) { const oldFn = this._fromSnapshotProcessor const newFn = fromSnapshot newFromSnapshot = (sn: any) => oldFn(newFn(sn)) } else if (fromSnapshot) { newFromSnapshot = fromSnapshot } else { newFromSnapshot = this._fromSnapshotProcessor } let newToSnapshot: ((sn: any) => any) | undefined if (this._toSnapshotProcessor && toSnapshot) { const oldFn: (sn: unknown) => any = this._toSnapshotProcessor const newFn = toSnapshot newToSnapshot = (sn: unknown) => newFn(oldFn(sn)) } else if (toSnapshot) { newToSnapshot = toSnapshot } else { newToSnapshot = this._toSnapshotProcessor } const obj: AnyModelProp = Object.create(this) obj._fromSnapshotProcessor = newFromSnapshot obj._toSnapshotProcessor = newToSnapshot return obj }, } /** * Defines a model property, with an optional function to generate a default value * if the input snapshot / model creation data is `null` or `undefined`. * * Example: * ```ts * x: prop(() => 10) // an optional number, with a default value of 10 * x: prop<number[]>(() => []) // an optional number array, with a default empty array * ``` * * @template TValue Value type. * @param defaultFn Default value generator function. * @returns */ export function prop<TValue>(defaultFn: () => TValue): OptionalModelProp<TValue> /** * Defines a model property, with an optional default value * if the input snapshot / model creation data is `null` or `undefined`. * You should only use this with primitive values and never with object values * (array, model, object, etc). * * Example: * ```ts * x: prop(10) // an optional number, with a default value of 10 * ``` * * @template TValue Value type. * @param defaultValue Default primitive value. * @returns */ export function prop<TValue>(defaultValue: OnlyPrimitives<TValue>): OptionalModelProp<TValue> /** * Defines a model property with no default value. * * Example: * ```ts * x: prop<number>() // a required number * x: prop<number | undefined>() // an optional number, which defaults to undefined * ``` * * @template TValue Value type. * @returns */ export function prop<TValue>(): MaybeOptionalModelProp<TValue> // base export function prop(def?: any): AnyModelProp { const hasDefaultValue = arguments.length > 0 if (!hasDefaultValue) { return baseProp } let p = propCache.get(def) if (!p) { p = Object.create(baseProp) if (typeof def === "function") { p!._defaultFn = def } else { p!._defaultValue = def } propCache.set(def, p!) } return p! } const propCache = new Map<unknown, AnyModelProp>() let cacheTransformResult = false const cacheTransformedValueFn = () => { cacheTransformResult = true } function toFullTransform(transformObject: ModelPropTransform<unknown, unknown>) { const cache = new WeakMap< object, Map<PropertyKey, { originalValue: unknown; transformedValue: unknown }> >() const transform = (params: { originalValue: unknown cachedTransformedValue: unknown setOriginalValue(newOriginalValue: unknown): void }) => (params.originalValue == null ? params.originalValue : transformObject.transform(params)) const untransform = (params: { transformedValue: unknown; cacheTransformedValue(): void }) => params.transformedValue == null ? params.transformedValue : transformObject.untransform(params) return { transform( originalValue: unknown, model: object, propName: PropertyKey, setOriginalValue: (newOriginalValue: unknown) => void ) { const modelCache = getOrCreate(cache, model, () => new Map()) let propCache = modelCache.get(propName) if (propCache?.originalValue !== originalValue) { // original changed, invalidate cache modelCache.delete(propName) propCache = undefined } const transformedValue = transform({ originalValue, cachedTransformedValue: propCache?.transformedValue, setOriginalValue, }) modelCache.set(propName, { originalValue, transformedValue, }) return transformedValue }, untransform(transformedValue: unknown, model: object, propName: PropertyKey) { const modelCache = getOrCreate(cache, model, () => new Map()) cacheTransformResult = false const originalValue = untransform({ transformedValue, cacheTransformedValue: cacheTransformedValueFn, }) if (cacheTransformResult) { modelCache.set(propName, { originalValue, transformedValue }) } else { modelCache.delete(propName) } return originalValue }, } } /** * @ignore */ export function getModelPropDefaultValue(propData: AnyModelProp): unknown | typeof noDefaultValue { if (propData._defaultFn !== noDefaultValue) { return propData._defaultFn() } if (propData._defaultValue !== noDefaultValue) { return propData._defaultValue } return noDefaultValue }