Home Reference Source Repository

src/IndexedShadowImpl.js

import flatten from "lodash.flatten";
import range from "lodash.range";

import ShadowImpl from "./ShadowImpl";
import modelizeArray from "./modelizeArray";
import reshadow from "./reshadow";


const _automounted = Symbol('automounted');
const _impls = Symbol('impls');
const _mapped = Symbol('mapped');
const _length = Symbol('length');
const _nextMapping = Symbol('nextMapping');
const _shadow = Symbol('shadow');

export default class IndexedShadowImpl extends ShadowImpl {
	constructor(time, property, name, state, parent, shader, prev) {
		super(time, property, name, state, parent, shader, prev);

		this[_mapped] = false;
		this[_impls] = [];

		// setup the length property
		this[_length] = state.length;

		// go ahead and map properties so can reuse valid properties without creating a closure reference.
		// Reusing properties allows for React components to do '===' to see if a property has changed.
		// if (prev) {
		// 	if (prev && prev[_mapped]) {
		// 		this.defineChildProperties(prev, true);
		// 	} else if (prev && Object.keys(prev[_impls]).length) {
		// 		this.automountChildren(prev);
		// 	}

		// 	// Kill invalid() nodes that are still active
		// 	prev._killInvalidButActiveChildren();
		// }
	}

	/*
		Map properties so can reuse valid properties. Reusing properties allows for React components
		to do '===' to see if a property has changed.
	*/
	onReshadow(prev) {
		// Kill invalid() nodes that are still active
		prev._killInvalidButActiveChildren();
	}

	isMapped() {
		return this[_mapped];
	}

	addChildren(start, num) {
		this[_nextMapping] = this[_nextMapping] || [...this[_impls]];

		const mapping = this[_nextMapping];
		const blanks = [];
		var count = num;

		while(count--) blanks.push(null);

		// rename children after newly added blanks
		for (let i=start, len=mapping.length; i<len; i++) {
			let idx = i + num;
			let child = mapping[i];

			if (child) {
				child.updateName(idx);
			}
		}

		mapping.splice(start, 0, ...blanks)
	}

	/*
		Its possible for children to move when array items are inserted or removed in update actions.
	*/
	childMapping() {
		return this[_nextMapping] ?this[_nextMapping] :this[_impls];
	}

	moveChildren(start, deleteCount, addCount) {
		this.removeChildren(start, deleteCount);
		this.addChildren(start, addCount);
	}

	removeChildren(start, num) {
		this[_nextMapping] = this[_nextMapping] || [...this[_impls]];

		const mapping = this[_nextMapping];

		// prevent changes in removed children
		for (let i=0; i<num; i++) {
			let child = mapping[start+i];

			if (child) {
				child.blockFurtherUpdates(true);
			}
		}

		// remove children from mapping
		mapping.splice(start, num);

		// rename remaining children
		for (let i=start, len=mapping.length; i<len; i++) {
			let child = mapping[i];

			if (child) {
				child.updateName(i);
			}
		}
	}

	toJSON() {
		return {
			...super.toJSON(),
			type: 'ArrayImpl',
			length: this.length,
			mapped: this[_mapped],
		};
	}


	//------------------------------------------------------------------------------------------------------
	//	Methods for access and manipulate subproperties - based on Array methods
	//------------------------------------------------------------------------------------------------------

	get length() {
		return this[_length];
	}

	/*
		Removes all subproperties.
	*/
	clear() {
		this.assign([], "clear()");
	}

	childAt(idx) {
		return this[_impls][i];
	}

	// This method does not change this object's state.
	concat(...values) {
		return flatten(this.values(), values);
	}

	pop() {
		var result;

		this.update( state => {
				const lastIdx = state.length - 1;

				state = [...state];
				result = state.pop();

				if (lastIdx != -1) {
					this.removeChildren(lastIdx, 1);
				}

				return { name: "pop()", nextState: state };
			});

		// value is no longer being managed so ok to return without cloning
		return result;
	}

	push(...values) {
		var result;

		modelizeArray(values);

		this.update( state => {
				state = [...state];
				result = state.push(...values);

				// no need to update children mapping only affecting tail

				return { name: "push()", nextState: state };
			});

		return result;
	}

	remove(idx) {
		var result;

		this.update( state => {
			state = [...state];
			result = state.splice(idx, 1);

			this.removeChildren(idx, 1);

			return { name: `remove(${idx})`, nextState: state }
		});

		// value is no longer being managed so ok to return without cloning
		return result[0];
	}

	shift() {
		var result;

		this.update( state => {
				state = [...state];
				result = state.shift();

				this.removeChildren(0, 1);

				return { name: "shift()", nextState: state };
			});

		// value is no longer being managed so ok to return without cloning
		return result;
	}

