UNPKG

lycabinet

Version:

A simple small JSON Object storage helper with good performance.

199 lines (182 loc) 6.46 kB
/** * 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); }