0.2.2 • Published 7 years ago

fri v0.2.2

Weekly downloads
2
License
GPL-3.0
Repository
github
Last release
7 years ago

website github codeclimate travis npm

Functional Reactive Immutable library

This is a utility library intended for Functional Reactive Programming.

The idea for the library is inspired from experience with re-frame, and the goal is to have:

  • reactions - similar to reagent
  • simple immutable JSON datastructure, like immutable-js but simpler / more lightweight, and optimised for reagent-like reactions

We are not there yet... The current initial version is just an atomic state, where you can subscribe to changes. Not functional reactive immutable yet :( This is the minimal needed for the current applications that I am writing.

Initialisation

This builds upon direape for message passing, and unit testing.

var da = require('direape');
var fri = module.exports; da.testSuite('fri');

Use immutable-js for the time being, - on then long term, I want to replace this by a more lightweight version, optimised for reactive usage.

var immutable = require('immutable');

State, is similar to a single application state atom in clojurescript apps.

var state, dirtyState, stateAccessed; 
var subscribers, eventSubscribers;

API

getJS(path, defaultValue)

fri.getJS = (path, defaultValue) => {
  path = toPath(path);
  stateAccessed.add(path);
  return toJS(state.getIn(path, defaultValue));
};

da.test('getJS', () => 
    da.assertEquals(fri.getJS('undefined', 123), 123));

setJS(path, value)

fri.setJS = (path, value) => {
  path = toPath(path);
  dirtyState.add(path);
  state = setIn(state, path, value);
  requestUpdate();
};

da.test('setJS+getJS', () => {
  fri.setJS(['foo', 2, 'bar'], 'hello');
  fri.setJS(['bar'], {quux: 123});
  fri.setJS('baz', 345);
  fri.setJS('qux.bar', 7);
  da.assertEquals(fri.getJS('bar'), {quux: 123});
  da.assertEquals(fri.getJS(['qux', 'bar']), 7);
  da.assertEquals(fri.getJS(['baz']), 345);
  da.assertEquals(fri.getJS(['foo', 2]), {bar: 'hello'});
});

rerun(name, fn)

fri.rerun = (name, fn) => {
  if(fn) {
    stateAccessed.clear();
    var subscriber = { fn: fn };
    updateSubscriber(subscriber);
    subscribers.set(name, subscriber);
  } else {
    subscribers.delete(name);
  }
};

da.test('rerun', () => new Promise((resolve, reject) => {
  var i = 0;

  da.nextTick(() => fri.setJS(['rerun-test'], 123));
  setTimeout(() => fri.setJS(['unaffect'], 789), 200);
  setTimeout(() => fri.setJS(['rerun-test'], 456), 400);

  fri.rerun('rerun-test', () => {
    ++i;
    da.assert(i !== 1 || 
        fri.getJS('rerun-test') === undefined);
    da.assert(i !== 2 || 
        fri.getJS('rerun-test') === 123);
    da.assert(i !== 3 || 
        fri.getJS('rerun-test') === 456);
    if(i === 3) {
      resolve();
    }
  });
}));

Handlers

fri:get (path, defaultValue)

da.handle('fri:get', fri.getJS);

fri:set (path, value)

da.handle('fri:set', fri.setJS);

fri:subscribe (pid, name, path)

da.handle('fri:subscribe', (pid, name, path) =>
    da.jsonify(fri.rerun(
        `fri:subscribe ${path} -> ${name}@${pid}`,
        () => da.emit(pid, name, path, fri.getJS(path)))));

fri:unsubscribe (pid, name, path)

da.handle('fri:unsubscribe', (pid, name, path) =>
    fri.rerun(`fri:subscribe ${path} -> ${name}@${pid}`));

Unit testing

da.test('handle subscribe/unsubscribe', 
    () => new Promise((resolve, reject) => {
      var i = 0;
      da.handle('fri:test:subscribe', (path, data) => {
        ++i;
        try {
          da.assertEquals(path, 'test-sub');
          if(i === 1) {
            da.assertEquals(data, undefined);
          } else if(i === 2) {
            da.assertEquals(data, 'hello'); 
          } else {
            da.assert(false);
          }
        } catch(e) {
          reject(e);
        }
      });
      da.emit(da.pid, 'fri:subscribe', 
          da.pid, 'fri:test:subscribe', 'test-sub');
      setTimeout(() => fri.setJS('test-sub', 'hello'), 100);
      setTimeout(() => da.emit(da.pid, 'fri:unsubscribe', 
            da.pid, 'fri:test:subscribe', 'test-sub'), 150); 
      setTimeout(() => fri.setJS('test-sub', 'arvh'), 200);
      setTimeout(() => { 
        da.assertEquals(i, 2); 
        resolve(); 
      }, 400);
    }));

Implementation details

state = new immutable.Map();
dirtyState = new Set();
stateAccessed = new Set();
subscribers = new Map();
eventSubscribers = new Map();

requestUpdate

var updateRequested = false;
var lastUpdate = Date.now();

function requestUpdate() {
  if(!updateRequested) {
    setTimeout(updateSubscribers, 
        Math.max(0, 1000/60 - (Date.now() - lastUpdate)));
    updateRequested = true;
  }
}

updateSubscribers

function updateSubscribers() {
  lastUpdate = Date.now();
  updateRequested = false;
  var accessMap = new immutable.Map();
  for(var path of dirtyState) {
    accessMap = setIn(accessMap, path, true);
  }
  dirtyState.clear();

  for(var subscriber of subscribers.values()) {
    var needsUpdate = false;
    for(path of subscriber.accessed) {
      if(accessMap.getIn(path)) {
        needsUpdate = true;
        break;
      }
    }
    if(needsUpdate) {
      updateSubscriber(subscriber);
    }
  }
}

updateSubscriber

function updateSubscriber(subscriber) {
  stateAccessed.clear();
  subscriber.fn();
  subscriber.accessed = Array.from(stateAccessed);
}

setIn

function setIn(o, path, value) {
  if(path.length) {
    var key = path[0];
    var rest = path.slice(1);
    if(!o) {
      if(typeof key === 'number') {
        o = new immutable.List();
      } else {
        o = new immutable.Map();
      }
    }
    return o.set(key, setIn(o.get(path[0]), 
          path.slice(1), value));
  } else {
    return immutable.fromJS(value);
  }
}

toJS

function toJS(o) {
  if(typeof o === 'object' && o !== null && 
      typeof o.toJS === 'function') {
    o = o.toJS();
  }
  return o;
}

toPath

function toPath(arr) {
  if(typeof arr === 'string') {
    arr = arr.split('.');
  } else if(!Array.isArray(arr)) {
    arr = [arr];
  }
  return arr;
}

Main

if(require.main === module) {
  da.ready(() => 
      da.runTests('fri')
      .then(() => da.isNodeJs() && process.exit(0))
      .catch(() => da.isNodeJs() && process.exit(-1)));
}