Skip to content

Commit b6271e0

Browse files
committed
normalization finished, need to update createSrt
1 parent 11c0d70 commit b6271e0

File tree

1 file changed

+145
-60
lines changed

1 file changed

+145
-60
lines changed

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

Lines changed: 145 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -18,26 +18,81 @@
1818

1919
import { 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
*/
2984
interface 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

Comments
 (0)