mobx-keystone-mindreframer
Version:
A MobX powered state management solution based on data trees with first class support for Typescript, snapshots, patches and much more
187 lines (152 loc) • 4.53 kB
text/typescript
import { fastGetParentIncludingDataObjects } from "../parent/path"
import type { Path } from "../parent/pathTypes"
import { isTweakedObject } from "../tweaker/core"
import { failure, lateVal } from "../utils"
import type { AnyStandardType } from "./schemas"
import { TypeCheckError } from "./TypeCheckError"
type CheckFunction = (value: any, path: Path) => TypeCheckError | null
const emptyPath: Path = []
type CheckResult = TypeCheckError | null
type CheckResultCache = WeakMap<object, CheckResult>
const typeCheckersWithCachedResultsOfObject = new WeakMap<object, Set<TypeChecker>>()
/**
* @ignore
*/
export function invalidateCachedTypeCheckerResult(obj: object) {
// we need to invalidate it for the object and all its parents
let current: any = obj
while (current) {
const set = typeCheckersWithCachedResultsOfObject.get(current)
if (set) {
for (const typeChecker of set) {
typeChecker.invalidateCachedResult(current)
}
typeCheckersWithCachedResultsOfObject.delete(current)
}
current = fastGetParentIncludingDataObjects(current)
}
}
/**
* @ignore
*/
export class TypeChecker {
private checkResultCache?: CheckResultCache
unchecked: boolean
private createCacheIfNeeded(): CheckResultCache {
if (!this.checkResultCache) {
this.checkResultCache = new WeakMap()
}
return this.checkResultCache
}
setCachedResult(obj: object, newCacheValue: CheckResult) {
this.createCacheIfNeeded().set(obj, newCacheValue)
// register this type checker as listener of that object changes
let typeCheckerSet = typeCheckersWithCachedResultsOfObject.get(obj)
if (!typeCheckerSet) {
typeCheckerSet = new Set()
typeCheckersWithCachedResultsOfObject.set(obj, typeCheckerSet)
}
typeCheckerSet.add(this)
}
invalidateCachedResult(obj: object) {
if (this.checkResultCache) {
this.checkResultCache.delete(obj)
}
}
private getCachedResult(obj: object): CheckResult | undefined {
return this.checkResultCache ? this.checkResultCache.get(obj) : undefined
}
check(value: any, path: Path): TypeCheckError | null {
if (this.unchecked) {
return null
}
if (!isTweakedObject(value, true)) {
return this._check!(value, path)
}
// optimized checking with cached values
let cachedResult = this.getCachedResult(value)
if (cachedResult === undefined) {
// we set the path empty since the result could be used for paths other than this base
cachedResult = this._check!(value, emptyPath)
this.setCachedResult(value, cachedResult)
}
if (cachedResult) {
return new TypeCheckError(
[...path, ...cachedResult.path],
cachedResult.expectedTypeName,
cachedResult.actualValue
)
} else {
return null
}
}
private _cachedTypeInfoGen: TypeInfoGen
get typeInfo() {
return this._cachedTypeInfoGen(this as any)
}
constructor(
private readonly _check: CheckFunction | null,
readonly getTypeName: (...recursiveTypeCheckers: TypeChecker[]) => string,
typeInfoGen: TypeInfoGen
) {
this.unchecked = !_check
this._cachedTypeInfoGen = lateVal(typeInfoGen)
}
}
/**
* @internal
* @ignore
*/
export function assertIsTypeChecker(value: unknown): asserts value is TypeChecker {
if (!(value instanceof TypeChecker)) {
throw failure("type checker expected")
}
}
const lateTypeCheckerSymbol = Symbol("lateTypeCheker")
/**
* @ignore
*/
export interface LateTypeChecker {
[lateTypeCheckerSymbol]: true
(): TypeChecker
typeInfo: TypeInfo
}
/**
* @ignore
*/
export function lateTypeChecker(fn: () => TypeChecker, typeInfoGen: TypeInfoGen): LateTypeChecker {
let cached: TypeChecker | undefined
const ltc = function () {
if (cached) {
return cached
}
cached = fn()
return cached
}
;(ltc as LateTypeChecker)[lateTypeCheckerSymbol] = true
const cachedTypeInfoGen = lateVal(typeInfoGen)
Object.defineProperty(ltc, "typeInfo", {
enumerable: true,
configurable: true,
get() {
return cachedTypeInfoGen(ltc as any)
},
})
return ltc as LateTypeChecker
}
/**
* @ignore
*/
export function isLateTypeChecker(ltc: any): ltc is LateTypeChecker {
return typeof ltc === "function" && ltc[lateTypeCheckerSymbol]
}
/**
* Type info base class.
*/
export class TypeInfo {
constructor(readonly thisType: AnyStandardType) {}
}
/**
* @ignore
*/
export type TypeInfoGen = (t: AnyStandardType) => TypeInfo