@@ -22,14 +22,30 @@ import { QueryResult } from '../api';
2222type Value = string | number | boolean | null | undefined | object | Value [ ] ;
2323
2424/**
25- * Defines the shape of query result data - "movies", "actor", etc.
25+ * Defines the shape of query result data that represents a single entity.
26+ * It must have __typename and __id for normalization.
2627 */
2728export interface QueryResultData {
2829 [ key : string ] : Value ;
2930 __typename : string ;
3031 __id : string ;
3132}
3233
34+ /**
35+ * A type guard to check if a value is a QueryResultData object.
36+ * @param value The value to check.
37+ * @returns True if the value is a QueryResultData object.
38+ */
39+ function isCacheableQueryResultData ( value : unknown ) : value is QueryResultData {
40+ return (
41+ value !== null &&
42+ typeof value === 'object' &&
43+ ! Array . isArray ( value ) &&
44+ '__typename' in value &&
45+ '__id' in value
46+ ) ;
47+ }
48+
3349/**
3450 * Interface for a stub result tree, with fields which are stub data objects
3551 */
@@ -38,7 +54,7 @@ interface StubResultTree {
3854}
3955
4056/**
41- * Interface for a stub data object, which contains a reference to its BackingDataObject .
57+ * Interface for a stub data object, which acts as a "live" view into cached data .
4258 * Generated Data implements this interface.
4359 * @public
4460 */
@@ -54,48 +70,51 @@ export interface StubDataObject {
5470class StubDataObjectList extends Array < StubDataObject > { }
5571
5672/**
57- * A class used to hold entity values across all queries.
73+ * A class used to hold the single source of truth for an entity's values across all queries.
5874 * @public
5975 */
6076export class BackingDataObject {
6177 /**
6278 * Stable unique key identifying the entity across types.
63- * TypeName + CompositePrimaryKey.
79+ * Format: TypeName|ID
6480 */
6581 readonly typedKey : string ;
6682
6783 /** Represents values received from the server. */
6884 private serverValues : Map < string , Value > ;
6985
70- /** A list of listeners (StubDataObjects) that need to be updated when values change. */
86+ /** A set of listeners (StubDataObjects) that need to be updated when values change. */
7187 private listeners : Set < StubDataObject > ;
72- /** Add a listener to this BDO */
88+
89+ /**
90+ * Adds a StubDataObject to the set of listeners for this BackingDataObject.
91+ * @param listener The StubDataObject to add.
92+ */
7393 addListener ( listener : StubDataObject ) : void {
7494 this . listeners . add ( listener ) ;
7595 }
76- /** Remove a listener from this BDO */
96+
97+ /**
98+ * Removes a StubDataObject from the set of listeners.
99+ * @param listener The StubDataObject to remove.
100+ */
77101 removeListener ( listener : StubDataObject ) : void {
78102 this . listeners . delete ( listener ) ;
79103 }
80104
81- constructor (
82- typedKey : string ,
83- listeners : StubDataObject [ ] ,
84- serverValues : Map < string , Value >
85- ) {
105+ constructor ( typedKey : string , serverValues : Map < string , Value > ) {
86106 this . typedKey = typedKey ;
87- this . listeners = new Set ( listeners ) ;
107+ this . listeners = new Set ( ) ;
88108 this . serverValues = serverValues ;
89109 }
90110
91111 /**
92- * Updates the value for a named property from the server.
112+ * Updates the value for a named property from the server and notifies all listeners .
93113 * @param value The new value from the server.
94114 * @param key The key of the property to update.
95115 */
96116 updateFromServer ( value : Value , key : string ) : void {
97117 this . serverValues . set ( key , value ) ;
98- // update listeners
99118 for ( const listener of this . listeners ) {
100119 listener [ key ] = value ;
101120 }
@@ -129,24 +148,17 @@ export class BackingDataObject {
129148 * @public
130149 */
131150export class Cache {
132- /**
133- * A map of ([query + variables] --> stubs returned from that query).
134- * @public
135- */
151+ /** A map of [query + variables] --> StubDataObjects returned from that query. */
136152 resultTreeCache = new Map < string , StubResultTree > ( ) ;
137153
138- /**
139- * A map of ([entity typename + id] --> BackingDataObject for that entity).
140- * @public
141- */
154+ /** A map of [entity typename + id] --> BackingDataObject for that entity. */
142155 bdoCache = new Map < string , BackingDataObject > ( ) ;
143156
144157 /**
145158 * Creates a unique StrubResultTree cache key for a given query and its variables.
146159 * @param queryName The name of the query.
147160 * @param vars The variables used in the query.
148161 * @returns A unique cache key string.
149- * @public
150162 */
151163 static makeResultTreeCacheKey ( queryName : string , vars : unknown ) : string {
152164 return queryName + '|' + JSON . stringify ( vars ) ;
@@ -157,81 +169,97 @@ export class Cache {
157169 * @param typename The typename of the entity being cached.
158170 * @param id The unique id / primary key of this entity.
159171 * @returns A unique cache key string.
160- * @public
161172 */
162173 static makeBdoCacheKey ( typename : string , id : unknown ) : string {
163174 return typename + '|' + JSON . stringify ( id ) ;
164175 }
165176
166177 /**
167- * Updates the cache with the results of a query.
178+ * Updates the cache with the results of a query. This is the main entry point.
168179 * @param queryResult The result of the query.
169- * @public
170180 */
171- updateCache < Data extends QueryResultData | QueryResultData [ ] , Variables > (
181+ updateCache < Data extends object , Variables > (
172182 queryResult : QueryResult < Data , Variables >
173183 ) : void {
174184 const resultTreeCacheKey = Cache . makeResultTreeCacheKey (
175185 queryResult . ref . name ,
176186 queryResult . ref . variables
177187 ) ;
178188 const stubResultTree : StubResultTree = { } ;
179- // key = "movies" or "actor", etc.
189+
180190 // eslint-disable-next-line guard-for-in
181191 for ( const key in queryResult . data ) {
182- const queryData = queryResult . data [ key ] ;
183- if ( Array . isArray ( queryData ) ) {
192+ const entityOrEntityList = ( queryResult . data as Record < string , unknown > ) [
193+ key
194+ ] ;
195+ if ( Array . isArray ( entityOrEntityList ) ) {
184196 const sdoList : StubDataObjectList = [ ] ;
185- queryData . forEach ( qd => {
186- const sdo : StubDataObject = {
187- ...qd
188- // todo: add in non-cacheable fields
189- } ;
190- sdoList . push ( sdo ) ;
191- const bdo : BackingDataObject = this . updateBdoCache ( qd , sdo ) ;
192- stubResultTree [ key ] = sdoList ;
197+ entityOrEntityList . forEach ( entity => {
198+ if ( isCacheableQueryResultData ( entity ) ) {
199+ const stubDataObject = this . cacheData ( entity ) ;
200+ sdoList . push ( stubDataObject ) ;
201+ }
193202 } ) ;
194- } else {
195- const sdo : StubDataObject = {
196- ...( queryData as QueryResultData ) // ! i don't think i should need a type assertion here, yet TS complains without it...
197- // todo: add in non-cacheable fields
198- } ;
199- stubResultTree [ key ] = sdo ;
200- const bdo = this . updateBdoCache ( queryData as QueryResultData , sdo ) ; // ! i don't think i should need a type assertion here, yet TS complains without it...
203+ stubResultTree [ key ] = sdoList ;
204+ } else if ( isCacheableQueryResultData ( entityOrEntityList ) ) {
205+ const stubDataObject = this . cacheData ( entityOrEntityList ) ;
206+ stubResultTree [ key ] = stubDataObject ;
201207 }
202208 }
203209 this . resultTreeCache . set ( resultTreeCacheKey , stubResultTree ) ;
204210 }
205211
206212 /**
207- * Update the BackingDataObject cache, either adding a new BDO or updating an existing BDO
208- * @param data A single entity from the database .
209- * @returns the BackingDataObject created/upated .
213+ * Caches a single entity: gets or creates its BDO and returns a linked stub.
214+ * @param data A single entity object from the query result .
215+ * @returns A StubDataObject linked to the entity's BackingDataObject .
210216 */
211- private updateBdoCache < Data extends QueryResultData > (
212- data : Data ,
213- stubDataObject : StubDataObject
214- ) : BackingDataObject {
215- const bdoCacheKey = Cache . makeBdoCacheKey ( data [ '__typename' ] , data [ '__id' ] ) ;
216- let backingDataObject = this . bdoCache . get ( bdoCacheKey ) ;
217-
218- if ( backingDataObject ) {
219- // BDO already exists, so update its values from the new data.
220- for ( const [ key , value ] of Object . entries ( data ) ) {
221- // key = "id" or "title", etc.
222- backingDataObject . updateFromServer ( value , key ) ;
223- }
224- backingDataObject . addListener ( stubDataObject ) ;
217+ private cacheData ( data : QueryResultData ) : StubDataObject {
218+ const stubDataaObject : StubDataObject = { ...data } ;
219+ const bdoCacheKey = Cache . makeBdoCacheKey ( data . __typename , data . __id ) ;
220+ const existingBdo = this . bdoCache . get ( bdoCacheKey ) ;
221+
222+ if ( existingBdo ) {
223+ this . updateBdo ( existingBdo , data , stubDataaObject ) ;
225224 } else {
226- // BDO does not exist, so create a new one.
227- const serverValues = new Map < string , Value > ( Object . entries ( data ) ) ;
228- backingDataObject = new BackingDataObject (
229- bdoCacheKey ,
230- [ stubDataObject ] ,
231- serverValues
232- ) ;
233- this . bdoCache . set ( bdoCacheKey , backingDataObject ) ;
225+ this . createBdo ( bdoCacheKey , data , stubDataaObject ) ;
226+ }
227+ return stubDataaObject ;
228+ }
229+
230+ /**
231+ * Creates a new BackingDataObject and adds it to the cache.
232+ * @param bdoCacheKey The cache key for the new BDO.
233+ * @param data The entity data from the server.
234+ * @param stubDataObject The first stub to listen to this BDO.
235+ */
236+ private createBdo (
237+ bdoCacheKey : string ,
238+ data : QueryResultData ,
239+ stubDataObject : StubDataObject
240+ ) : void {
241+ // TODO: don't cache non-cacheable fields!
242+ const serverValues = new Map < string , Value > ( Object . entries ( data ) ) ;
243+ const newBdo = new BackingDataObject ( bdoCacheKey , serverValues ) ;
244+ newBdo . addListener ( stubDataObject ) ;
245+ this . bdoCache . set ( bdoCacheKey , newBdo ) ;
246+ }
247+
248+ /**
249+ * Updates an existing BackingDataObject with new data and a new listener.
250+ * @param backingDataObject The existing BackingDataObject to update.
251+ * @param data The new entity data from the server.
252+ * @param stubDataObject The new stub to add as a listener.
253+ */
254+ private updateBdo (
255+ backingDataObject : BackingDataObject ,
256+ data : QueryResultData ,
257+ stubDataObject : StubDataObject
258+ ) : void {
259+ // TODO: don't cache non-cacheable fields!
260+ for ( const [ key , value ] of Object . entries ( data ) ) {
261+ backingDataObject . updateFromServer ( value , key ) ;
234262 }
235- return backingDataObject ;
263+ backingDataObject . addListener ( stubDataObject ) ;
236264 }
237265}
0 commit comments