Compare commits

..

2 Commits

18 changed files with 5665 additions and 3851 deletions

34
.gitignore vendored
View File

@ -1,32 +1,2 @@
# ---> Cloud9 coverage
# Cloud9 IDE - http://c9.io node_modules
.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

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 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 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 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. reactive method improves code readability and reduces the tendency toward spaghetti code.
Reactimal provides the following reactive programming primitives. Each type of primitive is 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 [subscribable](./docs/subscribable.md) and provides a single output value (or promise), but differ
receive their input. in how they receive their input.
## Input Primitives ## Input Primitives

9232
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -2,19 +2,12 @@
"name": "reactimal", "name": "reactimal",
"version": "1.0.0", "version": "1.0.0",
"description": "Reactive programming primitives", "description": "Reactive programming primitives",
"main": "dist/reactimal.js", "main": "src/index.js",
"module": "dist/reactimal.mjs",
"files": [ "files": [
"src" "src"
], ],
"scripts": { "scripts": {
"test": "jasmine", "test": "karmatic"
"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"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@ -26,20 +19,13 @@
"author": "Timothy Farrell <tim@thecookiejar.me> (https://github.com/explorigin)", "author": "Timothy Farrell <tim@thecookiejar.me> (https://github.com/explorigin)",
"license": "Apache-2.0", "license": "Apache-2.0",
"devDependencies": { "devDependencies": {
"jasmine": "^3.2.0", "husky": "^3.0.1",
"jasmine-es6": "^0.4.3", "karmatic": "^1.3.1",
"prettier": "^1.14.3", "webpack": "^4.37.0"
"babel-cli": "6.18.0", },
"babel-core": "6.21.0", "husky": {
"babel-eslint": "7.1.1", "hooks": {
"babel-preset-es2015": "6.18.0", "pre-commit": "npm run test"
"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"
} }
} }

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

View File

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

View File

@ -137,4 +137,21 @@ describe('A container', () => {
a.add(1); a.add(1);
expect(order).toEqual('abc'); 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 => { export const hashableProperty = hash => store => {
let subscribers = []; let subscribers = [];
@ -16,9 +18,8 @@ export const hashableProperty = hash => store => {
} }
return store; return store;
}; };
accessor.subscribe = registerSubscriptions(subscribers); // Add child nodes to the logic graph (value-based)
accessor.unsubscribeAll = () => (subscribers = []); Object.assign(accessor, subscribable());
accessor._fire = registerFire(subscribers);
accessor._lock = () => { accessor._lock = () => {
lockCount += 1; lockCount += 1;
return accessor(); 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 = []) => { export const hashableStream = hash => (fn, dependencies = []) => {
let subscribers = []; let subscribers = [];
@ -37,29 +39,23 @@ export const hashableStream = hash => (fn, dependencies = []) => {
}; };
// Add child nodes to the logic graph (value-based) // Add child nodes to the logic graph (value-based)
accessor.subscribe = registerSubscriptions(subscribers); Object.assign(accessor, subscribable());
accessor._fire = registerFire(subscribers);
// Receive dirty flag from parent logic node (dependency). Pass it down. // Receive dirty flag from parent logic node (dependency). Pass it down.
accessor._setDirty = function setDirty() { accessor._setDirty = function setDirty() {
if (!isDirty) { if (!isDirty) {
isDirty = true; 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 // Remove this node from the logic graph completely
accessor.detach = () => { accessor.detach = () => {
subscribers = []; accessor.unsubscribeAll();
dependentSubscriptions.forEach(call); dependentSubscriptions.forEach(call);
}; };
// Remove child nodes from the logic graph
accessor.unsubscribeAll = () => {
subscribers = [];
};
const dependentSubscriptions = dependencies.map(d => d.subscribe(accessor._setDirty)); const dependentSubscriptions = dependencies.map(d => d.subscribe(accessor._setDirty));
return accessor; return accessor;

View File

@ -155,4 +155,28 @@ describe('A stream', () => {
await aDep(); await aDep();
expect(aCount).toBe(2); 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 pick = (id, def) => doc => (doc && doc.hasOwnProperty(id) ? doc[id] : def);
export const call = a => (typeof a === 'function' ? a() : a); 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);
};