Compare commits
2 Commits
ea29882fae
...
278ec30827
| Author | SHA1 | Date | |
|---|---|---|---|
| 278ec30827 | |||
| 52d8c44a22 |
34
.gitignore
vendored
34
.gitignore
vendored
@ -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
|
||||
|
||||
@ -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
|
||||
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
9218
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",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 = []) => {
|
||||
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;
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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]'));
|
||||
});
|
||||
});
|
||||
@ -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();
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
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 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