var Montage = require("core/core").Montage,
DataObjectDescriptor = require("data/model/data-object-descriptor").DataObjectDescriptor,
ObjectDescriptor = require("data/model/object-descriptor").ObjectDescriptor,
WeakMap = require("collections/weak-map"),
DataTrigger;
/**
* Intercepts all calls to get and set an object's property and triggers any
* Montage Data action warranted by these calls.
*
* DataTrigger is a JavaScript Objects subclass rather than a Montage subclass
* so data triggers can be as lightweight as possible: They need to be
* lightweight because many will be created (one for each relationship or
* lazily loaded property of each model class) and there's no benefit for them
* to be derived from the Montage prototype because they don't use any of the
* Montage class functionality.
*
* @private
* @class
* @extends Object
*/
DataTrigger = exports.DataTrigger = function () {};
exports.DataTrigger.prototype = Object.create({}, /** @lends DataTrigger.prototype */ {
/**
* The constructor function for all trigger instances.
*
* @type {function}
*/
constructor: {
configurable: true,
writable: true,
value: exports.DataTrigger
},
/**
* The service used by this trigger to perform Montage Data actions.
*
* Typically a number of triggers use the same service so this property
* is defined in a single DataTrigger instance for each service and all
* triggers that share that service are then derived from that instance.
* This avoids the need for each trigger to have a reference to the service
* it uses and saves memory. See
* [_getTriggerPrototype()]{@link DataTrigger._getTriggerPrototype} for
* the implementation of this behavior.
*
* @private
* @type {Service}
*/
_service: {
configurable: true,
writable: true,
value: undefined
},
/**
* The prototype of objects whose property is managed by this trigger.
*
* @private
* @type {Object}
*/
_objectPrototype: {
configurable: true,
writable: true,
value: undefined
},
/**
* The name of the property managed by this trigger.
*
* @private
* @type {string}
*/
_propertyName: {
configurable: true,
writable: true,
value: undefined
},
/**
* The name of the private property corresponding to the public property
* managed by this trigger.
*
* The private property name is the
* [_propertyName]{@link DataTrigger#_propertyName} value prefixed with an
* underscore. To minimize the time and memory used by a trigger's call
* intercepts this private property name is generated lazilly the first time
* it is needed and then cached.
*
* @private
* @type {string}
*/
_privatePropertyName: {
configurable: true,
get: function () {
if (!this.__privatePropertyName && this._propertyName) {
this.__privatePropertyName = "_" + this._propertyName;
}
return this.__privatePropertyName;
}
},
/**
* Whether this trigger is for a global property or not.
*
* When a trigger is global and the value of the trigger's property is
* obtained or set for one object managed by the trigger then that
* property's value is assumed to have also been obtained or set for all
* objects managed by the trigger.
*
* Setting this value clears the
* [_valueStatus]{@link DataTrigger#_valueStatus} and should only be done
* before a trigger is used.
*
* @private
* @type {string}
*/
_isGlobal: {
configurable: true,
get: function () {
// To save memory a separate _isGlobal boolean is not maintained and
// the _isGlobal value is derived from the _valueStatus type.
return !(this._valueStatus instanceof WeakMap);
},
set: function (global) {
global = global ? true : false;
if (global !== this._isGlobal) {
this._valueStatus = global ? undefined : new WeakMap();
}
}
},
/**
* For global triggers, holds the status of this trigger's property value.
* For other triggers, holds a map from objects whose property is managed
* by this trigger to the status of that property's value for each of those
* objects.
*
* See [_getValueStatus()]{@link DataTrigger#_getValueStatus} for the
* possible status values.
*
* @private
* @type {Object|WeakMap}
*/
_valueStatus: {
configurable: true,
writable: true,
value: undefined
},
/**
* Gets the status of a property value managed by this trigger:
*
* - The status will be `undefined` when the value has not yet been
* requested or set.
*
* - The status will be `null` when the value was requested and has already
* been obtained or when it has been set.
*
* - When the value has been requested but is still in the process of being
* obtained, the status will be an object with a "promise" property set to
* a promise that will be resolved when the value is obtained and a
* "resolve" property set to a function that will resolve that promise.
*
* @private
* @method
* @argument {Object} object
* @returns {Object}
*/
_getValueStatus: {
configurable: true,
value: function (object) {
return this._isGlobal ? this._valueStatus : this._valueStatus.get(object);
}
},
/**
* Sets the status of a property value managed by this trigger.
*
* See [_getValueStatus()]{@link DataTrigger#_getValueStatus} for the
* possible status values.
*
* @private
* @method
* @argument {Object} object
* @argument {Object} status
*/
_setValueStatus: {
configurable: true,
value: function (object, status) {
if (this._isGlobal) {
this._valueStatus = status;
} else if (status !== undefined) {
this._valueStatus.set(object, status);
} else {
this._valueStatus.delete(object);
}
}
},
/**
* @method
* @argument {Object} object
* @returns {Object}
*
* #Performance #ToDo: Looks like the same walk-up logic
* is going to be done many times for individual instances,
* we should improve that.
*/
_getValue: {
configurable: true,
writable: true,
value: function (object) {
var prototype, descriptor, getter;
// Start an asynchronous fetch of the property's value if necessary.
this.getObjectProperty(object);
// Search the prototype chain for a getter for this property,
// starting just after the prototype that called this method.
prototype = Object.getPrototypeOf(this._objectPrototype);
while (prototype) {
descriptor = Object.getOwnPropertyDescriptor(prototype, this._propertyName);
getter = descriptor && descriptor.get;
prototype = !getter && Object.getPrototypeOf(prototype);
}
// Return the property's current value.
return getter ? getter.call(object) : object[this._privatePropertyName];
}
},
/**
* Note that if a trigger's property value is set after that values is
* requested but before it is obtained from the trigger's service the
* property's value will only temporarily be set to the specified value:
* When the service finishes obtaining the value the property's value will
* be reset to that obtained value.
*
* @method
* @argument {Object} object
* @argument {} value
*/
_setValue: {
configurable: true,
writable: true,
value: function (object, value) {
var status, prototype, descriptor, getter, setter, writable;
// Get the value's current status and update that status to indicate
// the value has been obtained. This way if the setter called below
// requests the property's value it will get the value the property
// had before it was set, and it will get that value immediately.
status = this._getValueStatus(object);
this._setValueStatus(object, null);
// Search the prototype chain for a setter for this trigger's
// property, starting just after the trigger prototype that caused
// this method to be called.
prototype = Object.getPrototypeOf(this._objectPrototype);
while (prototype) {
descriptor = Object.getOwnPropertyDescriptor(prototype, this._propertyName);
getter = descriptor && descriptor.get;
setter = getter && descriptor.set;
writable = !descriptor || setter || descriptor.writable;
prototype = writable && !setter && Object.getPrototypeOf(prototype);
}
// Set this trigger's property to the desired value, but only if
// that property is writable.
if (setter) {
setter.call(object, value);
} else if (writable) {
object[this._privatePropertyName] = value;
}
// Resolve any pending promise for this trigger's property value.
if (status) {
status.resolve(null);
}
}
},
/**
* @todo Rename and document API and implementation.
*
* @method
*/
decacheObjectProperty: {
value: function (object) {
this._setValueStatus(object, undefined);
}
},
/**
* Request a fetch of the value of this trigger's property for the
* specified object but only if that data isn't already in the process
* of being obtained and only if it wasn't previously obtained or
* set. To unconditionally request a fetch of this property data use
* [updateObjectProperty()]{@link DataTrigger#updateObjectProperty}.
*
* @method
* @argument {Object} object
* @returns {external:Promise}
*/
getObjectProperty: {
value: function (object) {
var status = this._getValueStatus(object);
return status ? status.promise :
status === null ? this._service.nullPromise :
this.updateObjectProperty(object);
}
},
/**
* If the value of this trigger's property for the specified object isn't in
* the process of being obtained, request the most up to date value of that
* data from this trigger's service.
*
* @method
* @argument {Object} object
* @returns {external:Promise}
*/
updateObjectProperty: {
value: function (object) {
var self = this,
status = this._getValueStatus(object) || {};
if (!status.promise) {
this._setValueStatus(object, status);
status.promise = new Promise(function (resolve, reject) {
status.resolve = resolve;
status.reject = reject;
self._fetchObjectProperty(object);
});
}
// Return the existing or just created promise for this data.
return status.promise;
}
},
/**
* @private
* @method
* @argument {Object} object
* @returns {external:Promise}
*/
_fetchObjectProperty: {
value: function (object) {
var self = this;
this._service.fetchObjectProperty(object, this._propertyName).then(function () {
return self._fulfillObjectPropertyFetch(object);
}).catch(function (error) {
console.error(error);
return self._fulfillObjectPropertyFetch(object, error);
});
}
},
_fulfillObjectPropertyFetch: {
value: function (object, error) {
var status = this._getValueStatus(object);
this._setValueStatus(object, null);
if (status && !error) {
status.resolve(null);
} else if (status && error) {
console.error(error);
status.reject(error);
}
return null;
}
}
});
Object.defineProperties(exports.DataTrigger, /** @lends DataTrigger */ {
/**
* @method
* @argument {DataService} service
* @argument {Object} prototype
* @argument {Set} property names to exclude from triggers.
* @returns {Object.<string, DataTrigger>}
*/
addTriggers: {
value: function (service, type, prototype, requisitePropertyNames) {
// This function was split into two to provide backwards compatibility
// to existing Montage data projects. Future montage data projects
// should base their object descriptors on Montage's version of object
// descriptor.
var isMontageDataType = type instanceof DataObjectDescriptor || type instanceof ObjectDescriptor;
return isMontageDataType ? this._addTriggersForMontageDataType(service, type, prototype, name) :
this._addTriggers(service, type, prototype, requisitePropertyNames);
}
},
_addTriggersForMontageDataType: {
value: function (service, type, prototype) {
var triggers = {},
names = Object.keys(type.propertyDescriptors),
trigger, name, i;
for (i = 0; (name = names[i]); ++i) {
trigger = this.addTrigger(service, type, prototype, name);
if (trigger) {
triggers[name] = trigger;
}
}
return triggers;
}
},
_addTriggers: {
value: function (service, objectDescriptor, prototype, requisitePropertyNames) {
var triggers = {}, propertyDescriptor, trigger, name, i, n;
for (i = 0, n = objectDescriptor.propertyDescriptors.length; i < n; i += 1) {
propertyDescriptor = objectDescriptor.propertyDescriptors[i];
if (!requisitePropertyNames.has(propertyDescriptor.name)) {
name = propertyDescriptor.name;
trigger = this.addTrigger(service, objectDescriptor, prototype, name);
if (trigger) {
triggers[name] = trigger;
}
}
}
return triggers;
}
},
/**
* @method
* @argument {DataService} service
* @argument {Object} prototype
* @argument {string} name
* @returns {?DataTrigger}
*/
addTrigger: {
value: function (service, type, prototype, name) {
// This function was split into two to provide backwards compatibility
// to existing Montage data projects. Future montage data projects
// should base their object descriptors on Montage's version of object
// descriptor.
var isMontageDataType = type instanceof DataObjectDescriptor || type instanceof ObjectDescriptor;
return isMontageDataType ? this._addTriggerForMontageDataType(service, type, prototype, name) :
this._addTrigger(service, type, prototype, name);
}
},
_addTriggerForMontageDataType: {
value: function (service, type, prototype, name) {
var descriptor = type.propertyDescriptors[name],
trigger;
if (descriptor && descriptor.isRelationship) {
trigger = Object.create(this._getTriggerPrototype(service));
trigger._objectPrototype = prototype;
trigger._propertyName = name;
trigger._isGlobal = descriptor.isGlobal;
Montage.defineProperty(prototype, name, {
get: function () {
return trigger._getValue(this);
},
set: function (value) {
trigger._setValue(this, value);
}
});
}
return trigger;
}
},
/**
* #Performance #ToDO: Here we're creating a trigger instance right away
* when we could do it only when the defineProperty get/set are called.
* This means this method wouldn't return the trigger which is added
* by caller in service._getTriggersForObject. Instead, when created in the
* get/set, we would added it to the service's ._getTriggersForObject.
* First draft is bellow, working with bugs, need baking
*
* @private
* @method
* @argument {DataService} service
* @argument {ObjectDescriptor} objectDescriptor
* @argument {Object} prototype
* @argument {String} name
* @returns {DataTrigger}
*/
_createTrigger: {
value: function(service, objectDescriptor, prototype, name, propertyDescriptor) {
var trigger = Object.create(this._getTriggerPrototype(service)),
serviceTriggers = service._dataObjectTriggers.get(objectDescriptor);
trigger._objectPrototype = prototype;
trigger._propertyName = name;
trigger._isGlobal = propertyDescriptor.isGlobal;
if(!serviceTriggers) {
serviceTriggers = {};
service._dataObjectTriggers.set(objectDescriptor,serviceTriggers);
}
serviceTriggers[name] = trigger;
return trigger;
}
},
_addTrigger: {
value: function (service, objectDescriptor, prototype, name) {
var descriptor = objectDescriptor.propertyDescriptorForName(name),
trigger;
if (descriptor) {
trigger = Object.create(this._getTriggerPrototype(service));
trigger._objectPrototype = prototype;
trigger._propertyName = name;
trigger._isGlobal = descriptor.isGlobal;
if (descriptor.definition) {
Montage.defineProperty(prototype, name, {
get: function () {
if (!this.getBinding(name)) {
this.defineBinding(name, {"<-": descriptor.definition});
}
return trigger._getValue(this);
// return (trigger||(trigger = DataTrigger._createTrigger(service, objectDescriptor, prototype, name,descriptor)))._getValue(this);
},
set: function (value) {
trigger._setValue(this, value);
// (trigger||(trigger = DataTrigger._createTrigger(service, objectDescriptor, prototype, name,descriptor)))._setValue(this, value);
}
});
} else {
Montage.defineProperty(prototype, name, {
get: function () {
return trigger._getValue(this);
// return (trigger||(trigger = DataTrigger._createTrigger(service, objectDescriptor, prototype, name,descriptor)))._getValue(this);
},
set: function (value) {
trigger._setValue(this, value);
// (trigger||(trigger = DataTrigger._createTrigger(service, objectDescriptor, prototype, name,descriptor)))._setValue(this, value);
}
});
}
trigger = Object.create(this._getTriggerPrototype(service));
trigger._objectPrototype = prototype;
trigger._propertyName = name;
trigger._isGlobal = descriptor.isGlobal;
}
return trigger;
}
},
/**
* To avoid having each trigger hold a reference to the service it uses, all
* triggers that use a service are derived from a prototype that contains
* this references. See [_service]{@link DataTrigger#_service} for details.
*
* @private
* @method
* @argument {DataService} service
* @returns {DataTrigger}
*/
_getTriggerPrototype: {
value: function (service) {
var trigger = this._triggerPrototypes && this._triggerPrototypes.get(service);
if (!trigger) {
trigger = new this();
trigger._service = service;
this._triggerPrototypes = this._triggerPrototypes || new WeakMap();
this._triggerPrototypes.set(service, trigger);
}
return trigger;
}
},
/**
* @method
* @argument {Object.<string, DataTrigger>} triggers
* @argument {Object} prototype
*/
removeTriggers: {
value: function (triggers, prototype) {
var triggerNames = Object.keys(triggers),
name, i;
for (i = 0; (name = triggerNames[i]); ++i) {
this.removeTrigger(triggers[name], prototype, name);
}
}
},
/**
* @method
* @argument {DataTrigger} trigger
* @argument {Object} prototype
*/
removeTrigger: {
value: function (trigger, prototype) {
if (trigger) {
delete prototype[trigger.name];
}
}
}
});