lycabinet
Version:
A simple small JSON Object storage helper with good performance.
199 lines (182 loc) • 6.46 kB
text/typescript
/**
* Observer Plugin for Lycabinet
* Adding an mini observer for storage variable listening.
*/
import {
curveGet, curveSet,
removeArrayItem, deepSupplement, is_PlainObject,
DEBUG,
is_Function,
LogToken
} from "../utils/util";
// change methods.
let onSetted: Function| null = null;
/**
* Targets
* @param Lycabinet
*/
export function addObserver(Lycabinet){
const Proto = Lycabinet.prototype;
Proto.initObserver = function(options: any = {}){
// Override the default options
Object.assign(options, {
deepMerge: true,
});
// configurate options
deepSupplement(options, {
lazy: true,
initWatch: true, // whether transform the origin property in Observer.
deepWatch: true, // whether consistently watch the Object type value setted in initial data.
shallowWatch: false, // whether just watch the surface of the Object.
});
// init proxy Interceptor.
onSetted = ()=>{
// this._trigger("setItem");
options.lazy? this.lazySave(): this.save();
};
if(!options.initWatch) return false;
this.__storage = deepConvert(this.__storage, options.deepWatch, options.shallowWatch);
this.setStore(this.__storage);
};
// Convert the path target to be reactive. Check redundant Prevent by default.
Lycabinet.$set = function(target: Object, pathList: string[], deep=false, shallow =true){
// CurveSet the target value.
return curveSet(target, pathList, (innerTarget, kname)=>{
// If target is not existed, set to plain Object; Reset the value if it has been converted.
if(!is_PlainObject(innerTarget[kname])){
innerTarget[kname] = {};
}
if(DEBUG && innerTarget[kname]["$addListener"]){
console.warn(`${LogToken}The target have been converted before!`, innerTarget[kname]);
}
innerTarget[kname] = convert(innerTarget[kname], deep, shallow);
});
};
Lycabinet.$get = function(target: Object, pathList: string[]){
return curveGet(target, pathList);
}
/**
* Makes the target to be reactive
* If the target path is not defined,
* it will be assigned as an Object.
* Warning: If the value in path end is assigned with non-PlainObject type value previously,
* the value will be override by `{}`
*/
Proto.$set = function(pathName: string, deep=false, shallow =true){
return Lycabinet.$set(this.__storage, pathName.split('.'), deep, shallow);
}
// Makes the target to be reactive
Proto.$get = function(pathName: string){
return curveGet(this.__storage, pathName.split('.') );
}
};
/**
* Proxy Modules.
* @author lozyue
* @time 2021
*/
// Convert the Object and its descendant Object from bottom to top.
function deepConvert(source: Object, deepWatch=true, shallowWatch=false){
const plainObjQueue: Array<any> = [];
// reverse for convert
const iterate = (current)=>{
plainObjQueue.unshift(current);
for(let item in current){
if(is_PlainObject(current[item])){
iterate(current[item]);
}
}
};
iterate(source);
plainObjQueue.forEach((item, index, arr)=>{
for(let ref in item){
// convert by reference.
if(is_PlainObject(item[ref]) )
arr[index][ref] = convert(item[ref], deepWatch, shallowWatch);
}
});
return convert(source, deepWatch, shallowWatch);
}
/**
* Convert the normal data to be reactive.
* todo: add the Array type support.
* @param source
* @param deepWatch
* @param shallowWatch
*/
type OnValueChange = (prop:symbol|string, newValue, oldValue)=>unknown;
type InternalValueType = {
_parent: null | unknown,
$addListener: (t:OnValueChange, onProp: string)=>unknown,
$removeListener: (h:OnValueChange, onProp: string)=>unknown,
// trigger: Record<string|symbol, OnValueChange[]>,
triggers: OnValueChange[],
value: Object,
};
function convert(source: Object, deepWatch = false, shallowWatch = true){
let internalValue: InternalValueType = Object.create(null);
// to do... Add trigger bubbule to its parents.
internalValue._parent = null;
internalValue.triggers = [];
// save the values
internalValue.value = source;
// Config it!
const propConfig = {
enumerable: false, // which is not enumerable in source either.
configurable: true,
writable: false,
};
const $addListener = (onchange: OnValueChange)=>{
return internalValue.triggers.push(onchange);
};
const $removeListener = (handle: Function)=>{
return removeArrayItem(internalValue.triggers, handle);
};
// Origin Accessable definition
const AccessQueue = ["$addListener", "$removeListener"];
AccessQueue.forEach((hook, index)=>{
internalValue[hook] = {value: null} as {value: unknown, trigger: Function[]};
Object.defineProperty(internalValue, hook, {
value: !index? $addListener: $removeListener,
...propConfig
});
});
const refValue = internalValue.value; // For reader accel.
const HandleRules = {
get(target, prop, receiver) {
DEBUG && console.info("Getted", target, prop, receiver, internalValue);
if(AccessQueue.indexOf(prop) > -1){
return internalValue[prop];
}
return refValue[prop];
},
set(target, prop, newValue, receiver) {
DEBUG && console.info("Setted", target, prop, receiver, internalValue);
const rawValue = refValue[prop];
// consistent deepWatch observer.
if(deepWatch){
if(is_PlainObject(newValue)){
if(shallowWatch){
refValue[prop] = convert(newValue, false, true);
}else{
refValue[prop] = deepConvert(newValue, deepWatch, false);
}
}
}else
refValue[prop] = newValue;
if(newValue !== rawValue){
let triggers = internalValue.triggers;
for(let index=0; index< triggers.length; index++){
// Check it!
if(!is_Function(triggers[index]) ){
throw new Error(`The get proxy handler listener added in target is Not a Function which type is ${typeof triggers[index]}`);
}
triggers[index](prop, newValue, rawValue);
}
if(onSetted!==null) onSetted();
}
return true;
},
};
return new Proxy(source, HandleRules);
}