Last active
October 12, 2018 17:15
-
-
Save intoxopox/85459c9b7d30b9e6b085d1bfb36007fc to your computer and use it in GitHub Desktop.
DeepProxy - Proxy wrapper with settable callbacks capable of safely wrapping deep-nested structures without re-wrapping already proxied objects.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//////////////////////////////////////////////////////////////////////////////// | |
// Copyright(C) 2018 David Hamiter | |
//////////////////////////////////////////////////////////////////////////////// | |
export type DeepProxyCallback = (prop?:PropertyKey | string[], newVal?:any) => any; | |
export default class DeepProxy<T extends object> | |
{ | |
//---------------------------------------------------------------------- | |
// | |
// Properties | |
// | |
//---------------------------------------------------------------------- | |
private _data: T; | |
public get data():T { | |
return this._data; | |
} | |
public set data(val:T) { | |
this._data = this._deepProxy(val, { | |
set: (target, prop: PropertyKey | string[], value, receiver) => { | |
// Something in datastore changed or was initialized, so call dataStoreChangeCallback methods. | |
// Note that _deepProxy internally handled the actual value setting and special cases. | |
for (let key of Object.keys(this._onDataChangeCallbacks)) { | |
//console.log(prop, oldVal, value, key); | |
this._onDataChangeCallbacks[key](prop, value); | |
} | |
return true; | |
} | |
}); | |
} | |
/** Object map of callbacks to run when datastore property changes. */ | |
private _onDataChangeCallbacks: { [key: string]: DeepProxyCallback } = Object.create(null); | |
//---------------------------------------------------------------------- | |
// | |
// Constructor | |
// | |
//---------------------------------------------------------------------- | |
constructor(data?: T) { | |
if (data) this.data = data; | |
} | |
//---------------------------------------------------------------------- | |
// | |
// Event Handlers | |
// | |
//---------------------------------------------------------------------- | |
//---------------------------------------------------------------------- | |
// | |
// Methods | |
// | |
//---------------------------------------------------------------------- | |
public addOnChange(callback: DeepProxyCallback): DeepProxy<T> { | |
this._onDataChangeCallbacks[callback.name] = callback; | |
return this; | |
} | |
public removeOnChange(callback: DeepProxyCallback): DeepProxy<T> { | |
delete this._onDataChangeCallbacks[callback.name]; | |
return this; | |
} | |
public removeAllOnChange(): DeepProxy<T> { | |
this._onDataChangeCallbacks = null; | |
this._onDataChangeCallbacks = Object.create(null); | |
return this; | |
} | |
public listOnChangeCallbacks(): string[] { | |
return Object.keys(this._onDataChangeCallbacks); | |
} | |
/** | |
* Creates a deep nested Proxy object | |
* @param dataStore | |
* @param handler | |
*/ | |
private _deepProxy(data: T, handler: ProxyHandler<T>): T { | |
const preproxy = new WeakMap(); | |
function makeHandler(path: PropertyKey[]) { | |
return { | |
set: (target, key, value, receiver) => { | |
//console.log("DEEP PROXYIFICATION"); | |
// Make sure value is valid and object it's being written to can be written to | |
if (typeof value === 'object' && value != null) { | |
if (value["__isProxy"] == undefined) { | |
value = proxify(value, [...path, key]); // we can safely proxy this value | |
} else { | |
//console.log("Preventing proxifying of proxy", "Key:", key); | |
} | |
} | |
var success = Reflect.set(target, key, value, receiver); | |
if (handler.set && success) { | |
handler.set(target, <any>[...path, key], value, receiver); | |
} | |
return success; | |
}, | |
get: (target, key, receiver) => { | |
if (key !== "__isProxy") { | |
if ((target instanceof Date) && target[key]) { | |
console.log("ACTED DIRECTLY ON DATE", key); | |
// Pass date object as 'this' to date's method call | |
return target[key].bind(target); | |
} | |
return Reflect.get(target, key, receiver); | |
} | |
return true; // __isProxy called and we are a Proxy | |
}, | |
deleteProperty(target, key) { | |
if (Reflect.has(target, key)) { | |
unproxy(target, key); | |
let deleted = Reflect.deleteProperty(target, key); | |
if (deleted && handler.deleteProperty) { | |
handler.deleteProperty(target, <any>[...path, key]); | |
} | |
return deleted; | |
} | |
return false; | |
} | |
} | |
} | |
function proxify(obj: any, path: PropertyKey[]) { | |
if (!obj) console.log(path, obj); | |
for (let key of Object.keys(obj)) { | |
const desc = Object.getOwnPropertyDescriptor(obj, key); | |
if (typeof obj[key] === 'object' && obj[key] != null && desc && desc.writable && desc.configurable) { | |
if (obj[key]["__isProxy"] == undefined) { | |
obj[key] = proxify(obj[key], [...path, key]); // we can safely proxy obj[key] | |
} else { | |
//console.log("Preventing proxifying of proxy", "Key:", key); | |
} | |
} | |
} | |
let p = new Proxy(obj, makeHandler(path)); | |
preproxy.set(p, obj); | |
return p; | |
} | |
function unproxy(obj, key) { | |
if (preproxy.has(obj[key])) { | |
// console.log('unproxy',key); | |
obj[key] = preproxy.get(obj[key]); | |
preproxy.delete(obj[key]); | |
} | |
for(let k of Object.keys(obj[key])) { | |
if(typeof obj[key][k] === 'object') { | |
unproxy(obj[key], k); | |
} | |
} | |
} | |
return proxify(data, []); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment