@warp-drive-mirror/json-api 5.10.0-alpha.0 → 5.10.0-alpha.10
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/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/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
|
import { getGlobalConfig, macroCondition } from "@embroider/macros";
|
|
@@ -938,14 +938,101 @@ const EMPTY_ITERATOR = { iterator() {
|
|
|
938
938
|
};
|
|
939
939
|
} };
|
|
940
940
|
} };
|
|
941
|
+
/** One attributes hash on a {@link CachedResource}: a **layer** a projection is read through. */
|
|
942
|
+
/**
|
|
943
|
+
* The cache's entry for a single resource: its id, its attribute values split
|
|
944
|
+
* across four **layers**, and the flags tracking where it sits in the
|
|
945
|
+
* create/update/delete lifecycle.
|
|
946
|
+
*
|
|
947
|
+
* A **projection** answers "what is this field's value" by reading a stack of
|
|
948
|
+
* layers top-down; the first layer holding the field wins. The two projections,
|
|
949
|
+
* with their resolution orders:
|
|
950
|
+
*
|
|
951
|
+
* - **remote state**, what the _immutable_ record reads:
|
|
952
|
+
* {@link RESOLUTION_ORDER_REMOTE_STATE}
|
|
953
|
+
* - **local state**, what an _editable_ copy reads:
|
|
954
|
+
* {@link RESOLUTION_ORDER_LOCAL_STATE}
|
|
955
|
+
*
|
|
956
|
+
* Local state is remote state with the uncommitted mutations laid over it. The
|
|
957
|
+
* guides call those mutations "the diff"; here they are simply the top layers of
|
|
958
|
+
* local state, {@link CachedResource.localAttrs | localAttrs} and
|
|
959
|
+
* {@link CachedResource.inflightAttrs | inflightAttrs}.
|
|
960
|
+
*
|
|
961
|
+
* A field is **dirty** while `localAttrs` or `inflightAttrs` holds it. A save
|
|
962
|
+
* **commits** the mutation: `didCommit` merges the in-flight values into
|
|
963
|
+
* `remoteAttrs` and removes them from `inflightAttrs`. A push carrying the same
|
|
964
|
+
* value as a pending edit in `localAttrs` has the same effect on that edit: the
|
|
965
|
+
* server already holds it, so it is removed from `localAttrs`. "Same value" is
|
|
966
|
+
* decided by the field's schema: a schema-object with an identity hash compares
|
|
967
|
+
* by that hash, everything else by reference.
|
|
968
|
+
*
|
|
969
|
+
* Thus, a dirty field reads as mutated for local readers only, and goes on doing
|
|
970
|
+
* so while its save is in flight.
|
|
971
|
+
*
|
|
972
|
+
* @internal
|
|
973
|
+
*/
|
|
974
|
+
/** A layer name on a {@link CachedResource}. */
|
|
975
|
+
/**
|
|
976
|
+
* A layer that exists only for the duration of a merge: the attributes arriving from the
|
|
977
|
+
* server in a save response or a push. {@link partitionChangedKeys} reads it as if it were
|
|
978
|
+
* already on the resource; {@link mergeIntoRemote} then folds the same attributes in, handed
|
|
979
|
+
* to it directly. Never part of a reader's resolution order.
|
|
980
|
+
*/
|
|
981
|
+
/** Anything a projection can be read through: a {@link CachedResource}, or the layers a merge reads, built around one. */
|
|
982
|
+
/** What the immutable record reads. */
|
|
983
|
+
const RESOLUTION_ORDER_REMOTE_STATE = ["remoteAttrs", "defaultAttrs"];
|
|
984
|
+
/**
|
|
985
|
+
* What a new local edit replaces: the in-flight value if a save is carrying one, else the
|
|
986
|
+
* persisted value. Put another way, what the record will read once the in-flight save lands,
|
|
987
|
+
* assuming the server agrees. This is local state beneath `localAttrs`, less `defaultAttrs`: an
|
|
988
|
+
* edit equal to a schema default is still an edit, since nothing persisted holds that value.
|
|
989
|
+
*
|
|
990
|
+
* Saving does not use this baseline. {@link JSONAPICache.changedAttrs | changedAttrs} still lists
|
|
991
|
+
* in-flight values as unsaved, so a second save re-sends them rather than assuming the first one
|
|
992
|
+
* will succeed.
|
|
993
|
+
*/
|
|
994
|
+
const RESOLUTION_ORDER_EDIT_BASELINE = ["inflightAttrs", "remoteAttrs"];
|
|
995
|
+
/** What an editable copy reads. */
|
|
996
|
+
const RESOLUTION_ORDER_LOCAL_STATE = [
|
|
997
|
+
"localAttrs",
|
|
998
|
+
...RESOLUTION_ORDER_EDIT_BASELINE,
|
|
999
|
+
"defaultAttrs"
|
|
1000
|
+
];
|
|
1001
|
+
const RESOLUTION_ORDER_REMOTE_AFTER_COMMIT = [
|
|
1002
|
+
"incomingAttrs",
|
|
1003
|
+
"inflightAttrs",
|
|
1004
|
+
...RESOLUTION_ORDER_REMOTE_STATE
|
|
1005
|
+
];
|
|
1006
|
+
const RESOLUTION_ORDER_LOCAL_AFTER_COMMIT = ["localAttrs", ...RESOLUTION_ORDER_REMOTE_AFTER_COMMIT];
|
|
1007
|
+
const RESOLUTION_ORDER_REMOTE_AFTER_UPSERT = ["incomingAttrs", ...RESOLUTION_ORDER_REMOTE_STATE];
|
|
1008
|
+
const RESOLUTION_ORDER_LOCAL_AFTER_UPSERT = [
|
|
1009
|
+
"localAttrs",
|
|
1010
|
+
"inflightAttrs",
|
|
1011
|
+
...RESOLUTION_ORDER_REMOTE_AFTER_UPSERT
|
|
1012
|
+
];
|
|
1013
|
+
/** The layers an after-merge order folds into `remoteAttrs`, lowest precedence first. */
|
|
1014
|
+
function layersFoldedIntoRemote(order) {
|
|
1015
|
+
return order.slice(0, order.indexOf("remoteAttrs")).reverse();
|
|
1016
|
+
}
|
|
1017
|
+
const MERGE_RESOLUTION = {
|
|
1018
|
+
commit: {
|
|
1019
|
+
remoteAfter: RESOLUTION_ORDER_REMOTE_AFTER_COMMIT,
|
|
1020
|
+
localAfter: RESOLUTION_ORDER_LOCAL_AFTER_COMMIT,
|
|
1021
|
+
folded: layersFoldedIntoRemote(RESOLUTION_ORDER_REMOTE_AFTER_COMMIT)
|
|
1022
|
+
},
|
|
1023
|
+
upsert: {
|
|
1024
|
+
remoteAfter: RESOLUTION_ORDER_REMOTE_AFTER_UPSERT,
|
|
1025
|
+
localAfter: RESOLUTION_ORDER_LOCAL_AFTER_UPSERT,
|
|
1026
|
+
folded: layersFoldedIntoRemote(RESOLUTION_ORDER_REMOTE_AFTER_UPSERT)
|
|
1027
|
+
}
|
|
1028
|
+
};
|
|
941
1029
|
function makeCache() {
|
|
942
1030
|
return {
|
|
943
1031
|
id: null,
|
|
944
|
-
remoteAttrs: null,
|
|
945
1032
|
localAttrs: null,
|
|
946
|
-
defaultAttrs: null,
|
|
947
1033
|
inflightAttrs: null,
|
|
948
|
-
|
|
1034
|
+
remoteAttrs: null,
|
|
1035
|
+
defaultAttrs: null,
|
|
949
1036
|
errors: null,
|
|
950
1037
|
isNew: false,
|
|
951
1038
|
isDeleted: false,
|
|
@@ -996,9 +1083,9 @@ var JSONAPICache = class {
|
|
|
996
1083
|
/**
|
|
997
1084
|
* Cache the response to a request
|
|
998
1085
|
*
|
|
999
|
-
* Implements
|
|
1086
|
+
* Implements {@link Cache.put | Cache.put}.
|
|
1000
1087
|
*
|
|
1001
|
-
* Expects a StructuredDocument whose `content` member is a JsonApiDocument.
|
|
1088
|
+
* Expects a {@link StructuredDocument} whose `content` member is a JsonApiDocument.
|
|
1002
1089
|
*
|
|
1003
1090
|
* ```js
|
|
1004
1091
|
* cache.put({
|
|
@@ -1133,6 +1220,16 @@ var JSONAPICache = class {
|
|
|
1133
1220
|
* Update the "remote" or "canonical" (persisted) state of the Cache
|
|
1134
1221
|
* by merging new information into the existing state.
|
|
1135
1222
|
*
|
|
1223
|
+
* @example
|
|
1224
|
+
* ```ts
|
|
1225
|
+
* cache.patch({
|
|
1226
|
+
* op: 'update',
|
|
1227
|
+
* record: identifier,
|
|
1228
|
+
* field: 'name',
|
|
1229
|
+
* value: 'Chris',
|
|
1230
|
+
* });
|
|
1231
|
+
* ```
|
|
1232
|
+
*
|
|
1136
1233
|
* @category Cache Management
|
|
1137
1234
|
* @public
|
|
1138
1235
|
* @param op the operation or list of operations to perform
|
|
@@ -1154,6 +1251,16 @@ var JSONAPICache = class {
|
|
|
1154
1251
|
/**
|
|
1155
1252
|
* Update the "local" or "current" (unpersisted) state of the Cache
|
|
1156
1253
|
*
|
|
1254
|
+
* @example
|
|
1255
|
+
* ```ts
|
|
1256
|
+
* cache.mutate({
|
|
1257
|
+
* op: 'replaceRelatedRecord',
|
|
1258
|
+
* record: identifier,
|
|
1259
|
+
* field: 'author',
|
|
1260
|
+
* value: authorIdentifier,
|
|
1261
|
+
* });
|
|
1262
|
+
* ```
|
|
1263
|
+
*
|
|
1157
1264
|
* @category Cache Management
|
|
1158
1265
|
* @public
|
|
1159
1266
|
*/
|
|
@@ -1194,8 +1301,8 @@ var JSONAPICache = class {
|
|
|
1194
1301
|
* not require retainining connections to the Store
|
|
1195
1302
|
* and Cache to present data on a per-field basis.
|
|
1196
1303
|
*
|
|
1197
|
-
* This generally takes the place of
|
|
1198
|
-
* an API and may even take the place of
|
|
1304
|
+
* This generally takes the place of {@link JSONAPICache.getAttr | getAttr} as
|
|
1305
|
+
* an API and may even take the place of {@link JSONAPICache.getRelationship | getRelationship}
|
|
1199
1306
|
* depending on implementation specifics, though this
|
|
1200
1307
|
* latter usage is less recommended due to the advantages
|
|
1201
1308
|
* of the Graph handling necessary entanglements and
|
|
@@ -1210,6 +1317,12 @@ var JSONAPICache = class {
|
|
|
1210
1317
|
* the various internal WarpDrive bookkeeping fields.
|
|
1211
1318
|
* :::
|
|
1212
1319
|
*
|
|
1320
|
+
* @example
|
|
1321
|
+
* ```ts
|
|
1322
|
+
* const resource = cache.peek(identifier);
|
|
1323
|
+
* const document = cache.peek(requestKey);
|
|
1324
|
+
* ```
|
|
1325
|
+
*
|
|
1213
1326
|
* @category Cache Management
|
|
1214
1327
|
* @public
|
|
1215
1328
|
*/
|
|
@@ -1249,6 +1362,12 @@ var JSONAPICache = class {
|
|
|
1249
1362
|
/**
|
|
1250
1363
|
* Peek the remote resource data from the Cache.
|
|
1251
1364
|
*
|
|
1365
|
+
* @example
|
|
1366
|
+
* ```ts
|
|
1367
|
+
* const resource = cache.peekRemoteState(identifier);
|
|
1368
|
+
* const document = cache.peekRemoteState(requestKey);
|
|
1369
|
+
* ```
|
|
1370
|
+
*
|
|
1252
1371
|
* @category Cache Management
|
|
1253
1372
|
* @public
|
|
1254
1373
|
*/
|
|
@@ -1289,9 +1408,14 @@ var JSONAPICache = class {
|
|
|
1289
1408
|
* Peek the Cache for the existing request data associated with
|
|
1290
1409
|
* a cacheable request.
|
|
1291
1410
|
*
|
|
1292
|
-
* This is effectively the reverse of
|
|
1411
|
+
* This is effectively the reverse of {@link JSONAPICache.put | put} for a request in
|
|
1293
1412
|
* that it will return the the request, response, and content
|
|
1294
|
-
* whereas
|
|
1413
|
+
* whereas {@link JSONAPICache.peek | peek} will return just the `content`.
|
|
1414
|
+
*
|
|
1415
|
+
* @example
|
|
1416
|
+
* ```ts
|
|
1417
|
+
* const doc = cache.peekRequest(requestKey);
|
|
1418
|
+
* ```
|
|
1295
1419
|
*
|
|
1296
1420
|
* @category Cache Management
|
|
1297
1421
|
* @public
|
|
@@ -1302,9 +1426,20 @@ var JSONAPICache = class {
|
|
|
1302
1426
|
/**
|
|
1303
1427
|
* Push resource data from a remote source into the cache for this identifier
|
|
1304
1428
|
*
|
|
1429
|
+
* @example
|
|
1430
|
+
* ```ts
|
|
1431
|
+
* cache.upsert(identifier, {
|
|
1432
|
+
* type: 'user',
|
|
1433
|
+
* id: '1',
|
|
1434
|
+
* attributes: { name: 'Chris' },
|
|
1435
|
+
* });
|
|
1436
|
+
* ```
|
|
1437
|
+
*
|
|
1305
1438
|
* @category Cache Management
|
|
1306
1439
|
* @public
|
|
1307
|
-
* @return
|
|
1440
|
+
* @return when `calculateChanges` is true, the names of the attributes whose persisted value
|
|
1441
|
+
* this push changed (the same keys the `'remote'` channel is notified with), or `undefined`
|
|
1442
|
+
* when none did. Otherwise `void`.
|
|
1308
1443
|
*/
|
|
1309
1444
|
upsert(identifier, data, calculateChanges) {
|
|
1310
1445
|
assertPrivateCapabilities(this._capabilities);
|
|
@@ -1350,7 +1485,7 @@ var JSONAPICache = class {
|
|
|
1350
1485
|
*
|
|
1351
1486
|
* Each individual resource or document that has
|
|
1352
1487
|
* been mutated should be described as an individual
|
|
1353
|
-
*
|
|
1488
|
+
* {@link Change} entry in the returned array.
|
|
1354
1489
|
*
|
|
1355
1490
|
* A `Change` is described by an object containing up to
|
|
1356
1491
|
* three properties: (1) the `identifier` of the entity that
|
|
@@ -1415,6 +1550,11 @@ var JSONAPICache = class {
|
|
|
1415
1550
|
* It returns properties from options that should be set on the record during the create
|
|
1416
1551
|
* process. This return value behavior is deprecated.
|
|
1417
1552
|
*
|
|
1553
|
+
* @example
|
|
1554
|
+
* ```ts
|
|
1555
|
+
* cache.clientDidCreate(identifier, { name: 'Chris' });
|
|
1556
|
+
* ```
|
|
1557
|
+
*
|
|
1418
1558
|
* @category Resource Lifecycle
|
|
1419
1559
|
* @public
|
|
1420
1560
|
*/
|
|
@@ -1479,6 +1619,11 @@ var JSONAPICache = class {
|
|
|
1479
1619
|
* [LIFECYCLE] Signals to the cache that a resource
|
|
1480
1620
|
* will be part of a save transaction.
|
|
1481
1621
|
*
|
|
1622
|
+
* @example
|
|
1623
|
+
* ```ts
|
|
1624
|
+
* cache.willCommit(identifier, context);
|
|
1625
|
+
* ```
|
|
1626
|
+
*
|
|
1482
1627
|
* @category Resource Lifecycle
|
|
1483
1628
|
* @public
|
|
1484
1629
|
*/
|
|
@@ -1490,6 +1635,11 @@ var JSONAPICache = class {
|
|
|
1490
1635
|
* [LIFECYCLE] Signals to the cache that a resource
|
|
1491
1636
|
* was successfully updated as part of a save transaction.
|
|
1492
1637
|
*
|
|
1638
|
+
* @example
|
|
1639
|
+
* ```ts
|
|
1640
|
+
* cache.didCommit(identifier, result);
|
|
1641
|
+
* ```
|
|
1642
|
+
*
|
|
1493
1643
|
* @category Resource Lifecycle
|
|
1494
1644
|
* @public
|
|
1495
1645
|
*/
|
|
@@ -1529,6 +1679,11 @@ var JSONAPICache = class {
|
|
|
1529
1679
|
* [LIFECYCLE] Signals to the cache that a resource
|
|
1530
1680
|
* was update via a save transaction failed.
|
|
1531
1681
|
*
|
|
1682
|
+
* @example
|
|
1683
|
+
* ```ts
|
|
1684
|
+
* cache.commitWasRejected(identifier, errors);
|
|
1685
|
+
* ```
|
|
1686
|
+
*
|
|
1532
1687
|
* @category Resource Lifecycle
|
|
1533
1688
|
* @public
|
|
1534
1689
|
*/
|
|
@@ -1545,6 +1700,11 @@ var JSONAPICache = class {
|
|
|
1545
1700
|
*
|
|
1546
1701
|
* This method is a candidate to become a mutation
|
|
1547
1702
|
*
|
|
1703
|
+
* @example
|
|
1704
|
+
* ```ts
|
|
1705
|
+
* cache.unloadRecord(identifier);
|
|
1706
|
+
* ```
|
|
1707
|
+
*
|
|
1548
1708
|
* @category Resource Lifecycle
|
|
1549
1709
|
* @public
|
|
1550
1710
|
*/
|
|
@@ -1585,6 +1745,12 @@ var JSONAPICache = class {
|
|
|
1585
1745
|
* Retrieve the data for an attribute from the cache
|
|
1586
1746
|
* with local mutations applied.
|
|
1587
1747
|
*
|
|
1748
|
+
* @example
|
|
1749
|
+
* ```ts
|
|
1750
|
+
* const name = cache.getAttr(identifier, 'name');
|
|
1751
|
+
* const zip = cache.getAttr(identifier, ['address', 'zip']);
|
|
1752
|
+
* ```
|
|
1753
|
+
*
|
|
1588
1754
|
* @category Resource Data
|
|
1589
1755
|
* @public
|
|
1590
1756
|
*/
|
|
@@ -1598,37 +1764,28 @@ var JSONAPICache = class {
|
|
|
1598
1764
|
if (!test) throw new Error(`Cannot retrieve attributes for identifier ${String(identifier)} as it is not present in the cache`);
|
|
1599
1765
|
})(cached);
|
|
1600
1766
|
if (!cached) return;
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
if (schemaHasLegacyDefaultValueFn(attrSchema)) {
|
|
1610
|
-
cached.defaultAttrs = cached.defaultAttrs || Object.create(null);
|
|
1611
|
-
cached.defaultAttrs[attribute] = defaultValue;
|
|
1612
|
-
}
|
|
1613
|
-
return defaultValue;
|
|
1767
|
+
const layer = layerHolding(attribute, cached, RESOLUTION_ORDER_LOCAL_STATE);
|
|
1768
|
+
if (layer) return layer[attribute];
|
|
1769
|
+
const attrSchema = getCacheFields(this, identifier).get(attribute);
|
|
1770
|
+
assertPrivateCapabilities(this._capabilities);
|
|
1771
|
+
const defaultValue = getDefaultValue(attrSchema, identifier, this._capabilities._store);
|
|
1772
|
+
if (schemaHasLegacyDefaultValueFn(attrSchema)) {
|
|
1773
|
+
cached.defaultAttrs = cached.defaultAttrs || Object.create(null);
|
|
1774
|
+
cached.defaultAttrs[attribute] = defaultValue;
|
|
1614
1775
|
}
|
|
1776
|
+
return defaultValue;
|
|
1615
1777
|
}
|
|
1616
|
-
const path = attr;
|
|
1617
1778
|
const cached = this.__peek(identifier, true);
|
|
1618
|
-
|
|
1619
|
-
let current = cached.localAttrs && basePath in cached.localAttrs ? cached.localAttrs[basePath] : void 0;
|
|
1620
|
-
if (current === void 0) current = cached.inflightAttrs && basePath in cached.inflightAttrs ? cached.inflightAttrs[basePath] : void 0;
|
|
1621
|
-
if (current === void 0) current = cached.remoteAttrs && basePath in cached.remoteAttrs ? cached.remoteAttrs[basePath] : void 0;
|
|
1622
|
-
if (current === void 0) return;
|
|
1623
|
-
for (let i = 1; i < path.length; i++) {
|
|
1624
|
-
current = current[path[i]];
|
|
1625
|
-
if (current === void 0) return;
|
|
1626
|
-
}
|
|
1627
|
-
return current;
|
|
1779
|
+
return resolveAttr(attr, cached, RESOLUTION_ORDER_LOCAL_STATE);
|
|
1628
1780
|
}
|
|
1629
1781
|
/**
|
|
1630
1782
|
* Retrieve the remote data for an attribute from the cache
|
|
1631
1783
|
*
|
|
1784
|
+
* @example
|
|
1785
|
+
* ```ts
|
|
1786
|
+
* const name = cache.getRemoteAttr(identifier, 'name');
|
|
1787
|
+
* ```
|
|
1788
|
+
*
|
|
1632
1789
|
* @category Resource Data
|
|
1633
1790
|
* @public
|
|
1634
1791
|
*/
|
|
@@ -1642,35 +1799,30 @@ var JSONAPICache = class {
|
|
|
1642
1799
|
if (!test) throw new Error(`Cannot retrieve remote attributes for identifier ${String(identifier)} as it is not present in the cache`);
|
|
1643
1800
|
})(cached);
|
|
1644
1801
|
if (!cached) return;
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
cached.defaultAttrs[attribute] = defaultValue;
|
|
1654
|
-
}
|
|
1655
|
-
return defaultValue;
|
|
1802
|
+
const layer = layerHolding(attribute, cached, RESOLUTION_ORDER_REMOTE_STATE);
|
|
1803
|
+
if (layer) return layer[attribute];
|
|
1804
|
+
const attrSchema = getCacheFields(this, identifier).get(attribute);
|
|
1805
|
+
assertPrivateCapabilities(this._capabilities);
|
|
1806
|
+
const defaultValue = getDefaultValue(attrSchema, identifier, this._capabilities._store);
|
|
1807
|
+
if (schemaHasLegacyDefaultValueFn(attrSchema)) {
|
|
1808
|
+
cached.defaultAttrs = cached.defaultAttrs || Object.create(null);
|
|
1809
|
+
cached.defaultAttrs[attribute] = defaultValue;
|
|
1656
1810
|
}
|
|
1811
|
+
return defaultValue;
|
|
1657
1812
|
}
|
|
1658
|
-
const path = attr;
|
|
1659
1813
|
const cached = this.__peek(identifier, true);
|
|
1660
|
-
|
|
1661
|
-
let current = cached.remoteAttrs && basePath in cached.remoteAttrs ? cached.remoteAttrs[basePath] : void 0;
|
|
1662
|
-
if (current === void 0) return;
|
|
1663
|
-
for (let i = 1; i < path.length; i++) {
|
|
1664
|
-
current = current[path[i]];
|
|
1665
|
-
if (current === void 0) return;
|
|
1666
|
-
}
|
|
1667
|
-
return current;
|
|
1814
|
+
return resolveAttr(attr, cached, RESOLUTION_ORDER_REMOTE_STATE);
|
|
1668
1815
|
}
|
|
1669
1816
|
/**
|
|
1670
1817
|
* Mutate the data for an attribute in the cache
|
|
1671
1818
|
*
|
|
1672
1819
|
* This method is a candidate to become a mutation
|
|
1673
1820
|
*
|
|
1821
|
+
* @example
|
|
1822
|
+
* ```ts
|
|
1823
|
+
* cache.setAttr(identifier, 'name', 'Chris');
|
|
1824
|
+
* ```
|
|
1825
|
+
*
|
|
1674
1826
|
* @category Resource Data
|
|
1675
1827
|
* @public
|
|
1676
1828
|
*/
|
|
@@ -1678,21 +1830,19 @@ var JSONAPICache = class {
|
|
|
1678
1830
|
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) && ((test) => {
|
|
1679
1831
|
if (!test) throw new Error("setAttr must receive at least one attribute path");
|
|
1680
1832
|
})(attr.length > 0);
|
|
1833
|
+
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) && ((test) => {
|
|
1834
|
+
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.`);
|
|
1835
|
+
})(value !== void 0);
|
|
1836
|
+
if (value === void 0) value = null;
|
|
1681
1837
|
const isSimplePath = !Array.isArray(attr) || attr.length === 1;
|
|
1682
1838
|
if (Array.isArray(attr) && attr.length === 1) attr = attr[0];
|
|
1683
1839
|
if (isSimplePath) {
|
|
1684
1840
|
const cached = this.__peek(identifier, false);
|
|
1685
1841
|
const currentAttr = attr;
|
|
1686
|
-
|
|
1687
|
-
if (existing !== value) {
|
|
1842
|
+
if (resolveAttr(currentAttr, cached, RESOLUTION_ORDER_EDIT_BASELINE) !== value) {
|
|
1688
1843
|
cached.localAttrs = cached.localAttrs || Object.create(null);
|
|
1689
1844
|
cached.localAttrs[currentAttr] = value;
|
|
1690
|
-
|
|
1691
|
-
cached.changes[currentAttr] = [existing, value];
|
|
1692
|
-
} else if (cached.localAttrs) {
|
|
1693
|
-
delete cached.localAttrs[currentAttr];
|
|
1694
|
-
delete cached.changes[currentAttr];
|
|
1695
|
-
}
|
|
1845
|
+
} else if (cached.localAttrs) delete cached.localAttrs[currentAttr];
|
|
1696
1846
|
if (cached.defaultAttrs && currentAttr in cached.defaultAttrs) delete cached.defaultAttrs[currentAttr];
|
|
1697
1847
|
this._capabilities.notifyChange(identifier, "attributes", currentAttr, "local");
|
|
1698
1848
|
return;
|
|
@@ -1700,32 +1850,50 @@ var JSONAPICache = class {
|
|
|
1700
1850
|
const path = attr;
|
|
1701
1851
|
const cached = this.__peek(identifier, false);
|
|
1702
1852
|
const basePath = path[0];
|
|
1703
|
-
const
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1853
|
+
const baseline = resolveAttr(basePath, cached, RESOLUTION_ORDER_EDIT_BASELINE);
|
|
1854
|
+
const isRevert = valueAtPath(baseline, path) === value;
|
|
1855
|
+
const hasLocalClone = !!cached.localAttrs && basePath in cached.localAttrs;
|
|
1856
|
+
if (isRevert && !hasLocalClone) return;
|
|
1857
|
+
cached.localAttrs = cached.localAttrs || Object.create(null);
|
|
1858
|
+
if (!hasLocalClone) {
|
|
1859
|
+
const seed = baseline ?? (cached.defaultAttrs ? cached.defaultAttrs[basePath] : void 0);
|
|
1860
|
+
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) && ((test) => {
|
|
1861
|
+
if (!test) throw new Error(`Cannot set '${path.join(".")}' on '${identifier.type}': '${basePath}' holds no object to write into`);
|
|
1862
|
+
})(!!seed && typeof seed === "object");
|
|
1863
|
+
cached.localAttrs[basePath] = structuredClone(seed);
|
|
1708
1864
|
}
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
if (!existing) return;
|
|
1720
|
-
if (JSON.stringify(existing) !== JSON.stringify(cached.localAttrs[basePath])) {
|
|
1721
|
-
delete cached.localAttrs[basePath];
|
|
1722
|
-
delete cached.changes[basePath];
|
|
1723
|
-
}
|
|
1724
|
-
} catch {}
|
|
1865
|
+
let currentLocal = cached.localAttrs[basePath];
|
|
1866
|
+
let nextLink = 1;
|
|
1867
|
+
while (nextLink < path.length - 1) currentLocal = currentLocal[path[nextLink++]];
|
|
1868
|
+
if (currentLocal[path[nextLink]] === value) return;
|
|
1869
|
+
currentLocal[path[nextLink]] = value;
|
|
1870
|
+
if (isRevert) {
|
|
1871
|
+
const field = getCacheFields(this, identifier).get(basePath);
|
|
1872
|
+
if (localCloneMatchesBaseline(this._capabilities.schema, field, baseline, cached.localAttrs[basePath])) delete cached.localAttrs[basePath];
|
|
1873
|
+
}
|
|
1874
|
+
if (cached.defaultAttrs && basePath in cached.defaultAttrs) delete cached.defaultAttrs[basePath];
|
|
1725
1875
|
this._capabilities.notifyChange(identifier, "attributes", basePath, "local");
|
|
1726
1876
|
}
|
|
1727
1877
|
/**
|
|
1728
|
-
* Query the cache for the changed attributes of a resource
|
|
1878
|
+
* Query the cache for the changed attributes of a resource: every unsaved
|
|
1879
|
+
* mutation, as a `[before, after]` pair per field.
|
|
1880
|
+
*
|
|
1881
|
+
* `before` is the value the mutation replaces, which is not always the
|
|
1882
|
+
* persisted one. A mutation a save is carrying replaces remote state; an edit
|
|
1883
|
+
* made while that save is in flight replaces the in-flight value. So this is
|
|
1884
|
+
* what saving from here would change, which is what `serializePatch` and the
|
|
1885
|
+
* legacy `Snapshot` consume, rather than a diff against persisted state.
|
|
1886
|
+
*
|
|
1887
|
+
* Derived from the layers on each call, so it is always consistent with
|
|
1888
|
+
* {@link JSONAPICache.getAttr | getAttr} and
|
|
1889
|
+
* {@link JSONAPICache.rollbackAttrs | rollbackAttrs}. Dirtiness does not go
|
|
1890
|
+
* through here; see {@link JSONAPICache.hasChangedAttrs | hasChangedAttrs}.
|
|
1891
|
+
*
|
|
1892
|
+
* @example
|
|
1893
|
+
* ```ts
|
|
1894
|
+
* const changes = cache.changedAttrs(identifier);
|
|
1895
|
+
* // { name: ['Igor', 'Chris'] }
|
|
1896
|
+
* ```
|
|
1729
1897
|
*
|
|
1730
1898
|
* @category Resource Data
|
|
1731
1899
|
* @public
|
|
@@ -1736,12 +1904,29 @@ var JSONAPICache = class {
|
|
|
1736
1904
|
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) && ((test) => {
|
|
1737
1905
|
if (!test) throw new Error(`Cannot retrieve changed attributes for identifier ${String(identifier)} as it is not present in the cache`);
|
|
1738
1906
|
})(cached);
|
|
1739
|
-
|
|
1740
|
-
|
|
1907
|
+
const changes = Object.create(null);
|
|
1908
|
+
if (!cached) return changes;
|
|
1909
|
+
const { localAttrs, inflightAttrs, remoteAttrs } = cached;
|
|
1910
|
+
if (inflightAttrs) {
|
|
1911
|
+
const keys = Object.keys(inflightAttrs);
|
|
1912
|
+
for (let i = 0; i < keys.length; i++) changes[keys[i]] = [remoteAttrs ? remoteAttrs[keys[i]] : void 0, inflightAttrs[keys[i]]];
|
|
1913
|
+
}
|
|
1914
|
+
if (localAttrs) {
|
|
1915
|
+
const keys = Object.keys(localAttrs);
|
|
1916
|
+
for (let i = 0; i < keys.length; i++) changes[keys[i]] = [resolveAttr(keys[i], cached, RESOLUTION_ORDER_EDIT_BASELINE), localAttrs[keys[i]]];
|
|
1917
|
+
}
|
|
1918
|
+
return changes;
|
|
1741
1919
|
}
|
|
1742
1920
|
/**
|
|
1743
1921
|
* Query the cache for whether any mutated attributes exist
|
|
1744
1922
|
*
|
|
1923
|
+
* @example
|
|
1924
|
+
* ```ts
|
|
1925
|
+
* if (cache.hasChangedAttrs(identifier)) {
|
|
1926
|
+
* // ...
|
|
1927
|
+
* }
|
|
1928
|
+
* ```
|
|
1929
|
+
*
|
|
1745
1930
|
* @category Resource Data
|
|
1746
1931
|
* @public
|
|
1747
1932
|
*/
|
|
@@ -1758,6 +1943,11 @@ var JSONAPICache = class {
|
|
|
1758
1943
|
*
|
|
1759
1944
|
* This method is a candidate to become a mutation
|
|
1760
1945
|
*
|
|
1946
|
+
* @example
|
|
1947
|
+
* ```ts
|
|
1948
|
+
* const restoredKeys = cache.rollbackAttrs(identifier);
|
|
1949
|
+
* ```
|
|
1950
|
+
*
|
|
1761
1951
|
* @category Resource Data
|
|
1762
1952
|
* @public
|
|
1763
1953
|
* @return the names of fields that were restored
|
|
@@ -1769,7 +1959,6 @@ var JSONAPICache = class {
|
|
|
1769
1959
|
if (cached.localAttrs !== null) {
|
|
1770
1960
|
dirtyKeys = Object.keys(cached.localAttrs);
|
|
1771
1961
|
cached.localAttrs = null;
|
|
1772
|
-
cached.changes = null;
|
|
1773
1962
|
}
|
|
1774
1963
|
if (cached.isNew) {
|
|
1775
1964
|
cached.isDeletionCommitted = true;
|
|
@@ -1789,7 +1978,7 @@ var JSONAPICache = class {
|
|
|
1789
1978
|
/**
|
|
1790
1979
|
* Query the cache for the changes to relationships of a resource.
|
|
1791
1980
|
*
|
|
1792
|
-
* Returns a map of relationship names to RelationshipDiff objects.
|
|
1981
|
+
* Returns a map of relationship names to {@link RelationshipDiff} objects.
|
|
1793
1982
|
*
|
|
1794
1983
|
* ```ts
|
|
1795
1984
|
* type RelationshipDiff =
|
|
@@ -1808,6 +1997,12 @@ var JSONAPICache = class {
|
|
|
1808
1997
|
};
|
|
1809
1998
|
```
|
|
1810
1999
|
*
|
|
2000
|
+
* @example
|
|
2001
|
+
* ```ts
|
|
2002
|
+
* const diffs = cache.changedRelationships(identifier);
|
|
2003
|
+
* const comments = diffs.get('comments');
|
|
2004
|
+
* ```
|
|
2005
|
+
*
|
|
1811
2006
|
* @category Resource Data
|
|
1812
2007
|
* @public
|
|
1813
2008
|
*/
|
|
@@ -1817,6 +2012,13 @@ var JSONAPICache = class {
|
|
|
1817
2012
|
/**
|
|
1818
2013
|
* Query the cache for whether any mutated relationships exist
|
|
1819
2014
|
*
|
|
2015
|
+
* @example
|
|
2016
|
+
* ```ts
|
|
2017
|
+
* if (cache.hasChangedRelationships(identifier)) {
|
|
2018
|
+
* // ...
|
|
2019
|
+
* }
|
|
2020
|
+
* ```
|
|
2021
|
+
*
|
|
1820
2022
|
* @category Resource Data
|
|
1821
2023
|
* @public
|
|
1822
2024
|
*/
|
|
@@ -1830,6 +2032,11 @@ var JSONAPICache = class {
|
|
|
1830
2032
|
*
|
|
1831
2033
|
* This method is a candidate to become a mutation
|
|
1832
2034
|
*
|
|
2035
|
+
* @example
|
|
2036
|
+
* ```ts
|
|
2037
|
+
* const restoredFields = cache.rollbackRelationships(identifier);
|
|
2038
|
+
* ```
|
|
2039
|
+
*
|
|
1833
2040
|
* @category Resource Data
|
|
1834
2041
|
* @public
|
|
1835
2042
|
* @return the names of relationships that were restored
|
|
@@ -1845,6 +2052,11 @@ var JSONAPICache = class {
|
|
|
1845
2052
|
/**
|
|
1846
2053
|
* Query the cache for the current state of a relationship property
|
|
1847
2054
|
*
|
|
2055
|
+
* @example
|
|
2056
|
+
* ```ts
|
|
2057
|
+
* const relationship = cache.getRelationship(identifier, 'comments');
|
|
2058
|
+
* ```
|
|
2059
|
+
*
|
|
1848
2060
|
* @category Resource Data
|
|
1849
2061
|
* @public
|
|
1850
2062
|
* @return resource relationship object
|
|
@@ -1855,6 +2067,11 @@ var JSONAPICache = class {
|
|
|
1855
2067
|
/**
|
|
1856
2068
|
* Query the cache for the remote state of a relationship property
|
|
1857
2069
|
*
|
|
2070
|
+
* @example
|
|
2071
|
+
* ```ts
|
|
2072
|
+
* const relationship = cache.getRemoteRelationship(identifier, 'comments');
|
|
2073
|
+
* ```
|
|
2074
|
+
*
|
|
1858
2075
|
* @category Resource Data
|
|
1859
2076
|
* @public
|
|
1860
2077
|
* @return resource relationship object
|
|
@@ -1868,6 +2085,11 @@ var JSONAPICache = class {
|
|
|
1868
2085
|
*
|
|
1869
2086
|
* This method is a candidate to become a mutation
|
|
1870
2087
|
*
|
|
2088
|
+
* @example
|
|
2089
|
+
* ```ts
|
|
2090
|
+
* cache.setIsDeleted(identifier, true);
|
|
2091
|
+
* ```
|
|
2092
|
+
*
|
|
1871
2093
|
* @category Resource State
|
|
1872
2094
|
* @public
|
|
1873
2095
|
*/
|
|
@@ -1879,6 +2101,11 @@ var JSONAPICache = class {
|
|
|
1879
2101
|
/**
|
|
1880
2102
|
* Query the cache for any validation errors applicable to the given resource.
|
|
1881
2103
|
*
|
|
2104
|
+
* @example
|
|
2105
|
+
* ```ts
|
|
2106
|
+
* const errors = cache.getErrors(identifier);
|
|
2107
|
+
* ```
|
|
2108
|
+
*
|
|
1882
2109
|
* @category Resource State
|
|
1883
2110
|
* @public
|
|
1884
2111
|
*/
|
|
@@ -1888,6 +2115,13 @@ var JSONAPICache = class {
|
|
|
1888
2115
|
/**
|
|
1889
2116
|
* Query the cache for whether a given resource has any available data
|
|
1890
2117
|
*
|
|
2118
|
+
* @example
|
|
2119
|
+
* ```ts
|
|
2120
|
+
* if (cache.isEmpty(identifier)) {
|
|
2121
|
+
* // ...
|
|
2122
|
+
* }
|
|
2123
|
+
* ```
|
|
2124
|
+
*
|
|
1891
2125
|
* @category Resource State
|
|
1892
2126
|
* @public
|
|
1893
2127
|
*/
|
|
@@ -1899,6 +2133,13 @@ var JSONAPICache = class {
|
|
|
1899
2133
|
* Query the cache for whether a given resource was created locally and not
|
|
1900
2134
|
* yet persisted.
|
|
1901
2135
|
*
|
|
2136
|
+
* @example
|
|
2137
|
+
* ```ts
|
|
2138
|
+
* if (cache.isNew(identifier)) {
|
|
2139
|
+
* // ...
|
|
2140
|
+
* }
|
|
2141
|
+
* ```
|
|
2142
|
+
*
|
|
1902
2143
|
* @category Resource State
|
|
1903
2144
|
* @public
|
|
1904
2145
|
*/
|
|
@@ -1909,6 +2150,13 @@ var JSONAPICache = class {
|
|
|
1909
2150
|
* Query the cache for whether a given resource is marked as deleted (but not
|
|
1910
2151
|
* necessarily persisted yet).
|
|
1911
2152
|
*
|
|
2153
|
+
* @example
|
|
2154
|
+
* ```ts
|
|
2155
|
+
* if (cache.isDeleted(identifier)) {
|
|
2156
|
+
* // ...
|
|
2157
|
+
* }
|
|
2158
|
+
* ```
|
|
2159
|
+
*
|
|
1912
2160
|
* @category Resource State
|
|
1913
2161
|
* @public
|
|
1914
2162
|
*/
|
|
@@ -1919,6 +2167,13 @@ var JSONAPICache = class {
|
|
|
1919
2167
|
* Query the cache for whether a given resource has been deleted and that deletion
|
|
1920
2168
|
* has also been persisted.
|
|
1921
2169
|
*
|
|
2170
|
+
* @example
|
|
2171
|
+
* ```ts
|
|
2172
|
+
* if (cache.isDeletionCommitted(identifier)) {
|
|
2173
|
+
* // ...
|
|
2174
|
+
* }
|
|
2175
|
+
* ```
|
|
2176
|
+
*
|
|
1922
2177
|
* @category Resource State
|
|
1923
2178
|
* @public
|
|
1924
2179
|
*/
|
|
@@ -2136,20 +2391,189 @@ function getDefaultValue(schema, identifier, store) {
|
|
|
2136
2391
|
if (transform?.defaultValue) return transform.defaultValue(options || null, identifier);
|
|
2137
2392
|
}
|
|
2138
2393
|
}
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
if (!fields.has(key)) continue;
|
|
2148
|
-
const value = updates[key];
|
|
2149
|
-
if (localAttrs && localAttrs[key] !== void 0) continue;
|
|
2150
|
-
if (original[key] !== value) changedKeys.add(key);
|
|
2394
|
+
/**
|
|
2395
|
+
* The first layer, in the given resolution order, that holds `key`. Presence is `in`, so a value
|
|
2396
|
+
* explicitly set to `undefined` counts as present.
|
|
2397
|
+
*/
|
|
2398
|
+
function layerHolding(key, layers, order) {
|
|
2399
|
+
for (let i = 0; i < order.length; i++) {
|
|
2400
|
+
const layer = layers[order[i]];
|
|
2401
|
+
if (layer && key in layer) return layer;
|
|
2151
2402
|
}
|
|
2152
|
-
return
|
|
2403
|
+
return null;
|
|
2404
|
+
}
|
|
2405
|
+
/**
|
|
2406
|
+
* What the projection with the given resolution order reads for `attr`. A path resolves its first
|
|
2407
|
+
* segment through the layers and follows the rest into the value, stopping with `undefined` at the
|
|
2408
|
+
* first missing link.
|
|
2409
|
+
*/
|
|
2410
|
+
function resolveAttr(attr, layers, order) {
|
|
2411
|
+
const key = typeof attr === "string" ? attr : attr[0];
|
|
2412
|
+
const layer = layerHolding(key, layers, order);
|
|
2413
|
+
if (!layer) return void 0;
|
|
2414
|
+
return typeof attr === "string" ? layer[key] : valueAtPath(layer[key], attr);
|
|
2415
|
+
}
|
|
2416
|
+
/** Follow `path` (from its second segment) into `base`, stopping with `undefined` at the first missing link or `null`. */
|
|
2417
|
+
function valueAtPath(base, path) {
|
|
2418
|
+
let current = base;
|
|
2419
|
+
for (let i = 1; i < path.length; i++) {
|
|
2420
|
+
if (current === void 0 || current === null) return void 0;
|
|
2421
|
+
current = current[path[i]];
|
|
2422
|
+
}
|
|
2423
|
+
return current;
|
|
2424
|
+
}
|
|
2425
|
+
/**
|
|
2426
|
+
* Whether two values of `field` count as the same value for change detection.
|
|
2427
|
+
*
|
|
2428
|
+
* A `schema-object`, and each element of a `schema-array`, compares by the identity hash its
|
|
2429
|
+
* `ObjectSchema` declares (`identity: { kind: '@hash', ... }`), so the schema decides what "same"
|
|
2430
|
+
* means for it. With no hash declared, and for every other field kind, only the same reference
|
|
2431
|
+
* counts. That knowingly over-notifies for equal-content objects: content equality is the schema's
|
|
2432
|
+
* to define, not the cache's to guess.
|
|
2433
|
+
*/
|
|
2434
|
+
function attrValuesEqual(schema, field, a, b) {
|
|
2435
|
+
if (a === b) return true;
|
|
2436
|
+
if (field.kind === "schema-object") return schemaObjectsEqual(schema, field, a, b);
|
|
2437
|
+
if (field.kind === "schema-array") return schemaArraysEqual(schema, field, a, b);
|
|
2438
|
+
return false;
|
|
2439
|
+
}
|
|
2440
|
+
/**
|
|
2441
|
+
* A schema-array has no hash of its own: two arrays are the same when they have the same length
|
|
2442
|
+
* and every element compares equal as a schema-object of the element type.
|
|
2443
|
+
*/
|
|
2444
|
+
function schemaArraysEqual(schema, field, a, b) {
|
|
2445
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
|
|
2446
|
+
for (let i = 0; i < a.length; i++) if (!schemaObjectsEqual(schema, field, a[i], b[i])) return false;
|
|
2447
|
+
return true;
|
|
2448
|
+
}
|
|
2449
|
+
function schemaObjectsEqual(schema, field, a, b) {
|
|
2450
|
+
if (a === b) return true;
|
|
2451
|
+
if (!a || !b || typeof a !== "object" || typeof b !== "object") return false;
|
|
2452
|
+
const ia = fieldValueIdentity(schema, field, a);
|
|
2453
|
+
const ib = fieldValueIdentity(schema, field, b);
|
|
2454
|
+
return ia !== null && ib !== null && ia.type === ib.type && ia.hash !== null && ia.hash === ib.hash;
|
|
2455
|
+
}
|
|
2456
|
+
/**
|
|
2457
|
+
* Whether a nested edit's local clone is back to the baseline, so the edit can end. The schema
|
|
2458
|
+
* decides first, through {@link attrValuesEqual}: a schema-object with an identity hash matches
|
|
2459
|
+
* when its hash does. A value with no schema-defined equality falls back to comparing serialized
|
|
2460
|
+
* content, as this path always has: the clone began as a copy of the baseline, so key order
|
|
2461
|
+
* agrees while edits only replace existing keys. Anything that cannot serialize counts as still
|
|
2462
|
+
* edited, so no edit the cache cannot understand is thrown away.
|
|
2463
|
+
*/
|
|
2464
|
+
function localCloneMatchesBaseline(schema, field, baseline, clone) {
|
|
2465
|
+
if (field && attrValuesEqual(schema, field, baseline, clone)) return true;
|
|
2466
|
+
if (!baseline || !clone) return false;
|
|
2467
|
+
try {
|
|
2468
|
+
return JSON.stringify(baseline) === JSON.stringify(clone);
|
|
2469
|
+
} catch {
|
|
2470
|
+
return false;
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
/**
|
|
2474
|
+
* Which projection each changed key moved in, so the caller can notify on the matching channel:
|
|
2475
|
+
* `localOnly` and `remoteOnly` on theirs, `both` unscoped.
|
|
2476
|
+
*
|
|
2477
|
+
* `undefined` rather than empty Sets: this runs on every `upsert`, and a resource where nothing
|
|
2478
|
+
* moved should cost no allocation.
|
|
2479
|
+
*/
|
|
2480
|
+
const NO_PROJECTION_CHANGES = Object.freeze({
|
|
2481
|
+
localOnly: void 0,
|
|
2482
|
+
remoteOnly: void 0,
|
|
2483
|
+
both: void 0
|
|
2484
|
+
});
|
|
2485
|
+
/** The layers a merge reads: the resource's own, plus the attributes arriving from the server. */
|
|
2486
|
+
function layersForMerge(cached, incomingAttrs) {
|
|
2487
|
+
return {
|
|
2488
|
+
localAttrs: cached.localAttrs,
|
|
2489
|
+
inflightAttrs: cached.inflightAttrs,
|
|
2490
|
+
remoteAttrs: cached.remoteAttrs,
|
|
2491
|
+
defaultAttrs: cached.defaultAttrs,
|
|
2492
|
+
incomingAttrs
|
|
2493
|
+
};
|
|
2494
|
+
}
|
|
2495
|
+
/**
|
|
2496
|
+
* Every key a merge could move: every key in every layer the after-merge order folds into
|
|
2497
|
+
* `remoteAttrs`. `null` when there is nothing to examine.
|
|
2498
|
+
*/
|
|
2499
|
+
function candidateKeys(layers, remoteAfterOrder) {
|
|
2500
|
+
let keys = null;
|
|
2501
|
+
for (let i = 0; i < remoteAfterOrder.length; i++) {
|
|
2502
|
+
const layer = remoteAfterOrder[i];
|
|
2503
|
+
if (layer === "remoteAttrs") break;
|
|
2504
|
+
const hash = layers[layer];
|
|
2505
|
+
if (!hash) continue;
|
|
2506
|
+
const layerKeys = Object.keys(hash);
|
|
2507
|
+
if (!layerKeys.length) continue;
|
|
2508
|
+
if (keys === null) {
|
|
2509
|
+
keys = layerKeys;
|
|
2510
|
+
continue;
|
|
2511
|
+
}
|
|
2512
|
+
for (let j = 0; j < layerKeys.length; j++) if (!keys.includes(layerKeys[j])) keys.push(layerKeys[j]);
|
|
2513
|
+
}
|
|
2514
|
+
return keys;
|
|
2515
|
+
}
|
|
2516
|
+
/**
|
|
2517
|
+
* Fold every layer above `remoteAttrs` in the after-merge order for `kind` into `remoteAttrs`,
|
|
2518
|
+
* lowest precedence first, and clear each resource layer that was folded in. The same order
|
|
2519
|
+
* {@link partitionChangedKeys} used to predict the result, so the two cannot disagree.
|
|
2520
|
+
*
|
|
2521
|
+
* Reads the resource layers off `cached` rather than taking a {@link Layered}, so an upsert that
|
|
2522
|
+
* is not calculating changes allocates nothing beyond the merge itself.
|
|
2523
|
+
*/
|
|
2524
|
+
function mergeIntoRemote(cached, incomingAttrs, kind) {
|
|
2525
|
+
const folded = MERGE_RESOLUTION[kind].folded;
|
|
2526
|
+
const target = cached.remoteAttrs || Object.create(null);
|
|
2527
|
+
for (let i = 0; i < folded.length; i++) {
|
|
2528
|
+
const layer = folded[i];
|
|
2529
|
+
const hash = layer === "incomingAttrs" ? incomingAttrs : cached[layer];
|
|
2530
|
+
if (!hash) continue;
|
|
2531
|
+
Object.assign(target, hash);
|
|
2532
|
+
if (layer !== "incomingAttrs") cached[layer] = null;
|
|
2533
|
+
if (cached.defaultAttrs) dropMemoizedDefaults(cached.defaultAttrs, hash);
|
|
2534
|
+
}
|
|
2535
|
+
cached.remoteAttrs = target;
|
|
2536
|
+
}
|
|
2537
|
+
function dropMemoizedDefaults(defaultAttrs, replacedBy) {
|
|
2538
|
+
const keys = Object.keys(replacedBy);
|
|
2539
|
+
for (let i = 0; i < keys.length; i++) if (keys[i] in defaultAttrs) delete defaultAttrs[keys[i]];
|
|
2540
|
+
}
|
|
2541
|
+
/**
|
|
2542
|
+
* Partition the keys a merge touches by which projection each one moves in. Must run *before*
|
|
2543
|
+
* {@link mergeIntoRemote}: it reads the pre-merge layers from `layers` and predicts the merge with
|
|
2544
|
+
* the same `RESOLUTION_ORDER_*_AFTER_*` orders `mergeIntoRemote` applies.
|
|
2545
|
+
*/
|
|
2546
|
+
function partitionChangedKeys(schema, layers, fields, kind) {
|
|
2547
|
+
const { remoteAfter: remoteAfterOrder, localAfter: localAfterOrder } = MERGE_RESOLUTION[kind];
|
|
2548
|
+
const keys = candidateKeys(layers, remoteAfterOrder);
|
|
2549
|
+
if (keys === null) return NO_PROJECTION_CHANGES;
|
|
2550
|
+
let localOnly;
|
|
2551
|
+
let remoteOnly;
|
|
2552
|
+
let both;
|
|
2553
|
+
for (let i = 0; i < keys.length; i++) {
|
|
2554
|
+
const key = keys[i];
|
|
2555
|
+
const field = fields.get(key);
|
|
2556
|
+
if (!field || isRelationship(field)) continue;
|
|
2557
|
+
const remoteBefore = resolveAttr(key, layers, RESOLUTION_ORDER_REMOTE_STATE);
|
|
2558
|
+
const remoteAfter = resolveAttr(key, layers, remoteAfterOrder);
|
|
2559
|
+
const localBefore = resolveAttr(key, layers, RESOLUTION_ORDER_LOCAL_STATE);
|
|
2560
|
+
const localAfter = resolveAttr(key, layers, localAfterOrder);
|
|
2561
|
+
const remoteMoved = !attrValuesEqual(schema, field, remoteBefore, remoteAfter);
|
|
2562
|
+
const localMoved = !attrValuesEqual(schema, field, localBefore, localAfter);
|
|
2563
|
+
if (remoteMoved && localMoved) (both ??= /* @__PURE__ */ new Set()).add(key);
|
|
2564
|
+
else if (remoteMoved) (remoteOnly ??= /* @__PURE__ */ new Set()).add(key);
|
|
2565
|
+
else if (localMoved) (localOnly ??= /* @__PURE__ */ new Set()).add(key);
|
|
2566
|
+
}
|
|
2567
|
+
return both || remoteOnly || localOnly ? {
|
|
2568
|
+
localOnly,
|
|
2569
|
+
remoteOnly,
|
|
2570
|
+
both
|
|
2571
|
+
} : NO_PROJECTION_CHANGES;
|
|
2572
|
+
}
|
|
2573
|
+
function notifyProjectionChanges(cache, identifier, { both, remoteOnly, localOnly }) {
|
|
2574
|
+
if (both?.size) cache._capabilities.notifyChange(identifier, "attributes", both);
|
|
2575
|
+
if (remoteOnly?.size) cache._capabilities.notifyChange(identifier, "attributes", remoteOnly, "remote");
|
|
2576
|
+
if (localOnly?.size) cache._capabilities.notifyChange(identifier, "attributes", localOnly, "local");
|
|
2153
2577
|
}
|
|
2154
2578
|
function cacheIsEmpty(cached) {
|
|
2155
2579
|
return !cached || cached.remoteAttrs === null && cached.inflightAttrs === null && cached.localAttrs === null;
|
|
@@ -2190,25 +2614,26 @@ function isRelationship(field) {
|
|
|
2190
2614
|
const { kind } = field;
|
|
2191
2615
|
return kind === "hasMany" || kind === "belongsTo" || kind === "resource" || kind === "collection";
|
|
2192
2616
|
}
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
}
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2617
|
+
/**
|
|
2618
|
+
* After a merge: drop every local edit the new baseline agrees with, by the same equality
|
|
2619
|
+
* {@link partitionChangedKeys} uses. Returns whether any edit was dropped.
|
|
2620
|
+
*/
|
|
2621
|
+
function reconcileLocalEdits(schema, cached, fields) {
|
|
2622
|
+
const { localAttrs, defaultAttrs } = cached;
|
|
2623
|
+
if (!localAttrs) return false;
|
|
2624
|
+
let droppedAnEdit = false;
|
|
2625
|
+
const editedKeys = Object.keys(localAttrs);
|
|
2626
|
+
for (let i = 0; i < editedKeys.length; i++) {
|
|
2627
|
+
const key = editedKeys[i];
|
|
2628
|
+
const field = fields.get(key);
|
|
2629
|
+
const baseline = resolveAttr(key, cached, RESOLUTION_ORDER_EDIT_BASELINE);
|
|
2630
|
+
if (field ? attrValuesEqual(schema, field, baseline, localAttrs[key]) : baseline === localAttrs[key]) {
|
|
2631
|
+
droppedAnEdit = true;
|
|
2632
|
+
delete localAttrs[key];
|
|
2208
2633
|
}
|
|
2209
|
-
if (defaultAttrs &&
|
|
2634
|
+
if (defaultAttrs && key in defaultAttrs) delete defaultAttrs[key];
|
|
2210
2635
|
}
|
|
2211
|
-
return
|
|
2636
|
+
return droppedAnEdit;
|
|
2212
2637
|
}
|
|
2213
2638
|
function putOne(cache, identifiers, resource) {
|
|
2214
2639
|
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) && ((test) => {
|
|
@@ -2300,7 +2725,7 @@ function copyLinksAndMeta(target, source) {
|
|
|
2300
2725
|
if ("meta" in source) target.meta = source.meta;
|
|
2301
2726
|
}
|
|
2302
2727
|
function cacheUpsert(cache, identifier, data, calculateChanges) {
|
|
2303
|
-
let
|
|
2728
|
+
let changes = NO_PROJECTION_CHANGES;
|
|
2304
2729
|
const peeked = cache.__safePeek(identifier, false);
|
|
2305
2730
|
const existed = !!peeked;
|
|
2306
2731
|
const cached = peeked || cache._createCache(identifier);
|
|
@@ -2323,19 +2748,29 @@ function cacheUpsert(cache, identifier, data, calculateChanges) {
|
|
|
2323
2748
|
cache._capabilities.notifyChange(identifier, "state", null);
|
|
2324
2749
|
}
|
|
2325
2750
|
const fields = getCacheFields(cache, identifier);
|
|
2326
|
-
if (calculateChanges && existed && data.attributes)
|
|
2327
|
-
cached
|
|
2751
|
+
if (calculateChanges && existed && data.attributes) changes = partitionChangedKeys(cache._capabilities.schema, layersForMerge(cached, data.attributes), fields, "upsert");
|
|
2752
|
+
mergeIntoRemote(cached, data.attributes ?? null, "upsert");
|
|
2328
2753
|
if (cached.localAttrs) {
|
|
2329
|
-
if (
|
|
2754
|
+
if (reconcileLocalEdits(cache._capabilities.schema, cached, fields)) cache._capabilities.notifyChange(identifier, "state", null);
|
|
2330
2755
|
}
|
|
2331
2756
|
if (!isUpdate) cache._capabilities.notifyChange(identifier, "added", null);
|
|
2332
2757
|
if (data.id) cached.id = data.id;
|
|
2333
2758
|
if (data.relationships) setupRelationships(cache.__graph, fields, identifier, data);
|
|
2334
|
-
|
|
2759
|
+
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) && ((test) => {
|
|
2760
|
+
if (!test) throw new Error("An upsert merges nothing into remote state, so no key can move in the local projection alone");
|
|
2761
|
+
})(!changes.localOnly?.size);
|
|
2762
|
+
notifyProjectionChanges(cache, identifier, changes);
|
|
2335
2763
|
if (macroCondition(getGlobalConfig().WarpDriveMirror.activeLogging.LOG_CACHE)) {
|
|
2336
2764
|
if (getGlobalConfig().WarpDriveMirror.debug.LOG_CACHE || globalThis.getWarpDriveRuntimeConfig().debug.LOG_CACHE) console.groupEnd();
|
|
2337
2765
|
}
|
|
2338
|
-
return
|
|
2766
|
+
return remoteChangedKeys(changes);
|
|
2767
|
+
}
|
|
2768
|
+
/** Every key whose remote value moved, as the `upsert` return value. */
|
|
2769
|
+
function remoteChangedKeys({ both, remoteOnly }) {
|
|
2770
|
+
if (!both?.size) return remoteOnly?.size ? Array.from(remoteOnly) : void 0;
|
|
2771
|
+
const keys = Array.from(both);
|
|
2772
|
+
if (remoteOnly?.size) keys.push(...remoteOnly);
|
|
2773
|
+
return keys;
|
|
2339
2774
|
}
|
|
2340
2775
|
function patchCache(Cache, op) {
|
|
2341
2776
|
const isRecord = isResourceKey(op.record);
|
|
@@ -2438,10 +2873,11 @@ function commitDidError(cache, identifier, errors) {
|
|
|
2438
2873
|
const keys = Object.keys(cached.inflightAttrs);
|
|
2439
2874
|
if (keys.length > 0) {
|
|
2440
2875
|
const attrs = cached.localAttrs = cached.localAttrs || Object.create(null);
|
|
2441
|
-
for (let i = 0; i < keys.length; i++) if (
|
|
2876
|
+
for (let i = 0; i < keys.length; i++) if (!(keys[i] in attrs)) attrs[keys[i]] = cached.inflightAttrs[keys[i]];
|
|
2442
2877
|
}
|
|
2443
2878
|
cached.inflightAttrs = null;
|
|
2444
2879
|
}
|
|
2880
|
+
if (reconcileLocalEdits(cache._capabilities.schema, cached, getCacheFields(cache, identifier))) cache._capabilities.notifyChange(identifier, "state", null);
|
|
2445
2881
|
if (errors) cached.errors = errors;
|
|
2446
2882
|
cache._capabilities.notifyChange(identifier, "errors", null);
|
|
2447
2883
|
}
|
|
@@ -2473,7 +2909,7 @@ function didCommit(cache, committedIdentifier, data, op) {
|
|
|
2473
2909
|
}
|
|
2474
2910
|
const fields = getCacheFields(cache, identifier);
|
|
2475
2911
|
cached.isNew = false;
|
|
2476
|
-
let
|
|
2912
|
+
let responseAttrs = null;
|
|
2477
2913
|
if (data) {
|
|
2478
2914
|
if (data.id && !cached.id) cached.id = data.id;
|
|
2479
2915
|
if (identifier === committedIdentifier && identifier.id !== existingId) cache._capabilities.notifyChange(identifier, "identity", null);
|
|
@@ -2501,17 +2937,16 @@ function didCommit(cache, committedIdentifier, data, op) {
|
|
|
2501
2937
|
}
|
|
2502
2938
|
setupRelationships(cache.__graph, fields, identifier, data);
|
|
2503
2939
|
}
|
|
2504
|
-
|
|
2940
|
+
responseAttrs = data.attributes ?? null;
|
|
2505
2941
|
}
|
|
2506
|
-
const
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
patchLocalAttributes(cached, changedKeys);
|
|
2942
|
+
const changes = partitionChangedKeys(cache._capabilities.schema, layersForMerge(cached, responseAttrs), fields, "commit");
|
|
2943
|
+
mergeIntoRemote(cached, responseAttrs, "commit");
|
|
2944
|
+
reconcileLocalEdits(cache._capabilities.schema, cached, fields);
|
|
2510
2945
|
if (cached.errors) {
|
|
2511
2946
|
cached.errors = null;
|
|
2512
2947
|
cache._capabilities.notifyChange(identifier, "errors", null);
|
|
2513
2948
|
}
|
|
2514
|
-
|
|
2949
|
+
notifyProjectionChanges(cache, identifier, changes);
|
|
2515
2950
|
cache._capabilities.notifyChange(identifier, "state", null);
|
|
2516
2951
|
}
|
|
2517
2952
|
function willCommit(cache, identifier) {
|