Revamp FRPTools docs in preparation for renaming and rehoming

This commit is contained in:
Timothy Farrell 2018-09-28 22:14:21 -05:00
parent 90461529d6
commit dfb7c31840
17 changed files with 350 additions and 316 deletions

308
README.md
View File

@ -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
View 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
View 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
View 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
View 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
View 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
View 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.

View File

@ -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"

View File

@ -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);

View File

@ -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'));

View File

@ -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);

View File

@ -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]);

View File

@ -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);

View File

@ -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();

View File

@ -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';

View File

@ -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);

View File

@ -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);