signal-chain
Advanced tools
Comparing version 0.12.1 to 0.12.2
{ | ||
"name": "signal-chain", | ||
"version": "0.12.1", | ||
"version": "0.12.2", | ||
"author": "Christoph Franke", | ||
@@ -5,0 +5,0 @@ "description": "Declarative Reactive Programming Library", |
123
README.md
@@ -17,3 +17,2 @@ ![size](https://deno.bundlejs.com/badge?q=signal-chain&treeshake=[{+default+as+$+}]) | ||
- **Signal**: A value/state, that runs from top to bottom through a *connected chain*, being manipulated according to the *operators* of the *chain*. | ||
- **Listener**: A subscription to a reactive value. Can be an *operator* in a *chain*. | ||
@@ -33,12 +32,15 @@ ## Example | ||
// serverData is a reactive primitive. | ||
// Whenever the signal reaches the end of the chain, its value is updated. | ||
// The call to connect sends an initial signal immediately. | ||
// Changes to the input primitive will trigger new signals. | ||
const serverData = $.primitive.connect( | ||
// fetchData is a Chain that can listen to the input field | ||
// and fetch data from the server when it changes. | ||
// Until it is connected, it will not do anything, similar to a function definition. | ||
// Once it is connected and a signal arrives, | ||
// the signal will traverse the chain from top to bottom producing a result | ||
const fetchData = $.chain( | ||
input.listen, // listen to changes | ||
$.if(input => input.length > 2, | ||
$.await.latest( | ||
$.await.latest( // will discard all results but the latest | ||
// make http request to search endpoint whenever user input is changed | ||
$.select(input => fetch(`/api/search?q=${input}`).then(res => res.json())), | ||
$.select( | ||
input => fetch(`/api/search?q=${input}`).then(res => res.json() as Promise<string[]>) | ||
), | ||
), | ||
@@ -48,3 +50,4 @@ $.emit([]) // fallback to empty array if input is too short | ||
$.error.handle( | ||
$.error.log('API request failed:') | ||
$.error.log('API request failed:'), | ||
$.effect(error => window.alert(`Error: ${error.toString()}`)), | ||
$.stop() // stop execution of chain here | ||
@@ -54,18 +57,48 @@ ) | ||
// Defines a chain without connecting it. | ||
// An unconnected chain is like a function declaration: | ||
// It does nothing by itself. | ||
const handleDataChain = $.chain( | ||
serverData.listen, // listen to incoming data | ||
$.effect(searchResults => { | ||
// do something with the data | ||
console.log('new search results:', searchResults) | ||
}) | ||
// serverData is a reactive primitive. | ||
// $.primitive.connect takes a chain and connects it. | ||
// It will immediately send a signal through the chain | ||
// and write the result to the serverData primitive until disconnected. | ||
const serverData = $.primitive.connect(fetchData) | ||
// let's presume we have a filter string that can be changed by the user | ||
const filter = $.primitive.create('some filter string') | ||
// We can combine the server data and the filter to produce filtered results | ||
// With $.primitive.connect we can define the chain inline, | ||
// It will be connected immediately. | ||
const filteredResults = $.primitive.connect( | ||
$.combine(serverData.listen, filter.listen), // fires on any change | ||
$.select(([data, filter]) => data.filter(elem => elem.includes(filter))) | ||
) | ||
// Once the chain is connected, it will run until disconnect is called | ||
const disconnect = $.connect(handleDataChain) | ||
// let's define a reactive timer | ||
const timer = $.primitive.create(0) | ||
const intervalId = setInterval(() => { timer.value += 1 }, 1000) // update every second | ||
// If we do not care about the resulting values | ||
// we can use $.connect to connect a chain. | ||
// Here we can also define the chain inline. | ||
const disconnect = $.connect( | ||
timer.listen, | ||
$.await.parallel( // executes and resolves all promises as they come in | ||
// post tracking data to server | ||
$.select(() => fetch('/api/tracking/impressions', { | ||
method: 'POST', | ||
body: JSON.stringify(filteredResults.value) | ||
})), | ||
), | ||
$.error.discard(), // we want only success here | ||
$.if(res => res?.status == 200, | ||
$.effect(() => console.log('Impression request success')) | ||
) | ||
) | ||
serverData.disconnect() // stop fetching | ||
filteredResults.disconnect() // stop filtering | ||
disconnect() // stop sending tracking data | ||
clearInterval(intervalId) // stop timer | ||
``` | ||
When a *Chain* is *connected*, a *Signal* of runs from top to bottom through the *Chain*. Each *Operator* can change, delay, or stop the *Signal*. Some *Operators*, like the *listener* can trigger a new *Signal*. *Chains* can have output data, that can be stored in a *Primitive*. *Chains* can also have input data, like a function or a procedure that needs some arguments to run. *Chains* can also be chained together into new *Chains*, making them flexible and reusable. *Primitives*, *Operators* and *Chains* are fully typed and types are automatically inferred, making it impossible to chain incompatible data. | ||
When a *Chain* is *connected*, a *Signal* of runs from top to bottom through the *Chain*. Each *Operator* can change, delay, or stop the *Signal*. Some *Operators* can trigger a new *Signal*. *Chains* can have output data, that can be stored in a *Primitive*. *Chains* can also have input data, like a function or a procedure that needs some arguments to run. *Chains* can also be chained together into new *Chains*, making them flexible and reusable. *Primitives*, *Operators* and *Chains* are fully typed and types are automatically inferred, making it impossible to chain incompatible data. | ||
@@ -98,3 +131,3 @@ Error handling in Javascript is difficult: All kinds of things can throw. When a *Chain* throws, it breaks. **Signal-Chain** provides *Error Handling Operators* to catch and handle errors effectively, following the principles of *Errors as Value*. | ||
We can also listen to changes of the counter. | ||
We can listen to changes of the counter. | ||
```typescript | ||
@@ -125,3 +158,3 @@ const disconnect = counter.listen(newValue => { | ||
This example has a serious downside though: We remove and append the element each time a value is updated, which feels somewhat wasteful. We can improve on this by using the parameter passed to the cleanup: A `boolean` that is true on disconnect. | ||
This example has a serious downside though: We remove and append the element each time a value is updated, which is inefficient. We can improve on this by using the parameter passed to the cleanup: A `boolean` that is true only on disconnect. | ||
@@ -160,3 +193,3 @@ ```typescript | ||
The *Chain* type is broken down into multiple subtypes: | ||
The *Chain* type is broken down into four subtypes: | ||
- `SyncChain`: This *Chain* executes **synchronously** and will **complete** | ||
@@ -167,4 +200,6 @@ - `AsyncChain`: This *Chain* executes **asynchronously** and will **complete** | ||
These types give static hints about the behaviour of the *Chain* and when used with `$.evaluate` or `$.function` will produce slightly different results. | ||
These types give static hints about the behaviour of the *Chain* and effect the result types of `$.evaluate` and `$.function`. | ||
> This is similar to **function coloring**: as soon as an Async Operator is inserted into the chain, the whole Chain becomes an *AsyncChain*, and with that immediately all other chains that use the now Async Chain. The same happens if you introduce an Operator that can stop the chain: It becomes a *WeakChain* and with it all its dependants. The good news is, that this happens **statically** in Typescript, so you know what to expect depending on the type. Of course, this is being **inferred** from the types of Operators uesd in the Chain and requires no manual adjustment. | ||
Once we connect the chain, it will start listening to the counter. Note that it executes immediately and synchronously on connection. | ||
@@ -193,7 +228,7 @@ ```typescript | ||
> The updates follow a push model: A connected chain will run, no matter how many subscribers its computed primitive has. | ||
> Updates follow a **push model**: A connected chain will run, no matter how many subscribers its computed primitive has. | ||
### Reusability | ||
In the above example, we squared a counter value. Sometimes, we want to specify behaviour, but want to be able to apply it to different sources later. We can do that, by creating a chain that requires an input value. | ||
In the above example, we squared a counter value. Sometimes, we want to specify behaviour, but want to be able to apply it to different sources later. We can do that, by creating a chain that requires an **input value**. | ||
```typescript | ||
@@ -229,3 +264,3 @@ const appleFormat = $.chain( | ||
This results in `appleFormat` being of type `WeakChain<number, string>`, a *Chain* that requires a `number` input and will produce a `string` output or not finish. | ||
This results in `appleFormat` being of type `WeakChain<number, string>`, a *Chain* that requires a `number` input and will produce a `string` output or not complete. | ||
@@ -252,3 +287,3 @@ We can now use this chain to format a number. | ||
Admittedly, this type of formatting could have been done more easy with a traditional function. Let us take this approach and combine it with some asynchronous logic. This is a main strength of **Signal-Chain**: It allows to define asynchronous reactive behaviour in a declaritive way. | ||
Admittedly, this type of formatting could have been done easily with a traditional function. Let us take this approach and combine it with some asynchronous logic. This is a main strength of **Signal-Chain**: It allows to define asynchronous reactive behaviour in a declaritive way. | ||
@@ -304,6 +339,6 @@ Here, we will implement an auto suggest feature, that fetches some data from an API and logs the result. | ||
Let's have a look at the debounce part: | ||
- The `$.await.latest` operator will pass on the latest resolved value. If a value is incoming while the previous promise is still pending, the previous promise will be cancelled and the resolve of the new one is awaited instead. | ||
- Together with the wait function, this will effectively create a debounce, only passing on the input when there is no new value for 150 ms. | ||
- The `$.await.latest` operator will execute the *inner chain* whenever a new value arrives. It will pass on the latest resolved value. If a value is incoming while the previous promise is still pending, the previous promise will be discarded and the resolve of the new one is awaited instead. | ||
- Together with the wait function, this will effectively create a debounce, only passing on the input when there is no new input for 150 ms. | ||
The `$.await.latest` operator will resolve the promise or pass on an `Error` if the promise is rejected. Its output type is `TypeOfPromiseResolve | Error`. In this case we know, that `wait` cannot reject, so we can safely discard the error. | ||
The `$.await.latest` operator will (like all promise resolvers) resolve the promise or pass on an `Error` if the promise is rejected. Its output type is `TypeOfPromiseResolve | Error`. In this case we know, that `wait` cannot reject, so we can safely discard the error. | ||
@@ -315,10 +350,10 @@ This design follows the principle of **errors as values**. It reminds the developer that something can go wrong here and need be handled. If we remove the error handling code from the *Chain*, the resulting suggestion pimitive would have an inferred type of `string[] | Error`. Because the promise is being used inside `$.await.latest`, the rejection will be caught and passed on. | ||
The `$.await.latest` is also exactly what we want in fetching data. If a new input is given while the previous request is still pending, the previous request will be discarded. This is similar to the RxJS behviour of `switchMap`. For other scenarios there are 4 more await operators with different strategies: | ||
- `$.await.parallel`: Passes on each resolved value as soon as it resolves. | ||
- `$.await.order`: Passes on resolved values in the order they were requested. | ||
- `$.await.block`: Will only enter the inner block when no promise is pending. Incoming values will be discarded. | ||
- `$.await.queue`: Will only enter the inner block when no promise is pending. Incoming values will be queued and processed by the inner block once the pending promise is resolved. | ||
- `$.await.parallel`: Executes eager and passes on each resolved value as soon as it resolves. | ||
- `$.await.order`: Executes eager and passes on resolved values in the order they were requested. | ||
- `$.await.block`: Will not execute when a promise is already running. Incoming values will be ignored and discarded. | ||
- `$.await.queue`: Will execute one promise at a time. Incoming values will be queued and processed by the inner block once the currently pending promise is resolved. | ||
### Reactivity with Plain Objects | ||
Sometimes you may work with existing logic, or maybe you prefer to store our state in plain objects. **Signal-Chain** can listen to plain objects and arrays using proxies. | ||
If you work with existing logic, or your state becomes big enough, you may prefer to store state in plain objects. **Signal-Chain** can listen to plain objects and arrays using proxies. | ||
@@ -338,3 +373,3 @@ Let's assumet we have a state object like this: | ||
And let's further assume there is pre-existing logic spread out over the source code, that sets the filter and the elements. We can still listen to changes of the filter and the elements: | ||
We can listen to changes of the filter and the elements like this: | ||
```typescript | ||
@@ -346,12 +381,15 @@ const filter = $.primitive.connect( | ||
const elements = $.primitive.connect( | ||
$.emit(state), // emit the state object | ||
$.listen.key('elements'), // listen to changes in the elements key | ||
$.emit(state), | ||
$.listen.key('elements'), | ||
) | ||
``` | ||
- The `$.emit` operator has no input and emits the passed argument. | ||
- The `$.listen.key` operator will listen to changes in the given key of the incoming object. Whenever the value changes, it will fire. If the value of the key is an array type, the listener will be attached to the array itself via proxy, so that any changes to the array will also be detected. | ||
- The `$.listen.key` operator will listen to changes in the given key of the incoming object. Whenever an observed value changes, it will fire. | ||
In order for the `$.listen.key` operator to work on the object, it inserts a proxy in place of the object key resp. array. The proxy is designed to not interfere with the object itself, but it will make the object appear slightly different, for example on `console.log` operations. | ||
The `$.listen.key` listener is **semi-shallow**, that means: If the value of the key is a **Primitive value** (`string`, `number` etc...), changes will be detected. If it is an **Array of Primitives**, changes will be detected. If it is an **Object** or an **Array of Objects**, only reassignments will be detected, none of their property changes. To achieve that, use another `$.listen.key` and/or combine it with `$.each`, as done in the following examples. | ||
> The `$.listen.key` operator inserts a proxy in place of the object key resp. array. This allows normal read and write operations to the object keys and values. | ||
> There is currently no support for reactive Object iterators, like Object.keys, Object.entries etc. | ||
This is how we could implement a reactive filter: | ||
@@ -366,2 +404,3 @@ ```typescript | ||
([elements, filter]) => elements.filter( | ||
// Don't do this, this will NOT be fully reactive: | ||
element => element.name.includes(filter) | ||
@@ -368,0 +407,0 @@ ) |
306351
649