From dfb7c318409305c7ac79d2b40e61c31810c04918 Mon Sep 17 00:00:00 2001 From: Timothy Farrell Date: Fri, 28 Sep 2018 22:14:21 -0500 Subject: [PATCH] Revamp FRPTools docs in preparation for renaming and rehoming --- README.md | 308 ++++------------------------------------- docs/computed.md | 71 ++++++++++ docs/container.md | 41 ++++++ docs/property.md | 67 +++++++++ docs/stream.md | 38 +++++ docs/subscribable.md | 33 +++++ docs/utilities.md | 27 ++++ package.json | 6 +- spec/computed.spec.js | 18 +-- spec/container.spec.js | 15 +- spec/property.spec.js | 6 +- spec/stream.spec.js | 4 +- src/computed.js | 6 +- src/container.js | 6 +- src/index.js | 8 +- src/property.js | 6 +- src/stream.js | 6 +- 17 files changed, 350 insertions(+), 316 deletions(-) create mode 100644 docs/computed.md create mode 100644 docs/container.md create mode 100644 docs/property.md create mode 100644 docs/stream.md create mode 100644 docs/subscribable.md create mode 100644 docs/utilities.md diff --git a/README.md b/README.md index 8d99e75..05d2c26 100644 --- a/README.md +++ b/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". diff --git a/docs/computed.md b/docs/computed.md new file mode 100644 index 0000000..06b78f0 --- /dev/null +++ b/docs/computed.md @@ -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]); +``` diff --git a/docs/container.md b/docs/container.md new file mode 100644 index 0000000..149236a --- /dev/null +++ b/docs/container.md @@ -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. diff --git a/docs/property.md b/docs/property.md new file mode 100644 index 0000000..9a43092 --- /dev/null +++ b/docs/property.md @@ -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. diff --git a/docs/stream.md b/docs/stream.md new file mode 100644 index 0000000..eff6d4a --- /dev/null +++ b/docs/stream.md @@ -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]); +``` diff --git a/docs/subscribable.md b/docs/subscribable.md new file mode 100644 index 0000000..e069ed2 --- /dev/null +++ b/docs/subscribable.md @@ -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. diff --git a/docs/utilities.md b/docs/utilities.md new file mode 100644 index 0000000..575b589 --- /dev/null +++ b/docs/utilities.md @@ -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. diff --git a/package.json b/package.json index 0ccd469..1f68040 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/spec/computed.spec.js b/spec/computed.spec.js index 3896f16..264eacf 100644 --- a/spec/computed.spec.js +++ b/spec/computed.spec.js @@ -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); diff --git a/spec/container.spec.js b/spec/container.spec.js index c22181f..9ce055a 100644 --- a/spec/container.spec.js +++ b/spec/container.spec.js @@ -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')); diff --git a/spec/property.spec.js b/spec/property.spec.js index dc0f8ed..0f2cd4f 100644 --- a/spec/property.spec.js +++ b/spec/property.spec.js @@ -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); diff --git a/spec/stream.spec.js b/spec/stream.spec.js index 33b44bf..fad6efb 100644 --- a/spec/stream.spec.js +++ b/spec/stream.spec.js @@ -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]); diff --git a/src/computed.js b/src/computed.js index f64d0bb..8bd6965 100644 --- a/src/computed.js +++ b/src/computed.js @@ -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); diff --git a/src/container.js b/src/container.js index e926d42..af348dd 100644 --- a/src/container.js +++ b/src/container.js @@ -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(); diff --git a/src/index.js b/src/index.js index a412cb1..9e6acf2 100644 --- a/src/index.js +++ b/src/index.js @@ -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'; diff --git a/src/property.js b/src/property.js index 4489d3e..d9fd808 100644 --- a/src/property.js +++ b/src/property.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); diff --git a/src/stream.js b/src/stream.js index d39f72d..65b61f6 100644 --- a/src/stream.js +++ b/src/stream.js @@ -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);