Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

reactive-function

Package Overview
Dependencies
Maintainers
1
Versions
17
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

reactive-function - npm Package Compare versions

Comparing version 0.9.0 to 0.10.0

35

index.js

@@ -100,5 +100,15 @@ var ReactiveProperty = require("reactive-property");

// Remove property nodes with no edges connected.
inputs.concat([output]).forEach(function (property){
var node = property.id;
if(graph.indegree(node) + graph.outdegree(node) === 0){
graph.removeNode(property.id);
}
});
// Remove the reference to the 'evaluate' function.
delete output.evaluate;
// Remove references to everything.
inputs = callback = output = undefined;
}

@@ -156,2 +166,27 @@ };

ReactiveFunction.serializeGraph = function (){
var serialized = graph.serialize();
// Replace ids with names for nodes.
serialized.nodes.forEach(function (node){
var name = properties[node.id].propertyName;
if(name){
node.id = name;
}
});
// Replace ids with names for links.
serialized.links.forEach(function (link){
var sourceName = properties[link.source].propertyName;
if(sourceName){
link.source = sourceName;
}
var targetName = properties[link.target].propertyName;
if(targetName){
link.target = targetName;
}
});
return serialized;
}
module.exports = ReactiveFunction;

15

package.json
{
"name": "reactive-function",
"version": "0.9.0",
"version": "0.10.0",
"description": "A library for managing data flows and changing state.",

@@ -11,3 +11,3 @@ "main": "index.js",

"type": "git",
"url": "git+https://github.com/curran/reactive-function.git"
"url": "git+https://github.com/datavis-tech/reactive-function.git"
},

@@ -30,12 +30,13 @@ "keywords": [

"bugs": {
"url": "https://github.com/curran/reactive-function/issues"
"url": "https://github.com/datavis-tech/reactive-function/issues"
},
"homepage": "https://github.com/curran/reactive-function#readme",
"homepage": "https://github.com/datavis-tech/reactive-function#readme",
"dependencies": {
"graph-data-structure": "^0.5.0",
"reactive-property": "^0.8.0"
"graph-data-structure": "^0.8.0",
"reactive-property": "^1.0.0"
},
"devDependencies": {
"mocha": "^2.4.5"
"graph-diagrams": "^0.5.0",
"mocha": "^2.5.3"
}
}

@@ -1,27 +0,32 @@

