@@ -1089,27 +1089,58 @@ export default class JSONAPICache implements Cache {
1089
1089
* @param field
1090
1090
* @return {unknown }
1091
1091
*/
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
+ }
1104
1097
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 ;
1110
1141
}
1111
- return defaultValue ;
1112
1142
}
1143
+ return current ;
1113
1144
}
1114
1145
1115
1146
/**
@@ -1123,29 +1154,114 @@ export default class JSONAPICache implements Cache {
1123
1154
* @param field
1124
1155
* @param value
1125
1156
*/
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
+
1127
1203
const cached = this . __peek ( identifier , false ) ;
1204
+
1205
+ // get existing cache record for base path
1206
+ const basePath = path [ 0 ] ;
1128
1207
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 ]
1133
1212
: 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 ) {
1135
1225
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 ) ;
1137
1227
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.
1139
1247
} 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 ] ) ;
1143
1254
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
+ }
1146
1262
}
1147
1263
1148
- this . _capabilities . notifyChange ( identifier , 'attributes' , attr ) ;
1264
+ this . _capabilities . notifyChange ( identifier , 'attributes' , basePath ) ;
1149
1265
}
1150
1266
1151
1267
/**
0 commit comments