-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmobx-computedtree.js
302 lines (251 loc) · 12.6 KB
/
mobx-computedtree.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
/* jshint esversion: 6 */
/* globals mobx, __mobxGlobals, patch */
(function(window, mobx, globalState, patchBoxed, undefined) {
"use strict";
// UTILITIES FUNCTIONS:
var $mobx = mobx.$mobx,
observable = mobx.observable,
computed = mobx.computed,
action = mobx.action,
set = mobx.set,
intercept = mobx.intercept,
onBecomeUnobserved = mobx.onBecomeUnobserved,
getAtom = mobx.getAtom,
getDebugName = mobx.getDebugName,
allowStateChangesInsideComputed = function(func) { // See: https://github.com/mobxjs/mobx/blob/5.6.0/src/core/action.ts#108
return function() {
var prev = globalState.computationDepth;
globalState.computationDepth = 0;
var res;
try {
res = func.apply(this, arguments);
} finally {
globalState.computationDepth = prev;
}
return res;
};
},
// We need to use the same type checks as MobX:
isPlainObject = function(value) { // See: https://github.com/mobxjs/mobx/blob/5.6.0/src/utils/utils.ts#L85
if (value === null || typeof value !== "object") return false;
var proto = Object.getPrototypeOf(value);
return proto === Object.prototype || proto === null;
},
isMap = function(thing) { // See: https://github.com/mobxjs/mobx/blob/5.6.0/src/utils/utils.ts#L145
return thing instanceof Map;
},
isObservable = mobx.isObservable,
isObservableMap = mobx.isObservableMap,
isComputedProp = mobx.isComputedProp,
invariant = function(check, message) {
if (!check) throw new Error("[computedTree] " + message);
};
// COMPUTEDTREE FUNCTIONS:
// Note that MobX decorators may change in the future:
// https://github.com/mobxjs/mobx/labels/waiting-for-standardized-decorators
// Conceptually, this is how it works:
// - computedTree is a decorator that has to be run before other MobX decorators.
// - It injects a custom get() function before calling the computed() decorator
// to create a normal computed value property.
// - The custom get() function is the one that updates/patches the tree.
// - While patching the tree, we need special interceptor and enhancer functions to
// prevent modification of any generated observable.
// Additional constraints:
// - Prevent outside modification of the generated observables in the trees.
// => Done using:
// - a global property `globalState.cT_patchingTree_id` which indicates which tree is being
// patched. This works because only 1 computedTree can be patched at a time.
// - `intercept()` every generated observable of the tree. It throws an error when modification
// is not allowed.
// - The patching must only reuse observables that were previously generated by the
// same computedTree, to prevent accidental modification of unrelated observables.
// => Done using a different id for each computedTree, named `cT_id`. In the tree, every generated
// observable is assigned the same id so we can recognize to which tree they are belong.
// A string or undefined: the cT_id of the computedTree being patched
globalState.cT_patchingTree_id = undefined;
// Function that launches the patching. It ensures that modifications of the tree are allowed,
// and that they are executed in a transaction (batch):
var patchComputedTreeBox = action(allowStateChangesInsideComputed(function(computedTreeBox, newValue, _patchBoxed, replaceValue) {
globalState.cT_patchingTree_id = computedTreeBox.cT_id;
_patchBoxed(computedTreeBox, newValue, replaceValue);
globalState.cT_patchingTree_id = undefined;
}));
// Creates a computedTree decorator for the given parameters:
var createComputedTreeDecorator = function(_patchBoxed, _treeDecorator, _patch_replaceValue) {
// The decorator.
// See:
// - https://github.com/mobxjs/mobx/blob/5.6.0/src/utils/decorators.ts#74
// - https://github.com/mobxjs/mobx/blob/5.6.0/src/api/decorate.ts#39
return function(target, prop, descriptor, applyImmediately) {
// The boxed observable that contains all data for the current computedTree:
var computedTreeBox = observable.box(undefined, {
name: getDebugName(target) + "." + prop,
defaultDecorator: _treeDecorator
});
computedTreeBox.cT_computeFn = descriptor.get || descriptor.value;
if ("value" in descriptor) delete descriptor.value;
descriptor.get = function() {
// Update and patch the computedTree:
var newValue = computedTreeBox.cT_computeFn.call(this);
patchComputedTreeBox(computedTreeBox, newValue, _patchBoxed, _patch_replaceValue);
newValue = computedTreeBox.value; // computedTreeBox might have changed it, so we need to get it back. Also, we need to bypass reportObserved(), because if we want to dispose the value without triggering a recomputation.
// May need to stay suspended:
if (!computedTreeBox.cT_computedValue.isBeingObserved) computedTreeBox.set(undefined);
return newValue;
};
// Execute the computed decorator on the current property:
computed(target, prop, descriptor, applyImmediately);
var computedValue = getAtom(target, prop);
computedValue.computedTreeBox = computedTreeBox;
computedTreeBox.cT_computedValue = computedValue;
computedTreeBox.cT_id = computedValue.__mapid;
onBecomeUnobserved(target, prop, function() {
// Suspend:
computedTreeBox.set(undefined);
});
};
};
var isComputedTreeProp = function(thing, property) {
return isComputedProp(thing, property) && getAtom(thing, property).computedTreeBox !== undefined;
};
// TREE FUNCTIONS:
// While building the tree, we need a few functions to manage the generated observables:
// - a decorator: `treeDecorator` (this is just an empty container to pass treeEnhancer into MobX).
// - an enhancer: `treeEnhancer`, that generates the observables of the tree.
// - an interceptor: `modificationInterceptor`, that intercepts modifications to the tree.
// - a hook into the patch function: `patch_replaceValue`, which allows us to choose
// which observable can be reused.
// Interceptor that prevents outside modification of the tree:
var modificationInterceptor = function(change) {
var object = change.object,
cT_id = isObservableMap(object) ? object.cT_id : object[$mobx].cT_id;
if (cT_id !== globalState.cT_patchingTree_id) {
var objName = getDebugName(object);
var propName = name in change ? change.name : change.index;
throw new Error("[computedTree] It is not possible to modify a computedTree. Trying to modify property '" + propName + "' of '" + objName + "'.");
}
return change;
};
// An improved `mobx.observer.deep.enhancer`, which also:
// - uses `treeDecorator` as default decorator for generated observables.
// - assigns the tree id `cT_id` to the generated observables.
// - sets up the `modificationInterceptor` to intercept changes in the generated observables.
// See: https://github.com/mobxjs/mobx/blob/5.6.0/src/types/modifiers.ts#L17
var treeEnhancer = function(v, oldValue, name) { // TODO: use oldValue to patch here instead of using the `patch()` function ?
if (isObservable(v)) return v;
var admin;
if (Array.isArray(v)) {
v = observable.array(v, { name: name , defaultDecorator: treeDecorator});
admin = v[$mobx];
}
else if (isPlainObject(v)) {
v = observable.object(v, undefined, { name: name , defaultDecorator: treeDecorator});
admin = v[$mobx];
}
else if (isMap(v)) {
v = observable.map(v, { name: name , defaultDecorator: treeDecorator});
admin = v;
}
else return v;
invariant(globalState.cT_patchingTree_id !== undefined, "globalState.cT_patchingTree_id is undefined");
admin.cT_id = globalState.cT_patchingTree_id;
intercept(v, modificationInterceptor);
return v;
};
var treeDecorator = function(target, propertyName, descriptor, _decoratorTarget, decoratorArgs) {
// This decorator is only called for new observables created from extenders because new
// observable objects are always created empty (before being filled with mobx.set()).
// Thus we know that target will always be a normal plain object, which means that the
// implementation can be simpler than the original mobx implementation.
// See: createDecoratorForEnhancer() (https://github.com/mobxjs/mobx/blob/5.6.0/src/api/observabledecorator.ts#L15)
// Note: descriptor can be undefined (how ?)
if (descriptor && descriptor.get) {
computed(target, propertyName, descriptor, true);
}
else {
set(target, propertyName, descriptor && descriptor.value);
}
};
// Required to pass the enhancer deeper in the tree:
// (See: `IObservableDecorator` https://github.com/mobxjs/mobx/blob/5.6.0/src/api/observabledecorator.ts#12)
treeDecorator.enhancer = treeEnhancer;
// Hook into `patch` which prevents reusing observables that don't belong to the tree:
// See function `defaultReplaceValue()` in MobX-patch.
var patch_replaceValue = function(oldValue_OT, newValue_T, oldValue, newValue, _defaultReplaceValue) {
// Constants from mobx-patch.js:
var OT_OTHER = 0,
OT_OBJECT = 1,
OT_ARRAY = 2,
OT_MAP = 3,
OT_EXTENDEDOBJECT = 4;
invariant(globalState.cT_patchingTree_id !== undefined, "globalState.cT_patchingTree_id is undefined");
// Check if the observable oldValue was generated by the same tree:
if (oldValue_OT !== OT_OTHER) {
if (oldValue_OT === OT_OBJECT && oldValue[$mobx].cT_id !== globalState.cT_patchingTree_id
|| oldValue_OT === OT_ARRAY && oldValue[$mobx].cT_id !== globalState.cT_patchingTree_id
|| oldValue_OT === OT_MAP && oldValue.cT_id !== globalState.cT_patchingTree_id
|| oldValue_OT === OT_EXTENDEDOBJECT && oldValue[$mobx].cT_id !== globalState.cT_patchingTree_id) {
oldValue_OT = OT_OTHER; // Setting to OT_OTHER will force the use of a new value.
}
}
// Now execute the default replaceValue:
return _defaultReplaceValue(oldValue_OT, newValue_T, oldValue, newValue);
};
// ES6 SYMBOLS FOR DEFINING COMPUTEDTREES
var $computedTree = Symbol("mobx-computedTree: $computedTree"),
$decorators = Symbol("mobx-computedTree: $decorators"),
$defaultDecorator = Symbol("mobx-computedTree: $defaultDecorator");
// Extends an observable with the computedTree definitions from the properties's symbol $computedTree.
var extendFromComputedTreeSymbols = function(target, properties) {
// This is not wrapped in a batch like in mobx.extendObservable(). But as there should
// only be computedValues, it shouldn't matter.
var props = properties[$computedTree];
if (props) {
var decorators = props[$decorators] || undefined;
var defaultDecorator = props[$defaultDecorator] || computedTreeDecorator;
Object.keys(props).forEach(function(key) {
var descriptor = Object.getOwnPropertyDescriptor(props, key);
var decorator = ( decorators && decorators[key] ) || defaultDecorator;
decorator(target, key, descriptor, true);
});
}
return target;
};
// Adding support for $computedTree in mobx.extendObservable().
// We just wrap the original function in a new custom function:
mobx.extendObservable = (function(extendObservable) { // See: https://github.com/mobxjs/mobx/blob/5.6.0/src/api/extendobservable.ts#19
return function(target, properties, decorators, options) {
return extendFromComputedTreeSymbols(extendObservable(target, properties, decorators, options), properties);
};
})(mobx.extendObservable);
// Adding support for $computedTree in mobx.observable.object().
// We just wrap the original function in a new custom function:
mobx.observable.object = (function(observable_object) { // See: https://github.com/mobxjs/mobx/blob/5.6.0/src/api/observable.ts#160
return function(props, decorators, options) {
return extendFromComputedTreeSymbols(observable_object(props, decorators, options), props);
};
})(mobx.observable.object);
// Adding support for $computedTree in patch.extender().
// We just wrap the original function in a new custom function:
window.patch.extender = (function(extender) {
return function(base) {
if (base[$computedTree]) {
if (base[$computedTree][$decorators]) Object.freeze(base[$computedTree][$decorators]);
Object.freeze(base[$computedTree]);
}
return extender(base);
};
})(window.patch.extender);
// EXPORTING:
var computedTreeDecorator = createComputedTreeDecorator(patchBoxed, treeDecorator, patch_replaceValue);
var computedTreeDecoratorObjectAsMap = createComputedTreeDecorator(patchBoxed.objToMap, treeDecorator, patch_replaceValue);
window.computedTree = computedTreeDecorator;
computedTreeDecorator.objToMap = computedTreeDecoratorObjectAsMap;
computedTreeDecorator.isComputedTreeProp = isComputedTreeProp;
computedTreeDecorator.$computedTree = $computedTree;
computedTreeDecorator.$decorators = $decorators;
computedTreeDecorator.$defaultDecorator = $defaultDecorator;
computedTreeDecorator._createComputedTreeDecorator = createComputedTreeDecorator;
computedTreeDecorator._patchComputedTreeBox = patchComputedTreeBox;
computedTreeDecorator._extendFromComputedTreeSymbols = extendFromComputedTreeSymbols;
})(window, mobx, __mobxGlobals, patch.boxed);