1818
1919import { QueryResult } from '../api' ;
2020
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 ;
2423
2524/**
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.
2883 */
2984interface QueryResultData {
30- [ key : string ] : Value ;
31- __typename ?: string ;
32- __id ?: string ;
85+ [ tableName : string ] : SelectionSet | SelectionSet [ ] ;
3386}
3487
3588/**
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) .
3790 * @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).
3992 */
40- function isNormalizeable ( value : unknown ) : value is QueryResultData {
93+ function isNormalizeable (
94+ value : SelectionSet | Scalar
95+ ) : value is StubDataObject {
4196 return (
4297 value !== undefined &&
4398 value !== null &&
@@ -58,11 +113,13 @@ export interface StubResultTree {
58113
59114/**
60115 * 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.
62117 * @public
63118 */
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 ;
66123}
67124
68125/**
@@ -84,12 +141,12 @@ export class BackingDataObject {
84141 readonly typedKey : string ;
85142
86143 /** Represents values received from the server. */
87- private serverValues : Map < string , Value > ;
144+ private serverValues : Map < string , Scalar > ;
88145
89146 /** A set of listeners (StubDataObjects) that need to be updated when values change. */
90147 readonly listeners : Set < StubDataObject > ;
91148
92- constructor ( typedKey : string , serverValues : Map < string , Value > ) {
149+ constructor ( typedKey : string , serverValues : Map < string , Scalar > ) {
93150 this . typedKey = typedKey ;
94151 this . listeners = new Set ( ) ;
95152 this . serverValues = serverValues ;
@@ -101,7 +158,7 @@ export class BackingDataObject {
101158 * @param value The new value from the server.
102159 * @param key The key of the property to update.
103160 */
104- updateFromServer ( value : Value , key : string ) : void {
161+ updateFromServer ( value : Scalar , key : string ) : void {
105162 this . serverValues . set ( key , value ) ;
106163 for ( const listener of this . listeners ) {
107164 if ( key in listener ) {
@@ -115,19 +172,19 @@ export class BackingDataObject {
115172 * @param key The key of the value to retrieve.
116173 * @returns The value associated with the key, or undefined if not found.
117174 */
118- protected value ( key : string ) : Value | undefined {
175+ protected value ( key : string ) : Scalar | undefined {
119176 return this . serverValues . get ( key ) ;
120177 }
121178
122179 /** Values modified locally for latency compensation. */
123- private localValues : Map < string , Value > = new Map ( ) ;
180+ private localValues : Map < string , Scalar > = new Map ( ) ;
124181
125182 /**
126183 * Updates the value for a named property locally.
127184 * @param value The new local value.
128185 * @param key The key of the property to update.
129186 */
130- updateLocal ( value : Value , key : string ) : void {
187+ updateLocal ( value : Scalar , key : string ) : void {
131188 this . localValues . set ( key , value ) ;
132189 for ( const listener of this . listeners ) {
133190 if ( key in listener ) {
@@ -145,9 +202,6 @@ export class Cache {
145202 /** A map of [query + variables] --> StubResultTree returned from that query. */
146203 srtCache = new Map < string , StubResultTree > ( ) ;
147204
148- /** A map of [entity typename + id] --> BackingDataObject for that entity. */
149- bdoCache = new Map < string , BackingDataObject > ( ) ;
150-
151205 /**
152206 * Creates a unique StrubResultTree cache key for a given query and its variables.
153207 * @param queryName The name of the query.
@@ -158,6 +212,9 @@ export class Cache {
158212 return queryName + '|' + JSON . stringify ( vars ) ;
159213 }
160214
215+ /** A map of [entity typename + id] --> BackingDataObject for that entity. */
216+ bdoCache = new Map < string , BackingDataObject > ( ) ;
217+
161218 /**
162219 * Creates a unique BackingDataObject cache key for a given entity.
163220 * @param typename The typename of the entity being cached.
@@ -172,62 +229,89 @@ export class Cache {
172229 * Updates the cache with the results of a query. This is the main entry point.
173230 * @param queryResult The result of the query.
174231 */
175- updateCache < Data extends object , Variables > (
232+ updateCache < Data extends QueryResultData , Variables > (
176233 queryResult : QueryResult < Data , Variables >
177234 ) : void {
178235 const resultTreeCacheKey = Cache . srtCacheKey (
179236 queryResult . ref . name ,
180237 queryResult . ref . variables
181238 ) ;
182- const stubResultTree = this . normalize ( queryResult . data ) as StubResultTree ;
239+ const stubResultTree = this . createSrt ( queryResult . data ) ;
183240 this . srtCache . set ( resultTreeCacheKey , stubResultTree ) ;
184241 }
185242
186243 /**
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).
191249 */
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 = { } ;
196254
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 ;
208268 }
269+ } else {
270+ cachedSelectionSet [ field ] = this . cacheField ( value ) ;
209271 }
272+ }
210273
274+ if ( isNormalizeable ( cachedSelectionSet ) ) {
275+ const bdoCacheKey = Cache . bdoCacheKey (
276+ cachedSelectionSet . __typename ,
277+ cachedSelectionSet . __id
278+ ) ;
279+ const existingBdo = this . bdoCache . get ( bdoCacheKey ) ;
211280 if ( existingBdo ) {
212- this . updateBdo ( existingBdo , stub , stub ) ;
281+ this . updateBdo ( existingBdo , selectionSet , cachedSelectionSet ) ;
213282 } else {
214- this . createBdo ( bdoCacheKey , stub , stub ) ;
283+ this . createBdo ( bdoCacheKey , selectionSet , cachedSelectionSet ) ;
215284 }
216- return stub ;
285+ return cachedSelectionSet ;
217286 }
218287
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 ) ;
228295 }
296+ // return scalars
297+ return value ;
298+ }
229299
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 ;
231315 }
232316
233317 /**
@@ -238,10 +322,11 @@ export class Cache {
238322 */
239323 private createBdo (
240324 bdoCacheKey : string ,
241- data : QueryResultData ,
325+ data : SelectionSet ,
242326 stubDataObject : StubDataObject
243327 ) : void {
244- const serverValues = new Map < string , Value > ( Object . entries ( data ) ) ;
328+ const test = Object . entries ( data ) ;
329+ const serverValues = new Map < string , Scalar > ( ) ;
245330 const newBdo = new BackingDataObject ( bdoCacheKey , serverValues ) ;
246331 newBdo . listeners . add ( stubDataObject ) ;
247332 this . bdoCache . set ( bdoCacheKey , newBdo ) ;
@@ -255,7 +340,7 @@ export class Cache {
255340 */
256341 private updateBdo (
257342 backingDataObject : BackingDataObject ,
258- data : QueryResultData ,
343+ data : SelectionSet ,
259344 stubDataObject : StubDataObject
260345 ) : void {
261346 // TODO: don't cache non-cacheable fields!
0 commit comments