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
@@ -260,21 +281,42 @@ export function register<Instance, Klass extends { new (...args: any[]): Instanc
260
281
} ) ;
261
282
262
283
// 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
264
285
. model ( klass . name , mstPropsFromQuickProps ( klass . properties ) )
265
286
. views ( ( self ) => bindToSelf ( self , mstViews ) )
266
287
. actions ( ( self ) => bindToSelf ( self , mstActions ) ) ;
267
288
268
289
if ( Object . keys ( mstVolatiles ) . length > 0 ) {
269
290
// 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 ) ) ;
271
292
}
272
293
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
+
273
315
// define the class constructor and the following hot path functions dynamically
274
316
// .createReadOnly
275
317
// .is
276
318
// .instantiate
277
- klass = buildFastInstantiator ( klass ) ;
319
+ klass = new InstantiatorBuilder ( klass , cachedViews ) . build ( ) ;
278
320
279
321
( klass as any ) [ $registered ] = true ;
280
322
@@ -305,6 +347,36 @@ export const view = (target: any, property: string, _descriptor: PropertyDescrip
305
347
Reflect . defineMetadata ( `${ viewKeyPrefix } :${ property } ` , metadata , target ) ;
306
348
} ;
307
349
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
+
308
380
/**
309
381
* A function for defining a volatile
310
382
**/
0 commit comments