Revamp FRPTools docs in preparation for renaming and rehoming
This commit is contained in:
parent
90461529d6
commit
dfb7c31840
308
README.md
308
README.md
@ -1,296 +1,38 @@
|
|||||||
# FRP tools
|
# Reactimal
|
||||||
|
|
||||||
Property, Container, Computed and Stream value stores designed to work together for storing discrete
|
Reactimal is a set of tools that can be used to express your code as a logic graph using reactive
|
||||||
and derived state.
|
programming methods.
|
||||||
|
|
||||||
# [property](./src/property.js)
|
In computer science, we learn that a function is a process with some inputs and an output.
|
||||||
|
Imperative programming languages focus on constructing the operations that convert inputs to output.
|
||||||
|
Since a program is a collection of functions, there is inevitably shared state that the many
|
||||||
|
functions act on. In this situation, one value in the shared state could be updated through any
|
||||||
|
number of functions. The situation that causes this is called "spaghetti code" where code size grows
|
||||||
|
to the point such that it's difficult to understand the code path that each task takes through the
|
||||||
|
code base.
|
||||||
|
|
||||||
A `property` is a simple value store that can report when its value changes. It is good for wrapping
|
To avoid this situation, Reactimal maintains links to the whole logic graph so it's easy to see what
|
||||||
external values passed into a component so compute types can depend on them and only recompute when
|
outputs come from what inputs.
|
||||||
these values change. It can also be used to receive events such as _window.onresize_ to always
|
|
||||||
provide the current viewport size.
|
|
||||||
|
|
||||||
## Usage
|
Reactimal provides the following reactive programming primitives. Each type of primitive is
|
||||||
|
[subscribable](./subscribable.md) and provides a single output value, but differ in how they receive
|
||||||
|
their input. In short:
|
||||||
|
|
||||||
### Creation
|
- A [property](./docs/property.md) directly receives a single value as an entry point to a logic
|
||||||
|
graph
|
||||||
|
- A [container](./docs/container.md) wraps javascript container objects and triggers an update when
|
||||||
|
a property changes.
|
||||||
|
- A [computed](./docs/computed.md) only receives values from other subscribables.
|
||||||
|
- A [stream](./docs/stream.md) is the asynchronous version of a computed.
|
||||||
|
|
||||||
Creates and sets initial value to `true`
|
Reactimal also includes a few [utilities](./docs/utilities.md) that may be useful when building logic
|
||||||
|
graphs.
|
||||||
|
|
||||||
```js
|
## Inspiration
|
||||||
const inViewport = prop(true);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Read
|
Reactimal is the result of years of learning from the following projects:
|
||||||
|
|
||||||
Call it to receive the stored value.
|
|
||||||
|
|
||||||
```js
|
|
||||||
if (inViewport()) {
|
|
||||||
/* inViewport is truthy */
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Change
|
|
||||||
|
|
||||||
Call it passing the new value. If any computed stores depend on this value they will be marked dirty
|
|
||||||
and re-evaluated the next time they are read from.
|
|
||||||
|
|
||||||
```js
|
|
||||||
inViewport(false);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Provide a hash function for complex types
|
|
||||||
|
|
||||||
When storing a type that is not determined to be equal with simple equality (===), provide a hash
|
|
||||||
function to be used for simple comparison to determine if the new provided value should be
|
|
||||||
propagated to dependents.
|
|
||||||
|
|
||||||
```js
|
|
||||||
function hashSet(_a) {
|
|
||||||
if (_a instanceof Set) {
|
|
||||||
return Array.from(_a.keys())
|
|
||||||
.sort()
|
|
||||||
.map(k => `${(typeof k).substr(0, 1)}:${encodeURIComponent(k)}/`)
|
|
||||||
.join('?');
|
|
||||||
}
|
|
||||||
return _a;
|
|
||||||
}
|
|
||||||
|
|
||||||
const a = prop(new Set([1, 2]), hashSet);
|
|
||||||
```
|
|
||||||
|
|
||||||
# [computed](./src/computed.js)
|
|
||||||
|
|
||||||
`computed` is a functional store that depends on the values of properties or other computeds. They
|
|
||||||
derive value from properties rather than store value and hence cannot be set directly.
|
|
||||||
|
|
||||||
## Behavior
|
|
||||||
|
|
||||||
A `computed` will subscribe to its dependencies in such a way that it will be marked as _dirty_ when
|
|
||||||
any dependency changes. Whenever it is read from, if will recompute its result if the _dirty_ flag
|
|
||||||
is set, otherwise it just return the stored result from the last time it computed.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
### Creation
|
|
||||||
|
|
||||||
```js
|
|
||||||
const showDialog = computed(
|
|
||||||
(inVP, shouldShow) => inVP && shouldShow, // computation function
|
|
||||||
[inViewport, shouldShow] // array of dependencies, can be either a property or computed
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Read
|
|
||||||
|
|
||||||
```js
|
|
||||||
if (showDialog()) {
|
|
||||||
/* showDialog() is truthy */
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Call it to receive the stored value, recomputing if necessary.
|
|
||||||
|
|
||||||
### Subscribe to changes
|
|
||||||
|
|
||||||
Call the subscribe method with a callback that will be called when the computed result changes to a
|
|
||||||
different value. The returned function can be called to unsubscribe from the property. When called
|
|
||||||
it will provide the count of remaining subscriptions.
|
|
||||||
|
|
||||||
```js
|
|
||||||
const unsubscribe = inViewport.subscribe(console.log.bind(console));
|
|
||||||
const remainingSubscriptionCount = unsubscribe();
|
|
||||||
```
|
|
||||||
|
|
||||||
**NOTE**: Subscribing to a computed forces it to recompute every time an upstream dependency
|
|
||||||
changes. This could negatively performance if it depends on multiple values that change sequentially
|
|
||||||
and the computation function is non-trivial. For example:
|
|
||||||
|
|
||||||
```js
|
|
||||||
const inViewport = prop(false);
|
|
||||||
const shouldShow = prop(false);
|
|
||||||
|
|
||||||
const showDialog = computed((inVP, shouldShow) => inVP && shouldShow, [inViewport, shouldShow]);
|
|
||||||
|
|
||||||
inViewport(true); // showDialog marked as dirty but does not recompute its stored result.
|
|
||||||
shouldShow(true); // showDialog is already marked as dirty. Nothing else happens.
|
|
||||||
showDialog(); // showDialog recomputes its stored result and unsets the dirty flag.
|
|
||||||
|
|
||||||
// adding a subscription will change showDialog's internal behavior
|
|
||||||
showDialog.subscribe(console.log.bind(console));
|
|
||||||
|
|
||||||
inViewport(false); // showDialog result recomputed and `false` is written to the console.
|
|
||||||
shouldShow(false); // showDialog result recomputed, console.log is not called.
|
|
||||||
showDialog(); // showDialog does not recompute, console.log is not called. `false` is returned.
|
|
||||||
|
|
||||||
showDialog.detach(); // Call detach to remove this computed from the logic tree.
|
|
||||||
showDialog.unsubscribeAll(); // Call unsubscribeAll to remove child property/computed subscriptions.
|
|
||||||
```
|
|
||||||
|
|
||||||
### Provide a hash function for complex types
|
|
||||||
|
|
||||||
When the computed result is a type that is not determined to be equal with simple equality (===),
|
|
||||||
provide a hash function to be used for simple comparison to determine if the new provided value
|
|
||||||
should be propagated to dependents.
|
|
||||||
|
|
||||||
```js
|
|
||||||
function hashSet(_a) {
|
|
||||||
if (_a instanceof Set) {
|
|
||||||
return Array.from(_a.keys())
|
|
||||||
.sort()
|
|
||||||
.map(k => `${(typeof k).substr(0, 1)}:${encodeURIComponent(k)}/`)
|
|
||||||
.join('?');
|
|
||||||
}
|
|
||||||
return _a;
|
|
||||||
}
|
|
||||||
|
|
||||||
function _intersection(a, b) {
|
|
||||||
return new Set([...a].filter(x => b.has(x)));
|
|
||||||
}
|
|
||||||
|
|
||||||
const a = prop(new Set([1, 2]), hashSet);
|
|
||||||
const b = prop(new Set([2, 3]), hashSet);
|
|
||||||
const intersection = computed(_intersection, [a, b], hashSet);
|
|
||||||
```
|
|
||||||
|
|
||||||
# [stream](./src/stream.js)
|
|
||||||
|
|
||||||
`stream` is a computed that works asynchronously. Dependencies can be synchronous or asynchronous
|
|
||||||
functions but the hash function remains synchronous. Also calling a stream multiple overlapping
|
|
||||||
times will return the same promise and result (so long as dependencies do not change in the time
|
|
||||||
gap).
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
### Creation
|
|
||||||
|
|
||||||
```js
|
|
||||||
const getToken = stream(
|
|
||||||
async tokenUrl => fetch(tokenUrl), // computation function
|
|
||||||
[getTokenUrl, tokenTimeout] // array of dependencies, can be a property, computed or stream
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Read
|
|
||||||
|
|
||||||
```js
|
|
||||||
if (await getToken()) {
|
|
||||||
/* do stuff with the token */
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Call it to receive the stored value, recomputing if necessary.
|
|
||||||
|
|
||||||
# [container](./src/container.js)
|
|
||||||
|
|
||||||
`container` is a wrapper around any container type (Object, Set, Map, or Array) while monitoring
|
|
||||||
changes to the container. A container can be subscribed to and `computed` instances can depend on
|
|
||||||
them.
|
|
||||||
|
|
||||||
## Behavior
|
|
||||||
|
|
||||||
Anytime a property is set or a method is gotten and called, the container will check for an updated
|
|
||||||
state and trigger subscribers if it is updated. An optional hash function may be applied to
|
|
||||||
determine updated status. If the hash function is not supplied, every update will be propagated.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
### Creation
|
|
||||||
|
|
||||||
```js
|
|
||||||
const monkeys = contained([], arr => arr.join('$'));
|
|
||||||
const firstMonkey = computed(m => (m.length ? m[0] : null), [monkeys]);
|
|
||||||
firstMonkey.subscribe(console.log.bind(console));
|
|
||||||
```
|
|
||||||
|
|
||||||
### Add a member to the container
|
|
||||||
|
|
||||||
```js
|
|
||||||
monkeys.push('Bill');
|
|
||||||
```
|
|
||||||
|
|
||||||
_firstMonkey_ would be computed and "Bill" would be logged to the console.
|
|
||||||
|
|
||||||
### Access the contained object directly
|
|
||||||
|
|
||||||
Reference the `_` (underscore) property to access the contained object directly.
|
|
||||||
|
|
||||||
```js
|
|
||||||
monkeys._.push('Bill');
|
|
||||||
```
|
|
||||||
|
|
||||||
The array in _monkeys_ would get a new value without _firstMonkey_ being notified of the change.
|
|
||||||
|
|
||||||
# Common Behaviors
|
|
||||||
|
|
||||||
All frptools types have the following methods available:
|
|
||||||
|
|
||||||
## `.subscribe(fn)` - Subscribe to changes
|
|
||||||
|
|
||||||
Call the `subscribe` method with a callback that will be called when the property value changes. The
|
|
||||||
returned function can be called to unsubscribe from the property. When called it will provide the
|
|
||||||
count of remaining subscriptions.
|
|
||||||
|
|
||||||
```js
|
|
||||||
const unsubscribe = inViewport.subscribe(console.log.bind(console));
|
|
||||||
const remainingSubscriptionCount = unsubscribe();
|
|
||||||
```
|
|
||||||
|
|
||||||
## `.unsubscribeAll()` - Remove child subscriptions
|
|
||||||
|
|
||||||
Call the `unsubscribeAll` method to remove all child-node subscriptions.
|
|
||||||
|
|
||||||
```js
|
|
||||||
const unsubscribe = inViewport.subscribe(console.log.bind(console));
|
|
||||||
const remainingSubscriptionCount = unsubscribe();
|
|
||||||
|
|
||||||
inViewport.unsubscribeAll(); // console.log will no longer be called.
|
|
||||||
```
|
|
||||||
|
|
||||||
## `.fire(val)` - Send a value to the node's subscribers
|
|
||||||
|
|
||||||
Call the `fire` method to send a value to each of the node's subscribers. This is designed for the
|
|
||||||
node to use to propagate updates but firing other values could have some uses.
|
|
||||||
|
|
||||||
```js
|
|
||||||
const unsubscribe = inViewport.subscribe(console.log.bind(console));
|
|
||||||
inViewport.fire(false); // "false" logged to console.
|
|
||||||
```
|
|
||||||
|
|
||||||
# Utilities
|
|
||||||
|
|
||||||
## id
|
|
||||||
|
|
||||||
`id(anything) -> anything`
|
|
||||||
|
|
||||||
`id` is a function that returns its first parameter. It is used as the default hash function for
|
|
||||||
each subscribable.
|
|
||||||
|
|
||||||
## call
|
|
||||||
|
|
||||||
`call(anything) -> void`
|
|
||||||
|
|
||||||
`call` will call the first parameter if it is a function. It is used for iterating over
|
|
||||||
subscribables.
|
|
||||||
|
|
||||||
## pick
|
|
||||||
|
|
||||||
`pick(propName, default) -> (obj) -> any`
|
|
||||||
|
|
||||||
`pick` returns a function that accepts an object and will return the object's property for the
|
|
||||||
provided name or the default if one is supplied. `pick` is not directly used with any subscribable
|
|
||||||
but can be useful as the computed function when breaking down a `prop` that contains an object or a
|
|
||||||
container.
|
|
||||||
|
|
||||||
# Inspiration
|
|
||||||
|
|
||||||
FRPTools is the result of years of learning from the following projects:
|
|
||||||
|
|
||||||
- [KnockoutJS](http://knockoutjs.com/)
|
- [KnockoutJS](http://knockoutjs.com/)
|
||||||
- [Overture](https://github.com/fastmail/overture)
|
- [Overture](https://github.com/fastmail/overture)
|
||||||
- [Redux](https://redux.js.org/)
|
- [Redux](https://redux.js.org/)
|
||||||
- [Mithril](https://mithril.js.org/)
|
- [Mithril](https://mithril.js.org/)
|
||||||
|
|
||||||
# Possible Future Plans
|
|
||||||
|
|
||||||
- Invert the param signature to allow currying the hash function in to create a sort of "type".
|
|
||||||
|
|||||||
71
docs/computed.md
Normal file
71
docs/computed.md
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
# computed
|
||||||
|
|
||||||
|
A `computed` is a [subscribable](./subscribable.md), computed value expression. It takes any number
|
||||||
|
of dependent inputs and provides one output. Subscribers are only notified if the computed value
|
||||||
|
changes. `computed` forms the core of a logic graph.
|
||||||
|
|
||||||
|
## Behavior
|
||||||
|
|
||||||
|
A `computed` takes any kind of dependency but if a dependency is a [subscribable](./subscribable.md)
|
||||||
|
then the `computed` will subscribe to it such that the `computed` will be marked as _dirty_ when any
|
||||||
|
that subscribable value changes. Whenever the `computed` is read from, if will recompute its result
|
||||||
|
if the _dirty_ flag has been set, otherwise it just return the stored result from the last time it
|
||||||
|
computed.
|
||||||
|
|
||||||
|
Unlike [KnockoutJS](http://knockoutjs.com/) and [Vuejs](https://vuejs.org), Reactimal's `computed`
|
||||||
|
requires explicit dependency expression to help avoid dependency loops and allow code retain
|
||||||
|
readability as it grows.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Creation
|
||||||
|
|
||||||
|
Creates a computed instance. Internally the passed computation function is called once to establish
|
||||||
|
the initial value.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const showDialog = computed(
|
||||||
|
(inVP, shouldShow) => inVP && shouldShow, // computation function
|
||||||
|
[inViewport, shouldShow] // array of subscribable dependencies
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Read
|
||||||
|
|
||||||
|
Call the computed to receive the computed value. The value will be computed if any upstream
|
||||||
|
dependencies have changed.
|
||||||
|
|
||||||
|
```js
|
||||||
|
if (showDialog()) {
|
||||||
|
/* showDialog() is truthy */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Provide a hash function for complex result types
|
||||||
|
|
||||||
|
When the computed result is a type that is not determined to be equal with simple equality (===),
|
||||||
|
provide a hash function to the `hashableComputed` type to create a computed that will reduce the
|
||||||
|
result value to a value that can be compared with the identity operator before comparison. The
|
||||||
|
computed result value will still be stored unmodified.
|
||||||
|
|
||||||
|
```js
|
||||||
|
function hashSet(_a) {
|
||||||
|
if (_a instanceof Set) {
|
||||||
|
return Array.from(_a.keys())
|
||||||
|
.sort()
|
||||||
|
.map(k => `${(typeof k).substr(0, 1)}:${encodeURIComponent(k)}/`)
|
||||||
|
.join('?');
|
||||||
|
}
|
||||||
|
return _a;
|
||||||
|
}
|
||||||
|
|
||||||
|
const computedSet = hashableComputed(hashSet);
|
||||||
|
|
||||||
|
function _intersection(a, b) {
|
||||||
|
return new Set([...a].filter(x => b.has(x)));
|
||||||
|
}
|
||||||
|
|
||||||
|
const a = prop(new Set([1, 2]), hashSet);
|
||||||
|
const b = prop(new Set([2, 3]), hashSet);
|
||||||
|
const intersection = computedSet(_intersection, [a, b]);
|
||||||
|
```
|
||||||
41
docs/container.md
Normal file
41
docs/container.md
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# container
|
||||||
|
|
||||||
|
`container` is a [subscribable](./subscribable.md)
|
||||||
|
[object proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy)
|
||||||
|
around any container type (Object, Set, Map, or Array) that will propagate the object to any
|
||||||
|
subscribers whenever it changes.
|
||||||
|
|
||||||
|
## Behavior
|
||||||
|
|
||||||
|
Anytime a property is set or a method is called, the container will trigger subscribers. Unlike
|
||||||
|
other subscribables in Reactimal, the identity operator is not sufficient to detect a noteworthy
|
||||||
|
change. To limit propagated changes, build a `hashableContainer` and provide a hash function to
|
||||||
|
determine if the proxied container has changed.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Creation
|
||||||
|
|
||||||
|
```js
|
||||||
|
const monkeys = hashableContainer(arr => arr.join('$'))([]);
|
||||||
|
const firstMonkey = computed(m => (m.length ? m[0] : null), [monkeys]);
|
||||||
|
firstMonkey.subscribe(console.log.bind(console));
|
||||||
|
```
|
||||||
|
|
||||||
|
### Add a member to the container
|
||||||
|
|
||||||
|
```js
|
||||||
|
monkeys.push('Bill');
|
||||||
|
```
|
||||||
|
|
||||||
|
_firstMonkey_ would be computed and "Bill" would be logged to the console.
|
||||||
|
|
||||||
|
### Access the contained object directly
|
||||||
|
|
||||||
|
Reference the `_` (underscore) property to access the contained object directly.
|
||||||
|
|
||||||
|
```js
|
||||||
|
monkeys._.push('Bill');
|
||||||
|
```
|
||||||
|
|
||||||
|
The array in _monkeys_ would get a new value without _firstMonkey_ being notified of the change.
|
||||||
67
docs/property.md
Normal file
67
docs/property.md
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
# property
|
||||||
|
|
||||||
|
A `property` is a [subscribable](./subscribable.md), scalar value store. It serves as a change
|
||||||
|
deduplicator or the entry point to a logic graph built with [computed](./computed.md)s or
|
||||||
|
[stream](./stream.md)s.
|
||||||
|
|
||||||
|
Subscribers will only be notified when the value passed to the property is different from the
|
||||||
|
previously stored value. By default, `property` will use Javascript's identity operator (===) to
|
||||||
|
determine difference but this is customizable by passing a hash function.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Creation
|
||||||
|
|
||||||
|
Creates a property instance and sets its initial value to `true`
|
||||||
|
|
||||||
|
```js
|
||||||
|
const inViewport = prop(true);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Change
|
||||||
|
|
||||||
|
Call the property passing in the new value. If the passed value is different from the stored value,
|
||||||
|
the passed value will be stored and any subscribers will be notified of the change.
|
||||||
|
|
||||||
|
```js
|
||||||
|
inViewport(false);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Read
|
||||||
|
|
||||||
|
Call the property with no parameters to receive the stored value.
|
||||||
|
|
||||||
|
_NOTE: This use-case is available but discouraged. Instead build a logic graph that uses the
|
||||||
|
property as an input._
|
||||||
|
|
||||||
|
```js
|
||||||
|
if (inViewport()) {
|
||||||
|
/* inViewport is truthy */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Provide a hash function for storing complex types
|
||||||
|
|
||||||
|
When storing a type that is not determined to be equal with an identity operator (===), provide a
|
||||||
|
hash function to the `hashableProperty` type to create a property that will reduce the passed values
|
||||||
|
to a value that can be compared with the identity operator before comparison. The passed value will
|
||||||
|
still be stored unmodified.
|
||||||
|
|
||||||
|
```js
|
||||||
|
function hashSet(_a) {
|
||||||
|
if (_a instanceof Set) {
|
||||||
|
return Array.from(_a.keys())
|
||||||
|
.sort()
|
||||||
|
.map(k => `${(typeof k).substr(0, 1)}:${encodeURIComponent(k)}/`)
|
||||||
|
.join('?');
|
||||||
|
}
|
||||||
|
return _a;
|
||||||
|
}
|
||||||
|
|
||||||
|
const setProp = hashableProperty(hashSet);
|
||||||
|
|
||||||
|
const a = setProp(new Set([1, 2]));
|
||||||
|
```
|
||||||
|
|
||||||
|
In the above example, `hashSet` will be called any time `setProp` receives a new value. The result
|
||||||
|
of `hashSet` will be compared against the previous hashSet result for the previously stored value.
|
||||||
38
docs/stream.md
Normal file
38
docs/stream.md
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# stream
|
||||||
|
|
||||||
|
A `stream` is a [computed](./computed.md) that works asynchronously. Dependencies can be synchronous
|
||||||
|
or asynchronous functions returning a scalar or a promise respectively. Also calling a stream
|
||||||
|
multiple overlapping times will return the same promise and result (so long as dependencies do not
|
||||||
|
change in the time gap). In this way, stream can serve as a temporal deduplicator.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Creation
|
||||||
|
|
||||||
|
```js
|
||||||
|
const getToken = stream(
|
||||||
|
async tokenUrl => fetch(tokenUrl), // computation function
|
||||||
|
[getTokenUrl, tokenTimeout] // array of subscribable dependencies
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Read
|
||||||
|
|
||||||
|
```js
|
||||||
|
if (await getToken()) {
|
||||||
|
/* do stuff with the token */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Provide a hash function for complex result types
|
||||||
|
|
||||||
|
When the stream result is a type that is not determined to be equal with simple equality (===),
|
||||||
|
provide a hash function to the `hashableStream` type to create a stream that will reduce the result
|
||||||
|
value to a value that can be compared with the identity operator before comparison. The computed
|
||||||
|
result value will still be stored unmodified.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const tokenStream = hashableStream(token => token && token.id);
|
||||||
|
|
||||||
|
const getToken = tokenStream(async tokenUrl => fetch(tokenUrl), [getTokenUrl]);
|
||||||
|
```
|
||||||
33
docs/subscribable.md
Normal file
33
docs/subscribable.md
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# Subscribable
|
||||||
|
|
||||||
|
A subscribable is a common interface for all of the primitives provided by this library.
|
||||||
|
|
||||||
|
Each subscribable has the following methods:
|
||||||
|
|
||||||
|
## subscribe(callback)
|
||||||
|
|
||||||
|
`subscribe` takes a _callback_ function that will be called when the primitive's value changes.
|
||||||
|
`subscribe` will return an _unsubscribe_ function that will unsubscribe the _callback_ from the
|
||||||
|
primitive when called. The returned _unsubscribe_ function will also return the remaining number of
|
||||||
|
subscriptions for that primitive.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const p = prop(false);
|
||||||
|
const unsubscribe = p.subscribe(val => console.log(val));
|
||||||
|
|
||||||
|
p(true); // Logs "true" to the console
|
||||||
|
p(true); // Nothing logged because the value of p did not change.
|
||||||
|
|
||||||
|
const remainingSubscriptionCount = unsubscribe();
|
||||||
|
p(false); // Nothing logged because the subscription was revoked.
|
||||||
|
|
||||||
|
if (remainingSubscriptionCount === 0) {
|
||||||
|
// Run any necessary cleanup
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## unsubscribeAll()
|
||||||
|
|
||||||
|
`unsubscribeAll` simply disconnects all subscribers from the primitive.
|
||||||
27
docs/utilities.md
Normal file
27
docs/utilities.md
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# Utilities
|
||||||
|
|
||||||
|
Reactimal provides a few utility functions that may be useful when working with its primitives.
|
||||||
|
|
||||||
|
## id
|
||||||
|
|
||||||
|
`id(anything) -> anything`
|
||||||
|
|
||||||
|
`id` is a function that returns its first parameter. It is used as the default hash function for
|
||||||
|
each [subscribable](./subscribable.md).
|
||||||
|
|
||||||
|
## call
|
||||||
|
|
||||||
|
`call(anything) -> void`
|
||||||
|
|
||||||
|
`call` will call its first parameter if it is a function otherwise return the parameter. It is used
|
||||||
|
for iterating over dependent inputs of [computeds](./docs/computed.md) and
|
||||||
|
[streams](./docs/stream.md).
|
||||||
|
|
||||||
|
## pick
|
||||||
|
|
||||||
|
`pick(propName, default) -> (obj) -> any`
|
||||||
|
|
||||||
|
`pick` returns a function that accepts an object and will return the object's property for the
|
||||||
|
provided name or the default if one is supplied. `pick` is not directly used with any
|
||||||
|
[subscribable](./subscribable.md) but can be useful as the [computed](./computed.md) function when
|
||||||
|
breaking down a `prop` that contains an object or a container.
|
||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "frptools",
|
"name": "reactimal",
|
||||||
"version": "3.2.3",
|
"version": "1.0.0",
|
||||||
"description": "Observable Property and Computed data streams",
|
"description": "Reactive programming primitives",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"files": [
|
"files": [
|
||||||
"src"
|
"src"
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
import { prop, computed } from '../src/index.js';
|
import { prop, hashableProperty, computed, hashableComputed } from '../src/index.js';
|
||||||
import { dirtyMock, hashSet } from '../src/testUtil.js';
|
import { dirtyMock, hashSet } from '../src/testUtil.js';
|
||||||
|
|
||||||
describe('A computed', () => {
|
describe('A computed', () => {
|
||||||
const add = (a, b) => a + b;
|
const add = (a, b) => a + b;
|
||||||
const square = a => a * a;
|
const square = a => a * a;
|
||||||
|
const setProp = hashableProperty(hashSet);
|
||||||
|
const computedSet = hashableComputed(hashSet);
|
||||||
|
|
||||||
it('returns the value computed from its dependencies', () => {
|
it('returns the value computed from its dependencies', () => {
|
||||||
const a = prop(0);
|
const a = prop(0);
|
||||||
@ -157,7 +159,7 @@ describe('A computed', () => {
|
|||||||
expect(c()).toEqual(7);
|
expect(c()).toEqual(7);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('uses a comparator', () => {
|
it('uses a hash function', () => {
|
||||||
let runCount = 0;
|
let runCount = 0;
|
||||||
|
|
||||||
function intersection(a, b) {
|
function intersection(a, b) {
|
||||||
@ -165,9 +167,9 @@ describe('A computed', () => {
|
|||||||
return new Set([...a].filter(x => b.has(x)));
|
return new Set([...a].filter(x => b.has(x)));
|
||||||
}
|
}
|
||||||
|
|
||||||
const a = prop(new Set([1, 2]), hashSet);
|
const a = setProp(new Set([1, 2]));
|
||||||
const b = prop(new Set([2, 3]), hashSet);
|
const b = setProp(new Set([2, 3]));
|
||||||
const ABintersection = computed(intersection, [a, b], hashSet);
|
const ABintersection = computedSet(intersection, [a, b]);
|
||||||
|
|
||||||
expect(runCount).toEqual(0);
|
expect(runCount).toEqual(0);
|
||||||
expect([...ABintersection()]).toEqual([2]);
|
expect([...ABintersection()]).toEqual([2]);
|
||||||
@ -186,9 +188,9 @@ describe('A computed', () => {
|
|||||||
return new Set([...a].filter(x => b.has(x)));
|
return new Set([...a].filter(x => b.has(x)));
|
||||||
}
|
}
|
||||||
|
|
||||||
const a = prop(new Set([1, 2]), hashSet);
|
const a = setProp(new Set([1, 2]));
|
||||||
const b = prop(new Set([2, 3]), hashSet);
|
const b = setProp(new Set([2, 3]));
|
||||||
const ABintersection = computed(intersection, [a, b], hashSet);
|
const ABintersection = computedSet(intersection, [a, b]);
|
||||||
|
|
||||||
const [dirtyA, dirtyB, checker] = dirtyMock(2);
|
const [dirtyA, dirtyB, checker] = dirtyMock(2);
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
import { container, computed } from '../src/index.js';
|
import { container, hashableContainer, computed } from '../src/index.js';
|
||||||
import { dirtyMock, hashSet } from '../src/testUtil.js';
|
import { dirtyMock, hashSet } from '../src/testUtil.js';
|
||||||
|
|
||||||
describe('A container', () => {
|
describe('A container', () => {
|
||||||
|
const containedSet = hashableContainer(hashSet);
|
||||||
|
const containedArray = hashableContainer(arr => arr.join('x'));
|
||||||
|
|
||||||
it('tracks properties', () => {
|
it('tracks properties', () => {
|
||||||
let i = 0;
|
let i = 0;
|
||||||
const a = container({}, () => i++);
|
const a = container({}, () => i++);
|
||||||
@ -16,7 +19,7 @@ describe('A container', () => {
|
|||||||
it('notifies dependents of updates', () => {
|
it('notifies dependents of updates', () => {
|
||||||
let runCount = 0;
|
let runCount = 0;
|
||||||
let currentValue = new Set();
|
let currentValue = new Set();
|
||||||
const a = container(new Set(), hashSet);
|
const a = containedSet(new Set());
|
||||||
const b = computed(s => Array.from(s).reduce((i, acc) => i + acc, 0), [a]);
|
const b = computed(s => Array.from(s).reduce((i, acc) => i + acc, 0), [a]);
|
||||||
a.subscribe(val => {
|
a.subscribe(val => {
|
||||||
runCount += 1;
|
runCount += 1;
|
||||||
@ -35,7 +38,7 @@ describe('A container', () => {
|
|||||||
it('works for arrays', () => {
|
it('works for arrays', () => {
|
||||||
let runCount = 0;
|
let runCount = 0;
|
||||||
let currentValue = [];
|
let currentValue = [];
|
||||||
const a = container([], arr => arr.join('x'));
|
const a = containedArray([]);
|
||||||
a.subscribe(val => {
|
a.subscribe(val => {
|
||||||
runCount += 1;
|
runCount += 1;
|
||||||
expect(a.join('x')).toEqual(currentValue.join('x'));
|
expect(a.join('x')).toEqual(currentValue.join('x'));
|
||||||
@ -54,7 +57,7 @@ describe('A container', () => {
|
|||||||
it('._ returns the proxied element', () => {
|
it('._ returns the proxied element', () => {
|
||||||
let runCount = 0;
|
let runCount = 0;
|
||||||
let currentValue = new Set();
|
let currentValue = new Set();
|
||||||
const a = container(new Set(), hashSet);
|
const a = containedSet(new Set());
|
||||||
a.subscribe(val => {
|
a.subscribe(val => {
|
||||||
runCount += 1;
|
runCount += 1;
|
||||||
expect(hashSet(a)).toEqual(hashSet(currentValue));
|
expect(hashSet(a)).toEqual(hashSet(currentValue));
|
||||||
@ -71,7 +74,7 @@ describe('A container', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('flags all subscribers as dirty before propagating change', () => {
|
it('flags all subscribers as dirty before propagating change', () => {
|
||||||
const a = container(new Set(), hashSet);
|
const a = containedSet(new Set());
|
||||||
|
|
||||||
const [dirtyA, dirtyB, checker] = dirtyMock(2);
|
const [dirtyA, dirtyB, checker] = dirtyMock(2);
|
||||||
|
|
||||||
@ -86,7 +89,7 @@ describe('A container', () => {
|
|||||||
it('calls subscriptions in order', () => {
|
it('calls subscriptions in order', () => {
|
||||||
let order = '';
|
let order = '';
|
||||||
|
|
||||||
const a = container(new Set(), hashSet);
|
const a = containedSet(new Set());
|
||||||
a.subscribe(() => (order += 'a'));
|
a.subscribe(() => (order += 'a'));
|
||||||
a.subscribe(() => (order += 'b'));
|
a.subscribe(() => (order += 'b'));
|
||||||
a.subscribe(() => (order += 'c'));
|
a.subscribe(() => (order += 'c'));
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
import { prop } from '../src/index.js';
|
import { prop, hashableProperty } from '../src/index.js';
|
||||||
import { dirtyMock, hashSet } from '../src/testUtil.js';
|
import { dirtyMock, hashSet } from '../src/testUtil.js';
|
||||||
|
|
||||||
describe('A property', () => {
|
describe('A property', () => {
|
||||||
|
const setProperty = hashableProperty(hashSet);
|
||||||
|
|
||||||
it('returns its initialized value', () => {
|
it('returns its initialized value', () => {
|
||||||
const a = prop(true);
|
const a = prop(true);
|
||||||
expect(a()).toEqual(true);
|
expect(a()).toEqual(true);
|
||||||
@ -66,7 +68,7 @@ describe('A property', () => {
|
|||||||
it('uses a hash function', () => {
|
it('uses a hash function', () => {
|
||||||
let runCount = 0;
|
let runCount = 0;
|
||||||
|
|
||||||
const a = prop(new Set([1, 2]), hashSet);
|
const a = setProperty(new Set([1, 2]));
|
||||||
a.subscribe(() => (runCount += 1));
|
a.subscribe(() => (runCount += 1));
|
||||||
expect([...a()]).toEqual([1, 2]);
|
expect([...a()]).toEqual([1, 2]);
|
||||||
expect(runCount).toEqual(0);
|
expect(runCount).toEqual(0);
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { prop, computed, stream } from '../src/index.js';
|
import { prop, computed, stream, hashableStream } from '../src/index.js';
|
||||||
import { dirtyMock, hashSet } from '../src/testUtil.js';
|
import { dirtyMock, hashSet } from '../src/testUtil.js';
|
||||||
|
|
||||||
describe('A stream', () => {
|
describe('A stream', () => {
|
||||||
@ -20,7 +20,7 @@ describe('A stream', () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
it('accepts prop, computed and stream dependencies', async done => {
|
it('accepts subscribable dependencies', async done => {
|
||||||
const a = prop(0);
|
const a = prop(0);
|
||||||
const b = computed(square, [a]);
|
const b = computed(square, [a]);
|
||||||
const c = stream(delaySquare, [a]);
|
const c = stream(delaySquare, [a]);
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { id, registerFire, registerSubscriptions, call } from './util.js';
|
import { id, registerFire, registerSubscriptions, call } from './util.js';
|
||||||
|
|
||||||
export function computed(fn, dependencies = [], hash = id) {
|
export const hashableComputed = hash => (fn, dependencies = []) => {
|
||||||
let subscribers = [];
|
let subscribers = [];
|
||||||
let isDirty = true;
|
let isDirty = true;
|
||||||
let val;
|
let val;
|
||||||
@ -48,4 +48,6 @@ export function computed(fn, dependencies = [], hash = id) {
|
|||||||
const dependentSubscriptions = dependencies.map(d => d.subscribe(accessor.setDirty));
|
const dependentSubscriptions = dependencies.map(d => d.subscribe(accessor.setDirty));
|
||||||
|
|
||||||
return accessor;
|
return accessor;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export const computed = hashableComputed(id);
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { registerSubscriptions, registerFire } from './util.js';
|
import { registerSubscriptions, registerFire } from './util.js';
|
||||||
|
|
||||||
export function container(store, hash) {
|
export const hashableContainer = hash => store => {
|
||||||
let subscribers = [];
|
let subscribers = [];
|
||||||
let id = hash && hash(store);
|
let id = hash && hash(store);
|
||||||
|
|
||||||
@ -55,4 +55,6 @@ export function container(store, hash) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return p;
|
return p;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export const container = hashableContainer();
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
export { prop } from './property.js';
|
export { prop, hashableProperty } from './property.js';
|
||||||
export { computed } from './computed.js';
|
export { computed, hashableComputed } from './computed.js';
|
||||||
export { stream } from './stream.js';
|
export { stream, hashableStream } from './stream.js';
|
||||||
export { container } from './container.js';
|
export { container, hashableContainer } from './container.js';
|
||||||
export { call, id, pick } from './util.js';
|
export { call, id, pick } from './util.js';
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { id, registerSubscriptions, registerFire } from './util.js';
|
import { id, registerSubscriptions, registerFire } from './util.js';
|
||||||
|
|
||||||
export function prop(store, hash = id) {
|
export const hashableProperty = hash => store => {
|
||||||
let subscribers = [];
|
let subscribers = [];
|
||||||
let oldId = hash(store);
|
let oldId = hash(store);
|
||||||
|
|
||||||
@ -18,4 +18,6 @@ export function prop(store, hash = id) {
|
|||||||
accessor.unsubscribeAll = () => (subscribers = []);
|
accessor.unsubscribeAll = () => (subscribers = []);
|
||||||
|
|
||||||
return accessor;
|
return accessor;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export const prop = hashableProperty(id);
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { id, registerFire, registerSubscriptions, call } from './util.js';
|
import { id, registerFire, registerSubscriptions, call } from './util.js';
|
||||||
|
|
||||||
export function stream(fn, dependencies = [], hash = id) {
|
export const hashableStream = hash => (fn, dependencies = []) => {
|
||||||
let subscribers = [];
|
let subscribers = [];
|
||||||
let isDirty = true;
|
let isDirty = true;
|
||||||
let val;
|
let val;
|
||||||
@ -60,4 +60,6 @@ export function stream(fn, dependencies = [], hash = id) {
|
|||||||
const dependentSubscriptions = dependencies.map(d => d.subscribe(accessor.setDirty));
|
const dependentSubscriptions = dependencies.map(d => d.subscribe(accessor.setDirty));
|
||||||
|
|
||||||
return accessor;
|
return accessor;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export const stream = hashableStream(id);
|
||||||
|
|||||||
Reference in New Issue
Block a user