Compare commits

..

2 Commits

18 changed files with 5665 additions and 3851 deletions

34
.gitignore vendored
View File

@ -1,32 +1,2 @@
# ---> Cloud9
# Cloud9 IDE - http://c9.io
.c9revisions
.c9
# ---> Node
# Logs
logs
*.log
npm-debug.log*
lerna-debug.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Dependency directories
node_modules/
dist/
# Optional npm cache directory
.npm
# ---> SublimeText
# workspace files are user-specific
*.sublime-workspace
# project files should be checked into the repository, unless a significant
# proportion of contributors will probably not be using SublimeText
# *.sublime-project
coverage
node_modules

View File

@ -1,3 +0,0 @@
package.json
package-lock.json

View File

@ -2,13 +2,13 @@
Reactimal provides a set of primitives that can be used to express your code as a logic graph using
reactive programming methods. Reactive programming inverts the dependency relationship between
functions and their parameters. No longer do callers need to know the signature of the functions
functions and their inputs. No longer do callers need to know the signature of the functions
they call. Instead computed values subscribe to scalar properties or other computed values. The
reactive method improves code readability and reduces the tendency toward spaghetti code.
Reactimal provides the following reactive programming primitives. Each type of primitive is
[subscribable](./docs/subscribable.md) and provides a single output value, but differ in how they
receive their input.
[subscribable](./docs/subscribable.md) and provides a single output value (or promise), but differ
in how they receive their input.
## Input Primitives

9218
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -2,19 +2,12 @@
"name": "reactimal",
"version": "1.0.0",
"description": "Reactive programming primitives",
"main": "dist/reactimal.js",
"module": "dist/reactimal.mjs",
"main": "src/index.js",
"files": [
"src"
],
"scripts": {
"test": "jasmine",
"clean": "rimraf dist",
"format_code": "nodejs node_modules/prettier/bin-prettier.js --config ./prettier.config.js --write \"{.,{src,spec,docs}/**}/*.{js,json,md}\"",
"build:umd": "rollup -c rollup.config.js && uglifyjs -m --screw-ie8 -c -o dist/reactimal.min.js dist/reactimal.js",
"build:umd:zip": "npm run build:umd && gzip -c9 dist/reactimal.min.js > dist/reactimal.min.js.gz && gzip -c9 dist/reactimal.min.mjs > dist/reactimal.min.mjs.gz",
"build": "npm run build:umd:zip && ls -l dist/",
"prepublish": "npm run clean && npm run build"
"test": "karmatic"
},
"repository": {
"type": "git",
@ -26,20 +19,13 @@
"author": "Timothy Farrell <tim@thecookiejar.me> (https://github.com/explorigin)",
"license": "Apache-2.0",
"devDependencies": {
"jasmine": "^3.2.0",
"jasmine-es6": "^0.4.3",
"prettier": "^1.14.3",
"babel-cli": "6.18.0",
"babel-core": "6.21.0",
"babel-eslint": "7.1.1",
"babel-preset-es2015": "6.18.0",
"babel-preset-es2015-rollup": "3.0.0",
"babel-preset-stage-0": "6.16.0",
"eslint": "3.2.0",
"rimraf": "2.5.4",
"rollup": "0.66.6",
"rollup-plugin-babel": "2.7.1",
"rollup-plugin-esmin": "0.1.3",
"uglify-es": "3.3.9"
"husky": "^3.0.1",
"karmatic": "^1.3.1",
"webpack": "^4.37.0"
},
"husky": {
"hooks": {
"pre-commit": "npm run test"
}
}
}

View File

@ -1,12 +0,0 @@
module.exports = {
printWidth: 100,
tabWidth: 1,
useTabs: true,
singleQuote: true,
trailingComma: 'none',
bracketSpacing: true,
semi: true,
requirePragma: false,
proseWrap: 'always',
arrowParens: 'avoid'
};

View File

