Skip to content

Commit

Permalink
init
Browse files Browse the repository at this point in the history
  • Loading branch information
aldonline committed Jun 30, 2013
0 parents commit df6e658
Show file tree
Hide file tree
Showing 17 changed files with 431 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .gitignore
@@ -0,0 +1,4 @@
/lib/*.js
/node_modules
.DS_Store
npm-debug.log
7 changes: 7 additions & 0 deletions .npmignore
@@ -0,0 +1,7 @@
/lib/*.coffee
.gitignore
/Makefile
/test
/node_modules
.DS_Store
npm-debug.log
11 changes: 11 additions & 0 deletions Makefile
@@ -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)
98 changes: 98 additions & 0 deletions README.md
@@ -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.


49 changes: 49 additions & 0 deletions lib/core.coffee
@@ -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 }
40 changes: 40 additions & 0 deletions lib/index.coffee
@@ -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
42 changes: 42 additions & 0 deletions lib/util.coffee
@@ -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 }
26 changes: 26 additions & 0 deletions package.json
@@ -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"
}
}
15 changes: 15 additions & 0 deletions test/api.coffee
@@ -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'
6 changes: 6 additions & 0 deletions test/evaluating_a_reactive_error.coffee
@@ -0,0 +1,6 @@






Empty file.
20 changes: 20 additions & 0 deletions test/evaluating_a_reactive_result.coffee
@@ -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
29 changes: 29 additions & 0 deletions test/evaluating_a_reactive_result_that_changes.coffee
@@ -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
27 changes: 27 additions & 0 deletions test/evaluating_a_static_error.coffee
@@ -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

0 comments on commit df6e658

Please sign in to comment.