Skip to content

Commit

Permalink
Merge pull request #23 from creately/setters
Browse files Browse the repository at this point in the history
Support setters in proxy targets (with flags)
  • Loading branch information
ramishka authored Jun 27, 2019
2 parents 3b0f5f3 + e8dcd7e commit 633dfd1
Show file tree
Hide file tree
Showing 2 changed files with 140 additions and 9 deletions.
77 changes: 77 additions & 0 deletions src/__tests__/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { Sakota } from '../';

Sakota.enableESGetters();
Sakota.enableESSetters();

/**
* Function returns an array of different types of values.
*/
Expand All @@ -20,6 +23,10 @@ export const values = () => [

export class Point {
constructor(public x = 1, public y = 2) {}
set p(p: { x: number; y: number }) {
this.x = p.x;
this.y = p.y;
}
get d() {
return this.x + this.y;
}
Expand Down Expand Up @@ -349,6 +356,76 @@ describe('Sakota', () => {
},
}),

// setting a value using setter functions in target
// ------------------------------------------------
() => ({
target: {
x: 1,
y: 2,
set p(p: { x: number; y: number }) {
this.x = p.x;
this.y = p.y;
},
},
action: (obj: any) => {
obj.p = { x: 10, y: 20 };
},
result: { x: 10, y: 20, p: undefined },
change: {
$set: { x: 10, y: 20 },
},
}),

// setting a value using setter functions in target (nested)
// ---------------------------------------------------------
() => ({
target: {
t: {
x: 1,
y: 2,
set p(p: { x: number; y: number }) {
this.x = p.x;
this.y = p.y;
},
},
},
action: (obj: any) => {
obj.t.p = { x: 10, y: 20 };
},
result: { t: { x: 10, y: 20, p: undefined } },
change: {
$set: { 't.x': 10, 't.y': 20 },
},
}),

// setting a value using setter functions in prototype
// ---------------------------------------------------
() => ({
target: new Point(),
action: (obj: any) => {
obj.p = { x: 10, y: 20 };
},
result: new Point(10, 20),
change: {
$set: { x: 10, y: 20 },
},
}),

// setting a value using setter functions in prototype (nested)
// ------------------------------------------------------------
() => ({
target: {
t: new Point(),
},
action: (obj: any) => {
obj.t.p = { x: 10, y: 20 };
},
result: { t: new Point(10, 20) },
change: {
$set: { 't.x': 10, 't.y': 20 },
},
}),

// modify the object and check result multiple times
// -------------------------------------------------
() => ({
Expand Down
72 changes: 63 additions & 9 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type KeyType = string | number | symbol;
* These weakmaps hold caches of property descriptors of objects and getters used by sakota.
*/
const $getters = new WeakMap<object, { [key: string]: (() => any) | null }>();
const $setters = new WeakMap<object, { [key: string]: ((val: any) => void) | null }>();
const $descriptors = new WeakMap<object, { [key: string]: PropertyDescriptor | null }>();

/**
Expand All @@ -36,15 +37,33 @@ const $descriptors = new WeakMap<object, { [key: string]: PropertyDescriptor | n
*/
export class Sakota<T extends object> implements ProxyHandler<T> {
/**
* This flag should be set to 'true' to enable optimizations.
* Globally configure how Sakota proxies should behave.
*/
private static prodmode = false;
private static config = {
prodmode: false,
esgetter: false,
essetter: false,
};

/**
* Makes Sakota work faster by removing dev-only code.
*/
public static enableProdMode(): void {
this.prodmode = true;
this.config.prodmode = true;
}

/**
* Makes Sakota support javascript getters (expensive!).
*/
public static enableESGetters(): void {
this.config.esgetter = true;
}

/**
* Makes Sakota support javascript getters (expensive!).
*/
public static enableESSetters(): void {
this.config.essetter = true;
}

/**
Expand Down Expand Up @@ -138,9 +157,11 @@ export class Sakota<T extends object> implements ProxyHandler<T> {
return this.diff.$set[key as any];
}
}
const getter = this.getGetterFunction(obj, key);
if (getter) {
return getter.call(this.proxy);
if (Sakota.config.esgetter) {
const getter = this.getGetterFunction(obj, key);
if (getter) {
return getter.call(this.proxy);
}
}
const value = obj[key];
if (!value) {
Expand Down Expand Up @@ -200,10 +221,17 @@ export class Sakota<T extends object> implements ProxyHandler<T> {
/**
* Proxy handler trap for setting a property.
*/
public set(_obj: any, key: KeyType, val: any): boolean {
if (!Sakota.prodmode) {
public set(obj: any, key: KeyType, val: any): boolean {
if (!Sakota.config.prodmode) {
if (this._hasSakota(val)) {
console.warn('Sakota: value is also wrapped by Sakota!', { obj: _obj, key, val });
console.warn('Sakota: value is also wrapped by Sakota!', { obj: obj, key, val });
}
}
if (Sakota.config.essetter) {
const setter = this.getSetterFunction(obj, key);
if (setter) {
setter.call(this.proxy, val);
return true;
}
}
if (!this.diff) {
Expand Down Expand Up @@ -322,6 +350,32 @@ export class Sakota<T extends object> implements ProxyHandler<T> {
return null;
}

/**
* Returns the setter function of a property if available. Checks prototypes as well.
*/
private getSetterFunction(obj: any, key: KeyType): ((val: any) => void) | null {
let settersMap = $setters.get(obj);
if (settersMap) {
// NOTE: hasOwnProperty canm also he available as a value
if (Object.prototype.hasOwnProperty.call(settersMap, key)) {
return settersMap[key as any];
}
} else {
settersMap = {};
$setters.set(obj, settersMap);
}
for (let p = obj; p && p !== Object.prototype; p = Object.getPrototypeOf(p)) {
const desc = this.getObjPropertyDescriptor(p, key);
if (desc) {
const setter = desc.set || null;
settersMap[key as any] = setter;
return setter;
}
}
settersMap[key as any] = null;
return null;
}

/**
* Returns the property descriptor for an object. Use cached value when available.
*/
Expand Down

0 comments on commit 633dfd1

Please sign in to comment.