Compare commits
No commits in common. "278ec30827272134dcb8dfde34051b8d0cf6063b" and "ea29882faeec9d9f4c737e1506204d77a8984c29" have entirely different histories.
278ec30827
...
ea29882fae
34
.gitignore
vendored
34
.gitignore
vendored
@ -1,2 +1,32 @@
|
|||||||
coverage
|
# ---> Cloud9
|
||||||
node_modules
|
# 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
|
||||||
|
|||||||
3
.prettierignore
Normal file
3
.prettierignore
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
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 inputs. No longer do callers need to know the signature of the functions
|
functions and their parameters. 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 (or promise), but differ
|
[subscribable](./docs/subscribable.md) and provides a single output value, but differ in how they
|
||||||
in how they receive their input.
|
receive their input.
|
||||||
|
|
||||||
## Input Primitives
|
## Input Primitives
|
||||||
|
|
||||||
|
|||||||
8996
package-lock.json
generated
8996
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
34
package.json
34
package.json
@ -2,12 +2,19 @@
|
|||||||
"name": "reactimal",
|
"name": "reactimal",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Reactive programming primitives",
|
"description": "Reactive programming primitives",
|
||||||
"main": "src/index.js",
|
"main": "dist/reactimal.js",
|
||||||
|
"module": "dist/reactimal.mjs",
|
||||||
"files": [
|
"files": [
|
||||||
"src"
|
"src"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "karmatic"
|
"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"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@ -19,13 +26,20 @@
|
|||||||
"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": {
|
||||||
"husky": "^3.0.1",
|
"jasmine": "^3.2.0",
|
||||||
"karmatic": "^1.3.1",
|
"jasmine-es6": "^0.4.3",
|
||||||
"webpack": "^4.37.0"
|
"prettier": "^1.14.3",
|
||||||
},
|
"babel-cli": "6.18.0",
|
||||||
"husky": {
|
"babel-core": "6.21.0",
|
||||||
"hooks": {
|
"babel-eslint": "7.1.1",
|
||||||
"pre-commit": "npm run test"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
12
prettier.config.js
Normal file
12
prettier.config.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
module.exports = {
|
||||||
|
printWidth: 100,
|
||||||
|
tabWidth: 1,
|
||||||
|
useTabs: true,
|
||||||
|
singleQuote: true,
|
||||||
|
trailingComma: 'none',
|
||||||
|
bracketSpacing: true,
|
||||||
|
semi: true,
|
||||||
|
requirePragma: false,
|
||||||
|
proseWrap: 'always',
|
||||||
|
arrowParens: 'avoid'
|
||||||
|
};
|
||||||
43
rollup.config.js
Normal file
43
rollup.config.js
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
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' })]
|
||||||
|
}
|
||||||
|
];
|
||||||
@ -137,21 +137,4 @@ 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]'));
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
@ -155,28 +155,4 @@ 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);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
7
spec/support/jasmine.json
Normal file
7
spec/support/jasmine.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"spec_dir": "spec",
|
||||||
|
"spec_files": ["**/*[sS]pec.js"],
|
||||||
|
"helpers": ["helpers/**/*.js"],
|
||||||
|
"stopSpecOnExpectationFailure": false,
|
||||||
|
"random": true
|
||||||
|
}
|
||||||
@ -1,8 +1,7 @@
|
|||||||
import { id, call } from './util.js';
|
import { id, registerFire, registerSubscriptions, 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;
|
||||||
@ -26,23 +25,29 @@ export const hashableComputed = hash => (fn, dependencies = []) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Add child nodes to the logic graph (value-based)
|
// Add child nodes to the logic graph (value-based)
|
||||||
Object.assign(accessor, subscribable());
|
accessor.subscribe = registerSubscriptions(subscribers);
|
||||||
|
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;
|
||||||
accessor._forEachSubscription(s => s._setDirty && s._setDirty());
|
subscribers.forEach(s => s._setDirty && s._setDirty());
|
||||||
}
|
}
|
||||||
return accessor._hasSubscribers() && accessor;
|
return subscribers.length && accessor;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Remove this node from the logic graph completely
|
// Remove this node from the logic graph completely
|
||||||
accessor.detach = () => {
|
accessor.detach = () => {
|
||||||
accessor.unsubscribeAll();
|
subscribers = [];
|
||||||
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,10 +1,16 @@
|
|||||||
import { subscribable } from './subscribable.js';
|
import { registerSubscriptions, registerFire } from './util.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;
|
||||||
@ -13,8 +19,7 @@ export const hashableContainer = hash => store => {
|
|||||||
if (lockCount && --lockCount === 0) {
|
if (lockCount && --lockCount === 0) {
|
||||||
checkUpdate(store);
|
checkUpdate(store);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
...subscribable()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function checkUpdate(target) {
|
function checkUpdate(target) {
|
||||||
|
|||||||
@ -1,6 +1,4 @@
|
|||||||
import { id } from './util.js';
|
import { id, registerSubscriptions, registerFire } from './util.js';
|
||||||
import { subscribable } from './subscribable.js';
|
|
||||||
|
|
||||||
|
|
||||||
export const hashableProperty = hash => store => {
|
export const hashableProperty = hash => store => {
|
||||||
let subscribers = [];
|
let subscribers = [];
|
||||||
@ -18,8 +16,9 @@ export const hashableProperty = hash => store => {
|
|||||||
}
|
}
|
||||||
return store;
|
return store;
|
||||||
};
|
};
|
||||||
// Add child nodes to the logic graph (value-based)
|
accessor.subscribe = registerSubscriptions(subscribers);
|
||||||
Object.assign(accessor, subscribable());
|
accessor.unsubscribeAll = () => (subscribers = []);
|
||||||
|
accessor._fire = registerFire(subscribers);
|
||||||
accessor._lock = () => {
|
accessor._lock = () => {
|
||||||
lockCount += 1;
|
lockCount += 1;
|
||||||
return accessor();
|
return accessor();
|
||||||
|
|||||||
@ -1,6 +1,4 @@
|
|||||||
import { id, call } from './util.js';
|
import { id, registerFire, registerSubscriptions, 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 = [];
|
||||||
@ -39,23 +37,29 @@ export const hashableStream = hash => (fn, dependencies = []) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Add child nodes to the logic graph (value-based)
|
// Add child nodes to the logic graph (value-based)
|
||||||
Object.assign(accessor, subscribable());
|
accessor.subscribe = registerSubscriptions(subscribers);
|
||||||
|
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;
|
||||||
accessor._forEachSubscription(s => s._setDirty && s._setDirty());
|
subscribers.forEach(s => s._setDirty && s._setDirty());
|
||||||
}
|
}
|
||||||
return accessor._hasSubscribers() && accessor;
|
return subscribers.length && accessor;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Remove this node from the logic graph completely
|
// Remove this node from the logic graph completely
|
||||||
accessor.detach = () => {
|
accessor.detach = () => {
|
||||||
accessor.unsubscribeAll();
|
subscribers = [];
|
||||||
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,30 +0,0 @@
|
|||||||
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,3 +3,20 @@ 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