From efb5906496d4f962d4322f1d31d449f104bf652c Mon Sep 17 00:00:00 2001 From: Curran Date: Sat, 13 Oct 2018 21:52:38 +0530 Subject: [PATCH 1/7] Internal refactoring defining dataflow internally --- topologica.js | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/topologica.js b/topologica.js index f741a3d..b5f34a0 100644 --- a/topologica.js +++ b/topologica.js @@ -1,7 +1,18 @@ const keys = Object.keys; export default reactiveFunctions => { - const state = {}; + const dataflow = function(stateChange) { + depthFirstSearch(keys(stateChange).map(property => { + if (dataflow[property] !== stateChange[property]) { + dataflow[property] = stateChange[property]; + return property; + } + })) + .reverse() + .forEach(invoke); + return this; + }; + const functions = {}; const edges = {}; @@ -12,8 +23,8 @@ export default reactiveFunctions => { const allDefined = dependencies => { const arg = {}; return dependencies.every(property => { - if (state[property] !== undefined){ - arg[property] = state[property]; + if (dataflow[property] !== undefined){ + arg[property] = dataflow[property]; return true; } }) ? arg : null; @@ -36,7 +47,7 @@ export default reactiveFunctions => { functions[property] = () => { const arg = allDefined(dependencies); if (arg) { - state[property] = fn(arg); + dataflow[property] = fn(arg); } }; }); @@ -62,20 +73,8 @@ export default reactiveFunctions => { return nodeList; } - const set = function(stateChange) { - depthFirstSearch(keys(stateChange).map(property => { - if (state[property] !== stateChange[property]) { - state[property] = stateChange[property]; - return property; - } - })) - .reverse() - .forEach(invoke); - return this; - }; - return { - set, - get: () => state + set: dataflow, + get: () => dataflow }; }; From 9d5ea3f86635b184747f8d963ce1f5b65989bbcb Mon Sep 17 00:00:00 2001 From: Curran Date: Sat, 13 Oct 2018 22:00:50 +0530 Subject: [PATCH 2/7] Pass instance into reactive functions. Closes #47 --- README.md | 9 +++++---- test.js | 31 +++++++++++++++++-------------- topologica.js | 16 ++++------------ 3 files changed, 26 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 3b85409..38d6e30 100644 --- a/README.md +++ b/README.md @@ -179,9 +179,11 @@ Here's an example that uses an asynchronous function. There is no specific funct * Call `.set` asynchronously after the promise resolves. ```javascript -const dataflow = Topologica({ +Topologica({ bPromise: [ - ({a}) => Promise.resolve(a + 5).then(b => dataflow.set({ b })), + dataflow => Promise + .resolve(dataflow.a + 5) + .then(b => dataflow({ b })), 'a' ], c: [ @@ -190,8 +192,7 @@ const dataflow = Topologica({ }, 'b' ] -}); -dataflow.set({ a: 5 }); +}).set({ a: 5 }); ```

diff --git a/test.js b/test.js index 4efa5b2..0883320 100644 --- a/test.js +++ b/test.js @@ -274,6 +274,23 @@ describe('Topologica.js', () => { }); }); + it('Should work with async functions.', done => { + Topologica({ + bPromise: [ + dataflow => Promise.resolve(dataflow.a + 5) + .then(b => dataflow({ b })), + 'a' + ], + c: [ + ({b}) => { + assert.equal(b, 10); + done(); + }, + 'b' + ] + }).set({ a: 5 }); + }); + it('Should only propagate changes when values change.', () => { let invocations = 0; @@ -312,20 +329,6 @@ describe('Topologica.js', () => { assert.equal(invocations, 2); }); - it('Should pass only dependencies into reactive functions.', () => { - const dataflow = Topologica({ - b: [ - props => Object.keys(props), - 'a' - ] - }); - dataflow.set({ - a: 'Foo', - foo: 'Bar' - }); - assert.deepEqual(dataflow.get().b, ['a']); - }); - it('Should be fast.', () => { const dataflow = Topologica({ b: [({a}) => a + 1, 'a'], diff --git a/topologica.js b/topologica.js index b5f34a0..b3422ff 100644 --- a/topologica.js +++ b/topologica.js @@ -20,15 +20,8 @@ export default reactiveFunctions => { functions[property](); }; - const allDefined = dependencies => { - const arg = {}; - return dependencies.every(property => { - if (dataflow[property] !== undefined){ - arg[property] = dataflow[property]; - return true; - } - }) ? arg : null; - }; + const allDefined = dependencies => dependencies + .every(property => dataflow[property] !== undefined); keys(reactiveFunctions).forEach(property => { const reactiveFunction = reactiveFunctions[property]; @@ -45,9 +38,8 @@ export default reactiveFunctions => { }); functions[property] = () => { - const arg = allDefined(dependencies); - if (arg) { - dataflow[property] = fn(arg); + if (allDefined(dependencies)) { + dataflow[property] = fn(dataflow); } }; }); From c04294a8b3f857a47e2b4bbdb31df8be6d8cc373 Mon Sep 17 00:00:00 2001 From: Curran Date: Sat, 13 Oct 2018 22:08:09 +0530 Subject: [PATCH 3/7] Eliminate get and set. Closes #45. Closes #46 --- README.md | 37 +++++++++--------- test.js | 102 +++++++++++++++++++++++++------------------------- topologica.js | 7 +--- 3 files changed, 72 insertions(+), 74 deletions(-) diff --git a/README.md b/README.md index 38d6e30..36ba12d 100644 --- a/README.md +++ b/README.md @@ -90,12 +90,12 @@ This table shows all 4 ways of defining a reactive function, each of which may b * **dependencies** If you are typing the dependencies by hand, it makes sense to use the comma-delimited string variant, so that you can easily copy-paste between it and a destructuring assignment (most common case). If you are deriving dependencies programmatically, it makes sense to use the array variant instead. * **reactive functions** If you want to define a reactive function in a self-contained way, for example as a separate module, it makes sense to use the variant where you specify `.dependencies` on a function (most common case). If you want to define multiple smaller reactive functions as a group, for example in the statement that constructs the Topologica instance, then it makes sense to use the more compact two element array variant. -# dataflow.set(stateChange) +# dataflow(stateChange) Performs a shallow merge of `stateChange` into the current state, and propages the change through the data flow graph (synchronously) using topological sort. You can use this to set the values for properties that reactive functions depend on. If a property is not included in `stateChange`, it retains its previous value. ```js -dataflow.set({ +dataflow({ firstName: 'Fred', lastName: 'Flintstone' }); @@ -111,11 +111,10 @@ If a property in `stateChange` is not equal to its previous value using strict e Gets the current state of all properties, including derived properties. ```js -const state = dataflow.get(); -console.log(state.fullName); // Prints 'Fred Flintstone' +console.log(dataflow.fullName); // Prints 'Fred Flintstone' ``` -Assigning values directly to the returned `state` object (for example `state.firstName = 'Wilma'`) will _not_ trigger reactive functions. Use [set](#set) instead. +Assigning values directly to the `dataflow` object (for example `dataflow.firstName = 'Wilma'`) will _not_ trigger reactive functions. Use [dataflow as a function](#set) instead. ## Usage Examples @@ -138,10 +137,10 @@ b.dependencies = ['a']; const dataflow = Topologica({ b }); // Setting the value of a will synchronously propagate changes to B. -dataflow.set({ a: 2 }); +dataflow({ a: 2 }); -// You can use dataflow.get to retreive computed values. -assert.equal(dataflow.get().b, 3); +// You can use dataflow to retreive computed values. +assert.equal(dataflow.b, 3); ```

@@ -159,8 +158,8 @@ b.dependencies = ['a']; const c = ({b}) => b + 1; c.dependencies = ['b']; -const dataflow = Topologica({ b, c }).set({ a: 5 }); -assert.equal(dataflow.get().c, 7); +const dataflow = Topologica({ b, c })({ a: 5 }); +assert.equal(dataflow.c, 7); ``` Note that `set` returns the `Topologica` instance, so it is chainable. @@ -176,7 +175,7 @@ Note that `set` returns the `Topologica` instance, so it is chainable. Here's an example that uses an asynchronous function. There is no specific functionality in the library for supporting asynchronous functions differently, but this is a recommended pattern for working with them: * Use a property for the promise itself, where nothing depends on this property. - * Call `.set` asynchronously after the promise resolves. + * Call `dataflow` asynchronously after the promise resolves. ```javascript Topologica({ @@ -192,9 +191,11 @@ Topologica({ }, 'b' ] -}).set({ a: 5 }); +})({ a: 5 }); ``` +Note that `dataflow` is passed into reactive functions, so you can invoke it asynchronously if required (without the need to assign the Topologica instance to a variable in scope). +


@@ -213,15 +214,15 @@ fullName.dependencies = 'firstName, lastName'; const dataflow = Topologica({ fullName }); -dataflow.set({ firstName: 'Fred', lastName: 'Flintstone' }); -assert.equal(dataflow.get().fullName, 'Fred Flintstone'); +dataflow({ firstName: 'Fred', lastName: 'Flintstone' }); +assert.equal(dataflow.fullName, 'Fred Flintstone'); ``` Now if either firstName or `lastName` changes, `fullName` will be updated (synchronously). ```js -dataflow.set({ firstName: 'Wilma' }); -assert.equal(dataflow.get().fullName, 'Wilma Flintstone'); +dataflow({ firstName: 'Wilma' }); +assert.equal(dataflow.fullName, 'Wilma Flintstone'); ```

@@ -270,13 +271,13 @@ const dataflow = Topologica({ d: [({a}) => a + 1, 'a'], e: [({b, d}) => b + d, 'b, d'] }); -dataflow.set({ a: 5 }); +dataflow({ a: 5 }); const a = 5; const b = a + 1; const c = b + 1; const d = a + 1; const e = b + d; -assert.equal(dataflow.get().e, e); +assert.equal(dataflow.e, e); ``` For more examples, have a look at the [tests](/test/test.js). diff --git a/test.js b/test.js index 0883320..d2cbef7 100644 --- a/test.js +++ b/test.js @@ -5,32 +5,32 @@ describe('Topologica.js', () => { it('Should set and get a value.', () => { const dataflow = Topologica({}); - dataflow.set({ + dataflow({ foo: 'bar' }); - assert.equal(dataflow.get().foo, 'bar'); + assert.equal(dataflow.foo, 'bar'); }); it('Should chain set.', () => { - const dataflow = Topologica({}).set({ foo: 'bar' }); - assert.equal(dataflow.get().foo, 'bar'); + const dataflow = Topologica({})({ foo: 'bar' }); + assert.equal(dataflow.foo, 'bar'); }); it('Should compute a derived property.', () => { const dataflow = Topologica({ b: [({a}) => a + 1, 'a'] }); - dataflow.set({ + dataflow({ a: 5 }); - assert.equal(dataflow.get().b, 6); + assert.equal(dataflow.b, 6); }); it('Should handle uninitialized property.', () => { const dataflow = Topologica({ b: [({a}) => a + 1, 'a'] }); - assert.equal(dataflow.get().b, undefined); + assert.equal(dataflow.b, undefined); }); it('Should propagate changes synchronously.', () => { @@ -38,15 +38,15 @@ describe('Topologica.js', () => { b: [({a}) => a + 1, 'a'] }); - dataflow.set({ + dataflow({ a: 2 }); - assert.equal(dataflow.get().b, 3); + assert.equal(dataflow.b, 3); - dataflow.set({ + dataflow({ a: 99 }); - assert.equal(dataflow.get().b, 100); + assert.equal(dataflow.b, 100); }); it('Should compute a derived property with 2 hops.', () => { @@ -54,10 +54,10 @@ describe('Topologica.js', () => { b: [({a}) => a + 1, 'a'], c: [({b}) => b + 1, 'b'] }); - dataflow.set({ + dataflow({ a: 5 }); - assert.equal(dataflow.get().c, 7); + assert.equal(dataflow.c, 7); }); it('Should handle case of 2 inputs.', () => { @@ -67,33 +67,33 @@ describe('Topologica.js', () => { 'firstName, lastName' ] }); - dataflow.set({ + dataflow({ firstName: 'Fred', lastName: 'Flintstone' }); - assert.equal(dataflow.get().fullName, 'Fred Flintstone'); + assert.equal(dataflow.fullName, 'Fred Flintstone'); }); it('Should accept an array of strings as dependencies.', () => { const fullName = ({firstName, lastName}) => `${firstName} ${lastName}`; fullName.dependencies = ['firstName', 'lastName']; const dataflow = Topologica({ fullName }); - dataflow.set({ + dataflow({ firstName: 'Fred', lastName: 'Flintstone' }); - assert.equal(dataflow.get().fullName, 'Fred Flintstone'); + assert.equal(dataflow.fullName, 'Fred Flintstone'); }); it('Should accept a comma delimited string as dependencies.', () => { const fullName = ({firstName, lastName}) => `${firstName} ${lastName}`; fullName.dependencies = 'firstName, lastName'; const dataflow = Topologica({ fullName }); - dataflow.set({ + dataflow({ firstName: 'Fred', lastName: 'Flintstone' }); - assert.equal(dataflow.get().fullName, 'Fred Flintstone'); + assert.equal(dataflow.fullName, 'Fred Flintstone'); }); it('Should accept reactive function as an array.', () => { @@ -102,11 +102,11 @@ describe('Topologica.js', () => { ['firstName', 'lastName'] ] const dataflow = Topologica({ fullName }); - dataflow.set({ + dataflow({ firstName: 'Fred', lastName: 'Flintstone' }); - assert.equal(dataflow.get().fullName, 'Fred Flintstone'); + assert.equal(dataflow.fullName, 'Fred Flintstone'); }); it('Should accept reactive function as an array, with dependencies as a string.', () => { @@ -115,11 +115,11 @@ describe('Topologica.js', () => { 'firstName,lastName' ] const dataflow = Topologica({ fullName }); - dataflow.set({ + dataflow({ firstName: 'Fred', lastName: 'Flintstone' }); - assert.equal(dataflow.get().fullName, 'Fred Flintstone'); + assert.equal(dataflow.fullName, 'Fred Flintstone'); }); it('Should only execute when all inputs are defined.', () => { @@ -130,39 +130,39 @@ describe('Topologica.js', () => { ] }); - dataflow.set({ + dataflow({ lastName: 'Flintstone' }); - assert.equal(dataflow.get().fullName, undefined); + assert.equal(dataflow.fullName, undefined); - dataflow.set({ + dataflow({ firstName: 'Wilma' }); - assert.equal(dataflow.get().fullName, 'Wilma Flintstone'); + assert.equal(dataflow.fullName, 'Wilma Flintstone'); }); it('Should handle case of 3 inputs.', () => { const dataflow = Topologica({ d: [({a, b, c}) => a + b + c, 'a,b,c'] }); - dataflow.set({ + dataflow({ a: 5, b: 8, c: 2 }); - assert.equal(dataflow.get().d, 15); + assert.equal(dataflow.d, 15); }); it('Should handle spaces in input string.', () => { const dataflow = Topologica({ d: [({a, b, c}) => a + b + c, ' a , b, c '] }); - dataflow.set({ + dataflow({ a: 5, b: 8, c: 2 }); - assert.equal(dataflow.get().d, 15); + assert.equal(dataflow.d, 15); }); // Data flow graph, read from top to bottom. @@ -179,11 +179,11 @@ describe('Topologica.js', () => { d: [({c}) => c + 1, 'c'], e: [({b, d}) => b + d, 'b, d'] }); - dataflow.set({ + dataflow({ a: 1, c: 2 }); - assert.equal(dataflow.get().e, (1 + 1) + (2 + 1)); + assert.equal(dataflow.e, (1 + 1) + (2 + 1)); }); // a @@ -200,15 +200,15 @@ describe('Topologica.js', () => { d: [({a}) => a + 1, 'a'], e: [({b, d}) => b + d, 'b, d'] }); - dataflow.set({ + dataflow({ a: 5 }); - const a = dataflow.get().a; + const a = dataflow.a; const b = a + 1; const c = b + 1; const d = a + 1; const e = b + d; - assert.equal(dataflow.get().e, e); + assert.equal(dataflow.e, e); }); @@ -231,10 +231,10 @@ describe('Topologica.js', () => { g: [({a}) => a + 1, 'a'], h: [({d, f, g}) => d + f + g, 'd, f, g'] }); - dataflow.set({ + dataflow({ a: 5 }); - const a = dataflow.get().a; + const a = dataflow.a; const b = a + 1; const c = b + 1; const d = c + 1; @@ -242,23 +242,23 @@ describe('Topologica.js', () => { const f = e + 1; const g = a + 1; const h = d + f + g; - assert.equal(dataflow.get().h, h); + assert.equal(dataflow.h, h); }); it('Should work with booleans.', () => { const dataflow = Topologica({ b: [({a}) => !a, 'a'] }); - dataflow.set({ + dataflow({ a: false }); - assert.equal(dataflow.get().b, true); + assert.equal(dataflow.b, true); }); it('Should work with async functions.', done => { const dataflow = Topologica({ bPromise: [ - ({a}) => Promise.resolve(a + 5).then(b => dataflow.set({ b })), + ({a}) => Promise.resolve(a + 5).then(b => dataflow({ b })), 'a' ], c: [ @@ -269,7 +269,7 @@ describe('Topologica.js', () => { 'b' ] }); - dataflow.set({ + dataflow({ a: 5 }); }); @@ -288,7 +288,7 @@ describe('Topologica.js', () => { }, 'b' ] - }).set({ a: 5 }); + })({ a: 5 }); }); it('Should only propagate changes when values change.', () => { @@ -300,13 +300,13 @@ describe('Topologica.js', () => { assert.equal(invocations, 0); - dataflow.set({ a: 2 }); + dataflow({ a: 2 }); assert.equal(invocations, 1); - dataflow.set({ a: 2 }); + dataflow({ a: 2 }); assert.equal(invocations, 1); - dataflow.set({ a: 99 }); + dataflow({ a: 99 }); assert.equal(invocations, 2); }); @@ -319,13 +319,13 @@ describe('Topologica.js', () => { assert.equal(invocations, 0); - dataflow.set({ a: 2, b: 4 }); + dataflow({ a: 2, b: 4 }); assert.equal(invocations, 1); - dataflow.set({ a: 2 }); + dataflow({ a: 2 }); assert.equal(invocations, 1); - dataflow.set({ a: 2, b: 6 }); + dataflow({ a: 2, b: 6 }); assert.equal(invocations, 2); }); @@ -344,7 +344,7 @@ describe('Topologica.js', () => { for(let j = 0; j < numRuns; j++){ const begin = Date.now(); for(let i = 0; i < 200000; i++){ - dataflow.set({ a: i }); + dataflow({ a: i }); } const end = Date.now(); const time = end - begin; diff --git a/topologica.js b/topologica.js index b3422ff..037ebd9 100644 --- a/topologica.js +++ b/topologica.js @@ -10,7 +10,7 @@ export default reactiveFunctions => { })) .reverse() .forEach(invoke); - return this; + return dataflow; }; const functions = {}; @@ -65,8 +65,5 @@ export default reactiveFunctions => { return nodeList; } - return { - set: dataflow, - get: () => dataflow - }; + return dataflow; }; From 933193d62aa40a52292466d4913d184acc27b7e0 Mon Sep 17 00:00:00 2001 From: Curran Date: Sat, 13 Oct 2018 22:17:42 +0530 Subject: [PATCH 4/7] Rename dependencies to inputs. Closes #44 --- README.md | 31 ++++++++++++++++--------------- test.js | 10 +++++----- topologica.js | 18 +++++++++--------- 3 files changed, 30 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 36ba12d..e1ed803 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Topologica.js A library for [reactive programming](https://en.wikipedia.org/wiki/Reactive_programming). Weighs [1KB minified](https://unpkg.com/topologica). -This library provides an abstraction for **reactive data flows**. This means you can declaratively specify a [dependency graph](https://en.wikipedia.org/wiki/Dependency_graph), and the library will take care of executing _only_ the required functions to propagate changes through the graph in the correct order. Nodes in the dependency graph are named properties, and edges are reactive functions that compute derived properties as functions of their dependencies. The order of execution is determined using the [topological sorting algorithm](https://en.wikipedia.org/wiki/Topological_sorting), hence the name _Topologica_. +This library provides an abstraction for **reactive data flows**. This means you can declaratively specify a [dependency graph](https://en.wikipedia.org/wiki/Dependency_graph), and the library will take care of executing _only_ the required functions to propagate changes through the graph in the correct order. Nodes in the dependency graph are named properties, and edges are reactive functions that compute derived properties as functions of their inputs (dependencies). The order of execution is determined using the [topological sorting algorithm](https://en.wikipedia.org/wiki/Topological_sorting), hence the name _Topologica_. Topologica is primarily intended for use in optimizing interactive data visualizations created using [D3.js](https://d3js.org/) and a unidirectional data flow approach. The problem with using unidirectional data flow with interactive data visualizations is that it leads to **unnecessary execution of heavyweight computations over data on every render**. For example, if you change the highlighted element, or the text of an axis label, the entire visualization including scales and rendering of all marks would be recomputed and re-rendered to the DOM unnecessarily. Topologica.js lets you improve performance by only executing heavy computation and rendering operations when they are actually required. It also allows you to simplify your code by splitting it into logical chunks based on reactive functions, and makes it so you don't need to think about order of execution at all. @@ -45,7 +45,7 @@ Constructs a new data flow graph with the given reactiveFunctions argumen const dataflow = Topologica({ fullName }); ``` -A reactive function accepts a single argument, an object containing values for its dependencies, and has an explicit representation of its dependencies. A reactive function can either be represented as a **function** with a _dependencies_ property, or as an **array** where the first element is the function and the second element is the dependencies. Dependencies can be represented either as an array of property name strings, or as a comma delimited string of property names. +A reactive function accepts a single argument, an object containing values for its inputs, and has an explicit representation of its inputs. A reactive function can either be represented as a **function** with an _inputs_ property, or as an **array** where the first element is the function and the second element is the inputs. Dependencies can be represented either as an array of property name strings, or as a comma delimited string of property names. @@ -61,7 +61,7 @@ A reactive function accepts a single argument, an object containing values for i
const fullName =
   ({firstName, lastName}) =>
     ${firstName} ${lastName};
-fullName.dependencies =
+fullName.inputs =
   ['firstName', 'lastName'];
const fullName = [
   ({firstName, lastName}) =>
@@ -74,7 +74,7 @@ fullName.dependencies =
       
const fullName =
   ({firstName, lastName}) =>
     ${firstName} ${lastName};
-fullName.dependencies =
+fullName.inputs =
   'firstName, lastName';
const fullName = [
   ({firstName, lastName}) =>
@@ -87,8 +87,8 @@ fullName.dependencies =
 
 This table shows all 4 ways of defining a reactive function, each of which may be useful in different contexts.
 
- * **dependencies** If you are typing the dependencies by hand, it makes sense to use the comma-delimited string variant, so that you can easily copy-paste between it and a destructuring assignment (most common case). If you are deriving dependencies programmatically, it makes sense to use the array variant instead.
- * **reactive functions** If you want to define a reactive function in a self-contained way, for example as a separate module, it makes sense to use the variant where you specify `.dependencies` on a function (most common case). If you want to define multiple smaller reactive functions as a group, for example in the statement that constructs the Topologica instance, then it makes sense to use the more compact two element array variant.
+ * **inputs** If you are typing the inputs by hand, it makes sense to use the comma-delimited string variant, so that you can easily copy-paste between it and a destructuring assignment (most common case). If you are deriving inputs programmatically, it makes sense to use the array variant instead.
+ * **reactive functions** If you want to define a reactive function in a self-contained way, for example as a separate module, it makes sense to use the variant where you specify `.inputs` on a function (most common case). If you want to define multiple smaller reactive functions as a group, for example in the statement that constructs the Topologica instance, then it makes sense to use the more compact two element array variant.
 
 # dataflow(stateChange)
 
@@ -101,7 +101,7 @@ dataflow({
 });
 ```
 
-The above example sets two properties at once, `firstName` and `lastName`. When this is invoked, all dependencies of `fullName` are defined, so `fullName` is synchronously computed.
+The above example sets two properties at once, `firstName` and `lastName`. When this is invoked, all inputs of `fullName` are defined, so `fullName` is synchronously computed.
 
 If a property in `stateChange` is equal to its previous value using strict equality (`===`), it is _not_ considered changed, and reactive functions that depend on it will _not_ be invoked. You should therefore use only [immutable update patterns](https://redux.js.org/recipes/structuringreducers/immutableupdatepatterns) when changing objects and arrays.
 
@@ -130,8 +130,8 @@ You can define _reactive functions_ that compute properties that depend on other
 // First, define a function that accepts an options object as an argument.
 const b = ({a}) => a + 1;
 
-// Next, declare the dependencies of this function as an array of names.
-b.dependencies = ['a'];
+// Next, declare the inputs of this function as an array of names.
+b.inputs = ['a'];
 
 // Pass this function into the Topologica constructor.
 const dataflow = Topologica({ b });
@@ -153,10 +153,10 @@ Here's an example that assigns `b = a + 1` and `c = b + 1`.
 
 ```javascript
 const b = ({a}) => a + 1
-b.dependencies = ['a'];
+b.inputs = ['a'];
 
 const c = ({b}) => b + 1;
-c.dependencies = ['b'];
+c.inputs = ['b'];
 
 const dataflow = Topologica({ b, c })({ a: 5 });
 assert.equal(dataflow.c, 7);
@@ -210,7 +210,7 @@ Here's an example that computes a person's full name from their first name and a
 
 ```js
 const fullName = ({firstName, lastName}) => `${firstName} ${lastName}`;
-fullName.dependencies = 'firstName, lastName';
+fullName.inputs = 'firstName, lastName';
 
 const dataflow = Topologica({ fullName });
 
@@ -228,10 +228,10 @@ assert.equal(dataflow.fullName, 'Wilma Flintstone');
 


- Full name changes whenever its dependencies change. + Full name changes whenever its inputs change.

-Here's the previous example re-written to specify the reactive function using a two element array with dependencies specified as a comma delimited string. This is the form we'll use for the rest of the examples here. +Here's the previous example re-written to specify the reactive function using a two element array with inputs specified as a comma delimited string. This is the form we'll use for the rest of the examples here. ```js const dataflow = Topologica({ @@ -294,10 +294,11 @@ The minimalism and synchronous execution are inspired by similar features in [Ob Similar initiatives: + * [Observable Notebook Runtime](https://github.com/observablehq/notebook-runtime#variable_define) Implements a reactive runtime environment with named variables that update when their inputs change. * [Mobx](https://github.com/mobxjs/mobx) Very similar library, with React bindings and more API surface area. * [DVL](https://github.com/vogievetsky/DVL) Early work on reactive data visualizations. * [ZJONSSON/clues](https://github.com/ZJONSSON/clues) A very similar library based on Promises. - * [Ember Computed Properties](https://guides.emberjs.com/v2.18.0/object-model/computed-properties/) Similar structure of dependencies and reactivity. + * [Ember Computed Properties](https://guides.emberjs.com/v2.18.0/object-model/computed-properties/) Similar structure of inputs and reactivity. * [AngularJS Dependency Injection](https://docs.angularjs.org/guide/di) Inspired the API for reactive functions. * [AngularJS $digest()](https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$digest) Inspired the "digest" term. * [RxJS](https://github.com/Reactive-Extensions/RxJS) and [Bacon](https://baconjs.github.io/) Full blown FRP packages. diff --git a/test.js b/test.js index d2cbef7..df4c43a 100644 --- a/test.js +++ b/test.js @@ -74,9 +74,9 @@ describe('Topologica.js', () => { assert.equal(dataflow.fullName, 'Fred Flintstone'); }); - it('Should accept an array of strings as dependencies.', () => { + it('Should accept an array of strings as inputs.', () => { const fullName = ({firstName, lastName}) => `${firstName} ${lastName}`; - fullName.dependencies = ['firstName', 'lastName']; + fullName.inputs = ['firstName', 'lastName']; const dataflow = Topologica({ fullName }); dataflow({ firstName: 'Fred', @@ -85,9 +85,9 @@ describe('Topologica.js', () => { assert.equal(dataflow.fullName, 'Fred Flintstone'); }); - it('Should accept a comma delimited string as dependencies.', () => { + it('Should accept a comma delimited string as inputs.', () => { const fullName = ({firstName, lastName}) => `${firstName} ${lastName}`; - fullName.dependencies = 'firstName, lastName'; + fullName.inputs = 'firstName, lastName'; const dataflow = Topologica({ fullName }); dataflow({ firstName: 'Fred', @@ -109,7 +109,7 @@ describe('Topologica.js', () => { assert.equal(dataflow.fullName, 'Fred Flintstone'); }); - it('Should accept reactive function as an array, with dependencies as a string.', () => { + it('Should accept reactive function as an array, with inputs as a string.', () => { const fullName = [ ({firstName, lastName}) => `${firstName} ${lastName}`, 'firstName,lastName' diff --git a/topologica.js b/topologica.js index 037ebd9..b9b3bec 100644 --- a/topologica.js +++ b/topologica.js @@ -20,25 +20,25 @@ export default reactiveFunctions => { functions[property](); }; - const allDefined = dependencies => dependencies + const allDefined = inputs => inputs .every(property => dataflow[property] !== undefined); keys(reactiveFunctions).forEach(property => { const reactiveFunction = reactiveFunctions[property]; - let dependencies = reactiveFunction.dependencies; - const fn = dependencies ? reactiveFunction : reactiveFunction[0]; - dependencies = dependencies || reactiveFunction[1]; + let inputs = reactiveFunction.inputs; + const fn = inputs ? reactiveFunction : reactiveFunction[0]; + inputs = inputs || reactiveFunction[1]; - dependencies = dependencies.split - ? dependencies.split(',').map(str => str.trim()) - : dependencies; + inputs = inputs.split + ? inputs.split(',').map(str => str.trim()) + : inputs; - dependencies.forEach(input => { + inputs.forEach(input => { (edges[input] = edges[input] || []).push(property); }); functions[property] = () => { - if (allDefined(dependencies)) { + if (allDefined(inputs)) { dataflow[property] = fn(dataflow); } }; From 60e57370d8362fe76cb4fdad3d970887aa665283 Mon Sep 17 00:00:00 2001 From: Curran Date: Sat, 13 Oct 2018 22:28:45 +0530 Subject: [PATCH 5/7] Rename the exported global from Topologica to topologica. Closes #43. Closes #42 --- README.md | 33 ++++++++++++++++---------------- rollup.config.js | 2 +- test.js | 50 ++++++++++++++++++++++++------------------------ 3 files changed, 42 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index e1ed803..b9e416a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Topologica.js +# topologica.js A library for [reactive programming](https://en.wikipedia.org/wiki/Reactive_programming). Weighs [1KB minified](https://unpkg.com/topologica). This library provides an abstraction for **reactive data flows**. This means you can declaratively specify a [dependency graph](https://en.wikipedia.org/wiki/Dependency_graph), and the library will take care of executing _only_ the required functions to propagate changes through the graph in the correct order. Nodes in the dependency graph are named properties, and edges are reactive functions that compute derived properties as functions of their inputs (dependencies). The order of execution is determined using the [topological sorting algorithm](https://en.wikipedia.org/wiki/Topological_sorting), hence the name _Topologica_. @@ -24,7 +24,7 @@ npm install --save-dev topologica Then import it into your code like this: ```js -import Topologica from 'topologica'; +import topologica from 'topologica'; ``` You can also include the library in a script tag from Unpkg, like this: @@ -33,16 +33,16 @@ You can also include the library in a script tag from Unpkg, like this: ``` -This script tag introduces the global `Topologica`. +This script tag introduces the global `topologica`. ## API Reference -# Topologica(reactiveFunctions) +# topologica(reactiveFunctions) -Constructs a new data flow graph with the given reactiveFunctions argument, an object whose keys are the names of computed properties and whose values are reactive functions. By convention, the variable name `dataflow` is used for instances of Topologica, because they are reactive data flow graphs. +Constructs a new data flow graph with the given reactiveFunctions argument, an object whose keys are the names of computed properties and whose values are reactive functions. By convention, the variable name `dataflow` is used for instances of `topologica`, because they are reactive data flow graphs. ```js -const dataflow = Topologica({ fullName }); +const dataflow = topologica({ fullName }); ``` A reactive function accepts a single argument, an object containing values for its inputs, and has an explicit representation of its inputs. A reactive function can either be represented as a **function** with an _inputs_ property, or as an **array** where the first element is the function and the second element is the inputs. Dependencies can be represented either as an array of property name strings, or as a comma delimited string of property names. @@ -88,7 +88,7 @@ fullName.inputs = This table shows all 4 ways of defining a reactive function, each of which may be useful in different contexts. * **inputs** If you are typing the inputs by hand, it makes sense to use the comma-delimited string variant, so that you can easily copy-paste between it and a destructuring assignment (most common case). If you are deriving inputs programmatically, it makes sense to use the array variant instead. - * **reactive functions** If you want to define a reactive function in a self-contained way, for example as a separate module, it makes sense to use the variant where you specify `.inputs` on a function (most common case). If you want to define multiple smaller reactive functions as a group, for example in the statement that constructs the Topologica instance, then it makes sense to use the more compact two element array variant. + * **reactive functions** If you want to define a reactive function in a self-contained way, for example as a separate module, it makes sense to use the variant where you specify `.inputs` on a function (most common case). If you want to define multiple smaller reactive functions as a group, for example in the statement that constructs the `dataflow` instance, then it makes sense to use the more compact two element array variant. # dataflow(stateChange) @@ -124,7 +124,7 @@ External running examples: * [Bowl of Fruit - Topologica Experiment](https://vizhub.com/curran/27c261085d8a48618c69f7983672903b) - A proposed approach for using Topologica with D3. * [Topologica Layers Experiment](https://vizhub.com/curran/f26d83673fca4d17a7579f3fdba400d6) - Experiment with interactive highlighting. -You can define _reactive functions_ that compute properties that depend on other properties as input. These properties exist on instances of `Topologica`, so in a sense they are namespaced rather than free-floating. For example, consider the following example where `b` gets set to `a + 1` whenever `a` changes. +You can define _reactive functions_ that compute properties that depend on other properties as input. These properties exist on instances of `topologica`, so in a sense they are namespaced rather than free-floating. For example, consider the following example where `b` gets set to `a + 1` whenever `a` changes. ```javascript // First, define a function that accepts an options object as an argument. @@ -134,7 +134,7 @@ const b = ({a}) => a + 1; b.inputs = ['a']; // Pass this function into the Topologica constructor. -const dataflow = Topologica({ b }); +const dataflow = topologica({ b }); // Setting the value of a will synchronously propagate changes to B. dataflow({ a: 2 }); @@ -158,12 +158,11 @@ b.inputs = ['a']; const c = ({b}) => b + 1; c.inputs = ['b']; -const dataflow = Topologica({ b, c })({ a: 5 }); +const dataflow = topologica({ b, c }); +dataflow({ a: 5 }); assert.equal(dataflow.c, 7); ``` -Note that `set` returns the `Topologica` instance, so it is chainable. -


@@ -178,7 +177,7 @@ Here's an example that uses an asynchronous function. There is no specific funct * Call `dataflow` asynchronously after the promise resolves. ```javascript -Topologica({ +topologica({ bPromise: [ dataflow => Promise .resolve(dataflow.a + 5) @@ -212,7 +211,7 @@ Here's an example that computes a person's full name from their first name and a const fullName = ({firstName, lastName}) => `${firstName} ${lastName}`; fullName.inputs = 'firstName, lastName'; -const dataflow = Topologica({ fullName }); +const dataflow = topologica({ fullName }); dataflow({ firstName: 'Fred', lastName: 'Flintstone' }); assert.equal(dataflow.fullName, 'Fred Flintstone'); @@ -234,7 +233,7 @@ assert.equal(dataflow.fullName, 'Wilma Flintstone'); Here's the previous example re-written to specify the reactive function using a two element array with inputs specified as a comma delimited string. This is the form we'll use for the rest of the examples here. ```js -const dataflow = Topologica({ +const dataflow = topologica({ fullName: [ ({firstName, lastName}) => `${firstName} ${lastName}`, 'firstName, lastName' @@ -245,7 +244,7 @@ const dataflow = Topologica({ You can use reactive functions to trigger code with side effects like DOM manipulation. ```js -const dataflow = Topologica({ +const dataflow = topologica({ fullName: [ ({firstName, lastName}) => `${firstName} ${lastName}`, 'firstName, lastName' @@ -265,7 +264,7 @@ Here's the tricky case, where breadth-first or time-tick-based propagation fails

```js -const dataflow = Topologica({ +const dataflow = topologica({ b: [({a}) => a + 1, 'a'], c: [({b}) => b + 1, 'b'], d: [({a}) => a + 1, 'a'], diff --git a/rollup.config.js b/rollup.config.js index 0d37bc8..45ad559 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -4,7 +4,7 @@ export default { input: 'topologica.js', output: { format: 'umd', - name: 'Topologica', + name: 'topologica', file: 'dist/topologica.js' }, plugins: [ diff --git a/test.js b/test.js index df4c43a..05b2671 100644 --- a/test.js +++ b/test.js @@ -1,10 +1,10 @@ -const Topologica = require('./dist/topologica.js'); +const topologica = require('./dist/topologica.js'); const assert = require('assert'); -describe('Topologica.js', () => { +describe('topologica.js', () => { it('Should set and get a value.', () => { - const dataflow = Topologica({}); + const dataflow = topologica({}); dataflow({ foo: 'bar' }); @@ -12,12 +12,12 @@ describe('Topologica.js', () => { }); it('Should chain set.', () => { - const dataflow = Topologica({})({ foo: 'bar' }); + const dataflow = topologica({})({ foo: 'bar' }); assert.equal(dataflow.foo, 'bar'); }); it('Should compute a derived property.', () => { - const dataflow = Topologica({ + const dataflow = topologica({ b: [({a}) => a + 1, 'a'] }); dataflow({ @@ -27,14 +27,14 @@ describe('Topologica.js', () => { }); it('Should handle uninitialized property.', () => { - const dataflow = Topologica({ + const dataflow = topologica({ b: [({a}) => a + 1, 'a'] }); assert.equal(dataflow.b, undefined); }); it('Should propagate changes synchronously.', () => { - const dataflow = Topologica({ + const dataflow = topologica({ b: [({a}) => a + 1, 'a'] }); @@ -50,7 +50,7 @@ describe('Topologica.js', () => { }); it('Should compute a derived property with 2 hops.', () => { - const dataflow = Topologica({ + const dataflow = topologica({ b: [({a}) => a + 1, 'a'], c: [({b}) => b + 1, 'b'] }); @@ -61,7 +61,7 @@ describe('Topologica.js', () => { }); it('Should handle case of 2 inputs.', () => { - const dataflow = Topologica({ + const dataflow = topologica({ fullName: [ ({firstName, lastName}) => `${firstName} ${lastName}`, 'firstName, lastName' @@ -77,7 +77,7 @@ describe('Topologica.js', () => { it('Should accept an array of strings as inputs.', () => { const fullName = ({firstName, lastName}) => `${firstName} ${lastName}`; fullName.inputs = ['firstName', 'lastName']; - const dataflow = Topologica({ fullName }); + const dataflow = topologica({ fullName }); dataflow({ firstName: 'Fred', lastName: 'Flintstone' @@ -88,7 +88,7 @@ describe('Topologica.js', () => { it('Should accept a comma delimited string as inputs.', () => { const fullName = ({firstName, lastName}) => `${firstName} ${lastName}`; fullName.inputs = 'firstName, lastName'; - const dataflow = Topologica({ fullName }); + const dataflow = topologica({ fullName }); dataflow({ firstName: 'Fred', lastName: 'Flintstone' @@ -101,7 +101,7 @@ describe('Topologica.js', () => { ({firstName, lastName}) => `${firstName} ${lastName}`, ['firstName', 'lastName'] ] - const dataflow = Topologica({ fullName }); + const dataflow = topologica({ fullName }); dataflow({ firstName: 'Fred', lastName: 'Flintstone' @@ -114,7 +114,7 @@ describe('Topologica.js', () => { ({firstName, lastName}) => `${firstName} ${lastName}`, 'firstName,lastName' ] - const dataflow = Topologica({ fullName }); + const dataflow = topologica({ fullName }); dataflow({ firstName: 'Fred', lastName: 'Flintstone' @@ -123,7 +123,7 @@ describe('Topologica.js', () => { }); it('Should only execute when all inputs are defined.', () => { - const dataflow = Topologica({ + const dataflow = topologica({ fullName: [ ({firstName, lastName}) => `${firstName} ${lastName}`, 'firstName, lastName' @@ -142,7 +142,7 @@ describe('Topologica.js', () => { }); it('Should handle case of 3 inputs.', () => { - const dataflow = Topologica({ + const dataflow = topologica({ d: [({a, b, c}) => a + b + c, 'a,b,c'] }); dataflow({ @@ -154,7 +154,7 @@ describe('Topologica.js', () => { }); it('Should handle spaces in input string.', () => { - const dataflow = Topologica({ + const dataflow = topologica({ d: [({a, b, c}) => a + b + c, ' a , b, c '] }); dataflow({ @@ -174,7 +174,7 @@ describe('Topologica.js', () => { // e // it('Should evaluate not-too-tricky case.', () => { - const dataflow = Topologica({ + const dataflow = topologica({ b: [({a}) => a + 1, 'a'], d: [({c}) => c + 1, 'c'], e: [({b, d}) => b + d, 'b, d'] @@ -194,7 +194,7 @@ describe('Topologica.js', () => { // \ / // e it('Should evaluate tricky case.', () => { - const dataflow = Topologica({ + const dataflow = topologica({ b: [({a}) => a + 1, 'a'], c: [({b}) => b + 1, 'b'], d: [({a}) => a + 1, 'a'], @@ -222,7 +222,7 @@ describe('Topologica.js', () => { // \ / / // h it('Should evaluate trickier case.', () => { - const dataflow = Topologica({ + const dataflow = topologica({ b: [({a}) => a + 1, 'a'], c: [({b}) => b + 1, 'b'], d: [({c}) => c + 1, 'c'], @@ -246,7 +246,7 @@ describe('Topologica.js', () => { }); it('Should work with booleans.', () => { - const dataflow = Topologica({ + const dataflow = topologica({ b: [({a}) => !a, 'a'] }); dataflow({ @@ -256,7 +256,7 @@ describe('Topologica.js', () => { }); it('Should work with async functions.', done => { - const dataflow = Topologica({ + const dataflow = topologica({ bPromise: [ ({a}) => Promise.resolve(a + 5).then(b => dataflow({ b })), 'a' @@ -275,7 +275,7 @@ describe('Topologica.js', () => { }); it('Should work with async functions.', done => { - Topologica({ + topologica({ bPromise: [ dataflow => Promise.resolve(dataflow.a + 5) .then(b => dataflow({ b })), @@ -294,7 +294,7 @@ describe('Topologica.js', () => { it('Should only propagate changes when values change.', () => { let invocations = 0; - const dataflow = Topologica({ + const dataflow = topologica({ b: [({a}) => invocations++, 'a'] }); @@ -313,7 +313,7 @@ describe('Topologica.js', () => { it('Should propagate changes if a single dependency changes.', () => { let invocations = 0; - const dataflow = Topologica({ + const dataflow = topologica({ c: [() => invocations++, 'a, b'] }); @@ -330,7 +330,7 @@ describe('Topologica.js', () => { }); it('Should be fast.', () => { - const dataflow = Topologica({ + const dataflow = topologica({ b: [({a}) => a + 1, 'a'], c: [({b}) => b + 1, 'b'], d: [({c}) => c + 1, 'c'], From 8ff79db9bf4c9395c85d68b0a6d5c9b98bb09613 Mon Sep 17 00:00:00 2001 From: Curran Date: Sun, 14 Oct 2018 13:58:20 +0530 Subject: [PATCH 6/7] Introduce naming convention topologica = Topologica() --- rollup.config.js | 2 +- test.js | 152 +++++++++++++++++++++++------------------------ topologica.js | 14 ++--- 3 files changed, 84 insertions(+), 84 deletions(-) diff --git a/rollup.config.js b/rollup.config.js index 45ad559..0d37bc8 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -4,7 +4,7 @@ export default { input: 'topologica.js', output: { format: 'umd', - name: 'topologica', + name: 'Topologica', file: 'dist/topologica.js' }, plugins: [ diff --git a/test.js b/test.js index 05b2671..d1025aa 100644 --- a/test.js +++ b/test.js @@ -1,99 +1,99 @@ -const topologica = require('./dist/topologica.js'); +const Topologica = require('./dist/topologica.js'); const assert = require('assert'); -describe('topologica.js', () => { +describe('Topologica.js', () => { it('Should set and get a value.', () => { - const dataflow = topologica({}); - dataflow({ + const topologica = Topologica({}); + topologica({ foo: 'bar' }); - assert.equal(dataflow.foo, 'bar'); + assert.equal(topologica.foo, 'bar'); }); it('Should chain set.', () => { - const dataflow = topologica({})({ foo: 'bar' }); - assert.equal(dataflow.foo, 'bar'); + const topologica = Topologica({})({ foo: 'bar' }); + assert.equal(topologica.foo, 'bar'); }); it('Should compute a derived property.', () => { - const dataflow = topologica({ + const topologica = Topologica({ b: [({a}) => a + 1, 'a'] }); - dataflow({ + topologica({ a: 5 }); - assert.equal(dataflow.b, 6); + assert.equal(topologica.b, 6); }); it('Should handle uninitialized property.', () => { - const dataflow = topologica({ + const topologica = Topologica({ b: [({a}) => a + 1, 'a'] }); - assert.equal(dataflow.b, undefined); + assert.equal(topologica.b, undefined); }); it('Should propagate changes synchronously.', () => { - const dataflow = topologica({ + const topologica = Topologica({ b: [({a}) => a + 1, 'a'] }); - dataflow({ + topologica({ a: 2 }); - assert.equal(dataflow.b, 3); + assert.equal(topologica.b, 3); - dataflow({ + topologica({ a: 99 }); - assert.equal(dataflow.b, 100); + assert.equal(topologica.b, 100); }); it('Should compute a derived property with 2 hops.', () => { - const dataflow = topologica({ + const topologica = Topologica({ b: [({a}) => a + 1, 'a'], c: [({b}) => b + 1, 'b'] }); - dataflow({ + topologica({ a: 5 }); - assert.equal(dataflow.c, 7); + assert.equal(topologica.c, 7); }); it('Should handle case of 2 inputs.', () => { - const dataflow = topologica({ + const topologica = Topologica({ fullName: [ ({firstName, lastName}) => `${firstName} ${lastName}`, 'firstName, lastName' ] }); - dataflow({ + topologica({ firstName: 'Fred', lastName: 'Flintstone' }); - assert.equal(dataflow.fullName, 'Fred Flintstone'); + assert.equal(topologica.fullName, 'Fred Flintstone'); }); it('Should accept an array of strings as inputs.', () => { const fullName = ({firstName, lastName}) => `${firstName} ${lastName}`; fullName.inputs = ['firstName', 'lastName']; - const dataflow = topologica({ fullName }); - dataflow({ + const topologica = Topologica({ fullName }); + topologica({ firstName: 'Fred', lastName: 'Flintstone' }); - assert.equal(dataflow.fullName, 'Fred Flintstone'); + assert.equal(topologica.fullName, 'Fred Flintstone'); }); it('Should accept a comma delimited string as inputs.', () => { const fullName = ({firstName, lastName}) => `${firstName} ${lastName}`; fullName.inputs = 'firstName, lastName'; - const dataflow = topologica({ fullName }); - dataflow({ + const topologica = Topologica({ fullName }); + topologica({ firstName: 'Fred', lastName: 'Flintstone' }); - assert.equal(dataflow.fullName, 'Fred Flintstone'); + assert.equal(topologica.fullName, 'Fred Flintstone'); }); it('Should accept reactive function as an array.', () => { @@ -101,12 +101,12 @@ describe('topologica.js', () => { ({firstName, lastName}) => `${firstName} ${lastName}`, ['firstName', 'lastName'] ] - const dataflow = topologica({ fullName }); - dataflow({ + const topologica = Topologica({ fullName }); + topologica({ firstName: 'Fred', lastName: 'Flintstone' }); - assert.equal(dataflow.fullName, 'Fred Flintstone'); + assert.equal(topologica.fullName, 'Fred Flintstone'); }); it('Should accept reactive function as an array, with inputs as a string.', () => { @@ -114,55 +114,55 @@ describe('topologica.js', () => { ({firstName, lastName}) => `${firstName} ${lastName}`, 'firstName,lastName' ] - const dataflow = topologica({ fullName }); - dataflow({ + const topologica = Topologica({ fullName }); + topologica({ firstName: 'Fred', lastName: 'Flintstone' }); - assert.equal(dataflow.fullName, 'Fred Flintstone'); + assert.equal(topologica.fullName, 'Fred Flintstone'); }); it('Should only execute when all inputs are defined.', () => { - const dataflow = topologica({ + const topologica = Topologica({ fullName: [ ({firstName, lastName}) => `${firstName} ${lastName}`, 'firstName, lastName' ] }); - dataflow({ + topologica({ lastName: 'Flintstone' }); - assert.equal(dataflow.fullName, undefined); + assert.equal(topologica.fullName, undefined); - dataflow({ + topologica({ firstName: 'Wilma' }); - assert.equal(dataflow.fullName, 'Wilma Flintstone'); + assert.equal(topologica.fullName, 'Wilma Flintstone'); }); it('Should handle case of 3 inputs.', () => { - const dataflow = topologica({ + const topologica = Topologica({ d: [({a, b, c}) => a + b + c, 'a,b,c'] }); - dataflow({ + topologica({ a: 5, b: 8, c: 2 }); - assert.equal(dataflow.d, 15); + assert.equal(topologica.d, 15); }); it('Should handle spaces in input string.', () => { - const dataflow = topologica({ + const topologica = Topologica({ d: [({a, b, c}) => a + b + c, ' a , b, c '] }); - dataflow({ + topologica({ a: 5, b: 8, c: 2 }); - assert.equal(dataflow.d, 15); + assert.equal(topologica.d, 15); }); // Data flow graph, read from top to bottom. @@ -174,16 +174,16 @@ describe('topologica.js', () => { // e // it('Should evaluate not-too-tricky case.', () => { - const dataflow = topologica({ + const topologica = Topologica({ b: [({a}) => a + 1, 'a'], d: [({c}) => c + 1, 'c'], e: [({b, d}) => b + d, 'b, d'] }); - dataflow({ + topologica({ a: 1, c: 2 }); - assert.equal(dataflow.e, (1 + 1) + (2 + 1)); + assert.equal(topologica.e, (1 + 1) + (2 + 1)); }); // a @@ -194,21 +194,21 @@ describe('topologica.js', () => { // \ / // e it('Should evaluate tricky case.', () => { - const dataflow = topologica({ + const topologica = Topologica({ b: [({a}) => a + 1, 'a'], c: [({b}) => b + 1, 'b'], d: [({a}) => a + 1, 'a'], e: [({b, d}) => b + d, 'b, d'] }); - dataflow({ + topologica({ a: 5 }); - const a = dataflow.a; + const a = topologica.a; const b = a + 1; const c = b + 1; const d = a + 1; const e = b + d; - assert.equal(dataflow.e, e); + assert.equal(topologica.e, e); }); @@ -222,7 +222,7 @@ describe('topologica.js', () => { // \ / / // h it('Should evaluate trickier case.', () => { - const dataflow = topologica({ + const topologica = Topologica({ b: [({a}) => a + 1, 'a'], c: [({b}) => b + 1, 'b'], d: [({c}) => c + 1, 'c'], @@ -231,10 +231,10 @@ describe('topologica.js', () => { g: [({a}) => a + 1, 'a'], h: [({d, f, g}) => d + f + g, 'd, f, g'] }); - dataflow({ + topologica({ a: 5 }); - const a = dataflow.a; + const a = topologica.a; const b = a + 1; const c = b + 1; const d = c + 1; @@ -242,23 +242,23 @@ describe('topologica.js', () => { const f = e + 1; const g = a + 1; const h = d + f + g; - assert.equal(dataflow.h, h); + assert.equal(topologica.h, h); }); it('Should work with booleans.', () => { - const dataflow = topologica({ + const topologica = Topologica({ b: [({a}) => !a, 'a'] }); - dataflow({ + topologica({ a: false }); - assert.equal(dataflow.b, true); + assert.equal(topologica.b, true); }); it('Should work with async functions.', done => { - const dataflow = topologica({ + const topologica = Topologica({ bPromise: [ - ({a}) => Promise.resolve(a + 5).then(b => dataflow({ b })), + ({a}) => Promise.resolve(a + 5).then(b => topologica({ b })), 'a' ], c: [ @@ -269,16 +269,16 @@ describe('topologica.js', () => { 'b' ] }); - dataflow({ + topologica({ a: 5 }); }); it('Should work with async functions.', done => { - topologica({ + Topologica({ bPromise: [ - dataflow => Promise.resolve(dataflow.a + 5) - .then(b => dataflow({ b })), + topologica => Promise.resolve(topologica.a + 5) + .then(b => topologica({ b })), 'a' ], c: [ @@ -294,43 +294,43 @@ describe('topologica.js', () => { it('Should only propagate changes when values change.', () => { let invocations = 0; - const dataflow = topologica({ + const topologica = Topologica({ b: [({a}) => invocations++, 'a'] }); assert.equal(invocations, 0); - dataflow({ a: 2 }); + topologica({ a: 2 }); assert.equal(invocations, 1); - dataflow({ a: 2 }); + topologica({ a: 2 }); assert.equal(invocations, 1); - dataflow({ a: 99 }); + topologica({ a: 99 }); assert.equal(invocations, 2); }); it('Should propagate changes if a single dependency changes.', () => { let invocations = 0; - const dataflow = topologica({ + const topologica = Topologica({ c: [() => invocations++, 'a, b'] }); assert.equal(invocations, 0); - dataflow({ a: 2, b: 4 }); + topologica({ a: 2, b: 4 }); assert.equal(invocations, 1); - dataflow({ a: 2 }); + topologica({ a: 2 }); assert.equal(invocations, 1); - dataflow({ a: 2, b: 6 }); + topologica({ a: 2, b: 6 }); assert.equal(invocations, 2); }); it('Should be fast.', () => { - const dataflow = topologica({ + const topologica = Topologica({ b: [({a}) => a + 1, 'a'], c: [({b}) => b + 1, 'b'], d: [({c}) => c + 1, 'c'], @@ -344,7 +344,7 @@ describe('topologica.js', () => { for(let j = 0; j < numRuns; j++){ const begin = Date.now(); for(let i = 0; i < 200000; i++){ - dataflow({ a: i }); + topologica({ a: i }); } const end = Date.now(); const time = end - begin; diff --git a/topologica.js b/topologica.js index b9b3bec..0f2b311 100644 --- a/topologica.js +++ b/topologica.js @@ -1,16 +1,16 @@ const keys = Object.keys; export default reactiveFunctions => { - const dataflow = function(stateChange) { + const topologica = function(stateChange) { depthFirstSearch(keys(stateChange).map(property => { - if (dataflow[property] !== stateChange[property]) { - dataflow[property] = stateChange[property]; + if (topologica[property] !== stateChange[property]) { + topologica[property] = stateChange[property]; return property; } })) .reverse() .forEach(invoke); - return dataflow; + return topologica; }; const functions = {}; @@ -21,7 +21,7 @@ export default reactiveFunctions => { }; const allDefined = inputs => inputs - .every(property => dataflow[property] !== undefined); + .every(property => topologica[property] !== undefined); keys(reactiveFunctions).forEach(property => { const reactiveFunction = reactiveFunctions[property]; @@ -39,7 +39,7 @@ export default reactiveFunctions => { functions[property] = () => { if (allDefined(inputs)) { - dataflow[property] = fn(dataflow); + topologica[property] = fn(topologica); } }; }); @@ -65,5 +65,5 @@ export default reactiveFunctions => { return nodeList; } - return dataflow; + return topologica; }; From dc248a2e9df31008a57555638c4b9e21a6d9d314 Mon Sep 17 00:00:00 2001 From: Curran Date: Sun, 14 Oct 2018 14:14:27 +0530 Subject: [PATCH 7/7] Revise README to use new topologica = Topologica() convention --- README.md | 93 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 48 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index b9e416a..d938e24 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# topologica.js +# Topologica.js A library for [reactive programming](https://en.wikipedia.org/wiki/Reactive_programming). Weighs [1KB minified](https://unpkg.com/topologica). This library provides an abstraction for **reactive data flows**. This means you can declaratively specify a [dependency graph](https://en.wikipedia.org/wiki/Dependency_graph), and the library will take care of executing _only_ the required functions to propagate changes through the graph in the correct order. Nodes in the dependency graph are named properties, and edges are reactive functions that compute derived properties as functions of their inputs (dependencies). The order of execution is determined using the [topological sorting algorithm](https://en.wikipedia.org/wiki/Topological_sorting), hence the name _Topologica_. @@ -24,7 +24,7 @@ npm install --save-dev topologica Then import it into your code like this: ```js -import topologica from 'topologica'; +import Topologica from 'topologica'; ``` You can also include the library in a script tag from Unpkg, like this: @@ -33,16 +33,16 @@ You can also include the library in a script tag from Unpkg, like this: ``` -This script tag introduces the global `topologica`. +This script tag introduces the global `Topologica`. ## API Reference -# topologica(reactiveFunctions) +# Topologica(reactiveFunctions) -Constructs a new data flow graph with the given reactiveFunctions argument, an object whose keys are the names of computed properties and whose values are reactive functions. By convention, the variable name `dataflow` is used for instances of `topologica`, because they are reactive data flow graphs. +Constructs a new data flow graph with the given reactiveFunctions argument, an object whose keys are the names of computed properties and whose values are reactive functions. By convention, the variable name `topologica` (lower case) is used for instances created by the `Topologica` constructor. ```js -const dataflow = topologica({ fullName }); +const topologica = Topologica({ fullName }); ``` A reactive function accepts a single argument, an object containing values for its inputs, and has an explicit representation of its inputs. A reactive function can either be represented as a **function** with an _inputs_ property, or as an **array** where the first element is the function and the second element is the inputs. Dependencies can be represented either as an array of property name strings, or as a comma delimited string of property names. @@ -88,14 +88,14 @@ fullName.inputs = This table shows all 4 ways of defining a reactive function, each of which may be useful in different contexts. * **inputs** If you are typing the inputs by hand, it makes sense to use the comma-delimited string variant, so that you can easily copy-paste between it and a destructuring assignment (most common case). If you are deriving inputs programmatically, it makes sense to use the array variant instead. - * **reactive functions** If you want to define a reactive function in a self-contained way, for example as a separate module, it makes sense to use the variant where you specify `.inputs` on a function (most common case). If you want to define multiple smaller reactive functions as a group, for example in the statement that constructs the `dataflow` instance, then it makes sense to use the more compact two element array variant. + * **reactive functions** If you want to define a reactive function in a self-contained way, for example as a separate module, it makes sense to use the variant where you specify `.inputs` on a function (most common case). If you want to define multiple smaller reactive functions as a group, for example in the statement that constructs the `topologica` instance, then it makes sense to use the more compact two element array variant. -# dataflow(stateChange) +# topologica(stateChange) Performs a shallow merge of `stateChange` into the current state, and propages the change through the data flow graph (synchronously) using topological sort. You can use this to set the values for properties that reactive functions depend on. If a property is not included in `stateChange`, it retains its previous value. ```js -dataflow({ +topologica({ firstName: 'Fred', lastName: 'Flintstone' }); @@ -107,14 +107,16 @@ If a property in `stateChange` is equal to its previous value using strict equal If a property in `stateChange` is not equal to its previous value using strict equality (`===`), it _is_ considered changed, and reactive functions that depend on it _will_ be invoked. This can be problematic if you're passing in callback functions and defining them inline in each invocation. For this case, consider defining the callbacks once, and passing in the same reference on each invocation ([example](https://vizhub.com/curran/27c261085d8a48618c69f7983672903b)), so that the strict equality check will succeed. -# dataflow.get() -Gets the current state of all properties, including derived properties. +# topologica +The `topologica` instance exposes the current state of all properties, including derived properties. For example: ```js console.log(dataflow.fullName); // Prints 'Fred Flintstone' ``` -Assigning values directly to the `dataflow` object (for example `dataflow.firstName = 'Wilma'`) will _not_ trigger reactive functions. Use [dataflow as a function](#set) instead. +There are no other properties on this object except for user-defined properties that are part of the data flow graph. + +**Nota bene** Assigning values directly to the `topologica` object (for example `topologica.firstName = 'Wilma'`) will _not_ trigger reactive functions. Use [topologica as a function](#set) instead. ## Usage Examples @@ -124,7 +126,7 @@ External running examples: * [Bowl of Fruit - Topologica Experiment](https://vizhub.com/curran/27c261085d8a48618c69f7983672903b) - A proposed approach for using Topologica with D3. * [Topologica Layers Experiment](https://vizhub.com/curran/f26d83673fca4d17a7579f3fdba400d6) - Experiment with interactive highlighting. -You can define _reactive functions_ that compute properties that depend on other properties as input. These properties exist on instances of `topologica`, so in a sense they are namespaced rather than free-floating. For example, consider the following example where `b` gets set to `a + 1` whenever `a` changes. +You can define _reactive functions_ that compute properties that depend on other properties as input. These properties exist on `topologica` instances, so they are namespaced within the instances, rather than free-floating. For example, consider the following example where `b` gets set to `a + 1` whenever `a` changes. ```javascript // First, define a function that accepts an options object as an argument. @@ -134,13 +136,13 @@ const b = ({a}) => a + 1; b.inputs = ['a']; // Pass this function into the Topologica constructor. -const dataflow = topologica({ b }); +const topologica = Topologica({ b }); // Setting the value of a will synchronously propagate changes to B. -dataflow({ a: 2 }); +topologica({ a: 2 }); -// You can use dataflow to retreive computed values. -assert.equal(dataflow.b, 3); +// You can use topologica to retreive computed values. +assert.equal(topologica.b, 3); ```

@@ -158,9 +160,9 @@ b.inputs = ['a']; const c = ({b}) => b + 1; c.inputs = ['b']; -const dataflow = topologica({ b, c }); -dataflow({ a: 5 }); -assert.equal(dataflow.c, 7); +const topologica = Topologica({ b, c }); +topologica({ a: 5 }); +assert.equal(topologica.c, 7); ```

@@ -174,14 +176,14 @@ assert.equal(dataflow.c, 7); Here's an example that uses an asynchronous function. There is no specific functionality in the library for supporting asynchronous functions differently, but this is a recommended pattern for working with them: * Use a property for the promise itself, where nothing depends on this property. - * Call `dataflow` asynchronously after the promise resolves. + * Call `topologica` asynchronously after the promise resolves. ```javascript -topologica({ +Topologica({ bPromise: [ - dataflow => Promise - .resolve(dataflow.a + 5) - .then(b => dataflow({ b })), + topologica => Promise + .resolve(topologica.a + 5) + .then(b => topologica({ b })), 'a' ], c: [ @@ -193,7 +195,7 @@ topologica({ })({ a: 5 }); ``` -Note that `dataflow` is passed into reactive functions, so you can invoke it asynchronously if required (without the need to assign the Topologica instance to a variable in scope). +Note that `topologica` is passed into reactive functions, so you can invoke it asynchronously if required (without the need to assign the Topologica instance to a variable in scope).

@@ -211,17 +213,17 @@ Here's an example that computes a person's full name from their first name and a const fullName = ({firstName, lastName}) => `${firstName} ${lastName}`; fullName.inputs = 'firstName, lastName'; -const dataflow = topologica({ fullName }); +const topologica = Topologica({ fullName }); -dataflow({ firstName: 'Fred', lastName: 'Flintstone' }); -assert.equal(dataflow.fullName, 'Fred Flintstone'); +topologica({ firstName: 'Fred', lastName: 'Flintstone' }); +assert.equal(topologica.fullName, 'Fred Flintstone'); ``` Now if either firstName or `lastName` changes, `fullName` will be updated (synchronously). ```js -dataflow({ firstName: 'Wilma' }); -assert.equal(dataflow.fullName, 'Wilma Flintstone'); +topologica({ firstName: 'Wilma' }); +assert.equal(topologica.fullName, 'Wilma Flintstone'); ```

@@ -233,7 +235,7 @@ assert.equal(dataflow.fullName, 'Wilma Flintstone'); Here's the previous example re-written to specify the reactive function using a two element array with inputs specified as a comma delimited string. This is the form we'll use for the rest of the examples here. ```js -const dataflow = topologica({ +const topologica = Topologica({ fullName: [ ({firstName, lastName}) => `${firstName} ${lastName}`, 'firstName, lastName' @@ -244,7 +246,7 @@ const dataflow = topologica({ You can use reactive functions to trigger code with side effects like DOM manipulation. ```js -const dataflow = topologica({ +const topologica = Topologica({ fullName: [ ({firstName, lastName}) => `${firstName} ${lastName}`, 'firstName, lastName' @@ -257,6 +259,8 @@ const dataflow = topologica({ assert.equal(d3.select('#full-name').text(), 'Fred Flintstone'); ``` +### The Tricky Case + Here's the tricky case, where breadth-first or time-tick-based propagation fails (e.g. `when` in RxJS) but topological sorting succeeds.

@@ -264,19 +268,19 @@ Here's the tricky case, where breadth-first or time-tick-based propagation fails

```js -const dataflow = topologica({ +const topologica = Topologica({ b: [({a}) => a + 1, 'a'], c: [({b}) => b + 1, 'b'], d: [({a}) => a + 1, 'a'], e: [({b, d}) => b + d, 'b, d'] }); -dataflow({ a: 5 }); +topologica({ a: 5 }); const a = 5; const b = a + 1; const c = b + 1; const d = a + 1; const e = b + d; -assert.equal(dataflow.e, e); +assert.equal(topologica.e, e); ``` For more examples, have a look at the [tests](/test/test.js). @@ -289,20 +293,19 @@ Feel free to [open an issue](https://github.com/datavis-tech/topologica/issues). This library is a minimalistic reincarnation of [ReactiveModel](https://github.com/datavis-tech/reactive-model), which is a re-write of its precursor [Model.js](https://github.com/curran/model). -The minimalism and synchronous execution are inspired by similar features in [Observable](https://beta.observablehq.com). - Similar initiatives: - * [Observable Notebook Runtime](https://github.com/observablehq/notebook-runtime#variable_define) Implements a reactive runtime environment with named variables that update when their inputs change. + * [Observable Notebook Runtime](https://github.com/observablehq/notebook-runtime#variable_define) The reactive runtime of [Observable](https://beta.observablehq.com). + * [Vega Dataflow](https://github.com/vega/vega-dataflow) The reactive runtime for [Vega](https://github.com/vega/vega). * [Mobx](https://github.com/mobxjs/mobx) Very similar library, with React bindings and more API surface area. - * [DVL](https://github.com/vogievetsky/DVL) Early work on reactive data visualizations. - * [ZJONSSON/clues](https://github.com/ZJONSSON/clues) A very similar library based on Promises. - * [Ember Computed Properties](https://guides.emberjs.com/v2.18.0/object-model/computed-properties/) Similar structure of inputs and reactivity. + * [Vue.js Computed Properties](https://vuejs.org/v2/guide/computed.html) + * [RxJS](https://github.com/Reactive-Extensions/RxJS) and [Bacon](https://baconjs.github.io/) Full blown FRP packages. + * Note that RxJS does _not_ use topological sorting, so [the tricky case](#the-tricky-case) introduces [glitches](https://en.wikipedia.org/wiki/Reactive_programming#Glitches). * [AngularJS Dependency Injection](https://docs.angularjs.org/guide/di) Inspired the API for reactive functions. * [AngularJS $digest()](https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$digest) Inspired the "digest" term. - * [RxJS](https://github.com/Reactive-Extensions/RxJS) and [Bacon](https://baconjs.github.io/) Full blown FRP packages. - * [Vue.js Computed Properties](https://vuejs.org/v2/guide/computed.html) - * [Vega Dataflow](https://github.com/vega/vega-dataflow) + * [Ember Computed Properties](https://guides.emberjs.com/v2.18.0/object-model/computed-properties/) Similar structure of inputs and reactivity. + * [DVL](https://github.com/vogievetsky/DVL) Early work on reactive data visualizations. + * [ZJONSSON/clues](https://github.com/ZJONSSON/clues) A very similar library based on Promises. * [Crosslink.js](https://github.com/monfera/crosslink) * [Flyd](https://github.com/paldepind/flyd) * [Javelin](https://github.com/hoplon/javelin)