@warp-drive-mirror/json-api 5.10.0-alpha.1 → 5.10.0-alpha.12
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.
- package/README.md +81 -7
- package/dist/index.d.ts +295 -21
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +569 -134
- package/dist/index.js.map +1 -1
- package/dist/unpkg/dev/index.js +570 -135
- package/dist/unpkg/dev/index.js.map +1 -1
- package/dist/unpkg/dev-deprecated/index.js +570 -135
- package/dist/unpkg/dev-deprecated/index.js.map +1 -1
- package/dist/unpkg/prod/index.js +559 -133
- package/dist/unpkg/prod/index.js.map +1 -1
- package/dist/unpkg/prod-deprecated/index.js +559 -133
- package/dist/unpkg/prod-deprecated/index.js.map +1 -1
- package/package.json +8 -8
package/dist/unpkg/dev/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { graphFor, isBelongsTo, peekGraph } from "@warp-drive-mirror/core/graph/-private";
|
|
2
|
-
import { assertPrivateCapabilities, isRequestKey, isResourceKey, logGroup } from "@warp-drive-mirror/core/store/-private";
|
|
2
|
+
import { assertPrivateCapabilities, fieldValueIdentity, isRequestKey, isResourceKey, logGroup } from "@warp-drive-mirror/core/store/-private";
|
|
3
3
|
import Fuse from "fuse.js";
|
|
4
4
|
import jsonToAst from "json-to-ast";
|
|
5
5
|
|
|
@@ -912,7 +912,7 @@ function validateResourceDocument(reporter, doc) {
|
|
|
912
912
|
}
|
|
913
913
|
|
|
914
914
|
//#endregion
|
|
915
|
-
//#region ../../node_modules/.pnpm/@embroider+macros@1.
|
|
915
|
+
//#region ../../node_modules/.pnpm/@embroider+macros@1.21.1/node_modules/@embroider/macros/src/addon/runtime.js
|
|
916
916
|
function config(packageRoot) {
|
|
917
917
|
return runtimeConfig.packages[packageRoot];
|
|
918
918
|
}
|
|
@@ -1032,14 +1032,101 @@ const EMPTY_ITERATOR = { iterator() {
|
|
|
1032
1032
|
};
|
|
1033
1033
|
} };
|
|
1034
1034
|
} };
|
|
1035
|
+
/** One attributes hash on a {@link CachedResource}: a **layer** a projection is read through. */
|
|
1036
|
+
/**
|
|
1037
|
+
* The cache's entry for a single resource: its id, its attribute values split
|
|
1038
|
+
* across four **layers**, and the flags tracking where it sits in the
|
|
1039
|
+
* create/update/delete lifecycle.
|
|
1040
|
+
*
|
|
1041
|
+
* A **projection** answers "what is this field's value" by reading a stack of
|
|
1042
|
+
* layers top-down; the first layer holding the field wins. The two projections,
|
|
1043
|
+
* with their resolution orders:
|
|
1044
|
+
*
|
|
1045
|
+
* - **remote state**, what the _immutable_ record reads:
|
|
1046
|
+
* {@link RESOLUTION_ORDER_REMOTE_STATE}
|
|
1047
|
+
* - **local state**, what an _editable_ copy reads:
|
|
1048
|
+
* {@link RESOLUTION_ORDER_LOCAL_STATE}
|
|
1049
|
+
*
|
|
1050
|
+
* Local state is remote state with the uncommitted mutations laid over it. The
|
|
1051
|
+
* guides call those mutations "the diff"; here they are simply the top layers of
|
|
1052
|
+
* local state, {@link CachedResource.localAttrs | localAttrs} and
|
|
1053
|
+
* {@link CachedResource.inflightAttrs | inflightAttrs}.
|
|
1054
|
+
*
|
|
1055
|
+
* A field is **dirty** while `localAttrs` or `inflightAttrs` holds it. A save
|
|
1056
|
+
* **commits** the mutation: `didCommit` merges the in-flight values into
|
|
1057
|
+
* `remoteAttrs` and removes them from `inflightAttrs`. A push carrying the same
|
|
1058
|
+
* value as a pending edit in `localAttrs` has the same effect on that edit: the
|
|
1059
|
+
* server already holds it, so it is removed from `localAttrs`. "Same value" is
|
|
1060
|
+
* decided by the field's schema: a schema-object with an identity hash compares
|
|
1061
|
+
* by that hash, everything else by reference.
|
|
1062
|
+
*
|
|
1063
|
+
* Thus, a dirty field reads as mutated for local readers only, and goes on doing
|
|
1064
|
+
* so while its save is in flight.
|
|
1065
|
+
*
|
|
1066
|
+
* @internal
|
|
1067
|
+
*/
|
|
1068
|
+
/** A layer name on a {@link CachedResource}. */
|
|
1069
|
+
/**
|
|
1070
|
+
* A layer that exists only for the duration of a merge: the attributes arriving from the
|
|
1071
|
+
* server in a save response or a push. {@link partitionChangedKeys} reads it as if it were
|
|
1072
|
+
* already on the resource; {@link mergeIntoRemote} then folds the same attributes in, handed
|
|
1073
|
+
* to it directly. Never part of a reader's resolution order.
|
|
1074
|
+
*/
|
|
1075
|
+
/** Anything a projection can be read through: a {@link CachedResource}, or the layers a merge reads, built around one. */
|
|
1076
|
+
/** What the immutable record reads. */
|
|
1077
|
+
const RESOLUTION_ORDER_REMOTE_STATE = ["remoteAttrs", "defaultAttrs"];
|
|
1078
|
+
/**
|
|
1079
|
+
* What a new local edit replaces: the in-flight value if a save is carrying one, else the
|
|
1080
|
+
* persisted value. Put another way, what the record will read once the in-flight save lands,
|
|
1081
|
+
* assuming the server agrees. This is local state beneath `localAttrs`, less `defaultAttrs`: an
|
|
1082
|
+
* edit equal to a schema default is still an edit, since nothing persisted holds that value.
|
|
1083
|
+
*
|
|
1084
|
+
* Saving does not use this baseline. {@link JSONAPICache.changedAttrs | changedAttrs} still lists
|
|
1085
|
+
* in-flight values as unsaved, so a second save re-sends them rather than assuming the first one
|
|
1086
|
+
* will succeed.
|
|
1087
|
+
*/
|
|
1088
|
+
const RESOLUTION_ORDER_EDIT_BASELINE = ["inflightAttrs", "remoteAttrs"];
|
|
1089
|
+
/** What an editable copy reads. */
|
|
1090
|
+
const RESOLUTION_ORDER_LOCAL_STATE = [
|
|
1091
|
+
"localAttrs",
|
|
1092
|
+
...RESOLUTION_ORDER_EDIT_BASELINE,
|
|
1093
|
+
"defaultAttrs"
|
|
1094
|
+
];
|
|
1095
|
+
const RESOLUTION_ORDER_REMOTE_AFTER_COMMIT = [
|
|
1096
|
+
"incomingAttrs",
|
|
1097
|
+
"inflightAttrs",
|
|
1098
|
+
...RESOLUTION_ORDER_REMOTE_STATE
|
|
1099
|
+
];
|
|
1100
|
+
const RESOLUTION_ORDER_LOCAL_AFTER_COMMIT = ["localAttrs", ...RESOLUTION_ORDER_REMOTE_AFTER_COMMIT];
|
|
1101
|
+
const RESOLUTION_ORDER_REMOTE_AFTER_UPSERT = ["incomingAttrs", ...RESOLUTION_ORDER_REMOTE_STATE];
|
|
1102
|
+
const RESOLUTION_ORDER_LOCAL_AFTER_UPSERT = [
|
|
1103
|
+
"localAttrs",
|
|
1104
|
+
"inflightAttrs",
|
|
1105
|
+
...RESOLUTION_ORDER_REMOTE_AFTER_UPSERT
|
|
1106
|
+
];
|
|
1107
|
+
/** The layers an after-merge order folds into `remoteAttrs`, lowest precedence first. */
|
|
1108
|
+
function layersFoldedIntoRemote(order) {
|
|
1109
|
+
return order.slice(0, order.indexOf("remoteAttrs")).reverse();
|
|
1110
|
+
}
|
|
1111
|
+
const MERGE_RESOLUTION = {
|
|
1112
|
+
commit: {
|
|
1113
|
+
remoteAfter: RESOLUTION_ORDER_REMOTE_AFTER_COMMIT,
|
|
1114
|
+
localAfter: RESOLUTION_ORDER_LOCAL_AFTER_COMMIT,
|
|
1115
|
+
folded: layersFoldedIntoRemote(RESOLUTION_ORDER_REMOTE_AFTER_COMMIT)
|
|
1116
|
+
},
|
|
1117
|
+
upsert: {
|
|
1118
|
+
remoteAfter: RESOLUTION_ORDER_REMOTE_AFTER_UPSERT,
|
|
1119
|
+
localAfter: RESOLUTION_ORDER_LOCAL_AFTER_UPSERT,
|
|
1120
|
+
folded: layersFoldedIntoRemote(RESOLUTION_ORDER_REMOTE_AFTER_UPSERT)
|
|
1121
|
+
}
|
|
1122
|
+
};
|
|
1035
1123
|
function makeCache() {
|
|
1036
1124
|
return {
|
|
1037
1125
|
id: null,
|
|
1038
|
-
remoteAttrs: null,
|
|
1039
1126
|
localAttrs: null,
|
|
1040
|
-
defaultAttrs: null,
|
|
1041
1127
|
inflightAttrs: null,
|
|
1042
|
-
|
|
1128
|
+
remoteAttrs: null,
|
|
1129
|
+
defaultAttrs: null,
|
|
1043
1130
|
errors: null,
|
|
1044
1131
|
isNew: false,
|
|
1045
1132
|
isDeleted: false,
|
|
@@ -1090,9 +1177,9 @@ var JSONAPICache = class {
|
|
|
1090
1177
|
/**
|
|
1091
1178
|
* Cache the response to a request
|
|
1092
1179
|
*
|
|
1093
|
-
* Implements
|
|
1180
|
+
* Implements {@link Cache.put | Cache.put}.
|
|
1094
1181
|
*
|
|
1095
|
-
* Expects a StructuredDocument whose `content` member is a JsonApiDocument.
|
|
1182
|
+
* Expects a {@link StructuredDocument} whose `content` member is a JsonApiDocument.
|
|
1096
1183
|
*
|
|
1097
1184
|
* ```js
|
|
1098
1185
|
* cache.put({
|
|
@@ -1225,6 +1312,16 @@ var JSONAPICache = class {
|
|
|
1225
1312
|
* Update the "remote" or "canonical" (persisted) state of the Cache
|
|
1226
1313
|
* by merging new information into the existing state.
|
|
1227
1314
|
*
|
|
1315
|
+
* @example
|
|
1316
|
+
* ```ts
|
|
1317
|
+
* cache.patch({
|
|
1318
|
+
* op: 'update',
|
|
1319
|
+
* record: identifier,
|
|
1320
|
+
* field: 'name',
|
|
1321
|
+
* value: 'Chris',
|
|
1322
|
+
* });
|
|
1323
|
+
* ```
|
|
1324
|
+
*
|
|
1228
1325
|
* @category Cache Management
|
|
1229
1326
|
* @public
|
|
1230
1327
|
* @param op the operation or list of operations to perform
|
|
@@ -1242,6 +1339,16 @@ var JSONAPICache = class {
|
|
|
1242
1339
|
/**
|
|
1243
1340
|
* Update the "local" or "current" (unpersisted) state of the Cache
|
|
1244
1341
|
*
|
|
1342
|
+
* @example
|
|
1343
|
+
* ```ts
|
|
1344
|
+
* cache.mutate({
|
|
1345
|
+
* op: 'replaceRelatedRecord',
|
|
1346
|
+
* record: identifier,
|
|
1347
|
+
* field: 'author',
|
|
1348
|
+
* value: authorIdentifier,
|
|
1349
|
+
* });
|
|
1350
|
+
* ```
|
|
1351
|
+
*
|
|
1245
1352
|
* @category Cache Management
|
|
1246
1353
|
* @public
|
|
1247
1354
|
*/
|
|
@@ -1278,8 +1385,8 @@ var JSONAPICache = class {
|
|
|
1278
1385
|
* not require retainining connections to the Store
|
|
1279
1386
|
* and Cache to present data on a per-field basis.
|
|
1280
1387
|
*
|
|
1281
|
-
* This generally takes the place of
|
|
1282
|
-
* an API and may even take the place of
|
|
1388
|
+
* This generally takes the place of {@link JSONAPICache.getAttr | getAttr} as
|
|
1389
|
+
* an API and may even take the place of {@link JSONAPICache.getRelationship | getRelationship}
|
|
1283
1390
|
* depending on implementation specifics, though this
|
|
1284
1391
|
* latter usage is less recommended due to the advantages
|
|
1285
1392
|
* of the Graph handling necessary entanglements and
|
|
@@ -1294,6 +1401,12 @@ var JSONAPICache = class {
|
|
|
1294
1401
|
* the various internal WarpDrive bookkeeping fields.
|
|
1295
1402
|
* :::
|
|
1296
1403
|
*
|
|
1404
|
+
* @example
|
|
1405
|
+
* ```ts
|
|
1406
|
+
* const resource = cache.peek(identifier);
|
|
1407
|
+
* const document = cache.peek(requestKey);
|
|
1408
|
+
* ```
|
|
1409
|
+
*
|
|
1297
1410
|
* @category Cache Management
|
|
1298
1411
|
* @public
|
|
1299
1412
|
*/
|
|
@@ -1333,6 +1446,12 @@ var JSONAPICache = class {
|
|
|
1333
1446
|
/**
|
|
1334
1447
|
* Peek the remote resource data from the Cache.
|
|
1335
1448
|
*
|
|
1449
|
+
* @example
|
|
1450
|
+
* ```ts
|
|
1451
|
+
* const resource = cache.peekRemoteState(identifier);
|
|
1452
|
+
* const document = cache.peekRemoteState(requestKey);
|
|
1453
|
+
* ```
|
|
1454
|
+
*
|
|
1336
1455
|
* @category Cache Management
|
|
1337
1456
|
* @public
|
|
1338
1457
|
*/
|
|
@@ -1373,9 +1492,14 @@ var JSONAPICache = class {
|
|
|
1373
1492
|
* Peek the Cache for the existing request data associated with
|
|
1374
1493
|
* a cacheable request.
|
|
1375
1494
|
*
|
|
1376
|
-
* This is effectively the reverse of
|
|
1495
|
+
* This is effectively the reverse of {@link JSONAPICache.put | put} for a request in
|
|
1377
1496
|
* that it will return the the request, response, and content
|
|
1378
|
-
* whereas
|
|
1497
|
+
* whereas {@link JSONAPICache.peek | peek} will return just the `content`.
|
|
1498
|
+
*
|
|
1499
|
+
* @example
|
|
1500
|
+
* ```ts
|
|
1501
|
+
* const doc = cache.peekRequest(requestKey);
|
|
1502
|
+
* ```
|
|
1379
1503
|
*
|
|
1380
1504
|
* @category Cache Management
|
|
1381
1505
|
* @public
|
|
@@ -1386,9 +1510,20 @@ var JSONAPICache = class {
|
|
|
1386
1510
|
/**
|
|
1387
1511
|
* Push resource data from a remote source into the cache for this identifier
|
|
1388
1512
|
*
|
|
1513
|
+
* @example
|
|
1514
|
+
* ```ts
|
|
1515
|
+
* cache.upsert(identifier, {
|
|
1516
|
+
* type: 'user',
|
|
1517
|
+
* id: '1',
|
|
1518
|
+
* attributes: { name: 'Chris' },
|
|
1519
|
+
* });
|
|
1520
|
+
* ```
|
|
1521
|
+
*
|
|
1389
1522
|
* @category Cache Management
|
|
1390
1523
|
* @public
|
|
1391
|
-
* @return
|
|
1524
|
+
* @return when `calculateChanges` is true, the names of the attributes whose persisted value
|
|
1525
|
+
* this push changed (the same keys the `'remote'` channel is notified with), or `undefined`
|
|
1526
|
+
* when none did. Otherwise `void`.
|
|
1392
1527
|
*/
|
|
1393
1528
|
upsert(identifier, data, calculateChanges) {
|
|
1394
1529
|
assertPrivateCapabilities(this._capabilities);
|
|
@@ -1434,7 +1569,7 @@ var JSONAPICache = class {
|
|
|
1434
1569
|
*
|
|
1435
1570
|
* Each individual resource or document that has
|
|
1436
1571
|
* been mutated should be described as an individual
|
|
1437
|
-
*
|
|
1572
|
+
* {@link Change} entry in the returned array.
|
|
1438
1573
|
*
|
|
1439
1574
|
* A `Change` is described by an object containing up to
|
|
1440
1575
|
* three properties: (1) the `identifier` of the entity that
|
|
@@ -1499,6 +1634,11 @@ var JSONAPICache = class {
|
|
|
1499
1634
|
* It returns properties from options that should be set on the record during the create
|
|
1500
1635
|
* process. This return value behavior is deprecated.
|
|
1501
1636
|
*
|
|
1637
|
+
* @example
|
|
1638
|
+
* ```ts
|
|
1639
|
+
* cache.clientDidCreate(identifier, { name: 'Chris' });
|
|
1640
|
+
* ```
|
|
1641
|
+
*
|
|
1502
1642
|
* @category Resource Lifecycle
|
|
1503
1643
|
* @public
|
|
1504
1644
|
*/
|
|
@@ -1561,6 +1701,11 @@ var JSONAPICache = class {
|
|
|
1561
1701
|
* [LIFECYCLE] Signals to the cache that a resource
|
|
1562
1702
|
* will be part of a save transaction.
|
|
1563
1703
|
*
|
|
1704
|
+
* @example
|
|
1705
|
+
* ```ts
|
|
1706
|
+
* cache.willCommit(identifier, context);
|
|
1707
|
+
* ```
|
|
1708
|
+
*
|
|
1564
1709
|
* @category Resource Lifecycle
|
|
1565
1710
|
* @public
|
|
1566
1711
|
*/
|
|
@@ -1572,6 +1717,11 @@ var JSONAPICache = class {
|
|
|
1572
1717
|
* [LIFECYCLE] Signals to the cache that a resource
|
|
1573
1718
|
* was successfully updated as part of a save transaction.
|
|
1574
1719
|
*
|
|
1720
|
+
* @example
|
|
1721
|
+
* ```ts
|
|
1722
|
+
* cache.didCommit(identifier, result);
|
|
1723
|
+
* ```
|
|
1724
|
+
*
|
|
1575
1725
|
* @category Resource Lifecycle
|
|
1576
1726
|
* @public
|
|
1577
1727
|
*/
|
|
@@ -1609,6 +1759,11 @@ var JSONAPICache = class {
|
|
|
1609
1759
|
* [LIFECYCLE] Signals to the cache that a resource
|
|
1610
1760
|
* was update via a save transaction failed.
|
|
1611
1761
|
*
|
|
1762
|
+
* @example
|
|
1763
|
+
* ```ts
|
|
1764
|
+
* cache.commitWasRejected(identifier, errors);
|
|
1765
|
+
* ```
|
|
1766
|
+
*
|
|
1612
1767
|
* @category Resource Lifecycle
|
|
1613
1768
|
* @public
|
|
1614
1769
|
*/
|
|
@@ -1625,6 +1780,11 @@ var JSONAPICache = class {
|
|
|
1625
1780
|
*
|
|
1626
1781
|
* This method is a candidate to become a mutation
|
|
1627
1782
|
*
|
|
1783
|
+
* @example
|
|
1784
|
+
* ```ts
|
|
1785
|
+
* cache.unloadRecord(identifier);
|
|
1786
|
+
* ```
|
|
1787
|
+
*
|
|
1628
1788
|
* @category Resource Lifecycle
|
|
1629
1789
|
* @public
|
|
1630
1790
|
*/
|
|
@@ -1665,6 +1825,12 @@ var JSONAPICache = class {
|
|
|
1665
1825
|
* Retrieve the data for an attribute from the cache
|
|
1666
1826
|
* with local mutations applied.
|
|
1667
1827
|
*
|
|
1828
|
+
* @example
|
|
1829
|
+
* ```ts
|
|
1830
|
+
* const name = cache.getAttr(identifier, 'name');
|
|
1831
|
+
* const zip = cache.getAttr(identifier, ['address', 'zip']);
|
|
1832
|
+
* ```
|
|
1833
|
+
*
|
|
1668
1834
|
* @category Resource Data
|
|
1669
1835
|
* @public
|
|
1670
1836
|
*/
|
|
@@ -1678,37 +1844,28 @@ var JSONAPICache = class {
|
|
|
1678
1844
|
if (!test) throw new Error(`Cannot retrieve attributes for identifier ${String(identifier)} as it is not present in the cache`);
|
|
1679
1845
|
})(cached);
|
|
1680
1846
|
if (!cached) return;
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
if (schemaHasLegacyDefaultValueFn(attrSchema)) {
|
|
1690
|
-
cached.defaultAttrs = cached.defaultAttrs || Object.create(null);
|
|
1691
|
-
cached.defaultAttrs[attribute] = defaultValue;
|
|
1692
|
-
}
|
|
1693
|
-
return defaultValue;
|
|
1847
|
+
const layer = layerHolding(attribute, cached, RESOLUTION_ORDER_LOCAL_STATE);
|
|
1848
|
+
if (layer) return layer[attribute];
|
|
1849
|
+
const attrSchema = getCacheFields(this, identifier).get(attribute);
|
|
1850
|
+
assertPrivateCapabilities(this._capabilities);
|
|
1851
|
+
const defaultValue = getDefaultValue(attrSchema, identifier, this._capabilities._store);
|
|
1852
|
+
if (schemaHasLegacyDefaultValueFn(attrSchema)) {
|
|
1853
|
+
cached.defaultAttrs = cached.defaultAttrs || Object.create(null);
|
|
1854
|
+
cached.defaultAttrs[attribute] = defaultValue;
|
|
1694
1855
|
}
|
|
1856
|
+
return defaultValue;
|
|
1695
1857
|
}
|
|
1696
|
-
const path = attr;
|
|
1697
1858
|
const cached = this.__peek(identifier, true);
|
|
1698
|
-
|
|
1699
|
-
let current = cached.localAttrs && basePath in cached.localAttrs ? cached.localAttrs[basePath] : void 0;
|
|
1700
|
-
if (current === void 0) current = cached.inflightAttrs && basePath in cached.inflightAttrs ? cached.inflightAttrs[basePath] : void 0;
|
|
1701
|
-
if (current === void 0) current = cached.remoteAttrs && basePath in cached.remoteAttrs ? cached.remoteAttrs[basePath] : void 0;
|
|
1702
|
-
if (current === void 0) return;
|
|
1703
|
-
for (let i = 1; i < path.length; i++) {
|
|
1704
|
-
current = current[path[i]];
|
|
1705
|
-
if (current === void 0) return;
|
|
1706
|
-
}
|
|
1707
|
-
return current;
|
|
1859
|
+
return resolveAttr(attr, cached, RESOLUTION_ORDER_LOCAL_STATE);
|
|
1708
1860
|
}
|
|
1709
1861
|
/**
|
|
1710
1862
|
* Retrieve the remote data for an attribute from the cache
|
|
1711
1863
|
*
|
|
1864
|
+
* @example
|
|
1865
|
+
* ```ts
|
|
1866
|
+
* const name = cache.getRemoteAttr(identifier, 'name');
|
|
1867
|
+
* ```
|
|
1868
|
+
*
|
|
1712
1869
|
* @category Resource Data
|
|
1713
1870
|
* @public
|
|
1714
1871
|
*/
|
|
@@ -1722,35 +1879,30 @@ var JSONAPICache = class {
|
|
|
1722
1879
|
if (!test) throw new Error(`Cannot retrieve remote attributes for identifier ${String(identifier)} as it is not present in the cache`);
|
|
1723
1880
|
})(cached);
|
|
1724
1881
|
if (!cached) return;
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
cached.defaultAttrs[attribute] = defaultValue;
|
|
1734
|
-
}
|
|
1735
|
-
return defaultValue;
|
|
1882
|
+
const layer = layerHolding(attribute, cached, RESOLUTION_ORDER_REMOTE_STATE);
|
|
1883
|
+
if (layer) return layer[attribute];
|
|
1884
|
+
const attrSchema = getCacheFields(this, identifier).get(attribute);
|
|
1885
|
+
assertPrivateCapabilities(this._capabilities);
|
|
1886
|
+
const defaultValue = getDefaultValue(attrSchema, identifier, this._capabilities._store);
|
|
1887
|
+
if (schemaHasLegacyDefaultValueFn(attrSchema)) {
|
|
1888
|
+
cached.defaultAttrs = cached.defaultAttrs || Object.create(null);
|
|
1889
|
+
cached.defaultAttrs[attribute] = defaultValue;
|
|
1736
1890
|
}
|
|
1891
|
+
return defaultValue;
|
|
1737
1892
|
}
|
|
1738
|
-
const path = attr;
|
|
1739
1893
|
const cached = this.__peek(identifier, true);
|
|
1740
|
-
|
|
1741
|
-
let current = cached.remoteAttrs && basePath in cached.remoteAttrs ? cached.remoteAttrs[basePath] : void 0;
|
|
1742
|
-
if (current === void 0) return;
|
|
1743
|
-
for (let i = 1; i < path.length; i++) {
|
|
1744
|
-
current = current[path[i]];
|
|
1745
|
-
if (current === void 0) return;
|
|
1746
|
-
}
|
|
1747
|
-
return current;
|
|
1894
|
+
return resolveAttr(attr, cached, RESOLUTION_ORDER_REMOTE_STATE);
|
|
1748
1895
|
}
|
|
1749
1896
|
/**
|
|
1750
1897
|
* Mutate the data for an attribute in the cache
|
|
1751
1898
|
*
|
|
1752
1899
|
* This method is a candidate to become a mutation
|
|
1753
1900
|
*
|
|
1901
|
+
* @example
|
|
1902
|
+
* ```ts
|
|
1903
|
+
* cache.setAttr(identifier, 'name', 'Chris');
|
|
1904
|
+
* ```
|
|
1905
|
+
*
|
|
1754
1906
|
* @category Resource Data
|
|
1755
1907
|
* @public
|
|
1756
1908
|
*/
|
|
@@ -1758,21 +1910,19 @@ var JSONAPICache = class {
|
|
|
1758
1910
|
((test) => {
|
|
1759
1911
|
if (!test) throw new Error("setAttr must receive at least one attribute path");
|
|
1760
1912
|
})(attr.length > 0);
|
|
1913
|
+
((test) => {
|
|
1914
|
+
if (!test) throw new Error(`Cannot set '${Array.isArray(attr) ? attr.join(".") : attr}' on '${identifier.type}' to undefined: undefined is not a JSON value. Use null instead. If you feel like this behavior is incorrect, open an issue and ping @runspired.`);
|
|
1915
|
+
})(value !== void 0);
|
|
1916
|
+
if (value === void 0) value = null;
|
|
1761
1917
|
const isSimplePath = !Array.isArray(attr) || attr.length === 1;
|
|
1762
1918
|
if (Array.isArray(attr) && attr.length === 1) attr = attr[0];
|
|
1763
1919
|
if (isSimplePath) {
|
|
1764
1920
|
const cached = this.__peek(identifier, false);
|
|
1765
1921
|
const currentAttr = attr;
|
|
1766
|
-
|
|
1767
|
-
if (existing !== value) {
|
|
1922
|
+
if (resolveAttr(currentAttr, cached, RESOLUTION_ORDER_EDIT_BASELINE) !== value) {
|
|
1768
1923
|
cached.localAttrs = cached.localAttrs || Object.create(null);
|
|
1769
1924
|
cached.localAttrs[currentAttr] = value;
|
|
1770
|
-
|
|
1771
|
-
cached.changes[currentAttr] = [existing, value];
|
|
1772
|
-
} else if (cached.localAttrs) {
|
|
1773
|
-
delete cached.localAttrs[currentAttr];
|
|
1774
|
-
delete cached.changes[currentAttr];
|
|
1775
|
-
}
|
|
1925
|
+
} else if (cached.localAttrs) delete cached.localAttrs[currentAttr];
|
|
1776
1926
|
if (cached.defaultAttrs && currentAttr in cached.defaultAttrs) delete cached.defaultAttrs[currentAttr];
|
|
1777
1927
|
this._capabilities.notifyChange(identifier, "attributes", currentAttr, "local");
|
|
1778
1928
|
return;
|
|
@@ -1780,32 +1930,50 @@ var JSONAPICache = class {
|
|
|
1780
1930
|
const path = attr;
|
|
1781
1931
|
const cached = this.__peek(identifier, false);
|
|
1782
1932
|
const basePath = path[0];
|
|
1783
|
-
const
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1933
|
+
const baseline = resolveAttr(basePath, cached, RESOLUTION_ORDER_EDIT_BASELINE);
|
|
1934
|
+
const isRevert = valueAtPath(baseline, path) === value;
|
|
1935
|
+
const hasLocalClone = !!cached.localAttrs && basePath in cached.localAttrs;
|
|
1936
|
+
if (isRevert && !hasLocalClone) return;
|
|
1937
|
+
cached.localAttrs = cached.localAttrs || Object.create(null);
|
|
1938
|
+
if (!hasLocalClone) {
|
|
1939
|
+
const seed = baseline ?? (cached.defaultAttrs ? cached.defaultAttrs[basePath] : void 0);
|
|
1940
|
+
((test) => {
|
|
1941
|
+
if (!test) throw new Error(`Cannot set '${path.join(".")}' on '${identifier.type}': '${basePath}' holds no object to write into`);
|
|
1942
|
+
})(!!seed && typeof seed === "object");
|
|
1943
|
+
cached.localAttrs[basePath] = structuredClone(seed);
|
|
1788
1944
|
}
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
if (!existing) return;
|
|
1800
|
-
if (JSON.stringify(existing) !== JSON.stringify(cached.localAttrs[basePath])) {
|
|
1801
|
-
delete cached.localAttrs[basePath];
|
|
1802
|
-
delete cached.changes[basePath];
|
|
1803
|
-
}
|
|
1804
|
-
} catch {}
|
|
1945
|
+
let currentLocal = cached.localAttrs[basePath];
|
|
1946
|
+
let nextLink = 1;
|
|
1947
|
+
while (nextLink < path.length - 1) currentLocal = currentLocal[path[nextLink++]];
|
|
1948
|
+
if (currentLocal[path[nextLink]] === value) return;
|
|
1949
|
+
currentLocal[path[nextLink]] = value;
|
|
1950
|
+
if (isRevert) {
|
|
1951
|
+
const field = getCacheFields(this, identifier).get(basePath);
|
|
1952
|
+
if (localCloneMatchesBaseline(this._capabilities.schema, field, baseline, cached.localAttrs[basePath])) delete cached.localAttrs[basePath];
|
|
1953
|
+
}
|
|
1954
|
+
if (cached.defaultAttrs && basePath in cached.defaultAttrs) delete cached.defaultAttrs[basePath];
|
|
1805
1955
|
this._capabilities.notifyChange(identifier, "attributes", basePath, "local");
|
|
1806
1956
|
}
|
|
1807
1957
|
/**
|
|
1808
|
-
* Query the cache for the changed attributes of a resource
|
|
1958
|
+
* Query the cache for the changed attributes of a resource: every unsaved
|
|
1959
|
+
* mutation, as a `[before, after]` pair per field.
|
|
1960
|
+
*
|
|
1961
|
+
* `before` is the value the mutation replaces, which is not always the
|
|
1962
|
+
* persisted one. A mutation a save is carrying replaces remote state; an edit
|
|
1963
|
+
* made while that save is in flight replaces the in-flight value. So this is
|
|
1964
|
+
* what saving from here would change, which is what `serializePatch` and the
|
|
1965
|
+
* legacy `Snapshot` consume, rather than a diff against persisted state.
|
|
1966
|
+
*
|
|
1967
|
+
* Derived from the layers on each call, so it is always consistent with
|
|
1968
|
+
* {@link JSONAPICache.getAttr | getAttr} and
|
|
1969
|
+
* {@link JSONAPICache.rollbackAttrs | rollbackAttrs}. Dirtiness does not go
|
|
1970
|
+
* through here; see {@link JSONAPICache.hasChangedAttrs | hasChangedAttrs}.
|
|
1971
|
+
*
|
|
1972
|
+
* @example
|
|
1973
|
+
* ```ts
|
|
1974
|
+
* const changes = cache.changedAttrs(identifier);
|
|
1975
|
+
* // { name: ['Igor', 'Chris'] }
|
|
1976
|
+
* ```
|
|
1809
1977
|
*
|
|
1810
1978
|
* @category Resource Data
|
|
1811
1979
|
* @public
|
|
@@ -1816,12 +1984,29 @@ var JSONAPICache = class {
|
|
|
1816
1984
|
((test) => {
|
|
1817
1985
|
if (!test) throw new Error(`Cannot retrieve changed attributes for identifier ${String(identifier)} as it is not present in the cache`);
|
|
1818
1986
|
})(cached);
|
|
1819
|
-
|
|
1820
|
-
|
|
1987
|
+
const changes = Object.create(null);
|
|
1988
|
+
if (!cached) return changes;
|
|
1989
|
+
const { localAttrs, inflightAttrs, remoteAttrs } = cached;
|
|
1990
|
+
if (inflightAttrs) {
|
|
1991
|
+
const keys = Object.keys(inflightAttrs);
|
|
1992
|
+
for (let i = 0; i < keys.length; i++) changes[keys[i]] = [remoteAttrs ? remoteAttrs[keys[i]] : void 0, inflightAttrs[keys[i]]];
|
|
1993
|
+
}
|
|
1994
|
+
if (localAttrs) {
|
|
1995
|
+
const keys = Object.keys(localAttrs);
|
|
1996
|
+
for (let i = 0; i < keys.length; i++) changes[keys[i]] = [resolveAttr(keys[i], cached, RESOLUTION_ORDER_EDIT_BASELINE), localAttrs[keys[i]]];
|
|
1997
|
+
}
|
|
1998
|
+
return changes;
|
|
1821
1999
|
}
|
|
1822
2000
|
/**
|
|
1823
2001
|
* Query the cache for whether any mutated attributes exist
|
|
1824
2002
|
*
|
|
2003
|
+
* @example
|
|
2004
|
+
* ```ts
|
|
2005
|
+
* if (cache.hasChangedAttrs(identifier)) {
|
|
2006
|
+
* // ...
|
|
2007
|
+
* }
|
|
2008
|
+
* ```
|
|
2009
|
+
*
|
|
1825
2010
|
* @category Resource Data
|
|
1826
2011
|
* @public
|
|
1827
2012
|
*/
|
|
@@ -1838,6 +2023,11 @@ var JSONAPICache = class {
|
|
|
1838
2023
|
*
|
|
1839
2024
|
* This method is a candidate to become a mutation
|
|
1840
2025
|
*
|
|
2026
|
+
* @example
|
|
2027
|
+
* ```ts
|
|
2028
|
+
* const restoredKeys = cache.rollbackAttrs(identifier);
|
|
2029
|
+
* ```
|
|
2030
|
+
*
|
|
1841
2031
|
* @category Resource Data
|
|
1842
2032
|
* @public
|
|
1843
2033
|
* @return the names of fields that were restored
|
|
@@ -1849,7 +2039,6 @@ var JSONAPICache = class {
|
|
|
1849
2039
|
if (cached.localAttrs !== null) {
|
|
1850
2040
|
dirtyKeys = Object.keys(cached.localAttrs);
|
|
1851
2041
|
cached.localAttrs = null;
|
|
1852
|
-
cached.changes = null;
|
|
1853
2042
|
}
|
|
1854
2043
|
if (cached.isNew) {
|
|
1855
2044
|
cached.isDeletionCommitted = true;
|
|
@@ -1869,7 +2058,7 @@ var JSONAPICache = class {
|
|
|
1869
2058
|
/**
|
|
1870
2059
|
* Query the cache for the changes to relationships of a resource.
|
|
1871
2060
|
*
|
|
1872
|
-
* Returns a map of relationship names to RelationshipDiff objects.
|
|
2061
|
+
* Returns a map of relationship names to {@link RelationshipDiff} objects.
|
|
1873
2062
|
*
|
|
1874
2063
|
* ```ts
|
|
1875
2064
|
* type RelationshipDiff =
|
|
@@ -1888,6 +2077,12 @@ var JSONAPICache = class {
|
|
|
1888
2077
|
};
|
|
1889
2078
|
```
|
|
1890
2079
|
*
|
|
2080
|
+
* @example
|
|
2081
|
+
* ```ts
|
|
2082
|
+
* const diffs = cache.changedRelationships(identifier);
|
|
2083
|
+
* const comments = diffs.get('comments');
|
|
2084
|
+
* ```
|
|
2085
|
+
*
|
|
1891
2086
|
* @category Resource Data
|
|
1892
2087
|
* @public
|
|
1893
2088
|
*/
|
|
@@ -1897,6 +2092,13 @@ var JSONAPICache = class {
|
|
|
1897
2092
|
/**
|
|
1898
2093
|
* Query the cache for whether any mutated relationships exist
|
|
1899
2094
|
*
|
|
2095
|
+
* @example
|
|
2096
|
+
* ```ts
|
|
2097
|
+
* if (cache.hasChangedRelationships(identifier)) {
|
|
2098
|
+
* // ...
|
|
2099
|
+
* }
|
|
2100
|
+
* ```
|
|
2101
|
+
*
|
|
1900
2102
|
* @category Resource Data
|
|
1901
2103
|
* @public
|
|
1902
2104
|
*/
|
|
@@ -1910,6 +2112,11 @@ var JSONAPICache = class {
|
|
|
1910
2112
|
*
|
|
1911
2113
|
* This method is a candidate to become a mutation
|
|
1912
2114
|
*
|
|
2115
|
+
* @example
|
|
2116
|
+
* ```ts
|
|
2117
|
+
* const restoredFields = cache.rollbackRelationships(identifier);
|
|
2118
|
+
* ```
|
|
2119
|
+
*
|
|
1913
2120
|
* @category Resource Data
|
|
1914
2121
|
* @public
|
|
1915
2122
|
* @return the names of relationships that were restored
|
|
@@ -1925,6 +2132,11 @@ var JSONAPICache = class {
|
|
|
1925
2132
|
/**
|
|
1926
2133
|
* Query the cache for the current state of a relationship property
|
|
1927
2134
|
*
|
|
2135
|
+
* @example
|
|
2136
|
+
* ```ts
|
|
2137
|
+
* const relationship = cache.getRelationship(identifier, 'comments');
|
|
2138
|
+
* ```
|
|
2139
|
+
*
|
|
1928
2140
|
* @category Resource Data
|
|
1929
2141
|
* @public
|
|
1930
2142
|
* @return resource relationship object
|
|
@@ -1935,6 +2147,11 @@ var JSONAPICache = class {
|
|
|
1935
2147
|
/**
|
|
1936
2148
|
* Query the cache for the remote state of a relationship property
|
|
1937
2149
|
*
|
|
2150
|
+
* @example
|
|
2151
|
+
* ```ts
|
|
2152
|
+
* const relationship = cache.getRemoteRelationship(identifier, 'comments');
|
|
2153
|
+
* ```
|
|
2154
|
+
*
|
|
1938
2155
|
* @category Resource Data
|
|
1939
2156
|
* @public
|
|
1940
2157
|
* @return resource relationship object
|
|
@@ -1948,6 +2165,11 @@ var JSONAPICache = class {
|
|
|
1948
2165
|
*
|
|
1949
2166
|
* This method is a candidate to become a mutation
|
|
1950
2167
|
*
|
|
2168
|
+
* @example
|
|
2169
|
+
* ```ts
|
|
2170
|
+
* cache.setIsDeleted(identifier, true);
|
|
2171
|
+
* ```
|
|
2172
|
+
*
|
|
1951
2173
|
* @category Resource State
|
|
1952
2174
|
* @public
|
|
1953
2175
|
*/
|
|
@@ -1959,6 +2181,11 @@ var JSONAPICache = class {
|
|
|
1959
2181
|
/**
|
|
1960
2182
|
* Query the cache for any validation errors applicable to the given resource.
|
|
1961
2183
|
*
|
|
2184
|
+
* @example
|
|
2185
|
+
* ```ts
|
|
2186
|
+
* const errors = cache.getErrors(identifier);
|
|
2187
|
+
* ```
|
|
2188
|
+
*
|
|
1962
2189
|
* @category Resource State
|
|
1963
2190
|
* @public
|
|
1964
2191
|
*/
|
|
@@ -1968,6 +2195,13 @@ var JSONAPICache = class {
|
|
|
1968
2195
|
/**
|
|
1969
2196
|
* Query the cache for whether a given resource has any available data
|
|
1970
2197
|
*
|
|
2198
|
+
* @example
|
|
2199
|
+
* ```ts
|
|
2200
|
+
* if (cache.isEmpty(identifier)) {
|
|
2201
|
+
* // ...
|
|
2202
|
+
* }
|
|
2203
|
+
* ```
|
|
2204
|
+
*
|
|
1971
2205
|
* @category Resource State
|
|
1972
2206
|
* @public
|
|
1973
2207
|
*/
|
|
@@ -1979,6 +2213,13 @@ var JSONAPICache = class {
|
|
|
1979
2213
|
* Query the cache for whether a given resource was created locally and not
|
|
1980
2214
|
* yet persisted.
|
|
1981
2215
|
*
|
|
2216
|
+
* @example
|
|
2217
|
+
* ```ts
|
|
2218
|
+
* if (cache.isNew(identifier)) {
|
|
2219
|
+
* // ...
|
|
2220
|
+
* }
|
|
2221
|
+
* ```
|
|
2222
|
+
*
|
|
1982
2223
|
* @category Resource State
|
|
1983
2224
|
* @public
|
|
1984
2225
|
*/
|
|
@@ -1989,6 +2230,13 @@ var JSONAPICache = class {
|
|
|
1989
2230
|
* Query the cache for whether a given resource is marked as deleted (but not
|
|
1990
2231
|
* necessarily persisted yet).
|
|
1991
2232
|
*
|
|
2233
|
+
* @example
|
|
2234
|
+
* ```ts
|
|
2235
|
+
* if (cache.isDeleted(identifier)) {
|
|
2236
|
+
* // ...
|
|
2237
|
+
* }
|
|
2238
|
+
* ```
|
|
2239
|
+
*
|
|
1992
2240
|
* @category Resource State
|
|
1993
2241
|
* @public
|
|
1994
2242
|
*/
|
|
@@ -1999,6 +2247,13 @@ var JSONAPICache = class {
|
|
|
1999
2247
|
* Query the cache for whether a given resource has been deleted and that deletion
|
|
2000
2248
|
* has also been persisted.
|
|
2001
2249
|
*
|
|
2250
|
+
* @example
|
|
2251
|
+
* ```ts
|
|
2252
|
+
* if (cache.isDeletionCommitted(identifier)) {
|
|
2253
|
+
* // ...
|
|
2254
|
+
* }
|
|
2255
|
+
* ```
|
|
2256
|
+
*
|
|
2002
2257
|
* @category Resource State
|
|
2003
2258
|
* @public
|
|
2004
2259
|
*/
|
|
@@ -2216,20 +2471,189 @@ function getDefaultValue(schema, identifier, store) {
|
|
|
2216
2471
|
if (transform?.defaultValue) return transform.defaultValue(options || null, identifier);
|
|
2217
2472
|
}
|
|
2218
2473
|
}
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
if (!fields.has(key)) continue;
|
|
2228
|
-
const value = updates[key];
|
|
2229
|
-
if (localAttrs && localAttrs[key] !== void 0) continue;
|
|
2230
|
-
if (original[key] !== value) changedKeys.add(key);
|
|
2474
|
+
/**
|
|
2475
|
+
* The first layer, in the given resolution order, that holds `key`. Presence is `in`, so a value
|
|
2476
|
+
* explicitly set to `undefined` counts as present.
|
|
2477
|
+
*/
|
|
2478
|
+
function layerHolding(key, layers, order) {
|
|
2479
|
+
for (let i = 0; i < order.length; i++) {
|
|
2480
|
+
const layer = layers[order[i]];
|
|
2481
|
+
if (layer && key in layer) return layer;
|
|
2231
2482
|
}
|
|
2232
|
-
return
|
|
2483
|
+
return null;
|
|
2484
|
+
}
|
|
2485
|
+
/**
|
|
2486
|
+
* What the projection with the given resolution order reads for `attr`. A path resolves its first
|
|
2487
|
+
* segment through the layers and follows the rest into the value, stopping with `undefined` at the
|
|
2488
|
+
* first missing link.
|
|
2489
|
+
*/
|
|
2490
|
+
function resolveAttr(attr, layers, order) {
|
|
2491
|
+
const key = typeof attr === "string" ? attr : attr[0];
|
|
2492
|
+
const layer = layerHolding(key, layers, order);
|
|
2493
|
+
if (!layer) return void 0;
|
|
2494
|
+
return typeof attr === "string" ? layer[key] : valueAtPath(layer[key], attr);
|
|
2495
|
+
}
|
|
2496
|
+
/** Follow `path` (from its second segment) into `base`, stopping with `undefined` at the first missing link or `null`. */
|
|
2497
|
+
function valueAtPath(base, path) {
|
|
2498
|
+
let current = base;
|
|
2499
|
+
for (let i = 1; i < path.length; i++) {
|
|
2500
|
+
if (current === void 0 || current === null) return void 0;
|
|
2501
|
+
current = current[path[i]];
|
|
2502
|
+
}
|
|
2503
|
+
return current;
|
|
2504
|
+
}
|
|
2505
|
+
/**
|
|
2506
|
+
* Whether two values of `field` count as the same value for change detection.
|
|
2507
|
+
*
|
|
2508
|
+
* A `schema-object`, and each element of a `schema-array`, compares by the identity hash its
|
|
2509
|
+
* `ObjectSchema` declares (`identity: { kind: '@hash', ... }`), so the schema decides what "same"
|
|
2510
|
+
* means for it. With no hash declared, and for every other field kind, only the same reference
|
|
2511
|
+
* counts. That knowingly over-notifies for equal-content objects: content equality is the schema's
|
|
2512
|
+
* to define, not the cache's to guess.
|
|
2513
|
+
*/
|
|
2514
|
+
function attrValuesEqual(schema, field, a, b) {
|
|
2515
|
+
if (a === b) return true;
|
|
2516
|
+
if (field.kind === "schema-object") return schemaObjectsEqual(schema, field, a, b);
|
|
2517
|
+
if (field.kind === "schema-array") return schemaArraysEqual(schema, field, a, b);
|
|
2518
|
+
return false;
|
|
2519
|
+
}
|
|
2520
|
+
/**
|
|
2521
|
+
* A schema-array has no hash of its own: two arrays are the same when they have the same length
|
|
2522
|
+
* and every element compares equal as a schema-object of the element type.
|
|
2523
|
+
*/
|
|
2524
|
+
function schemaArraysEqual(schema, field, a, b) {
|
|
2525
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
|
|
2526
|
+
for (let i = 0; i < a.length; i++) if (!schemaObjectsEqual(schema, field, a[i], b[i])) return false;
|
|
2527
|
+
return true;
|
|
2528
|
+
}
|
|
2529
|
+
function schemaObjectsEqual(schema, field, a, b) {
|
|
2530
|
+
if (a === b) return true;
|
|
2531
|
+
if (!a || !b || typeof a !== "object" || typeof b !== "object") return false;
|
|
2532
|
+
const ia = fieldValueIdentity(schema, field, a);
|
|
2533
|
+
const ib = fieldValueIdentity(schema, field, b);
|
|
2534
|
+
return ia !== null && ib !== null && ia.type === ib.type && ia.hash !== null && ia.hash === ib.hash;
|
|
2535
|
+
}
|
|
2536
|
+
/**
|
|
2537
|
+
* Whether a nested edit's local clone is back to the baseline, so the edit can end. The schema
|
|
2538
|
+
* decides first, through {@link attrValuesEqual}: a schema-object with an identity hash matches
|
|
2539
|
+
* when its hash does. A value with no schema-defined equality falls back to comparing serialized
|
|
2540
|
+
* content, as this path always has: the clone began as a copy of the baseline, so key order
|
|
2541
|
+
* agrees while edits only replace existing keys. Anything that cannot serialize counts as still
|
|
2542
|
+
* edited, so no edit the cache cannot understand is thrown away.
|
|
2543
|
+
*/
|
|
2544
|
+
function localCloneMatchesBaseline(schema, field, baseline, clone) {
|
|
2545
|
+
if (field && attrValuesEqual(schema, field, baseline, clone)) return true;
|
|
2546
|
+
if (!baseline || !clone) return false;
|
|
2547
|
+
try {
|
|
2548
|
+
return JSON.stringify(baseline) === JSON.stringify(clone);
|
|
2549
|
+
} catch {
|
|
2550
|
+
return false;
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
/**
|
|
2554
|
+
* Which projection each changed key moved in, so the caller can notify on the matching channel:
|
|
2555
|
+
* `localOnly` and `remoteOnly` on theirs, `both` unscoped.
|
|
2556
|
+
*
|
|
2557
|
+
* `undefined` rather than empty Sets: this runs on every `upsert`, and a resource where nothing
|
|
2558
|
+
* moved should cost no allocation.
|
|
2559
|
+
*/
|
|
2560
|
+
const NO_PROJECTION_CHANGES = Object.freeze({
|
|
2561
|
+
localOnly: void 0,
|
|
2562
|
+
remoteOnly: void 0,
|
|
2563
|
+
both: void 0
|
|
2564
|
+
});
|
|
2565
|
+
/** The layers a merge reads: the resource's own, plus the attributes arriving from the server. */
|
|
2566
|
+
function layersForMerge(cached, incomingAttrs) {
|
|
2567
|
+
return {
|
|
2568
|
+
localAttrs: cached.localAttrs,
|
|
2569
|
+
inflightAttrs: cached.inflightAttrs,
|
|
2570
|
+
remoteAttrs: cached.remoteAttrs,
|
|
2571
|
+
defaultAttrs: cached.defaultAttrs,
|
|
2572
|
+
incomingAttrs
|
|
2573
|
+
};
|
|
2574
|
+
}
|
|
2575
|
+
/**
|
|
2576
|
+
* Every key a merge could move: every key in every layer the after-merge order folds into
|
|
2577
|
+
* `remoteAttrs`. `null` when there is nothing to examine.
|
|
2578
|
+
*/
|
|
2579
|
+
function candidateKeys(layers, remoteAfterOrder) {
|
|
2580
|
+
let keys = null;
|
|
2581
|
+
for (let i = 0; i < remoteAfterOrder.length; i++) {
|
|
2582
|
+
const layer = remoteAfterOrder[i];
|
|
2583
|
+
if (layer === "remoteAttrs") break;
|
|
2584
|
+
const hash = layers[layer];
|
|
2585
|
+
if (!hash) continue;
|
|
2586
|
+
const layerKeys = Object.keys(hash);
|
|
2587
|
+
if (!layerKeys.length) continue;
|
|
2588
|
+
if (keys === null) {
|
|
2589
|
+
keys = layerKeys;
|
|
2590
|
+
continue;
|
|
2591
|
+
}
|
|
2592
|
+
for (let j = 0; j < layerKeys.length; j++) if (!keys.includes(layerKeys[j])) keys.push(layerKeys[j]);
|
|
2593
|
+
}
|
|
2594
|
+
return keys;
|
|
2595
|
+
}
|
|
2596
|
+
/**
|
|
2597
|
+
* Fold every layer above `remoteAttrs` in the after-merge order for `kind` into `remoteAttrs`,
|
|
2598
|
+
* lowest precedence first, and clear each resource layer that was folded in. The same order
|
|
2599
|
+
* {@link partitionChangedKeys} used to predict the result, so the two cannot disagree.
|
|
2600
|
+
*
|
|
2601
|
+
* Reads the resource layers off `cached` rather than taking a {@link Layered}, so an upsert that
|
|
2602
|
+
* is not calculating changes allocates nothing beyond the merge itself.
|
|
2603
|
+
*/
|
|
2604
|
+
function mergeIntoRemote(cached, incomingAttrs, kind) {
|
|
2605
|
+
const folded = MERGE_RESOLUTION[kind].folded;
|
|
2606
|
+
const target = cached.remoteAttrs || Object.create(null);
|
|
2607
|
+
for (let i = 0; i < folded.length; i++) {
|
|
2608
|
+
const layer = folded[i];
|
|
2609
|
+
const hash = layer === "incomingAttrs" ? incomingAttrs : cached[layer];
|
|
2610
|
+
if (!hash) continue;
|
|
2611
|
+
Object.assign(target, hash);
|
|
2612
|
+
if (layer !== "incomingAttrs") cached[layer] = null;
|
|
2613
|
+
if (cached.defaultAttrs) dropMemoizedDefaults(cached.defaultAttrs, hash);
|
|
2614
|
+
}
|
|
2615
|
+
cached.remoteAttrs = target;
|
|
2616
|
+
}
|
|
2617
|
+
function dropMemoizedDefaults(defaultAttrs, replacedBy) {
|
|
2618
|
+
const keys = Object.keys(replacedBy);
|
|
2619
|
+
for (let i = 0; i < keys.length; i++) if (keys[i] in defaultAttrs) delete defaultAttrs[keys[i]];
|
|
2620
|
+
}
|
|
2621
|
+
/**
|
|
2622
|
+
* Partition the keys a merge touches by which projection each one moves in. Must run *before*
|
|
2623
|
+
* {@link mergeIntoRemote}: it reads the pre-merge layers from `layers` and predicts the merge with
|
|
2624
|
+
* the same `RESOLUTION_ORDER_*_AFTER_*` orders `mergeIntoRemote` applies.
|
|
2625
|
+
*/
|
|
2626
|
+
function partitionChangedKeys(schema, layers, fields, kind) {
|
|
2627
|
+
const { remoteAfter: remoteAfterOrder, localAfter: localAfterOrder } = MERGE_RESOLUTION[kind];
|
|
2628
|
+
const keys = candidateKeys(layers, remoteAfterOrder);
|
|
2629
|
+
if (keys === null) return NO_PROJECTION_CHANGES;
|
|
2630
|
+
let localOnly;
|
|
2631
|
+
let remoteOnly;
|
|
2632
|
+
let both;
|
|
2633
|
+
for (let i = 0; i < keys.length; i++) {
|
|
2634
|
+
const key = keys[i];
|
|
2635
|
+
const field = fields.get(key);
|
|
2636
|
+
if (!field || isRelationship(field)) continue;
|
|
2637
|
+
const remoteBefore = resolveAttr(key, layers, RESOLUTION_ORDER_REMOTE_STATE);
|
|
2638
|
+
const remoteAfter = resolveAttr(key, layers, remoteAfterOrder);
|
|
2639
|
+
const localBefore = resolveAttr(key, layers, RESOLUTION_ORDER_LOCAL_STATE);
|
|
2640
|
+
const localAfter = resolveAttr(key, layers, localAfterOrder);
|
|
2641
|
+
const remoteMoved = !attrValuesEqual(schema, field, remoteBefore, remoteAfter);
|
|
2642
|
+
const localMoved = !attrValuesEqual(schema, field, localBefore, localAfter);
|
|
2643
|
+
if (remoteMoved && localMoved) (both ??= /* @__PURE__ */ new Set()).add(key);
|
|
2644
|
+
else if (remoteMoved) (remoteOnly ??= /* @__PURE__ */ new Set()).add(key);
|
|
2645
|
+
else if (localMoved) (localOnly ??= /* @__PURE__ */ new Set()).add(key);
|
|
2646
|
+
}
|
|
2647
|
+
return both || remoteOnly || localOnly ? {
|
|
2648
|
+
localOnly,
|
|
2649
|
+
remoteOnly,
|
|
2650
|
+
both
|
|
2651
|
+
} : NO_PROJECTION_CHANGES;
|
|
2652
|
+
}
|
|
2653
|
+
function notifyProjectionChanges(cache, identifier, { both, remoteOnly, localOnly }) {
|
|
2654
|
+
if (both?.size) cache._capabilities.notifyChange(identifier, "attributes", both);
|
|
2655
|
+
if (remoteOnly?.size) cache._capabilities.notifyChange(identifier, "attributes", remoteOnly, "remote");
|
|
2656
|
+
if (localOnly?.size) cache._capabilities.notifyChange(identifier, "attributes", localOnly, "local");
|
|
2233
2657
|
}
|
|
2234
2658
|
function cacheIsEmpty(cached) {
|
|
2235
2659
|
return !cached || cached.remoteAttrs === null && cached.inflightAttrs === null && cached.localAttrs === null;
|
|
@@ -2270,25 +2694,26 @@ function isRelationship(field) {
|
|
|
2270
2694
|
const { kind } = field;
|
|
2271
2695
|
return kind === "hasMany" || kind === "belongsTo" || kind === "resource" || kind === "collection";
|
|
2272
2696
|
}
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
}
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2697
|
+
/**
|
|
2698
|
+
* After a merge: drop every local edit the new baseline agrees with, by the same equality
|
|
2699
|
+
* {@link partitionChangedKeys} uses. Returns whether any edit was dropped.
|
|
2700
|
+
*/
|
|
2701
|
+
function reconcileLocalEdits(schema, cached, fields) {
|
|
2702
|
+
const { localAttrs, defaultAttrs } = cached;
|
|
2703
|
+
if (!localAttrs) return false;
|
|
2704
|
+
let droppedAnEdit = false;
|
|
2705
|
+
const editedKeys = Object.keys(localAttrs);
|
|
2706
|
+
for (let i = 0; i < editedKeys.length; i++) {
|
|
2707
|
+
const key = editedKeys[i];
|
|
2708
|
+
const field = fields.get(key);
|
|
2709
|
+
const baseline = resolveAttr(key, cached, RESOLUTION_ORDER_EDIT_BASELINE);
|
|
2710
|
+
if (field ? attrValuesEqual(schema, field, baseline, localAttrs[key]) : baseline === localAttrs[key]) {
|
|
2711
|
+
droppedAnEdit = true;
|
|
2712
|
+
delete localAttrs[key];
|
|
2288
2713
|
}
|
|
2289
|
-
if (defaultAttrs &&
|
|
2714
|
+
if (defaultAttrs && key in defaultAttrs) delete defaultAttrs[key];
|
|
2290
2715
|
}
|
|
2291
|
-
return
|
|
2716
|
+
return droppedAnEdit;
|
|
2292
2717
|
}
|
|
2293
2718
|
function putOne(cache, identifiers, resource) {
|
|
2294
2719
|
((test) => {
|
|
@@ -2380,7 +2805,7 @@ function copyLinksAndMeta(target, source) {
|
|
|
2380
2805
|
if ("meta" in source) target.meta = source.meta;
|
|
2381
2806
|
}
|
|
2382
2807
|
function cacheUpsert(cache, identifier, data, calculateChanges) {
|
|
2383
|
-
let
|
|
2808
|
+
let changes = NO_PROJECTION_CHANGES;
|
|
2384
2809
|
const peeked = cache.__safePeek(identifier, false);
|
|
2385
2810
|
const existed = !!peeked;
|
|
2386
2811
|
const cached = peeked || cache._createCache(identifier);
|
|
@@ -2401,17 +2826,27 @@ function cacheUpsert(cache, identifier, data, calculateChanges) {
|
|
|
2401
2826
|
cache._capabilities.notifyChange(identifier, "state", null);
|
|
2402
2827
|
}
|
|
2403
2828
|
const fields = getCacheFields(cache, identifier);
|
|
2404
|
-
if (calculateChanges && existed && data.attributes)
|
|
2405
|
-
cached
|
|
2829
|
+
if (calculateChanges && existed && data.attributes) changes = partitionChangedKeys(cache._capabilities.schema, layersForMerge(cached, data.attributes), fields, "upsert");
|
|
2830
|
+
mergeIntoRemote(cached, data.attributes ?? null, "upsert");
|
|
2406
2831
|
if (cached.localAttrs) {
|
|
2407
|
-
if (
|
|
2832
|
+
if (reconcileLocalEdits(cache._capabilities.schema, cached, fields)) cache._capabilities.notifyChange(identifier, "state", null);
|
|
2408
2833
|
}
|
|
2409
2834
|
if (!isUpdate) cache._capabilities.notifyChange(identifier, "added", null);
|
|
2410
2835
|
if (data.id) cached.id = data.id;
|
|
2411
2836
|
if (data.relationships) setupRelationships(cache.__graph, fields, identifier, data);
|
|
2412
|
-
|
|
2837
|
+
((test) => {
|
|
2838
|
+
if (!test) throw new Error("An upsert merges nothing into remote state, so no key can move in the local projection alone");
|
|
2839
|
+
})(!changes.localOnly?.size);
|
|
2840
|
+
notifyProjectionChanges(cache, identifier, changes);
|
|
2413
2841
|
if (getGlobalConfig().WarpDriveMirror.debug.LOG_CACHE || globalThis.getWarpDriveRuntimeConfig().debug.LOG_CACHE) console.groupEnd();
|
|
2414
|
-
return
|
|
2842
|
+
return remoteChangedKeys(changes);
|
|
2843
|
+
}
|
|
2844
|
+
/** Every key whose remote value moved, as the `upsert` return value. */
|
|
2845
|
+
function remoteChangedKeys({ both, remoteOnly }) {
|
|
2846
|
+
if (!both?.size) return remoteOnly?.size ? Array.from(remoteOnly) : void 0;
|
|
2847
|
+
const keys = Array.from(both);
|
|
2848
|
+
if (remoteOnly?.size) keys.push(...remoteOnly);
|
|
2849
|
+
return keys;
|
|
2415
2850
|
}
|
|
2416
2851
|
function patchCache(Cache, op) {
|
|
2417
2852
|
const isRecord = isResourceKey(op.record);
|
|
@@ -2510,10 +2945,11 @@ function commitDidError(cache, identifier, errors) {
|
|
|
2510
2945
|
const keys = Object.keys(cached.inflightAttrs);
|
|
2511
2946
|
if (keys.length > 0) {
|
|
2512
2947
|
const attrs = cached.localAttrs = cached.localAttrs || Object.create(null);
|
|
2513
|
-
for (let i = 0; i < keys.length; i++) if (
|
|
2948
|
+
for (let i = 0; i < keys.length; i++) if (!(keys[i] in attrs)) attrs[keys[i]] = cached.inflightAttrs[keys[i]];
|
|
2514
2949
|
}
|
|
2515
2950
|
cached.inflightAttrs = null;
|
|
2516
2951
|
}
|
|
2952
|
+
if (reconcileLocalEdits(cache._capabilities.schema, cached, getCacheFields(cache, identifier))) cache._capabilities.notifyChange(identifier, "state", null);
|
|
2517
2953
|
if (errors) cached.errors = errors;
|
|
2518
2954
|
cache._capabilities.notifyChange(identifier, "errors", null);
|
|
2519
2955
|
}
|
|
@@ -2543,7 +2979,7 @@ function didCommit(cache, committedIdentifier, data, op) {
|
|
|
2543
2979
|
}
|
|
2544
2980
|
const fields = getCacheFields(cache, identifier);
|
|
2545
2981
|
cached.isNew = false;
|
|
2546
|
-
let
|
|
2982
|
+
let responseAttrs = null;
|
|
2547
2983
|
if (data) {
|
|
2548
2984
|
if (data.id && !cached.id) cached.id = data.id;
|
|
2549
2985
|
if (identifier === committedIdentifier && identifier.id !== existingId) cache._capabilities.notifyChange(identifier, "identity", null);
|
|
@@ -2567,17 +3003,16 @@ function didCommit(cache, committedIdentifier, data, op) {
|
|
|
2567
3003
|
cached.inflightRelationships = null;
|
|
2568
3004
|
setupRelationships(cache.__graph, fields, identifier, data);
|
|
2569
3005
|
}
|
|
2570
|
-
|
|
3006
|
+
responseAttrs = data.attributes ?? null;
|
|
2571
3007
|
}
|
|
2572
|
-
const
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
patchLocalAttributes(cached, changedKeys);
|
|
3008
|
+
const changes = partitionChangedKeys(cache._capabilities.schema, layersForMerge(cached, responseAttrs), fields, "commit");
|
|
3009
|
+
mergeIntoRemote(cached, responseAttrs, "commit");
|
|
3010
|
+
reconcileLocalEdits(cache._capabilities.schema, cached, fields);
|
|
2576
3011
|
if (cached.errors) {
|
|
2577
3012
|
cached.errors = null;
|
|
2578
3013
|
cache._capabilities.notifyChange(identifier, "errors", null);
|
|
2579
3014
|
}
|
|
2580
|
-
|
|
3015
|
+
notifyProjectionChanges(cache, identifier, changes);
|
|
2581
3016
|
cache._capabilities.notifyChange(identifier, "state", null);
|
|
2582
3017
|
}
|
|
2583
3018
|
function willCommit(cache, identifier) {
|