Skip to content

Commit 8615a49

Browse files
committed
incorporating feedback
1 parent 91395bf commit 8615a49

File tree

1 file changed

+100
-72
lines changed

1 file changed

+100
-72
lines changed

packages/data-connect/src/core/Cache.ts

Lines changed: 100 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,30 @@ import { QueryResult } from '../api';
2222
type 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
*/
2728
export 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 {
5470
class 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
*/
6076
export 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
*/
131150
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. */
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

Comments
 (0)