SkillAgentSearch skills...

Causality

A library for reactive programming based on Javascript proxies (ES6)

Install / Use

/learn @erobwen/Causality
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

Causality

Alt text

Reactive programming for simple, scalable state management, using ES6 proxies.

Installation

Installation: npm install causalityjs --save

Usage

It is possible to create several instances of causality, called worlds, each with its separate config and dependencies. As the name implies, you typically just create one single causality world for your app to use. But you can sometimes benefit from created several isolated worlds where causality isolates observation and event propagation within each world.

import getWorld from "causality";
let myWorld = getWorld({name: "myWorld", ...moreOptions}); // Create an instance with possibility to configure it.
let { observable, repeat } = myWorld;

Calling getWorld multiple times with the same unique name given in the configuration, will return the same world object. Only the options given the first time will be considered. For consequtive calls with the same world name, the options will be ignored.

Quick Example

This is just to show a simple example of what causality is all about, using the simple repeat primitive.

import getWorld from "causality";
let { observable, repeat } = getWorld();

let x = observable({propA: 11});
let y = observable({propB: 11, propC: 100});
let z;

repeat(function(){
    z = x.propA + y.propB;  // Sets up a reactive setting of variable z
});
// z is now 22

y.propB = 2;      // Setting of propB will cause reevaluation of z
// z is now 13

x.propA = 2;
// z is now 4

However, there are a lot more interesting features to try out.

Features

observable

With observable you simply create a causality object. The observable function takes any other Javascript object as input. Example usage:

observable({a: 1, b: 2, c: 3});
observable([1, 2, 3]);
observable(new MyClass());

A causality object is an object that can be observed by causality. This means that changes in causality objects will be detected automatically by the causality framework. While it is possible to mix the use of causality object and plain Javascript object, it is not recommended to do so, as changes in plain Javascript objects will go undetected by causality.

In every other aspect, a causality object behaves just as an ordinary Javascript object would. So you could for example write:

var x = observable({a: 1, b: 2});
var y = x.a + b.2; // should result in 3!

var l = observable([]);
l.push("item1");
l.push("item2");
l.pop();
console.log(l); // should print out ["item1"];

If you find it too cumbersome to write "observable" upon every object creation, you can create an alias "o":

o({a: 1, b: 2});

To access the inner workings of a causality object, you can access it by the causality property. For example write object.causality.id to get a system wide unique id for that particular object.

repeat (MobX autorun)

Repeat is typically used to enforce some certain data constraint. Such as reactive validation of a form, or calculation of some. In its basic form, it is simply a function that is reevaluated every time any of the data it read changes. For example:

repeat(function() {  x.value = y.value + z.value; });

This will cause x.value to be assigned to y.value + z.value any time either y.value or z.value changes. The good part is that you can write any kind of code inside the function. There can be loops, function calls, recursive functions. Anything. And no matter what code is there, causality will always keep track of what data has been read by the repeater function at any given moment.

It will however not detect changes in local variables, so for example if local or global variable y is assigned in this example, there will be no reevaluation of x.value. In practice however, this is in general not a limitation as application code typically reacts to changes to a specific model, rather than changes in local variables.

Using repeat with rebuilding keys

repeat is equipped with some special features when it comes to creating data structures. Assume that you have an algorithm that constructs a data structure. For example a balanced index structure of some sort. However, when the input data is changed and the algorithm needs to repeat once more, you do not want a completely new output data structure. Instead, you want to execute the algorithm once more, and at the end of it, compare the difference to the result of the last run, and update the previous result in a minimal way. This is something that is supported with causality!

React users might notice that this is actually a generalization of how React works with the synthetic dom. But instead of being limited to just dom structures, causality can do this trick for any data structure!

For example:

repeat(() => {
    for(let input of inputArray) {
        addToIndex(observable({value: input.value},"buildId" + input.id))
    }
})

Note that the only difference is that we added a unique build id for each created object in the data structure. This way causality will know what previously created object correspond to the objects created at each repetition.

Using repeat with priority

When building a framework, such as a front end framework, it becomes apparent that some repeats should have higher priority than others. For example, if we have a rendering pipeline, that starts with model, view model and then ends with view. It would be highly innefficient if for every change in the model, we would update the view. Because say perhaps that there are 3 changes on the model level, that causes 9 changes in the view model level, that again causes 18 changes on the view itself. We want to finish all changes on the model level, before we start to work on the view model level, and we want to finish all changes on the view model, before we start to update the view. To do this, we need to prioritize repeaters.

You want the update to propagate through your dependency network breadth first for minimal re-work.

For example:

repeat(() => updateViewModel(), {
    priority: 1
})

To do this, causality has a limited number of priority levels to divide repeaters into. When you execute a repeat operation, you can set the priority of it. The implementation works with a short array, so you should not have more than 8 different priority levels in your application.

Also, in addition to using priority levels. causality internally uses a queue for repeater updates, and this by itself is a heuristic that makes it more likley for changes to propagate breadth first. But depending on your dependencies, reevaluation at some stage could potentially occur, so priority levels helps to enforce some degree of breadth first.

Separate cause and effect (MobX reaction)

A repeater can mix cause and effect, and there are some preventive measures in place to prevent a repeater from activating itself. So, in many cases it is safe to use a repeater with a single action (comparable to autorun in MobX). However, there could be situations where you would like to distinguish more between cause and effect. To do that, a second argument for the repeater is an action that is not recorded.

repeater([description for debug], recordedAction, nonRecordedAction, options)

The options object contains further configurations for the non-recorded action. Namley debounce and fireImmediatley.

The return value of the recorded action will be sent as an argument to the nonRecorded action, similar to how reaction works in MobX.

invalidateOnChange

There is a famous quote from programmer Jeff Atwood (author of blog Coding Horrors):

There are two hard things in computer science: cache invalidation, naming things, and off-by-one errors.

Well, at least cache invalidation just got much more simple thanks to invalidateOnChange. While you could use a repeat to invalidate a cache, invalidateOnChange is a more basic primitive that is optimized for this specific purpose. For example, if a cache is filled up using lazy evaluation, it is convenient to just clear the cache without any direct reevaluation.

Here is an example:

invalidateOnChange(
    function() {
        x.value = y.value + z.value; xIsValid = true;
    },
    function() {
        xIsValid = false;
    }
);

In this case, if y.value is changed for instance, it will only mean that the second function is run, setting xIsValid to false. invalidateOnChange is in particularly useful when integrating causality with other frameworks. For example, rendering code could be run using invalidateOnChange, and the second function could simply invalidate a certain view-component.

withoutRecording and withoutReactions

When working with causality it could be useful to sometimes break the rules. Reading data without creating a dependency could for example be useful for debug printouts. In the following code, the debug printout itself would create a false dependency on z.value if it wasnt for the withoutRecording clause.

let x = null
let y = observable({ value: false });
let z = observable({ value: 10});

repeat(function() {
    withoutRecording(function() {
        console.log("Repeating with these values:");
        console.log(y.value);
        console.log(z.value);
    })
    if (y.value) {
        x = z.value;
    } else {
        x = 42;
    }
}

// No one is going to notice!
withoutReactions(function() {
    y.value = true;
});

There are probably less use cases for being able to change data without triggering any reactions, using withoutReactions, it is available nevertheless.

Emiting Events

Somtimes you just need to observe objects, and record events. For this purpose you can set an onEventGlobal callback in the causality configuration. This callback ca

View on GitHub
GitHub Stars24
CategoryDevelopment
Updated10mo ago
Forks2

Languages

JavaScript

Security Score

67/100

Audited on May 7, 2025

No findings