hyperflow
Version:
A javascript state flow and mutation management toolkit & library for developing universal app.
458 lines (418 loc) • 15.6 kB
JavaScript
/**
* Copyright 2015-present Tuan Le.
*
* Licensed under the MIT License.
* You may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://opensource.org/licenses/mit-license.html
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*------------------------------------------------------------------------
*
* @module ObservableDescriptor
* @description - An obserevable descriptor that allows an object property to emit
* change events.
*
* @author Tuan Le (tuan.t.lei@gmail.com)
*
* @flow
*/
; // eslint-disable-line
import CommonElement from '../common-element';
/* load RxJs dependency */
import {
Subscriber as RxSubscriber,
Observable as RxObservable
} from 'rxjs';
const Hf = CommonElement();
/**
* @description - A emitter descriptor prototypes.
*
* ObservableDescriptorPrototype
*/
const ObservableDescriptorPrototype = Object.create({}).prototype = {
/* ----- Prototype Definitions --------- */
/**
* @description - Check if subscriber is in subscription list.
*
* @method hasSubscriber
* @param {string} handerKey
* @return {boolean}
*/
hasSubscriber (handerKey) {
const observable = this;
const eventId = `${observable._id}.${handerKey}`;
return observable._description.subscriber.hasOwnProperty(eventId);
},
/**
* @description - Check if condition is in condition list.
*
* @method hasCondition
* @param {string} conditionKey
* @return {boolean}
*/
hasCondition (conditionKey) {
const observable = this;
return observable._description.condition.hasOwnProperty(conditionKey);
},
/**
* @description - Get obvervable data stream.
*
* @method getStream
* @return {object}
*/
getStream () {
const observable = this;
if (Hf.DEVELOPMENT) {
if (!observable._description.assigned) {
Hf.log(`error`, `ObservableDescriptor.getStream - Descriptor as no assignment.`);
}
}
return observable._stream;
},
/**
* @description - Add a new subscriber to subscriber list.
*
* @method addSubscriber
* @param {function} handler - Subscriber callback function.
* @param {string} handerKey
* @return void
*/
addSubscriber (handler, handerKey) {
if (Hf.DEVELOPMENT) {
if (!Hf.isFunction(handler)) {
Hf.log(`error`, `ObservableDescriptor.addSubscriber - Input subscription handler function is invalid.`);
} else if (!Hf.isString(handerKey)) {
Hf.log(`error`, `ObservableDescriptor.addSubscriber - Input subscription handler key is invalid.`);
}
}
const observable = this;
const eventId = `${observable._id}.${handerKey}`;
if (!observable.hasSubscriber(eventId)) {
observable._description.subscriber[eventId] = handler;
}
},
/**
* @description - Remove a subscriber from subscriber list.
*
* @method removeSubscriber
* @param {string} handerKey
* @return void
*/
removeSubscriber (handerKey) {
if (Hf.DEVELOPMENT) {
if (!Hf.isString(handerKey)) {
Hf.log(`error`, `ObservableDescriptor.removeSubscriber - Input subscriber handler key is invalid.`);
}
}
const observable = this;
const eventId = `${observable._id}.${handerKey}`;
if (observable.hasSubscriber(eventId)) {
delete observable._description.subscriber[eventId];
// observable._description.subscriber[eventId] = undefined;
}
},
/**
* @description - Add a new condition to condition list.
*
* @method addCondition
* @param {function} trigger - Trigger callback function.
* @param {string} conditionKey
* @return void
*/
addCondition (trigger, conditionKey) {
if (Hf.DEVELOPMENT) {
if (!Hf.isFunction(trigger)) {
Hf.log(`error`, `ObservableDescriptor.addCondition - Input condition trigger function is invalid.`);
} else if (!Hf.isString(conditionKey)) {
Hf.log(`error`, `ObservableDescriptor.addCondition - Input condition key is invalid.`);
}
}
const observable = this;
const eventId = `${observable._id}.${conditionKey}`;
if (!observable.hasCondition(eventId)) {
observable._description.condition[eventId] = trigger;
}
},
/**
* @description - Remove a condition from condition list.
*
* @method removeCondition
* @param {string} conditionKey
* @return void
*/
removeCondition (conditionKey) {
if (Hf.DEVELOPMENT) {
if (!Hf.isString(conditionKey)) {
Hf.log(`error`, `ObservableDescriptor.removeCondition - Input condition key is invalid.`);
}
}
const observable = this;
const eventId = `${observable._id}.${conditionKey}`;
if (observable.hasCondition(eventId)) {
delete observable._description.condition[eventId];
// observable._description.condition[eventId] = undefined;
}
},
/**
* @description - Assign an observable description.
*
* @method assign
* @param {object} descPreset - A descriptor preset object.
* @return {object}
*/
assign (descPreset) {
if (Hf.DEVELOPMENT) {
if (!Hf.isSchema({
key: `string|number`
}).of(descPreset)) {
Hf.log(`error`, `ObservableDescriptor.assign - Input descriptor preset object is invalid.`);
}
}
const observable = this;
const {
key,
condition,
subscriber
} = Hf.fallback({
condition: {},
subscriber: {}
}, () => {
Hf.log(`warn1`, `ObservableDescriptor.assign - Input descriptor preset is invalid.`);
}).of(descPreset);
if (observable._description.assigned) {
observable.unassign();
}
if (!Hf.isEmpty(condition)) {
Hf.forEach(condition, (trigger, conditionKey) => {
observable.addCondition(trigger, conditionKey);
});
}
if (!Hf.isEmpty(subscriber)) {
Hf.forEach(subscriber, (handler, handlerKey) => {
observable.addSubscriber(handler, handlerKey);
});
}
observable._observer = RxSubscriber.create(
/**
* @description - On subscription to next incoming payload...
*
* @method next
* @param {object} payload
* @return void
*/
function next (payload) {
if (Hf.DEVELOPMENT) {
if (!Hf.isSchema({
eventId: `string`
}).of(payload)) {
Hf.log(`error`, `ObservableDescriptor.next - Payload event Id is invalid.`);
}
}
const {
eventId,
value
} = payload;
if (observable._description.subscriber.hasOwnProperty(eventId)) {
const handler = observable._description.subscriber[eventId];
handler(value);
}
},
/**
* @description - On subscription to error...
*
* @method error
* @param {string} error
* @return void
*/
function error (errorMessage) {
Hf.log(`error`, `ObservableDescriptor.error - Subscription error. ${errorMessage}`);
},
/**
* @description - On subscription to completion...
*
* @method complete
* @return void
*/
function complete () {
Hf.log(`info0`, `Subscription completed.`);
}
);
return {
/**
* @description - The target object to get this observable property.
*
* @method assign.to
* @param {object|array} target - Target object.
* @return void
*/
to (target) {
if (Hf.DEVELOPMENT) {
if (!(Hf.isObject(target) || Hf.isArray(target))) {
Hf.log(`error`, `ObservableDescriptor.assign.to - Input target is invalid.`);
} else if (!target.hasOwnProperty(key)) {
Hf.log(`error`, `ObservableDescriptor.assign.to - Property key:${key} is not defined.`);
}
}
observable._description.assigned = true;
observable._description.key = key;
observable._description.proxy = target;
observable._description.orgDesc = Object.getOwnPropertyDescriptor(target, key);
observable._stream = RxObservable.create((streamEmitter) => {
/* create the condition property for the assigned object */
Object.defineProperty(observable._description.proxy, key, {
get () {
if (observable._description.orgDesc.hasOwnProperty(`get`)) {
return observable._description.orgDesc.get();
}
return observable._description.orgDesc.value;
},
set (value) {
if (observable._description.orgDesc.hasOwnProperty(`set`)) {
observable._description.orgDesc.set(value);
} else {
observable._description.orgDesc.value = value;
}
try {
if (observable._description.orgDesc.hasOwnProperty(`get`)) {
streamEmitter.next(observable._description.orgDesc.get());
} else {
streamEmitter.next(value);
}
} catch (error) {
streamEmitter.error(error);
}
},
configurable: true,
enumerable: true
});
}).map((value) => {
const defaultPayload = {
eventId: `${observable._id}`,
value
};
if (!Hf.isEmpty(observable._description.condition)) {
return Object.keys(observable._description.condition).reduce((payload, eventId) => {
let context = {};
const trigger = observable._description.condition[eventId];
context[key] = value;
if (trigger.call(context)) {
payload = {
eventId,
value
};
}
return payload;
}, defaultPayload);
}
return defaultPayload;
});
observable._subscription = observable._stream.subscribe(observable._observer);
}
};
},
/**
* @description - Unassign an observable description.
*
* @method unassign
* @return void
*/
unassign () {
const observable = this;
if (observable._description.assigned) {
const key = observable._description.key;
observable._observer.complete();
observable._subscription.unsubscribe();
/* delete current property */
delete observable._description.proxy[key];
// observable._description.proxy[key] = undefined;
/* restore original property with it descriptor */
if (observable._description.orgDesc.hasOwnProperty(`get`) || observable._description.orgDesc.hasOwnProperty(`set`)) {
Object.defineProperty(observable._description.proxy, key, {
get: observable._description.orgDesc.get,
set: observable._description.orgDesc.set,
configurable: observable._description.orgDesc.configurable,
enumerable: observable._description.orgDesc.enumerable
});
} else {
observable._description.proxy[key] = observable._description.orgDesc.value;
}
observable._description.assigned = false;
observable._description.orgDesc = undefined;
observable._description.proxy = undefined;
observable._description.key = undefined;
observable._stream = undefined;
observable._observer = undefined;
observable._subscription = undefined;
Hf.clear(observable._description.condition);
Hf.clear(observable._description.subscriber);
}
}
};
/**
* @description - An observable descriptor module.
*
* @module ObservableDescriptor
* @param {string} id - Descriptor Id.
* @return {object}
*/
export default function ObservableDescriptor (id) {
if (Hf.DEVELOPMENT) {
if (!Hf.isString(id)) {
Hf.log(`error`, `ObservableDescriptor - Input descriptor Id is invalid.`);
}
}
const descriptor = Object.create(ObservableDescriptorPrototype, {
_id: {
value: id,
writable: false,
configurable: false,
enumerable: false
},
_stream: {
value: undefined,
writable: true,
configurable: true,
enumerable: false
},
_observer: {
value: undefined,
writable: true,
configurable: true,
enumerable: false
},
_subscription: {
value: undefined,
writable: true,
configurable: true,
enumerable: false
},
_description: {
value: {
assigned: false,
key: undefined,
orgDesc: undefined,
// TODO: Used es6 Proxy.
proxy: undefined,
subscriber: {},
condition: {}
},
writable: true,
configurable: true,
enumerable: false
}
});
if (Hf.DEVELOPMENT) {
if (!Hf.isObject(descriptor)) {
Hf.log(`error`, `ObservableDescriptor - Unable to create a computable descriptor instance.`);
}
}
/* reveal only the public properties and functions */
return Hf.compose(Hf.reveal, Object.freeze)(descriptor);
}