@@ -22,14 +22,30 @@ import { QueryResult } from '../api';
22
22
type Value = string | number | boolean | null | undefined | object | Value [ ] ;
23
23
24
24
/**
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.
26
27
*/
27
28
export interface QueryResultData {
28
29
[ key : string ] : Value ;
29
30
__typename : string ;
30
31
__id : string ;
31
32
}
32
33
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
+
33
49
/**
34
50
* Interface for a stub result tree, with fields which are stub data objects
35
51
*/
@@ -38,7 +54,7 @@ interface StubResultTree {
38
54
}
39
55
40
56
/**
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 .
42
58
* Generated Data implements this interface.
43
59
* @public
44
60
*/
@@ -54,48 +70,51 @@ export interface StubDataObject {
54
70
class StubDataObjectList extends Array < StubDataObject > { }
55
71
56
72
/**
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.
58
74
* @public
59
75
*/
60
76
export class BackingDataObject {
61
77
/**
62
78
* Stable unique key identifying the entity across types.
63
- * TypeName + CompositePrimaryKey.
79
+ * Format: TypeName|ID
64
80
*/
65
81
readonly typedKey : string ;
66
82
67
83
/** Represents values received from the server. */
68
84
private serverValues : Map < string , Value > ;
69
85
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. */
71
87
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
+ */
73
93
addListener ( listener : StubDataObject ) : void {
74
94
this . listeners . add ( listener ) ;
75
95
}
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
+ */
77
101
removeListener ( listener : StubDataObject ) : void {
78
102
this . listeners . delete ( listener ) ;
79
103
}
80
104
81
- constructor (
82
- typedKey : string ,
83
- listeners : StubDataObject [ ] ,
84
- serverValues : Map < string , Value >
85
- ) {
105
+ constructor ( typedKey : string , serverValues : Map < string , Value > ) {
86
106
this . typedKey = typedKey ;
87
- this . listeners = new Set ( listeners ) ;
107
+ this . listeners = new Set ( ) ;
88
108
this . serverValues = serverValues ;
89
109
}
90
110
91
111
/**
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 .
93
113
* @param value The new value from the server.
94
114
* @param key The key of the property to update.
95
115
*/
96
116
updateFromServer ( value : Value , key : string ) : void {
97
117
this . serverValues . set ( key , value ) ;
98
- // update listeners
99
118
for ( const listener of this . listeners ) {
100
119
listener [ key ] = value ;
101
120
}
@@ -129,24 +148,17 @@ export class BackingDataObject {
129
148
* @public
130
149
*/
131
150
export 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. */
136
152
resultTreeCache = new Map < string , StubResultTree > ( ) ;
137
153
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. */
142
155
bdoCache = new Map < string , BackingDataObject > ( ) ;
143
156
144
157
/**
145
158
* Creates a unique StrubResultTree cache key for a given query and its variables.
146
159
* @param queryName The name of the query.
147
160
* @param vars The variables used in the query.
148
161
* @returns A unique cache key string.
149
- * @public
150
162
*/
151
163
static makeResultTreeCacheKey ( queryName : string , vars : unknown ) : string {
152
164
return queryName + '|' + JSON . stringify ( vars ) ;
@@ -157,81 +169,97 @@ export class Cache {
157
169
* @param typename The typename of the entity being cached.
158
170
* @param id The unique id / primary key of this entity.
159
171
* @returns A unique cache key string.
160
- * @public
161
172
*/
162
173
static makeBdoCacheKey ( typename : string , id : unknown ) : string {
163
174
return typename + '|' + JSON . stringify ( id ) ;
164
175
}
165
176
166
177
/**
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.
168
179
* @param queryResult The result of the query.
169
- * @public
170
180
*/
171
- updateCache < Data extends QueryResultData | QueryResultData [ ] , Variables > (
181
+ updateCache < Data extends object , Variables > (
172
182
queryResult : QueryResult < Data , Variables >
173
183
) : void {
174
184
const resultTreeCacheKey = Cache . makeResultTreeCacheKey (
175
185
queryResult . ref . name ,
176
186
queryResult . ref . variables
177
187
) ;
178
188
const stubResultTree : StubResultTree = { } ;
179
- // key = "movies" or "actor", etc.
189
+
180
190
// eslint-disable-next-line guard-for-in
181
191
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 ) ) {
184
196
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
+ }
193
202
} ) ;
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 ;
201
207
}
202
208
}
203
209
this . resultTreeCache . set ( resultTreeCacheKey , stubResultTree ) ;
204
210
}
205
211
206
212
/**
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 .
210
216
*/
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 ) ;
225
224
} 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 ) ;
234
262
}
235
- return backingDataObject ;
263
+ backingDataObject . addListener ( stubDataObject ) ;
236
264
}
237
265
}
0 commit comments