Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit df6e658
Showing
17 changed files
with
431 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
/lib/*.js | ||
/node_modules | ||
.DS_Store | ||
npm-debug.log |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
/lib/*.coffee | ||
.gitignore | ||
/Makefile | ||
/test | ||
/node_modules | ||
.DS_Store | ||
npm-debug.log |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
MOCHA_OPTS= -w --check-leaks --compilers coffee:coffee-script | ||
REPORTER = spec | ||
|
||
check: test | ||
|
||
test: test-unit | ||
|
||
test-unit: | ||
@NODE_ENV=test ./node_modules/.bin/mocha \ | ||
--reporter $(REPORTER) \ | ||
$(MOCHA_OPTS) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
# Native Reactivity for Javascript | ||
|
||
![Native Reactivity for Javascript logo](https://raw.github.com/aldonline/njsr/master/etc/nrjs-250.png) | ||
|
||
Native Reactivity for Javascript ( aka NR.js ) is a micro-library that allows native functions and expressions in Javascript | ||
to become Natively Reactive. Which is a fancy way of saying that they can notify other functions their return value changes. | ||
|
||
The technique is so simple that it "blends into" the language. There is no need to create | ||
"Observable" objects, Event Emitters or evaluate expressions in a sublanguage. | ||
Any Javascript function can be Natively Reactive. And any function that depends on a Natively Reactive function becomes Natively Reactive automatically. | ||
|
||
There is no need to explicitly define dependency relations between functions. | ||
|
||
# Installation | ||
|
||
## NPM | ||
|
||
Install via NPM | ||
|
||
npm install nr | ||
|
||
```javascript | ||
var NR = require('nr') | ||
``` | ||
|
||
## Browser ( TODO ) | ||
|
||
Include the following JS file | ||
|
||
```html | ||
<script src="https://raw.github.com/aldonline/nrjs/master/dist/nr.min.js"></script> | ||
``` | ||
|
||
The global NR object is attached to the root scope ( window ) | ||
|
||
var NR = window.NR | ||
|
||
# Overview | ||
|
||
# Creating a Natively Reactive Function | ||
|
||
Functions throw an invalidation event up the stack. | ||
However, because the stack is transient and won't exist in the future, functions | ||
that wish to notify a change need to request a callback so they can | ||
throw the event in the future. | ||
|
||
```javascript | ||
function time(){ | ||
var notifier = NR() // request a notifier | ||
setTimeout( notifier, 1000 ) // call it in 1000MS | ||
return new Date().getTime() | ||
} | ||
``` | ||
|
||
The caller of the function, on the other hand, will have an opportunity to register | ||
a listener to be notified when any of the functions that participated in the evaluation are invalidated. | ||
At this point you can decide to re-evaluate the expression. | ||
|
||
# Consuming a Natively Reactive Function | ||
|
||
```javascript | ||
var r = NR( time ) // run the function in a native reactive context | ||
var time = r.result | ||
var notifier = r.notifier | ||
if ( notifier != null ){ // at least one notifier was registered downstream | ||
notifier.add( function(){ | ||
console.log( "we should reevaluate the expression" ) | ||
}) | ||
} | ||
``` | ||
|
||
# FAQ | ||
|
||
## Why do we need a "Standard" library? | ||
|
||
In order to be able to combine libraries developed by different people at different | ||
times we need to agree on a common way of notifying changes up the stack. | ||
|
||
This means ( at the least ) sharing a global object where invalidators and notifiers meet each other. | ||
NR.js provides a microlibrary and a set of conventions. | ||
If we all follow them, Javascript automatically becomes a MUCH more powerful language. | ||
|
||
## Why does't the Invalidation event provide me with a way to inspect previous and current values? | ||
|
||
Because an expression may depende on several reactive functions, | ||
the Invalidation event you catch on the top of the stack may come from any of them. | ||
The value of this specific function is not important. | ||
What's important is the result of evaluating the complete expression. | ||
|
||
## Where does this idea come from? | ||
|
||
Like all good ideas and patterns in software they have been discoverd and rediscovered over and over again by venturous programmers. The author of this library can attest that its first encounter with this pattern was as part of a strategy to invalidate caches when calling complex stored procedures probably 15 years ago. The implementation details were a bit different but the principle was the same. Naturally, this idea was ported to Javascript over a decade ago. | ||
|
||
Lately it has popped up in several places. The most notable of them all is probably the Meteor.js framework, where it is tightly coupled with the framework. | ||
|
||
Just search for "reactive" on NPM. | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
module.exports = -> | ||
|
||
stack = [] | ||
get_current = -> stack[stack.length - 1] | ||
|
||
construct_notifier = -> | ||
fired = no | ||
listeners = null | ||
notify: -> | ||
unless fired then fired = yes | ||
c() for c in listeners | ||
listener = null | ||
public: | ||
fired: -> fired | ||
notify: (f) -> ( listeners ?= [] ).push f | ||
|
||
class Result | ||
constructor: ( { @error, @result, @notifier } ) -> | ||
|
||
class Evaluation | ||
constructor : ( @func ) -> | ||
@n = undefined # lazy | ||
run : -> | ||
try | ||
new Result | ||
result: @func() | ||
notifier: @n?.public | ||
catch e | ||
new Result | ||
error: e | ||
notifier: @n?.public | ||
finally | ||
delete @func | ||
delete @n | ||
notifier : -> do ( n = @n ?= construct_notifier() ) -> -> n.notify() | ||
|
||
run = ( f ) -> | ||
try | ||
stack.push ev = new Evaluation f | ||
ev.run() | ||
finally | ||
stack.pop() | ||
|
||
# creates an expiration callback for the current Evaluation | ||
# if there is no ongoing Evaluation it returns null | ||
notifier = -> get_current()?.notifier() | ||
active = -> stack.length isnt 0 | ||
|
||
{ notifier, active, run } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
core = require './core' | ||
util = require './util' | ||
|
||
build = -> | ||
{notifier, active, run} = core() | ||
{subscribe, poll} = util() | ||
|
||
### | ||
m() = notifier() | ||
m( func ) = run(func) | ||
m( func, func ) = subscribe( func, func ) | ||
m( func, interval ) = poll( func, interval ) | ||
# more convenient | ||
m( interval, func ) = poll( func, interval ) | ||
### | ||
main = ( x, y ) -> | ||
switch typeof x + ' ' + typeof y | ||
when 'undefined undefined' then notifier() | ||
when 'function undefined' then run x | ||
when 'function function' then subscribe x, y | ||
when 'function number' then poll x, y | ||
when 'number function' then poll y, x | ||
else throw new Error 'Invalid Arguments' | ||
|
||
main.notifier = notifier | ||
main.active = active | ||
main.run = run | ||
main.subscribe = subscribe | ||
main.poll = poll | ||
|
||
main | ||
|
||
# only one module can exist per execution environment | ||
# otherwise we would not be able to share the stack | ||
module.exports = ( global or window ).NR ?= build() # lazily build module |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
core = require './core' | ||
|
||
module.exports = -> | ||
|
||
{notifier, active, run} = core() | ||
|
||
EQ = (a, b) -> a is b | ||
delay = -> setTimeout arguments[1], arguments[0] | ||
build_compare = ( eq ) -> ( out1, out2 ) -> eq( out1.result, out2.result ) and eq( out1.error, out2.error ) | ||
|
||
poll = ( f, interval = 100, eq = EQ ) -> | ||
run_ = -> do (args = arguments) -> run f.apply null, args | ||
compare = build_compare eq | ||
-> | ||
{result, error, notifier} = out = run_() | ||
if notifier? | ||
# thank god. | ||
# We can forget about polling and use the notifier directly | ||
# let's bubble it up | ||
notifier.notify notifier() | ||
else | ||
# request a new notifier and start polling | ||
# TODO: listen if notifier gets destroyed and stop polling | ||
n = notifier() | ||
do iter = -> # loop and test for equality | ||
delay interval, -> | ||
if compare out, run_() then iter() else n() | ||
# unbox response | ||
throw out.error if out.error? | ||
out.result | ||
|
||
subscribe = ( func, cb ) -> | ||
stopped = no | ||
stopper = -> stopped = yes | ||
do iter = -> | ||
unless stopped | ||
r = run func | ||
cb? r.error, r.result, r.notifier, stopper | ||
r.notifier?.notify iter | ||
stopper | ||
|
||
{ poll, subscribe } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
{ | ||
"name": "reactivity", | ||
"version": "0.0.0", | ||
"description": "Native Reactivity for Javascript", | ||
"author": "Aldo Bucchi <aldo.bucchi@gmail.com>", | ||
"main": "lib", | ||
"scripts": { | ||
"test": "make test", | ||
"build": "coffee -c lib/", | ||
"clean": "rm lib/*.js", | ||
"prepublish": "npm run build", | ||
"postpublish": "npm run clean" | ||
}, | ||
"devDependencies": { | ||
"mocha": "~1.9.0", | ||
"chai": "~1.5.0", | ||
"coffee-script": "~1.6.3" | ||
}, | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/aldonline/reactivity.js.git" | ||
}, | ||
"bugs": { | ||
"url": "https://github.com/aldonline/reactivity.js/issues" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
chai = require 'chai' | ||
should = chai.should() | ||
|
||
X = require '../lib' | ||
|
||
describe 'The module object', -> | ||
it 'should only have five methods: notifier, run, active, poll, subscribe', -> | ||
X.should.have.keys 'notifier run active poll subscribe'.split ' ' | ||
X.notifier.should.be.a 'function' | ||
X.run.should.be.a 'function' | ||
X.active.should.be.a 'function' | ||
X.poll.should.be.a 'function' | ||
X.subscribe.should.be.a 'function' | ||
it 'should be a function itself', -> | ||
X.should.be.a 'function' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
|
||
|
||
|
||
|
||
|
||
|
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
chai = require 'chai' | ||
should = chai.should() | ||
|
||
mock = require './util/expiring_mock' | ||
X = require '../lib' | ||
|
||
describe 'in a simple evaluation', -> | ||
f = mock.create() | ||
r = X.run f | ||
describe 'result.result', -> | ||
it 'should be a valid mock result', -> | ||
mock.test_result r.result | ||
describe 'result.error', -> | ||
it 'should be undefined', -> | ||
should.equal r.error, undefined | ||
describe 'result.notifier', -> | ||
it 'should be an object', -> | ||
r.notifier.should.be.a 'object' | ||
it 'should not be fired yet', -> | ||
r.notifier.fired().should.equal false |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
chai = require 'chai' | ||
should = chai.should() | ||
|
||
mock = require './util/expiring_mock' | ||
X = require '../lib' | ||
|
||
describe 'in an evaluation that changes', -> | ||
|
||
f = mock.create() | ||
r = X.run f | ||
|
||
{result, notifier} = r | ||
fired = no | ||
notifier.notify -> fired = yes | ||
[flag, ex] = result | ||
|
||
describe 'the first result we obtain', -> | ||
it 'must be true', -> flag.should.equal true | ||
|
||
describe 'but after making a change', -> | ||
ex() | ||
describe 'the notifier', -> | ||
it 'should have fired once', -> | ||
fired.should.equal true | ||
|
||
describe 'and the new result' ,-> | ||
new_result = f()[0] | ||
it 'must be false', -> | ||
new_result.should.equal false |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
chai = require 'chai' | ||
should = chai.should() | ||
|
||
mock = require './util/expiring_mock' | ||
X = require '../lib' | ||
|
||
describe 'while evaluating a static error', -> | ||
f = -> | ||
X.notifier() | ||
throw new Error 'E' | ||
|
||
r = X.run f | ||
|
||
describe 'result.result', -> | ||
it 'should be undefined', -> | ||
should.not.exist r.result | ||
|
||
describe 'result.error', -> | ||
it 'should be a valid error', -> | ||
r.error.should.be.an.instanceOf Error | ||
|
||
describe 'result.notifier', -> | ||
it 'should be an object', -> | ||
r.notifier.should.be.a 'object' | ||
|
||
it 'should not be fired yet', -> | ||
r.notifier.fired().should.equal false |
Oops, something went wrong.