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
|
||||
and derived state.
|
||||
Reactimal is a set of tools that can be used to express your code as a logic graph using reactive
|
||||
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
|
||||
external values passed into a component so compute types can depend on them and only recompute when
|
||||
these values change. It can also be used to receive events such as _window.onresize_ to always
|
||||
provide the current viewport size.
|
||||
To avoid this situation, Reactimal maintains links to the whole logic graph so it's easy to see what
|
||||
outputs come from what inputs.
|
||||
|
||||
## 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
|
||||
const inViewport = prop(true);
|
||||
```
|
||||
## Inspiration
|
||||
|
||||
### Read
|
||||
|
||||
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:
|
||||
Reactimal is the result of years of learning from the following projects:
|
||||
|
||||
- [KnockoutJS](http://knockoutjs.com/)
|
||||
- [Overture](https://github.com/fastmail/overture)
|
||||
- [Redux](https://redux.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",
|
||||
"version": "3.2.3",
|
||||
"description": "Observable Property and Computed data streams",
|
||||
"name": "reactimal",
|
||||
"version": "1.0.0",
|
||||
"description": "Reactive programming primitives",
|
||||
"main": "src/index.js",
|
||||
"files": [
|
||||
"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';
|
||||
|
||||
describe('A computed', () => {
|
||||
const add = (a, b) => a + b;
|
||||
const square = a => a * a;
|
||||
const setProp = hashableProperty(hashSet);
|
||||
const computedSet = hashableComputed(hashSet);
|
||||
|
||||
it('returns the value computed from its dependencies', () => {
|
||||
const a = prop(0);
|
||||
@ -157,7 +159,7 @@ describe('A computed', () => {
|
||||
expect(c()).toEqual(7);
|
||||
});
|
||||
|
||||
it('uses a comparator', () => {
|
||||
it('uses a hash function', () => {
|
||||
let runCount = 0;
|
||||
|
||||
function intersection(a, b) {
|
||||
@ -165,9 +167,9 @@ describe('A computed', () => {
|
||||
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 ABintersection = computed(intersection, [a, b], hashSet);
|
||||
const a = setProp(new Set([1, 2]));
|
||||
const b = setProp(new Set([2, 3]));
|
||||
const ABintersection = computedSet(intersection, [a, b]);
|
||||
|
||||
expect(runCount).toEqual(0);
|
||||
expect([...ABintersection()]).toEqual([2]);
|
||||
@ -186,9 +188,9 @@ describe('A computed', () => {
|
||||
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 ABintersection = computed(intersection, [a, b], hashSet);
|
||||
const a = setProp(new Set([1, 2]));
|
||||
const b = setProp(new Set([2, 3]));
|
||||
const ABintersection = computedSet(intersection, [a, b]);
|
||||
|
||||
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';
|
||||
|
||||
describe('A container', () => {
|
||||
const containedSet = hashableContainer(hashSet);
|
||||
const containedArray = hashableContainer(arr => arr.join('x'));
|
||||
|
||||
it('tracks properties', () => {
|
||||
let i = 0;
|
||||
const a = container({}, () => i++);
|
||||
@ -16,7 +19,7 @@ describe('A container', () => {
|
||||
it('notifies dependents of updates', () => {
|
||||
let runCount = 0;
|
||||
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]);
|
||||
a.subscribe(val => {
|
||||
runCount += 1;
|
||||
@ -35,7 +38,7 @@ describe('A container', () => {
|
||||
it('works for arrays', () => {
|
||||
let runCount = 0;
|
||||
let currentValue = [];
|
||||
const a = container([], arr => arr.join('x'));
|
||||
const a = containedArray([]);
|
||||
a.subscribe(val => {
|
||||
runCount += 1;
|
||||
expect(a.join('x')).toEqual(currentValue.join('x'));
|
||||
@ -54,7 +57,7 @@ describe('A container', () => {
|
||||
it('._ returns the proxied element', () => {
|
||||
let runCount = 0;
|
||||
let currentValue = new Set();
|
||||
const a = container(new Set(), hashSet);
|
||||
const a = containedSet(new Set());
|
||||
a.subscribe(val => {
|
||||
runCount += 1;
|
||||
expect(hashSet(a)).toEqual(hashSet(currentValue));
|
||||
@ -71,7 +74,7 @@ describe('A container', () => {
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
@ -86,7 +89,7 @@ describe('A container', () => {
|
||||
it('calls subscriptions in order', () => {
|
||||
let order = '';
|
||||
|
||||
const a = container(new Set(), hashSet);
|
||||
const a = containedSet(new Set());
|
||||
a.subscribe(() => (order += 'a'));
|
||||
a.subscribe(() => (order += 'b'));
|
||||
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';
|
||||
|
||||
describe('A property', () => {
|
||||
const setProperty = hashableProperty(hashSet);
|
||||
|
||||
it('returns its initialized value', () => {
|
||||
const a = prop(true);
|
||||
expect(a()).toEqual(true);
|
||||
@ -66,7 +68,7 @@ describe('A property', () => {
|
||||
it('uses a hash function', () => {
|
||||
let runCount = 0;
|
||||
|
||||
const a = prop(new Set([1, 2]), hashSet);
|
||||
const a = setProperty(new Set([1, 2]));
|
||||
a.subscribe(() => (runCount += 1));
|
||||
expect([...a()]).toEqual([1, 2]);
|
||||
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';
|
||||
|
||||
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 b = computed(square, [a]);
|
||||
const c = stream(delaySquare, [a]);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { id, registerFire, registerSubscriptions, call } from './util.js';
|
||||
|
||||
export function computed(fn, dependencies = [], hash = id) {
|
||||
export const hashableComputed = hash => (fn, dependencies = []) => {
|
||||
let subscribers = [];
|
||||
let isDirty = true;
|
||||
let val;
|
||||
@ -48,4 +48,6 @@ export function computed(fn, dependencies = [], hash = id) {
|
||||
const dependentSubscriptions = dependencies.map(d => d.subscribe(accessor.setDirty));
|
||||
|
||||
return accessor;
|
||||
}
|
||||
};
|
||||
|
||||
export const computed = hashableComputed(id);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { registerSubscriptions, registerFire } from './util.js';
|
||||
|
||||
export function container(store, hash) {
|
||||
export const hashableContainer = hash => store => {
|
||||
let subscribers = [];
|
||||
let id = hash && hash(store);
|
||||
|
||||
@ -55,4 +55,6 @@ export function container(store, hash) {
|
||||
});
|
||||
|
||||
return p;
|
||||
}
|
||||
};
|
||||
|
||||
export const container = hashableContainer();
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
export { prop } from './property.js';
|
||||
export { computed } from './computed.js';
|
||||
export { stream } from './stream.js';
|
||||
export { container } from './container.js';
|
||||
export { prop, hashableProperty } from './property.js';
|
||||
export { computed, hashableComputed } from './computed.js';
|
||||
export { stream, hashableStream } from './stream.js';
|
||||
export { container, hashableContainer } from './container.js';
|
||||
export { call, id, pick } from './util.js';
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { id, registerSubscriptions, registerFire } from './util.js';
|
||||
|
||||
export function prop(store, hash = id) {
|
||||
export const hashableProperty = hash => store => {
|
||||
let subscribers = [];
|
||||
let oldId = hash(store);
|
||||
|
||||
@ -18,4 +18,6 @@ export function prop(store, hash = id) {
|
||||
accessor.unsubscribeAll = () => (subscribers = []);
|
||||
|
||||
return accessor;
|
||||
}
|
||||
};
|
||||
|
||||
export const prop = hashableProperty(id);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { id, registerFire, registerSubscriptions, call } from './util.js';
|
||||
|
||||
export function stream(fn, dependencies = [], hash = id) {
|
||||
export const hashableStream = hash => (fn, dependencies = []) => {
|
||||
let subscribers = [];
|
||||
let isDirty = true;
|
||||
let val;
|
||||
@ -60,4 +60,6 @@ export function stream(fn, dependencies = [], hash = id) {
|
||||
const dependentSubscriptions = dependencies.map(d => d.subscribe(accessor.setDirty));
|
||||
|
||||
return accessor;
|
||||
}
|
||||
};
|
||||
|
||||
export const stream = hashableStream(id);
|
||||
|
||||
Reference in New Issue
Block a user