@warp-drive/core 5.8.0-alpha.4 → 5.8.0-alpha.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -38
- package/declarations/build-config.d.ts +18 -1
- package/declarations/graph/-private/-edge-definition.d.ts +12 -2
- package/declarations/index.d.ts +90 -8
- package/declarations/reactive/-private/document.d.ts +58 -46
- package/declarations/reactive/-private/record.d.ts +10 -1
- package/declarations/reactive/-private/schema.d.ts +77 -4
- package/declarations/reactive/-private.d.ts +1 -0
- package/declarations/reactive.d.ts +13 -7
- package/declarations/request/-private/types.d.ts +1 -1
- package/declarations/request.d.ts +47 -0
- package/declarations/store/-private/caches/instance-cache.d.ts +5 -6
- package/declarations/store/-private/default-cache-policy.d.ts +147 -129
- package/declarations/store/-private/managers/cache-capabilities-manager.d.ts +1 -1
- package/declarations/store/-private/managers/cache-key-manager.d.ts +26 -8
- package/declarations/store/-private/managers/cache-manager.d.ts +6 -4
- package/declarations/store/-private/managers/notification-manager.d.ts +1 -1
- package/declarations/store/-private/new-core-tmp/promise-state.d.ts +1 -0
- package/declarations/store/-private/new-core-tmp/request-state.d.ts +1 -1
- package/declarations/store/-private/store-service.d.ts +43 -64
- package/declarations/store/-private.d.ts +0 -1
- package/declarations/store/-types/q/cache-capabilities-manager.d.ts +1 -1
- package/declarations/store/deprecated/-private.d.ts +1 -1
- package/declarations/store/deprecated/store.d.ts +33 -32
- package/declarations/store.d.ts +1 -0
- package/declarations/types/cache.d.ts +8 -6
- package/declarations/types/record.d.ts +132 -0
- package/declarations/types/request.d.ts +26 -14
- package/declarations/types/schema/fields.d.ts +33 -9
- package/declarations/{store/-types/q → types/schema}/schema-service.d.ts +15 -13
- package/declarations/types/spec/document.d.ts +34 -0
- package/declarations/types/symbols.d.ts +2 -2
- package/declarations/types.d.ts +1 -1
- package/dist/build-config.js +1 -1
- package/dist/default-cache-policy-D7_u4YRH.js +572 -0
- package/dist/graph/-private.js +13 -4
- package/dist/{request-state-CUuZzgvE.js → index-BKcD4JZK.js} +10018 -8847
- package/dist/index.js +6 -382
- package/dist/reactive.js +4 -778
- package/dist/{context-C_7OLieY.js → request-oqoLC9rz.js} +219 -172
- package/dist/request.js +1 -1
- package/dist/store/-private.js +1 -1
- package/dist/store.js +1 -533
- package/dist/types/-private.js +1 -1
- package/dist/types/record.js +127 -0
- package/dist/types/request.js +14 -12
- package/dist/types/schema/fields.js +14 -0
- package/dist/types/schema/schema-service.js +0 -0
- package/dist/types/symbols.js +2 -2
- package/dist/unpkg/dev/-private-3C1OkYtZ.js +39 -0
- package/dist/unpkg/dev/build-config/babel-macros.js +1 -0
- package/dist/unpkg/dev/build-config/canary-features.js +1 -0
- package/dist/unpkg/dev/build-config/debugging.js +1 -0
- package/dist/unpkg/dev/build-config/deprecations.js +1 -0
- package/dist/unpkg/dev/build-config/env.js +1 -0
- package/dist/unpkg/dev/build-config/macros.js +1 -0
- package/dist/unpkg/dev/build-config.js +1 -0
- package/dist/unpkg/dev/configure-BC66sfNO.js +183 -0
- package/dist/unpkg/dev/configure.js +1 -0
- package/dist/unpkg/dev/graph/-private.js +3131 -0
- package/dist/unpkg/dev/index-DqhXrNZ_.js +11160 -0
- package/dist/unpkg/dev/index.js +6 -0
- package/dist/unpkg/dev/reactive/-private.js +1 -0
- package/dist/unpkg/dev/reactive.js +127 -0
- package/dist/unpkg/dev/request-CA9K0gXq.js +719 -0
- package/dist/unpkg/dev/request.js +1 -0
- package/dist/unpkg/dev/runtime-DGG4CvlW.js +135 -0
- package/dist/unpkg/dev/store/-private.js +56 -0
- package/dist/unpkg/dev/store.js +558 -0
- package/dist/unpkg/dev/types/-private.js +69 -0
- package/dist/unpkg/dev/types/cache/aliases.js +0 -0
- package/dist/unpkg/dev/types/cache/change.js +0 -0
- package/dist/unpkg/dev/types/cache/mutations.js +0 -0
- package/dist/unpkg/dev/types/cache/operations.js +0 -0
- package/dist/unpkg/dev/types/cache/relationship.js +0 -0
- package/dist/unpkg/dev/types/cache.js +0 -0
- package/dist/unpkg/dev/types/graph.js +0 -0
- package/dist/unpkg/dev/types/identifier.js +61 -0
- package/dist/unpkg/dev/types/json/raw.js +0 -0
- package/dist/unpkg/dev/types/params.js +0 -0
- package/dist/unpkg/dev/types/record.js +191 -0
- package/dist/unpkg/dev/types/request.js +77 -0
- package/dist/unpkg/dev/types/runtime.js +34 -0
- package/dist/unpkg/dev/types/schema/concepts.js +0 -0
- package/dist/unpkg/dev/types/schema/fields.js +505 -0
- package/dist/unpkg/dev/types/schema/fields.type-test.js +0 -0
- package/dist/unpkg/dev/types/schema/schema-service.js +0 -0
- package/dist/unpkg/dev/types/spec/document.js +0 -0
- package/dist/unpkg/dev/types/spec/error.js +0 -0
- package/dist/unpkg/dev/types/spec/json-api-raw.js +0 -0
- package/dist/unpkg/dev/types/symbols.js +84 -0
- package/dist/unpkg/dev/types/utils.js +0 -0
- package/dist/unpkg/dev/types.js +0 -0
- package/dist/unpkg/dev/utils/string.js +91 -0
- package/dist/unpkg/dev-deprecated/-private-3C1OkYtZ.js +39 -0
- package/dist/unpkg/dev-deprecated/build-config/babel-macros.js +1 -0
- package/dist/unpkg/dev-deprecated/build-config/canary-features.js +1 -0
- package/dist/unpkg/dev-deprecated/build-config/debugging.js +1 -0
- package/dist/unpkg/dev-deprecated/build-config/deprecations.js +1 -0
- package/dist/unpkg/dev-deprecated/build-config/env.js +1 -0
- package/dist/unpkg/dev-deprecated/build-config/macros.js +1 -0
- package/dist/unpkg/dev-deprecated/build-config.js +1 -0
- package/dist/unpkg/dev-deprecated/configure-BC66sfNO.js +183 -0
- package/dist/unpkg/dev-deprecated/configure.js +1 -0
- package/dist/unpkg/dev-deprecated/graph/-private.js +3326 -0
- package/dist/unpkg/dev-deprecated/index-BBlq5is_.js +11775 -0
- package/dist/unpkg/dev-deprecated/index.js +5 -0
- package/dist/unpkg/dev-deprecated/reactive/-private.js +1 -0
- package/dist/unpkg/dev-deprecated/reactive.js +127 -0
- package/dist/unpkg/dev-deprecated/request-CA9K0gXq.js +719 -0
- package/dist/unpkg/dev-deprecated/request.js +1 -0
- package/dist/unpkg/dev-deprecated/runtime-DfhJzpZH.js +135 -0
- package/dist/unpkg/dev-deprecated/store/-private.js +2 -0
- package/dist/unpkg/dev-deprecated/store.js +558 -0
- package/dist/unpkg/dev-deprecated/types/-private.js +69 -0
- package/dist/unpkg/dev-deprecated/types/cache/aliases.js +0 -0
- package/dist/unpkg/dev-deprecated/types/cache/change.js +0 -0
- package/dist/unpkg/dev-deprecated/types/cache/mutations.js +0 -0
- package/dist/unpkg/dev-deprecated/types/cache/operations.js +0 -0
- package/dist/unpkg/dev-deprecated/types/cache/relationship.js +0 -0
- package/dist/unpkg/dev-deprecated/types/cache.js +0 -0
- package/dist/unpkg/dev-deprecated/types/graph.js +0 -0
- package/dist/unpkg/dev-deprecated/types/identifier.js +61 -0
- package/dist/unpkg/dev-deprecated/types/json/raw.js +0 -0
- package/dist/unpkg/dev-deprecated/types/params.js +0 -0
- package/dist/unpkg/dev-deprecated/types/record.js +191 -0
- package/dist/unpkg/dev-deprecated/types/request.js +77 -0
- package/dist/unpkg/dev-deprecated/types/runtime.js +34 -0
- package/dist/unpkg/dev-deprecated/types/schema/concepts.js +0 -0
- package/dist/unpkg/dev-deprecated/types/schema/fields.js +505 -0
- package/dist/unpkg/dev-deprecated/types/schema/fields.type-test.js +0 -0
- package/dist/unpkg/dev-deprecated/types/schema/schema-service.js +0 -0
- package/dist/unpkg/dev-deprecated/types/spec/document.js +0 -0
- package/dist/unpkg/dev-deprecated/types/spec/error.js +0 -0
- package/dist/unpkg/dev-deprecated/types/spec/json-api-raw.js +0 -0
- package/dist/unpkg/dev-deprecated/types/symbols.js +84 -0
- package/dist/unpkg/dev-deprecated/types/utils.js +0 -0
- package/dist/unpkg/dev-deprecated/types.js +0 -0
- package/dist/unpkg/dev-deprecated/utils/string.js +91 -0
- package/dist/unpkg/prod/-private-3C1OkYtZ.js +39 -0
- package/dist/unpkg/prod/build-config/babel-macros.js +1 -0
- package/dist/unpkg/prod/build-config/canary-features.js +1 -0
- package/dist/unpkg/prod/build-config/debugging.js +1 -0
- package/dist/unpkg/prod/build-config/deprecations.js +1 -0
- package/dist/unpkg/prod/build-config/env.js +1 -0
- package/dist/unpkg/prod/build-config/macros.js +1 -0
- package/dist/unpkg/prod/build-config.js +1 -0
- package/dist/unpkg/prod/configure-C0C1LpG6.js +158 -0
- package/dist/unpkg/prod/configure.js +1 -0
- package/dist/unpkg/prod/graph/-private.js +2234 -0
- package/dist/unpkg/prod/handler-LAyD1Y5l.js +1619 -0
- package/dist/unpkg/prod/hooks-BfiqDg3O.js +26 -0
- package/dist/unpkg/prod/index.js +481 -0
- package/dist/unpkg/prod/promise-state-ipG60SdD.js +6738 -0
- package/dist/unpkg/prod/reactive/-private.js +1 -0
- package/dist/unpkg/prod/reactive.js +127 -0
- package/dist/unpkg/prod/request-CN2LxbYX.js +437 -0
- package/dist/unpkg/prod/request.js +1 -0
- package/dist/unpkg/prod/store/-private.js +127 -0
- package/dist/unpkg/prod/store.js +437 -0
- package/dist/unpkg/prod/types/-private.js +49 -0
- package/dist/unpkg/prod/types/cache/aliases.js +0 -0
- package/dist/unpkg/prod/types/cache/change.js +0 -0
- package/dist/unpkg/prod/types/cache/mutations.js +0 -0
- package/dist/unpkg/prod/types/cache/operations.js +0 -0
- package/dist/unpkg/prod/types/cache/relationship.js +0 -0
- package/dist/unpkg/prod/types/cache.js +0 -0
- package/dist/unpkg/prod/types/graph.js +0 -0
- package/dist/unpkg/prod/types/identifier.js +61 -0
- package/dist/unpkg/prod/types/json/raw.js +0 -0
- package/dist/unpkg/prod/types/params.js +0 -0
- package/dist/unpkg/prod/types/record.js +191 -0
- package/dist/unpkg/prod/types/request.js +77 -0
- package/dist/unpkg/prod/types/runtime.js +34 -0
- package/dist/unpkg/prod/types/schema/concepts.js +0 -0
- package/dist/unpkg/prod/types/schema/fields.js +505 -0
- package/dist/unpkg/prod/types/schema/fields.type-test.js +0 -0
- package/dist/unpkg/prod/types/schema/schema-service.js +0 -0
- package/dist/unpkg/prod/types/spec/document.js +0 -0
- package/dist/unpkg/prod/types/spec/error.js +0 -0
- package/dist/unpkg/prod/types/spec/json-api-raw.js +0 -0
- package/dist/unpkg/prod/types/symbols.js +84 -0
- package/dist/unpkg/prod/types/utils.js +0 -0
- package/dist/unpkg/prod/types.js +0 -0
- package/dist/unpkg/prod/utils/string.js +72 -0
- package/dist/unpkg/prod-deprecated/-private-3C1OkYtZ.js +39 -0
- package/dist/unpkg/prod-deprecated/build-config/babel-macros.js +1 -0
- package/dist/unpkg/prod-deprecated/build-config/canary-features.js +1 -0
- package/dist/unpkg/prod-deprecated/build-config/debugging.js +1 -0
- package/dist/unpkg/prod-deprecated/build-config/deprecations.js +1 -0
- package/dist/unpkg/prod-deprecated/build-config/env.js +1 -0
- package/dist/unpkg/prod-deprecated/build-config/macros.js +1 -0
- package/dist/unpkg/prod-deprecated/build-config.js +1 -0
- package/dist/unpkg/prod-deprecated/configure-BQ8CpIcW.js +158 -0
- package/dist/unpkg/prod-deprecated/configure.js +1 -0
- package/dist/unpkg/prod-deprecated/graph/-private.js +2407 -0
- package/dist/unpkg/prod-deprecated/handler-D639oFvl.js +334 -0
- package/dist/unpkg/prod-deprecated/hooks-DGvi9teJ.js +26 -0
- package/dist/unpkg/prod-deprecated/index.js +481 -0
- package/dist/unpkg/prod-deprecated/promise-state-CYvoIPna.js +8458 -0
- package/dist/unpkg/prod-deprecated/reactive/-private.js +1 -0
- package/dist/unpkg/prod-deprecated/reactive.js +126 -0
- package/dist/unpkg/prod-deprecated/request-CN2LxbYX.js +437 -0
- package/dist/unpkg/prod-deprecated/request.js +1 -0
- package/dist/unpkg/prod-deprecated/store/-private.js +89 -0
- package/dist/unpkg/prod-deprecated/store.js +437 -0
- package/dist/unpkg/prod-deprecated/types/-private.js +49 -0
- package/dist/unpkg/prod-deprecated/types/cache/aliases.js +0 -0
- package/dist/unpkg/prod-deprecated/types/cache/change.js +0 -0
- package/dist/unpkg/prod-deprecated/types/cache/mutations.js +0 -0
- package/dist/unpkg/prod-deprecated/types/cache/operations.js +0 -0
- package/dist/unpkg/prod-deprecated/types/cache/relationship.js +0 -0
- package/dist/unpkg/prod-deprecated/types/cache.js +0 -0
- package/dist/unpkg/prod-deprecated/types/graph.js +0 -0
- package/dist/unpkg/prod-deprecated/types/identifier.js +61 -0
- package/dist/unpkg/prod-deprecated/types/json/raw.js +0 -0
- package/dist/unpkg/prod-deprecated/types/params.js +0 -0
- package/dist/unpkg/prod-deprecated/types/record.js +191 -0
- package/dist/unpkg/prod-deprecated/types/request.js +77 -0
- package/dist/unpkg/prod-deprecated/types/runtime.js +34 -0
- package/dist/unpkg/prod-deprecated/types/schema/concepts.js +0 -0
- package/dist/unpkg/prod-deprecated/types/schema/fields.js +505 -0
- package/dist/unpkg/prod-deprecated/types/schema/fields.type-test.js +0 -0
- package/dist/unpkg/prod-deprecated/types/schema/schema-service.js +0 -0
- package/dist/unpkg/prod-deprecated/types/spec/document.js +0 -0
- package/dist/unpkg/prod-deprecated/types/spec/error.js +0 -0
- package/dist/unpkg/prod-deprecated/types/spec/json-api-raw.js +0 -0
- package/dist/unpkg/prod-deprecated/types/symbols.js +84 -0
- package/dist/unpkg/prod-deprecated/types/utils.js +0 -0
- package/dist/unpkg/prod-deprecated/types.js +0 -0
- package/dist/unpkg/prod-deprecated/utils/string.js +72 -0
- package/logos/README.md +2 -2
- package/logos/logo-yellow-slab.svg +1 -0
- package/logos/word-mark-black.svg +1 -0
- package/logos/word-mark-white.svg +1 -0
- package/package.json +11 -3
- package/logos/NCC-1701-a-blue.svg +0 -4
- package/logos/NCC-1701-a-gold.svg +0 -4
- package/logos/NCC-1701-a-gold_100.svg +0 -1
- package/logos/NCC-1701-a-gold_base-64.txt +0 -1
- package/logos/NCC-1701-a.svg +0 -4
- package/logos/docs-badge.svg +0 -2
- package/logos/ember-data-logo-dark.svg +0 -12
- package/logos/ember-data-logo-light.svg +0 -12
- package/logos/social1.png +0 -0
- package/logos/social2.png +0 -0
- package/logos/warp-drive-logo-dark.svg +0 -4
- package/logos/warp-drive-logo-gold.svg +0 -4
|
@@ -0,0 +1,2407 @@
|
|
|
1
|
+
import { getOrSetGlobal, peekTransient, setTransient } from '../types/-private.js';
|
|
2
|
+
function getStore(wrapper) {
|
|
3
|
+
return wrapper._store;
|
|
4
|
+
}
|
|
5
|
+
function expandingGet(cache, key1, key2) {
|
|
6
|
+
const mainCache = cache[key1] = cache[key1] || Object.create(null);
|
|
7
|
+
return mainCache[key2];
|
|
8
|
+
}
|
|
9
|
+
function expandingSet(cache, key1, key2, value) {
|
|
10
|
+
const mainCache = cache[key1] = cache[key1] || Object.create(null);
|
|
11
|
+
mainCache[key2] = value;
|
|
12
|
+
}
|
|
13
|
+
function checkIfNew(store, resourceKey) {
|
|
14
|
+
if (!resourceKey.id) {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
return store.cache.isNew(resourceKey);
|
|
18
|
+
}
|
|
19
|
+
function isBelongsTo(relationship) {
|
|
20
|
+
return relationship.definition.kind === 'belongsTo';
|
|
21
|
+
}
|
|
22
|
+
function isImplicit(relationship) {
|
|
23
|
+
return relationship.definition.isImplicit;
|
|
24
|
+
}
|
|
25
|
+
function isHasMany(relationship) {
|
|
26
|
+
return relationship.definition.kind === 'hasMany';
|
|
27
|
+
}
|
|
28
|
+
function forAllRelatedIdentifiers(rel, cb) {
|
|
29
|
+
if (isBelongsTo(rel)) {
|
|
30
|
+
if (rel.remoteState) {
|
|
31
|
+
cb(rel.remoteState);
|
|
32
|
+
}
|
|
33
|
+
if (rel.localState && rel.localState !== rel.remoteState) {
|
|
34
|
+
cb(rel.localState);
|
|
35
|
+
}
|
|
36
|
+
} else if (isHasMany(rel)) {
|
|
37
|
+
// TODO
|
|
38
|
+
// rel.remoteMembers.forEach(cb);
|
|
39
|
+
// might be simpler if performance is not a concern
|
|
40
|
+
for (let i = 0; i < rel.remoteState.length; i++) {
|
|
41
|
+
const inverseIdentifier = rel.remoteState[i];
|
|
42
|
+
cb(inverseIdentifier);
|
|
43
|
+
}
|
|
44
|
+
rel.additions?.forEach(cb);
|
|
45
|
+
} else {
|
|
46
|
+
rel.localMembers.forEach(cb);
|
|
47
|
+
rel.remoteMembers.forEach(inverseIdentifier => {
|
|
48
|
+
if (!rel.localMembers.has(inverseIdentifier)) {
|
|
49
|
+
cb(inverseIdentifier);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/*
|
|
56
|
+
Removes the given identifier from BOTH remote AND local state.
|
|
57
|
+
|
|
58
|
+
This method is useful when either a deletion or a rollback on a new record
|
|
59
|
+
needs to entirely purge itself from an inverse relationship.
|
|
60
|
+
*/
|
|
61
|
+
function removeIdentifierCompletelyFromRelationship(graph, relationship, value, silenceNotifications) {
|
|
62
|
+
if (isBelongsTo(relationship)) {
|
|
63
|
+
if (relationship.remoteState === value) {
|
|
64
|
+
relationship.remoteState = null;
|
|
65
|
+
}
|
|
66
|
+
if (relationship.localState === value) {
|
|
67
|
+
relationship.localState = null;
|
|
68
|
+
// This allows dematerialized inverses to be rematerialized
|
|
69
|
+
// we shouldn't be notifying here though, figure out where
|
|
70
|
+
// a notification was missed elsewhere.
|
|
71
|
+
{
|
|
72
|
+
notifyChange(graph, relationship);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
} else if (isHasMany(relationship)) {
|
|
76
|
+
relationship.remoteMembers.delete(value);
|
|
77
|
+
relationship.additions?.delete(value);
|
|
78
|
+
const wasInRemovals = relationship.removals?.delete(value);
|
|
79
|
+
const canonicalIndex = relationship.remoteState.indexOf(value);
|
|
80
|
+
if (canonicalIndex !== -1) {
|
|
81
|
+
relationship.remoteState.splice(canonicalIndex, 1);
|
|
82
|
+
}
|
|
83
|
+
if (!wasInRemovals) {
|
|
84
|
+
const currentIndex = relationship.localState?.indexOf(value);
|
|
85
|
+
if (currentIndex !== -1 && currentIndex !== undefined) {
|
|
86
|
+
relationship.localState.splice(currentIndex, 1);
|
|
87
|
+
// This allows dematerialized inverses to be rematerialized
|
|
88
|
+
// we shouldn't be notifying here though, figure out where
|
|
89
|
+
// a notification was missed elsewhere.
|
|
90
|
+
{
|
|
91
|
+
notifyChange(graph, relationship);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
relationship.remoteMembers.delete(value);
|
|
97
|
+
relationship.localMembers.delete(value);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function notifyChange(graph, relationship) {
|
|
101
|
+
if (!relationship.accessed) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const resourceKey = relationship.identifier;
|
|
105
|
+
const key = relationship.definition.key;
|
|
106
|
+
if (resourceKey === graph._removing) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
graph.store.notifyChange(resourceKey, 'relationships', key);
|
|
110
|
+
}
|
|
111
|
+
function isLegacyField(field) {
|
|
112
|
+
return field.kind === 'belongsTo' || field.kind === 'hasMany';
|
|
113
|
+
}
|
|
114
|
+
function temporaryConvertToLegacy(field) {
|
|
115
|
+
return {
|
|
116
|
+
kind: field.kind === 'resource' ? 'belongsTo' : 'hasMany',
|
|
117
|
+
name: field.name,
|
|
118
|
+
type: field.type,
|
|
119
|
+
options: Object.assign({}, {
|
|
120
|
+
async: false,
|
|
121
|
+
inverse: null,
|
|
122
|
+
resetOnRemoteUpdate: false
|
|
123
|
+
}, field.options)
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
*
|
|
129
|
+
* Given RHS (Right Hand Side)
|
|
130
|
+
*
|
|
131
|
+
* ```ts
|
|
132
|
+
* class User extends Model {
|
|
133
|
+
* @hasMany('animal', { async: false, inverse: 'owner' }) pets;
|
|
134
|
+
* }
|
|
135
|
+
* ```
|
|
136
|
+
*
|
|
137
|
+
* Given LHS (Left Hand Side)
|
|
138
|
+
*
|
|
139
|
+
* ```ts
|
|
140
|
+
* class Animal extends Model {
|
|
141
|
+
* @belongsTo('user', { async: false, inverse: 'pets' }) owner;
|
|
142
|
+
* }
|
|
143
|
+
* ```
|
|
144
|
+
*
|
|
145
|
+
* The UpgradedMeta for the RHS would be:
|
|
146
|
+
*
|
|
147
|
+
* ```ts
|
|
148
|
+
* {
|
|
149
|
+
* kind: 'hasMany',
|
|
150
|
+
* key: 'pets',
|
|
151
|
+
* type: 'animal',
|
|
152
|
+
* isAsync: false,
|
|
153
|
+
* isImplicit: false,
|
|
154
|
+
* isCollection: true,
|
|
155
|
+
* isPolymorphic: false,
|
|
156
|
+
* inverseKind: 'belongsTo',
|
|
157
|
+
* inverseKey: 'owner',
|
|
158
|
+
* inverseType: 'user',
|
|
159
|
+
* inverseIsAsync: false,
|
|
160
|
+
* inverseIsImplicit: false,
|
|
161
|
+
* inverseIsCollection: false,
|
|
162
|
+
* inverseIsPolymorphic: false,
|
|
163
|
+
* }
|
|
164
|
+
* ```
|
|
165
|
+
*
|
|
166
|
+
* The UpgradeMeta for the LHS would be:
|
|
167
|
+
*
|
|
168
|
+
* ```ts
|
|
169
|
+
* {
|
|
170
|
+
* kind: 'belongsTo',
|
|
171
|
+
* key: 'owner',
|
|
172
|
+
* type: 'user',
|
|
173
|
+
* isAsync: false,
|
|
174
|
+
* isImplicit: false,
|
|
175
|
+
* isCollection: false,
|
|
176
|
+
* isPolymorphic: false,
|
|
177
|
+
* inverseKind: 'hasMany',
|
|
178
|
+
* inverseKey: 'pets',
|
|
179
|
+
* inverseType: 'animal',
|
|
180
|
+
* inverseIsAsync: false,
|
|
181
|
+
* inverseIsImplicit: false,
|
|
182
|
+
* inverseIsCollection: true,
|
|
183
|
+
* inverseIsPolymorphic: false,
|
|
184
|
+
* }
|
|
185
|
+
* ```
|
|
186
|
+
*
|
|
187
|
+
* @private
|
|
188
|
+
*/
|
|
189
|
+
|
|
190
|
+
const BOOL_LATER = null;
|
|
191
|
+
const STR_LATER = '';
|
|
192
|
+
const IMPLICIT_KEY_RAND = Date.now();
|
|
193
|
+
function implicitKeyFor(type, key) {
|
|
194
|
+
return `implicit-${type}:${key}${IMPLICIT_KEY_RAND}`;
|
|
195
|
+
}
|
|
196
|
+
function syncMeta(definition, inverseDefinition) {
|
|
197
|
+
definition.inverseKind = inverseDefinition.kind;
|
|
198
|
+
definition.inverseKey = inverseDefinition.key;
|
|
199
|
+
definition.inverseName = inverseDefinition.name;
|
|
200
|
+
definition.inverseType = inverseDefinition.type;
|
|
201
|
+
definition.inverseIsAsync = inverseDefinition.isAsync;
|
|
202
|
+
definition.inverseIsCollection = inverseDefinition.isCollection;
|
|
203
|
+
definition.inverseIsPolymorphic = inverseDefinition.isPolymorphic;
|
|
204
|
+
definition.inverseIsImplicit = inverseDefinition.isImplicit;
|
|
205
|
+
definition.inverseIsLinksMode = inverseDefinition.isLinksMode;
|
|
206
|
+
const resetOnRemoteUpdate = definition.resetOnRemoteUpdate === false || inverseDefinition.resetOnRemoteUpdate === false ? false : true;
|
|
207
|
+
definition.resetOnRemoteUpdate = resetOnRemoteUpdate;
|
|
208
|
+
inverseDefinition.resetOnRemoteUpdate = resetOnRemoteUpdate;
|
|
209
|
+
}
|
|
210
|
+
function upgradeMeta(meta) {
|
|
211
|
+
if (!isLegacyField(meta)) {
|
|
212
|
+
meta = temporaryConvertToLegacy(meta);
|
|
213
|
+
}
|
|
214
|
+
const niceMeta = {};
|
|
215
|
+
const options = meta.options;
|
|
216
|
+
niceMeta.kind = meta.kind;
|
|
217
|
+
niceMeta.key = meta.sourceKey ?? meta.name;
|
|
218
|
+
niceMeta.name = meta.name;
|
|
219
|
+
niceMeta.type = meta.type;
|
|
220
|
+
niceMeta.isAsync = options.async;
|
|
221
|
+
niceMeta.isImplicit = false;
|
|
222
|
+
niceMeta.isCollection = meta.kind === 'hasMany';
|
|
223
|
+
niceMeta.isPolymorphic = options && !!options.polymorphic;
|
|
224
|
+
niceMeta.isLinksMode = options.linksMode ?? false;
|
|
225
|
+
niceMeta.inverseKey = options && options.inverse || STR_LATER;
|
|
226
|
+
niceMeta.inverseName = options && options.inverse || STR_LATER;
|
|
227
|
+
niceMeta.inverseType = STR_LATER;
|
|
228
|
+
niceMeta.inverseIsAsync = BOOL_LATER;
|
|
229
|
+
niceMeta.inverseIsImplicit = options && options.inverse === null || BOOL_LATER;
|
|
230
|
+
niceMeta.inverseIsCollection = BOOL_LATER;
|
|
231
|
+
niceMeta.inverseIsLinksMode = BOOL_LATER;
|
|
232
|
+
|
|
233
|
+
// prettier-ignore
|
|
234
|
+
niceMeta.resetOnRemoteUpdate = !isLegacyField(meta) ? false : meta.options?.linksMode ? false : meta.options?.resetOnRemoteUpdate === false ? false : true;
|
|
235
|
+
return niceMeta;
|
|
236
|
+
}
|
|
237
|
+
function isLHS(info, type, key) {
|
|
238
|
+
const isSelfReferential = info.isSelfReferential;
|
|
239
|
+
const isRelationship = key === info.lhs_relationshipName;
|
|
240
|
+
if (isRelationship === true) {
|
|
241
|
+
return isSelfReferential === true ||
|
|
242
|
+
// itself
|
|
243
|
+
type === info.lhs_baseModelName ||
|
|
244
|
+
// base or non-polymorphic
|
|
245
|
+
// if the other side is polymorphic then we need to scan our modelNames
|
|
246
|
+
info.rhs_isPolymorphic && info.lhs_modelNames.includes(type) // polymorphic
|
|
247
|
+
;
|
|
248
|
+
}
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
function upgradeDefinition(graph, key, propertyName, isImplicit = false) {
|
|
252
|
+
const cache = graph._definitionCache;
|
|
253
|
+
const storeWrapper = graph.store;
|
|
254
|
+
const polymorphicLookup = graph._potentialPolymorphicTypes;
|
|
255
|
+
const {
|
|
256
|
+
type
|
|
257
|
+
} = key;
|
|
258
|
+
let cached = /*#__NOINLINE__*/expandingGet(cache, type, propertyName);
|
|
259
|
+
|
|
260
|
+
// CASE: We have a cached resolution (null if no relationship exists)
|
|
261
|
+
if (cached !== undefined) {
|
|
262
|
+
return cached;
|
|
263
|
+
}
|
|
264
|
+
const relationships = storeWrapper.schema.fields(key);
|
|
265
|
+
const relationshipsBySourceKey = storeWrapper.schema.cacheFields?.(key) ?? relationships;
|
|
266
|
+
const meta = relationshipsBySourceKey.get(propertyName);
|
|
267
|
+
if (!meta) {
|
|
268
|
+
// TODO potentially we should just be permissive here since this is an implicit relationship
|
|
269
|
+
// and not require the lookup table to be populated
|
|
270
|
+
if (polymorphicLookup[type]) {
|
|
271
|
+
const altTypes = Object.keys(polymorphicLookup[type]);
|
|
272
|
+
for (let i = 0; i < altTypes.length; i++) {
|
|
273
|
+
const _cached = expandingGet(cache, altTypes[i], propertyName);
|
|
274
|
+
if (_cached) {
|
|
275
|
+
/*#__NOINLINE__*/expandingSet(cache, type, propertyName, _cached);
|
|
276
|
+
_cached.rhs_modelNames.push(type);
|
|
277
|
+
return _cached;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
cache[type][propertyName] = null;
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
const definition = /*#__NOINLINE__*/upgradeMeta(meta);
|
|
285
|
+
let inverseDefinition;
|
|
286
|
+
let inverseKey;
|
|
287
|
+
const inverseType = definition.type;
|
|
288
|
+
|
|
289
|
+
// CASE: Inverse is explicitly null
|
|
290
|
+
if (definition.inverseKey === null) {
|
|
291
|
+
inverseDefinition = null;
|
|
292
|
+
} else {
|
|
293
|
+
inverseKey = /*#__NOINLINE__*/inverseForRelationship(getStore(storeWrapper), key, propertyName);
|
|
294
|
+
|
|
295
|
+
// CASE: If we are polymorphic, and we declared an inverse that is non-null
|
|
296
|
+
// we must assume that the lack of inverseKey means that there is no
|
|
297
|
+
// concrete type as the baseType, so we must construct and artificial
|
|
298
|
+
// placeholder
|
|
299
|
+
if (!inverseKey && definition.isPolymorphic && definition.inverseKey) {
|
|
300
|
+
inverseDefinition = {
|
|
301
|
+
kind: 'belongsTo',
|
|
302
|
+
// this must be updated when we find the first belongsTo or hasMany definition that matches
|
|
303
|
+
key: definition.inverseKey,
|
|
304
|
+
name: definition.inverseName,
|
|
305
|
+
type: type,
|
|
306
|
+
isAsync: false,
|
|
307
|
+
// this must be updated when we find the first belongsTo or hasMany definition that matches
|
|
308
|
+
isImplicit: false,
|
|
309
|
+
isCollection: false,
|
|
310
|
+
// this must be updated when we find the first belongsTo or hasMany definition that matches
|
|
311
|
+
isPolymorphic: false
|
|
312
|
+
}; // the rest of the fields are populated by syncMeta
|
|
313
|
+
|
|
314
|
+
// CASE: Inverse resolves to null
|
|
315
|
+
} else if (!inverseKey) {
|
|
316
|
+
inverseDefinition = null;
|
|
317
|
+
} else {
|
|
318
|
+
// CASE: We have an explicit inverse or were able to resolve one
|
|
319
|
+
// for the inverse we use "name" for lookup not "sourceKey"
|
|
320
|
+
const inverseDefinitions = storeWrapper.schema.fields({
|
|
321
|
+
type: inverseType
|
|
322
|
+
});
|
|
323
|
+
const metaFromInverse = inverseDefinitions.get(inverseKey);
|
|
324
|
+
inverseDefinition = upgradeMeta(metaFromInverse);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// CASE: We have no inverse
|
|
329
|
+
if (!inverseDefinition) {
|
|
330
|
+
// polish off meta
|
|
331
|
+
inverseKey = /*#__NOINLINE__*/implicitKeyFor(type, propertyName);
|
|
332
|
+
inverseDefinition = {
|
|
333
|
+
kind: 'implicit',
|
|
334
|
+
key: inverseKey,
|
|
335
|
+
type: type,
|
|
336
|
+
isAsync: false,
|
|
337
|
+
isImplicit: true,
|
|
338
|
+
isCollection: true,
|
|
339
|
+
// with implicits any number of records could point at us
|
|
340
|
+
isPolymorphic: false
|
|
341
|
+
}; // the rest of the fields are populated by syncMeta
|
|
342
|
+
|
|
343
|
+
syncMeta(definition, inverseDefinition);
|
|
344
|
+
syncMeta(inverseDefinition, definition);
|
|
345
|
+
const info = {
|
|
346
|
+
lhs_key: `${type}:${propertyName}`,
|
|
347
|
+
lhs_modelNames: [type],
|
|
348
|
+
lhs_baseModelName: type,
|
|
349
|
+
lhs_relationshipName: propertyName,
|
|
350
|
+
lhs_definition: definition,
|
|
351
|
+
lhs_isPolymorphic: definition.isPolymorphic,
|
|
352
|
+
rhs_key: inverseDefinition.key,
|
|
353
|
+
rhs_modelNames: [inverseType],
|
|
354
|
+
rhs_baseModelName: inverseType,
|
|
355
|
+
rhs_relationshipName: inverseDefinition.key,
|
|
356
|
+
rhs_definition: inverseDefinition,
|
|
357
|
+
rhs_isPolymorphic: false,
|
|
358
|
+
hasInverse: false,
|
|
359
|
+
isSelfReferential: type === inverseType,
|
|
360
|
+
// this could be wrong if we are self-referential but also polymorphic
|
|
361
|
+
isReflexive: false // we can't be reflexive if we don't define an inverse
|
|
362
|
+
};
|
|
363
|
+
expandingSet(cache, inverseType, inverseKey, info);
|
|
364
|
+
expandingSet(cache, type, propertyName, info);
|
|
365
|
+
return info;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// CASE: We do have an inverse
|
|
369
|
+
const baseType = inverseDefinition.type;
|
|
370
|
+
cached = expandingGet(cache, baseType, propertyName) || expandingGet(cache, inverseType, inverseKey);
|
|
371
|
+
if (cached) {
|
|
372
|
+
const _isLHS = cached.lhs_baseModelName === baseType;
|
|
373
|
+
const modelNames = _isLHS ? cached.lhs_modelNames : cached.rhs_modelNames;
|
|
374
|
+
// make this lookup easier in the future by caching the key
|
|
375
|
+
modelNames.push(type);
|
|
376
|
+
expandingSet(cache, type, propertyName, cached);
|
|
377
|
+
return cached;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// this is our first time so polish off the metas
|
|
381
|
+
syncMeta(definition, inverseDefinition);
|
|
382
|
+
syncMeta(inverseDefinition, definition);
|
|
383
|
+
const lhs_modelNames = [type];
|
|
384
|
+
if (type !== baseType) {
|
|
385
|
+
lhs_modelNames.push(baseType);
|
|
386
|
+
}
|
|
387
|
+
const isSelfReferential = baseType === inverseType;
|
|
388
|
+
const info = {
|
|
389
|
+
lhs_key: `${baseType}:${propertyName}`,
|
|
390
|
+
lhs_modelNames,
|
|
391
|
+
lhs_baseModelName: baseType,
|
|
392
|
+
lhs_relationshipName: propertyName,
|
|
393
|
+
lhs_definition: definition,
|
|
394
|
+
lhs_isPolymorphic: definition.isPolymorphic,
|
|
395
|
+
rhs_key: `${inverseType}:${inverseKey}`,
|
|
396
|
+
rhs_modelNames: [inverseType],
|
|
397
|
+
rhs_baseModelName: inverseType,
|
|
398
|
+
rhs_relationshipName: inverseKey,
|
|
399
|
+
rhs_definition: inverseDefinition,
|
|
400
|
+
rhs_isPolymorphic: inverseDefinition.isPolymorphic,
|
|
401
|
+
hasInverse: true,
|
|
402
|
+
isSelfReferential,
|
|
403
|
+
isReflexive: isSelfReferential && propertyName === inverseKey
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
// Create entries for the baseModelName as well as modelName to speed up
|
|
407
|
+
// inverse lookups
|
|
408
|
+
expandingSet(cache, baseType, propertyName, info);
|
|
409
|
+
expandingSet(cache, type, propertyName, info);
|
|
410
|
+
|
|
411
|
+
// Greedily populate the inverse
|
|
412
|
+
expandingSet(cache, inverseType, inverseKey, info);
|
|
413
|
+
return info;
|
|
414
|
+
}
|
|
415
|
+
function inverseForRelationship(store, resourceKey, key) {
|
|
416
|
+
const fields = store.schema.fields(resourceKey);
|
|
417
|
+
const definition = fields.get(key);
|
|
418
|
+
if (!definition) {
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
return definition.options.inverse;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/*
|
|
425
|
+
case many:1
|
|
426
|
+
========
|
|
427
|
+
In a bi-directional graph with Many:1 edges, adding a value
|
|
428
|
+
results in up-to 3 discrete value transitions, while removing
|
|
429
|
+
a value is only 2 transitions.
|
|
430
|
+
|
|
431
|
+
For adding C to A
|
|
432
|
+
If: A <<-> B, C <->> D is the initial state,
|
|
433
|
+
and: B <->> A <<-> C, D is the final state
|
|
434
|
+
|
|
435
|
+
then we would undergo the following transitions.
|
|
436
|
+
|
|
437
|
+
add C to A
|
|
438
|
+
remove C from D
|
|
439
|
+
add A to C
|
|
440
|
+
|
|
441
|
+
For removing B from A
|
|
442
|
+
If: A <<-> B, C <->> D is the initial state,
|
|
443
|
+
and: A, B, C <->> D is the final state
|
|
444
|
+
|
|
445
|
+
then we would undergo the following transitions.
|
|
446
|
+
|
|
447
|
+
remove B from A
|
|
448
|
+
remove A from B
|
|
449
|
+
|
|
450
|
+
case many:many
|
|
451
|
+
===========
|
|
452
|
+
In a bi-directional graph with Many:Many edges, adding or
|
|
453
|
+
removing a value requires only 2 value transitions.
|
|
454
|
+
|
|
455
|
+
For Adding
|
|
456
|
+
If: A<<->>B, C<<->>D is the initial state (double arrows representing the many side)
|
|
457
|
+
And: D<<->>C<<->>A<<->>B is the final state
|
|
458
|
+
|
|
459
|
+
Then we would undergo two transitions.
|
|
460
|
+
|
|
461
|
+
add C to A.
|
|
462
|
+
add A to C
|
|
463
|
+
|
|
464
|
+
For Removing
|
|
465
|
+
If: A<<->>B, C<<->>D is the initial state (double arrows representing the many side)
|
|
466
|
+
And: A, B, C<<->>D is the final state
|
|
467
|
+
|
|
468
|
+
Then we would undergo two transitions.
|
|
469
|
+
|
|
470
|
+
remove B from A
|
|
471
|
+
remove A from B
|
|
472
|
+
|
|
473
|
+
case many:?
|
|
474
|
+
========
|
|
475
|
+
In a uni-directional graph with Many:? edges (modeled in WarpDrive with `inverse:null`) with
|
|
476
|
+
artificial (implicit) inverses, replacing a value results in 2 discrete value transitions.
|
|
477
|
+
This is because a Many:? relationship is effectively Many:Many.
|
|
478
|
+
*/
|
|
479
|
+
function replaceRelatedRecords(graph, op, isRemote) {
|
|
480
|
+
if (isRemote) {
|
|
481
|
+
replaceRelatedRecordsRemote(graph, op, isRemote);
|
|
482
|
+
} else {
|
|
483
|
+
replaceRelatedRecordsLocal(graph, op, isRemote);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
function replaceRelatedRecordsLocal(graph, op, isRemote) {
|
|
487
|
+
const resourceKeys = op.value;
|
|
488
|
+
const relationship = graph.get(op.record, op.field);
|
|
489
|
+
relationship.state.hasReceivedData = true;
|
|
490
|
+
const {
|
|
491
|
+
additions,
|
|
492
|
+
removals
|
|
493
|
+
} = relationship;
|
|
494
|
+
const {
|
|
495
|
+
inverseKey,
|
|
496
|
+
type
|
|
497
|
+
} = relationship.definition;
|
|
498
|
+
const {
|
|
499
|
+
record
|
|
500
|
+
} = op;
|
|
501
|
+
const wasDirty = relationship.isDirty;
|
|
502
|
+
let localBecameDirty = false;
|
|
503
|
+
const onAdd = resourceKey => {
|
|
504
|
+
// Since we are diffing against the remote state, we check
|
|
505
|
+
// if our previous local state did not contain this ResourceKey
|
|
506
|
+
const removalsHas = removals?.has(resourceKey);
|
|
507
|
+
if (removalsHas || !additions?.has(resourceKey)) {
|
|
508
|
+
if (type !== resourceKey.type) {
|
|
509
|
+
graph.registerPolymorphicType(type, resourceKey.type);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// we've added a record locally that wasn't in the local state before
|
|
513
|
+
localBecameDirty = true;
|
|
514
|
+
addToInverse(graph, resourceKey, inverseKey, op.record, isRemote);
|
|
515
|
+
if (removalsHas) {
|
|
516
|
+
removals.delete(resourceKey);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
const onRemove = resourceKey => {
|
|
521
|
+
// Since we are diffing against the remote state, we check
|
|
522
|
+
// if our previous local state had contained this ResourceKey
|
|
523
|
+
const additionsHas = additions?.has(resourceKey);
|
|
524
|
+
if (additionsHas || !removals?.has(resourceKey)) {
|
|
525
|
+
// we've removed a record locally that was in the local state before
|
|
526
|
+
localBecameDirty = true;
|
|
527
|
+
removeFromInverse(graph, resourceKey, inverseKey, record, isRemote);
|
|
528
|
+
if (additionsHas) {
|
|
529
|
+
additions.delete(resourceKey);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
const diff = diffCollection(resourceKeys, relationship, onAdd, onRemove);
|
|
534
|
+
|
|
535
|
+
// any additions no longer in the local state
|
|
536
|
+
// also need to be removed from the inverse
|
|
537
|
+
if (additions && additions.size > 0) {
|
|
538
|
+
additions.forEach(resourceKey => {
|
|
539
|
+
if (!diff.add.has(resourceKey)) {
|
|
540
|
+
localBecameDirty = true;
|
|
541
|
+
onRemove(resourceKey);
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// any removals no longer in the local state
|
|
547
|
+
// also need to be added back to the inverse
|
|
548
|
+
if (removals && removals.size > 0) {
|
|
549
|
+
removals.forEach(resourceKey => {
|
|
550
|
+
if (!diff.del.has(resourceKey)) {
|
|
551
|
+
localBecameDirty = true;
|
|
552
|
+
onAdd(resourceKey);
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
const becameDirty = diff.changed || localBecameDirty;
|
|
557
|
+
relationship.additions = diff.add;
|
|
558
|
+
relationship.removals = diff.del;
|
|
559
|
+
relationship.localState = diff.finalState;
|
|
560
|
+
|
|
561
|
+
// we only notify if the localState changed and were not already dirty before
|
|
562
|
+
// because if we were already dirty then we have already notified
|
|
563
|
+
if (becameDirty && !wasDirty) {
|
|
564
|
+
notifyChange(graph, relationship);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
function replaceRelatedRecordsRemote(graph, op, isRemote) {
|
|
568
|
+
const resourceKeys = op.value;
|
|
569
|
+
const relationship = graph.get(op.record, op.field);
|
|
570
|
+
if (isRemote) {
|
|
571
|
+
graph._addToTransaction(relationship);
|
|
572
|
+
}
|
|
573
|
+
const wasDirty = relationship.isDirty;
|
|
574
|
+
// if this is our first time receiving data
|
|
575
|
+
// we need to mark the relationship as dirty
|
|
576
|
+
// so that non-materializing APIs like `hasManyReference.value()`
|
|
577
|
+
// will get notified and updated.
|
|
578
|
+
if (!relationship.state.hasReceivedData) {
|
|
579
|
+
relationship.isDirty = true;
|
|
580
|
+
}
|
|
581
|
+
relationship.state.hasReceivedData = true;
|
|
582
|
+
|
|
583
|
+
// cache existing state
|
|
584
|
+
const {
|
|
585
|
+
definition
|
|
586
|
+
} = relationship;
|
|
587
|
+
const {
|
|
588
|
+
type
|
|
589
|
+
} = relationship.definition;
|
|
590
|
+
const diff = diffCollection(resourceKeys, relationship, resourceKey => {
|
|
591
|
+
if (type !== resourceKey.type) {
|
|
592
|
+
graph.registerPolymorphicType(type, resourceKey.type);
|
|
593
|
+
}
|
|
594
|
+
// commit additions
|
|
595
|
+
// TODO build this into the diff?
|
|
596
|
+
// because we are not dirty if this was a committed local addition
|
|
597
|
+
if (relationship.additions?.has(resourceKey)) {
|
|
598
|
+
relationship.additions.delete(resourceKey);
|
|
599
|
+
}
|
|
600
|
+
addToInverse(graph, resourceKey, definition.inverseKey, op.record, isRemote);
|
|
601
|
+
}, resourceKey => {
|
|
602
|
+
// commit removals
|
|
603
|
+
// TODO build this into the diff?
|
|
604
|
+
// because we are not dirty if this was a committed local addition
|
|
605
|
+
if (relationship.removals?.has(resourceKey)) {
|
|
606
|
+
relationship.removals.delete(resourceKey);
|
|
607
|
+
}
|
|
608
|
+
removeFromInverse(graph, resourceKey, definition.inverseKey, op.record, isRemote);
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
// replace existing state
|
|
612
|
+
relationship.remoteMembers = diff.finalSet;
|
|
613
|
+
relationship.remoteState = diff.finalState;
|
|
614
|
+
|
|
615
|
+
// changed also indicates a change in order
|
|
616
|
+
if (diff.changed) {
|
|
617
|
+
relationship.isDirty = true;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// TODO unsure if we need this but it
|
|
621
|
+
// may allow us to more efficiently patch
|
|
622
|
+
// the associated ManyArray
|
|
623
|
+
relationship._diff = diff;
|
|
624
|
+
{
|
|
625
|
+
// only do this for legacy hasMany, not collection
|
|
626
|
+
// and provide a way to incrementally migrate
|
|
627
|
+
if (
|
|
628
|
+
// we do not guard by diff.changed here
|
|
629
|
+
// because we want to clear local changes even if
|
|
630
|
+
// no change has occurred to preserve the legacy behavior
|
|
631
|
+
relationship.definition.kind === 'hasMany' && relationship.definition.resetOnRemoteUpdate !== false && (diff.changed || wasDirty)) {
|
|
632
|
+
if (relationship.removals) {
|
|
633
|
+
relationship.isDirty = true;
|
|
634
|
+
relationship.removals.forEach(resourceKey => {
|
|
635
|
+
// reverse the removal
|
|
636
|
+
// if we are still in removals at this point then
|
|
637
|
+
// we were not "committed" which means we are present
|
|
638
|
+
// in the remoteMembers. So we "add back" on the inverse.
|
|
639
|
+
addToInverse(graph, resourceKey, definition.inverseKey, op.record, false);
|
|
640
|
+
});
|
|
641
|
+
relationship.removals = null;
|
|
642
|
+
}
|
|
643
|
+
if (relationship.additions) {
|
|
644
|
+
relationship.additions.forEach(resourceKey => {
|
|
645
|
+
// reverse the addition
|
|
646
|
+
// if we are still in additions at this point then
|
|
647
|
+
// we were not "committed" which means we are not present
|
|
648
|
+
// in the remoteMembers. So we "remove" from the inverse.
|
|
649
|
+
// however we only do this if we are not a "new" record.
|
|
650
|
+
if (!checkIfNew(graph._realStore, resourceKey)) {
|
|
651
|
+
relationship.isDirty = true;
|
|
652
|
+
relationship.additions.delete(resourceKey);
|
|
653
|
+
removeFromInverse(graph, resourceKey, definition.inverseKey, op.record, false);
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
if (relationship.additions.size === 0) {
|
|
657
|
+
relationship.additions = null;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
if (relationship.isDirty && !wasDirty) {
|
|
663
|
+
flushCanonical(graph, relationship);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
function addToInverse(graph, resourceKey, key, value, isRemote) {
|
|
667
|
+
const relationship = graph.get(resourceKey, key);
|
|
668
|
+
const {
|
|
669
|
+
type
|
|
670
|
+
} = relationship.definition;
|
|
671
|
+
if (type !== value.type) {
|
|
672
|
+
graph.registerPolymorphicType(type, value.type);
|
|
673
|
+
}
|
|
674
|
+
if (isBelongsTo(relationship)) {
|
|
675
|
+
relationship.state.hasReceivedData = true;
|
|
676
|
+
relationship.state.isEmpty = false;
|
|
677
|
+
if (isRemote) {
|
|
678
|
+
graph._addToTransaction(relationship);
|
|
679
|
+
if (relationship.remoteState !== null) {
|
|
680
|
+
removeFromInverse(graph, relationship.remoteState, relationship.definition.inverseKey, resourceKey, isRemote);
|
|
681
|
+
}
|
|
682
|
+
relationship.remoteState = value;
|
|
683
|
+
}
|
|
684
|
+
if (relationship.localState !== value) {
|
|
685
|
+
if (!isRemote && relationship.localState) {
|
|
686
|
+
removeFromInverse(graph, relationship.localState, relationship.definition.inverseKey, resourceKey, isRemote);
|
|
687
|
+
}
|
|
688
|
+
relationship.localState = value;
|
|
689
|
+
notifyChange(graph, relationship);
|
|
690
|
+
}
|
|
691
|
+
} else if (isHasMany(relationship)) {
|
|
692
|
+
if (isRemote) {
|
|
693
|
+
// TODO this needs to alert stuffs
|
|
694
|
+
// And patch state better
|
|
695
|
+
// This is almost definitely wrong
|
|
696
|
+
// WARNING WARNING WARNING
|
|
697
|
+
|
|
698
|
+
if (!relationship.remoteMembers.has(value)) {
|
|
699
|
+
graph._addToTransaction(relationship);
|
|
700
|
+
relationship.remoteState.push(value);
|
|
701
|
+
relationship.remoteMembers.add(value);
|
|
702
|
+
if (relationship.additions?.has(value)) {
|
|
703
|
+
relationship.additions.delete(value);
|
|
704
|
+
} else {
|
|
705
|
+
relationship.isDirty = true;
|
|
706
|
+
relationship.state.hasReceivedData = true;
|
|
707
|
+
flushCanonical(graph, relationship);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
} else {
|
|
711
|
+
// if we are not dirty but have a null localState then we
|
|
712
|
+
// are mutating a relationship that has never been fetched
|
|
713
|
+
// so we initialize localState to an empty array
|
|
714
|
+
if (!relationship.isDirty && !relationship.localState) {
|
|
715
|
+
relationship.localState = [];
|
|
716
|
+
}
|
|
717
|
+
if (_add(graph, resourceKey, relationship, value, null, isRemote)) {
|
|
718
|
+
notifyChange(graph, relationship);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
} else {
|
|
722
|
+
if (isRemote) {
|
|
723
|
+
if (!relationship.remoteMembers.has(value)) {
|
|
724
|
+
relationship.remoteMembers.add(value);
|
|
725
|
+
relationship.localMembers.add(value);
|
|
726
|
+
}
|
|
727
|
+
} else {
|
|
728
|
+
if (!relationship.localMembers.has(value)) {
|
|
729
|
+
relationship.localMembers.add(value);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
function notifyInverseOfPotentialMaterialization(graph, resourceKey, key, value, isRemote) {
|
|
735
|
+
const relationship = graph.get(resourceKey, key);
|
|
736
|
+
if (isHasMany(relationship) && isRemote && relationship.remoteMembers.has(value)) {
|
|
737
|
+
notifyChange(graph, relationship);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
function removeFromInverse(graph, resourceKey, key, value, isRemote) {
|
|
741
|
+
const relationship = graph.get(resourceKey, key);
|
|
742
|
+
if (isBelongsTo(relationship)) {
|
|
743
|
+
relationship.state.isEmpty = true;
|
|
744
|
+
if (isRemote) {
|
|
745
|
+
graph._addToTransaction(relationship);
|
|
746
|
+
relationship.remoteState = null;
|
|
747
|
+
}
|
|
748
|
+
if (relationship.localState === value) {
|
|
749
|
+
relationship.localState = null;
|
|
750
|
+
notifyChange(graph, relationship);
|
|
751
|
+
}
|
|
752
|
+
} else if (isHasMany(relationship)) {
|
|
753
|
+
if (isRemote) {
|
|
754
|
+
graph._addToTransaction(relationship);
|
|
755
|
+
if (_removeRemote(relationship, value)) {
|
|
756
|
+
notifyChange(graph, relationship);
|
|
757
|
+
}
|
|
758
|
+
} else {
|
|
759
|
+
if (_removeLocal(relationship, value)) {
|
|
760
|
+
notifyChange(graph, relationship);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
} else {
|
|
764
|
+
if (isRemote) {
|
|
765
|
+
relationship.remoteMembers.delete(value);
|
|
766
|
+
relationship.localMembers.delete(value);
|
|
767
|
+
} else {
|
|
768
|
+
if (value && relationship.localMembers.has(value)) {
|
|
769
|
+
relationship.localMembers.delete(value);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
function flushCanonical(graph, rel) {
|
|
775
|
+
if (rel.accessed) {
|
|
776
|
+
graph._scheduleLocalSync(rel);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
function replaceRelatedRecord(graph, op, isRemote = false) {
|
|
780
|
+
const relationship = graph.get(op.record, op.field);
|
|
781
|
+
if (isRemote) {
|
|
782
|
+
graph._addToTransaction(relationship);
|
|
783
|
+
}
|
|
784
|
+
const {
|
|
785
|
+
definition,
|
|
786
|
+
state
|
|
787
|
+
} = relationship;
|
|
788
|
+
const prop = isRemote ? 'remoteState' : 'localState';
|
|
789
|
+
const existingState = relationship[prop];
|
|
790
|
+
|
|
791
|
+
/*
|
|
792
|
+
case 1:1
|
|
793
|
+
========
|
|
794
|
+
In a bi-directional graph with 1:1 edges, replacing a value
|
|
795
|
+
results in up-to 4 discrete value transitions.
|
|
796
|
+
If: A <-> B, C <-> D is the initial state,
|
|
797
|
+
and: A <-> C, B, D is the final state
|
|
798
|
+
then we would undergo the following 4 transitions.
|
|
799
|
+
remove A from B
|
|
800
|
+
add C to A
|
|
801
|
+
remove C from D
|
|
802
|
+
add A to C
|
|
803
|
+
case 1:many
|
|
804
|
+
===========
|
|
805
|
+
In a bi-directional graph with 1:Many edges, replacing a value
|
|
806
|
+
results in up-to 3 discrete value transitions.
|
|
807
|
+
If: A<->>B<<->D, C<<->D is the initial state (double arrows representing the many side)
|
|
808
|
+
And: A<->>C<<->D, B<<->D is the final state
|
|
809
|
+
Then we would undergo three transitions.
|
|
810
|
+
remove A from B
|
|
811
|
+
add C to A.
|
|
812
|
+
add A to C
|
|
813
|
+
case 1:?
|
|
814
|
+
========
|
|
815
|
+
In a uni-directional graph with 1:? edges (modeled in WarpDrive with `inverse:null`) with
|
|
816
|
+
artificial (implicit) inverses, replacing a value results in up-to 3 discrete value transitions.
|
|
817
|
+
This is because a 1:? relationship is effectively 1:many.
|
|
818
|
+
If: A->B, C->B is the initial state
|
|
819
|
+
And: A->C, C->B is the final state
|
|
820
|
+
Then we would undergo three transitions.
|
|
821
|
+
Remove A from B
|
|
822
|
+
Add C to A
|
|
823
|
+
Add A to C
|
|
824
|
+
*/
|
|
825
|
+
|
|
826
|
+
// nothing for us to do
|
|
827
|
+
if (op.value === existingState) {
|
|
828
|
+
// if we were empty before but now know we are empty this needs to be true
|
|
829
|
+
state.hasReceivedData = true;
|
|
830
|
+
// if this is a remote update we still sync
|
|
831
|
+
if (isRemote) {
|
|
832
|
+
const {
|
|
833
|
+
localState
|
|
834
|
+
} = relationship;
|
|
835
|
+
// don't sync if localState is a new record and our remoteState is null
|
|
836
|
+
if (localState && checkIfNew(graph._realStore, localState) && !existingState) {
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
if (existingState && localState === existingState) {
|
|
840
|
+
notifyInverseOfPotentialMaterialization(graph, existingState, definition.inverseKey, op.record, isRemote);
|
|
841
|
+
} else {
|
|
842
|
+
// if localState does not match existingState then we know
|
|
843
|
+
// we have a local mutation that has not been persisted yet
|
|
844
|
+
if (localState !== op.value && relationship.definition.resetOnRemoteUpdate !== false) {
|
|
845
|
+
relationship.localState = existingState;
|
|
846
|
+
notifyChange(graph, relationship);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// remove this value from the inverse if required
|
|
854
|
+
if (existingState) {
|
|
855
|
+
removeFromInverse(graph, existingState, definition.inverseKey, op.record, isRemote);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// update value to the new value
|
|
859
|
+
relationship[prop] = op.value;
|
|
860
|
+
state.hasReceivedData = true;
|
|
861
|
+
state.isEmpty = op.value === null;
|
|
862
|
+
state.isStale = false;
|
|
863
|
+
state.hasFailedLoadAttempt = false;
|
|
864
|
+
if (op.value) {
|
|
865
|
+
if (definition.type !== op.value.type) {
|
|
866
|
+
// assert(
|
|
867
|
+
// `The '<${definition.inverseType}>.${op.field}' relationship expects only '${definition.type}' records since it is not polymorphic. Received a Record of type '${op.value.type}'`,
|
|
868
|
+
// definition.isPolymorphic
|
|
869
|
+
// );
|
|
870
|
+
|
|
871
|
+
// TODO this should now handle the deprecation warning if isPolymorphic is not set
|
|
872
|
+
// but the record does turn out to be polymorphic
|
|
873
|
+
// this should still assert if the user is relying on legacy inheritance/mixins to
|
|
874
|
+
// provide polymorphic behavior and has not yet added the polymorphic flags
|
|
875
|
+
|
|
876
|
+
graph.registerPolymorphicType(definition.type, op.value.type);
|
|
877
|
+
}
|
|
878
|
+
addToInverse(graph, op.value, definition.inverseKey, op.record, isRemote);
|
|
879
|
+
}
|
|
880
|
+
if (isRemote) {
|
|
881
|
+
const {
|
|
882
|
+
localState,
|
|
883
|
+
remoteState
|
|
884
|
+
} = relationship;
|
|
885
|
+
if (localState && checkIfNew(graph._realStore, localState) && !remoteState) {
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
// when localState does not match the new remoteState and
|
|
889
|
+
// localState === existingState then we had no local mutation
|
|
890
|
+
// and we can safely sync the new remoteState to local
|
|
891
|
+
if (localState !== remoteState && localState === existingState) {
|
|
892
|
+
relationship.localState = remoteState;
|
|
893
|
+
notifyChange(graph, relationship);
|
|
894
|
+
// But when localState does not match the new remoteState and
|
|
895
|
+
// and localState !== existingState then we know we have a local mutation
|
|
896
|
+
// that has not been persisted yet.
|
|
897
|
+
} else {
|
|
898
|
+
if (localState !== remoteState && localState !== existingState && relationship.definition.resetOnRemoteUpdate !== false) {
|
|
899
|
+
relationship.localState = remoteState;
|
|
900
|
+
notifyChange(graph, relationship);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
} else {
|
|
904
|
+
notifyChange(graph, relationship);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
function _deprecatedCompare(priorLocalState, newState, newMembers, prevState, prevSet, onAdd, onDel, remoteClearsLocal) {
|
|
908
|
+
const newLength = newState.length;
|
|
909
|
+
const prevLength = prevState.length;
|
|
910
|
+
const iterationLength = Math.max(newLength, prevLength);
|
|
911
|
+
let changed = newMembers.size !== prevSet.size;
|
|
912
|
+
let remoteOrderChanged = false;
|
|
913
|
+
const added = new Set();
|
|
914
|
+
const removed = new Set();
|
|
915
|
+
const duplicates = new Map();
|
|
916
|
+
const finalSet = new Set();
|
|
917
|
+
const finalState = [];
|
|
918
|
+
const priorLocalLength = priorLocalState?.length ?? 0;
|
|
919
|
+
for (let i = 0, j = 0; i < iterationLength; i++) {
|
|
920
|
+
let adv = false;
|
|
921
|
+
let member;
|
|
922
|
+
|
|
923
|
+
// accumulate anything added
|
|
924
|
+
if (i < newLength) {
|
|
925
|
+
member = newState[i];
|
|
926
|
+
if (!finalSet.has(member)) {
|
|
927
|
+
finalState[j] = member;
|
|
928
|
+
finalSet.add(member);
|
|
929
|
+
adv = true;
|
|
930
|
+
if (!prevSet.has(member)) {
|
|
931
|
+
// Avoid unnecessarily notifying a change that already exists locally
|
|
932
|
+
if (i < priorLocalLength) {
|
|
933
|
+
const priorLocalMember = priorLocalState[i];
|
|
934
|
+
if (priorLocalMember !== member) {
|
|
935
|
+
changed = true;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
added.add(member);
|
|
939
|
+
onAdd(member);
|
|
940
|
+
}
|
|
941
|
+
} else {
|
|
942
|
+
let list = duplicates.get(member);
|
|
943
|
+
if (list === undefined) {
|
|
944
|
+
list = [];
|
|
945
|
+
duplicates.set(member, list);
|
|
946
|
+
}
|
|
947
|
+
list.push(i);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// accumulate anything removed
|
|
952
|
+
if (i < prevLength) {
|
|
953
|
+
const prevMember = prevState[i];
|
|
954
|
+
|
|
955
|
+
// detect reordering, adjusting index for duplicates
|
|
956
|
+
// j is always less than i and so if i < prevLength, j < prevLength
|
|
957
|
+
if (member !== prevState[j]) {
|
|
958
|
+
// the new remote order does not match the current remote order
|
|
959
|
+
// indicating a change in membership or reordering
|
|
960
|
+
remoteOrderChanged = true;
|
|
961
|
+
// however: if the new remote order matches the current local order
|
|
962
|
+
// we can disregard the change notification generation so long as
|
|
963
|
+
// we are not configured to reset on remote update (which is deprecated)
|
|
964
|
+
{
|
|
965
|
+
if (!remoteClearsLocal && i < priorLocalLength) {
|
|
966
|
+
const priorLocalMember = priorLocalState[j];
|
|
967
|
+
if (priorLocalMember !== member) {
|
|
968
|
+
changed = true;
|
|
969
|
+
}
|
|
970
|
+
} else {
|
|
971
|
+
changed = true;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// if remote order hasn't changed but local order differs
|
|
976
|
+
// and we are configured to reset on remote update (which is deprecated)
|
|
977
|
+
// then we still need to mark the relationship as changed
|
|
978
|
+
} else {
|
|
979
|
+
if (remoteClearsLocal) {
|
|
980
|
+
if (!changed && j < priorLocalLength) {
|
|
981
|
+
const priorLocalMember = priorLocalState[j];
|
|
982
|
+
if (priorLocalMember !== member) {
|
|
983
|
+
changed = true;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
if (!newMembers.has(prevMember)) {
|
|
989
|
+
changed = true;
|
|
990
|
+
removed.add(prevMember);
|
|
991
|
+
onDel(prevMember);
|
|
992
|
+
}
|
|
993
|
+
} else if (adv && j < prevLength && member !== prevState[j]) {
|
|
994
|
+
changed = true;
|
|
995
|
+
}
|
|
996
|
+
if (adv) {
|
|
997
|
+
j++;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
const diff = {
|
|
1001
|
+
add: added,
|
|
1002
|
+
del: removed,
|
|
1003
|
+
finalState,
|
|
1004
|
+
finalSet,
|
|
1005
|
+
changed,
|
|
1006
|
+
remoteOrderChanged
|
|
1007
|
+
};
|
|
1008
|
+
return {
|
|
1009
|
+
diff,
|
|
1010
|
+
duplicates
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
function _compare(priorLocalState, finalState, finalSet, prevState, prevSet, onAdd, onDel, remoteClearsLocal) {
|
|
1014
|
+
const finalLength = finalState.length;
|
|
1015
|
+
const prevLength = prevState.length;
|
|
1016
|
+
const iterationLength = Math.max(finalLength, prevLength);
|
|
1017
|
+
const equalLength = priorLocalState ? finalLength === priorLocalState.length : finalLength === prevLength;
|
|
1018
|
+
let remoteOrderChanged = finalSet.size !== prevSet.size;
|
|
1019
|
+
let changed = priorLocalState ? finalSet.size !== priorLocalState.length : remoteOrderChanged;
|
|
1020
|
+
const added = new Set();
|
|
1021
|
+
const removed = new Set();
|
|
1022
|
+
const priorLocalLength = priorLocalState?.length ?? 0;
|
|
1023
|
+
for (let i = 0; i < iterationLength; i++) {
|
|
1024
|
+
let member;
|
|
1025
|
+
|
|
1026
|
+
// accumulate anything added
|
|
1027
|
+
if (i < finalLength) {
|
|
1028
|
+
member = finalState[i];
|
|
1029
|
+
if (!prevSet.has(member)) {
|
|
1030
|
+
// Avoid unnecessarily notifying a change that already exists locally
|
|
1031
|
+
if (i < priorLocalLength) {
|
|
1032
|
+
const priorLocalMember = priorLocalState[i];
|
|
1033
|
+
if (priorLocalMember !== member) {
|
|
1034
|
+
changed = true;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
added.add(member);
|
|
1038
|
+
onAdd(member);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// accumulate anything removed
|
|
1043
|
+
if (i < prevLength) {
|
|
1044
|
+
const prevMember = prevState[i];
|
|
1045
|
+
|
|
1046
|
+
// detect reordering
|
|
1047
|
+
if (equalLength && member !== prevMember) {
|
|
1048
|
+
// the new remote order does not match the current remote order
|
|
1049
|
+
// indicating a change in membership or reordering
|
|
1050
|
+
remoteOrderChanged = true;
|
|
1051
|
+
// however: if the new remote order matches the current local order
|
|
1052
|
+
// we can disregard the change notification generation so long as
|
|
1053
|
+
// we are not configured to reset on remote update (which is deprecated)
|
|
1054
|
+
|
|
1055
|
+
if (i < priorLocalLength) {
|
|
1056
|
+
const priorLocalMember = priorLocalState[i];
|
|
1057
|
+
if (priorLocalMember !== member) {
|
|
1058
|
+
changed = true;
|
|
1059
|
+
}
|
|
1060
|
+
} else if (i < finalLength) {
|
|
1061
|
+
// if we have exceeded the length of priorLocalState and we are within the range
|
|
1062
|
+
// of the finalState then we must have changed
|
|
1063
|
+
|
|
1064
|
+
changed = true;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// if remote order hasn't changed but local order differs
|
|
1068
|
+
// and we are configured to reset on remote update (which is deprecated)
|
|
1069
|
+
// then we still need to mark the relationship as changed
|
|
1070
|
+
} else {
|
|
1071
|
+
if (remoteClearsLocal) {
|
|
1072
|
+
if (equalLength && !changed && i < priorLocalLength) {
|
|
1073
|
+
const priorLocalMember = priorLocalState[i];
|
|
1074
|
+
if (priorLocalMember !== prevMember) {
|
|
1075
|
+
changed = true;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
if (!finalSet.has(prevMember)) {
|
|
1081
|
+
// if we are within finalLength, we can only be "changed" if we've already exceeded
|
|
1082
|
+
// the index range of priorLocalState, as otherwise the previous member may still
|
|
1083
|
+
// be removed.
|
|
1084
|
+
//
|
|
1085
|
+
// prior local: [1, 2, 3, 4]
|
|
1086
|
+
// final state: [1, 2, 3]
|
|
1087
|
+
// prev remote state: [1, 2, 5, 3, 4]
|
|
1088
|
+
// i === 2
|
|
1089
|
+
// prevMember === 5
|
|
1090
|
+
// !finalSet.has(prevMember) === true
|
|
1091
|
+
//
|
|
1092
|
+
// because we will become changed at i===3,
|
|
1093
|
+
// we do not need to worry about becoming changed at i===2
|
|
1094
|
+
// as the arrays until now are still the same
|
|
1095
|
+
//
|
|
1096
|
+
// prior local: [1, 2, 3]
|
|
1097
|
+
// final state: [1, 2, 3, 4]
|
|
1098
|
+
// prev remote state: [1, 2, 5, 3, 4]
|
|
1099
|
+
// i === 2
|
|
1100
|
+
// prevMember === 5
|
|
1101
|
+
// !finalSet.has(prevMember) === true
|
|
1102
|
+
//
|
|
1103
|
+
// because we will become changed at i===3
|
|
1104
|
+
// we do not need to worry about becoming changed at i===2
|
|
1105
|
+
//
|
|
1106
|
+
// prior local: [1, 2, 3]
|
|
1107
|
+
// final state: [1, 2, 3]
|
|
1108
|
+
// prev remote state: [1, 2, 5, 3, 4]
|
|
1109
|
+
// i === 2
|
|
1110
|
+
// prevMember === 5
|
|
1111
|
+
// !finalSet.has(prevMember) === true
|
|
1112
|
+
//
|
|
1113
|
+
// because we have same length and same membership order
|
|
1114
|
+
// we do not need to worry about becoming changed at i===2
|
|
1115
|
+
//
|
|
1116
|
+
// if you do not have a priorLocalState you can't be changed
|
|
1117
|
+
// ergo, we never need to set changed in this branch.
|
|
1118
|
+
// this log can still be useful for debugging.
|
|
1119
|
+
|
|
1120
|
+
//
|
|
1121
|
+
// we do still set remoteOrderChanged as it has
|
|
1122
|
+
remoteOrderChanged = true;
|
|
1123
|
+
removed.add(prevMember);
|
|
1124
|
+
onDel(prevMember);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
return {
|
|
1129
|
+
add: added,
|
|
1130
|
+
del: removed,
|
|
1131
|
+
finalState,
|
|
1132
|
+
finalSet,
|
|
1133
|
+
changed,
|
|
1134
|
+
remoteOrderChanged
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
function diffCollection(finalState, relationship, onAdd, onDel) {
|
|
1138
|
+
const finalSet = new Set(finalState);
|
|
1139
|
+
const {
|
|
1140
|
+
localState: priorLocalState,
|
|
1141
|
+
remoteState,
|
|
1142
|
+
remoteMembers
|
|
1143
|
+
} = relationship;
|
|
1144
|
+
{
|
|
1145
|
+
if (finalState.length !== finalSet.size) {
|
|
1146
|
+
const {
|
|
1147
|
+
diff
|
|
1148
|
+
} = _deprecatedCompare(priorLocalState, finalState, finalSet, remoteState, remoteMembers, onAdd, onDel, relationship.definition.resetOnRemoteUpdate);
|
|
1149
|
+
return diff;
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
return _compare(priorLocalState, finalState, finalSet, remoteState, remoteMembers, onAdd, onDel, relationship.definition.resetOnRemoteUpdate);
|
|
1153
|
+
}
|
|
1154
|
+
function computeLocalState(storage) {
|
|
1155
|
+
if (!storage.isDirty) {
|
|
1156
|
+
return storage.localState;
|
|
1157
|
+
}
|
|
1158
|
+
const state = storage.remoteState.slice();
|
|
1159
|
+
storage.removals?.forEach(v => {
|
|
1160
|
+
const index = state.indexOf(v);
|
|
1161
|
+
state.splice(index, 1);
|
|
1162
|
+
});
|
|
1163
|
+
storage.additions?.forEach(v => {
|
|
1164
|
+
state.push(v);
|
|
1165
|
+
});
|
|
1166
|
+
storage.localState = state;
|
|
1167
|
+
storage.isDirty = false;
|
|
1168
|
+
return state;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
/**
|
|
1172
|
+
* A function which attempts to add a value to the local state of a collection
|
|
1173
|
+
* relationship, and returns true if the value was added, or false if it was
|
|
1174
|
+
* already present.
|
|
1175
|
+
*
|
|
1176
|
+
* It will not generate a notification, will not update the relationships to dirty,
|
|
1177
|
+
* and will not update the inverse relationships, making it suitable for use as
|
|
1178
|
+
* an internal util to perform the just the addition to a specific side of a
|
|
1179
|
+
* relationship.
|
|
1180
|
+
*
|
|
1181
|
+
* @internal
|
|
1182
|
+
*/
|
|
1183
|
+
function _add(graph, record, relationship, value, index, isRemote) {
|
|
1184
|
+
return !isRemote ? _addLocal(graph, record, relationship, value, index) : _addRemote(graph, record, relationship, value, index);
|
|
1185
|
+
}
|
|
1186
|
+
function _addRemote(graph, record, relationship, value, index) {
|
|
1187
|
+
const {
|
|
1188
|
+
remoteMembers,
|
|
1189
|
+
additions,
|
|
1190
|
+
removals,
|
|
1191
|
+
remoteState
|
|
1192
|
+
} = relationship;
|
|
1193
|
+
if (remoteMembers.has(value)) {
|
|
1194
|
+
return false;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
// add to the remote state
|
|
1198
|
+
remoteMembers.add(value);
|
|
1199
|
+
const hasValidIndex = index !== null && index >= 0 && index < remoteState.length;
|
|
1200
|
+
if (hasValidIndex) {
|
|
1201
|
+
remoteState.splice(index, 0, value);
|
|
1202
|
+
} else {
|
|
1203
|
+
remoteState.push(value);
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
// remove from additions if present
|
|
1207
|
+
if (additions?.has(value)) {
|
|
1208
|
+
additions.delete(value);
|
|
1209
|
+
|
|
1210
|
+
// nothing more to do this was our state already
|
|
1211
|
+
return false;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
// if the relationship already needs to recalc, we don't bother
|
|
1215
|
+
// attempting to patch the localState
|
|
1216
|
+
if (relationship.isDirty) {
|
|
1217
|
+
return true;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
// if we have existing localState
|
|
1221
|
+
// we attempt to patch it without blowing it away
|
|
1222
|
+
// as this is more efficient than recomputing
|
|
1223
|
+
// it allows us to preserve local ordering
|
|
1224
|
+
// to a small extent. Local ordering should not
|
|
1225
|
+
// be relied upon as any remote change could blow it away
|
|
1226
|
+
if (relationship.localState) {
|
|
1227
|
+
if (!hasValidIndex) {
|
|
1228
|
+
relationship.localState.push(value);
|
|
1229
|
+
} else if (index === 0) {
|
|
1230
|
+
relationship.localState.unshift(value);
|
|
1231
|
+
} else if (!removals?.size) {
|
|
1232
|
+
relationship.localState.splice(index, 0, value);
|
|
1233
|
+
} else {
|
|
1234
|
+
relationship.isDirty = true;
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
return true;
|
|
1238
|
+
}
|
|
1239
|
+
function _addLocal(graph, record, relationship, value, index) {
|
|
1240
|
+
const {
|
|
1241
|
+
remoteMembers,
|
|
1242
|
+
removals
|
|
1243
|
+
} = relationship;
|
|
1244
|
+
let additions = relationship.additions;
|
|
1245
|
+
const hasPresence = remoteMembers.has(value) || additions?.has(value);
|
|
1246
|
+
if (hasPresence && !removals?.has(value)) {
|
|
1247
|
+
return false;
|
|
1248
|
+
}
|
|
1249
|
+
if (removals?.has(value)) {
|
|
1250
|
+
removals.delete(value);
|
|
1251
|
+
} else {
|
|
1252
|
+
if (!additions) {
|
|
1253
|
+
additions = relationship.additions = new Set();
|
|
1254
|
+
}
|
|
1255
|
+
relationship.state.hasReceivedData = true;
|
|
1256
|
+
additions.add(value);
|
|
1257
|
+
const {
|
|
1258
|
+
type
|
|
1259
|
+
} = relationship.definition;
|
|
1260
|
+
if (type !== value.type) {
|
|
1261
|
+
graph.registerPolymorphicType(value.type, type);
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
// if we have existing localState
|
|
1266
|
+
// and we have an index
|
|
1267
|
+
// apply the change, as this is more efficient
|
|
1268
|
+
// than recomputing localState and
|
|
1269
|
+
// it allows us to preserve local ordering
|
|
1270
|
+
// to a small extend. Local ordering should not
|
|
1271
|
+
// be relied upon as any remote change will blow it away
|
|
1272
|
+
if (relationship.localState) {
|
|
1273
|
+
if (index !== null) {
|
|
1274
|
+
relationship.localState.splice(index, 0, value);
|
|
1275
|
+
} else {
|
|
1276
|
+
relationship.localState.push(value);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
return true;
|
|
1280
|
+
}
|
|
1281
|
+
function _remove(graph, record, relationship, value, index, isRemote) {
|
|
1282
|
+
return !isRemote ? _removeLocal(relationship, value) : _removeRemote(relationship, value);
|
|
1283
|
+
}
|
|
1284
|
+
function _removeLocal(relationship, value) {
|
|
1285
|
+
const {
|
|
1286
|
+
remoteMembers,
|
|
1287
|
+
additions
|
|
1288
|
+
} = relationship;
|
|
1289
|
+
let removals = relationship.removals;
|
|
1290
|
+
const hasPresence = remoteMembers.has(value) || additions?.has(value);
|
|
1291
|
+
if (!hasPresence || removals?.has(value)) {
|
|
1292
|
+
return false;
|
|
1293
|
+
}
|
|
1294
|
+
if (additions?.has(value)) {
|
|
1295
|
+
additions.delete(value);
|
|
1296
|
+
} else {
|
|
1297
|
+
if (!removals) {
|
|
1298
|
+
removals = relationship.removals = new Set();
|
|
1299
|
+
}
|
|
1300
|
+
removals.add(value);
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
// if we have existing localState
|
|
1304
|
+
// apply the change, as this is more efficient
|
|
1305
|
+
// than recomputing localState and
|
|
1306
|
+
// it allows us to preserve local ordering
|
|
1307
|
+
// to a small extend. Local ordering should not
|
|
1308
|
+
// be relied upon as any remote change will blow it away
|
|
1309
|
+
if (relationship.localState) {
|
|
1310
|
+
const index = relationship.localState.indexOf(value);
|
|
1311
|
+
relationship.localState.splice(index, 1);
|
|
1312
|
+
}
|
|
1313
|
+
return true;
|
|
1314
|
+
}
|
|
1315
|
+
function _removeRemote(relationship, value) {
|
|
1316
|
+
const {
|
|
1317
|
+
remoteMembers,
|
|
1318
|
+
additions,
|
|
1319
|
+
removals,
|
|
1320
|
+
remoteState
|
|
1321
|
+
} = relationship;
|
|
1322
|
+
if (!remoteMembers.has(value)) {
|
|
1323
|
+
return false;
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
// remove from remote state
|
|
1327
|
+
remoteMembers.delete(value);
|
|
1328
|
+
let index = remoteState.indexOf(value);
|
|
1329
|
+
remoteState.splice(index, 1);
|
|
1330
|
+
|
|
1331
|
+
// remove from removals if present
|
|
1332
|
+
if (removals?.has(value)) {
|
|
1333
|
+
removals.delete(value);
|
|
1334
|
+
|
|
1335
|
+
// nothing more to do this was our state already
|
|
1336
|
+
return false;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
// if we have existing localState
|
|
1340
|
+
// and we have an index
|
|
1341
|
+
// apply the change, as this is more efficient
|
|
1342
|
+
// than recomputing localState and
|
|
1343
|
+
// it allows us to preserve local ordering
|
|
1344
|
+
// to a small extend. Local ordering should not
|
|
1345
|
+
// be relied upon as any remote change will blow it away
|
|
1346
|
+
if (relationship.localState) {
|
|
1347
|
+
index = relationship.localState.indexOf(value);
|
|
1348
|
+
relationship.localState.splice(index, 1);
|
|
1349
|
+
}
|
|
1350
|
+
return true;
|
|
1351
|
+
}
|
|
1352
|
+
function rollbackRelationship(graph, key, field, relationship) {
|
|
1353
|
+
if (isBelongsTo(relationship)) {
|
|
1354
|
+
replaceRelatedRecord(graph, {
|
|
1355
|
+
record: key,
|
|
1356
|
+
field,
|
|
1357
|
+
value: relationship.remoteState
|
|
1358
|
+
}, false);
|
|
1359
|
+
} else {
|
|
1360
|
+
replaceRelatedRecords(graph, {
|
|
1361
|
+
record: key,
|
|
1362
|
+
field,
|
|
1363
|
+
value: relationship.remoteState.slice()
|
|
1364
|
+
}, false);
|
|
1365
|
+
|
|
1366
|
+
// when the change was a "reorder" only we wont have generated
|
|
1367
|
+
// a notification yet.
|
|
1368
|
+
// if we give rollback a unique operation we can use the ability of
|
|
1369
|
+
// diff to report a separate `remoteOrderChanged` flag to trigger this
|
|
1370
|
+
// if needed to avoid the duplicate.
|
|
1371
|
+
notifyChange(graph, relationship);
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
function createState() {
|
|
1375
|
+
return {
|
|
1376
|
+
hasReceivedData: false,
|
|
1377
|
+
isEmpty: true,
|
|
1378
|
+
isStale: false,
|
|
1379
|
+
hasFailedLoadAttempt: false,
|
|
1380
|
+
shouldForceReload: false,
|
|
1381
|
+
hasDematerializedInverse: false
|
|
1382
|
+
};
|
|
1383
|
+
}
|
|
1384
|
+
function createCollectionEdge(definition, identifier) {
|
|
1385
|
+
return {
|
|
1386
|
+
definition,
|
|
1387
|
+
identifier,
|
|
1388
|
+
state: createState(),
|
|
1389
|
+
remoteMembers: new Set(),
|
|
1390
|
+
remoteState: [],
|
|
1391
|
+
additions: null,
|
|
1392
|
+
removals: null,
|
|
1393
|
+
meta: null,
|
|
1394
|
+
links: null,
|
|
1395
|
+
localState: null,
|
|
1396
|
+
isDirty: false,
|
|
1397
|
+
transactionRef: 0,
|
|
1398
|
+
accessed: false,
|
|
1399
|
+
_diff: undefined
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
1402
|
+
function legacyGetCollectionRelationshipData(source, getRemoteState) {
|
|
1403
|
+
source.accessed = true;
|
|
1404
|
+
const payload = {};
|
|
1405
|
+
if (source.state.hasReceivedData) {
|
|
1406
|
+
payload.data = getRemoteState ? source.remoteState.slice() : computeLocalState(source);
|
|
1407
|
+
}
|
|
1408
|
+
if (source.links) {
|
|
1409
|
+
payload.links = source.links;
|
|
1410
|
+
}
|
|
1411
|
+
if (source.meta) {
|
|
1412
|
+
payload.meta = source.meta;
|
|
1413
|
+
}
|
|
1414
|
+
return payload;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
/**
|
|
1418
|
+
Implicit relationships are relationships which have not been declared but the inverse side exists on
|
|
1419
|
+
another record somewhere
|
|
1420
|
+
|
|
1421
|
+
For example consider the following two models
|
|
1422
|
+
|
|
1423
|
+
::: code-group
|
|
1424
|
+
|
|
1425
|
+
```js [./models/comment.js]
|
|
1426
|
+
import { Model, attr } from '@warp-drive/legacy/model';
|
|
1427
|
+
|
|
1428
|
+
export default class Comment extends Model {
|
|
1429
|
+
@attr text;
|
|
1430
|
+
}
|
|
1431
|
+
```
|
|
1432
|
+
|
|
1433
|
+
```js [./models/post.js]
|
|
1434
|
+
import { Model, attr, hasMany } from '@warp-drive/legacy/model';
|
|
1435
|
+
|
|
1436
|
+
export default class Post extends Model {
|
|
1437
|
+
@attr title;
|
|
1438
|
+
@hasMany('comment', { async: true, inverse: null }) comments;
|
|
1439
|
+
}
|
|
1440
|
+
```
|
|
1441
|
+
|
|
1442
|
+
:::
|
|
1443
|
+
|
|
1444
|
+
Then we would have a implicit 'post' relationship for the comment record in order
|
|
1445
|
+
to be do things like remove the comment from the post if the comment were to be deleted.
|
|
1446
|
+
*/
|
|
1447
|
+
|
|
1448
|
+
function createImplicitEdge(definition, identifier) {
|
|
1449
|
+
return {
|
|
1450
|
+
definition,
|
|
1451
|
+
identifier,
|
|
1452
|
+
localMembers: new Set(),
|
|
1453
|
+
remoteMembers: new Set()
|
|
1454
|
+
};
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
/**
|
|
1458
|
+
* Stores the data for one side of a "single" resource relationship.
|
|
1459
|
+
*
|
|
1460
|
+
* @private
|
|
1461
|
+
*/
|
|
1462
|
+
|
|
1463
|
+
function createResourceEdge(definition, identifier) {
|
|
1464
|
+
return {
|
|
1465
|
+
definition,
|
|
1466
|
+
identifier,
|
|
1467
|
+
state: createState(),
|
|
1468
|
+
transactionRef: 0,
|
|
1469
|
+
localState: null,
|
|
1470
|
+
remoteState: null,
|
|
1471
|
+
meta: null,
|
|
1472
|
+
links: null,
|
|
1473
|
+
accessed: false
|
|
1474
|
+
};
|
|
1475
|
+
}
|
|
1476
|
+
function legacyGetResourceRelationshipData(source, getRemoteState) {
|
|
1477
|
+
source.accessed = true;
|
|
1478
|
+
let data;
|
|
1479
|
+
const payload = {};
|
|
1480
|
+
if (getRemoteState && source.remoteState) {
|
|
1481
|
+
data = source.remoteState;
|
|
1482
|
+
} else if (!getRemoteState && source.localState) {
|
|
1483
|
+
data = source.localState;
|
|
1484
|
+
}
|
|
1485
|
+
if ((getRemoteState && source.remoteState === null || source.localState === null) && source.state.hasReceivedData) {
|
|
1486
|
+
data = null;
|
|
1487
|
+
}
|
|
1488
|
+
if (source.links) {
|
|
1489
|
+
payload.links = source.links;
|
|
1490
|
+
}
|
|
1491
|
+
if (data !== undefined) {
|
|
1492
|
+
payload.data = data;
|
|
1493
|
+
}
|
|
1494
|
+
if (source.meta) {
|
|
1495
|
+
payload.meta = source.meta;
|
|
1496
|
+
}
|
|
1497
|
+
return payload;
|
|
1498
|
+
}
|
|
1499
|
+
function addToRelatedRecords(graph, op, isRemote) {
|
|
1500
|
+
const {
|
|
1501
|
+
record,
|
|
1502
|
+
value,
|
|
1503
|
+
index
|
|
1504
|
+
} = op;
|
|
1505
|
+
const relationship = graph.get(record, op.field);
|
|
1506
|
+
const _isBelongsTo = isBelongsTo(relationship);
|
|
1507
|
+
if (isRemote && _isBelongsTo) {
|
|
1508
|
+
if (value !== relationship.remoteState) {
|
|
1509
|
+
const newOp = {
|
|
1510
|
+
record,
|
|
1511
|
+
field: op.field,
|
|
1512
|
+
value: value
|
|
1513
|
+
};
|
|
1514
|
+
return replaceRelatedRecord(graph, newOp, isRemote);
|
|
1515
|
+
}
|
|
1516
|
+
return;
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
// if we are not dirty but have a null localState then we
|
|
1520
|
+
// are mutating a relationship that has never been fetched
|
|
1521
|
+
// so we initialize localState to an empty array
|
|
1522
|
+
if (!relationship.isDirty && !relationship.localState) {
|
|
1523
|
+
relationship.localState = [];
|
|
1524
|
+
}
|
|
1525
|
+
if (Array.isArray(value)) {
|
|
1526
|
+
for (let i = 0; i < value.length; i++) {
|
|
1527
|
+
addRelatedRecord(graph, relationship, record, value[i], index !== undefined ? index + i : null, isRemote);
|
|
1528
|
+
}
|
|
1529
|
+
} else {
|
|
1530
|
+
addRelatedRecord(graph, relationship, record, value, index ?? null, isRemote);
|
|
1531
|
+
}
|
|
1532
|
+
notifyChange(graph, relationship);
|
|
1533
|
+
}
|
|
1534
|
+
function addRelatedRecord(graph, relationship, record, value, index, isRemote) {
|
|
1535
|
+
if (_add(graph, record, relationship, value, index, isRemote)) {
|
|
1536
|
+
addToInverse(graph, value, relationship.definition.inverseKey, record, isRemote);
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
function mergeIdentifier(graph, op, relationships) {
|
|
1540
|
+
Object.keys(relationships).forEach(key => {
|
|
1541
|
+
const rel = relationships[key];
|
|
1542
|
+
if (!rel) {
|
|
1543
|
+
return;
|
|
1544
|
+
}
|
|
1545
|
+
mergeIdentifierForRelationship(graph, op, rel);
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1548
|
+
function mergeIdentifierForRelationship(graph, op, rel) {
|
|
1549
|
+
rel.identifier = op.value;
|
|
1550
|
+
forAllRelatedIdentifiers(rel, identifier => {
|
|
1551
|
+
const inverse = graph.get(identifier, rel.definition.inverseKey);
|
|
1552
|
+
mergeInRelationship(graph, inverse, op);
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1555
|
+
function mergeInRelationship(graph, rel, op) {
|
|
1556
|
+
if (isBelongsTo(rel)) {
|
|
1557
|
+
mergeBelongsTo(graph, rel, op);
|
|
1558
|
+
} else if (isHasMany(rel)) {
|
|
1559
|
+
mergeHasMany(graph, rel, op);
|
|
1560
|
+
} else {
|
|
1561
|
+
mergeImplicit(graph, rel, op);
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
function mergeBelongsTo(graph, rel, op) {
|
|
1565
|
+
if (rel.remoteState === op.record) {
|
|
1566
|
+
rel.remoteState = op.value;
|
|
1567
|
+
}
|
|
1568
|
+
if (rel.localState === op.record) {
|
|
1569
|
+
rel.localState = op.value;
|
|
1570
|
+
notifyChange(graph, rel);
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
function mergeHasMany(graph, rel, op) {
|
|
1574
|
+
if (rel.remoteMembers.has(op.record)) {
|
|
1575
|
+
rel.remoteMembers.delete(op.record);
|
|
1576
|
+
rel.remoteMembers.add(op.value);
|
|
1577
|
+
const index = rel.remoteState.indexOf(op.record);
|
|
1578
|
+
rel.remoteState.splice(index, 1, op.value);
|
|
1579
|
+
rel.isDirty = true;
|
|
1580
|
+
}
|
|
1581
|
+
if (rel.additions?.has(op.record)) {
|
|
1582
|
+
rel.additions.delete(op.record);
|
|
1583
|
+
rel.additions.add(op.value);
|
|
1584
|
+
rel.isDirty = true;
|
|
1585
|
+
}
|
|
1586
|
+
if (rel.removals?.has(op.record)) {
|
|
1587
|
+
rel.removals.delete(op.record);
|
|
1588
|
+
rel.removals.add(op.value);
|
|
1589
|
+
rel.isDirty = true;
|
|
1590
|
+
}
|
|
1591
|
+
if (rel.isDirty) {
|
|
1592
|
+
notifyChange(graph, rel);
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
function mergeImplicit(graph, rel, op) {
|
|
1596
|
+
if (rel.remoteMembers.has(op.record)) {
|
|
1597
|
+
rel.remoteMembers.delete(op.record);
|
|
1598
|
+
rel.remoteMembers.add(op.value);
|
|
1599
|
+
}
|
|
1600
|
+
if (rel.localMembers.has(op.record)) {
|
|
1601
|
+
rel.localMembers.delete(op.record);
|
|
1602
|
+
rel.localMembers.add(op.value);
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
function removeFromRelatedRecords(graph, op, isRemote) {
|
|
1606
|
+
const {
|
|
1607
|
+
record,
|
|
1608
|
+
value
|
|
1609
|
+
} = op;
|
|
1610
|
+
const relationship = graph.get(record, op.field);
|
|
1611
|
+
const _isBelongsTo = isBelongsTo(relationship);
|
|
1612
|
+
if (isRemote && _isBelongsTo) {
|
|
1613
|
+
if (value === relationship.remoteState) {
|
|
1614
|
+
const newOp = {
|
|
1615
|
+
record,
|
|
1616
|
+
field: op.field,
|
|
1617
|
+
value: null
|
|
1618
|
+
};
|
|
1619
|
+
return replaceRelatedRecord(graph, newOp, isRemote);
|
|
1620
|
+
}
|
|
1621
|
+
return;
|
|
1622
|
+
}
|
|
1623
|
+
if (Array.isArray(value)) {
|
|
1624
|
+
for (let i = 0; i < value.length; i++) {
|
|
1625
|
+
removeRelatedRecord(graph, record, relationship, value[i], op.index ?? null, isRemote);
|
|
1626
|
+
}
|
|
1627
|
+
} else {
|
|
1628
|
+
removeRelatedRecord(graph, record, relationship, value, op.index ?? null, isRemote);
|
|
1629
|
+
}
|
|
1630
|
+
notifyChange(graph, relationship);
|
|
1631
|
+
}
|
|
1632
|
+
function removeRelatedRecord(graph, record, relationship, value, index, isRemote) {
|
|
1633
|
+
if (_remove(graph, record, relationship, value, index, isRemote)) {
|
|
1634
|
+
removeFromInverse(graph, value, relationship.definition.inverseKey, record, isRemote);
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
/*
|
|
1639
|
+
This method normalizes a link to an "links object". If the passed link is
|
|
1640
|
+
already an object it's returned without any modifications.
|
|
1641
|
+
|
|
1642
|
+
See http://jsonapi.org/format/#document-links for more information.
|
|
1643
|
+
*/
|
|
1644
|
+
function _normalizeLink(link) {
|
|
1645
|
+
switch (typeof link) {
|
|
1646
|
+
case 'object':
|
|
1647
|
+
return link;
|
|
1648
|
+
case 'string':
|
|
1649
|
+
return {
|
|
1650
|
+
href: link
|
|
1651
|
+
};
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
/*
|
|
1656
|
+
Updates the "canonical" or "remote" state of a relationship, replacing any existing
|
|
1657
|
+
state and blowing away any local changes (excepting new records).
|
|
1658
|
+
*/
|
|
1659
|
+
function updateRelationshipOperation(graph, op) {
|
|
1660
|
+
const relationship = graph.get(op.record, op.field);
|
|
1661
|
+
const {
|
|
1662
|
+
definition,
|
|
1663
|
+
state,
|
|
1664
|
+
identifier
|
|
1665
|
+
} = relationship;
|
|
1666
|
+
const {
|
|
1667
|
+
isCollection
|
|
1668
|
+
} = definition;
|
|
1669
|
+
const payload = op.value;
|
|
1670
|
+
let hasRelationshipDataProperty = false;
|
|
1671
|
+
let hasUpdatedLink = false;
|
|
1672
|
+
if (payload.meta) {
|
|
1673
|
+
relationship.meta = payload.meta;
|
|
1674
|
+
}
|
|
1675
|
+
if (payload.data !== undefined) {
|
|
1676
|
+
hasRelationshipDataProperty = true;
|
|
1677
|
+
if (isCollection) {
|
|
1678
|
+
// TODO deprecate this case. We
|
|
1679
|
+
// have tests saying we support it.
|
|
1680
|
+
if (payload.data === null) {
|
|
1681
|
+
payload.data = [];
|
|
1682
|
+
}
|
|
1683
|
+
const cache = graph.store.cacheKeyManager;
|
|
1684
|
+
graph.update({
|
|
1685
|
+
op: 'replaceRelatedRecords',
|
|
1686
|
+
record: identifier,
|
|
1687
|
+
field: op.field,
|
|
1688
|
+
value: upgradeIdentifiers(payload.data, cache)
|
|
1689
|
+
}, true);
|
|
1690
|
+
} else {
|
|
1691
|
+
graph.update({
|
|
1692
|
+
op: 'replaceRelatedRecord',
|
|
1693
|
+
record: identifier,
|
|
1694
|
+
field: op.field,
|
|
1695
|
+
value: payload.data ? graph.store.cacheKeyManager.upgradeIdentifier(payload.data) : null
|
|
1696
|
+
}, true);
|
|
1697
|
+
}
|
|
1698
|
+
} else if (definition.isAsync === false && !state.hasReceivedData) {
|
|
1699
|
+
hasRelationshipDataProperty = true;
|
|
1700
|
+
if (isCollection) {
|
|
1701
|
+
graph.update({
|
|
1702
|
+
op: 'replaceRelatedRecords',
|
|
1703
|
+
record: identifier,
|
|
1704
|
+
field: op.field,
|
|
1705
|
+
value: []
|
|
1706
|
+
}, true);
|
|
1707
|
+
} else {
|
|
1708
|
+
graph.update({
|
|
1709
|
+
op: 'replaceRelatedRecord',
|
|
1710
|
+
record: identifier,
|
|
1711
|
+
field: op.field,
|
|
1712
|
+
value: null
|
|
1713
|
+
}, true);
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
if (payload.links) {
|
|
1717
|
+
const originalLinks = relationship.links;
|
|
1718
|
+
relationship.links = payload.links;
|
|
1719
|
+
if (payload.links.related) {
|
|
1720
|
+
const relatedLink = _normalizeLink(payload.links.related);
|
|
1721
|
+
const currentLink = originalLinks && originalLinks.related ? _normalizeLink(originalLinks.related) : null;
|
|
1722
|
+
const currentLinkHref = currentLink ? currentLink.href : null;
|
|
1723
|
+
if (relatedLink && relatedLink.href && relatedLink.href !== currentLinkHref) {
|
|
1724
|
+
hasUpdatedLink = true;
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
/*
|
|
1730
|
+
Data being pushed into the relationship might contain only data or links,
|
|
1731
|
+
or a combination of both.
|
|
1732
|
+
IF contains only data
|
|
1733
|
+
IF contains both links and data
|
|
1734
|
+
state.isEmpty -> true if is empty array (has-many) or is null (belongs-to)
|
|
1735
|
+
state.hasReceivedData -> true
|
|
1736
|
+
hasDematerializedInverse -> false
|
|
1737
|
+
state.isStale -> false
|
|
1738
|
+
allInverseRecordsAreLoaded -> run-check-to-determine
|
|
1739
|
+
IF contains only links
|
|
1740
|
+
state.isStale -> true
|
|
1741
|
+
*/
|
|
1742
|
+
relationship.state.hasFailedLoadAttempt = false;
|
|
1743
|
+
if (hasRelationshipDataProperty) {
|
|
1744
|
+
const relationshipIsEmpty = payload.data === null || Array.isArray(payload.data) && payload.data.length === 0;
|
|
1745
|
+
|
|
1746
|
+
// we don't need to notify here as the update op we pushed in above will notify once
|
|
1747
|
+
// membership is in the correct state.
|
|
1748
|
+
relationship.state.hasReceivedData = true;
|
|
1749
|
+
relationship.state.isStale = false;
|
|
1750
|
+
relationship.state.hasDematerializedInverse = false;
|
|
1751
|
+
relationship.state.isEmpty = relationshipIsEmpty;
|
|
1752
|
+
} else if (hasUpdatedLink) {
|
|
1753
|
+
// only notify stale if we have not previously received membership data.
|
|
1754
|
+
// within this same transaction
|
|
1755
|
+
// this prevents refetching when only one side of the relationship in the
|
|
1756
|
+
// payload contains the info while the other side contains just a link
|
|
1757
|
+
// this only works when the side with just a link is a belongsTo, as we
|
|
1758
|
+
// don't know if a hasMany has full information or not.
|
|
1759
|
+
// see #7049 for context.
|
|
1760
|
+
if (isCollection || !relationship.state.hasReceivedData || isStaleTransaction(relationship.transactionRef, graph._transaction)) {
|
|
1761
|
+
relationship.state.isStale = true;
|
|
1762
|
+
notifyChange(graph, relationship);
|
|
1763
|
+
} else {
|
|
1764
|
+
relationship.state.isStale = false;
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
function isStaleTransaction(relationshipTransactionId, graphTransactionId) {
|
|
1769
|
+
return relationshipTransactionId === 0 ||
|
|
1770
|
+
// relationship has never notified
|
|
1771
|
+
graphTransactionId === null ||
|
|
1772
|
+
// we are not in a transaction
|
|
1773
|
+
relationshipTransactionId < graphTransactionId // we are not part of the current transaction
|
|
1774
|
+
;
|
|
1775
|
+
}
|
|
1776
|
+
function upgradeIdentifiers(arr, cache) {
|
|
1777
|
+
for (let i = 0; i < arr.length; i++) {
|
|
1778
|
+
arr[i] = cache.upgradeIdentifier(arr[i]);
|
|
1779
|
+
}
|
|
1780
|
+
return arr;
|
|
1781
|
+
}
|
|
1782
|
+
const Graphs = getOrSetGlobal('Graphs', new Map());
|
|
1783
|
+
/*
|
|
1784
|
+
* Graph acts as the cache for relationship data. It allows for
|
|
1785
|
+
* us to ask about and update relationships for a given Identifier
|
|
1786
|
+
* without requiring other objects for that Identifier to be
|
|
1787
|
+
* instantiated (such as `RecordData` or a `Record`)
|
|
1788
|
+
*
|
|
1789
|
+
* This also allows for us to make more substantive changes to relationships
|
|
1790
|
+
* with increasingly minor alterations to other portions of the internals
|
|
1791
|
+
* over time.
|
|
1792
|
+
*
|
|
1793
|
+
* The graph is made up of nodes and edges. Each unique identifier gets
|
|
1794
|
+
* its own node, which is a dictionary with a list of that node's edges
|
|
1795
|
+
* (or connections) to other nodes. In `Model` terms, a node represents a
|
|
1796
|
+
* record instance, with each key (an edge) in the dictionary correlating
|
|
1797
|
+
* to either a `hasMany` or `belongsTo` field on that record instance.
|
|
1798
|
+
*
|
|
1799
|
+
* The value for each key, or `edge` is the identifier(s) the node relates
|
|
1800
|
+
* to in the graph from that key.
|
|
1801
|
+
*/
|
|
1802
|
+
class Graph {
|
|
1803
|
+
constructor(store) {
|
|
1804
|
+
this._definitionCache = Object.create(null);
|
|
1805
|
+
this._metaCache = Object.create(null);
|
|
1806
|
+
this._potentialPolymorphicTypes = Object.create(null);
|
|
1807
|
+
this.identifiers = new Map();
|
|
1808
|
+
this.store = store;
|
|
1809
|
+
this._realStore = store._store;
|
|
1810
|
+
this.isDestroyed = false;
|
|
1811
|
+
this._willSyncRemote = false;
|
|
1812
|
+
this._willSyncLocal = false;
|
|
1813
|
+
this._pushedUpdates = {
|
|
1814
|
+
belongsTo: undefined,
|
|
1815
|
+
hasMany: undefined,
|
|
1816
|
+
deletions: []
|
|
1817
|
+
};
|
|
1818
|
+
this._updatedRelationships = new Set();
|
|
1819
|
+
this._transaction = null;
|
|
1820
|
+
this._removing = null;
|
|
1821
|
+
this.silenceNotifications = false;
|
|
1822
|
+
}
|
|
1823
|
+
has(resourceKey, propertyName) {
|
|
1824
|
+
const relationships = this.identifiers.get(resourceKey);
|
|
1825
|
+
if (!relationships) {
|
|
1826
|
+
return false;
|
|
1827
|
+
}
|
|
1828
|
+
return relationships[propertyName] !== undefined;
|
|
1829
|
+
}
|
|
1830
|
+
getDefinition(resourceKey, propertyName) {
|
|
1831
|
+
let defs = this._metaCache[resourceKey.type];
|
|
1832
|
+
let meta = defs?.[propertyName];
|
|
1833
|
+
if (!meta) {
|
|
1834
|
+
const info = /*#__NOINLINE__*/upgradeDefinition(this, resourceKey, propertyName);
|
|
1835
|
+
|
|
1836
|
+
// if (info.rhs_definition?.kind === 'implicit') {
|
|
1837
|
+
// we should possibly also do this
|
|
1838
|
+
// but it would result in being extremely permissive for other relationships by accident
|
|
1839
|
+
// this.registerPolymorphicType(info.rhs_baseModelName, identifier.type);
|
|
1840
|
+
// }
|
|
1841
|
+
|
|
1842
|
+
meta = /*#__NOINLINE__*/isLHS(info, resourceKey.type, propertyName) ? info.lhs_definition : info.rhs_definition;
|
|
1843
|
+
defs = this._metaCache[resourceKey.type] = defs || {};
|
|
1844
|
+
defs[propertyName] = meta;
|
|
1845
|
+
}
|
|
1846
|
+
return meta;
|
|
1847
|
+
}
|
|
1848
|
+
get(resourceKey, propertyName) {
|
|
1849
|
+
let relationships = this.identifiers.get(resourceKey);
|
|
1850
|
+
if (!relationships) {
|
|
1851
|
+
relationships = Object.create(null);
|
|
1852
|
+
this.identifiers.set(resourceKey, relationships);
|
|
1853
|
+
}
|
|
1854
|
+
let relationship = relationships[propertyName];
|
|
1855
|
+
if (!relationship) {
|
|
1856
|
+
const meta = this.getDefinition(resourceKey, propertyName);
|
|
1857
|
+
if (meta.kind === 'belongsTo') {
|
|
1858
|
+
relationship = relationships[propertyName] = createResourceEdge(meta, resourceKey);
|
|
1859
|
+
} else if (meta.kind === 'hasMany') {
|
|
1860
|
+
relationship = relationships[propertyName] = createCollectionEdge(meta, resourceKey);
|
|
1861
|
+
} else {
|
|
1862
|
+
relationship = relationships[propertyName] = createImplicitEdge(meta, resourceKey);
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
return relationship;
|
|
1866
|
+
}
|
|
1867
|
+
getData(resourceKey, propertyName) {
|
|
1868
|
+
const relationship = this.get(resourceKey, propertyName);
|
|
1869
|
+
if (isBelongsTo(relationship)) {
|
|
1870
|
+
return legacyGetResourceRelationshipData(relationship, false);
|
|
1871
|
+
}
|
|
1872
|
+
return legacyGetCollectionRelationshipData(relationship, false);
|
|
1873
|
+
}
|
|
1874
|
+
getRemoteData(resourceKey, propertyName) {
|
|
1875
|
+
const relationship = this.get(resourceKey, propertyName);
|
|
1876
|
+
if (isBelongsTo(relationship)) {
|
|
1877
|
+
return legacyGetResourceRelationshipData(relationship, true);
|
|
1878
|
+
}
|
|
1879
|
+
return legacyGetCollectionRelationshipData(relationship, true);
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
/*
|
|
1883
|
+
* Allows for the graph to dynamically discover polymorphic connections
|
|
1884
|
+
* without needing to walk prototype chains.
|
|
1885
|
+
*
|
|
1886
|
+
* Used by edges when an added `type` does not match the expected `type`
|
|
1887
|
+
* for that edge.
|
|
1888
|
+
*
|
|
1889
|
+
* Currently we assert before calling this. For a public API we will want
|
|
1890
|
+
* to call out to the schema manager to ask if we should consider these
|
|
1891
|
+
* types as equivalent for a given relationship.
|
|
1892
|
+
*/
|
|
1893
|
+
registerPolymorphicType(type1, type2) {
|
|
1894
|
+
const typeCache = this._potentialPolymorphicTypes;
|
|
1895
|
+
let t1 = typeCache[type1];
|
|
1896
|
+
if (!t1) {
|
|
1897
|
+
t1 = typeCache[type1] = Object.create(null);
|
|
1898
|
+
}
|
|
1899
|
+
t1[type2] = true;
|
|
1900
|
+
let t2 = typeCache[type2];
|
|
1901
|
+
if (!t2) {
|
|
1902
|
+
t2 = typeCache[type2] = Object.create(null);
|
|
1903
|
+
}
|
|
1904
|
+
t2[type1] = true;
|
|
1905
|
+
}
|
|
1906
|
+
isReleasable(resourceKey) {
|
|
1907
|
+
const relationships = this.identifiers.get(resourceKey);
|
|
1908
|
+
if (!relationships) {
|
|
1909
|
+
return true;
|
|
1910
|
+
}
|
|
1911
|
+
const keys = Object.keys(relationships);
|
|
1912
|
+
for (let i = 0; i < keys.length; i++) {
|
|
1913
|
+
const relationship = relationships[keys[i]];
|
|
1914
|
+
// account for previously unloaded relationships
|
|
1915
|
+
// typically from a prior deletion of a record that pointed to this one implicitly
|
|
1916
|
+
if (relationship === undefined) {
|
|
1917
|
+
continue;
|
|
1918
|
+
}
|
|
1919
|
+
if (relationship.definition.inverseIsAsync && !checkIfNew(this._realStore, resourceKey)) {
|
|
1920
|
+
return false;
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
return true;
|
|
1924
|
+
}
|
|
1925
|
+
unload(resourceKey, silenceNotifications) {
|
|
1926
|
+
const relationships = this.identifiers.get(resourceKey);
|
|
1927
|
+
if (relationships) {
|
|
1928
|
+
// cleans up the graph but retains some nodes
|
|
1929
|
+
// to allow for rematerialization
|
|
1930
|
+
Object.keys(relationships).forEach(key => {
|
|
1931
|
+
const rel = relationships[key];
|
|
1932
|
+
if (!rel) {
|
|
1933
|
+
return;
|
|
1934
|
+
}
|
|
1935
|
+
/*#__NOINLINE__*/
|
|
1936
|
+
destroyRelationship(this, rel, silenceNotifications);
|
|
1937
|
+
if (/*#__NOINLINE__*/isImplicit(rel)) {
|
|
1938
|
+
// @ts-expect-error
|
|
1939
|
+
relationships[key] = undefined;
|
|
1940
|
+
}
|
|
1941
|
+
});
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
_isDirty(resourceKey, field) {
|
|
1945
|
+
const relationships = this.identifiers.get(resourceKey);
|
|
1946
|
+
if (!relationships) {
|
|
1947
|
+
return false;
|
|
1948
|
+
}
|
|
1949
|
+
const relationship = relationships[field];
|
|
1950
|
+
if (!relationship) {
|
|
1951
|
+
return false;
|
|
1952
|
+
}
|
|
1953
|
+
if (isBelongsTo(relationship)) {
|
|
1954
|
+
return relationship.localState !== relationship.remoteState;
|
|
1955
|
+
} else if (isHasMany(relationship)) {
|
|
1956
|
+
const hasAdditions = relationship.additions !== null && relationship.additions.size > 0;
|
|
1957
|
+
const hasRemovals = relationship.removals !== null && relationship.removals.size > 0;
|
|
1958
|
+
return hasAdditions || hasRemovals || isReordered(relationship);
|
|
1959
|
+
}
|
|
1960
|
+
return false;
|
|
1961
|
+
}
|
|
1962
|
+
getChanged(resourceKey) {
|
|
1963
|
+
const relationships = this.identifiers.get(resourceKey);
|
|
1964
|
+
const changed = new Map();
|
|
1965
|
+
if (!relationships) {
|
|
1966
|
+
return changed;
|
|
1967
|
+
}
|
|
1968
|
+
const keys = Object.keys(relationships);
|
|
1969
|
+
for (let i = 0; i < keys.length; i++) {
|
|
1970
|
+
const field = keys[i];
|
|
1971
|
+
const relationship = relationships[field];
|
|
1972
|
+
if (!relationship) {
|
|
1973
|
+
continue;
|
|
1974
|
+
}
|
|
1975
|
+
if (isBelongsTo(relationship)) {
|
|
1976
|
+
if (relationship.localState !== relationship.remoteState) {
|
|
1977
|
+
changed.set(field, {
|
|
1978
|
+
kind: 'resource',
|
|
1979
|
+
remoteState: relationship.remoteState,
|
|
1980
|
+
localState: relationship.localState
|
|
1981
|
+
});
|
|
1982
|
+
}
|
|
1983
|
+
} else if (isHasMany(relationship)) {
|
|
1984
|
+
const hasAdditions = relationship.additions !== null && relationship.additions.size > 0;
|
|
1985
|
+
const hasRemovals = relationship.removals !== null && relationship.removals.size > 0;
|
|
1986
|
+
const reordered = isReordered(relationship);
|
|
1987
|
+
if (hasAdditions || hasRemovals || reordered) {
|
|
1988
|
+
changed.set(field, {
|
|
1989
|
+
kind: 'collection',
|
|
1990
|
+
additions: new Set(relationship.additions),
|
|
1991
|
+
removals: new Set(relationship.removals),
|
|
1992
|
+
remoteState: relationship.remoteState,
|
|
1993
|
+
localState: legacyGetCollectionRelationshipData(relationship, false).data || [],
|
|
1994
|
+
reordered
|
|
1995
|
+
});
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
return changed;
|
|
2000
|
+
}
|
|
2001
|
+
hasChanged(resourceKey) {
|
|
2002
|
+
const relationships = this.identifiers.get(resourceKey);
|
|
2003
|
+
if (!relationships) {
|
|
2004
|
+
return false;
|
|
2005
|
+
}
|
|
2006
|
+
const keys = Object.keys(relationships);
|
|
2007
|
+
for (let i = 0; i < keys.length; i++) {
|
|
2008
|
+
if (this._isDirty(resourceKey, keys[i])) {
|
|
2009
|
+
return true;
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
return false;
|
|
2013
|
+
}
|
|
2014
|
+
rollback(resourceKey) {
|
|
2015
|
+
const relationships = this.identifiers.get(resourceKey);
|
|
2016
|
+
const changed = [];
|
|
2017
|
+
if (!relationships) {
|
|
2018
|
+
return changed;
|
|
2019
|
+
}
|
|
2020
|
+
const keys = Object.keys(relationships);
|
|
2021
|
+
for (let i = 0; i < keys.length; i++) {
|
|
2022
|
+
const field = keys[i];
|
|
2023
|
+
const relationship = relationships[field];
|
|
2024
|
+
if (!relationship) {
|
|
2025
|
+
continue;
|
|
2026
|
+
}
|
|
2027
|
+
if (this._isDirty(resourceKey, field)) {
|
|
2028
|
+
rollbackRelationship(this, resourceKey, field, relationship);
|
|
2029
|
+
changed.push(field);
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
return changed;
|
|
2033
|
+
}
|
|
2034
|
+
remove(resourceKey) {
|
|
2035
|
+
this._removing = resourceKey;
|
|
2036
|
+
this.unload(resourceKey);
|
|
2037
|
+
this.identifiers.delete(resourceKey);
|
|
2038
|
+
this._removing = null;
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
/*
|
|
2042
|
+
* Remote state changes
|
|
2043
|
+
*/
|
|
2044
|
+
push(op) {
|
|
2045
|
+
if (op.op === 'deleteRecord') {
|
|
2046
|
+
this._pushedUpdates.deletions.push(op);
|
|
2047
|
+
} else {
|
|
2048
|
+
const definition = this.getDefinition(op.record, op.field);
|
|
2049
|
+
addPending(this._pushedUpdates, definition, op);
|
|
2050
|
+
}
|
|
2051
|
+
if (!this._willSyncRemote) {
|
|
2052
|
+
this._willSyncRemote = true;
|
|
2053
|
+
const store = getStore(this.store);
|
|
2054
|
+
if (!store._cbs) {
|
|
2055
|
+
store._run(() => this._flushRemoteQueue());
|
|
2056
|
+
} else {
|
|
2057
|
+
store._schedule('coalesce', () => this._flushRemoteQueue());
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
/*
|
|
2063
|
+
* Local state changes
|
|
2064
|
+
*/
|
|
2065
|
+
|
|
2066
|
+
update(op, isRemote = false) {
|
|
2067
|
+
switch (op.op) {
|
|
2068
|
+
case 'mergeIdentifiers':
|
|
2069
|
+
{
|
|
2070
|
+
const relationships = this.identifiers.get(op.record);
|
|
2071
|
+
if (relationships) {
|
|
2072
|
+
/*#__NOINLINE__*/mergeIdentifier(this, op, relationships);
|
|
2073
|
+
}
|
|
2074
|
+
break;
|
|
2075
|
+
}
|
|
2076
|
+
case 'update':
|
|
2077
|
+
case 'updateRelationship':
|
|
2078
|
+
/*#__NOINLINE__*/updateRelationshipOperation(this, op);
|
|
2079
|
+
break;
|
|
2080
|
+
case 'deleteRecord':
|
|
2081
|
+
{
|
|
2082
|
+
const identifier = op.record;
|
|
2083
|
+
const relationships = this.identifiers.get(identifier);
|
|
2084
|
+
if (relationships) {
|
|
2085
|
+
Object.keys(relationships).forEach(key => {
|
|
2086
|
+
const rel = relationships[key];
|
|
2087
|
+
if (!rel) {
|
|
2088
|
+
return;
|
|
2089
|
+
}
|
|
2090
|
+
// works together with the has check
|
|
2091
|
+
// @ts-expect-error
|
|
2092
|
+
relationships[key] = undefined;
|
|
2093
|
+
/*#__NOINLINE__*/
|
|
2094
|
+
removeCompletelyFromInverse(this, rel);
|
|
2095
|
+
});
|
|
2096
|
+
this.identifiers.delete(identifier);
|
|
2097
|
+
}
|
|
2098
|
+
break;
|
|
2099
|
+
}
|
|
2100
|
+
case 'replaceRelatedRecord':
|
|
2101
|
+
/*#__NOINLINE__*/replaceRelatedRecord(this, op, isRemote);
|
|
2102
|
+
break;
|
|
2103
|
+
case 'add':
|
|
2104
|
+
/*#__NOINLINE__*/addToRelatedRecords(this, op, isRemote);
|
|
2105
|
+
break;
|
|
2106
|
+
case 'remove':
|
|
2107
|
+
/*#__NOINLINE__*/removeFromRelatedRecords(this, op, isRemote);
|
|
2108
|
+
break;
|
|
2109
|
+
case 'replaceRelatedRecords':
|
|
2110
|
+
/*#__NOINLINE__*/replaceRelatedRecords(this, op, isRemote);
|
|
2111
|
+
break;
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
_scheduleLocalSync(relationship) {
|
|
2115
|
+
this._updatedRelationships.add(relationship);
|
|
2116
|
+
if (!this._willSyncLocal) {
|
|
2117
|
+
this._willSyncLocal = true;
|
|
2118
|
+
getStore(this.store)._schedule('sync', () => this._flushLocalQueue());
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
_flushRemoteQueue() {
|
|
2122
|
+
if (!this._willSyncRemote) {
|
|
2123
|
+
return;
|
|
2124
|
+
}
|
|
2125
|
+
let transactionRef = peekTransient('transactionRef') ?? 0;
|
|
2126
|
+
this._transaction = ++transactionRef;
|
|
2127
|
+
setTransient('transactionRef', transactionRef);
|
|
2128
|
+
this._willSyncRemote = false;
|
|
2129
|
+
const updates = this._pushedUpdates;
|
|
2130
|
+
const {
|
|
2131
|
+
deletions,
|
|
2132
|
+
hasMany,
|
|
2133
|
+
belongsTo
|
|
2134
|
+
} = updates;
|
|
2135
|
+
updates.deletions = [];
|
|
2136
|
+
updates.hasMany = undefined;
|
|
2137
|
+
updates.belongsTo = undefined;
|
|
2138
|
+
for (let i = 0; i < deletions.length; i++) {
|
|
2139
|
+
this.update(deletions[i], true);
|
|
2140
|
+
}
|
|
2141
|
+
if (hasMany) {
|
|
2142
|
+
flushPending(this, hasMany);
|
|
2143
|
+
}
|
|
2144
|
+
if (belongsTo) {
|
|
2145
|
+
flushPending(this, belongsTo);
|
|
2146
|
+
}
|
|
2147
|
+
this._transaction = null;
|
|
2148
|
+
}
|
|
2149
|
+
_addToTransaction(relationship) {
|
|
2150
|
+
relationship.transactionRef = this._transaction;
|
|
2151
|
+
}
|
|
2152
|
+
_flushLocalQueue() {
|
|
2153
|
+
if (!this._willSyncLocal) {
|
|
2154
|
+
return;
|
|
2155
|
+
}
|
|
2156
|
+
if (this.silenceNotifications) {
|
|
2157
|
+
this.silenceNotifications = false;
|
|
2158
|
+
this._updatedRelationships = new Set();
|
|
2159
|
+
return;
|
|
2160
|
+
}
|
|
2161
|
+
this._willSyncLocal = false;
|
|
2162
|
+
const updated = this._updatedRelationships;
|
|
2163
|
+
this._updatedRelationships = new Set();
|
|
2164
|
+
updated.forEach(rel => notifyChange(this, rel));
|
|
2165
|
+
}
|
|
2166
|
+
destroy() {
|
|
2167
|
+
Graphs.delete(this.store);
|
|
2168
|
+
this.identifiers.clear();
|
|
2169
|
+
this.store = null;
|
|
2170
|
+
this.isDestroyed = true;
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
function flushPending(graph, ops) {
|
|
2174
|
+
for (const type of ops.values()) {
|
|
2175
|
+
for (const opList of type.values()) {
|
|
2176
|
+
flushPendingList(graph, opList);
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
function flushPendingList(graph, opList) {
|
|
2181
|
+
for (let i = 0; i < opList.length; i++) {
|
|
2182
|
+
graph.update(opList[i], true);
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
// Handle dematerialization for relationship `rel`. In all cases, notify the
|
|
2187
|
+
// relationship of the dematerialization: this is done so the relationship can
|
|
2188
|
+
// notify its inverse which needs to update state
|
|
2189
|
+
//
|
|
2190
|
+
// If the inverse is sync, unloading this record is treated as a client-side
|
|
2191
|
+
// delete, so we remove the inverse records from this relationship to
|
|
2192
|
+
// disconnect the graph. Because it's not async, we don't need to keep around
|
|
2193
|
+
// the identifier as an id-wrapper for references
|
|
2194
|
+
function destroyRelationship(graph, rel, silenceNotifications) {
|
|
2195
|
+
if (isImplicit(rel)) {
|
|
2196
|
+
if (graph.isReleasable(rel.identifier)) {
|
|
2197
|
+
/*#__NOINLINE__*/removeCompletelyFromInverse(graph, rel);
|
|
2198
|
+
}
|
|
2199
|
+
return;
|
|
2200
|
+
}
|
|
2201
|
+
const {
|
|
2202
|
+
identifier
|
|
2203
|
+
} = rel;
|
|
2204
|
+
const {
|
|
2205
|
+
inverseKey
|
|
2206
|
+
} = rel.definition;
|
|
2207
|
+
if (!rel.definition.inverseIsImplicit) {
|
|
2208
|
+
/*#__NOINLINE__*/forAllRelatedIdentifiers(rel, inverseIdentifer => /*#__NOINLINE__*/notifyInverseOfDematerialization(graph, inverseIdentifer, inverseKey, identifier, silenceNotifications));
|
|
2209
|
+
}
|
|
2210
|
+
if (!rel.definition.inverseIsImplicit && !rel.definition.inverseIsAsync) {
|
|
2211
|
+
rel.state.isStale = true;
|
|
2212
|
+
/*#__NOINLINE__*/
|
|
2213
|
+
clearRelationship(rel);
|
|
2214
|
+
|
|
2215
|
+
// necessary to clear relationships in the ui from dematerialized records
|
|
2216
|
+
// hasMany is managed by Model which calls `retreiveLatest` after
|
|
2217
|
+
// dematerializing the resource-cache instance.
|
|
2218
|
+
// but sync belongsTo requires this since they don't have a proxy to update.
|
|
2219
|
+
// so we have to notify so it will "update" to null.
|
|
2220
|
+
// we should discuss whether we still care about this, probably fine to just
|
|
2221
|
+
// leave the ui relationship populated since the record is destroyed and
|
|
2222
|
+
// internally we've fully cleaned up.
|
|
2223
|
+
if (!rel.definition.isAsync && !silenceNotifications) {
|
|
2224
|
+
/*#__NOINLINE__*/notifyChange(graph, rel);
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
function notifyInverseOfDematerialization(graph, inverseIdentifier, inverseKey, resourceKey, silenceNotifications) {
|
|
2229
|
+
if (!graph.has(inverseIdentifier, inverseKey)) {
|
|
2230
|
+
return;
|
|
2231
|
+
}
|
|
2232
|
+
const relationship = graph.get(inverseIdentifier, inverseKey);
|
|
2233
|
+
|
|
2234
|
+
// For remote members, it is possible that inverseRecordData has already been associated to
|
|
2235
|
+
// to another record. For such cases, do not dematerialize the inverseRecordData
|
|
2236
|
+
if (!isBelongsTo(relationship) || !relationship.localState || resourceKey === relationship.localState) {
|
|
2237
|
+
/*#__NOINLINE__*/removeDematerializedInverse(graph, relationship, resourceKey, silenceNotifications);
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
function clearRelationship(relationship) {
|
|
2241
|
+
if (isBelongsTo(relationship)) {
|
|
2242
|
+
relationship.localState = null;
|
|
2243
|
+
relationship.remoteState = null;
|
|
2244
|
+
relationship.state.hasReceivedData = false;
|
|
2245
|
+
relationship.state.isEmpty = true;
|
|
2246
|
+
} else {
|
|
2247
|
+
relationship.remoteMembers.clear();
|
|
2248
|
+
relationship.remoteState = [];
|
|
2249
|
+
relationship.additions = null;
|
|
2250
|
+
relationship.removals = null;
|
|
2251
|
+
relationship.localState = null;
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
function removeDematerializedInverse(graph, relationship, inverseIdentifier, silenceNotifications) {
|
|
2255
|
+
if (isBelongsTo(relationship)) {
|
|
2256
|
+
const localInverse = relationship.localState;
|
|
2257
|
+
if (!relationship.definition.isAsync || localInverse && checkIfNew(graph._realStore, localInverse)) {
|
|
2258
|
+
// unloading inverse of a sync relationship is treated as a client-side
|
|
2259
|
+
// delete, so actually remove the models don't merely invalidate the cp
|
|
2260
|
+
// cache.
|
|
2261
|
+
// if the record being unloaded only exists on the client, we similarly
|
|
2262
|
+
// treat it as a client side delete
|
|
2263
|
+
if (relationship.localState === localInverse && localInverse !== null) {
|
|
2264
|
+
relationship.localState = null;
|
|
2265
|
+
}
|
|
2266
|
+
if (relationship.remoteState === localInverse && localInverse !== null) {
|
|
2267
|
+
relationship.remoteState = null;
|
|
2268
|
+
relationship.state.hasReceivedData = true;
|
|
2269
|
+
relationship.state.isEmpty = true;
|
|
2270
|
+
if (relationship.localState && !checkIfNew(graph._realStore, relationship.localState)) {
|
|
2271
|
+
relationship.localState = null;
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
} else {
|
|
2275
|
+
relationship.state.hasDematerializedInverse = true;
|
|
2276
|
+
}
|
|
2277
|
+
if (!silenceNotifications) {
|
|
2278
|
+
notifyChange(graph, relationship);
|
|
2279
|
+
}
|
|
2280
|
+
} else {
|
|
2281
|
+
if (!relationship.definition.isAsync || inverseIdentifier && checkIfNew(graph._realStore, inverseIdentifier)) {
|
|
2282
|
+
// unloading inverse of a sync relationship is treated as a client-side
|
|
2283
|
+
// delete, so actually remove the models don't merely invalidate the cp
|
|
2284
|
+
// cache.
|
|
2285
|
+
// if the record being unloaded only exists on the client, we similarly
|
|
2286
|
+
// treat it as a client side delete
|
|
2287
|
+
/*#__NOINLINE__*/
|
|
2288
|
+
removeIdentifierCompletelyFromRelationship(graph, relationship, inverseIdentifier);
|
|
2289
|
+
} else {
|
|
2290
|
+
relationship.state.hasDematerializedInverse = true;
|
|
2291
|
+
}
|
|
2292
|
+
if (!silenceNotifications) {
|
|
2293
|
+
notifyChange(graph, relationship);
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
function removeCompletelyFromInverse(graph, relationship) {
|
|
2298
|
+
const {
|
|
2299
|
+
identifier
|
|
2300
|
+
} = relationship;
|
|
2301
|
+
const {
|
|
2302
|
+
inverseKey
|
|
2303
|
+
} = relationship.definition;
|
|
2304
|
+
forAllRelatedIdentifiers(relationship, inverseIdentifier => {
|
|
2305
|
+
if (graph.has(inverseIdentifier, inverseKey)) {
|
|
2306
|
+
removeIdentifierCompletelyFromRelationship(graph, graph.get(inverseIdentifier, inverseKey), identifier);
|
|
2307
|
+
}
|
|
2308
|
+
});
|
|
2309
|
+
if (isBelongsTo(relationship)) {
|
|
2310
|
+
if (!relationship.definition.isAsync) {
|
|
2311
|
+
clearRelationship(relationship);
|
|
2312
|
+
}
|
|
2313
|
+
relationship.localState = null;
|
|
2314
|
+
} else if (isHasMany(relationship)) {
|
|
2315
|
+
if (!relationship.definition.isAsync) {
|
|
2316
|
+
clearRelationship(relationship);
|
|
2317
|
+
notifyChange(graph, relationship);
|
|
2318
|
+
}
|
|
2319
|
+
} else {
|
|
2320
|
+
relationship.remoteMembers.clear();
|
|
2321
|
+
relationship.localMembers.clear();
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
function addPending(cache, definition, op) {
|
|
2325
|
+
const cacheForKind = cache[definition.kind] = cache[definition.kind] || new Map();
|
|
2326
|
+
let cacheForType = cacheForKind.get(definition.inverseType);
|
|
2327
|
+
if (!cacheForType) {
|
|
2328
|
+
cacheForType = new Map();
|
|
2329
|
+
cacheForKind.set(definition.inverseType, cacheForType);
|
|
2330
|
+
}
|
|
2331
|
+
let cacheForField = cacheForType.get(op.field);
|
|
2332
|
+
if (!cacheForField) {
|
|
2333
|
+
cacheForField = [];
|
|
2334
|
+
cacheForType.set(op.field, cacheForField);
|
|
2335
|
+
}
|
|
2336
|
+
cacheForField.push(op);
|
|
2337
|
+
}
|
|
2338
|
+
function isReordered(relationship) {
|
|
2339
|
+
// if we are dirty we are never re-ordered because accessing
|
|
2340
|
+
// the state would flush away any reordering.
|
|
2341
|
+
if (relationship.isDirty) {
|
|
2342
|
+
return false;
|
|
2343
|
+
}
|
|
2344
|
+
const {
|
|
2345
|
+
remoteState,
|
|
2346
|
+
localState,
|
|
2347
|
+
additions,
|
|
2348
|
+
removals
|
|
2349
|
+
} = relationship;
|
|
2350
|
+
if (localState === null) {
|
|
2351
|
+
// the relationship has never been accessed, so it hasn't been reordered either
|
|
2352
|
+
return false;
|
|
2353
|
+
}
|
|
2354
|
+
for (let i = 0, j = 0; i < remoteState.length; i++) {
|
|
2355
|
+
const member = remoteState[i];
|
|
2356
|
+
const localMember = localState[j];
|
|
2357
|
+
if (member !== localMember) {
|
|
2358
|
+
if (removals && removals.has(member)) {
|
|
2359
|
+
// dont increment j because we want to skip this
|
|
2360
|
+
continue;
|
|
2361
|
+
}
|
|
2362
|
+
if (additions && additions.has(localMember)) {
|
|
2363
|
+
// increment j to skip this localMember
|
|
2364
|
+
// decrement i to repeat this remoteMember
|
|
2365
|
+
j++;
|
|
2366
|
+
i--;
|
|
2367
|
+
continue;
|
|
2368
|
+
}
|
|
2369
|
+
return true;
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
// if we made it here, increment j
|
|
2373
|
+
j++;
|
|
2374
|
+
}
|
|
2375
|
+
return false;
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
/**
|
|
2379
|
+
Provides a performance tuned normalized graph for intelligently managing relationships between resources based on identity
|
|
2380
|
+
|
|
2381
|
+
While this Graph is abstract, it currently is a private implementation required as a peer-dependency by the {json:api} Cache Implementation.
|
|
2382
|
+
|
|
2383
|
+
We intend to make this Graph public API after some additional iteration during the 5.x timeframe, until then all APIs should be considered experimental and unstable, not fit for direct application or 3rd party library usage.
|
|
2384
|
+
|
|
2385
|
+
@module
|
|
2386
|
+
*/
|
|
2387
|
+
|
|
2388
|
+
function isStore(maybeStore) {
|
|
2389
|
+
return maybeStore._instanceCache !== undefined;
|
|
2390
|
+
}
|
|
2391
|
+
function getWrapper(store) {
|
|
2392
|
+
return isStore(store) ? store._instanceCache._storeWrapper : store;
|
|
2393
|
+
}
|
|
2394
|
+
function peekGraph(store) {
|
|
2395
|
+
return Graphs.get(getWrapper(store));
|
|
2396
|
+
}
|
|
2397
|
+
function graphFor(store) {
|
|
2398
|
+
const wrapper = getWrapper(store);
|
|
2399
|
+
let graph = Graphs.get(wrapper);
|
|
2400
|
+
if (!graph) {
|
|
2401
|
+
graph = new Graph(wrapper);
|
|
2402
|
+
Graphs.set(wrapper, graph);
|
|
2403
|
+
getStore(wrapper)._graph = graph;
|
|
2404
|
+
}
|
|
2405
|
+
return graph;
|
|
2406
|
+
}
|
|
2407
|
+
export { graphFor, isBelongsTo, peekGraph };
|