Compare commits
2 Commits
ea29882fae
...
278ec30827
| Author | SHA1 | Date | |
|---|---|---|---|
| 278ec30827 | |||
| 52d8c44a22 |
34
.gitignore
vendored
34
.gitignore
vendored
@ -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
|
|
||||||
|
|||||||
@ -1,3 +0,0 @@
|
|||||||
package.json
|
|
||||||
package-lock.json
|
|
||||||
|
|
||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
9216
package-lock.json
generated
9216
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
34
package.json
34
package.json
@ -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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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'
|
|
||||||
};
|
|
||||||
@ -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' })]
|
|
||||||
}
|
|
||||||
];
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"spec_dir": "spec",
|
|
||||||
"spec_files": ["**/*[sS]pec.js"],
|
|
||||||
"helpers": ["helpers/**/*.js"],
|
|
||||||
"stopSpecOnExpectationFailure": false,
|
|
||||||
"random": true
|
|
||||||
}
|
|
||||||
@ -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;
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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]'));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@ -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();
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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
30
src/subscribable.js
Normal 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
17
src/util.js
17
src/util.js
@ -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);
|
|
||||||
};
|
|
||||||
|
|||||||
Reference in New Issue
Block a user