11import memoize from "lodash.memoize" ;
2- import type { IModelType as MSTIModelType , ModelActions } from "mobx-state-tree" ;
2+ import type { Instance , IModelType as MSTIModelType , ModelActions } from "mobx-state-tree" ;
33import { types as mstTypes } from "mobx-state-tree" ;
44import "reflect-metadata" ;
55import { RegistrationError } from "./errors" ;
6- import { buildFastInstantiator } from "./fast-instantiator" ;
6+ import { InstantiatorBuilder } from "./fast-instantiator" ;
77import { defaultThrowAction , mstPropsFromQuickProps , propsFromModelPropsDeclaration } from "./model" ;
88import {
99 $env ,
@@ -22,6 +22,7 @@ import {
2222import type {
2323 Constructor ,
2424 ExtendedClassModel ,
25+ IAnyClassModelType ,
2526 IAnyType ,
2627 IClassModelType ,
2728 IStateTreeNode ,
@@ -39,12 +40,24 @@ type ActionMetadata = {
3940 volatile : boolean ;
4041} ;
4142
43+ export interface CachedViewOptions < V , T extends IAnyClassModelType > {
44+ createReadOnly ?: ( value : V | undefined , snapshot : T [ "InputType" ] , node : Instance < T > ) => V | undefined ;
45+ getSnapshot ?: ( value : V , snapshot : T [ "InputType" ] , node : Instance < T > ) => any ;
46+ }
47+
4248/** @internal */
43- type ViewMetadata = {
49+ export type ViewMetadata = {
4450 type : "view" ;
4551 property : string ;
4652} ;
4753
54+ /** @internal */
55+ export type CachedViewMetadata = {
56+ type : "cached-view" ;
57+ property : string ;
58+ cache : CachedViewOptions < any , any > ;
59+ } ;
60+
4861/** @internal */
4962export type VolatileMetadata = {
5063 type : "volatile" ;
@@ -53,7 +66,7 @@ export type VolatileMetadata = {
5366} ;
5467
5568type VolatileInitializer < T > = ( instance : T ) => Record < string , any > ;
56- type PropertyMetadata = ActionMetadata | ViewMetadata | VolatileMetadata ;
69+ type PropertyMetadata = ActionMetadata | ViewMetadata | CachedViewMetadata | VolatileMetadata ;
5770
5871const metadataPrefix = "mqt:properties" ;
5972const viewKeyPrefix = `${ metadataPrefix } :view` ;
@@ -158,13 +171,20 @@ export function register<Instance, Klass extends { new (...args: any[]): Instanc
158171
159172 for ( const metadata of metadatas ) {
160173 switch ( metadata . type ) {
174+ case "cached-view" :
161175 case "view" : {
162176 const property = metadata . property ;
163177 const descriptor = getPropertyDescriptor ( klass . prototype , property ) ;
164178 if ( ! descriptor ) {
165179 throw new RegistrationError ( `Property ${ property } not found on ${ klass } prototype, can't register view for class model` ) ;
166180 }
167181
182+ if ( "cache" in metadata && ! descriptor . get ) {
183+ throw new RegistrationError (
184+ `Cached view property ${ property } on ${ klass } must be a getter -- can't use cached views with views that are functions or take arguments`
185+ ) ;
186+ }
187+
168188 // memoize getters on readonly instances
169189 if ( descriptor . get ) {
170190 Object . defineProperty ( klass . prototype , property , {
@@ -186,6 +206,7 @@ export function register<Instance, Klass extends { new (...args: any[]): Instanc
186206 ...descriptor ,
187207 enumerable : true ,
188208 } ) ;
209+
189210 break ;
190211 }
191212
@@ -260,21 +281,42 @@ export function register<Instance, Klass extends { new (...args: any[]): Instanc
260281 } ) ;
261282
262283 // create the MST type for not-readonly versions of this using the views and actions extracted from the class
263- klass . mstType = mstTypes
284+ let mstType = mstTypes
264285 . model ( klass . name , mstPropsFromQuickProps ( klass . properties ) )
265286 . views ( ( self ) => bindToSelf ( self , mstViews ) )
266287 . actions ( ( self ) => bindToSelf ( self , mstActions ) ) ;
267288
268289 if ( Object . keys ( mstVolatiles ) . length > 0 ) {
269290 // define the volatile properties in one shot by running any passed initializers
270- ( klass as any ) . mstType = ( klass as any ) . mstType . volatile ( ( self : any ) => initializeVolatiles ( { } , self , mstVolatiles ) ) ;
291+ mstType = mstType . volatile ( ( self : any ) => initializeVolatiles ( { } , self , mstVolatiles ) ) ;
271292 }
272293
294+ const cachedViews = metadatas . filter ( ( metadata ) => metadata . type == "cached-view" ) as CachedViewMetadata [ ] ;
295+ if ( cachedViews . length > 0 ) {
296+ mstType = mstTypes . snapshotProcessor ( mstType , {
297+ postProcessor ( snapshot , node ) {
298+ const stn = node . $treenode ! ;
299+ if ( stn . state == 2 /** NodeLifeCycle.FINALIZED */ ) {
300+ for ( const cachedView of cachedViews ) {
301+ let value = node [ cachedView . property ] ;
302+ if ( cachedView . cache . getSnapshot ) {
303+ value = cachedView . cache . getSnapshot ( value , snapshot , node ) ;
304+ }
305+ snapshot [ cachedView . property ] = value ;
306+ }
307+ }
308+ return snapshot ;
309+ } ,
310+ } ) as any ;
311+ }
312+
313+ klass . mstType = mstType ;
314+
273315 // define the class constructor and the following hot path functions dynamically
274316 // .createReadOnly
275317 // .is
276318 // .instantiate
277- klass = buildFastInstantiator ( klass ) ;
319+ klass = new InstantiatorBuilder ( klass , cachedViews ) . build ( ) ;
278320
279321 ( klass as any ) [ $registered ] = true ;
280322
@@ -305,6 +347,36 @@ export const view = (target: any, property: string, _descriptor: PropertyDescrip
305347 Reflect . defineMetadata ( `${ viewKeyPrefix } :${ property } ` , metadata , target ) ;
306348} ;
307349
350+ /**
351+ * Function decorator for registering MQT cached views within MQT class models. Stores the view's value into the snapshot when an instance is snapshotted, and uses that stored value for readonly instances created from snapshots.
352+ *
353+ * Can be passed an `options` object with a `preProcess` and/or `postProcess` function for transforming the cached value stored in the snapshot to and from the snapshot state.
354+ *
355+ * @example
356+ * class Example extends ClassModel({ name: types.string }) {
357+ * @cachedView
358+ * get slug() {
359+ * return this.name.toLowerCase().replace(/ /g, "-");
360+ * }
361+ * }
362+ *
363+ * @example
364+ * class Example extends ClassModel({ timestamp: types.string }) {
365+ * @cachedView ({ preProcess: (value) => new Date(value), postProcess: (value) => value.toISOString() })
366+ * get date() {
367+ * return new Date(timestamp).setTime(0);
368+ * }
369+ * }
370+ */
371+ export function cachedView < V , T extends IAnyClassModelType = IAnyClassModelType > (
372+ options : CachedViewOptions < V , T > = { }
373+ ) : ( target : any , property : string , _descriptor : PropertyDescriptor ) => void {
374+ return ( target : any , property : string , _descriptor : PropertyDescriptor ) => {
375+ const metadata : CachedViewMetadata = { type : "cached-view" , property, cache : options } ;
376+ Reflect . defineMetadata ( `${ viewKeyPrefix } :${ property } ` , metadata , target ) ;
377+ } ;
378+ }
379+
308380/**
309381 * A function for defining a volatile
310382 **/
0 commit comments