# reactive-function [![Build Status](https://travis-ci.org/curran/reactive-function.svg?branch=master)](https://travis-ci.org/curran/reactive-function)
# reactive-function
A library for managing reactive data flows.
[![NPM](https://nodei.co/npm/reactive-function.png)](https://npmjs.org/package/reactive-function)
[![NPM](https://nodei.co/npm-dl/reactive-function.png?months=3)](https://npmjs.org/package/reactive-function)
[![Build Status](https://travis-ci.org/datavis-tech/reactive-function.svg?branch=master)](https://travis-ci.org/datavis-tech/reactive-function)
* A library for managing data flows graphs and changing state.
* Built on [reactive-property](https://github.com/curran/reactive-property).
* The foundation for [reactive-model](https://github.com/curran/reactive-model).
This library provides the ability to define reactive data flows by modeling application state as a directed graph and using [topological sorting](https://en.wikipedia.org/wiki/Topological_sorting) to compute the order in which changes should be propagated.
# Usage
<p align="center">
<img src="https://cloud.githubusercontent.com/assets/68416/15476439/82b83244-212c-11e6-91d5-d975de8b6b8a.png">
<br>
This library is built on top of <a href="https://github.com/datavis-tech/reactive-property">reactive-property</a> and <a href="https://github.com/datavis-tech/graph-data-structure">graph-data-structure</a>
</p>
If you are using NPM, install this package with:
**Table of Contents**
`npm install reactive-function`
* [Examples](#examples)
* [Full Name](#full-name)
* [ABC](#abc)
* [Tricky Case](#tricky-case)
* [Installing](#installing)
* [API Reference](#api-reference)
* [Managing Reactive Functions](#managing-reactive-functions)
* [Data Flow Execution](#data-flow-execution)
* [Serialization](#serialization)
Require it in your code like this:
## Examples
```javascript
var ReactiveFunction = require("reactive-function");
```
This library is designed to work with [reactive-property](https://github.com/curran/reactive-property), you'll need that too.
```javascript
var ReactiveProperty = require("reactive-property");
```
### Full Name
Suppose you have two reactive properties to represent someone's first and last name.

@@ -34,3 +39,3 @@

Suppose you'd like to have another property that represents the full name of the person.
Another reactive property can represent the full name of the person.

@@ -47,8 +52,6 @@ ```javascript

However, this sets the value of `fullName` only once, and it does not get updated when `firstName` or `lastName` change.
However, the above code sets the value of `fullName` only once. Here's how you can define a **[ReactiveFunction](#constructor)** that automatically updates `fullName` whenever `firstName` or `lastName` change.
Here's how you can define a reactive function that updates the full name whenever the first name or last name changes.
```javascript
ReactiveFunction({
var reactiveFunction = ReactiveFunction({
inputs: [firstName, lastName],

@@ -62,5 +65,9 @@ output: fullName,

This defines a "reactive function" that will be invoked when its inputs (`firstName` and `lastName`) are both defined and whenever either one changes ([`null` is considered a defined value](https://github.com/curran/reactive-function/issues/1)). The function will be invoked on the next tick of the JavaScript event loop after it is defined and after any dependencies change.
<p align="center">
<img src="https://cloud.githubusercontent.com/assets/68416/15389922/cf3f24dc-1dd6-11e6-92d6-058051b752ea.png">
<br>
The data flow graph for the example code above.
</p>
To force a synchronous evaluation of all reactive functions whose dependencies have updated, you can call
Whenever `firstName` or `lastName` change, the callback defined above will be executed on the next animation frame. If you don't want to wait until the next animation frame, you can force a synchronous evaluation of the data flow graph by invoking **[digest](#digest)**.

@@ -71,3 +78,3 @@ ```javascript

Now you can access the computed value of the reactive function by invoking it as a getter.
Now you can access the computed `fullName` value by invoking it as a getter.

@@ -78,6 +85,195 @@ ```javascript

For more detailed example code, have a look at the [tests](https://github.com/curran/reactive-function/blob/master/test.js).
### ABC
Related work:
The output of one reactive function can be used as an input to another.
<p align="center">
<img src="https://cloud.githubusercontent.com/assets/68416/15385597/44a10522-1dc0-11e6-9054-2150f851db46.png">
<br>
Here, b is both an output and an input.
</p>
```javascript
var a = ReactiveProperty(5);
var b = ReactiveProperty();
var c = ReactiveProperty();
ReactiveFunction({
inputs: [a],
output: b,
callback: function (a){ return a * 2; }
});
ReactiveFunction({
inputs: [b],
output: c,
callback: function (b){ return b / 2; }
});
ReactiveFunction.digest();
assert.equal(c(), 5);
```
### Tricky Case
This is the case where [Model.js](https://github.com/curran/model) fails because it uses [Breadth-first Search](https://en.wikipedia.org/wiki/Breadth-first_search) to propagate changes. In this graph, propagation using breadth-first search would cause `e` to be set twice, and the first time it would be set with an *inconsistent state*. This fundamental flaw cropped up as flashes of inconstistent states in some interactive visualizations built on Model.js. For example, it happens when you change the X column in this [Magic Heat Map](http://bl.ocks.org/curran/a54fc3a6578efcdc19f4). This flaw in Model.js is the main inspiration for making this library and using [topological sort](https://en.wikipedia.org/wiki/Topological_sorting), which is the correct algorithm for propagating data flows and avoiding inconsistent states.
<p align="center">
<img src="https://cloud.githubusercontent.com/assets/68416/15400254/7f779c9a-1e08-11e6-8992-9d2362bfba63.png">
<br>
The tricky case, where breadth-first propagation fails.
</p>
```javascript
var a = ReactiveProperty(5);
var b = ReactiveProperty();
var c = ReactiveProperty();
var d = ReactiveProperty();
var e = ReactiveProperty();
ReactiveFunction({ inputs: [a], output: b, callback: function (a){ return a * 2; } });
ReactiveFunction({ inputs: [b], output: c, callback: function (b){ return b + 5; } });
ReactiveFunction({ inputs: [a], output: d, callback: function (a){ return a * 3; } });
ReactiveFunction({ inputs: [c, d], output: e, callback: function (c, d){ return c + d; } });
ReactiveFunction.digest();
assert.equal(e(), ((a() * 2) + 5) + (a() * 3));
a(10);
ReactiveFunction.digest();
assert.equal(e(), ((a() * 2) + 5) + (a() * 3));
```
For more detailed example code, have a look at the [tests](https://github.com/datavis-tech/reactive-function/blob/master/test.js).
## Installing
If you are using [NPM](npmjs.com), install this package with:
`npm install reactive-function`
Require it in your code like this:
```javascript
var ReactiveFunction = require("reactive-function");
```
This library is designed to work with [reactive-property](https://github.com/datavis-tech/reactive-property), you'll need that too.
`npm install reactive-property`
```javascript
var ReactiveProperty = require("reactive-property");
```
## API Reference
* [Managing Reactive Functions](#managing-reactive-functions)
* [Data Flow Execution](#data-flow-execution)
* [Serialization](#serialization)
### Managing Reactive Functions
<a name="constructor" href="#constructor">#</a> <b>ReactiveFunction</b>(<i>options</i>)
Construct a new reactive function. The *options* argument should have the following properties.
* *inputs* - The input properties. An array of **[ReactiveProperty](https://github.com/datavis-tech/reactive-property#constructor)** instances.
* *output* (optional) - The output property. An instance of **[ReactiveProperty](https://github.com/datavis-tech/reactive-property#constructor)**.
* *callback* - The reactive function callback. Arguments are values of *inputs*. The return value will be assigned to *output*.
This constructor sets up a reactive function such that *callback* be invoked
* when all input properties are defined,
* after any input properties change,
* during a **[digest](#digest)**.
An input property is considered "defined" if it has any value other than `undefined`. The special value `null` is considered to be defined.
An input property is considered "changed" when
* the reactive function is initially set up, and
* whenever its value is set.
Input properties for one reactive function may also be outputs of another.
<a name="destroy" href="#destroy">#</a> <i>reactiveFunction</i>.<b>destroy</b>()
Cleans up resources allocated to this reactive function.
More specifically:
* Removes listeners from inputs.
* Removes edges from the data flow graph (from each input).
* Removes property nodes from the data flow graph if they have no incoming or outgoing edges.
You should invoke this function when finished using reactive functions in order to avoid memory leaks.
### Data Flow Execution
<a name="digest" href="#digest">#</a> ReactiveFunction.<b>digest</b>()
Propagates changes from input properties through the data flow graph defined by all reactive properties using [topological sorting](https://en.wikipedia.org/wiki/Topological_sorting). An edge in the data flow graph corresponds to a case where the output of one reactive function is used as an input to another.
Whenever any input properties for any reactive function change, **[digest](#digest)** is debounced (scheduled for invocation) on the **[nextFrame](#next-frame)**. Because it is debounced, multiple synchronous changes to input properties collapse into a single digest invocation.
Digests are debounced to the next animation frame rather than the next tick because browsers will render the page at most every animation frame (approximately 60 frames per second). This means that if DOM manipulations are triggered by reactive functions, and input properties are changed more frequently than 60 times per second (e.g. mouse or keyboard events), the DOM manipulations will only occur at most 60 times per second, not more than that.
<a name="next-frame" href="#next-frame">#</a> ReactiveFunction.<b>nextFrame</b>(<i>callback</i>)
Schedules the given function to execute on the next animation frame or next tick.
This is a simple polyfill for [requestAnimationFrame](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame) that falls back to [setTimeout](https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setTimeout). The main reason for having this is for use in the [tests](https://github.com/datavis-tech/reactive-function/blob/master/test.js), which run in a Node.js environment where `requestAnimationFrame` is not available. Automatic digests are debounced against this function.
### Serialization
Data flow graphs can be serialized to JSON, then visualized using [graph-diagrams](https://github.com/datavis-tech/graph-diagrams/).
<a name="serialize" href="#serialize">#</a> ReactiveFunction.<b>serializeGraph</b>()
Returns a serialized form of the graph. Node names are derived from `property.propertyName` for each property. If `propertyName` is not specified, then the automaticelly generated node id (an integer) is used as the node name.
Example:
```javascript
var firstName = ReactiveProperty("Jane");
var lastName = ReactiveProperty("Smith");
var fullName = ReactiveProperty();
// For serialization.
firstName.propertyName = "firstName";
lastName.propertyName = "lastName";
fullName.propertyName = "fullName";
ReactiveFunction({
inputs: [firstName, lastName],
output: fullName,
callback: function (first, last){
return first + " " + last;
}
});
var serialized = ReactiveFunction.serializeGraph();
```
The value of `serialized` will be:
```json
{
"nodes": [
{ "id": "fullName" },
{ "id": "firstName" },
{ "id": "lastName" }
],
"links": [
{ "source": "firstName", "target": "fullName" },
{ "source": "lastName", "target": "fullName" }
]
}
```
See also <a href="https://github.com/datavis-tech/graph-data-structure#serialize"><i>graph</i>.<b>serialize</b>()</a>.
## Related Work
* [Functional Reactive Animation (academic paper)](https://www.eecs.northwestern.edu/~robby/courses/395-495-2009-winter/fran.pdf)
* [ReactiveJS](https://github.com/mattbaker/Reactive.js)

@@ -89,3 +285,9 @@ * [vogievetsky/DVL](https://github.com/vogievetsky/DVL)

* [Model.js](https://github.com/curran/model)
* [RxJS - when](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/when.md)
* [Bacon - When](https://github.com/baconjs/bacon.js/tree/master#bacon-when)
Thanks to Mike Bostock and Vadim Ogievetsky for suggesting to have the ability to digest within a single tick of the event loop (the main difference between this project and the original [model-js](https://github.com/curran/model)). This idea has inspired the construction of this library, which I hope will provide a solid foundation for complex interactive visualization systems.
<p align="center">
<a href="https://datavis.tech/">
<img src="https://cloud.githubusercontent.com/assets/68416/15298394/a7a0a66a-1bbc-11e6-9636-367bed9165fc.png">
</a>
</p>

@@ -10,2 +10,13 @@ // Unit tests for reactive-function.

var outputGraph = require("graph-diagrams")({
// If true, writes graph files to ../graph-diagrams for visualization.
outputGraphs: true,
project: "reactive-function"
});
function output(name){
outputGraph(ReactiveFunction.serializeGraph(), name);
}
describe("ReactiveFunction", function() {

@@ -20,3 +31,3 @@

ReactiveFunction({
var reactiveFunction = ReactiveFunction({
inputs: [firstName, lastName],

@@ -39,2 +50,10 @@ output: fullName,

assert.equal(fullName(), "John Lennon");
// For serialization.
firstName.propertyName = "firstName";
lastName.propertyName = "lastName";
fullName.propertyName = "fullName";
output("full-name");
reactiveFunction.destroy();
});

@@ -50,3 +69,3 @@

ReactiveFunction({
var rf1 = ReactiveFunction({
inputs: [a],

@@ -59,3 +78,3 @@ output: d,

ReactiveFunction({
var rf2 = ReactiveFunction({
inputs: [a, b, c],

@@ -72,2 +91,13 @@ output: e,

assert.equal(e(), 30);
// For serialization.
a.propertyName = "a";
b.propertyName = "b";
c.propertyName = "c";
d.propertyName = "d";
e.propertyName = "e";
output("any-number");
rf1.destroy();
rf2.destroy();
});

@@ -78,7 +108,6 @@

var a = ReactiveProperty(5);
var b = ReactiveProperty();
var c = ReactiveProperty();
ReactiveFunction({
var rf1 = ReactiveFunction({
inputs: [a],

@@ -91,3 +120,3 @@ output: b,

ReactiveFunction({
var rf2 = ReactiveFunction({
inputs: [b],

@@ -102,2 +131,11 @@ output: c,

assert.equal(c(), 5);
// For serialization.
a.propertyName = "a";
b.propertyName = "b";
c.propertyName = "c";
output("abc");
rf1.destroy();
rf2.destroy();
});

@@ -113,3 +151,3 @@

ReactiveFunction({
var rf1 = ReactiveFunction({
inputs: [a],

@@ -122,3 +160,3 @@ output: c,

ReactiveFunction({
var rf2 = ReactiveFunction({
inputs: [b, c],

@@ -133,2 +171,12 @@ output: d,

assert.equal(d(), 20);
// For serialization.
a.propertyName = "a";
b.propertyName = "b";
c.propertyName = "c";
d.propertyName = "d";
output("abcd");
rf1.destroy();
rf2.destroy();
});

@@ -153,3 +201,2 @@

var a = ReactiveProperty(5);
var b = ReactiveProperty();

@@ -160,6 +207,8 @@ var c = ReactiveProperty();

ReactiveFunction({ inputs: [a], output: b, callback: function (a){ return a * 2; } });
ReactiveFunction({ inputs: [b], output: c, callback: function (b){ return b + 5; } });
ReactiveFunction({ inputs: [a], output: d, callback: function (a){ return a * 3; } });
ReactiveFunction({ inputs: [c, d], output: e, callback: function (c, d){ return c + d; } });
var rfs = [
ReactiveFunction({ inputs: [a], output: b, callback: function (a){ return a * 2; } }),
ReactiveFunction({ inputs: [b], output: c, callback: function (b){ return b + 5; } }),
ReactiveFunction({ inputs: [a], output: d, callback: function (a){ return a * 3; } }),
ReactiveFunction({ inputs: [c, d], output: e, callback: function (c, d){ return c + d; } })
];

@@ -172,2 +221,14 @@ ReactiveFunction.digest();

assert.equal(e(), ((a() * 2) + 5) + (a() * 3));
// For serialization.
a.propertyName = "a";
b.propertyName = "b";
c.propertyName = "c";
d.propertyName = "d";
e.propertyName = "e";
output("tricky-case");
rfs.forEach(function (reactiveFunction){
reactiveFunction.destroy();
});
});

@@ -180,3 +241,3 @@

ReactiveFunction({
var reactiveFunction = ReactiveFunction({
inputs: [a],

@@ -193,2 +254,4 @@ output: b,

assert.equal(numInvocations, 1);
reactiveFunction.destroy();
});

@@ -201,3 +264,3 @@

ReactiveFunction({
var reactiveFunction = ReactiveFunction({
inputs: [a, b],

@@ -212,4 +275,6 @@ output: c,

assert.equal(c(), 15);
reactiveFunction.destroy();
done();
}, 0);
});

@@ -280,3 +345,3 @@

ReactiveFunction({
var reactiveFunction = ReactiveFunction({
inputs: [a, b],

@@ -292,2 +357,4 @@ output: c,

assert.equal(numInvocations, 0);
reactiveFunction.destroy();
});

@@ -301,3 +368,3 @@

ReactiveFunction({
var reactiveFunction = ReactiveFunction({
inputs: [a, b],

@@ -313,2 +380,4 @@ output: c,

assert.equal(numInvocations, 1);
reactiveFunction.destroy();
});

@@ -320,3 +389,3 @@

ReactiveFunction({
var reactiveFunction = ReactiveFunction({
inputs: [a],

@@ -330,2 +399,4 @@ callback: function (a, b){

assert.equal(numInvocations, 1);
reactiveFunction.destroy();
});

@@ -338,3 +409,3 @@

ReactiveFunction({
var reactiveFunction = ReactiveFunction({
inputs: [a],

@@ -350,2 +421,9 @@ output: b,

assert.equal(b(), 5);
// For serialization.
a.propertyName = "a";
b.propertyName = "b";
output("ab");
reactiveFunction.destroy();
});

@@ -360,4 +438,4 @@

ReactiveFunction({ inputs: [a], output: b, callback: identity });
ReactiveFunction({ inputs: [b], output: a, callback: identity });
var rf1 = ReactiveFunction({ inputs: [a], output: b, callback: identity });
var rf2 = ReactiveFunction({ inputs: [b], output: a, callback: identity });

@@ -377,2 +455,9 @@ ReactiveFunction.digest();

// For serialization.
a.propertyName = "a";
b.propertyName = "b";
output("data-binding");
rf1.destroy();
rf2.destroy();
});

@@ -388,23 +473,26 @@

// I = V / R
ReactiveFunction({
inputs: [V, R],
output: I,
callback: function (v, r){ return v / r; }
});
var rfs = [
// V = I * R
ReactiveFunction({
inputs: [I, R],
output: V,
callback: function (i, r){ return i * r; }
});
// I = V / R
ReactiveFunction({
inputs: [V, R],
output: I,
callback: function (v, r){ return v / r; }
}),
// R = V / I
ReactiveFunction({
inputs: [V, I],
output: R,
callback: function (v, i){ return v / i; }
});
// V = I * R
ReactiveFunction({
inputs: [I, R],
output: V,
callback: function (i, r){ return i * r; }
}),
// R = V / I
ReactiveFunction({
inputs: [V, I],
output: R,
callback: function (v, i){ return v / i; }
})
];
V(9)

@@ -430,2 +518,11 @@ I(2)

// For serialization.
V.propertyName = "V";
I.propertyName = "I";
R.propertyName = "R";
output("ohms-law");
rfs.forEach(function (reactiveFunction){
reactiveFunction.destroy();
});
});

@@ -467,2 +564,3 @@

rf.destroy();
});

@@ -486,6 +584,77 @@

assert.equal(value, 15);
rf.destroy();
done();
});
});
it("Should serialize the data flow graph.", function (){
var firstName = ReactiveProperty("Jane");
var lastName = ReactiveProperty("Smith");
var fullName = ReactiveProperty();
// For serialization.
firstName.propertyName = "firstName";
lastName.propertyName = "lastName";
fullName.propertyName = "fullName";
var rf = ReactiveFunction({
inputs: [firstName, lastName],
output: fullName,
callback: function (first, last){
return first + " " + last;
}
});
var serialized = ReactiveFunction.serializeGraph();
//console.log(JSON.stringify(serialized, null, 2));
assert.equal(serialized.nodes.length, 3);
assert.equal(serialized.links.length, 2);
assert.equal(serialized.nodes[0].id, "fullName");
assert.equal(serialized.nodes[1].id, "firstName");
assert.equal(serialized.nodes[2].id, "lastName");
assert.equal(serialized.links[0].source, "firstName");
assert.equal(serialized.links[0].target, "fullName");
assert.equal(serialized.links[1].source, "lastName");
assert.equal(serialized.links[1].target, "fullName");
rf.destroy();
});
// Holding off until https://github.com/datavis-tech/graph-data-structure/issues/12
//it("Should serialize the data flow graph, falling back to property ids.", function (){
// var firstName = ReactiveProperty("Jane");
// var lastName = ReactiveProperty("Smith");
// var fullName = ReactiveProperty();
// ReactiveFunction({
// inputs: [firstName, lastName],
// output: fullName,
// callback: function (first, last){
// return first + " " + last;
// }
// });
// var serialized = ReactiveFunction.serializeGraph();
// console.log(JSON.stringify(serialized, null, 2));
// assert.equal(serialized.nodes.length, 3);
// assert.equal(serialized.links.length, 2);
// assert.equal(serialized.nodes[0].id, "fullName");
// assert.equal(serialized.nodes[1].id, "firstName");
// assert.equal(serialized.nodes[2].id, "lastName");
// assert.equal(serialized.links[0].source, "firstName");
// assert.equal(serialized.links[0].target, "fullName");
// assert.equal(serialized.links[1].source, "lastName");
// assert.equal(serialized.links[1].target, "fullName");
//});
});
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc