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

341 lines (290 loc) 7.84 kB
import { action, IMapWillChange, IObjectDidChange, IObservableArray, intercept, isObservableArray, isObservableObject, ObservableMap, observable, observe, remove, runInAction, transaction, untracked, } from "mobx" import { assertIsMap, assertIsObservableArray, assertIsObservableObject, failure, getMobxVersion, inDevMode, isArray, } from "../utils" import { setIfDifferent } from "../utils/setIfDifferent" import { tag } from "../utils/tag" const observableMapBackedByObservableObject = action( <T>( obj: object ): ObservableMap<string, T> & { dataObject: typeof obj } => { if (inDevMode) { if (!isObservableObject(obj)) { throw failure("assertion failed: expected an observable object") } } const map = transaction(() => untracked(() => { const map = observable.map() const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) { const k = keys[i] map.set(k, (obj as any)[k]) } return map }) ) ;(map as any).dataObject = obj let mapAlreadyChanged = false let objectAlreadyChanged = false // when the object changes the map changes observe( obj, action((change: IObjectDidChange) => { if (mapAlreadyChanged) { return } objectAlreadyChanged = true try { switch (change.type) { case "add": case "update": { map.set(change.name, change.newValue) break } case "remove": { map.delete(change.name) break } default: throw failure(`assertion error: unsupported object change type`) } } finally { objectAlreadyChanged = false } }) ) // when the map changes also change the object intercept( map, action((change: IMapWillChange<string, T>) => { if (mapAlreadyChanged) { return null } if (objectAlreadyChanged) { return change } mapAlreadyChanged = true try { switch (change.type) { case "add": case "update": { setIfDifferent(obj, change.name, change.newValue) break } case "delete": { remove(obj, change.name) break } default: throw failure(`assertion error: unsupported map change type`) } return change } finally { mapAlreadyChanged = false } }) ) return map as any } ) const observableMapBackedByObservableArray = <T>( array: IObservableArray<[string, T]> ): ObservableMap<string, T> & { dataObject: typeof array } => { if (inDevMode) { if (!isObservableArray(array)) { throw failure("assertion failed: expected an observable array") } } const map = untracked(() => { if (getMobxVersion() >= 6) { return observable.map(array) } else { const map = observable.map() runInAction(() => { array.forEach(([k, v]) => { map.set(k, v) }) }) return map } }) ;(map as any).dataObject = array if (map.size !== array.length) { throw failure("arrays backing a map cannot contain duplicate keys") } let mapAlreadyChanged = false let arrayAlreadyChanged = false // for speed reasons we will just assume distinct values are only once in the array // also we assume tuples themselves are immutable // when the array changes the map changes observe( array, action((change: any /*IArrayDidChange<[string, T]>*/) => { if (mapAlreadyChanged) { return } arrayAlreadyChanged = true try { switch (change.type) { case "splice": { { const removed = change.removed for (let i = 0; i < removed.length; i++) { map.delete(removed[i][0]) } } { const added = change.added for (let i = 0; i < added.length; i++) { map.set(added[i][0], added[i][1]) } } break } case "update": { map.delete(change.oldValue[0]) map.set(change.newValue[0], change.newValue[1]) break } default: throw failure(`assertion error: unsupported array change type`) } } finally { arrayAlreadyChanged = false } }) ) // when the map changes also change the array intercept( map, action((change: IMapWillChange<string, T>) => { if (mapAlreadyChanged) { return null } if (arrayAlreadyChanged) { return change } mapAlreadyChanged = true try { switch (change.type) { case "update": { // replace the whole tuple to keep tuple immutability const i = array.findIndex((i) => i[0] === change.name) array[i] = [change.name, change.newValue!] break } case "add": { array.push([change.name, change.newValue!]) break } case "delete": { const i = array.findIndex((i) => i[0] === change.name) if (i >= 0) { array.splice(i, 1) } break } default: throw failure(`assertion error: unsupported map change type`) } return change } finally { mapAlreadyChanged = false } }) ) return map as any } const asMapTag = tag((objOrArray: Record<string, any> | Array<[string, any]>) => { if (isArray(objOrArray)) { assertIsObservableArray(objOrArray, "objOrArray") return observableMapBackedByObservableArray(objOrArray) } else { assertIsObservableObject(objOrArray, "objOrArray") return observableMapBackedByObservableObject(objOrArray) } }) /** * Wraps an observable object or a tuple array to offer a map like interface. * * @param array Array. */ export function asMap<K, V>( array: Array<[K, V]> ): ObservableMap<K, V> & { dataObject: Array<[K, V]> } /** * Wraps an observable object or a tuple array to offer a map like interface. * * @param object Object. */ export function asMap<T>( object: Record<string, T> ): ObservableMap<string, T> & { dataObject: Record<string, T> } /** * Wraps an observable object or a tuple array to offer a map like interface. * * @param objOrArray Object or array. */ export function asMap( objOrArray: Record<string, unknown> | Array<[unknown, unknown]> ): ObservableMap<unknown, unknown> & { dataObject: typeof objOrArray } { return asMapTag.for(objOrArray) as any } /** * Converts a map to an object. If the map is a collection wrapper it will return the backed object. * * @param map */ export function mapToObject<T>(map: Pick<Map<string, T>, "forEach">): Record<string, T> { assertIsMap(map, "map") const dataObject = (map as any).dataObject if (dataObject && !isArray(dataObject)) { return dataObject } const obj: Record<string, T> = {} map.forEach((v, k) => { obj[k] = v }) return obj } /** * Converts a map to an array. If the map is a collection wrapper it will return the backed array. * * @param map */ export function mapToArray<K, V>(map: Pick<Map<K, V>, "forEach">): Array<[K, V]> { assertIsMap(map, "map") const dataObject = (map as any).dataObject if (dataObject && isArray(dataObject)) { return dataObject } const arr: [K, V][] = [] map.forEach((v, k) => { arr.push([k, v]) }) return arr }