@ -1,43 +0,0 @@
import esmin from 'rollup-plugin-esmin';
import babel from 'rollup-plugin-babel';
import pkg from './package.json';
const config = {
input: 'src/index.js',
output: {
file: pkg.module,
format: 'es'
}
};
export default [
// reactimal.mjs
config,
// reactimal.min.mjs
{
...config,
output: {
...config.output,
file: 'dist/reactimal.min.mjs'
},
plugins: [
esmin({
overrides: {
sourceType: 'module'
}
})
]
},
// reactimal.js
{
...config,
output: {
format: 'umd',
file: 'dist/reactimal.js',
name: 'reactimal'
},
plugins: [babel({ sourceType: 'module' })]
}
];

View File

@ -1,7 +0,0 @@
{
"spec_dir": "spec",
"spec_files": ["**/*[sS]pec.js"],
"helpers": ["helpers/**/*.js"],
"stopSpecOnExpectationFailure": false,
"random": true
}

View File

@ -1,7 +1,8 @@
import { id, registerFire, registerSubscriptions, call } from './util.js';
import { id, call } from './util.js';
import { subscribable } from './subscribable.js';
export const hashableComputed = hash => (fn, dependencies = []) => {
let subscribers = [];
let isDirty = true;
let val;
let oldId;
@ -25,29 +26,23 @@ export const hashableComputed = hash => (fn, dependencies = []) => {
};
// Add child nodes to the logic graph (value-based)
accessor.subscribe = registerSubscriptions(subscribers);
accessor._fire = registerFire(subscribers);
Object.assign(accessor, subscribable());
// Receive dirty flag from parent logic node (dependency). Pass it down.
accessor._setDirty = function setDirty() {
if (!isDirty) {
isDirty = true;
subscribers.forEach(s => s._setDirty && s._setDirty());
accessor._forEachSubscription(s => s._setDirty && s._setDirty());
}
return subscribers.length && accessor;
return accessor._hasSubscribers() && accessor;
};
// Remove this node from the logic graph completely
accessor.detach = () => {
subscribers = [];
accessor.unsubscribeAll();
dependentSubscriptions.forEach(call);
};
// Remove child nodes from the logic graph
accessor.unsubscribeAll = () => {
subscribers = [];
};
const dependentSubscriptions = dependencies.map(d => d.subscribe(accessor._setDirty));
return accessor;

View File

@ -1,16 +1,10 @@
import { registerSubscriptions, registerFire } from './util.js';
import { subscribable } from './subscribable.js';
export const hashableContainer = hash => store => {
let subscribers = [];
let id = hash && hash(store);
let lockCount = 0;
const containerMethods = {
subscribe: registerSubscriptions(subscribers),
_fire: registerFire(subscribers),
unsubscribeAll: () => {
subscribers = [];
},
_lock: () => {
lockCount += 1;
return p;
@ -19,7 +13,8 @@ export const hashableContainer = hash => store => {
if (lockCount && --lockCount === 0) {
checkUpdate(store);
}
}
},
...subscribable()
};
function checkUpdate(target) {

View File

@ -137,4 +137,21 @@ describe('A container', () => {
a.add(1);
expect(order).toEqual('abc');
});
it('unsubscribeAll clears subscriptions', () => {
let order = '';
const a = containedSet(new Set());
a.subscribe(() => (order += 'a'));
a.subscribe(() => (order += 'b'));
a.subscribe(() => (order += 'c'));
a.unsubscribeAll()
a.add(1);
expect(order).toEqual('');
});
it('protects method properties.', () => {
const a = containedSet(new Set());
expect(() => a.subscribe = 'test').toThrow(new ReferenceError('Cannot set subscribe in [object Set]'));
});
});

View File

@ -1,4 +1,6 @@
import { id, registerSubscriptions, registerFire } from './util.js';
import { id } from './util.js';
import { subscribable } from './subscribable.js';
export const hashableProperty = hash => store => {
let subscribers = [];
@ -16,9 +18,8 @@ export const hashableProperty = hash => store => {
}
return store;
};
accessor.subscribe = registerSubscriptions(subscribers);
accessor.unsubscribeAll = () => (subscribers = []);
accessor._fire = registerFire(subscribers);
// Add child nodes to the logic graph (value-based)
Object.assign(accessor, subscribable());
accessor._lock = () => {
lockCount += 1;
return accessor();

View File

@ -1,4 +1,6 @@
import { id, registerFire, registerSubscriptions, call } from './util.js';
import { id, call } from './util.js';
import { subscribable } from './subscribable.js';
export const hashableStream = hash => (fn, dependencies = []) => {
let subscribers = [];
@ -37,29 +39,23 @@ export const hashableStream = hash => (fn, dependencies = []) => {
};
// Add child nodes to the logic graph (value-based)
accessor.subscribe = registerSubscriptions(subscribers);
accessor._fire = registerFire(subscribers);
Object.assign(accessor, subscribable());
// Receive dirty flag from parent logic node (dependency). Pass it down.
accessor._setDirty = function setDirty() {
if (!isDirty) {
isDirty = true;
subscribers.forEach(s => s._setDirty && s._setDirty());
accessor._forEachSubscription(s => s._setDirty && s._setDirty());
}
return subscribers.length && accessor;
return accessor._hasSubscribers() && accessor;
};
// Remove this node from the logic graph completely
accessor.detach = () => {
subscribers = [];
accessor.unsubscribeAll();
dependentSubscriptions.forEach(call);
};
// Remove child nodes from the logic graph
accessor.unsubscribeAll = () => {
subscribers = [];
};
const dependentSubscriptions = dependencies.map(d => d.subscribe(accessor._setDirty));
return accessor;

View File

@ -155,4 +155,28 @@ describe('A stream', () => {
await aDep();
expect(aCount).toBe(2);
});
it('detach breaks the logic chain', async () => {
const a = prop(0);
const b = computed(square, [a]);
const c = stream(delaySquare, [a]);
const d = stream(delayAdd, [a, c]);
const e = stream(delaySquare, [b]);
expect(await c()).toEqual(0);
expect(await d()).toEqual(0);
expect(await e()).toEqual(0);
a(1);
expect(await c()).toEqual(1);
expect(await d()).toEqual(2);
expect(await e()).toEqual(1);
c.detach();
a(2);
expect(await c()).toEqual(1);
expect(await d()).toEqual(3);
expect(await e()).toEqual(16);
});
});

30
src/subscribable.js Normal file
View File

@ -0,0 +1,30 @@
import { call } from './util.js';
export const subscribable = () => {
const subscriptions = [];
return {
subscribe(fn) {
subscriptions.push(fn);
return () => {
const idx = subscriptions.indexOf(fn);
if (idx !== -1) {
subscriptions.splice(idx, 1);
}
return subscriptions.length;
};
},
unsubscribeAll() {
subscriptions.splice(0, Infinity);
},
_fire(val) {
return subscriptions.map(s => s(val)).forEach(call);
},
_forEachSubscription(fn) {
subscriptions.forEach(fn);
},
_hasSubscribers() {
return subscriptions.length > 0;
}
};
}

View File

@ -3,20 +3,3 @@ export const id = a => a;
export const pick = (id, def) => doc => (doc && doc.hasOwnProperty(id) ? doc[id] : def);
export const call = a => (typeof a === 'function' ? a() : a);
// internal utilities
export const registerSubscriptions = subscriptionsArray => fn => {
subscriptionsArray.push(fn);
return () => {
const idx = subscriptionsArray.indexOf(fn);
if (idx !== -1) {
subscriptionsArray.splice(idx, 1);
}
return subscriptionsArray.length;
};
};
export const registerFire = subscriptionsArray => val => {
subscriptionsArray.map(s => s(val)).forEach(call);
};