- Notifications
You must be signed in to change notification settings - Fork27
A Javascript Membrane implementation using Proxies to observe mutation on an object graph
License
salesforce/observable-membrane
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
Creating robust JavaScript code becomes increasingly important as web applications become more sophisticated. The dynamic nature of JavaScript code at runtime has always presented challenges for developers.
This package implements an observable membrane in JavaScript using Proxies.
A membrane can be created to observe access to a module graph and observe what the other part is attempting to do with certain objects.
- Tom van Cutsem's original article, "Isolating application sub-components with membranes"
- Tom van Cutsem's original article, "Membranes in JavaScript"
- es-membrane library by Alexander J. Vincent
One of the prime use-cases for observable membranes is the popular@observed
or@tracked
decorator used in components to detect mutations on the state of the component to re-render the component when needed. In this case, any object value set into a decorated field can be wrapped into an observable membrane to monitor if the object is accessed during the rendering phase, and if so, the component must be re-rendered when mutations on the object value are detected. And this process is applied not only at the object value level, but at any level in the object graph accessible via the observed object value.
The following example illustrates how to create an observable membrane, and proxies:
import{ObservableMembrane}from'observable-membrane';constmembrane=newObservableMembrane();consto={x:2,y:{z:1},};constp=membrane.getProxy(o);p.x;// yields 2p.y.z;// yields 1
Note: If the value that you're accessing via the membrane is an object that can be observed, then the membrane will return a proxy around it. In the example above,o.y !== p.y
becausep.y
is a proxy that applies the exact same mechanism. In other words, the membrane is applicable to an entire object graph.
The most basic operation in an observable membrane is to observe property member access and mutations. For that, the constructor accepts an optional argumentsoptions
that accepts two callbacks,valueObserved
andvalueMutated
:
import{ObservableMembrane}from'observable-membrane';constmembrane=newObservableMembrane({valueObserved(target,key){// where target is the object that was accessed// and key is the key that was readconsole.log('accessed ',key);},valueMutated(target,key){// where target is the object that was mutated// and key is the key that was mutatedconsole.log('mutated ',key);},});consto={x:2,y:{z:1},};constp=membrane.getProxy(o);p.x;// console output -> 'accessed x'// yields 2p.y.z;// console output -> 'accessed z'// yields 1p.y.z=3;// console output -> 'mutated z'// yields 3
Another use-case for observable membranes is to prevent mutations in the object graph. For that,ObservableMembrane
provides an additional method that gets a read-only version of any object value. One of the prime use-cases for read-only membranes is to hand over an object to another actor, observe how the actor uses that object reference, but prevent the actor from mutating the object. E.g.: passing an object property down to a child component that can consume the object value but not mutate it.
This is also a very cheap way of doing deep-freeze, although it is not exactly the same, but can cover a lot of ground without having to actually freeze the original object, or a copy of it:
import{ObservableMembrane}from'observable-membrane';constmembrane=newObservableMembrane({valueObserved(target,key){// where target is the object that was accessed// and key is the key that was readconsole.log('accessed ',key);},});consto={x:2,y:{z:1},};constr=membrane.getReadOnlyProxy(o);r.x;// yields 2r.y.z;// yields 1r.y.z=2;// throws Error in dev-mode, and does nothing in production mode
For advanced usages, the observable membrane instance offers the ability to unwrap any proxy generated by the membrane. This can be used to detect membrane presence and other detections that may be useful to framework authors. E.g.:
import{ObservableMembrane}from'observable-membrane';constmembrane=newObservableMembrane();consto={x:2,y:{z:1,},};constp=membrane.getProxy(o);o.y!==p.y;// yields true because `p` is a proxy of `o`o.y===membrane.unwrapProxy(p.y);// yields true because `membrane.unwrapProxy(p.y)` returns the original target `o.y`
This membrane implementation is tailored for what we call "observable objects", which are objects with basic functionalities that do not require identity to be preserved. Usually any object with the prototype chain set toObject.prototype
ornull
. You can control what gets wrapped by implementing a custom callback for thevalueIsObservable
option:
import{ObservableMembrane}from'observable-membrane';constmembrane=newObservableMembrane({valueIsObservable(value){// intentionally checking for nullif(value===null){returnfalse;}// treat all non-object types, including undefined, as non-observable valuesif(typeofvalue!=='object'){returnfalse;}if(isArray(value)){returntrue;}constproto=Reflect.getPrototypeOf(value);return(proto===Object.prototype||proto===null);},});consto={x:1};membrane.getProxy(o)!==o;// yields true because it was wrappedmembrane.getProxy(document)!==document;// yields false because a DOM object is not observable
Note: be very careful about customizingvalueIsObservable
. Any object with methods that requires identity (e.g.: objects with private fields, or methods accessing weakmaps with thethis
value as a key), will simply not work because those methods will be called with thethis
value being the actual proxy.
There arerunnable examples in this Git repository. You must build this package as described in theContributing Guide before attempting to run the examples. Additionally, some of the examples might be relying on features that are not supported in all browsers (e.g.:reactivo-element example relies on Web Components APIs).
Create a new membrane.
Parameters
config
[Object] [Optional] The membrane configurationvalueObserved
[Function] [Optional] Callback invoked when an observed property is accessed. This function receives as argument the original target and the property key.valueMutated
[Function] [Optional] Callback invoked when an observed property is mutated. This function receives as argument the original target and the property key.valueIsObservable
[Function] [Optional] Callback to determine whether or not a value qualifies as a proxy target for the membrane. The default implementation will only observe plain objects (objects with their prototype set tonull
orObject.prototype
).tagPropertyKey
[PropertyKey] [Optional] A valid string or symbol that can be used to identify proxies created by the membrane. This is useful for any kind of debugging tools or object identity mechanism.
Wrap an object in the membrane. If theobject
is observable it will return a proxified version of the object, otherwise it returns the original value.
Parameters
object
[Object] The object to wrap in the membrane.
Wrap an object in the read-only membrane. If theobject
is observable it will return a proxified version of the object, otherwise it returns the original value.
Parameters
object
[Object] The object to wrap in the membrane.
Unwrap the proxified version of the object from the membrane and return it's original value.
Parameters
proxy
[Object] Proxified object to unwrap from the membrane.
- Not all objects can be wrapped by this membrane. More information about this in the examples above.
- The ability for the membrane creator to revoke all proxies within it to prevent further mutations to the underlying objects (aka, membrane shutdown switch) is not supported at the moment.
- A value mutation that is set to a read-only proxy value is allowed, but the subtree will still be read-only, e.g.:
const p = membrane.getProxy({}); p.abc = membrane.getReadOnlyProxy({}); p.abc.qwe = 1;
will throw because the value assigned toabc
is still read only.
Observable membranes requires Proxy (ECMAScript 6)to be available.
This library is production ready, it has been used at Salesforce in production for over a year. It is very lightweight (~1k - minified and gzipped), that can be used with any framework or library. It is designed to be very performant.
Please make sure to read theContributing Guide before making a pull request.
Copyright (C) 2017 salesforce.com, Inc.
About
A Javascript Membrane implementation using Proxies to observe mutation on an object graph
Topics
Resources
License
Code of conduct
Contributing
Security policy
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Packages0
Uh oh!
There was an error while loading.Please reload this page.