	splice(start, deleteCount, ...newItems) {
		var result;

		modelizeArray(newItems);

		this.update( state => {
				state = [...state];

				result = state.splice(start, deleteCount, ...newItems);
				this.moveChildren(start, deleteCount, newItems.length);

				return { name: `splice(${start}, ${deleteCount}, ...)`, nextState: state };
			});

		// values is no longer being managed so ok to return without cloning
		return result;
	}

	unshift(...values) {
		var result;

		modelizeArray(values);

		this.update( state => {
				state = [...state];
				result = state.unshift(...values);

				this.addChildren(0, values.length);

				return { name: "unshift()", nextState: state };
			});

		// values are no longer being managed so ok to return without cloning
		return result;
	}

	values() {
		return [ ...this[_impls] ];
	}


	//------------------------------------------------------------------------------------------------------
	//	Methods that must be implemented by subclasses
	//------------------------------------------------------------------------------------------------------

	defaults(newItems) {
		if (!Array.isArray(newItems)) {
			// nothing to do
			return;
		}

		const impls = this.childMapping();
		var child, value;

		for (let i=0, len=newItems.length; i<len; i++) {
			child = impls[i];
			value = newItems[i];

			if (child) {
				child.defaults(value);
			} else {
				this.update( state => {
						state = [...state];

						state.splice(i, 0, value);
						this.addChildren(i, newItems.length);

						return { nextState: state };
					});
			}
		}
	}

	merge(newItems) {
		// original algorithm (commented out below) is adding duplicates where in most
		// cases what is really wanted is one of two things:
		//    1) add if no duplicate (exact deep compare)
		//    2) merge if object's 'ID' is matched and add otherwise
		//
		// So probably need multiple algorithms driven by a property setting. Would need to worry
		// about ordering, inserts, deletions,...
		//
		// In the meantime just going to do a full replace
		return this.assign(newItems);

		// Original algorithm (broken)
		// if (!Array.isArray(newItems)) {
		// 	return this.assign(newItems);
		// }

		// const impls = this.childMapping();
		// var child, value;

		// for (let i=0, len=newItems.length; i<len; i++) {
		// 	child = impls[i];
		// 	value = newItems[i];

		// 	if (child) {
		// 		child.merge(value);
		// 	} else {
		// 		this.update( state => {
		// 				state = [...state];

		// 				state.splice(i, 0, value);
		// 				this.addChildren(i, newItems.length);

		// 				return { nextState: state };
		// 			});
		// 	}
		// }
	}


	//------------------------------------------------------------------------------------------------------
	//	Methods that subclasses with children must implement
	//------------------------------------------------------------------------------------------------------

	automountChildren(prev) {
		if (this[_mapped] || this[_automounted]) { return }

		this[_automounted] = true;

		const state = this.state();
		const shader = this.shader(state);

		for (let i=0, len=state.length; i<len; i++) {
			if (shader.isAutomount(i)) {
				this.defineChildProperty(i, shader, state, prev, true);
			}
		}
	}

	/*
		Subclasses should implement this method in such a way as not to trigger a mapping.
	*/
	childCount() {
		return this[_length];
	}

	/*
		Gets the implementation objects managed by this property.
	*/
	children() {
		return [ ...this[_impls] ];
	}

	getChild(idx) {
		return this.childAt(idx);
	}

	/*
		Gets the keys/indices for this property.

		Implementation note: Subclasses should implement this method in such a way as not to trigger a mapping.
	*/
	keys() {
		if (!this._keys) {
			this._keys = range(this.length);
		}

		return this._keys;
	}

	/*
		Maps all child properties onto this property using Object.defineProperty().
	*/
	defineChildProperties(prev, inCtor) {
		if (this[_mapped]) { return }

		this[_mapped] = true;

		const shader = this.shader(state);
		const state = this.state();
		var elementShader;

		for (let i=0, len=state.length; i<len; i++) {
			elementShader = shader.shaderFor(i, state);

			this.defineChildProperty(i, elementShader, state, prev, inCtor);
		}

		if (state) {
			shader.shadowUndefinedProperties(state, this, (name, shader) => {
					this.defineChildProperty(name, shader, state, prev, inCtor);
				});
		}
	}

	defineChildProperty(idx, elementShader, state, prev, inCtor=false) {
		// ensure not already defined
		if (this[_impls][idx]) { return }

		const prevMapping = prev && prev.childMapping();
		const prevChild = prevMapping && prevMapping[idx];
		var child;

		if (prevChild && !prevChild.replaced()) {
			prevChild.switchName();

			child = reshadow(this.time(), state, prevChild, this);
		} else {
			child = elementShader.shadowProperty(this.time(), idx, state, this, this.store());
		}

		if (child) {
			this[_impls][idx] = child;

			if (!prevChild && !inCtor) {
				child.didShadow(this.time());
			}
		}
	}

	_killInvalidButActiveChildren() {
		// kill any unvisited children
			const children = this.children();
			var child;

			for (let i=0, len=children.length; i<len; i++) {
				child = children[i];

				if (!child.isValid() && child.isActive()) {
					child.obsoleteTree();
				}
			}
	}
}