@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/unpkg/prod/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 } from "@warp-drive-mirror/core/store/-private";
|
|
2
|
+
import { assertPrivateCapabilities, fieldValueIdentity, isRequestKey, isResourceKey } from "@warp-drive-mirror/core/store/-private";
|
|
3
3
|
import "fuse.js";
|
|
4
4
|
import "json-to-ast";
|
|
5
5
|
|
|
@@ -24,14 +24,101 @@ const EMPTY_ITERATOR = { iterator() {
|
|
|
24
24
|
};
|
|
25
25
|
} };
|
|
26
26
|
} };
|
|
27
|
+
/** One attributes hash on a {@link CachedResource}: a **layer** a projection is read through. */
|
|
28
|
+
/**
|
|
29
|
+
* The cache's entry for a single resource: its id, its attribute values split
|
|
30
|
+
* across four **layers**, and the flags tracking where it sits in the
|
|
31
|
+
* create/update/delete lifecycle.
|
|
32
|
+
*
|
|
33
|
+
* A **projection** answers "what is this field's value" by reading a stack of
|
|
34
|
+
* layers top-down; the first layer holding the field wins. The two projections,
|
|
35
|
+
* with their resolution orders:
|
|
36
|
+
*
|
|
37
|
+
* - **remote state**, what the _immutable_ record reads:
|
|
38
|
+
* {@link RESOLUTION_ORDER_REMOTE_STATE}
|
|
39
|
+
* - **local state**, what an _editable_ copy reads:
|
|
40
|
+
* {@link RESOLUTION_ORDER_LOCAL_STATE}
|
|
41
|
+
*
|
|
42
|
+
* Local state is remote state with the uncommitted mutations laid over it. The
|
|
43
|
+
* guides call those mutations "the diff"; here they are simply the top layers of
|
|
44
|
+
* local state, {@link CachedResource.localAttrs | localAttrs} and
|
|
45
|
+
* {@link CachedResource.inflightAttrs | inflightAttrs}.
|
|
46
|
+
*
|
|
47
|
+
* A field is **dirty** while `localAttrs` or `inflightAttrs` holds it. A save
|
|
48
|
+
* **commits** the mutation: `didCommit` merges the in-flight values into
|
|
49
|
+
* `remoteAttrs` and removes them from `inflightAttrs`. A push carrying the same
|
|
50
|
+
* value as a pending edit in `localAttrs` has the same effect on that edit: the
|
|
51
|
+
* server already holds it, so it is removed from `localAttrs`. "Same value" is
|
|
52
|
+
* decided by the field's schema: a schema-object with an identity hash compares
|
|
53
|
+
* by that hash, everything else by reference.
|
|
54
|
+
*
|
|
55
|
+
* Thus, a dirty field reads as mutated for local readers only, and goes on doing
|
|
56
|
+
* so while its save is in flight.
|
|
57
|
+
*
|
|
58
|
+
* @internal
|
|
59
|
+
*/
|
|
60
|
+
/** A layer name on a {@link CachedResource}. */
|
|
61
|
+
/**
|
|
62
|
+
* A layer that exists only for the duration of a merge: the attributes arriving from the
|
|
63
|
+
* server in a save response or a push. {@link partitionChangedKeys} reads it as if it were
|
|
64
|
+
* already on the resource; {@link mergeIntoRemote} then folds the same attributes in, handed
|
|
65
|
+
* to it directly. Never part of a reader's resolution order.
|
|
66
|
+
*/
|
|
67
|
+
/** Anything a projection can be read through: a {@link CachedResource}, or the layers a merge reads, built around one. */
|
|
68
|
+
/** What the immutable record reads. */
|
|
69
|
+
const RESOLUTION_ORDER_REMOTE_STATE = ["remoteAttrs", "defaultAttrs"];
|
|
70
|
+
/**
|
|
71
|
+
* What a new local edit replaces: the in-flight value if a save is carrying one, else the
|
|
72
|
+
* persisted value. Put another way, what the record will read once the in-flight save lands,
|
|
73
|
+
* assuming the server agrees. This is local state beneath `localAttrs`, less `defaultAttrs`: an
|
|
74
|
+
* edit equal to a schema default is still an edit, since nothing persisted holds that value.
|
|
75
|
+
*
|
|
76
|
+
* Saving does not use this baseline. {@link JSONAPICache.changedAttrs | changedAttrs} still lists
|
|
77
|
+
* in-flight values as unsaved, so a second save re-sends them rather than assuming the first one
|
|
78
|
+
* will succeed.
|
|
79
|
+
*/
|
|
80
|
+
const RESOLUTION_ORDER_EDIT_BASELINE = ["inflightAttrs", "remoteAttrs"];
|
|
81
|
+
/** What an editable copy reads. */
|
|
82
|
+
const RESOLUTION_ORDER_LOCAL_STATE = [
|
|
83
|
+
"localAttrs",
|
|
84
|
+
...RESOLUTION_ORDER_EDIT_BASELINE,
|
|
85
|
+
"defaultAttrs"
|
|
86
|
+
];
|
|
87
|
+
const RESOLUTION_ORDER_REMOTE_AFTER_COMMIT = [
|
|
88
|
+
"incomingAttrs",
|
|
89
|
+
"inflightAttrs",
|
|
90
|
+
...RESOLUTION_ORDER_REMOTE_STATE
|
|
91
|
+
];
|
|
92
|
+
const RESOLUTION_ORDER_LOCAL_AFTER_COMMIT = ["localAttrs", ...RESOLUTION_ORDER_REMOTE_AFTER_COMMIT];
|
|
93
|
+
const RESOLUTION_ORDER_REMOTE_AFTER_UPSERT = ["incomingAttrs", ...RESOLUTION_ORDER_REMOTE_STATE];
|
|
94
|
+
const RESOLUTION_ORDER_LOCAL_AFTER_UPSERT = [
|
|
95
|
+
"localAttrs",
|
|
96
|
+
"inflightAttrs",
|
|
97
|
+
...RESOLUTION_ORDER_REMOTE_AFTER_UPSERT
|
|
98
|
+
];
|
|
99
|
+
/** The layers an after-merge order folds into `remoteAttrs`, lowest precedence first. */
|
|
100
|
+
function layersFoldedIntoRemote(order) {
|
|
101
|
+
return order.slice(0, order.indexOf("remoteAttrs")).reverse();
|
|
102
|
+
}
|
|
103
|
+
const MERGE_RESOLUTION = {
|
|
104
|
+
commit: {
|
|
105
|
+
remoteAfter: RESOLUTION_ORDER_REMOTE_AFTER_COMMIT,
|
|
106
|
+
localAfter: RESOLUTION_ORDER_LOCAL_AFTER_COMMIT,
|
|
107
|
+
folded: layersFoldedIntoRemote(RESOLUTION_ORDER_REMOTE_AFTER_COMMIT)
|
|
108
|
+
},
|
|
109
|
+
upsert: {
|
|
110
|
+
remoteAfter: RESOLUTION_ORDER_REMOTE_AFTER_UPSERT,
|
|
111
|
+
localAfter: RESOLUTION_ORDER_LOCAL_AFTER_UPSERT,
|
|
112
|
+
folded: layersFoldedIntoRemote(RESOLUTION_ORDER_REMOTE_AFTER_UPSERT)
|
|
113
|
+
}
|
|
114
|
+
};
|
|
27
115
|
function makeCache() {
|
|
28
116
|
return {
|
|
29
117
|
id: null,
|
|
30
|
-
remoteAttrs: null,
|
|
31
118
|
localAttrs: null,
|
|
32
|
-
defaultAttrs: null,
|
|
33
119
|
inflightAttrs: null,
|
|
34
|
-
|
|
120
|
+
remoteAttrs: null,
|
|
121
|
+
defaultAttrs: null,
|
|
35
122
|
errors: null,
|
|
36
123
|
isNew: false,
|
|
37
124
|
isDeleted: false,
|
|
@@ -82,9 +169,9 @@ var JSONAPICache = class {
|
|
|
82
169
|
/**
|
|
83
170
|
* Cache the response to a request
|
|
84
171
|
*
|
|
85
|
-
* Implements
|
|
172
|
+
* Implements {@link Cache.put | Cache.put}.
|
|
86
173
|
*
|
|
87
|
-
* Expects a StructuredDocument whose `content` member is a JsonApiDocument.
|
|
174
|
+
* Expects a {@link StructuredDocument} whose `content` member is a JsonApiDocument.
|
|
88
175
|
*
|
|
89
176
|
* ```js
|
|
90
177
|
* cache.put({
|
|
@@ -168,6 +255,16 @@ var JSONAPICache = class {
|
|
|
168
255
|
* Update the "remote" or "canonical" (persisted) state of the Cache
|
|
169
256
|
* by merging new information into the existing state.
|
|
170
257
|
*
|
|
258
|
+
* @example
|
|
259
|
+
* ```ts
|
|
260
|
+
* cache.patch({
|
|
261
|
+
* op: 'update',
|
|
262
|
+
* record: identifier,
|
|
263
|
+
* field: 'name',
|
|
264
|
+
* value: 'Chris',
|
|
265
|
+
* });
|
|
266
|
+
* ```
|
|
267
|
+
*
|
|
171
268
|
* @category Cache Management
|
|
172
269
|
* @public
|
|
173
270
|
* @param op the operation or list of operations to perform
|
|
@@ -183,6 +280,16 @@ var JSONAPICache = class {
|
|
|
183
280
|
/**
|
|
184
281
|
* Update the "local" or "current" (unpersisted) state of the Cache
|
|
185
282
|
*
|
|
283
|
+
* @example
|
|
284
|
+
* ```ts
|
|
285
|
+
* cache.mutate({
|
|
286
|
+
* op: 'replaceRelatedRecord',
|
|
287
|
+
* record: identifier,
|
|
288
|
+
* field: 'author',
|
|
289
|
+
* value: authorIdentifier,
|
|
290
|
+
* });
|
|
291
|
+
* ```
|
|
292
|
+
*
|
|
186
293
|
* @category Cache Management
|
|
187
294
|
* @public
|
|
188
295
|
*/
|
|
@@ -209,8 +316,8 @@ var JSONAPICache = class {
|
|
|
209
316
|
* not require retainining connections to the Store
|
|
210
317
|
* and Cache to present data on a per-field basis.
|
|
211
318
|
*
|
|
212
|
-
* This generally takes the place of
|
|
213
|
-
* an API and may even take the place of
|
|
319
|
+
* This generally takes the place of {@link JSONAPICache.getAttr | getAttr} as
|
|
320
|
+
* an API and may even take the place of {@link JSONAPICache.getRelationship | getRelationship}
|
|
214
321
|
* depending on implementation specifics, though this
|
|
215
322
|
* latter usage is less recommended due to the advantages
|
|
216
323
|
* of the Graph handling necessary entanglements and
|
|
@@ -225,6 +332,12 @@ var JSONAPICache = class {
|
|
|
225
332
|
* the various internal WarpDrive bookkeeping fields.
|
|
226
333
|
* :::
|
|
227
334
|
*
|
|
335
|
+
* @example
|
|
336
|
+
* ```ts
|
|
337
|
+
* const resource = cache.peek(identifier);
|
|
338
|
+
* const document = cache.peek(requestKey);
|
|
339
|
+
* ```
|
|
340
|
+
*
|
|
228
341
|
* @category Cache Management
|
|
229
342
|
* @public
|
|
230
343
|
*/
|
|
@@ -264,6 +377,12 @@ var JSONAPICache = class {
|
|
|
264
377
|
/**
|
|
265
378
|
* Peek the remote resource data from the Cache.
|
|
266
379
|
*
|
|
380
|
+
* @example
|
|
381
|
+
* ```ts
|
|
382
|
+
* const resource = cache.peekRemoteState(identifier);
|
|
383
|
+
* const document = cache.peekRemoteState(requestKey);
|
|
384
|
+
* ```
|
|
385
|
+
*
|
|
267
386
|
* @category Cache Management
|
|
268
387
|
* @public
|
|
269
388
|
*/
|
|
@@ -304,9 +423,14 @@ var JSONAPICache = class {
|
|
|
304
423
|
* Peek the Cache for the existing request data associated with
|
|
305
424
|
* a cacheable request.
|
|
306
425
|
*
|
|
307
|
-
* This is effectively the reverse of
|
|
426
|
+
* This is effectively the reverse of {@link JSONAPICache.put | put} for a request in
|
|
308
427
|
* that it will return the the request, response, and content
|
|
309
|
-
* whereas
|
|
428
|
+
* whereas {@link JSONAPICache.peek | peek} will return just the `content`.
|
|
429
|
+
*
|
|
430
|
+
* @example
|
|
431
|
+
* ```ts
|
|
432
|
+
* const doc = cache.peekRequest(requestKey);
|
|
433
|
+
* ```
|
|
310
434
|
*
|
|
311
435
|
* @category Cache Management
|
|
312
436
|
* @public
|
|
@@ -317,9 +441,20 @@ var JSONAPICache = class {
|
|
|
317
441
|
/**
|
|
318
442
|
* Push resource data from a remote source into the cache for this identifier
|
|
319
443
|
*
|
|
444
|
+
* @example
|
|
445
|
+
* ```ts
|
|
446
|
+
* cache.upsert(identifier, {
|
|
447
|
+
* type: 'user',
|
|
448
|
+
* id: '1',
|
|
449
|
+
* attributes: { name: 'Chris' },
|
|
450
|
+
* });
|
|
451
|
+
* ```
|
|
452
|
+
*
|
|
320
453
|
* @category Cache Management
|
|
321
454
|
* @public
|
|
322
|
-
* @return
|
|
455
|
+
* @return when `calculateChanges` is true, the names of the attributes whose persisted value
|
|
456
|
+
* this push changed (the same keys the `'remote'` channel is notified with), or `undefined`
|
|
457
|
+
* when none did. Otherwise `void`.
|
|
323
458
|
*/
|
|
324
459
|
upsert(identifier, data, calculateChanges) {
|
|
325
460
|
assertPrivateCapabilities(this._capabilities);
|
|
@@ -365,7 +500,7 @@ var JSONAPICache = class {
|
|
|
365
500
|
*
|
|
366
501
|
* Each individual resource or document that has
|
|
367
502
|
* been mutated should be described as an individual
|
|
368
|
-
*
|
|
503
|
+
* {@link Change} entry in the returned array.
|
|
369
504
|
*
|
|
370
505
|
* A `Change` is described by an object containing up to
|
|
371
506
|
* three properties: (1) the `identifier` of the entity that
|
|
@@ -430,6 +565,11 @@ var JSONAPICache = class {
|
|
|
430
565
|
* It returns properties from options that should be set on the record during the create
|
|
431
566
|
* process. This return value behavior is deprecated.
|
|
432
567
|
*
|
|
568
|
+
* @example
|
|
569
|
+
* ```ts
|
|
570
|
+
* cache.clientDidCreate(identifier, { name: 'Chris' });
|
|
571
|
+
* ```
|
|
572
|
+
*
|
|
433
573
|
* @category Resource Lifecycle
|
|
434
574
|
* @public
|
|
435
575
|
*/
|
|
@@ -486,6 +626,11 @@ var JSONAPICache = class {
|
|
|
486
626
|
* [LIFECYCLE] Signals to the cache that a resource
|
|
487
627
|
* will be part of a save transaction.
|
|
488
628
|
*
|
|
629
|
+
* @example
|
|
630
|
+
* ```ts
|
|
631
|
+
* cache.willCommit(identifier, context);
|
|
632
|
+
* ```
|
|
633
|
+
*
|
|
489
634
|
* @category Resource Lifecycle
|
|
490
635
|
* @public
|
|
491
636
|
*/
|
|
@@ -497,6 +642,11 @@ var JSONAPICache = class {
|
|
|
497
642
|
* [LIFECYCLE] Signals to the cache that a resource
|
|
498
643
|
* was successfully updated as part of a save transaction.
|
|
499
644
|
*
|
|
645
|
+
* @example
|
|
646
|
+
* ```ts
|
|
647
|
+
* cache.didCommit(identifier, result);
|
|
648
|
+
* ```
|
|
649
|
+
*
|
|
500
650
|
* @category Resource Lifecycle
|
|
501
651
|
* @public
|
|
502
652
|
*/
|
|
@@ -525,6 +675,11 @@ var JSONAPICache = class {
|
|
|
525
675
|
* [LIFECYCLE] Signals to the cache that a resource
|
|
526
676
|
* was update via a save transaction failed.
|
|
527
677
|
*
|
|
678
|
+
* @example
|
|
679
|
+
* ```ts
|
|
680
|
+
* cache.commitWasRejected(identifier, errors);
|
|
681
|
+
* ```
|
|
682
|
+
*
|
|
528
683
|
* @category Resource Lifecycle
|
|
529
684
|
* @public
|
|
530
685
|
*/
|
|
@@ -541,6 +696,11 @@ var JSONAPICache = class {
|
|
|
541
696
|
*
|
|
542
697
|
* This method is a candidate to become a mutation
|
|
543
698
|
*
|
|
699
|
+
* @example
|
|
700
|
+
* ```ts
|
|
701
|
+
* cache.unloadRecord(identifier);
|
|
702
|
+
* ```
|
|
703
|
+
*
|
|
544
704
|
* @category Resource Lifecycle
|
|
545
705
|
* @public
|
|
546
706
|
*/
|
|
@@ -581,6 +741,12 @@ var JSONAPICache = class {
|
|
|
581
741
|
* Retrieve the data for an attribute from the cache
|
|
582
742
|
* with local mutations applied.
|
|
583
743
|
*
|
|
744
|
+
* @example
|
|
745
|
+
* ```ts
|
|
746
|
+
* const name = cache.getAttr(identifier, 'name');
|
|
747
|
+
* const zip = cache.getAttr(identifier, ['address', 'zip']);
|
|
748
|
+
* ```
|
|
749
|
+
*
|
|
584
750
|
* @category Resource Data
|
|
585
751
|
* @public
|
|
586
752
|
*/
|
|
@@ -591,37 +757,28 @@ var JSONAPICache = class {
|
|
|
591
757
|
const attribute = attr;
|
|
592
758
|
const cached = this.__peek(identifier, true);
|
|
593
759
|
if (!cached) return;
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
if (schemaHasLegacyDefaultValueFn(attrSchema)) {
|
|
603
|
-
cached.defaultAttrs = cached.defaultAttrs || Object.create(null);
|
|
604
|
-
cached.defaultAttrs[attribute] = defaultValue;
|
|
605
|
-
}
|
|
606
|
-
return defaultValue;
|
|
760
|
+
const layer = layerHolding(attribute, cached, RESOLUTION_ORDER_LOCAL_STATE);
|
|
761
|
+
if (layer) return layer[attribute];
|
|
762
|
+
const attrSchema = getCacheFields(this, identifier).get(attribute);
|
|
763
|
+
assertPrivateCapabilities(this._capabilities);
|
|
764
|
+
const defaultValue = getDefaultValue(attrSchema, identifier, this._capabilities._store);
|
|
765
|
+
if (schemaHasLegacyDefaultValueFn(attrSchema)) {
|
|
766
|
+
cached.defaultAttrs = cached.defaultAttrs || Object.create(null);
|
|
767
|
+
cached.defaultAttrs[attribute] = defaultValue;
|
|
607
768
|
}
|
|
769
|
+
return defaultValue;
|
|
608
770
|
}
|
|
609
|
-
const path = attr;
|
|
610
771
|
const cached = this.__peek(identifier, true);
|
|
611
|
-
|
|
612
|
-
let current = cached.localAttrs && basePath in cached.localAttrs ? cached.localAttrs[basePath] : void 0;
|
|
613
|
-
if (current === void 0) current = cached.inflightAttrs && basePath in cached.inflightAttrs ? cached.inflightAttrs[basePath] : void 0;
|
|
614
|
-
if (current === void 0) current = cached.remoteAttrs && basePath in cached.remoteAttrs ? cached.remoteAttrs[basePath] : void 0;
|
|
615
|
-
if (current === void 0) return;
|
|
616
|
-
for (let i = 1; i < path.length; i++) {
|
|
617
|
-
current = current[path[i]];
|
|
618
|
-
if (current === void 0) return;
|
|
619
|
-
}
|
|
620
|
-
return current;
|
|
772
|
+
return resolveAttr(attr, cached, RESOLUTION_ORDER_LOCAL_STATE);
|
|
621
773
|
}
|
|
622
774
|
/**
|
|
623
775
|
* Retrieve the remote data for an attribute from the cache
|
|
624
776
|
*
|
|
777
|
+
* @example
|
|
778
|
+
* ```ts
|
|
779
|
+
* const name = cache.getRemoteAttr(identifier, 'name');
|
|
780
|
+
* ```
|
|
781
|
+
*
|
|
625
782
|
* @category Resource Data
|
|
626
783
|
* @public
|
|
627
784
|
*/
|
|
@@ -632,54 +789,44 @@ var JSONAPICache = class {
|
|
|
632
789
|
const attribute = attr;
|
|
633
790
|
const cached = this.__peek(identifier, true);
|
|
634
791
|
if (!cached) return;
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
cached.defaultAttrs[attribute] = defaultValue;
|
|
644
|
-
}
|
|
645
|
-
return defaultValue;
|
|
792
|
+
const layer = layerHolding(attribute, cached, RESOLUTION_ORDER_REMOTE_STATE);
|
|
793
|
+
if (layer) return layer[attribute];
|
|
794
|
+
const attrSchema = getCacheFields(this, identifier).get(attribute);
|
|
795
|
+
assertPrivateCapabilities(this._capabilities);
|
|
796
|
+
const defaultValue = getDefaultValue(attrSchema, identifier, this._capabilities._store);
|
|
797
|
+
if (schemaHasLegacyDefaultValueFn(attrSchema)) {
|
|
798
|
+
cached.defaultAttrs = cached.defaultAttrs || Object.create(null);
|
|
799
|
+
cached.defaultAttrs[attribute] = defaultValue;
|
|
646
800
|
}
|
|
801
|
+
return defaultValue;
|
|
647
802
|
}
|
|
648
|
-
const path = attr;
|
|
649
803
|
const cached = this.__peek(identifier, true);
|
|
650
|
-
|
|
651
|
-
let current = cached.remoteAttrs && basePath in cached.remoteAttrs ? cached.remoteAttrs[basePath] : void 0;
|
|
652
|
-
if (current === void 0) return;
|
|
653
|
-
for (let i = 1; i < path.length; i++) {
|
|
654
|
-
current = current[path[i]];
|
|
655
|
-
if (current === void 0) return;
|
|
656
|
-
}
|
|
657
|
-
return current;
|
|
804
|
+
return resolveAttr(attr, cached, RESOLUTION_ORDER_REMOTE_STATE);
|
|
658
805
|
}
|
|
659
806
|
/**
|
|
660
807
|
* Mutate the data for an attribute in the cache
|
|
661
808
|
*
|
|
662
809
|
* This method is a candidate to become a mutation
|
|
663
810
|
*
|
|
811
|
+
* @example
|
|
812
|
+
* ```ts
|
|
813
|
+
* cache.setAttr(identifier, 'name', 'Chris');
|
|
814
|
+
* ```
|
|
815
|
+
*
|
|
664
816
|
* @category Resource Data
|
|
665
817
|
* @public
|
|
666
818
|
*/
|
|
667
819
|
setAttr(identifier, attr, value) {
|
|
820
|
+
if (value === void 0) value = null;
|
|
668
821
|
const isSimplePath = !Array.isArray(attr) || attr.length === 1;
|
|
669
822
|
if (Array.isArray(attr) && attr.length === 1) attr = attr[0];
|
|
670
823
|
if (isSimplePath) {
|
|
671
824
|
const cached = this.__peek(identifier, false);
|
|
672
825
|
const currentAttr = attr;
|
|
673
|
-
|
|
674
|
-
if (existing !== value) {
|
|
826
|
+
if (resolveAttr(currentAttr, cached, RESOLUTION_ORDER_EDIT_BASELINE) !== value) {
|
|
675
827
|
cached.localAttrs = cached.localAttrs || Object.create(null);
|
|
676
828
|
cached.localAttrs[currentAttr] = value;
|
|
677
|
-
|
|
678
|
-
cached.changes[currentAttr] = [existing, value];
|
|
679
|
-
} else if (cached.localAttrs) {
|
|
680
|
-
delete cached.localAttrs[currentAttr];
|
|
681
|
-
delete cached.changes[currentAttr];
|
|
682
|
-
}
|
|
829
|
+
} else if (cached.localAttrs) delete cached.localAttrs[currentAttr];
|
|
683
830
|
if (cached.defaultAttrs && currentAttr in cached.defaultAttrs) delete cached.defaultAttrs[currentAttr];
|
|
684
831
|
this._capabilities.notifyChange(identifier, "attributes", currentAttr, "local");
|
|
685
832
|
return;
|
|
@@ -687,32 +834,47 @@ var JSONAPICache = class {
|
|
|
687
834
|
const path = attr;
|
|
688
835
|
const cached = this.__peek(identifier, false);
|
|
689
836
|
const basePath = path[0];
|
|
690
|
-
const
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
837
|
+
const baseline = resolveAttr(basePath, cached, RESOLUTION_ORDER_EDIT_BASELINE);
|
|
838
|
+
const isRevert = valueAtPath(baseline, path) === value;
|
|
839
|
+
const hasLocalClone = !!cached.localAttrs && basePath in cached.localAttrs;
|
|
840
|
+
if (isRevert && !hasLocalClone) return;
|
|
841
|
+
cached.localAttrs = cached.localAttrs || Object.create(null);
|
|
842
|
+
if (!hasLocalClone) {
|
|
843
|
+
const seed = baseline ?? (cached.defaultAttrs ? cached.defaultAttrs[basePath] : void 0);
|
|
844
|
+
cached.localAttrs[basePath] = structuredClone(seed);
|
|
695
845
|
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
if (!existing) return;
|
|
707
|
-
if (JSON.stringify(existing) !== JSON.stringify(cached.localAttrs[basePath])) {
|
|
708
|
-
delete cached.localAttrs[basePath];
|
|
709
|
-
delete cached.changes[basePath];
|
|
710
|
-
}
|
|
711
|
-
} catch {}
|
|
846
|
+
let currentLocal = cached.localAttrs[basePath];
|
|
847
|
+
let nextLink = 1;
|
|
848
|
+
while (nextLink < path.length - 1) currentLocal = currentLocal[path[nextLink++]];
|
|
849
|
+
if (currentLocal[path[nextLink]] === value) return;
|
|
850
|
+
currentLocal[path[nextLink]] = value;
|
|
851
|
+
if (isRevert) {
|
|
852
|
+
const field = getCacheFields(this, identifier).get(basePath);
|
|
853
|
+
if (localCloneMatchesBaseline(this._capabilities.schema, field, baseline, cached.localAttrs[basePath])) delete cached.localAttrs[basePath];
|
|
854
|
+
}
|
|
855
|
+
if (cached.defaultAttrs && basePath in cached.defaultAttrs) delete cached.defaultAttrs[basePath];
|
|
712
856
|
this._capabilities.notifyChange(identifier, "attributes", basePath, "local");
|
|
713
857
|
}
|
|
714
858
|
/**
|
|
715
|
-
* Query the cache for the changed attributes of a resource
|
|
859
|
+
* Query the cache for the changed attributes of a resource: every unsaved
|
|
860
|
+
* mutation, as a `[before, after]` pair per field.
|
|
861
|
+
*
|
|
862
|
+
* `before` is the value the mutation replaces, which is not always the
|
|
863
|
+
* persisted one. A mutation a save is carrying replaces remote state; an edit
|
|
864
|
+
* made while that save is in flight replaces the in-flight value. So this is
|
|
865
|
+
* what saving from here would change, which is what `serializePatch` and the
|
|
866
|
+
* legacy `Snapshot` consume, rather than a diff against persisted state.
|
|
867
|
+
*
|
|
868
|
+
* Derived from the layers on each call, so it is always consistent with
|
|
869
|
+
* {@link JSONAPICache.getAttr | getAttr} and
|
|
870
|
+
* {@link JSONAPICache.rollbackAttrs | rollbackAttrs}. Dirtiness does not go
|
|
871
|
+
* through here; see {@link JSONAPICache.hasChangedAttrs | hasChangedAttrs}.
|
|
872
|
+
*
|
|
873
|
+
* @example
|
|
874
|
+
* ```ts
|
|
875
|
+
* const changes = cache.changedAttrs(identifier);
|
|
876
|
+
* // { name: ['Igor', 'Chris'] }
|
|
877
|
+
* ```
|
|
716
878
|
*
|
|
717
879
|
* @category Resource Data
|
|
718
880
|
* @public
|
|
@@ -720,12 +882,29 @@ var JSONAPICache = class {
|
|
|
720
882
|
*/
|
|
721
883
|
changedAttrs(identifier) {
|
|
722
884
|
const cached = this.__peek(identifier, false);
|
|
723
|
-
|
|
724
|
-
|
|
885
|
+
const changes = Object.create(null);
|
|
886
|
+
if (!cached) return changes;
|
|
887
|
+
const { localAttrs, inflightAttrs, remoteAttrs } = cached;
|
|
888
|
+
if (inflightAttrs) {
|
|
889
|
+
const keys = Object.keys(inflightAttrs);
|
|
890
|
+
for (let i = 0; i < keys.length; i++) changes[keys[i]] = [remoteAttrs ? remoteAttrs[keys[i]] : void 0, inflightAttrs[keys[i]]];
|
|
891
|
+
}
|
|
892
|
+
if (localAttrs) {
|
|
893
|
+
const keys = Object.keys(localAttrs);
|
|
894
|
+
for (let i = 0; i < keys.length; i++) changes[keys[i]] = [resolveAttr(keys[i], cached, RESOLUTION_ORDER_EDIT_BASELINE), localAttrs[keys[i]]];
|
|
895
|
+
}
|
|
896
|
+
return changes;
|
|
725
897
|
}
|
|
726
898
|
/**
|
|
727
899
|
* Query the cache for whether any mutated attributes exist
|
|
728
900
|
*
|
|
901
|
+
* @example
|
|
902
|
+
* ```ts
|
|
903
|
+
* if (cache.hasChangedAttrs(identifier)) {
|
|
904
|
+
* // ...
|
|
905
|
+
* }
|
|
906
|
+
* ```
|
|
907
|
+
*
|
|
729
908
|
* @category Resource Data
|
|
730
909
|
* @public
|
|
731
910
|
*/
|
|
@@ -739,6 +918,11 @@ var JSONAPICache = class {
|
|
|
739
918
|
*
|
|
740
919
|
* This method is a candidate to become a mutation
|
|
741
920
|
*
|
|
921
|
+
* @example
|
|
922
|
+
* ```ts
|
|
923
|
+
* const restoredKeys = cache.rollbackAttrs(identifier);
|
|
924
|
+
* ```
|
|
925
|
+
*
|
|
742
926
|
* @category Resource Data
|
|
743
927
|
* @public
|
|
744
928
|
* @return the names of fields that were restored
|
|
@@ -750,7 +934,6 @@ var JSONAPICache = class {
|
|
|
750
934
|
if (cached.localAttrs !== null) {
|
|
751
935
|
dirtyKeys = Object.keys(cached.localAttrs);
|
|
752
936
|
cached.localAttrs = null;
|
|
753
|
-
cached.changes = null;
|
|
754
937
|
}
|
|
755
938
|
if (cached.isNew) {
|
|
756
939
|
cached.isDeletionCommitted = true;
|
|
@@ -770,7 +953,7 @@ var JSONAPICache = class {
|
|
|
770
953
|
/**
|
|
771
954
|
* Query the cache for the changes to relationships of a resource.
|
|
772
955
|
*
|
|
773
|
-
* Returns a map of relationship names to RelationshipDiff objects.
|
|
956
|
+
* Returns a map of relationship names to {@link RelationshipDiff} objects.
|
|
774
957
|
*
|
|
775
958
|
* ```ts
|
|
776
959
|
* type RelationshipDiff =
|
|
@@ -789,6 +972,12 @@ var JSONAPICache = class {
|
|
|
789
972
|
};
|
|
790
973
|
```
|
|
791
974
|
*
|
|
975
|
+
* @example
|
|
976
|
+
* ```ts
|
|
977
|
+
* const diffs = cache.changedRelationships(identifier);
|
|
978
|
+
* const comments = diffs.get('comments');
|
|
979
|
+
* ```
|
|
980
|
+
*
|
|
792
981
|
* @category Resource Data
|
|
793
982
|
* @public
|
|
794
983
|
*/
|
|
@@ -798,6 +987,13 @@ var JSONAPICache = class {
|
|
|
798
987
|
/**
|
|
799
988
|
* Query the cache for whether any mutated relationships exist
|
|
800
989
|
*
|
|
990
|
+
* @example
|
|
991
|
+
* ```ts
|
|
992
|
+
* if (cache.hasChangedRelationships(identifier)) {
|
|
993
|
+
* // ...
|
|
994
|
+
* }
|
|
995
|
+
* ```
|
|
996
|
+
*
|
|
801
997
|
* @category Resource Data
|
|
802
998
|
* @public
|
|
803
999
|
*/
|
|
@@ -811,6 +1007,11 @@ var JSONAPICache = class {
|
|
|
811
1007
|
*
|
|
812
1008
|
* This method is a candidate to become a mutation
|
|
813
1009
|
*
|
|
1010
|
+
* @example
|
|
1011
|
+
* ```ts
|
|
1012
|
+
* const restoredFields = cache.rollbackRelationships(identifier);
|
|
1013
|
+
* ```
|
|
1014
|
+
*
|
|
814
1015
|
* @category Resource Data
|
|
815
1016
|
* @public
|
|
816
1017
|
* @return the names of relationships that were restored
|
|
@@ -826,6 +1027,11 @@ var JSONAPICache = class {
|
|
|
826
1027
|
/**
|
|
827
1028
|
* Query the cache for the current state of a relationship property
|
|
828
1029
|
*
|
|
1030
|
+
* @example
|
|
1031
|
+
* ```ts
|
|
1032
|
+
* const relationship = cache.getRelationship(identifier, 'comments');
|
|
1033
|
+
* ```
|
|
1034
|
+
*
|
|
829
1035
|
* @category Resource Data
|
|
830
1036
|
* @public
|
|
831
1037
|
* @return resource relationship object
|
|
@@ -836,6 +1042,11 @@ var JSONAPICache = class {
|
|
|
836
1042
|
/**
|
|
837
1043
|
* Query the cache for the remote state of a relationship property
|
|
838
1044
|
*
|
|
1045
|
+
* @example
|
|
1046
|
+
* ```ts
|
|
1047
|
+
* const relationship = cache.getRemoteRelationship(identifier, 'comments');
|
|
1048
|
+
* ```
|
|
1049
|
+
*
|
|
839
1050
|
* @category Resource Data
|
|
840
1051
|
* @public
|
|
841
1052
|
* @return resource relationship object
|
|
@@ -849,6 +1060,11 @@ var JSONAPICache = class {
|
|
|
849
1060
|
*
|
|
850
1061
|
* This method is a candidate to become a mutation
|
|
851
1062
|
*
|
|
1063
|
+
* @example
|
|
1064
|
+
* ```ts
|
|
1065
|
+
* cache.setIsDeleted(identifier, true);
|
|
1066
|
+
* ```
|
|
1067
|
+
*
|
|
852
1068
|
* @category Resource State
|
|
853
1069
|
* @public
|
|
854
1070
|
*/
|
|
@@ -860,6 +1076,11 @@ var JSONAPICache = class {
|
|
|
860
1076
|
/**
|
|
861
1077
|
* Query the cache for any validation errors applicable to the given resource.
|
|
862
1078
|
*
|
|
1079
|
+
* @example
|
|
1080
|
+
* ```ts
|
|
1081
|
+
* const errors = cache.getErrors(identifier);
|
|
1082
|
+
* ```
|
|
1083
|
+
*
|
|
863
1084
|
* @category Resource State
|
|
864
1085
|
* @public
|
|
865
1086
|
*/
|
|
@@ -869,6 +1090,13 @@ var JSONAPICache = class {
|
|
|
869
1090
|
/**
|
|
870
1091
|
* Query the cache for whether a given resource has any available data
|
|
871
1092
|
*
|
|
1093
|
+
* @example
|
|
1094
|
+
* ```ts
|
|
1095
|
+
* if (cache.isEmpty(identifier)) {
|
|
1096
|
+
* // ...
|
|
1097
|
+
* }
|
|
1098
|
+
* ```
|
|
1099
|
+
*
|
|
872
1100
|
* @category Resource State
|
|
873
1101
|
* @public
|
|
874
1102
|
*/
|
|
@@ -880,6 +1108,13 @@ var JSONAPICache = class {
|
|
|
880
1108
|
* Query the cache for whether a given resource was created locally and not
|
|
881
1109
|
* yet persisted.
|
|
882
1110
|
*
|
|
1111
|
+
* @example
|
|
1112
|
+
* ```ts
|
|
1113
|
+
* if (cache.isNew(identifier)) {
|
|
1114
|
+
* // ...
|
|
1115
|
+
* }
|
|
1116
|
+
* ```
|
|
1117
|
+
*
|
|
883
1118
|
* @category Resource State
|
|
884
1119
|
* @public
|
|
885
1120
|
*/
|
|
@@ -890,6 +1125,13 @@ var JSONAPICache = class {
|
|
|
890
1125
|
* Query the cache for whether a given resource is marked as deleted (but not
|
|
891
1126
|
* necessarily persisted yet).
|
|
892
1127
|
*
|
|
1128
|
+
* @example
|
|
1129
|
+
* ```ts
|
|
1130
|
+
* if (cache.isDeleted(identifier)) {
|
|
1131
|
+
* // ...
|
|
1132
|
+
* }
|
|
1133
|
+
* ```
|
|
1134
|
+
*
|
|
893
1135
|
* @category Resource State
|
|
894
1136
|
* @public
|
|
895
1137
|
*/
|
|
@@ -900,6 +1142,13 @@ var JSONAPICache = class {
|
|
|
900
1142
|
* Query the cache for whether a given resource has been deleted and that deletion
|
|
901
1143
|
* has also been persisted.
|
|
902
1144
|
*
|
|
1145
|
+
* @example
|
|
1146
|
+
* ```ts
|
|
1147
|
+
* if (cache.isDeletionCommitted(identifier)) {
|
|
1148
|
+
* // ...
|
|
1149
|
+
* }
|
|
1150
|
+
* ```
|
|
1151
|
+
*
|
|
903
1152
|
* @category Resource State
|
|
904
1153
|
* @public
|
|
905
1154
|
*/
|
|
@@ -1035,20 +1284,189 @@ function getDefaultValue(schema, identifier, store) {
|
|
|
1035
1284
|
if (transform?.defaultValue) return transform.defaultValue(options || null, identifier);
|
|
1036
1285
|
}
|
|
1037
1286
|
}
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1287
|
+
/**
|
|
1288
|
+
* The first layer, in the given resolution order, that holds `key`. Presence is `in`, so a value
|
|
1289
|
+
* explicitly set to `undefined` counts as present.
|
|
1290
|
+
*/
|
|
1291
|
+
function layerHolding(key, layers, order) {
|
|
1292
|
+
for (let i = 0; i < order.length; i++) {
|
|
1293
|
+
const layer = layers[order[i]];
|
|
1294
|
+
if (layer && key in layer) return layer;
|
|
1295
|
+
}
|
|
1296
|
+
return null;
|
|
1297
|
+
}
|
|
1298
|
+
/**
|
|
1299
|
+
* What the projection with the given resolution order reads for `attr`. A path resolves its first
|
|
1300
|
+
* segment through the layers and follows the rest into the value, stopping with `undefined` at the
|
|
1301
|
+
* first missing link.
|
|
1302
|
+
*/
|
|
1303
|
+
function resolveAttr(attr, layers, order) {
|
|
1304
|
+
const key = typeof attr === "string" ? attr : attr[0];
|
|
1305
|
+
const layer = layerHolding(key, layers, order);
|
|
1306
|
+
if (!layer) return void 0;
|
|
1307
|
+
return typeof attr === "string" ? layer[key] : valueAtPath(layer[key], attr);
|
|
1308
|
+
}
|
|
1309
|
+
/** Follow `path` (from its second segment) into `base`, stopping with `undefined` at the first missing link or `null`. */
|
|
1310
|
+
function valueAtPath(base, path) {
|
|
1311
|
+
let current = base;
|
|
1312
|
+
for (let i = 1; i < path.length; i++) {
|
|
1313
|
+
if (current === void 0 || current === null) return void 0;
|
|
1314
|
+
current = current[path[i]];
|
|
1315
|
+
}
|
|
1316
|
+
return current;
|
|
1317
|
+
}
|
|
1318
|
+
/**
|
|
1319
|
+
* Whether two values of `field` count as the same value for change detection.
|
|
1320
|
+
*
|
|
1321
|
+
* A `schema-object`, and each element of a `schema-array`, compares by the identity hash its
|
|
1322
|
+
* `ObjectSchema` declares (`identity: { kind: '@hash', ... }`), so the schema decides what "same"
|
|
1323
|
+
* means for it. With no hash declared, and for every other field kind, only the same reference
|
|
1324
|
+
* counts. That knowingly over-notifies for equal-content objects: content equality is the schema's
|
|
1325
|
+
* to define, not the cache's to guess.
|
|
1326
|
+
*/
|
|
1327
|
+
function attrValuesEqual(schema, field, a, b) {
|
|
1328
|
+
if (a === b) return true;
|
|
1329
|
+
if (field.kind === "schema-object") return schemaObjectsEqual(schema, field, a, b);
|
|
1330
|
+
if (field.kind === "schema-array") return schemaArraysEqual(schema, field, a, b);
|
|
1331
|
+
return false;
|
|
1332
|
+
}
|
|
1333
|
+
/**
|
|
1334
|
+
* A schema-array has no hash of its own: two arrays are the same when they have the same length
|
|
1335
|
+
* and every element compares equal as a schema-object of the element type.
|
|
1336
|
+
*/
|
|
1337
|
+
function schemaArraysEqual(schema, field, a, b) {
|
|
1338
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
|
|
1339
|
+
for (let i = 0; i < a.length; i++) if (!schemaObjectsEqual(schema, field, a[i], b[i])) return false;
|
|
1340
|
+
return true;
|
|
1341
|
+
}
|
|
1342
|
+
function schemaObjectsEqual(schema, field, a, b) {
|
|
1343
|
+
if (a === b) return true;
|
|
1344
|
+
if (!a || !b || typeof a !== "object" || typeof b !== "object") return false;
|
|
1345
|
+
const ia = fieldValueIdentity(schema, field, a);
|
|
1346
|
+
const ib = fieldValueIdentity(schema, field, b);
|
|
1347
|
+
return ia !== null && ib !== null && ia.type === ib.type && ia.hash !== null && ia.hash === ib.hash;
|
|
1348
|
+
}
|
|
1349
|
+
/**
|
|
1350
|
+
* Whether a nested edit's local clone is back to the baseline, so the edit can end. The schema
|
|
1351
|
+
* decides first, through {@link attrValuesEqual}: a schema-object with an identity hash matches
|
|
1352
|
+
* when its hash does. A value with no schema-defined equality falls back to comparing serialized
|
|
1353
|
+
* content, as this path always has: the clone began as a copy of the baseline, so key order
|
|
1354
|
+
* agrees while edits only replace existing keys. Anything that cannot serialize counts as still
|
|
1355
|
+
* edited, so no edit the cache cannot understand is thrown away.
|
|
1356
|
+
*/
|
|
1357
|
+
function localCloneMatchesBaseline(schema, field, baseline, clone) {
|
|
1358
|
+
if (field && attrValuesEqual(schema, field, baseline, clone)) return true;
|
|
1359
|
+
if (!baseline || !clone) return false;
|
|
1360
|
+
try {
|
|
1361
|
+
return JSON.stringify(baseline) === JSON.stringify(clone);
|
|
1362
|
+
} catch {
|
|
1363
|
+
return false;
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
/**
|
|
1367
|
+
* Which projection each changed key moved in, so the caller can notify on the matching channel:
|
|
1368
|
+
* `localOnly` and `remoteOnly` on theirs, `both` unscoped.
|
|
1369
|
+
*
|
|
1370
|
+
* `undefined` rather than empty Sets: this runs on every `upsert`, and a resource where nothing
|
|
1371
|
+
* moved should cost no allocation.
|
|
1372
|
+
*/
|
|
1373
|
+
const NO_PROJECTION_CHANGES = Object.freeze({
|
|
1374
|
+
localOnly: void 0,
|
|
1375
|
+
remoteOnly: void 0,
|
|
1376
|
+
both: void 0
|
|
1377
|
+
});
|
|
1378
|
+
/** The layers a merge reads: the resource's own, plus the attributes arriving from the server. */
|
|
1379
|
+
function layersForMerge(cached, incomingAttrs) {
|
|
1380
|
+
return {
|
|
1381
|
+
localAttrs: cached.localAttrs,
|
|
1382
|
+
inflightAttrs: cached.inflightAttrs,
|
|
1383
|
+
remoteAttrs: cached.remoteAttrs,
|
|
1384
|
+
defaultAttrs: cached.defaultAttrs,
|
|
1385
|
+
incomingAttrs
|
|
1386
|
+
};
|
|
1387
|
+
}
|
|
1388
|
+
/**
|
|
1389
|
+
* Every key a merge could move: every key in every layer the after-merge order folds into
|
|
1390
|
+
* `remoteAttrs`. `null` when there is nothing to examine.
|
|
1391
|
+
*/
|
|
1392
|
+
function candidateKeys(layers, remoteAfterOrder) {
|
|
1393
|
+
let keys = null;
|
|
1394
|
+
for (let i = 0; i < remoteAfterOrder.length; i++) {
|
|
1395
|
+
const layer = remoteAfterOrder[i];
|
|
1396
|
+
if (layer === "remoteAttrs") break;
|
|
1397
|
+
const hash = layers[layer];
|
|
1398
|
+
if (!hash) continue;
|
|
1399
|
+
const layerKeys = Object.keys(hash);
|
|
1400
|
+
if (!layerKeys.length) continue;
|
|
1401
|
+
if (keys === null) {
|
|
1402
|
+
keys = layerKeys;
|
|
1403
|
+
continue;
|
|
1404
|
+
}
|
|
1405
|
+
for (let j = 0; j < layerKeys.length; j++) if (!keys.includes(layerKeys[j])) keys.push(layerKeys[j]);
|
|
1406
|
+
}
|
|
1407
|
+
return keys;
|
|
1408
|
+
}
|
|
1409
|
+
/**
|
|
1410
|
+
* Fold every layer above `remoteAttrs` in the after-merge order for `kind` into `remoteAttrs`,
|
|
1411
|
+
* lowest precedence first, and clear each resource layer that was folded in. The same order
|
|
1412
|
+
* {@link partitionChangedKeys} used to predict the result, so the two cannot disagree.
|
|
1413
|
+
*
|
|
1414
|
+
* Reads the resource layers off `cached` rather than taking a {@link Layered}, so an upsert that
|
|
1415
|
+
* is not calculating changes allocates nothing beyond the merge itself.
|
|
1416
|
+
*/
|
|
1417
|
+
function mergeIntoRemote(cached, incomingAttrs, kind) {
|
|
1418
|
+
const folded = MERGE_RESOLUTION[kind].folded;
|
|
1419
|
+
const target = cached.remoteAttrs || Object.create(null);
|
|
1420
|
+
for (let i = 0; i < folded.length; i++) {
|
|
1421
|
+
const layer = folded[i];
|
|
1422
|
+
const hash = layer === "incomingAttrs" ? incomingAttrs : cached[layer];
|
|
1423
|
+
if (!hash) continue;
|
|
1424
|
+
Object.assign(target, hash);
|
|
1425
|
+
if (layer !== "incomingAttrs") cached[layer] = null;
|
|
1426
|
+
if (cached.defaultAttrs) dropMemoizedDefaults(cached.defaultAttrs, hash);
|
|
1427
|
+
}
|
|
1428
|
+
cached.remoteAttrs = target;
|
|
1429
|
+
}
|
|
1430
|
+
function dropMemoizedDefaults(defaultAttrs, replacedBy) {
|
|
1431
|
+
const keys = Object.keys(replacedBy);
|
|
1432
|
+
for (let i = 0; i < keys.length; i++) if (keys[i] in defaultAttrs) delete defaultAttrs[keys[i]];
|
|
1433
|
+
}
|
|
1434
|
+
/**
|
|
1435
|
+
* Partition the keys a merge touches by which projection each one moves in. Must run *before*
|
|
1436
|
+
* {@link mergeIntoRemote}: it reads the pre-merge layers from `layers` and predicts the merge with
|
|
1437
|
+
* the same `RESOLUTION_ORDER_*_AFTER_*` orders `mergeIntoRemote` applies.
|
|
1438
|
+
*/
|
|
1439
|
+
function partitionChangedKeys(schema, layers, fields, kind) {
|
|
1440
|
+
const { remoteAfter: remoteAfterOrder, localAfter: localAfterOrder } = MERGE_RESOLUTION[kind];
|
|
1441
|
+
const keys = candidateKeys(layers, remoteAfterOrder);
|
|
1442
|
+
if (keys === null) return NO_PROJECTION_CHANGES;
|
|
1443
|
+
let localOnly;
|
|
1444
|
+
let remoteOnly;
|
|
1445
|
+
let both;
|
|
1446
|
+
for (let i = 0; i < keys.length; i++) {
|
|
1045
1447
|
const key = keys[i];
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1448
|
+
const field = fields.get(key);
|
|
1449
|
+
if (!field || isRelationship(field)) continue;
|
|
1450
|
+
const remoteBefore = resolveAttr(key, layers, RESOLUTION_ORDER_REMOTE_STATE);
|
|
1451
|
+
const remoteAfter = resolveAttr(key, layers, remoteAfterOrder);
|
|
1452
|
+
const localBefore = resolveAttr(key, layers, RESOLUTION_ORDER_LOCAL_STATE);
|
|
1453
|
+
const localAfter = resolveAttr(key, layers, localAfterOrder);
|
|
1454
|
+
const remoteMoved = !attrValuesEqual(schema, field, remoteBefore, remoteAfter);
|
|
1455
|
+
const localMoved = !attrValuesEqual(schema, field, localBefore, localAfter);
|
|
1456
|
+
if (remoteMoved && localMoved) (both ??= /* @__PURE__ */ new Set()).add(key);
|
|
1457
|
+
else if (remoteMoved) (remoteOnly ??= /* @__PURE__ */ new Set()).add(key);
|
|
1458
|
+
else if (localMoved) (localOnly ??= /* @__PURE__ */ new Set()).add(key);
|
|
1050
1459
|
}
|
|
1051
|
-
return
|
|
1460
|
+
return both || remoteOnly || localOnly ? {
|
|
1461
|
+
localOnly,
|
|
1462
|
+
remoteOnly,
|
|
1463
|
+
both
|
|
1464
|
+
} : NO_PROJECTION_CHANGES;
|
|
1465
|
+
}
|
|
1466
|
+
function notifyProjectionChanges(cache, identifier, { both, remoteOnly, localOnly }) {
|
|
1467
|
+
if (both?.size) cache._capabilities.notifyChange(identifier, "attributes", both);
|
|
1468
|
+
if (remoteOnly?.size) cache._capabilities.notifyChange(identifier, "attributes", remoteOnly, "remote");
|
|
1469
|
+
if (localOnly?.size) cache._capabilities.notifyChange(identifier, "attributes", localOnly, "local");
|
|
1052
1470
|
}
|
|
1053
1471
|
function cacheIsEmpty(cached) {
|
|
1054
1472
|
return !cached || cached.remoteAttrs === null && cached.inflightAttrs === null && cached.localAttrs === null;
|
|
@@ -1089,25 +1507,26 @@ function isRelationship(field) {
|
|
|
1089
1507
|
const { kind } = field;
|
|
1090
1508
|
return kind === "hasMany" || kind === "belongsTo" || kind === "resource" || kind === "collection";
|
|
1091
1509
|
}
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1510
|
+
/**
|
|
1511
|
+
* After a merge: drop every local edit the new baseline agrees with, by the same equality
|
|
1512
|
+
* {@link partitionChangedKeys} uses. Returns whether any edit was dropped.
|
|
1513
|
+
*/
|
|
1514
|
+
function reconcileLocalEdits(schema, cached, fields) {
|
|
1515
|
+
const { localAttrs, defaultAttrs } = cached;
|
|
1516
|
+
if (!localAttrs) return false;
|
|
1517
|
+
let droppedAnEdit = false;
|
|
1518
|
+
const editedKeys = Object.keys(localAttrs);
|
|
1519
|
+
for (let i = 0; i < editedKeys.length; i++) {
|
|
1520
|
+
const key = editedKeys[i];
|
|
1521
|
+
const field = fields.get(key);
|
|
1522
|
+
const baseline = resolveAttr(key, cached, RESOLUTION_ORDER_EDIT_BASELINE);
|
|
1523
|
+
if (field ? attrValuesEqual(schema, field, baseline, localAttrs[key]) : baseline === localAttrs[key]) {
|
|
1524
|
+
droppedAnEdit = true;
|
|
1525
|
+
delete localAttrs[key];
|
|
1107
1526
|
}
|
|
1108
|
-
if (defaultAttrs &&
|
|
1527
|
+
if (defaultAttrs && key in defaultAttrs) delete defaultAttrs[key];
|
|
1109
1528
|
}
|
|
1110
|
-
return
|
|
1529
|
+
return droppedAnEdit;
|
|
1111
1530
|
}
|
|
1112
1531
|
function putOne(cache, identifiers, resource) {
|
|
1113
1532
|
let identifier = identifiers.peekResourceKey(resource);
|
|
@@ -1193,7 +1612,7 @@ function copyLinksAndMeta(target, source) {
|
|
|
1193
1612
|
if ("meta" in source) target.meta = source.meta;
|
|
1194
1613
|
}
|
|
1195
1614
|
function cacheUpsert(cache, identifier, data, calculateChanges) {
|
|
1196
|
-
let
|
|
1615
|
+
let changes = NO_PROJECTION_CHANGES;
|
|
1197
1616
|
const peeked = cache.__safePeek(identifier, false);
|
|
1198
1617
|
const existed = !!peeked;
|
|
1199
1618
|
const cached = peeked || cache._createCache(identifier);
|
|
@@ -1205,16 +1624,23 @@ function cacheUpsert(cache, identifier, data, calculateChanges) {
|
|
|
1205
1624
|
cache._capabilities.notifyChange(identifier, "state", null);
|
|
1206
1625
|
}
|
|
1207
1626
|
const fields = getCacheFields(cache, identifier);
|
|
1208
|
-
if (calculateChanges && existed && data.attributes)
|
|
1209
|
-
cached
|
|
1627
|
+
if (calculateChanges && existed && data.attributes) changes = partitionChangedKeys(cache._capabilities.schema, layersForMerge(cached, data.attributes), fields, "upsert");
|
|
1628
|
+
mergeIntoRemote(cached, data.attributes ?? null, "upsert");
|
|
1210
1629
|
if (cached.localAttrs) {
|
|
1211
|
-
if (
|
|
1630
|
+
if (reconcileLocalEdits(cache._capabilities.schema, cached, fields)) cache._capabilities.notifyChange(identifier, "state", null);
|
|
1212
1631
|
}
|
|
1213
1632
|
if (!isUpdate) cache._capabilities.notifyChange(identifier, "added", null);
|
|
1214
1633
|
if (data.id) cached.id = data.id;
|
|
1215
1634
|
if (data.relationships) setupRelationships(cache.__graph, fields, identifier, data);
|
|
1216
|
-
|
|
1217
|
-
return
|
|
1635
|
+
notifyProjectionChanges(cache, identifier, changes);
|
|
1636
|
+
return remoteChangedKeys(changes);
|
|
1637
|
+
}
|
|
1638
|
+
/** Every key whose remote value moved, as the `upsert` return value. */
|
|
1639
|
+
function remoteChangedKeys({ both, remoteOnly }) {
|
|
1640
|
+
if (!both?.size) return remoteOnly?.size ? Array.from(remoteOnly) : void 0;
|
|
1641
|
+
const keys = Array.from(both);
|
|
1642
|
+
if (remoteOnly?.size) keys.push(...remoteOnly);
|
|
1643
|
+
return keys;
|
|
1218
1644
|
}
|
|
1219
1645
|
function patchCache(Cache, op) {
|
|
1220
1646
|
const isRecord = isResourceKey(op.record);
|
|
@@ -1277,10 +1703,11 @@ function commitDidError(cache, identifier, errors) {
|
|
|
1277
1703
|
const keys = Object.keys(cached.inflightAttrs);
|
|
1278
1704
|
if (keys.length > 0) {
|
|
1279
1705
|
const attrs = cached.localAttrs = cached.localAttrs || Object.create(null);
|
|
1280
|
-
for (let i = 0; i < keys.length; i++) if (
|
|
1706
|
+
for (let i = 0; i < keys.length; i++) if (!(keys[i] in attrs)) attrs[keys[i]] = cached.inflightAttrs[keys[i]];
|
|
1281
1707
|
}
|
|
1282
1708
|
cached.inflightAttrs = null;
|
|
1283
1709
|
}
|
|
1710
|
+
if (reconcileLocalEdits(cache._capabilities.schema, cached, getCacheFields(cache, identifier))) cache._capabilities.notifyChange(identifier, "state", null);
|
|
1284
1711
|
if (errors) cached.errors = errors;
|
|
1285
1712
|
cache._capabilities.notifyChange(identifier, "errors", null);
|
|
1286
1713
|
}
|
|
@@ -1302,22 +1729,21 @@ function didCommit(cache, committedIdentifier, data, op) {
|
|
|
1302
1729
|
}
|
|
1303
1730
|
const fields = getCacheFields(cache, identifier);
|
|
1304
1731
|
cached.isNew = false;
|
|
1305
|
-
let
|
|
1732
|
+
let responseAttrs = null;
|
|
1306
1733
|
if (data) {
|
|
1307
1734
|
if (data.id && !cached.id) cached.id = data.id;
|
|
1308
1735
|
if (identifier === committedIdentifier && identifier.id !== existingId) cache._capabilities.notifyChange(identifier, "identity", null);
|
|
1309
1736
|
if (data.relationships) setupRelationships(cache.__graph, fields, identifier, data);
|
|
1310
|
-
|
|
1737
|
+
responseAttrs = data.attributes ?? null;
|
|
1311
1738
|
}
|
|
1312
|
-
const
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
patchLocalAttributes(cached, changedKeys);
|
|
1739
|
+
const changes = partitionChangedKeys(cache._capabilities.schema, layersForMerge(cached, responseAttrs), fields, "commit");
|
|
1740
|
+
mergeIntoRemote(cached, responseAttrs, "commit");
|
|
1741
|
+
reconcileLocalEdits(cache._capabilities.schema, cached, fields);
|
|
1316
1742
|
if (cached.errors) {
|
|
1317
1743
|
cached.errors = null;
|
|
1318
1744
|
cache._capabilities.notifyChange(identifier, "errors", null);
|
|
1319
1745
|
}
|
|
1320
|
-
|
|
1746
|
+
notifyProjectionChanges(cache, identifier, changes);
|
|
1321
1747
|
cache._capabilities.notifyChange(identifier, "state", null);
|
|
1322
1748
|
}
|
|
1323
1749
|
function willCommit(cache, identifier) {
|