1
1
import 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" ;
3
3
import { types as mstTypes } from "mobx-state-tree" ;
4
4
import "reflect-metadata" ;
5
5
import { RegistrationError } from "./errors" ;
6
- import { buildFastInstantiator } from "./fast-instantiator" ;
6
+ import { InstantiatorBuilder } from "./fast-instantiator" ;
7
7
import { defaultThrowAction , mstPropsFromQuickProps , propsFromModelPropsDeclaration } from "./model" ;
8
8
import {
9
9
$env ,
@@ -22,6 +22,7 @@ import {
22
22
import type {
23
23
Constructor ,
24
24
ExtendedClassModel ,
25
+ IAnyClassModelType ,
25
26
IAnyType ,
26
27
IClassModelType ,
27
28
IStateTreeNode ,
@@ -39,12 +40,24 @@ type ActionMetadata = {
39
40
volatile : boolean ;
40
41
} ;
41
42
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
+
42
48
/** @internal */
43
- type ViewMetadata = {
49
+ export type ViewMetadata = {
44
50
type : "view" ;
45
51
property : string ;
46
52
} ;
47
53
54
+ /** @internal */
55
+ export type CachedViewMetadata = {
56
+ type : "cached-view" ;
57
+ property : string ;
58
+ cache : CachedViewOptions < any , any > ;
59
+ } ;
60
+
48
61
/** @internal */
49
62
export type VolatileMetadata = {
50
63
type : "volatile" ;
@@ -53,7 +66,7 @@ export type VolatileMetadata = {
53
66
} ;
54
67
55
68
type VolatileInitializer < T > = ( instance : T ) => Record < string , any > ;
56
- type PropertyMetadata = ActionMetadata | ViewMetadata | VolatileMetadata ;
69
+ type PropertyMetadata = ActionMetadata | ViewMetadata | CachedViewMetadata | VolatileMetadata ;
57
70
58
71
const metadataPrefix = "mqt:properties" ;
59
72
const viewKeyPrefix = `${ metadataPrefix } :view` ;
@@ -158,13 +171,20 @@ export function register<Instance, Klass extends { new (...args: any[]): Instanc
158
171
159
172
for ( const metadata of metadatas ) {
160
173
switch ( metadata . type ) {
174
+ case "cached-view" :
161
175
case "view" : {
162
176
const property = metadata . property ;
163
177
const descriptor = getPropertyDescriptor ( klass . prototype , property ) ;
164
178
if ( ! descriptor ) {
165
179
throw new RegistrationError ( `Property ${ property } not found on ${ klass } prototype, can't register view for class model` ) ;
166
180
}
167
181
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
+
168
188
// memoize getters on readonly instances
169
189
if ( descriptor . get ) {
170
190
Object . defineProperty ( klass . prototype , property , {
@@ -186,6 +206,7 @@ export function register<Instance, Klass extends { new (...args: any[]): Instanc
186
206
...descriptor ,
187
207
enumerable : true ,
188
208
} ) ;
209
+
189
210
break ;
190
211
}
191
212
@@ -278,17 +299,37 @@ export function register<Instance, Klass extends { new (...args: any[]): Instanc
278
299
} ) ;
279
300
280
301
// create the MST type for not-readonly versions of this using the views and actions extracted from the class
281
- klass . mstType = mstTypes
302
+ let mstType = mstTypes
282
303
. model ( klass . name , mstPropsFromQuickProps ( klass . properties ) )
283
304
. views ( ( self ) => bindToSelf ( self , mstViews ) )
284
305
. actions ( ( self ) => bindToSelf ( self , mstActions ) ) ;
285
306
286
307
if ( Object . keys ( mstVolatiles ) . length > 0 ) {
287
308
// define the volatile properties in one shot by running any passed initializers
288
- ( klass as any ) . mstType = ( klass as any ) . mstType . volatile ( ( self : any ) => initializeVolatiles ( { } , self , mstVolatiles ) ) ;
309
+ mstType = mstType . volatile ( ( self : any ) => initializeVolatiles ( { } , self , mstVolatiles ) ) ;
310
+ }
311
+
312
+ const cachedViews = metadatas . filter ( ( metadata ) => metadata . type == "cached-view" ) as CachedViewMetadata [ ] ;
313
+ if ( cachedViews . length > 0 ) {
314
+ mstType = mstTypes . snapshotProcessor ( mstType , {
315
+ postProcessor ( snapshot , node ) {
316
+ const stn = node . $treenode ! ;
317
+ if ( stn . state == 2 /** NodeLifeCycle.FINALIZED */ ) {
318
+ for ( const cachedView of cachedViews ) {
319
+ let value = node [ cachedView . property ] ;
320
+ if ( cachedView . cache . getSnapshot ) {
321
+ value = cachedView . cache . getSnapshot ( value , snapshot , node ) ;
322
+ }
323
+ snapshot [ cachedView . property ] = value ;
324
+ }
325
+ }
326
+ return snapshot ;
327
+ } ,
328
+ } ) as any ;
289
329
}
290
330
291
- klass = buildFastInstantiator ( klass ) ;
331
+ klass . mstType = mstType ;
332
+ klass = new InstantiatorBuilder ( klass , cachedViews ) . build ( ) ;
292
333
( klass as any ) [ $registered ] = true ;
293
334
294
335
return klass as any ;
@@ -318,6 +359,36 @@ export const view = (target: any, property: string, _descriptor: PropertyDescrip
318
359
Reflect . defineMetadata ( `${ viewKeyPrefix } :${ property } ` , metadata , target ) ;
319
360
} ;
320
361
362
+ /**
363
+ * 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.
364
+ *
365
+ * 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.
366
+ *
367
+ * @example
368
+ * class Example extends ClassModel({ name: types.string }) {
369
+ * @cachedView
370
+ * get slug() {
371
+ * return this.name.toLowerCase().replace(/ /g, "-");
372
+ * }
373
+ * }
374
+ *
375
+ * @example
376
+ * class Example extends ClassModel({ timestamp: types.string }) {
377
+ * @cachedView ({ preProcess: (value) => new Date(value), postProcess: (value) => value.toISOString() })
378
+ * get date() {
379
+ * return new Date(timestamp).setTime(0);
380
+ * }
381
+ * }
382
+ */
383
+ export function cachedView < V , T extends IAnyClassModelType = IAnyClassModelType > (
384
+ options : CachedViewOptions < V , T > = { }
385
+ ) : ( target : any , property : string , _descriptor : PropertyDescriptor ) => void {
386
+ return ( target : any , property : string , _descriptor : PropertyDescriptor ) => {
387
+ const metadata : CachedViewMetadata = { type : "cached-view" , property, cache : options } ;
388
+ Reflect . defineMetadata ( `${ viewKeyPrefix } :${ property } ` , metadata , target ) ;
389
+ } ;
390
+ }
391
+
321
392
/**
322
393
* A function for defining a volatile
323
394
**/
0 commit comments