Skip to content

Commit

Permalink
@caridy on behalf the group of original contributors
Browse files Browse the repository at this point in the history
 - @davidturissini David Turissini
 - @diervo: Diego Ferreiro Val
 - @caridy: Caridy Patiño

This library was extracted from another multi-package, and we could not preserve the full history, which goes back for about one year from this commit.
  • Loading branch information
caridy committed Jul 16, 2018
1 parent adcaea4 commit ed0dfa0
Show file tree
Hide file tree
Showing 20 changed files with 6,064 additions and 0 deletions.
11 changes: 11 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
root = true

[*]
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4
trim_trailing_whitespace = true

[*.{json,yml}]
indent_size = 2
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
coverage
dist/
7 changes: 7 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
src
coverage
examples/
jest.config.js
rollup.config.js
tsconfig.json
.*
172 changes: 172 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
# Contributing

[Set up SSH access to Github][setup-github-ssh] if you haven't done so already.

## Requirements

* Node 8.x
* NPM 5.x
* Yarn >= 0.27.5

## Installation

### 1) Download the repository

```bash
git clone git@github.com:salesforce/observable-membrane.git
```

### 2) Install Dependencies

*We use [yarn](https://yarnpkg.com/) because it is significantly faster than npm for our use case. See this command [cheatsheet](https://yarnpkg.com/lang/en/docs/migrating-from-npm/).*

```bash
yarn install
```

## Building

When using `yarn build`, it will build the entire project into the `dist/` folder where you can find the different distributions:

```bash
yarn build
```

As a result, this is the output:

```
dist/
├── commonjs
│   └── observable-membrane.js
├── modules
│   └── observable-membrane.js
└── umd
├── observable-membrane.js
└── observable-membrane.min.js
```

By default, when using this package in node, the `commonjs/` or `modules/` distro will be used. Additionally, you can use the `umd/` version directly in browsers that support `Proxy`.

## Testing

When using `yarn test`, it will execute the unit tests using `jest`:

```bash
yarn test
```

## Linter

When using `yarn lint`, it will lint the `src/` folder using `tslint`:

```bash
yarn lint
```

The above command may display lint issues that are unrelated to your changes.
The recommended way to avoid lint issues is to [configure your
editor][eslint-integrations] to warn you in real time as you edit the file.

Fixing all existing lint issues is a tedious task so please pitch in by fixing
the ones related to the files you make changes to!

## Editor Configurations

Configuring your editor to use our lint and code style rules will help make the
code review process delightful!

### types

This project relies on type annotations heavily.

* Make sure your editor supports [typescript](https://www.typescriptlang.org/).

### eslint

[Configure your editor][eslint-integrations] to use our eslint configurations.

### editorconfig

[Configure your editor][editorconfig-plugins] to use our editor configurations.

### Visual Studio Code

```
ext install EditorConfig
```

## Git Workflow

The process of submitting a pull request is fairly straightforward and
generally follows the same pattern each time:

1. [Create a feature branch](#create-a-feature-branch)
1. [Make your changes](#make-your-changes)
1. [Rebase](#rebase)
1. [Check your submission](#check-your-submission)
1. [Create a pull request](#create-a-pull-request)
1. [Update the pull request](#update-the-pull-request)
1. [Commit Message Guidelines](#commit)

### Create a feature branch

```bash
git checkout master
git pull origin master
git checkout -b <name-of-the-feature>
```

### Make your changes

Modify the files, build, test, lint and eventually commit your code using the following command:

```bash
git add <path/to/file/to/commit>
git commit
git push origin <name-of-the-feature>
```

The above commands will commit the files into your feature branch. You can keep
pushing new changes into the same branch until you are ready to create a pull
request.

### Rebase

Sometimes your feature branch will get stale with respect to the master branch,
and it will require a rebase. The following steps can help:

```bash
git checkout master
git pull origin master
git checkout <name-of-the-feature>
git rebase master <name-of-the-feature>
```

_note: If no conflicts arise, these commands will ensure that your changes are applied on top of the master branch. Any conflicts will have to be manually resolved._

### Create a pull request

If you've never created a pull request before, follow [these
instructions][creating-a-pull-request].
Pull request title must be formatted according to [Commit Message Guidelines](#commit).
Pull request samples can be found [here](https://github.com/salesforce/observable-membrane/pulls)

### Update the pull request

```sh
git fetch origin
git rebase origin/${base_branch}

# If there were no merge conflicts in the rebase
git push origin ${feature_branch}

# If there was a merge conflict that was resolved
git push origin ${feature_branch} --force
```

_note: If more changes are needed as part of the pull request, just keep committing and pushing your feature branch as described above and the pull request will automatically update._

[setup-github-ssh]: https://help.github.com/articles/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent/
[creating-a-pull-request]: https://help.github.com/articles/creating-a-pull-request/
[eslint-integrations]: http://eslint.org/docs/user-guide/integrations
[editorconfig-plugins]: http://editorconfig.org/#download
189 changes: 189 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
# Observable Membrane

This package implements an observable membrane in JavaScript using Proxies.

A membrane can be created to control access to a module graph, observe what the other part is attempting to do with the objects that were handed over to them, and even distort the way they see the module graph.

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.

Additionally, it supports distorting objects within an object graph, which could be used for:

* avoid leaking symbols and other non-observables objects.
* distorting values observed through the membrane.

### Disclaimer

This is very lightweight library (~1k - minified and gzipped), that can be used with any framework and library that requires a basic membrane. It is designed to be very performant, and be used in production. It has been battle tested at Salesforce in production for over a year.

### Usage

This package exposes one constructor as the default export. This constructor, often called `ObservableMembrane` creates a new membrane object that contains two methods, `getProxy` and `getReadOnlyProxy`. The following example illustrate how to create an observable membrane, and proxies:

```js
import ObservableMembrane from 'observable-membrane';

const membrane = new ObservableMembrane();

const o = {
x: 2,
y: {
z: 1
},
};

const p = membrane.getProxy(o);

p.x;
// yields 4

p.y.z // 1
// yields 1
```

_Note: If the value that your accessing via the membrane is an object that can be observed, the membrane will return a new proxy. In the example above, `o.y !== p.y` because it is a proxy that apply the exact same mechanism. In other words, the membrane is applicable to an entire object graph._

#### Observing access and mutations

The most basic operation in an observable membrane is to observe property member access and mutations. For that, the constructor accepts an optional arguments `options` that accepts two callbacks, `valueObserved` and `valueMutated`:

```js
import ObservableMembrane from 'observable-membrane';

const membrane = new ObservableMembrane({
valueObserved(target, key) {
// where the target is the object that was accessed
// and the key is the key that was read
console.log('accessed ', key);
},
valueMutated(target, key) {
// where the target is the object that was mutated
// and the key is the key that was mutated
console.log('mutated ', key);
},
});

const o = {
x: 2,
y: {
z: 1
},
};

const p = membrane.getProxy(o);

p.x;
// console output -> 'accessed x'
// yields 4

p.y.z;
// console output -> 'accessed z'
// yields 1

p.y.z = 3;
// console output -> 'mutated z'
// yields 3
```

#### Read Only Proxies

Another use-case for observable membranes is to prevent mutations in the object graph. For that, `ObservableMembrane` provides an additional method that get 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 for mutating the object. E.g.: passing an object property down to a child component that can consume the object value, but cannot mutated.

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.

Here is an example on top of the previous one:

```js
const r = membrane.getReadOnlyProxy(o);

r.x;
// yields 4

r.y.z;
// yields 1

r.y.z = 2;
// throws Error in dev-mode, and does nothing in production mode
```

#### Distortion

As described above, you could use distortion to avoid leaking non-observables objects and distorting values observed through the membrane:

```js
import ObservableMembrane from 'observable-membrane';

const membrane = new ObservableMembrane({
valueDistortion(value) {
if (value instanceof Node) {
throw new ReferenceError(`Invalid access to a non-observable Node`);
}
console.log('distorting ', value);
if (value === 1) {
return 10;
}
return value;
},
});

const o = {
x: 2,
y: {
z: 1,
node: document.createElement('p'),
},
};

const p = membrane.getProxy(o);

p.x;
// console output -> 'distorting 2'
// yields 2

p.y.z;
// console output -> 'distorting 1'
// yields 10

p.y.node;
// throws ReferenceError
```

_Note: You could use a `WeakMap` to remap symbols to avoid leaking the original symbols and other non-observable objects through the distortion mechanism._

#### Unwrapping Proxies

For advanced usages, the observable membrane instance offers the ability to unwrap any proxy generated by the membrane. This can help to detect membrane presence and other detections that might be useful for framework authorsm, e.g.:

```js
import ObservableMembrane from 'observable-membrane';

const membrane = new ObservableMembrane();

const o = {
x: 2,
y: {
z: 1,
},
};

const p = membrane.getProxy(o);

o.y !== p.x;
// 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`
```

## Browser Compatibility

Observable membranes requires Proxy (ECMAScript 6) [to be available](https://caniuse.com/#search=proxy).

## Contribution

Please make sure to read the [Contributing Guide](CONTRIBUTING.md) before making a pull request.

## License

[MIT](http://opensource.org/licenses/MIT)

Copyright (C) 2017 salesforce.com, inc.
11 changes: 11 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module.exports = {
moduleFileExtensions: ['ts', 'js', 'json'],
transform: {
'.ts': require.resolve('ts-jest/preprocessor.js'),
'.js': require.resolve('ts-jest/preprocessor.js')
},
testMatch: [
'<rootDir>/**/__tests__/*.spec.(js|ts)'
],
displayName: 'observable-membrane',
};
Loading

0 comments on commit ed0dfa0

Please sign in to comment.