Home Reference Source Repository

src/ShadowImpl.js

  1. import clone from "lodash.clone";
  2. import cloneDeep from "lodash.clonedeep";
  3. import isString from "lodash.isstring";
  4. import result from "lodash.result";
  5.  
  6. import {
  7. assert,
  8. isPrimitive,
  9. isSomething,
  10. } from "akutils";
  11.  
  12. import Access from "./Access";
  13. import extendProperty from "./extendProperty";
  14. import isShadow from "./isShadow";
  15. import reshadow from "./reshadow";
  16.  
  17. import appDebug, { ShadowImplKey as DebugKey } from "./debug";
  18. const debug = appDebug(DebugKey);
  19.  
  20.  
  21. // instance variable names
  22. const _access = Symbol('access');
  23. // object to store expensive derived values
  24. const _cache = Symbol('cache');
  25. // flag indicating property has pending updates. Not safe to rely on this[_futureState] as it could be
  26. // undefined if that is the next value
  27. const _changed = Symbol('changed');
  28. const _date = Symbol('date');
  29. const _dead = Symbol('dead');
  30. const _didShadowCalled = Symbol('didShadowCalled');
  31. const _futureState = Symbol('futureState');
  32. const _invalid = Symbol('invalid');
  33. const _name = Symbol('name');
  34. const _nextName = Symbol('nextName');
  35. const _path = Symbol('path');
  36.  
  37. // flag marks this property as obsolete and thus no longer to effect updates on the
  38. // next data model
  39. const _preventUpdates = Symbol('preventUpdates');
  40. const _previousTime = Symbol('previousTime');
  41. const _property = Symbol('property');
  42. const _readonly = Symbol('readonly');
  43. const _replaced = Symbol('replaced');
  44. const _scheduled = Symbol('scheduled');
  45. const _shadow = Symbol('shadow');
  46. const _state = Symbol('state');
  47. const _time = Symbol('time');
  48.  
  49. // private method symbols
  50. const _createShadow = Symbol('createShadow');
  51. const _defineProperty = Symbol('defineProperty');
  52. const _modelForUpdate = Symbol('modelForUpdate');
  53. const _scheduleUpdate = Symbol('scheduleUpdate');
  54. const _setupShadow = Symbol('setupShadow');
  55.  
  56.  
  57. /*
  58. Todos:
  59. * Isolated support
  60. -
  61.  
  62. * Reduce memory footprint:
  63. 1) investigate _time and _previousTime really needed
  64. 2) investigate getting access from property (reduce object creation and memory footprint)
  65.  
  66. * Investigate replacing impl with Proxy
  67. */
  68.  
  69. /**
  70. The base class for {@link Shadow} backing objects. Each shadow property has an 'impl' that
  71. performs the f.lux bookkeeping to enable the shadow state to work properly. The 'impl' is
  72. broken out from the shadow proper to prevent polluting the namespace with a bunch of crazy
  73. looking variables and methods.
  74.  
  75. A shadow property's 'impl' is available through the {@link Shadow.__} method. Direct access
  76. to the 'impl' is rarely needed by custom properties, shadows, or application logic. And there
  77. almost certainly no reason to directly subclass this class.
  78.  
  79. @see {@link Shadow.__}
  80. */
  81. export default class ShadowImpl {
  82. constructor(time, property, name, state, parent, shader, prev) {
  83. this[_property] = property;
  84. this[_name] = name;
  85. this[_state] = state;
  86. this[_time] = time;
  87.  
  88. if (prev) {
  89. this[_previousTime] = prev[_time];
  90. }
  91.  
  92. // TODO: quick hack till have unit tests and thought out life-cycle design
  93. // didShadow() is being called multiple times which is causing a problem with property
  94. // initialization that should only occur once.
  95. this[_didShadowCalled] = false;
  96. }
  97.  
  98. access() {
  99. const parent = this.parent();
  100.  
  101. if (!this[_access]) {
  102. if (parent && parent.access().create$ForChild) {
  103. // property does not know about this impl yet. So impl.property() will work but property.__() will not
  104. this[_access] = parent.access().create$ForChild(this);
  105. } else {
  106. this[_access] = this[_property].create$(this);
  107. }
  108. }
  109.  
  110. return this[_access];
  111. }
  112.  
  113. /**
  114. Replace the value of this property. This will result in this property tree being recreated.
  115.  
  116. Note: This value will be used directly (not copied) so ensure the state is not altered.
  117. */
  118. assign(nextState, name) {
  119. nextState = isShadow(nextState) ?nextState.__().state() :nextState;
  120.  
  121. //create a deep copy so not shared with the passed in value
  122. //this.deepcopy() will use current model if no value passed or value passed is null or undefined
  123. //in case of assigned those are legal values so must check explicitly
  124. return this.update( state => {
  125. return { name: name || "assign()", nextState, replace: true };
  126. });
  127. }
  128.  
  129. /**
  130. Prevents all children from being able to obtain model in update() callbacks. Update callbacks
  131. should invoke this method when they perform wholesale
  132. */
  133. blockFurtherChildUpdates() {
  134. if (!this.hasChildren()) { return }
  135.  
  136. const children = this.children();
  137.  
  138. for (let i=0, child; child=children[i]; i++) {
  139. child.blockFurtherUpdates(true);
  140. }
  141. }
  142.  
  143. /**
  144. Prevents this property and descendents from providing a model to update() callbacks.
  145.  
  146. The update() method invokes this method when the callback returns a different object than the
  147. one passed into the callback.
  148. */
  149. blockFurtherUpdates(replaced) {
  150. this[_preventUpdates] = true;
  151. this.invalidate(null, this);
  152.  
  153. if (replaced) {
  154. this[_replaced] = true;
  155. }
  156.  
  157. this.blockFurtherChildUpdates();
  158. }
  159.  
  160. changeParent(newParent) {
  161. assert( a => a.is(this.isValid(), `Property must be valid to change parent: ${ this.dotPath() }`)
  162. .not(this.isRoot(), `Root properties do not have parents: ${ this.dotPath() }`) );
  163.  
  164. debug( d => d(`changeParent(): ${this.dotPath()}`) );
  165.  
  166. // clear cache
  167. delete this[_cache];
  168.  
  169. // setup access through shadows
  170. this[_defineProperty]();
  171. }
  172.  
  173. cache() {
  174. if (!this[_cache]) {
  175. this[_cache] = {};
  176. }
  177.  
  178. return this[_cache];
  179. }
  180.  
  181. /**
  182. Create a copy of the internals during reshadowing when the property has not changed during the
  183. update process but some descendant has been modified.
  184. */
  185. createCopy(time, newModel, parentImpl) {
  186. const property = this[_property];
  187. const ImplClass = property.implementationClass();
  188. const name = this.nextName();
  189. const shader = this.shader(newModel);
  190.  
  191. return new ImplClass(time, property, name, newModel, parentImpl, shader, this);
  192. }
  193.  
  194. didShadow(time, newRoot) {
  195. const storeRootImpl = this.store().rootImpl;
  196.  
  197. if (this[_time] == time && !this[_didShadowCalled] && storeRootImpl === this.root()) {
  198. this[_didShadowCalled] = true;
  199.  
  200. if (this.isRoot()) {
  201. if (this[_previousTime] || !newRoot) {
  202. this[_property].onPropertyDidUpdate();
  203. } else {
  204. this[_property].onPropertyDidShadow();
  205. }
  206. } else {
  207. this[_previousTime] ?this[_property].onPropertyDidUpdate() :this[_property].onPropertyDidShadow();
  208. }
  209.  
  210. if (this.hasChildren()) {
  211. const children = this.children();
  212. var childImpl;
  213.  
  214. for (let i=0, len=children.length; i<len; i++) {
  215. let childImpl = children[i];
  216.  
  217. if (childImpl) {
  218. childImpl.didShadow(time);
  219. }
  220. }
  221. }
  222. }
  223. }
  224.  
  225. /**
  226. Intended for use by update() and replaying actions.
  227. */
  228. dispatchUpdate(action) {
  229. if (!this[_preventUpdates] && this.isUpdatable() && this.isActive()) {
  230. const { name, nextState, replace } = action;
  231.  
  232. // Sending to store first ensures:
  233. // 1) nextState() returns value from before this udpate
  234. // 2) middleware provided chance to make changes to action
  235. this.store().onPreStateUpdate(action, this);
  236.  
  237. // replacing the current object prevents further next state changes for sub-properties
  238. if (replace) {
  239. this[_replaced] = true;
  240. // block child updates because replacement makes them unreachable
  241. this.blockFurtherChildUpdates();
  242. this.onReplaced();
  243. }
  244.  
  245. // set the next model data
  246. this[_futureState] = nextState;
  247.  
  248. // update the parent's future state to reference the state returned by the action
  249. if (!this.isRoot() && !this.isIsolated()) {
  250. const parentNextData = this.parent()[_modelForUpdate]();
  251.  
  252. // do nothing if parentNextData is not assignable
  253. if (parentNextData && !isPrimitive(parentNextData)) {
  254. parentNextData[this[_name]] = nextState;
  255. }
  256. }
  257.  
  258. this.invalidate(null, this);
  259.  
  260. this.store().onPostStateUpdate(action, this);
  261. this.root()[_scheduleUpdate]();
  262. }
  263. }
  264.  
  265. /**
  266. Helpful debugging utility that returns the path joined by '.'. The root node will return the
  267. word 'root' for the path.
  268. */
  269. dotPath() {
  270. const cache = this.cache();
  271.  
  272. if (!cache.dotPath) {
  273. const path = this.path();
  274.  
  275. cache.dotPath = path.length ?path.join('.') :'root';
  276. }
  277.  
  278. return cache.dotPath;
  279. }
  280.  
  281. ensureMounted() {
  282. if (this.isRoot() || this.isIsolated() || this.__getCalled__) { return }
  283.  
  284. result(this.store().shadow, this.dotPath())
  285. }
  286.  
  287. findByPath(path) {
  288. if (path.length === 0) { return this; }
  289.  
  290. const next = this.getChild(path[0]);
  291.  
  292. return next && next.findByPath(path.slice(1));
  293. }
  294.  
  295. /**
  296. Gets if an update has occurred directly to this property.
  297. */
  298. hasPendingChanges() {
  299. return !!this[_changed];
  300. }
  301.  
  302. /**
  303. Marks property and ancestors as invalid. This means this property or one of its children
  304. has been updated. The invalid flag is set to the earliest timestamp when this property
  305. or one of its children was changed.
  306.  
  307. Parameters:
  308. childImpl - the child implementation triggering this call or undefined if this implementation
  309. started the invalidation process
  310. source - the shadow implementation that triggered the invalidation
  311. */
  312. invalidate(childImpl, source=this) {
  313. const owner = this.owner();
  314.  
  315. if (childImpl) {
  316. this[_property].onChildInvalidated(childImpl.property(), source.property());
  317. }
  318.  
  319. if (this.isValid() && this.isActive()) {
  320. this[_invalid] = true;
  321.  
  322. if (owner) {
  323. owner.invalidate(this, source);
  324. }
  325. }
  326. }
  327.  
  328. /**
  329. Gets if the property represents live data.
  330. */
  331. isActive() {
  332. return !this[_dead];
  333. }
  334.  
  335. isIsolated() {
  336. return this.property().isIsolated();
  337. }
  338.  
  339. isLeaf() {
  340. return !this.hasChildren();
  341. }
  342.  
  343. isRoot() {
  344. return this.property().isRoot();
  345. }
  346.  
  347. /**
  348. Gets if this property or one of its child properties has pending updates. Returns true if there are no
  349. pending updates.
  350. */
  351. isValid() {
  352. return !this[_invalid];
  353. }
  354.  
  355. latest() {
  356. return this.store().findByPath(this.path());
  357. }
  358.  
  359. name() {
  360. return this[_name];
  361. }
  362.  
  363. /**
  364. Gets the name after all model updates are performed.
  365. */
  366. nextName() {
  367. return this[_nextName] !== undefined ?this[_nextName] :this[_name];
  368. }
  369.  
  370. /**
  371. Gets the model as it will be once all pending changes are recorded with the store. This must
  372. not be altered.
  373. */
  374. nextState() {
  375. return this.hasPendingChanges() || !this.isValid() ?this[_futureState] :this.state();
  376. }
  377.  
  378. /**
  379. Marks this property as obsolete. Once marked obsolete a property may not interact with the store.
  380. A property becomes obsolete after it's value or ancestor's value has changed and the update process
  381. has completed.
  382.  
  383. This method does not affect subproperties.
  384. */
  385. obsolete(callback) {
  386. if (callback) {
  387. callback(this);
  388. }
  389.  
  390. this[_dead] = true;
  391. }
  392.  
  393. obsoleteChildren() {
  394. if (this.hasChildren()) {
  395. const children = this.children();
  396.  
  397. for (let i=0, len=children.length; i<len; i++) {
  398. let child = children[i];
  399.  
  400. if (child) {
  401. child.obsoleteTree();
  402. }
  403. }
  404. }
  405. }
  406.  
  407. /**
  408. Marks the entire subtree as inactive, aka dead.
  409. */
  410. obsoleteTree(callback) {
  411. if (!this[_dead]) {
  412. this.obsolete(callback);
  413. this.obsoleteChildren();
  414. }
  415. }
  416.  
  417. owner() {
  418. const ownerProperty = this[_property].owner();
  419.  
  420. return ownerProperty && ownerProperty.__();
  421. }
  422.  
  423. parent() {
  424. const parentProperty = this.property().parent();
  425.  
  426. return parentProperty && parentProperty.__();
  427. }
  428.  
  429. /**
  430. Gets an array with the property names/indices from the root to this property.
  431. */
  432. path() {
  433. const cache = this.cache();
  434.  
  435. if (this.isRoot()) {
  436. return [];
  437. } else if (!cache.path) {
  438. cache.path = this.parent().path().concat(this[_name]);
  439. }
  440.  
  441. return cache.path;
  442. }
  443.  
  444. property() {
  445. return this[_property];
  446. }
  447.  
  448. readonly() {
  449. return this[_readonly] === undefined ?this[_property].isReadonly() :this[_readonly];
  450. }
  451.  
  452. replaced() {
  453. return !!this[_replaced];
  454. }
  455.  
  456. /**
  457. Invoked by reshadow() function for invalid parent property implementations when the directly
  458. managed state did not change.
  459.  
  460. Calls the onReshadow(prev) method to provide subclasses an oppotunity to setup for futher
  461. action after a parent change.
  462. */
  463. reshadowed(prev) {
  464. debug( d => d(`reshadowed(): ${this.dotPath()}, mapped=${prev.isMapped()}, time=${this[_time]}, prevTime=${prev[_time]}`) );
  465.  
  466. if (prev.__getCalled__) {
  467. this[_setupShadow](prev, true);
  468. }
  469.  
  470. this.onReshadow(prev);
  471. }
  472.  
  473. root() {
  474. if (this[_property].isRoot()) { return this }
  475.  
  476. const cache = this.cache();
  477.  
  478. if (!cache.root) {
  479. cache.root = this.owner().root();
  480. }
  481.  
  482. return cache.root;
  483. }
  484.  
  485. /**
  486. Sets the readonly flag which will prevent a 'set' function being set in defineProeprty().
  487.  
  488. Note: this method must be called before defineProperty() is invoked or it will have no affect.
  489. */
  490. setReadonly(readonly) {
  491. this[_readonly] = readonly;
  492. }
  493.  
  494. /**
  495. Creates shadow properties for root properties and sets this property on the parent property for
  496. non-root properties.
  497.  
  498. Note: This method is called by shadowProperty() and reshadow() functions.
  499. */
  500. setupPropertyAccess(prev) {
  501. const property = this[_property];
  502.  
  503. if (this.isRoot() || this.isIsolated()) {
  504. this[_setupShadow](prev);
  505. } else {
  506. this[_defineProperty](prev);
  507. }
  508. }
  509.  
  510. /**
  511. Gets the shader needed to recreate the shadow property for the state.
  512. */
  513. shader(state) {
  514. return this[_property].shader(state);
  515. }
  516.  
  517. /**
  518. Gets the user facing property represented by this implementation object.
  519. */
  520. shadow() {
  521. if (!this.isMapped()) { throw new Error(`Property implementation not mapped: ${this.dotPath()}`) }
  522.  
  523. return this[_setupShadow]()
  524. }
  525.  
  526. /**
  527. Helpful debugging utility that returns the path joined by '.'. The root node will return the
  528. word 'root' for the path.
  529. */
  530. slashPath() {
  531. const cache = this.cache();
  532.  
  533. if (!cache.slashPath) {
  534. const path = this.path();
  535.  
  536. cache.slashPath = path.length ?`/${path.join('/')}` :'/';
  537. }
  538.  
  539. return cache.slashPath;
  540. }
  541.  
  542. state() {
  543. return this[_state];
  544. }
  545.  
  546. store() {
  547. return this[_property].store();
  548. }
  549.  
  550. /**
  551. Transfers the nextName to the name attribute.
  552. */
  553. switchName() {
  554. if (this[_nextName] !== undefined) {
  555. this[_name] = this[_nextName];
  556. delete this[_nextName];
  557. }
  558. }
  559.  
  560. time() {
  561. return this[_time];
  562. }
  563.  
  564. /**
  565. Gets a compact version of this internal's state. It does NOT provide a JSON representation of the
  566. model state. The actual Property.toJSON() method returns the model JSON representation.
  567. */
  568. toJSON() {
  569. return {
  570. name: this[_name],
  571. path: this.dotPath(),
  572. active: !this[_dead],
  573. valid: this.isValid(),
  574. state: this.state(),
  575. }
  576. }
  577.  
  578. //Gets a stringified version of the toJSON() method.
  579. toString() {
  580. return JSON.stringify(this);
  581. }
  582.  
  583. /**
  584. Makes changes to the next property state. The callback should be pure (no side affects) but that
  585. is not a requirement. The callback must be of the form:
  586.  
  587. (state) => return { nextState, replace }
  588.  
  589. where:
  590. state - the next property state
  591. nextState - the state following the callback
  592. replace - boolean for whether nextState replaces the current value. The implication of true
  593. is that this property and all of it's children will not be able to make future changes
  594. to the model.
  595.  
  596. To understand the reasoning behind the replace flag consider the following example:
  597.  
  598. const model = { a: { b: { c: 1 } } }
  599. const oldB = model.a.b
  600.  
  601. model.a.b = "foo"
  602. oldB.c = 5
  603.  
  604. model.a.b.c === undefined
  605.  
  606. Thus, oldB.c may change oldB'c property 'c' to 5 but model.a.b is still "foo".
  607. */
  608. update(callback) {
  609. assert( a => a.is(this.isActive(), `Property is not active: ${ this.dotPath() }`) );
  610.  
  611. if (!this[_preventUpdates] && this.isUpdatable() && this.isActive()) {
  612. const next = this[_modelForUpdate]();
  613.  
  614. // invoke callback without bind context to reduce overhead
  615. const action = callback(next);
  616. const { nextState, replace } = action;
  617.  
  618. // mark property as having pending updates if the action callback returns a different
  619. // object/value or requests a replacement be created. An example where neither would be
  620. // true is a property touch() call because its shadow function signature changed.
  621. if (nextState !== next || replace) {
  622. this[_changed] = true;
  623. }
  624.  
  625. this.dispatchUpdate(action);
  626.  
  627. return true;
  628. }
  629.  
  630. return false;
  631. }
  632.  
  633. /**
  634. Marks this property as dead. Once marked obsolete a property may not accept further updates.
  635. A property is updated when the state changes but not a wholesale replacement or a descendents's
  636. value has changed and the update process has completed.
  637.  
  638. This method does not affect subproperties.
  639. */
  640. updated() {
  641. this[_dead] = true;
  642.  
  643. this.onUpdate();
  644. }
  645.  
  646. /**
  647. Changes the name this property will have after updates. This is used when moving properties
  648. around in the model, such as when splice is used on an array. The nextName() method
  649. will return the property name for after updates are applied.
  650.  
  651. Note: this method does not have any side effects beyond setting the _nextName instance
  652. variable. Subclasss will need to perform any book keeping associated with sub-properties.
  653. */
  654. updateName(name) {
  655. this[_nextName] = name;
  656. }
  657.  
  658. /**
  659. Gets if shadow property is allowing state updates.
  660.  
  661. @return {boolean} `false` if the property or its parent has been replaced, `true` otherwise.
  662. */
  663. updatesAllowed() {
  664. return !this[_preventUpdates] && !this[_replaced];
  665. }
  666.  
  667. /**
  668. Invokes a callback once all pending changes have occurred. The callback should have the form:
  669.  
  670. callback(property, implementation)
  671.  
  672. where the property and implementation arguments are the latest version if they still exist.
  673.  
  674. This method is safe to call on a dead property.
  675. */
  676. waitFor(callback) {
  677. if (this.isValid() && this.isActive()) {
  678. // short circuit if no changes pending
  679. callback(this.shadow());
  680. } else {
  681. this.store().waitFor( () => {
  682. const latest = this.latest();
  683.  
  684. callback(latest && latest.shadow(), latest);
  685. });
  686. }
  687. }
  688.  
  689. /**
  690. Invoked by the shadowing process to invoke appropriate {@link Property} life-cycle methods.
  691. The method name is a reflection that shadow state tree invocation chain for `willShadow()`
  692. occurs when the {@link Store} is going to shadow that state.
  693.  
  694. @param {boolean} parentWillUnshadow - `true` when parent property is unshadowing.
  695. */
  696. willShadow(parentWillUnshadow) {
  697. var willUnshadow = parentWillUnshadow || false;
  698.  
  699. if (parentWillUnshadow) {
  700. // all properties under an unshadowed proeprty also get unshadowed
  701. this[_property].onPropertyWillUnshadow();
  702. willUnshadow = true;
  703. } else if (this.isValid()) {
  704. // nothing else to do since this property and all subproperties must be fine
  705. return;
  706. } else if (this[_replaced] || this[_preventUpdates]) {
  707. this[_property].onPropertyWillUnshadow();
  708. willUnshadow = true;
  709. }
  710.  
  711. if (this.hasChildren()) {
  712. const children = this.children();
  713. var childImpl;
  714.  
  715. for (let i=0, len=children.length; i<len; i++) {
  716. let childImpl = children[i];
  717.  
  718. if (childImpl) {
  719. childImpl.willShadow(willUnshadow);
  720. }
  721. }
  722. }
  723. }
  724.  
  725.  
  726. //------------------------------------------------------------------------------------------------------
  727. // Methods with base implementations that subclasses may need to override - no need to call super
  728. //------------------------------------------------------------------------------------------------------
  729.  
  730. /**
  731. Creates a deep clone of the current property state.
  732. */
  733. copyState() {
  734. return cloneDeep(this.state());
  735. }
  736.  
  737. /**
  738. Invoked on shadow getter access to obtain the get value.
  739.  
  740. The default implementation returns the shadow.
  741.  
  742. @return the shadow or other f.lux representative value for the shadow proeprty.
  743. */
  744. definePropertyGetValue(state) {
  745. return this[_createShadow]();
  746. }
  747.  
  748. /**
  749. Invoked on shadow property assignment to perform the replacement f.lux action.
  750.  
  751. The default implementation is to assign the new value with no checking.
  752.  
  753. @param newValue - the new state value
  754. */
  755. definePropertySetValue(newValue) {
  756. this.assign(newValue);
  757. }
  758.  
  759. /**
  760. Gets if the property has an child properties (not whether child properties are supported).
  761. */
  762. hasChildren() {
  763. return this.childCount() != 0;
  764. }
  765.  
  766. /**
  767. Gets if this property type reprsents a primitive javascript type.
  768. */
  769. isPrimitive() {
  770. return false;
  771. }
  772.  
  773. /**
  774. Gets whether the property value supports calls to update().
  775. */
  776. isUpdatable() {
  777. return true;
  778. }
  779.  
  780. /**
  781. Property has just been reshadowed.
  782. */
  783. onReshadow(prev) { }
  784.  
  785. /**
  786. Hook for when this property is no longer represented in the system state.
  787. */
  788. onReplaced() { }
  789.  
  790. /**
  791. Hook for when this property is no longer represented in the system state due to a state
  792. update - not a replacement.
  793. */
  794. onUpdate() { }
  795.  
  796.  
  797. //------------------------------------------------------------------------------------------------------
  798. // Methods that ShadowImpl subclasses must be implemented by subclasses
  799. //------------------------------------------------------------------------------------------------------
  800.  
  801. /**
  802. Merges a new state into this property by using the 'state' parameter to set default values, ie it
  803. will not overwrite any existing values. Useful when model objects arrive from external sources,
  804. such as an asyncrhonous save or a websocket based update.
  805. */
  806. defaults(state) {
  807. throw new Error("ShadowImpl subclasses must implement defaults()");
  808. }
  809.  
  810. /**
  811. Merges a new state into this property. Useful when model objects arrive from external
  812. sources, such as an asyncrhonous save or a websocket based update.
  813. */
  814. merge(state) {
  815. throw new Error("ShadowImpl subclasses must implement merge()");
  816. }
  817.  
  818.  
  819. //------------------------------------------------------------------------------------------------------
  820. // Methods that ShadowImpl subclasses with children must implement
  821. //------------------------------------------------------------------------------------------------------
  822.  
  823. /**
  824. Invoked during defineProperty() to define children properties marked for automount
  825. */
  826. automountChildren() {
  827. // throw new Error("ShadowImpl subclasses with children must implement children()");
  828. }
  829.  
  830. /**
  831. Subclasses should implement this method in such a way as not to trigger a mapping.
  832. */
  833. childCount() {
  834. return 0;
  835. }
  836.  
  837. /**
  838. Gets the implementation objects managed by this property.
  839. */
  840. children() {
  841. throw new Error("ShadowImpl subclasses with children must implement children()");
  842. }
  843.  
  844. /**
  845. Gets a child implementation matching a property name or undefined if no such property exists.
  846. */
  847. getChild(name) {
  848. return undefined;
  849. }
  850.  
  851. /**
  852. Maps all child properties onto this property using Object.defineProperty().
  853.  
  854. @param {ShadowImpl} prev - the previous property shadow implementation instance.
  855. @param {boolean} inCtor - `true` if call occuring during shadowing process.
  856. */
  857. defineChildProperties(prev, inCtor) { }
  858.  
  859. /**
  860. Gets if defineChildProperties() has been invoked.
  861. */
  862. isMapped() {
  863. return true;
  864. }
  865.  
  866. /**
  867. Gets the keys/indices for this property.
  868.  
  869. Implementation note: Subclasses should implement this method in such a way as not to trigger a mapping.
  870. */
  871. keys() {
  872. throw new Error("ShadowImpl subclasses with children must implement keys()");
  873. }
  874.  
  875.  
  876. //------------------------------------------------------------------------------------------------------
  877. // Private functions - should not be called by code outside this file.
  878. //------------------------------------------------------------------------------------------------------
  879.  
  880. [_createShadow]() {
  881. if (!this[_shadow]) {
  882. let ShadowClass = this[_property].shadowClass();
  883.  
  884. this[_shadow] = new ShadowClass(this);
  885. extendProperty(this[_property], this, this[_shadow]);
  886. }
  887.  
  888. return this[_shadow];
  889. }
  890.  
  891. [_setupShadow](prev, inCtor) {
  892. if (!this.__getCalled__) {
  893. let state = this.state();
  894.  
  895. debug( d => d(`_setupShadow(): ${this.dotPath()}, time=${this[_time]}`) );
  896.  
  897. this.__getCalled__ = true;
  898. var shadow = this.__getResonse__ = this.definePropertyGetValue(state);
  899.  
  900. this.defineChildProperties(prev, inCtor);
  901.  
  902. // freeze shadows in dev mode to provide check not assigning to non-shadowed property
  903. // this can have performance penalties so skip in production mode
  904. if (process.env.NODE_ENV !== 'production') {
  905. !Object.isFrozen(shadow) && Object.freeze(shadow);
  906. }
  907. }
  908.  
  909. return this.__getResonse__;
  910. }
  911.  
  912. /**
  913. Maps the getter and setter (if appropriate) onto the parent property.
  914. */
  915. [_defineProperty](prev) {
  916. if (this.isRoot() || this.isIsolated()) { return }
  917.  
  918. // names with a leading '_' are not enumerable (way of hiding them)
  919. const enumerable = !(isString(this[_name]) && this[_name].startsWith('_'));
  920. const parentShadow = this.parent().shadow();
  921. const state = this.state();
  922. const set = this.readonly()
  923. ?undefined
  924. :newValue => {
  925. if (!this.isActive()) {
  926. if (process.env.NODE_ENV !== 'production') {
  927. console.error(`Attempting to set value on inactive property: ${this.dotPath()}`, newValue);
  928. }
  929.  
  930. return
  931. }
  932.  
  933. return this.definePropertySetValue(newValue);
  934. }
  935.  
  936. try {
  937. Object.defineProperty(parentShadow, this[_name], {
  938. enumerable: enumerable,
  939. get: () => {
  940. if (isSomething(state)) {
  941. return this[_setupShadow]();
  942. } else {
  943. return state;
  944. }
  945. },
  946. set: set
  947. });
  948. } catch(error) {
  949. console.warn(`_defineProperty() Error: name=${this[_name]}, parent=${this.parent().dotPath()}`, error.stack);
  950. debugger
  951. }
  952.  
  953. this.automountChildren(prev);
  954. }
  955.  
  956. /**
  957. Gets the next model state for the property. This value is used for performing property updates through
  958. the update() function.
  959.  
  960. Calls to update() trigger an update through the dispatcher upon which the new object will be mapped
  961. and the store informed of the change.
  962. */
  963. [_modelForUpdate]() {
  964. if (!this[_futureState]) {
  965. if (this.isRoot() || this.isIsolated()) {
  966. // next data will be a shallow copy of current model
  967. this[_futureState] = clone(this.state());
  968. } else {
  969. const parentNextState = this.parent()[_modelForUpdate]();
  970.  
  971. // Primitive parent models do not support adding properties
  972. if (isPrimitive(parentNextState)) {
  973. return undefined;
  974. }
  975.  
  976. // next data is a shallow copy of the parent's value of this property
  977. this[_futureState] = clone(parentNextState[this[_name]]);
  978.  
  979. // place a shallow clone in place of current value
  980. parentNextState[this.nextName()] = this[_futureState]
  981. }
  982. }
  983.  
  984. return this[_futureState];
  985. }
  986.  
  987. /**
  988. Schedules an UPDATE action with the store. On action execution, the new property will be generated
  989. and returned to the store.
  990. */
  991. [_scheduleUpdate]() {
  992. if (!this.isRoot()) {
  993. return this.root()[_scheduleUpdate]();
  994. }
  995.  
  996. if (!this[_scheduled] && !this.isValid() && !this[_dead]) {
  997. // flag never gets cleared
  998. this[_scheduled] = true;
  999.  
  1000. this.store().dispatchUpdate( time => reshadow(time, this[_futureState], this) );
  1001. }
  1002. }
  1003. }
  1004.  
  1005.