Skip to content

Instantly share code, notes, and snippets.

@intoxopox
Last active October 12, 2018 17:15
Show Gist options
  • Save intoxopox/85459c9b7d30b9e6b085d1bfb36007fc to your computer and use it in GitHub Desktop.
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.
////////////////////////////////////////////////////////////////////////////////
// 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