@warp-drive-mirror/json-api 5.10.0-alpha.7 → 5.10.0-alpha.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1035,20 +1035,89 @@ function getDefaultValue(schema, identifier, store) {
1035
1035
  if (transform?.defaultValue) return transform.defaultValue(options || null, identifier);
1036
1036
  }
1037
1037
  }
1038
- function calculateChangedKeys(cached, updates, fields) {
1039
- const changedKeys = /* @__PURE__ */ new Set();
1040
- const keys = Object.keys(updates);
1041
- const length = keys.length;
1042
- const localAttrs = cached.localAttrs;
1043
- const original = Object.assign(Object.create(null), cached.remoteAttrs, cached.inflightAttrs);
1044
- for (let i = 0; i < length; i++) {
1038
+ /**
1039
+ * Narrow `subset` to the members that survived {@link patchLocalAttributes}' pruning. Only called
1040
+ * when that pruning actually removed something, so the allocation is skipped on the common path
1041
+ * where a resource has no local edits at all.
1042
+ */
1043
+ function retainSurviving(subset, survivors) {
1044
+ if (!subset?.size) return subset;
1045
+ const result = /* @__PURE__ */ new Set();
1046
+ for (const key of subset) if (survivors.has(key)) result.add(key);
1047
+ return result.size ? result : void 0;
1048
+ }
1049
+ /**
1050
+ * Which projection each changed key moved, so the caller can notify on the matching channel:
1051
+ * `localOnly` and `remoteOnly` on theirs, `both` unscoped. `remoteChanged` is the union
1052
+ * {@link patchLocalAttributes} reconciles against.
1053
+ *
1054
+ * `undefined` rather than empty Sets: this runs on every `upsert`, and a resource where nothing
1055
+ * moved should cost no allocation.
1056
+ */
1057
+ const NO_PROJECTION_CHANGES = Object.freeze({
1058
+ localOnly: void 0,
1059
+ remoteOnly: void 0,
1060
+ both: void 0,
1061
+ remoteChanged: void 0
1062
+ });
1063
+ /**
1064
+ * The keys a merge could move: everything the payload carries, plus anything being promoted that
1065
+ * the payload did not mention. `null` when there is nothing to examine.
1066
+ *
1067
+ * `upsert` promotes nothing, so it gets the `Object.keys` array back unchanged. Only a commit
1068
+ * carrying both a payload and promoted attrs pays for a copy, and that is once per save rather
1069
+ * than once per resource in a document.
1070
+ */
1071
+ function candidateKeys(updates, promoted) {
1072
+ const updateKeys = updates ? Object.keys(updates) : null;
1073
+ const promotedKeys = promoted ? Object.keys(promoted) : null;
1074
+ if (!promotedKeys?.length) return updateKeys?.length ? updateKeys : null;
1075
+ if (!updateKeys?.length) return promotedKeys;
1076
+ const keys = updateKeys.slice();
1077
+ for (let i = 0; i < promotedKeys.length; i++) if (!(promotedKeys[i] in updates)) keys.push(promotedKeys[i]);
1078
+ return keys;
1079
+ }
1080
+ /**
1081
+ * Partition the keys an incoming payload touches by which projection each one moves.
1082
+ *
1083
+ * Takes the values rather than the `CachedResource` so it cannot observe the merge it is
1084
+ * describing: `prevRemote` is the remote state as it stands *before* `promoted` and `updates` are
1085
+ * applied, and the two callers merge different things. Reading them off `cached` would make the
1086
+ * result depend on whether the caller had already mutated it.
1087
+ */
1088
+ function calculateChangedKeys(prevRemote, localAttrs, updates, promoted, fields) {
1089
+ const keys = candidateKeys(updates, promoted);
1090
+ if (keys === null) return NO_PROJECTION_CHANGES;
1091
+ let localOnly;
1092
+ let remoteOnly;
1093
+ let both;
1094
+ let remoteChanged;
1095
+ for (let i = 0; i < keys.length; i++) {
1045
1096
  const key = keys[i];
1046
- if (!fields.has(key)) continue;
1047
- const value = updates[key];
1048
- if (localAttrs && localAttrs[key] !== void 0) continue;
1049
- if (original[key] !== value) changedKeys.add(key);
1097
+ const field = fields.get(key);
1098
+ if (!field || isRelationship(field)) continue;
1099
+ const isPromoted = promoted !== null && key in promoted;
1100
+ const isUpdated = updates !== void 0 && key in updates;
1101
+ const wasRemote = prevRemote ? prevRemote[key] : void 0;
1102
+ const nowRemote = isUpdated ? updates[key] : isPromoted ? promoted[key] : wasRemote;
1103
+ const localEdit = localAttrs ? localAttrs[key] : void 0;
1104
+ const isEdited = localEdit !== void 0;
1105
+ const wasLocal = isEdited ? localEdit : isPromoted ? promoted[key] : wasRemote;
1106
+ const nowLocal = isEdited ? localEdit : nowRemote;
1107
+ const remoteMoved = wasRemote !== nowRemote;
1108
+ const localMoved = wasLocal !== nowLocal;
1109
+ if (remoteMoved) {
1110
+ (remoteChanged ??= /* @__PURE__ */ new Set()).add(key);
1111
+ if (localMoved) (both ??= /* @__PURE__ */ new Set()).add(key);
1112
+ else (remoteOnly ??= /* @__PURE__ */ new Set()).add(key);
1113
+ } else if (localMoved) (localOnly ??= /* @__PURE__ */ new Set()).add(key);
1050
1114
  }
1051
- return changedKeys;
1115
+ return remoteChanged || localOnly ? {
1116
+ localOnly,
1117
+ remoteOnly,
1118
+ both,
1119
+ remoteChanged
1120
+ } : NO_PROJECTION_CHANGES;
1052
1121
  }
1053
1122
  function cacheIsEmpty(cached) {
1054
1123
  return !cached || cached.remoteAttrs === null && cached.inflightAttrs === null && cached.localAttrs === null;
@@ -1205,7 +1274,7 @@ function cacheUpsert(cache, identifier, data, calculateChanges) {
1205
1274
  cache._capabilities.notifyChange(identifier, "state", null);
1206
1275
  }
1207
1276
  const fields = getCacheFields(cache, identifier);
1208
- if (calculateChanges && existed && data.attributes) changedKeys = calculateChangedKeys(cached, data.attributes, fields);
1277
+ if (calculateChanges && existed && data.attributes) changedKeys = calculateChangedKeys(cached.remoteAttrs, cached.localAttrs, data.attributes, null, fields).remoteChanged;
1209
1278
  cached.remoteAttrs = Object.assign(cached.remoteAttrs || Object.create(null), data.attributes);
1210
1279
  if (cached.localAttrs) {
1211
1280
  if (patchLocalAttributes(cached, changedKeys)) cache._capabilities.notifyChange(identifier, "state", null);
@@ -1309,15 +1378,21 @@ function didCommit(cache, committedIdentifier, data, op) {
1309
1378
  if (data.relationships) setupRelationships(cache.__graph, fields, identifier, data);
1310
1379
  newCanonicalAttributes = data.attributes;
1311
1380
  }
1312
- const changedKeys = newCanonicalAttributes && calculateChangedKeys(cached, newCanonicalAttributes, fields);
1381
+ const changes = calculateChangedKeys(cached.remoteAttrs, cached.localAttrs, newCanonicalAttributes, cached.inflightAttrs, fields);
1313
1382
  cached.remoteAttrs = Object.assign(cached.remoteAttrs || Object.create(null), cached.inflightAttrs, newCanonicalAttributes);
1314
1383
  cached.inflightAttrs = null;
1315
- patchLocalAttributes(cached, changedKeys);
1384
+ const pruned = patchLocalAttributes(cached, changes.remoteChanged);
1316
1385
  if (cached.errors) {
1317
1386
  cached.errors = null;
1318
1387
  cache._capabilities.notifyChange(identifier, "errors", null);
1319
1388
  }
1320
- if (changedKeys?.size) cache._capabilities.notifyChange(identifier, "attributes", changedKeys);
1389
+ const { remoteChanged, localOnly } = changes;
1390
+ const reprune = pruned && remoteChanged !== void 0;
1391
+ const both = reprune ? retainSurviving(changes.both, remoteChanged) : changes.both;
1392
+ const remoteOnly = reprune ? retainSurviving(changes.remoteOnly, remoteChanged) : changes.remoteOnly;
1393
+ if (both?.size) cache._capabilities.notifyChange(identifier, "attributes", both);
1394
+ if (remoteOnly?.size) cache._capabilities.notifyChange(identifier, "attributes", remoteOnly, "remote");
1395
+ if (localOnly?.size) cache._capabilities.notifyChange(identifier, "attributes", localOnly, "local");
1321
1396
  cache._capabilities.notifyChange(identifier, "state", null);
1322
1397
  }
1323
1398
  function willCommit(cache, identifier) {