npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

topologica

v3.2.0

Published

Minimal library for reactive dataflow programming.

Downloads

44

Readme

Topologica.js

A library for reactive programming. Weighs 1KB minified.

This library provides an abstraction for reactive data flows. This means you can declaratively specify a 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, hence the name Topologica.

Topologica is primarily intended for use in optimizing interactive data visualizations created using D3.js 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.

Why use topological sorting? To avoid inconsistent state. In the following data flow graph, propagation using breadth-first search (which is what Model.js and some other libraries use) would cause e to be set twice, and the first time it would be set with an inconsistent state (as occurs with "glitches" in reactive programming). Using topological sorting for change propagation guarantees that e will only be set once, and there will never be inconsistent states.

Installing

You can install via NPM like this:

npm install --save-dev topologica

Then import it into your code like this:

import Topologica from 'topologica';

You can also include the library in a script tag from Unpkg, like this:

<script src="https://unpkg.com/[email protected]/dist/topologica.min.js"></script>

This script tag introduces the global Topologica.

API Reference

# 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.

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.

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.

# dataflow.set(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.

dataflow.set({
  firstName: 'Fred',
  lastName: 'Flintstone'
});

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.

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 when changing objects and arrays.

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), so that the strict equality check will succeed.

# dataflow.get() Gets the current state of all properties, including derived properties.

const state = dataflow.get();
console.log(state.fullName); // Prints 'Fred Flintstone'

Assigning values directly to the returned state object (for example state.firstName = 'Wilma') will not trigger reactive functions. Use set instead.

Usage Examples

External running examples:

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.

// 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'];

// Pass this function into the Topologica constructor.
const dataflow = Topologica({ b });

// Setting the value of a will synchronously propagate changes to B.
dataflow.set({ a: 2 });

// You can use dataflow.get to retreive computed values.
assert.equal(dataflow.get().b, 3);

Here's an example that assigns b = a + 1 and c = b + 1.

const b = ({a}) => a + 1
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);

Note that set returns the Topologica instance, so it is chainable.

Asynchronous Functions

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.
const dataflow = Topologica({
  bPromise: [
    ({a}) => Promise.resolve(a + 5).then(b => dataflow.set({ b })),
    'a'
  ],
  c: [
    ({b}) => {
      console.log(b); // Prints 10
    },
    'b'
  ]
});
dataflow.set({ a: 5 });

Complex Dependency Graphs

The dependency graphs within an instance of Topologa can be arbitrarily complex directed acyclic graphs. This section shows some examples building in complexity.

Here's an example that computes a person's full name from their first name and and last name.

const fullName = ({firstName, lastName}) => `${firstName} ${lastName}`;
fullName.dependencies = 'firstName, lastName';

const dataflow = Topologica({ fullName });

dataflow.set({ firstName: 'Fred', lastName: 'Flintstone' });
assert.equal(dataflow.get().fullName, 'Fred Flintstone');

Now if either firstName or lastName changes, fullName will be updated (synchronously).

dataflow.set({ firstName: 'Wilma' });
assert.equal(dataflow.get().fullName, 'Wilma Flintstone');

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.

const dataflow = Topologica({
  fullName: [
    ({firstName, lastName}) => `${firstName} ${lastName}`,
    'firstName, lastName'
  ]
});

You can use reactive functions to trigger code with side effects like DOM manipulation.

const dataflow = Topologica({
  fullName: [
    ({firstName, lastName}) => `${firstName} ${lastName}`,
    'firstName, lastName'
  ]
  fullNameText: [
    ({fullName}) => d3.select('#full-name').text(fullName),
    'fullName'
  ]
});
assert.equal(d3.select('#full-name').text(), 'Fred Flintstone');

Here's the tricky case, where breadth-first or time-tick-based propagation fails (e.g. when in RxJS) but topological sorting succeeds.

const dataflow = Topologica({
  b: [({a}) => a + 1, 'a'],
  c: [({b}) => b + 1, 'b'],
  d: [({a}) => a + 1, 'a'],
  e: [({b, d}) => b + d, 'b, d']
});
dataflow.set({ 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);

For more examples, have a look at the tests.

Contributing

Feel free to open an issue. Pull requests for open issues are welcome.

Related Work

This library is a minimalistic reincarnation of ReactiveModel, which is a re-write of its precursor Model.js.

The minimalism and synchronous execution are inspired by similar features in Observable.

Similar initiatives:

See also this excellent article State management in JavaScript by David Meister.