1 | import {Reference, Handle} from './Reference.js';
|
2 | import angular from './angularCompatibility.js';
|
3 | import stats from './utils/stats.js';
|
4 | import {makePathMatcher, joinPath, splitPath, escapeKey, unescapeKey} from './utils/paths.js';
|
5 | import {isTrussEqual, copyPrototype} from './utils/utils.js';
|
6 | import {promiseFinally} from './utils/promises.js';
|
7 |
|
8 | import _ from 'lodash';
|
9 | import performanceNow from 'performance-now';
|
10 |
|
11 |
|
12 | const RESERVED_VALUE_PROPERTY_NAMES = {$$$trussCheck: true, __ob__: true};
|
13 |
|
14 |
|
15 |
|
16 |
|
17 | let creatingObjectProperties;
|
18 |
|
19 | let currentPropertyFrozen;
|
20 |
|
21 |
|
22 | export class BaseValue {
|
23 | get $meta() {return this.$truss.meta;}
|
24 | get $store() {return this.$truss.store;}
|
25 | get $now() {return this.$truss.now;}
|
26 |
|
27 | $newKey() {return this.$truss.newKey();}
|
28 |
|
29 | $intercept(actionType, callbacks) {
|
30 | if (this.$destroyed) throw new Error('Object already destroyed');
|
31 | const unintercept = this.$truss.intercept(actionType, callbacks);
|
32 | const uninterceptAndRemoveFinalizer = () => {
|
33 | unintercept();
|
34 | _.pull(this.$$finalizers, uninterceptAndRemoveFinalizer);
|
35 | };
|
36 | this.$$finalizers.push(uninterceptAndRemoveFinalizer);
|
37 | return uninterceptAndRemoveFinalizer;
|
38 | }
|
39 |
|
40 | $connect(scope, connections) {
|
41 | if (this.$destroyed) throw new Error('Object already destroyed');
|
42 | if (!connections) {
|
43 | connections = scope;
|
44 | scope = undefined;
|
45 | }
|
46 | const connector = this.$truss.connect(scope, wrapConnections(this, connections));
|
47 | const originalDestroy = connector.destroy;
|
48 | const destroy = () => {
|
49 | _.pull(this.$$finalizers, destroy);
|
50 | return originalDestroy.call(connector);
|
51 | };
|
52 | this.$$finalizers.push(destroy);
|
53 | connector.destroy = destroy;
|
54 | return connector;
|
55 | }
|
56 |
|
57 | $peek(target, callback) {
|
58 | if (this.$destroyed) throw new Error('Object already destroyed');
|
59 | const promise = promiseFinally(
|
60 | this.$truss.peek(target, callback), () => {_.pull(this.$$finalizers, promise.cancel);}
|
61 | );
|
62 | this.$$finalizers.push(promise.cancel);
|
63 | return promise;
|
64 | }
|
65 |
|
66 | $observe(subjectFn, callbackFn, options) {
|
67 | if (this.$destroyed) throw new Error('Object already destroyed');
|
68 | let unobserveAndRemoveFinalizer;
|
69 |
|
70 | const unobserve = this.$truss.observe(() => {
|
71 | this.$$touchThis();
|
72 | return subjectFn.call(this);
|
73 | }, callbackFn.bind(this), options);
|
74 |
|
75 | unobserveAndRemoveFinalizer = () => {
|
76 | unobserve();
|
77 | _.pull(this.$$finalizers, unobserveAndRemoveFinalizer);
|
78 | };
|
79 | this.$$finalizers.push(unobserveAndRemoveFinalizer);
|
80 | return unobserveAndRemoveFinalizer;
|
81 | }
|
82 |
|
83 | $when(expression, options) {
|
84 | if (this.$destroyed) throw new Error('Object already destroyed');
|
85 | const promise = this.$truss.when(() => {
|
86 | this.$$touchThis();
|
87 | return expression.call(this);
|
88 | }, options);
|
89 | promiseFinally(promise, () => {_.pull(this.$$finalizers, promise.cancel);});
|
90 | this.$$finalizers.push(promise.cancel);
|
91 | return promise;
|
92 | }
|
93 |
|
94 | get $$finalizers() {
|
95 | Object.defineProperty(this, '$$finalizers', {
|
96 | value: [], writable: false, enumerable: false, configurable: false});
|
97 | return this.$$finalizers;
|
98 | }
|
99 | }
|
100 |
|
101 |
|
102 | class Value {
|
103 | get $parent() {return creatingObjectProperties.$parent.value;}
|
104 | get $path() {return creatingObjectProperties.$path.value;}
|
105 | get $truss() {
|
106 | Object.defineProperty(this, '$truss', {value: this.$parent.$truss});
|
107 | return this.$truss;
|
108 | }
|
109 | get $ref() {
|
110 | Object.defineProperty(this, '$ref', {value: new Reference(this.$truss._tree, this.$path)});
|
111 | return this.$ref;
|
112 | }
|
113 | get $refs() {return this.$ref;}
|
114 | get $key() {
|
115 | Object.defineProperty(
|
116 | this, '$key', {value: unescapeKey(this.$path.slice(this.$path.lastIndexOf('/') + 1))});
|
117 | return this.$key;
|
118 | }
|
119 | get $data() {return this;}
|
120 | get $hidden() {return false;}
|
121 | get $empty() {return _.isEmpty(this.$data);}
|
122 | get $keys() {return _.keys(this.$data);}
|
123 | get $values() {return _.values(this.$data);}
|
124 | get $ready() {return this.$ref.ready;}
|
125 | get $overridden() {return false;}
|
126 |
|
127 | $nextTick() {
|
128 | if (this.$destroyed) throw new Error('Object already destroyed');
|
129 | const promise = this.$truss.nextTick();
|
130 | promiseFinally(promise, () => {_.pull(this.$$finalizers, promise.cancel);});
|
131 | this.$$finalizers.push(promise.cancel);
|
132 | return promise;
|
133 | }
|
134 |
|
135 | $freezeComputedProperty() {
|
136 | if (!_.isBoolean(currentPropertyFrozen)) {
|
137 | throw new Error('Cannot freeze a computed property outside of its getter function');
|
138 | }
|
139 | currentPropertyFrozen = true;
|
140 | }
|
141 |
|
142 | $set(value) {return this.$ref.set(value);}
|
143 | $update(values) {return this.$ref.update(values);}
|
144 | $override(values) {return this.$ref.override(values);}
|
145 | $commit(options, updateFn) {return this.$ref.commit(options, updateFn);}
|
146 |
|
147 | $$touchThis() {
|
148 |
|
149 | if (this.__ob__) {
|
150 | this.__ob__.dep.depend();
|
151 | } else if (this.$parent) {
|
152 | (this.$parent.hasOwnProperty('$data') ? this.$parent.$data : this.$parent)[this.$key];
|
153 | } else {
|
154 | this.$store;
|
155 | }
|
156 |
|
157 | }
|
158 |
|
159 | get $$initializers() {
|
160 | Object.defineProperty(this, '$$initializers', {
|
161 | value: [], writable: false, enumerable: false, configurable: true});
|
162 | return this.$$initializers;
|
163 | }
|
164 |
|
165 | get $destroyed() {
|
166 | return false;
|
167 | }
|
168 | }
|
169 |
|
170 | copyPrototype(BaseValue, Value);
|
171 |
|
172 | _.forEach(Value.prototype, (prop, name) => {
|
173 | Object.defineProperty(
|
174 | Value.prototype, name, {value: prop, enumerable: false, configurable: false, writable: false});
|
175 | });
|
176 |
|
177 |
|
178 | class ErrorWrapper {
|
179 | constructor(error) {
|
180 | this.error = error;
|
181 | }
|
182 | }
|
183 |
|
184 |
|
185 | class FrozenWrapper {
|
186 | constructor(value) {
|
187 | this.value = value;
|
188 | }
|
189 | }
|
190 |
|
191 |
|
192 | export default class Modeler {
|
193 | constructor(debug) {
|
194 | this._trie = {Class: Value};
|
195 | this._debug = debug;
|
196 | Object.freeze(this);
|
197 | }
|
198 |
|
199 | init(classes, rootAcceptable) {
|
200 | if (_.isPlainObject(classes)) {
|
201 | _.forEach(classes, (Class, path) => {
|
202 | if (Class.$trussMount) return;
|
203 | Class.$$trussMount = Class.$$trussMount || [];
|
204 | Class.$$trussMount.push(path);
|
205 | });
|
206 | classes = _.values(classes);
|
207 | _.forEach(classes, Class => {
|
208 | if (!Class.$trussMount && Class.$$trussMount) {
|
209 | Class.$trussMount = Class.$$trussMount;
|
210 | delete Class.$$trussMount;
|
211 | }
|
212 | });
|
213 | }
|
214 | classes = _.uniq(classes);
|
215 | _.forEach(classes, Class => this._mountClass(Class, rootAcceptable));
|
216 | this._decorateTrie(this._trie);
|
217 | }
|
218 |
|
219 | destroy() {
|
220 | }
|
221 |
|
222 | _getMount(path, scaffold, predicate) {
|
223 | const segments = splitPath(path, true);
|
224 | let node;
|
225 | for (const segment of segments) {
|
226 | let child = segment ?
|
227 | node.children && (node.children[segment] || !scaffold && node.children.$) : this._trie;
|
228 | if (!child) {
|
229 | if (!scaffold) return;
|
230 | node.children = node.children || {};
|
231 | child = node.children[segment] = {Class: Value};
|
232 | }
|
233 | node = child;
|
234 | if (predicate && predicate(node)) break;
|
235 | }
|
236 | return node;
|
237 | }
|
238 |
|
239 | _findMount(predicate, node) {
|
240 | if (!node) node = this._trie;
|
241 | if (predicate(node)) return node;
|
242 | for (const childKey of _.keys(node.children)) {
|
243 | const result = this._findMount(predicate, node.children[childKey]);
|
244 | if (result) return result;
|
245 | }
|
246 | }
|
247 |
|
248 | _decorateTrie(node) {
|
249 | _.forEach(node.children, child => {
|
250 | this._decorateTrie(child);
|
251 | if (child.local || child.localDescendants) node.localDescendants = true;
|
252 | });
|
253 | }
|
254 |
|
255 | _augmentClass(Class) {
|
256 | let computedProperties;
|
257 | let proto = Class.prototype;
|
258 | while (proto && proto.constructor !== Object) {
|
259 | for (const name of Object.getOwnPropertyNames(proto)) {
|
260 | const descriptor = Object.getOwnPropertyDescriptor(proto, name);
|
261 | if (name.charAt(0) === '$') {
|
262 | if (name === '$finalize') continue;
|
263 | if (_.isEqual(descriptor, Object.getOwnPropertyDescriptor(Value.prototype, name))) {
|
264 | continue;
|
265 | }
|
266 | throw new Error(`Property names starting with "$" are reserved: ${Class.name}.${name}`);
|
267 | }
|
268 | if (descriptor.get && !(computedProperties && computedProperties[name])) {
|
269 | (computedProperties || (computedProperties = {}))[name] = {
|
270 | name, fullName: `${proto.constructor.name}.${name}`, get: descriptor.get,
|
271 | set: descriptor.set
|
272 | };
|
273 | }
|
274 | }
|
275 | proto = Object.getPrototypeOf(proto);
|
276 | }
|
277 | for (const name of Object.getOwnPropertyNames(Value.prototype)) {
|
278 | if (name === 'constructor' || Class.prototype.hasOwnProperty(name)) continue;
|
279 | Object.defineProperty(
|
280 | Class.prototype, name, Object.getOwnPropertyDescriptor(Value.prototype, name));
|
281 | }
|
282 | return computedProperties;
|
283 | }
|
284 |
|
285 | _mountClass(Class, rootAcceptable) {
|
286 | const computedProperties = this._augmentClass(Class);
|
287 | const allVariables = [];
|
288 | let mounts = Class.$trussMount;
|
289 | if (!mounts) throw new Error(`Class ${Class.name} lacks a $trussMount static property`);
|
290 | if (!_.isArray(mounts)) mounts = [mounts];
|
291 | _.forEach(mounts, mount => {
|
292 | if (_.isString(mount)) mount = {path: mount};
|
293 | if (!rootAcceptable && mount.path === '/') {
|
294 | throw new Error('Data root already accessed, too late to mount class');
|
295 | }
|
296 | const matcher = makePathMatcher(mount.path);
|
297 | for (const variable of matcher.variables) {
|
298 | if (variable === '$' || variable.charAt(1) === '$') {
|
299 | throw new Error(`Invalid variable name: ${variable}`);
|
300 | }
|
301 | if (variable.charAt(0) === '$' && (
|
302 | _.has(Value.prototype, variable) || RESERVED_VALUE_PROPERTY_NAMES[variable]
|
303 | )) {
|
304 | throw new Error(`Variable name conflicts with built-in property or method: ${variable}`);
|
305 | }
|
306 | allVariables.push(variable);
|
307 | }
|
308 | const escapedKey = mount.path.match(/\/([^/]*)$/)[1];
|
309 | if (escapedKey.charAt(0) === '$') {
|
310 | if (mount.placeholder) {
|
311 | throw new Error(
|
312 | `Class ${Class.name} mounted at wildcard ${escapedKey} cannot be a placeholder`);
|
313 | }
|
314 | } else if (!_.has(mount, 'placeholder')) {
|
315 | mount.placeholder = {};
|
316 | }
|
317 | const targetMount = this._getMount(mount.path.replace(/\$[^/]*/g, '$'), true);
|
318 | if (targetMount.matcher && (
|
319 | targetMount.escapedKey === escapedKey ||
|
320 | targetMount.escapedKey.charAt(0) === '$' && escapedKey.charAt(0) === '$'
|
321 | )) {
|
322 | throw new Error(
|
323 | `Multiple classes mounted at ${mount.path}: ${targetMount.Class.name}, ${Class.name}`);
|
324 | }
|
325 | _.assign(
|
326 | targetMount, {Class, matcher, computedProperties, escapedKey},
|
327 | _.pick(mount, 'placeholder', 'local', 'keysUnsafe', 'hidden'));
|
328 | });
|
329 | _(allVariables).uniq().forEach(variable => {
|
330 | Object.defineProperty(Class.prototype, variable, {get() {
|
331 | return creatingObjectProperties ?
|
332 | creatingObjectProperties[variable] && creatingObjectProperties[variable].value :
|
333 | undefined;
|
334 | }});
|
335 | });
|
336 | }
|
337 |
|
338 | |
339 |
|
340 |
|
341 |
|
342 |
|
343 | createObject(path, properties) {
|
344 | const mount = this._getMount(path) || {Class: Value};
|
345 | try {
|
346 | if (mount.matcher) {
|
347 | const match = mount.matcher.match(path);
|
348 | for (const variable in match) {
|
349 | properties[variable] = {value: match[variable]};
|
350 | }
|
351 | }
|
352 |
|
353 | creatingObjectProperties = properties;
|
354 | const object = new mount.Class();
|
355 | creatingObjectProperties = null;
|
356 |
|
357 | if (angular.active) this._wrapProperties(object);
|
358 |
|
359 | if (mount.keysUnsafe) {
|
360 | properties.$data = {value: Object.create(null), configurable: true, enumerable: true};
|
361 | }
|
362 | if (mount.hidden) properties.$hidden = {value: true};
|
363 | if (mount.computedProperties) {
|
364 | _.forEach(mount.computedProperties, prop => {
|
365 | properties[prop.name] = this._buildComputedPropertyDescriptor(object, prop);
|
366 | });
|
367 | }
|
368 |
|
369 | return object;
|
370 | } catch (e) {
|
371 | e.extra = _.assign({mount, properties, className: mount.Class && mount.Class.name}, e.extra);
|
372 | throw e;
|
373 | }
|
374 | }
|
375 |
|
376 | _wrapProperties(object) {
|
377 | _.forEach(object, (value, key) => {
|
378 | const valueKey = '$_' + key;
|
379 | Object.defineProperties(object, {
|
380 | [valueKey]: {value, writable: true},
|
381 | [key]: {
|
382 | get: () => object[valueKey],
|
383 | set: arg => {object[valueKey] = arg; angular.digest();},
|
384 | enumerable: true, configurable: true
|
385 | }
|
386 | });
|
387 | });
|
388 | }
|
389 |
|
390 | _buildComputedPropertyDescriptor(object, prop) {
|
391 | const propertyStats = stats.for(prop.fullName);
|
392 |
|
393 | let value, pendingPromise;
|
394 | let writeAllowed = false;
|
395 |
|
396 | object.$$initializers.push(vue => {
|
397 | let unwatchNow = false;
|
398 | const compute = computeValue.bind(object, prop, propertyStats);
|
399 | if (this._debug) compute.toString = () => {return prop.fullName;};
|
400 | let unwatch = () => {unwatchNow = true;};
|
401 | unwatch = vue.$watch(compute, newValue => {
|
402 | if (object.$destroyed) {
|
403 | unwatch();
|
404 | return;
|
405 | }
|
406 | if (pendingPromise) {
|
407 | if (pendingPromise.cancel) pendingPromise.cancel();
|
408 | pendingPromise = undefined;
|
409 | }
|
410 | if (_.isObject(newValue) && _.isFunction(newValue.then)) {
|
411 | const promise = newValue.then(finalValue => {
|
412 | if (promise === pendingPromise) update(finalValue);
|
413 |
|
414 |
|
415 | }, error => {
|
416 | if (promise === pendingPromise && update(new ErrorWrapper(error)) &&
|
417 | !error.trussExpectedException) throw error;
|
418 | });
|
419 | pendingPromise = promise;
|
420 | } else if (update(newValue)) {
|
421 | angular.digest();
|
422 | if (newValue instanceof ErrorWrapper && !newValue.error.trussExpectedException) {
|
423 | throw newValue.error;
|
424 | }
|
425 | }
|
426 | }, {immediate: true});
|
427 |
|
428 |
|
429 |
|
430 | const watcher = _.last(vue._watchers || vue._scope.effects);
|
431 | watcher.id = -watcher.id;
|
432 |
|
433 | function update(newValue) {
|
434 | if (newValue instanceof FrozenWrapper) {
|
435 | newValue = newValue.value;
|
436 | unwatch();
|
437 | _.pull(object.$$finalizers, unwatch);
|
438 | }
|
439 | if (isTrussEqual(value, newValue)) return;
|
440 |
|
441 | propertyStats.numUpdates += 1;
|
442 | writeAllowed = true;
|
443 | object[prop.name] = newValue;
|
444 | writeAllowed = false;
|
445 |
|
446 |
|
447 |
|
448 |
|
449 |
|
450 |
|
451 |
|
452 | freeze(newValue);
|
453 | return true;
|
454 | }
|
455 |
|
456 | if (unwatchNow) {
|
457 | unwatch();
|
458 | } else {
|
459 | object.$$finalizers.push(unwatch);
|
460 | }
|
461 | });
|
462 | return {
|
463 | enumerable: true, configurable: true,
|
464 | get() {
|
465 | if (!writeAllowed && value instanceof ErrorWrapper) throw value.error;
|
466 | return value;
|
467 | },
|
468 | set(newValue) {
|
469 | if (writeAllowed) {
|
470 | value = newValue;
|
471 | } else if (prop.set) {
|
472 | prop.set.call(this, newValue);
|
473 | } else {
|
474 | throw new Error(`You cannot set a computed property: ${prop.name}`);
|
475 | }
|
476 | }
|
477 | };
|
478 | }
|
479 |
|
480 | destroyObject(object) {
|
481 | if (_.has(object, '$$finalizers')) {
|
482 |
|
483 | for (const fn of _.clone(object.$$finalizers)) fn();
|
484 | }
|
485 | if (_.isFunction(object.$finalize)) object.$finalize();
|
486 | Object.defineProperty(
|
487 | object, '$destroyed', {value: true, enumerable: false, configurable: false});
|
488 | }
|
489 |
|
490 | isPlaceholder(path) {
|
491 | const mount = this._getMount(path);
|
492 | return mount && mount.placeholder;
|
493 | }
|
494 |
|
495 | isLocal(path, value) {
|
496 |
|
497 | const mount = this._getMount(path, false, mount => mount.local);
|
498 | if (mount && mount.local) return true;
|
499 | if (this._hasLocalProperties(mount, value)) {
|
500 | throw new Error('Write on a mix of local and remote tree paths.');
|
501 | }
|
502 | return false;
|
503 | }
|
504 |
|
505 | _hasLocalProperties(mount, value) {
|
506 | if (!mount) return false;
|
507 | if (mount.local) return true;
|
508 | if (!mount.localDescendants || !_.isObject(value)) return false;
|
509 | for (const key in value) {
|
510 | const local =
|
511 | this._hasLocalProperties(mount.children[escapeKey(key)] || mount.children.$, value[key]);
|
512 | if (local) return true;
|
513 | }
|
514 | return false;
|
515 | }
|
516 |
|
517 | forEachPlaceholderChild(path, iteratee) {
|
518 | const mount = this._getMount(path);
|
519 | _.forEach(mount && mount.children, child => {
|
520 | if (child.placeholder) iteratee(child);
|
521 | });
|
522 | }
|
523 |
|
524 | checkVueObject(object, path, checkedObjects) {
|
525 | const top = !checkedObjects;
|
526 | if (top) checkedObjects = [];
|
527 | try {
|
528 | for (const key of Object.getOwnPropertyNames(object)) {
|
529 | if (RESERVED_VALUE_PROPERTY_NAMES[key] || Value.prototype.hasOwnProperty(key) ||
|
530 | /^\$_/.test(key)) continue;
|
531 |
|
532 | const mount = this._findMount(mount => mount.Class === object.constructor);
|
533 | if (mount && mount.matcher && _.includes(mount.matcher.variables, key)) continue;
|
534 | let value;
|
535 | try {
|
536 | value = object[key];
|
537 | } catch (e) {
|
538 |
|
539 |
|
540 | continue;
|
541 | }
|
542 | if (!(_.isArray(object) && (/\d+/.test(key) || key === 'length'))) {
|
543 | const descriptor = Object.getOwnPropertyDescriptor(object, key);
|
544 | if ('value' in descriptor || !descriptor.get) {
|
545 | throw new Error(
|
546 | `Value at ${path}, contained in a Firetruss object, has a rogue property: ${key}`);
|
547 | }
|
548 | if (object.$truss && descriptor.enumerable) {
|
549 | try {
|
550 | object[key] = value;
|
551 | throw new Error(
|
552 | `Firetruss object at ${path} has an enumerable non-Firebase property: ${key}`);
|
553 | } catch (e) {
|
554 | if (e.trussCode !== 'firebase_overwrite') throw e;
|
555 | }
|
556 | }
|
557 | }
|
558 | if (_.isObject(value) && !value.$$$trussCheck && Object.isExtensible(value) &&
|
559 | !(_.isFunction(value) || value instanceof Promise)) {
|
560 | value.$$$trussCheck = true;
|
561 | checkedObjects.push(value);
|
562 | this.checkVueObject(value, joinPath(path, escapeKey(key)), checkedObjects);
|
563 | }
|
564 | }
|
565 | } finally {
|
566 | if (top) {
|
567 | for (const item of checkedObjects) delete item.$$$trussCheck;
|
568 | }
|
569 | }
|
570 | }
|
571 | }
|
572 |
|
573 |
|
574 | function computeValue(prop, propertyStats) {
|
575 |
|
576 | if (this.$destroyed) return;
|
577 |
|
578 |
|
579 | this.$$touchThis();
|
580 |
|
581 | const oldPropertyFrozen = currentPropertyFrozen;
|
582 | currentPropertyFrozen = false;
|
583 | const startTime = performanceNow();
|
584 | let value;
|
585 | try {
|
586 | try {
|
587 | value = prop.get.call(this);
|
588 | } catch (e) {
|
589 | value = new ErrorWrapper(e);
|
590 | } finally {
|
591 | propertyStats.runtime += performanceNow() - startTime;
|
592 | propertyStats.numRecomputes += 1;
|
593 | }
|
594 | if (currentPropertyFrozen) value = new FrozenWrapper(value);
|
595 | return value;
|
596 | } finally {
|
597 | currentPropertyFrozen = oldPropertyFrozen;
|
598 | }
|
599 |
|
600 | }
|
601 |
|
602 | function wrapConnections(object, connections) {
|
603 | if (!connections || connections instanceof Handle) return connections;
|
604 | return _.mapValues(connections, descriptor => {
|
605 | if (descriptor instanceof Handle) return descriptor;
|
606 | if (_.isFunction(descriptor)) {
|
607 | const fn = function() {
|
608 |
|
609 | object.$$touchThis();
|
610 | return wrapConnections(object, descriptor.call(this));
|
611 |
|
612 | };
|
613 | fn.angularWatchSuppressed = true;
|
614 | return fn;
|
615 | }
|
616 | return wrapConnections(object, descriptor);
|
617 | });
|
618 | }
|
619 |
|
620 | function freeze(object) {
|
621 | if (_.isNil(object) || !_.isObject(object) || Object.isFrozen(object) || object.$truss) {
|
622 | return object;
|
623 | }
|
624 | object = Object.freeze(object);
|
625 | if (_.isArray(object)) return _.map(object, value => freeze(value));
|
626 | return _.mapValues(object, value => freeze(value));
|
627 | }
|