18
18
19
19
import { QueryResult } from '../api' ;
20
20
21
- /** Internal utility type. Value is any FDC scalar value. */
22
- // TODO: make this more accurate... what type should we use to represent any FDC scalar value?
23
- type Value = string | number | boolean | null | undefined | object | Value [ ] ;
21
+ /** Internal utility type. Scalar is any FDC scalar value. */
22
+ type Scalar = undefined | null | boolean | number | string ;
24
23
25
24
/**
26
- * Internal utility type. Defines the shape of query result data that represents a single entity.
27
- * It must have __typename and __id for normalization.
25
+ * Checks if the provided value is a valid SelectionSet.
26
+ *
27
+ * Note that this does not check the contents of fields in the selection set, so it's possible that
28
+ * one of the fields, or a nested field, contains an invalid type (such as an array of mixed types).
29
+ * @param value the value to check
30
+ * @returns True if the value is a valid SelectionSet
31
+ */
32
+ function isScalar ( value : unknown ) : value is Scalar {
33
+ if ( Array . isArray ( value ) ) {
34
+ return false ;
35
+ }
36
+ switch ( typeof value ) {
37
+ case 'undefined' :
38
+ case 'boolean' :
39
+ case 'number' :
40
+ case 'string' :
41
+ return true ;
42
+ case 'object' :
43
+ // null has typeof === 'object' for historical reasons.
44
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof#typeof_null
45
+ return value === null ;
46
+ default :
47
+ return false ;
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Internal utility type. Defines the shape of selection set for a table. It must have
53
+ * __typename and __id for normalization.
54
+ */
55
+ interface SelectionSet {
56
+ [ field : string ] : Scalar | Scalar [ ] | SelectionSet | SelectionSet [ ] ;
57
+ }
58
+
59
+ /**
60
+ * A type guard to check if a value is, at the top level, a valid SelectionSet (it is an object,
61
+ * which is not an array, and which has at least one field).
62
+ *
63
+ * Note that this is only a "top-level" check - this does not check the selection set recursively,
64
+ * or the contents of arrays in the selection set, so it's possible that one of the fields, or a
65
+ * nested field, contains a type which would make it an invalid FDC selection set (such as an array
66
+ * of mixed types).
67
+ * @param value the value to check
68
+ * @returns True if the value is a valid SelectionSet at the top level of the object
69
+ */
70
+ function isTopLevelSelectionSet ( value : unknown ) : value is SelectionSet {
71
+ // null has typeof === 'object' for historical reasons.
72
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof#typeof_null
73
+ return (
74
+ typeof value === 'object' &&
75
+ value !== null &&
76
+ ! Array . isArray ( value ) &&
77
+ Object . keys ( value ) . length >= 1
78
+ ) ;
79
+ }
80
+
81
+ /**
82
+ * Internal utility type. Defines the shape of query result data, made up of selection sets on tables.
28
83
*/
29
84
interface QueryResultData {
30
- [ key : string ] : Value ;
31
- __typename ?: string ;
32
- __id ?: string ;
85
+ [ tableName : string ] : SelectionSet | SelectionSet [ ] ;
33
86
}
34
87
35
88
/**
36
- * A type guard to check if a value is normalizeable .
89
+ * A type guard to check if a value is cacheable (it has the fields __typename and __id) .
37
90
* @param value The value to check.
38
- * @returns True if the value is normalizeable (it has the fields __typename and __id).
91
+ * @returns True if the value is cacheable (it has the fields __typename and __id).
39
92
*/
40
- function isNormalizeable ( value : unknown ) : value is QueryResultData {
93
+ function isNormalizeable (
94
+ value : SelectionSet | Scalar
95
+ ) : value is StubDataObject {
41
96
return (
42
97
value !== undefined &&
43
98
value !== null &&
@@ -58,11 +113,13 @@ export interface StubResultTree {
58
113
59
114
/**
60
115
* Interface for a stub data object, which acts as a snapshot view of cached data.
61
- * Selection sets in generated data types extend this interface.
116
+ * Selection sets in cached generated data types extend this interface.
62
117
* @public
63
118
*/
64
- export interface StubDataObject {
65
- [ key : string ] : Value | StubDataObject ;
119
+ export interface StubDataObject extends SelectionSet {
120
+ [ key : string ] : Scalar | SelectionSet ;
121
+ __typename : string ;
122
+ __id : string ;
66
123
}
67
124
68
125
/**
@@ -84,12 +141,12 @@ export class BackingDataObject {
84
141
readonly typedKey : string ;
85
142
86
143
/** Represents values received from the server. */
87
- private serverValues : Map < string , Value > ;
144
+ private serverValues : Map < string , Scalar > ;
88
145
89
146
/** A set of listeners (StubDataObjects) that need to be updated when values change. */
90
147
readonly listeners : Set < StubDataObject > ;
91
148
92
- constructor ( typedKey : string , serverValues : Map < string , Value > ) {
149
+ constructor ( typedKey : string , serverValues : Map < string , Scalar > ) {
93
150
this . typedKey = typedKey ;
94
151
this . listeners = new Set ( ) ;
95
152
this . serverValues = serverValues ;
@@ -101,7 +158,7 @@ export class BackingDataObject {
101
158
* @param value The new value from the server.
102
159
* @param key The key of the property to update.
103
160
*/
104
- updateFromServer ( value : Value , key : string ) : void {
161
+ updateFromServer ( value : Scalar , key : string ) : void {
105
162
this . serverValues . set ( key , value ) ;
106
163
for ( const listener of this . listeners ) {
107
164
if ( key in listener ) {
@@ -115,19 +172,19 @@ export class BackingDataObject {
115
172
* @param key The key of the value to retrieve.
116
173
* @returns The value associated with the key, or undefined if not found.
117
174
*/
118
- protected value ( key : string ) : Value | undefined {
175
+ protected value ( key : string ) : Scalar | undefined {
119
176
return this . serverValues . get ( key ) ;
120
177
}
121
178
122
179
/** Values modified locally for latency compensation. */
123
- private localValues : Map < string , Value > = new Map ( ) ;
180
+ private localValues : Map < string , Scalar > = new Map ( ) ;
124
181
125
182
/**
126
183
* Updates the value for a named property locally.
127
184
* @param value The new local value.
128
185
* @param key The key of the property to update.
129
186
*/
130
- updateLocal ( value : Value , key : string ) : void {
187
+ updateLocal ( value : Scalar , key : string ) : void {
131
188
this . localValues . set ( key , value ) ;
132
189
for ( const listener of this . listeners ) {
133
190
if ( key in listener ) {
@@ -145,9 +202,6 @@ export class Cache {
145
202
/** A map of [query + variables] --> StubResultTree returned from that query. */
146
203
srtCache = new Map < string , StubResultTree > ( ) ;
147
204
148
- /** A map of [entity typename + id] --> BackingDataObject for that entity. */
149
- bdoCache = new Map < string , BackingDataObject > ( ) ;
150
-
151
205
/**
152
206
* Creates a unique StrubResultTree cache key for a given query and its variables.
153
207
* @param queryName The name of the query.
@@ -158,6 +212,9 @@ export class Cache {
158
212
return queryName + '|' + JSON . stringify ( vars ) ;
159
213
}
160
214
215
+ /** A map of [entity typename + id] --> BackingDataObject for that entity. */
216
+ bdoCache = new Map < string , BackingDataObject > ( ) ;
217
+
161
218
/**
162
219
* Creates a unique BackingDataObject cache key for a given entity.
163
220
* @param typename The typename of the entity being cached.
@@ -172,62 +229,89 @@ export class Cache {
172
229
* Updates the cache with the results of a query. This is the main entry point.
173
230
* @param queryResult The result of the query.
174
231
*/
175
- updateCache < Data extends object , Variables > (
232
+ updateCache < Data extends QueryResultData , Variables > (
176
233
queryResult : QueryResult < Data , Variables >
177
234
) : void {
178
235
const resultTreeCacheKey = Cache . srtCacheKey (
179
236
queryResult . ref . name ,
180
237
queryResult . ref . variables
181
238
) ;
182
- const stubResultTree = this . normalize ( queryResult . data ) as StubResultTree ;
239
+ const stubResultTree = this . createSrt ( queryResult . data ) ;
183
240
this . srtCache . set ( resultTreeCacheKey , stubResultTree ) ;
184
241
}
185
242
186
243
/**
187
- * Recursively traverses a data object, normalizing cacheable entities into BDOs
188
- * and replacing them with stubs.
189
- * @param data The data to normalize (can be an object, array, or primitive).
190
- * @returns The normalized data with stubs.
244
+ * Caches the provided selection set. Attempts to normalize the set by recursively traversing it's,
245
+ * fields, caching them along the way.
246
+ *
247
+ * @param selectionSet The data to ATTEMPT TO normalize.
248
+ * @returns the top-level selection set (which may be an SDO if it was normalizeable).
191
249
*/
192
- private normalize ( data : QueryResultData | Value ) : Value | StubDataObject {
193
- if ( Array . isArray ( data ) ) {
194
- return data . map ( item => this . normalize ( item ) ) ;
195
- }
250
+ private cacheSelectionSet (
251
+ selectionSet : SelectionSet
252
+ ) : SelectionSet | StubDataObject {
253
+ const cachedSelectionSet : SelectionSet = { } ;
196
254
197
- if ( isNormalizeable ( data ) ) {
198
- const stub : StubDataObject = { } ;
199
- const bdoCacheKey = Cache . bdoCacheKey ( data . __typename , data . __id ) ;
200
- const existingBdo = this . bdoCache . get ( bdoCacheKey ) ;
201
-
202
- // data is a single "movie" or "actor"
203
- // key is a field of the returned data, such as "name"
204
- for ( const key in data ) {
205
- // eslint-disable-next-line no-prototype-builtins
206
- if ( data . hasOwnProperty ( key ) ) {
207
- stub [ key ] = this . normalize ( data [ key ] ) ;
255
+ // recursively traverse selection set, creating a new selection set which will be cached.
256
+ for ( const [ field , value ] of Object . entries ( selectionSet ) ) {
257
+ if ( Array . isArray ( value ) ) {
258
+ const cachedField = value . map ( this . cacheField ) ;
259
+ // type assertion because typescript thinks this could be a mixed array
260
+ if (
261
+ cachedField . every ( isScalar ) ||
262
+ cachedField . every ( isTopLevelSelectionSet )
263
+ ) {
264
+ cachedSelectionSet [ field ] = cachedField ;
265
+ } else {
266
+ // mixed array, should never happen
267
+ cachedSelectionSet [ field ] = value ;
208
268
}
269
+ } else {
270
+ cachedSelectionSet [ field ] = this . cacheField ( value ) ;
209
271
}
272
+ }
210
273
274
+ if ( isNormalizeable ( cachedSelectionSet ) ) {
275
+ const bdoCacheKey = Cache . bdoCacheKey (
276
+ cachedSelectionSet . __typename ,
277
+ cachedSelectionSet . __id
278
+ ) ;
279
+ const existingBdo = this . bdoCache . get ( bdoCacheKey ) ;
211
280
if ( existingBdo ) {
212
- this . updateBdo ( existingBdo , stub , stub ) ;
281
+ this . updateBdo ( existingBdo , selectionSet , cachedSelectionSet ) ;
213
282
} else {
214
- this . createBdo ( bdoCacheKey , stub , stub ) ;
283
+ this . createBdo ( bdoCacheKey , selectionSet , cachedSelectionSet ) ;
215
284
}
216
- return stub ;
285
+ return cachedSelectionSet ;
217
286
}
218
287
219
- if ( typeof data === 'object' && data !== null ) {
220
- const newObj : { [ key : string ] : Value } = { } ;
221
- for ( const key in data ) {
222
- // eslint-disable-next-line no-prototype-builtins
223
- if ( data . hasOwnProperty ( key ) ) {
224
- newObj [ key ] = this . normalize ( data [ key ] ) ;
225
- }
226
- }
227
- return newObj ;
288
+ return cachedSelectionSet ;
289
+ }
290
+
291
+ private cacheField ( value : Scalar | SelectionSet ) : Scalar | SelectionSet {
292
+ if ( isTopLevelSelectionSet ( value ) ) {
293
+ // recurse, and replace cacheable selection sets with SDOs
294
+ return this . cacheSelectionSet ( value ) ;
228
295
}
296
+ // return scalars
297
+ return value ;
298
+ }
229
299
230
- return data ;
300
+ /**
301
+ * Creates a StubResultTree based on the data returned from a query
302
+ * @param data the data property of the query result
303
+ * @returns
304
+ */
305
+ private createSrt ( data : QueryResultData ) : StubResultTree {
306
+ const srt : StubResultTree = { } ;
307
+ for ( const [ tableName , selectionSet ] of Object . entries ( data ) ) {
308
+ if ( Array . isArray ( selectionSet ) ) {
309
+ srt [ tableName ] = selectionSet . map ( this . cacheSelectionSet ) ;
310
+ } else {
311
+ srt [ tableName ] = this . cacheSelectionSet ( selectionSet ) ;
312
+ }
313
+ }
314
+ return srt ;
231
315
}
232
316
233
317
/**
@@ -238,10 +322,11 @@ export class Cache {
238
322
*/
239
323
private createBdo (
240
324
bdoCacheKey : string ,
241
- data : QueryResultData ,
325
+ data : SelectionSet ,
242
326
stubDataObject : StubDataObject
243
327
) : void {
244
- const serverValues = new Map < string , Value > ( Object . entries ( data ) ) ;
328
+ const test = Object . entries ( data ) ;
329
+ const serverValues = new Map < string , Scalar > ( ) ;
245
330
const newBdo = new BackingDataObject ( bdoCacheKey , serverValues ) ;
246
331
newBdo . listeners . add ( stubDataObject ) ;
247
332
this . bdoCache . set ( bdoCacheKey , newBdo ) ;
@@ -255,7 +340,7 @@ export class Cache {
255
340
*/
256
341
private updateBdo (
257
342
backingDataObject : BackingDataObject ,
258
- data : QueryResultData ,
343
+ data : SelectionSet ,
259
344
stubDataObject : StubDataObject
260
345
) : void {
261
346
// TODO: don't cache non-cacheable fields!
0 commit comments