Skip to content

Commit 2356697

Browse files
richgtrunspired
andauthored
Implement schema-array for schema-record (#9384)
feat: schema array Co-authored-by: Chris Thoburn <runspired@users.noreply.github.com>
1 parent 2416e6d commit 2356697

File tree

12 files changed

+1476
-198
lines changed

12 files changed

+1476
-198
lines changed

packages/core-types/src/-private.ts

+2
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ type GlobalKey =
9595
| 'Destroy'
9696
| 'Identifier'
9797
| 'Editable'
98+
| 'EmbeddedPath'
99+
| 'EmbeddedType'
98100
| 'Parent'
99101
| 'Checkout'
100102
| 'Legacy';

packages/core-types/src/cache.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,7 @@ export interface Cache {
339339
* @param field
340340
* @return {unknown}
341341
*/
342-
getAttr(identifier: StableRecordIdentifier, field: string): Value | undefined;
342+
getAttr(identifier: StableRecordIdentifier, field: string | string[]): Value | undefined;
343343

344344
/**
345345
* Mutate the data for an attribute in the cache
@@ -352,7 +352,7 @@ export interface Cache {
352352
* @param field
353353
* @param value
354354
*/
355-
setAttr(identifier: StableRecordIdentifier, field: string, value: Value): void;
355+
setAttr(identifier: StableRecordIdentifier, field: string | string[], value: Value): void;
356356

357357
/**
358358
* Query the cache for the changed attributes of a resource.

packages/json-api/src/-private/cache.ts

+148-32
Original file line numberDiff line numberDiff line change
@@ -1089,27 +1089,58 @@ export default class JSONAPICache implements Cache {
10891089
* @param field
10901090
* @return {unknown}
10911091
*/
1092-
getAttr(identifier: StableRecordIdentifier, attr: string): Value | undefined {
1093-
const cached = this.__peek(identifier, true);
1094-
if (cached.localAttrs && attr in cached.localAttrs) {
1095-
return cached.localAttrs[attr];
1096-
} else if (cached.inflightAttrs && attr in cached.inflightAttrs) {
1097-
return cached.inflightAttrs[attr];
1098-
} else if (cached.remoteAttrs && attr in cached.remoteAttrs) {
1099-
return cached.remoteAttrs[attr];
1100-
} else if (cached.defaultAttrs && attr in cached.defaultAttrs) {
1101-
return cached.defaultAttrs[attr];
1102-
} else {
1103-
const attrSchema = this._capabilities.schema.fields(identifier).get(attr);
1092+
getAttr(identifier: StableRecordIdentifier, attr: string | string[]): Value | undefined {
1093+
const isSimplePath = !Array.isArray(attr) || attr.length === 1;
1094+
if (Array.isArray(attr) && attr.length === 1) {
1095+
attr = attr[0];
1096+
}
11041097

1105-
upgradeCapabilities(this._capabilities);
1106-
const defaultValue = getDefaultValue(attrSchema, identifier, this._capabilities._store);
1107-
if (schemaHasLegacyDefaultValueFn(attrSchema)) {
1108-
cached.defaultAttrs = cached.defaultAttrs || (Object.create(null) as Record<string, Value>);
1109-
cached.defaultAttrs[attr] = defaultValue;
1098+
if (isSimplePath) {
1099+
const attribute = attr as string;
1100+
const cached = this.__peek(identifier, true);
1101+
if (cached.localAttrs && attribute in cached.localAttrs) {
1102+
return cached.localAttrs[attribute];
1103+
} else if (cached.inflightAttrs && attribute in cached.inflightAttrs) {
1104+
return cached.inflightAttrs[attribute];
1105+
} else if (cached.remoteAttrs && attribute in cached.remoteAttrs) {
1106+
return cached.remoteAttrs[attribute];
1107+
} else if (cached.defaultAttrs && attribute in cached.defaultAttrs) {
1108+
return cached.defaultAttrs[attribute];
1109+
} else {
1110+
const attrSchema = this._capabilities.schema.fields(identifier).get(attribute);
1111+
1112+
upgradeCapabilities(this._capabilities);
1113+
const defaultValue = getDefaultValue(attrSchema, identifier, this._capabilities._store);
1114+
if (schemaHasLegacyDefaultValueFn(attrSchema)) {
1115+
cached.defaultAttrs = cached.defaultAttrs || (Object.create(null) as Record<string, Value>);
1116+
cached.defaultAttrs[attribute] = defaultValue;
1117+
}
1118+
return defaultValue;
1119+
}
1120+
}
1121+
1122+
// TODO @runspired consider whether we need a defaultValue cache in SchemaRecord
1123+
// like we do for the simple case above.
1124+
const path: string[] = attr as string[];
1125+
const cached = this.__peek(identifier, true);
1126+
const basePath = path[0];
1127+
let current = cached.localAttrs && basePath in cached.localAttrs ? cached.localAttrs[basePath] : undefined;
1128+
if (current === undefined) {
1129+
current = cached.inflightAttrs && basePath in cached.inflightAttrs ? cached.inflightAttrs[basePath] : undefined;
1130+
}
1131+
if (current === undefined) {
1132+
current = cached.remoteAttrs && basePath in cached.remoteAttrs ? cached.remoteAttrs[basePath] : undefined;
1133+
}
1134+
if (current === undefined) {
1135+
return undefined;
1136+
}
1137+
for (let i = 1; i < path.length; i++) {
1138+
current = (current as ObjectValue)[path[i]];
1139+
if (current === undefined) {
1140+
return undefined;
11101141
}
1111-
return defaultValue;
11121142
}
1143+
return current;
11131144
}
11141145

11151146
/**
@@ -1123,29 +1154,114 @@ export default class JSONAPICache implements Cache {
11231154
* @param field
11241155
* @param value
11251156
*/
1126-
setAttr(identifier: StableRecordIdentifier, attr: string, value: Value): void {
1157+
setAttr(identifier: StableRecordIdentifier, attr: string | string[], value: Value): void {
1158+
// this assert works to ensure we have a non-empty string and/or a non-empty array
1159+
assert('setAttr must receive at least one attribute path', attr.length > 0);
1160+
const isSimplePath = !Array.isArray(attr) || attr.length === 1;
1161+
1162+
if (Array.isArray(attr) && attr.length === 1) {
1163+
attr = attr[0];
1164+
}
1165+
1166+
if (isSimplePath) {
1167+
const cached = this.__peek(identifier, false);
1168+
const currentAttr = attr as string;
1169+
const existing =
1170+
cached.inflightAttrs && currentAttr in cached.inflightAttrs
1171+
? cached.inflightAttrs[currentAttr]
1172+
: cached.remoteAttrs && currentAttr in cached.remoteAttrs
1173+
? cached.remoteAttrs[currentAttr]
1174+
: undefined;
1175+
1176+
if (existing !== value) {
1177+
cached.localAttrs = cached.localAttrs || (Object.create(null) as Record<string, Value>);
1178+
cached.localAttrs[currentAttr] = value;
1179+
cached.changes = cached.changes || (Object.create(null) as Record<string, [Value, Value]>);
1180+
cached.changes[currentAttr] = [existing, value];
1181+
} else if (cached.localAttrs) {
1182+
delete cached.localAttrs[currentAttr];
1183+
delete cached.changes![currentAttr];
1184+
}
1185+
1186+
if (cached.defaultAttrs && currentAttr in cached.defaultAttrs) {
1187+
delete cached.defaultAttrs[currentAttr];
1188+
}
1189+
1190+
this._capabilities.notifyChange(identifier, 'attributes', currentAttr);
1191+
return;
1192+
}
1193+
1194+
// get current value from local else inflight else remote
1195+
// structuredClone current if not local (or always?)
1196+
// traverse path, update value at path
1197+
// notify change at first link in path.
1198+
// second pass optimization is change notifyChange signature to take an array path
1199+
1200+
// guaranteed that we have path of at least 2 in length
1201+
const path: string[] = attr as string[];
1202+
11271203
const cached = this.__peek(identifier, false);
1204+
1205+
// get existing cache record for base path
1206+
const basePath = path[0];
11281207
const existing =
1129-
cached.inflightAttrs && attr in cached.inflightAttrs
1130-
? cached.inflightAttrs[attr]
1131-
: cached.remoteAttrs && attr in cached.remoteAttrs
1132-
? cached.remoteAttrs[attr]
1208+
cached.inflightAttrs && basePath in cached.inflightAttrs
1209+
? cached.inflightAttrs[basePath]
1210+
: cached.remoteAttrs && basePath in cached.remoteAttrs
1211+
? cached.remoteAttrs[basePath]
11331212
: undefined;
1134-
if (existing !== value) {
1213+
1214+
let existingAttr;
1215+
if (existing) {
1216+
existingAttr = (existing as ObjectValue)[path[1]];
1217+
1218+
for (let i = 2; i < path.length; i++) {
1219+
// the specific change we're making is at path[length - 1]
1220+
existingAttr = (existingAttr as ObjectValue)[path[i]];
1221+
}
1222+
}
1223+
1224+
if (existingAttr !== value) {
11351225
cached.localAttrs = cached.localAttrs || (Object.create(null) as Record<string, Value>);
1136-
cached.localAttrs[attr] = value;
1226+
cached.localAttrs[basePath] = cached.localAttrs[basePath] || structuredClone(existing);
11371227
cached.changes = cached.changes || (Object.create(null) as Record<string, [Value, Value]>);
1138-
cached.changes[attr] = [existing, value];
1228+
let currentLocal = cached.localAttrs[basePath] as ObjectValue;
1229+
let nextLink = 1;
1230+
1231+
while (nextLink < path.length - 1) {
1232+
currentLocal = currentLocal[path[nextLink++]] as ObjectValue;
1233+
}
1234+
currentLocal[path[nextLink]] = value as ObjectValue;
1235+
1236+
cached.changes[basePath] = [existing, cached.localAttrs[basePath] as ObjectValue];
1237+
1238+
// since we initiaize the value as basePath as a clone of the value at the remote basePath
1239+
// then in theory we can use JSON.stringify to compare the two values as key insertion order
1240+
// ought to be consistent.
1241+
// we try/catch this because users have a habit of doing "Bad Things"TM wherein the cache contains
1242+
// stateful values that are not JSON serializable correctly such as Dates.
1243+
// in the case that we error, we fallback to not removing the local value
1244+
// so that any changes we don't understand are preserved. Thse objects would then sometimes
1245+
// appear to be dirty unnecessarily, and for folks that open an issue we can guide them
1246+
// to make their cache data less stateful.
11391247
} else if (cached.localAttrs) {
1140-
delete cached.localAttrs[attr];
1141-
delete cached.changes![attr];
1142-
}
1248+
try {
1249+
if (!existing) {
1250+
return;
1251+
}
1252+
const existingStr = JSON.stringify(existing);
1253+
const newStr = JSON.stringify(cached.localAttrs[basePath]);
11431254

1144-
if (cached.defaultAttrs && attr in cached.defaultAttrs) {
1145-
delete cached.defaultAttrs[attr];
1255+
if (existingStr !== newStr) {
1256+
delete cached.localAttrs[basePath];
1257+
delete cached.changes![basePath];
1258+
}
1259+
} catch (e) {
1260+
// noop
1261+
}
11461262
}
11471263

1148-
this._capabilities.notifyChange(identifier, 'attributes', attr);
1264+
this._capabilities.notifyChange(identifier, 'attributes', basePath);
11491265
}
11501266

11511267
/**

0 commit comments

Comments
 (0)