Skip to content

Commit

Permalink
fixes #20: supporting access to value via descriptor, and setting new…
Browse files Browse the repository at this point in the history
… values via descriptor.
  • Loading branch information
caridy committed Sep 14, 2018
1 parent 35ad3a4 commit a8128aa
Show file tree
Hide file tree
Showing 5 changed files with 89 additions and 26 deletions.
25 changes: 13 additions & 12 deletions src/reactive-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,13 @@ import {
import {
ReactiveMembrane,
ReactiveMembraneShadowTarget,
wrapDescriptor,
} from './reactive-membrane';

function getWrappedValue(membrane: ReactiveMembrane, value: any): any {
return membrane.valueIsObservable(value) ? membrane.getProxy(value) : value;
}

// Unwrap property descriptors
// We only need to unwrap if value is specified
function unwrapDescriptor(descriptor: PropertyDescriptor): PropertyDescriptor {
Expand All @@ -28,13 +33,6 @@ function unwrapDescriptor(descriptor: PropertyDescriptor): PropertyDescriptor {
return descriptor;
}

function wrapDescriptor(membrane: ReactiveMembrane, descriptor: PropertyDescriptor): PropertyDescriptor {
if ('value' in descriptor) {
descriptor.value = membrane.valueIsObservable(descriptor.value) ? membrane.getProxy(descriptor.value) : descriptor.value;
}
return descriptor;
}

function lockShadowTarget(membrane: ReactiveMembrane, shadowTarget: ReactiveMembraneShadowTarget, originalTarget: any): void {
const targetKeys = ArrayConcat.call(getOwnPropertyNames(originalTarget), getOwnPropertySymbols(originalTarget));
targetKeys.forEach((key: PropertyKey) => {
Expand All @@ -46,7 +44,7 @@ function lockShadowTarget(membrane: ReactiveMembrane, shadowTarget: ReactiveMemb
// could change sometime in the future, so we can defer wrapping
// until we need to
if (!descriptor.configurable) {
descriptor = wrapDescriptor(membrane, descriptor);
descriptor = wrapDescriptor(membrane, descriptor, getWrappedValue);
}
ObjectDefineProperty(shadowTarget, key, descriptor);
});
Expand Down Expand Up @@ -145,16 +143,19 @@ export class ReactiveProxyHandler {
return desc;
}
const shadowDescriptor = getOwnPropertyDescriptor(shadowTarget, key);
if (!desc.configurable && !shadowDescriptor) {
if (!isUndefined(shadowDescriptor)) {
return shadowDescriptor;
}
desc = wrapDescriptor(membrane, desc, getWrappedValue);
if (!desc.configurable) {
// If descriptor from original target is not configurable,
// We must copy the wrapped descriptor over to the shadow target.
// Otherwise, proxy will throw an invariant error.
// This is our last chance to lock the value.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/getOwnPropertyDescriptor#Invariants
desc = wrapDescriptor(membrane, desc);
ObjectDefineProperty(shadowTarget, key, desc);
}
return shadowDescriptor || desc;
return desc;
}
preventExtensions(shadowTarget: ReactiveMembraneShadowTarget): boolean {
const { originalTarget, membrane } = this;
Expand All @@ -180,7 +181,7 @@ export class ReactiveProxyHandler {
}
ObjectDefineProperty(originalTarget, key, unwrapDescriptor(descriptor));
if (configurable === false) {
ObjectDefineProperty(shadowTarget, key, wrapDescriptor(membrane, descriptor));
ObjectDefineProperty(shadowTarget, key, wrapDescriptor(membrane, descriptor, getWrappedValue));
}

valueMutated(originalTarget, key);
Expand Down
18 changes: 18 additions & 0 deletions src/reactive-membrane.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,24 @@ const defaultValueMutated: ReactiveMembraneMutationCallback = (obj: any, key: Pr
};
const defaultValueDistortion: ReactiveMembraneDistortionCallback = (value: any) => value;

export function wrapDescriptor(membrane: ReactiveMembrane, descriptor: PropertyDescriptor, getValue: (membrane: ReactiveMembrane, originalValue: any) => any): PropertyDescriptor {
const { set, get } = descriptor;
if ('value' in descriptor) {
descriptor.value = getValue(membrane, descriptor.value);
}
if (!isUndefined(get)) {
descriptor.get = function() {
return getValue(membrane, get.call(this));
};
}
if (!isUndefined(set)) {
descriptor.set = function(value) {
set.call(this, membrane.unwrapProxy(value));
};
}
return descriptor;
}

export class ReactiveMembrane {
valueDistortion: ReactiveMembraneDistortionCallback = defaultValueDistortion;
valueMutated: ReactiveMembraneMutationCallback = defaultValueMutated;
Expand Down
18 changes: 10 additions & 8 deletions src/read-only-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,11 @@ import {
import {
ReactiveMembrane,
ReactiveMembraneShadowTarget,
wrapDescriptor,
} from './reactive-membrane';

function wrapDescriptor(membrane: ReactiveMembrane, descriptor: PropertyDescriptor): PropertyDescriptor {
if ('value' in descriptor) {
descriptor.value = membrane.valueIsObservable(descriptor.value) ? membrane.getReadOnlyProxy(descriptor.value) : descriptor.value;
}
return descriptor;
function getReadOnlyValue(membrane: ReactiveMembrane, value: any): any {
return membrane.valueIsObservable(value) ? membrane.getReadOnlyProxy(value) : value;
}

export class ReadOnlyHandler {
Expand Down Expand Up @@ -85,16 +83,20 @@ export class ReadOnlyHandler {
return desc;
}
const shadowDescriptor = getOwnPropertyDescriptor(shadowTarget, key);
if (!desc.configurable && !shadowDescriptor) {
if (!isUndefined(shadowDescriptor)) {
return shadowDescriptor;
}
desc = wrapDescriptor(membrane, desc, getReadOnlyValue);
delete desc.set; // readOnly membrane does not allow setters
if (!desc.configurable) {
// If descriptor from original target is not configurable,
// We must copy the wrapped descriptor over to the shadow target.
// Otherwise, proxy will throw an invariant error.
// This is our last chance to lock the value.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/getOwnPropertyDescriptor#Invariants
desc = wrapDescriptor(membrane, desc);
ObjectDefineProperty(shadowTarget, key, desc);
}
return shadowDescriptor || desc;
return desc;
}
preventExtensions(shadowTarget: ReactiveMembraneShadowTarget): boolean {
if (process.env.NODE_ENV !== 'production') {
Expand Down
25 changes: 23 additions & 2 deletions test/reactive-handler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -594,7 +594,7 @@ describe('ReactiveHandler', () => {
expect(accessSpy).toHaveBeenLastCalledWith(obj.foo, 'bar');
});

describe.skip('issue#20 - getOwnPropertyDescriptor', () => {
describe('issue#20 - getOwnPropertyDescriptor', () => {
it('should return reactive proxy when property value accessed via accessor descriptor', () => {
const target = new ReactiveMembrane();
const todos = {};
Expand Down Expand Up @@ -631,5 +631,26 @@ describe('ReactiveHandler', () => {
const { value } = desc;
expect(value).toBe(expected);
});
})
it('should allow set invocation via descriptor', () => {
const target = new ReactiveMembrane();
const todos = {};
let value = 0;
const newValue = {};
Object.defineProperty(todos, 'entry', {
get() {
return value;
},
set(v) {
value = v;
},
configurable: true
});
const proxy = target.getProxy(todos);
const desc = Object.getOwnPropertyDescriptor(proxy, 'entry');
const { set, get } = desc;
set.call(proxy, newValue);
expect(todos.entry).toEqual(newValue);
expect(proxy.entry).toEqual(get.call(proxy));
});
});
});
29 changes: 25 additions & 4 deletions test/read-only-handler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ describe('ReadOnlyHandler', () => {
doNothing(readOnly.foo);
}).not.toThrow();
});
describe.skip('issue#20 - getOwnPropertyDescriptor', () => {
describe('issue #20 - getOwnPropertyDescriptor', () => {
it('readonly proxy prevents mutation when value accessed via accessor descriptor', () => {
const target = new ReactiveMembrane();
const todos = {};
Expand All @@ -285,10 +285,10 @@ describe('ReadOnlyHandler', () => {
const proxy = target.getReadOnlyProxy(todos);
const desc = Object.getOwnPropertyDescriptor(proxy, 'entry');
const { get } = desc;
expect(() => {
expect(() => {
get().foo = '';
}).toThrow();
expect(todos['entry'].foo).toEqual('bar');
expect(todos.entry.foo).toEqual('bar');
});
it('readonly proxy prevents mutation when value accessed via data descriptor', () => {
const target = new ReactiveMembrane();
Expand All @@ -304,7 +304,28 @@ describe('ReadOnlyHandler', () => {
expect( () => {
value.foo = '';
}).toThrow();
expect(todos['entry'].foo).toEqual('bar');
expect(todos.entry.foo).toEqual('bar');
});
it('readonly proxy prevents access to any setter in descriptor', () => {
const target = new ReactiveMembrane();
const todos = {};
let value = 0;
Object.defineProperty(todos, 'entry', {
get() {
return value;
},
set(v) {
value = v;
},
configurable: true
});
const proxy = target.getReadOnlyProxy(todos);
const desc = Object.getOwnPropertyDescriptor(proxy, 'entry');
const { set, get } = desc;
expect(() => {
set.call(proxy, 1);
}).toThrow();
expect(get.call(proxy)).toEqual(0);
});
});
it('should throw when attempting to change the prototype', function() {
Expand Down

0 comments on commit a8128aa

Please sign in to comment.