3.0.0 refactor
Bundle is no longer necessary since all tools have the "set all subscribers dirty before updating" behavior. Added additional guarantees around subscription call order. tool.fire() can be called externally
This commit is contained in:
parent
44000f9d77
commit
1c8d52253d
@ -1,6 +1,7 @@
|
|||||||
# FRP tools
|
# FRP tools
|
||||||
|
|
||||||
Property and Computed value stores designed to work together for storing real and derived state.
|
Property, Container and Computed value stores designed to work together for storing discrete and
|
||||||
|
derived state.
|
||||||
|
|
||||||
# [property](./src/property.js)
|
# [property](./src/property.js)
|
||||||
|
|
||||||
@ -38,19 +39,6 @@ and re-evaluated the next time they are read from.
|
|||||||
inViewport(false);
|
inViewport(false);
|
||||||
```
|
```
|
||||||
|
|
||||||
### 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();
|
|
||||||
|
|
||||||
inViewport.unsubscribeAll(); // Call unsubscribeAll to remove child property/computed subscriptions.
|
|
||||||
```
|
|
||||||
|
|
||||||
### Provide a hash function for complex types
|
### Provide a hash function for complex types
|
||||||
|
|
||||||
When storing a type that is not determined to be equal with simple equality (===), provide a hash
|
When storing a type that is not determined to be equal with simple equality (===), provide a hash
|
||||||
@ -165,57 +153,9 @@ const b = prop(new Set([2, 3]), hashSet);
|
|||||||
const intersection = computed(_intersection, [a, b], hashSet);
|
const intersection = computed(_intersection, [a, b], hashSet);
|
||||||
```
|
```
|
||||||
|
|
||||||
# [bundle](./src/bundle.js)
|
|
||||||
|
|
||||||
`bundle` is a wrapper around a group of properties for the purpose of applying changes to all of
|
|
||||||
them at once without having to trigger a subscription that may depend on more than property in the
|
|
||||||
group.
|
|
||||||
|
|
||||||
Another way to think of a `bundle` is a `property` that takes an object and exposes the object's
|
|
||||||
properties as individual `property` instances.
|
|
||||||
|
|
||||||
## Behavior
|
|
||||||
|
|
||||||
A `bundle` wraps properties to intercept dependency hooks in such a way that updating all `property`
|
|
||||||
instances can happen at once before any downstream `computed` instances are evaluated. A bundle
|
|
||||||
returns a function that can be called with an object to set values for the mapped member `property`
|
|
||||||
instances.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
### Creation
|
|
||||||
|
|
||||||
```js
|
|
||||||
const layoutEventBundle = bundle({
|
|
||||||
width: prop(1),
|
|
||||||
height: prop(2)
|
|
||||||
});
|
|
||||||
const ratio = computed((a, b) => a / b, [layoutEventBundle.width, layoutEventBundle.height]);
|
|
||||||
ratio.subscribe(render);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Change Member Properties atomically
|
|
||||||
|
|
||||||
```js
|
|
||||||
layoutEventBundle({ width: 640, height: 480 });
|
|
||||||
```
|
|
||||||
|
|
||||||
`ratio` would normally be evaluated twice and `render` would be called after each intermediate
|
|
||||||
change. But bundle allows both values to change and `ratio` will only be evaluated once and `render`
|
|
||||||
called once.
|
|
||||||
|
|
||||||
### Change Member Properties individually
|
|
||||||
|
|
||||||
```js
|
|
||||||
layoutEventBundle.width(640);
|
|
||||||
layoutEventBundle.height(480);
|
|
||||||
```
|
|
||||||
|
|
||||||
The properties exposed by the bundle can also be updated apart from their grouping.
|
|
||||||
|
|
||||||
# [container](./src/container.js)
|
# [container](./src/container.js)
|
||||||
|
|
||||||
`container` is a wrapper around any container type (object, Set, Map, or Array) while monitoring
|
`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
|
changes to the container. A container can be subscribed to and `computed` instances can depend on
|
||||||
them.
|
them.
|
||||||
|
|
||||||
@ -223,7 +163,7 @@ them.
|
|||||||
|
|
||||||
Anytime a property is set or a method is gotten and called, the container will check for an updated
|
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 hash function must be applied to determine
|
state and trigger subscribers if it is updated. An hash function must be applied to determine
|
||||||
updated status otherwise subscribers will be called on any potential update.
|
updated status.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@ -242,3 +182,49 @@ monkeys.push('Bill');
|
|||||||
```
|
```
|
||||||
|
|
||||||
_firstMonkey_ would be computed and "Bill" would be logged to the console.
|
_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.
|
||||||
|
```
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "frptools",
|
"name": "frptools",
|
||||||
"version": "2.2.1",
|
"version": "3.0.0",
|
||||||
"description": "Observable Property and Computed data streams",
|
"description": "Observable Property and Computed data streams",
|
||||||
"main": "lib/index.js",
|
"main": "lib/index.js",
|
||||||
"jsnext:main": "src/index.js",
|
"jsnext:main": "src/index.js",
|
||||||
|
|||||||
@ -1,114 +0,0 @@
|
|||||||
const { prop, computed, bundle } = require('../lib/index.js');
|
|
||||||
|
|
||||||
describe('bundle', () => {
|
|
||||||
const methods = {
|
|
||||||
add: (a, b) => a + b,
|
|
||||||
square: a => a * a,
|
|
||||||
getVal: val => {}
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
spyOn(methods, 'add').and.callThrough();
|
|
||||||
spyOn(methods, 'square').and.callThrough();
|
|
||||||
spyOn(methods, 'getVal').and.callThrough();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('bundles property changes together', () => {
|
|
||||||
const a = bundle({
|
|
||||||
a: prop(0),
|
|
||||||
b: prop(10)
|
|
||||||
});
|
|
||||||
const b = computed(methods.square, [a.a]);
|
|
||||||
const c = computed(methods.add, [a.a, a.b]);
|
|
||||||
|
|
||||||
b.subscribe(methods.getVal);
|
|
||||||
c.subscribe(methods.getVal);
|
|
||||||
expect(methods.getVal).toHaveBeenCalledTimes(0);
|
|
||||||
|
|
||||||
expect(b()).toEqual(0);
|
|
||||||
expect(methods.getVal).toHaveBeenCalledTimes(1);
|
|
||||||
b();
|
|
||||||
expect(methods.getVal).toHaveBeenCalledTimes(1);
|
|
||||||
expect(c()).toEqual(10);
|
|
||||||
c();
|
|
||||||
expect(methods.getVal).toHaveBeenCalledTimes(2);
|
|
||||||
expect(methods.add).toHaveBeenCalledTimes(1);
|
|
||||||
expect(methods.square).toHaveBeenCalledTimes(1);
|
|
||||||
|
|
||||||
a({ a: 2, b: 20 });
|
|
||||||
expect(methods.add).toHaveBeenCalledTimes(2);
|
|
||||||
expect(methods.square).toHaveBeenCalledTimes(2);
|
|
||||||
expect(methods.getVal).toHaveBeenCalledTimes(4);
|
|
||||||
expect(b()).toEqual(4);
|
|
||||||
expect(c()).toEqual(22);
|
|
||||||
expect(methods.add).toHaveBeenCalledTimes(2);
|
|
||||||
expect(methods.square).toHaveBeenCalledTimes(2);
|
|
||||||
expect(methods.getVal).toHaveBeenCalledTimes(4);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('unbundled changes are less efficient', () => {
|
|
||||||
const a = prop(0);
|
|
||||||
const _b = prop(10);
|
|
||||||
const b = computed(methods.square, [a]);
|
|
||||||
const c = computed(methods.add, [a, _b]);
|
|
||||||
|
|
||||||
b.subscribe(methods.getVal);
|
|
||||||
c.subscribe(methods.getVal);
|
|
||||||
expect(methods.getVal).toHaveBeenCalledTimes(0);
|
|
||||||
|
|
||||||
expect(b()).toEqual(0);
|
|
||||||
expect(methods.getVal).toHaveBeenCalledTimes(1);
|
|
||||||
b();
|
|
||||||
expect(methods.getVal).toHaveBeenCalledTimes(1);
|
|
||||||
expect(c()).toEqual(10);
|
|
||||||
c();
|
|
||||||
expect(methods.getVal).toHaveBeenCalledTimes(2);
|
|
||||||
expect(methods.add).toHaveBeenCalledTimes(1);
|
|
||||||
expect(methods.square).toHaveBeenCalledTimes(1);
|
|
||||||
|
|
||||||
a(2);
|
|
||||||
_b(20);
|
|
||||||
expect(methods.add).toHaveBeenCalledTimes(3);
|
|
||||||
expect(methods.square).toHaveBeenCalledTimes(2);
|
|
||||||
expect(methods.getVal).toHaveBeenCalledTimes(5);
|
|
||||||
expect(b()).toEqual(4);
|
|
||||||
expect(c()).toEqual(22);
|
|
||||||
expect(methods.add).toHaveBeenCalledTimes(3);
|
|
||||||
expect(methods.square).toHaveBeenCalledTimes(2);
|
|
||||||
expect(methods.getVal).toHaveBeenCalledTimes(5);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('allows individual members to be updated', () => {
|
|
||||||
const a = bundle({
|
|
||||||
a: prop(0),
|
|
||||||
b: prop(10)
|
|
||||||
});
|
|
||||||
const b = computed(methods.square, [a.a]);
|
|
||||||
const c = computed(methods.add, [a.a, a.b]);
|
|
||||||
|
|
||||||
b.subscribe(methods.getVal);
|
|
||||||
c.subscribe(methods.getVal);
|
|
||||||
expect(methods.getVal).toHaveBeenCalledTimes(0);
|
|
||||||
|
|
||||||
expect(b()).toEqual(0);
|
|
||||||
expect(methods.getVal).toHaveBeenCalledTimes(1);
|
|
||||||
b();
|
|
||||||
expect(methods.getVal).toHaveBeenCalledTimes(1);
|
|
||||||
expect(c()).toEqual(10);
|
|
||||||
c();
|
|
||||||
expect(methods.getVal).toHaveBeenCalledTimes(2);
|
|
||||||
expect(methods.add).toHaveBeenCalledTimes(1);
|
|
||||||
expect(methods.square).toHaveBeenCalledTimes(1);
|
|
||||||
|
|
||||||
a.a(2);
|
|
||||||
a.b(20);
|
|
||||||
expect(methods.add).toHaveBeenCalledTimes(3);
|
|
||||||
expect(methods.square).toHaveBeenCalledTimes(2);
|
|
||||||
expect(methods.getVal).toHaveBeenCalledTimes(5);
|
|
||||||
expect(b()).toEqual(4);
|
|
||||||
expect(c()).toEqual(22);
|
|
||||||
expect(methods.add).toHaveBeenCalledTimes(3);
|
|
||||||
expect(methods.square).toHaveBeenCalledTimes(2);
|
|
||||||
expect(methods.getVal).toHaveBeenCalledTimes(5);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
const { prop, computed } = require('../lib/index.js');
|
const { prop, computed } = require('../lib/index.js');
|
||||||
const { hashSet } = require('../lib/util.js');
|
const { dirtyMock, hashSet } = require('../lib/testUtil.js');
|
||||||
|
|
||||||
describe('computed', () => {
|
describe('computed', () => {
|
||||||
const add = (a, b) => a + b;
|
const add = (a, b) => a + b;
|
||||||
@ -180,4 +180,35 @@ describe('computed', () => {
|
|||||||
expect([...ABintersection()]).toEqual([1, 2]);
|
expect([...ABintersection()]).toEqual([1, 2]);
|
||||||
expect(runCount).toEqual(2);
|
expect(runCount).toEqual(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('flags all subscribers as dirty before propagating change', () => {
|
||||||
|
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 ABintersection = computed(intersection, [a, b], hashSet);
|
||||||
|
|
||||||
|
const [dirtyA, dirtyB, checker] = dirtyMock(2);
|
||||||
|
|
||||||
|
ABintersection.subscribe(dirtyA.setDirty);
|
||||||
|
ABintersection.subscribe(dirtyB.setDirty);
|
||||||
|
|
||||||
|
a(new Set([3, 4]));
|
||||||
|
|
||||||
|
expect(checker()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls subscriptions in order', () => {
|
||||||
|
let order = '';
|
||||||
|
|
||||||
|
const a = prop(null);
|
||||||
|
const b = computed(a => a, [a]);
|
||||||
|
b.subscribe(() => (order += 'a'));
|
||||||
|
b.subscribe(() => (order += 'b'));
|
||||||
|
b.subscribe(() => (order += 'c'));
|
||||||
|
a(1);
|
||||||
|
expect(order).toEqual('abc');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
const { container, computed } = require('../lib/index.js');
|
const { container, computed } = require('../lib/index.js');
|
||||||
const { hashSet } = require('../lib/util.js');
|
const { dirtyMock, hashSet } = require('../lib/testUtil.js');
|
||||||
|
|
||||||
describe('A container', () => {
|
describe('A container', () => {
|
||||||
it('notifies dependents of updates', () => {
|
it('notifies dependents of updates', () => {
|
||||||
@ -58,4 +58,28 @@ describe('A container', () => {
|
|||||||
a._.add(3);
|
a._.add(3);
|
||||||
expect(runCount).toEqual(2);
|
expect(runCount).toEqual(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('flags all subscribers as dirty before propagating change', () => {
|
||||||
|
const a = container(new Set(), hashSet);
|
||||||
|
|
||||||
|
const [dirtyA, dirtyB, checker] = dirtyMock(2);
|
||||||
|
|
||||||
|
a.subscribe(dirtyA.setDirty);
|
||||||
|
a.subscribe(dirtyB.setDirty);
|
||||||
|
|
||||||
|
a.add(1);
|
||||||
|
|
||||||
|
expect(checker()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls subscriptions in order', () => {
|
||||||
|
let order = '';
|
||||||
|
|
||||||
|
const a = container(new Set(), hashSet);
|
||||||
|
a.subscribe(() => (order += 'a'));
|
||||||
|
a.subscribe(() => (order += 'b'));
|
||||||
|
a.subscribe(() => (order += 'c'));
|
||||||
|
a.add(1);
|
||||||
|
expect(order).toEqual('abc');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
const { prop } = require('../lib/index.js');
|
const { prop } = require('../lib/index.js');
|
||||||
const { hashSet } = require('../lib/util.js');
|
const { dirtyMock, hashSet } = require('../lib/testUtil.js');
|
||||||
|
|
||||||
describe('A property', () => {
|
describe('A property', () => {
|
||||||
it('returns its initialized value', () => {
|
it('returns its initialized value', () => {
|
||||||
@ -75,4 +75,28 @@ describe('A property', () => {
|
|||||||
expect([...a(new Set([3, 2, 1]))]).toEqual([3, 2, 1]);
|
expect([...a(new Set([3, 2, 1]))]).toEqual([3, 2, 1]);
|
||||||
expect(runCount).toEqual(1);
|
expect(runCount).toEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('flags all subscribers as dirty before propagating change', () => {
|
||||||
|
const a = prop(true);
|
||||||
|
|
||||||
|
const [dirtyA, dirtyB, checker] = dirtyMock(2);
|
||||||
|
|
||||||
|
a.subscribe(dirtyA.setDirty);
|
||||||
|
a.subscribe(dirtyB.setDirty);
|
||||||
|
|
||||||
|
a(false);
|
||||||
|
|
||||||
|
expect(checker()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls subscriptions in order', () => {
|
||||||
|
let order = '';
|
||||||
|
|
||||||
|
const a = prop(0);
|
||||||
|
a.subscribe(() => (order += 'a'));
|
||||||
|
a.subscribe(() => (order += 'b'));
|
||||||
|
a.subscribe(() => (order += 'c'));
|
||||||
|
a(1);
|
||||||
|
expect(order).toEqual('abc');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,43 +0,0 @@
|
|||||||
export function bundle(props) {
|
|
||||||
const activeSubscribers = new Set();
|
|
||||||
let activeUpdate = false;
|
|
||||||
|
|
||||||
const accessor = function _bundle(values) {
|
|
||||||
const result = {};
|
|
||||||
activeUpdate = true;
|
|
||||||
Object.keys(values)
|
|
||||||
.filter(k => typeof props[k] === 'function')
|
|
||||||
.forEach(k => {
|
|
||||||
result[k] = props[k](values[k]);
|
|
||||||
});
|
|
||||||
|
|
||||||
const subscribers = Array.from(activeSubscribers);
|
|
||||||
// Set them dirty but don't propagate.
|
|
||||||
subscribers.forEach(s => s(result, true));
|
|
||||||
// Now propagate.
|
|
||||||
subscribers.forEach(s => s(result));
|
|
||||||
activeSubscribers.clear();
|
|
||||||
activeUpdate = false;
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
const subscriptionFactory = obsFn => fn => {
|
|
||||||
return obsFn(v => {
|
|
||||||
if (activeUpdate) {
|
|
||||||
activeSubscribers.add(fn);
|
|
||||||
} else {
|
|
||||||
fn(v);
|
|
||||||
}
|
|
||||||
return v;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
Object.keys(props).forEach(k => {
|
|
||||||
const obs = props[k];
|
|
||||||
|
|
||||||
accessor[k] = obs;
|
|
||||||
obs._d = subscriptionFactory(obs._d);
|
|
||||||
});
|
|
||||||
|
|
||||||
return accessor;
|
|
||||||
}
|
|
||||||
@ -1,26 +1,11 @@
|
|||||||
import { id } from './util.js';
|
import { id, registerFire, registerSubscriptions, call } from './util.js';
|
||||||
|
|
||||||
export function computed(fn, dependencies = [], hash = id) {
|
export function computed(fn, dependencies = [], hash = id) {
|
||||||
const subscribers = new Set();
|
let subscribers = [];
|
||||||
const dependents = new Set();
|
|
||||||
let isDirty = true;
|
let isDirty = true;
|
||||||
let val;
|
let val;
|
||||||
let id;
|
let id;
|
||||||
|
|
||||||
// Receive dirty flag from parent logic node (dependency). Pass it down.
|
|
||||||
function _computedDirtyReporter(_, skipPropagation) {
|
|
||||||
if (!isDirty) {
|
|
||||||
isDirty = true;
|
|
||||||
dependents.forEach(d => d(_, skipPropagation));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (subscribers.size && !skipPropagation) {
|
|
||||||
accessor();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const dependentSubscriptions = Array.from(dependencies).map(d => d._d(_computedDirtyReporter));
|
|
||||||
|
|
||||||
// Compute new value, call subscribers if changed.
|
// Compute new value, call subscribers if changed.
|
||||||
const accessor = function _computed() {
|
const accessor = function _computed() {
|
||||||
if (isDirty) {
|
if (isDirty) {
|
||||||
@ -30,40 +15,38 @@ export function computed(fn, dependencies = [], hash = id) {
|
|||||||
if (id !== newId) {
|
if (id !== newId) {
|
||||||
id = newId;
|
id = newId;
|
||||||
val = newVal;
|
val = newVal;
|
||||||
subscribers.forEach(s => s(val));
|
accessor.fire(val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return val;
|
return val;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add child nodes to the logic graph (value-based)
|
// Add child nodes to the logic graph (value-based)
|
||||||
accessor.subscribe = fn => {
|
accessor.subscribe = registerSubscriptions(subscribers);
|
||||||
subscribers.add(fn);
|
accessor.fire = registerFire(subscribers);
|
||||||
return () => {
|
|
||||||
subscribers.delete(fn);
|
|
||||||
return subscribers.size;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add child nodes to the logic graph (dirty-based)
|
// Receive dirty flag from parent logic node (dependency). Pass it down.
|
||||||
accessor._d = fn => {
|
accessor.setDirty = function setDirty() {
|
||||||
dependents.add(fn);
|
if (!isDirty) {
|
||||||
return () => dependents.delete(fn);
|
isDirty = true;
|
||||||
|
subscribers.forEach(s => s.setDirty && s.setDirty());
|
||||||
|
}
|
||||||
|
return subscribers.length && accessor;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Remove this node from the logic graph completely
|
// Remove this node from the logic graph completely
|
||||||
accessor.detach = () => {
|
accessor.detach = () => {
|
||||||
subscribers.clear();
|
subscribers = [];
|
||||||
dependents.clear();
|
dependentSubscriptions.forEach(call);
|
||||||
dependentSubscriptions.forEach(runParam);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Remove child nodes from the logic graph
|
// Remove child nodes from the logic graph
|
||||||
accessor.unsubscribeAll = () => {
|
accessor.unsubscribeAll = () => {
|
||||||
subscribers.clear();
|
subscribers = [];
|
||||||
dependents.clear();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const dependentSubscriptions = dependencies.map(d => d.subscribe(accessor.setDirty));
|
||||||
|
|
||||||
return accessor;
|
return accessor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,24 +1,22 @@
|
|||||||
|
import { id, registerSubscriptions, registerFire } from './util.js';
|
||||||
|
|
||||||
export function container(store, hash) {
|
export function container(store, hash) {
|
||||||
const subscribers = new Set();
|
let subscribers = [];
|
||||||
let id = hash(store);
|
let id = hash(store);
|
||||||
|
|
||||||
const containerMethods = {
|
const containerMethods = {
|
||||||
subscribe: fn => {
|
subscribe: registerSubscriptions(subscribers),
|
||||||
subscribers.add(fn);
|
fire: registerFire(subscribers),
|
||||||
return () => {
|
unsubscribeAll: () => {
|
||||||
subscribers.delete(fn);
|
subscribers = [];
|
||||||
return subscribers.size;
|
}
|
||||||
};
|
};
|
||||||
},
|
|
||||||
unsubscribeAll: () => subscribers.clear()
|
|
||||||
};
|
|
||||||
containerMethods._d = containerMethods.subscribe;
|
|
||||||
|
|
||||||
function checkUpdate(target) {
|
function checkUpdate(target) {
|
||||||
const newId = hash(target);
|
const newId = hash(target);
|
||||||
if (id !== newId) {
|
if (id !== newId) {
|
||||||
id = newId;
|
id = newId;
|
||||||
subscribers.forEach(s => s(target));
|
containerMethods.fire(target);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
export { prop } from './property';
|
export { prop } from './property';
|
||||||
export { computed } from './computed';
|
export { computed } from './computed';
|
||||||
export { bundle } from './bundle';
|
|
||||||
export { container } from './container';
|
export { container } from './container';
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { id } from './util.js';
|
import { id, registerSubscriptions, registerFire } from './util.js';
|
||||||
|
|
||||||
export function prop(store, hash = id) {
|
export function prop(store, hash = id) {
|
||||||
const subscribers = new Set();
|
let subscribers = [];
|
||||||
let id = hash(store);
|
let id = hash(store);
|
||||||
|
|
||||||
const accessor = function _prop(newVal) {
|
const accessor = function _prop(newVal) {
|
||||||
@ -9,20 +9,13 @@ export function prop(store, hash = id) {
|
|||||||
if (newVal !== undefined && id !== newId) {
|
if (newVal !== undefined && id !== newId) {
|
||||||
id = newId;
|
id = newId;
|
||||||
store = newVal;
|
store = newVal;
|
||||||
subscribers.forEach(s => s(store));
|
accessor.fire(store);
|
||||||
}
|
}
|
||||||
return store;
|
return store;
|
||||||
};
|
};
|
||||||
|
accessor.subscribe = registerSubscriptions(subscribers);
|
||||||
accessor.subscribe = accessor._d = fn => {
|
accessor.fire = registerFire(subscribers);
|
||||||
subscribers.add(fn);
|
accessor.unsubscribeAll = () => (subscribers = []);
|
||||||
return () => {
|
|
||||||
subscribers.delete(fn);
|
|
||||||
return subscribers.size;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
accessor.unsubscribeAll = () => subscribers.clear();
|
|
||||||
|
|
||||||
return accessor;
|
return accessor;
|
||||||
}
|
}
|
||||||
|
|||||||
45
packages/frptools/src/testUtil.js
Normal file
45
packages/frptools/src/testUtil.js
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
export function dirtyMock(count) {
|
||||||
|
const result = {};
|
||||||
|
let state = 'init';
|
||||||
|
|
||||||
|
const fakeProp = function(i) {
|
||||||
|
const a = val => {
|
||||||
|
if (state === 'dirty') {
|
||||||
|
expect(Object.keys(result).length).toEqual(count);
|
||||||
|
state = 'cleaning';
|
||||||
|
}
|
||||||
|
if (val === undefined) {
|
||||||
|
if (result[i] === false) {
|
||||||
|
delete result[i];
|
||||||
|
} else {
|
||||||
|
result[i] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Object.keys(result).length === 0) {
|
||||||
|
state = 'clean';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
a.setDirty = () => {
|
||||||
|
state = 'dirty';
|
||||||
|
result[i] = false;
|
||||||
|
return a;
|
||||||
|
};
|
||||||
|
return a;
|
||||||
|
};
|
||||||
|
const output = [];
|
||||||
|
for (let i = 0; i < count; ++i) {
|
||||||
|
output.push(fakeProp(i));
|
||||||
|
}
|
||||||
|
output.push(() => Object.keys(result).length === 0 && state === 'clean');
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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;
|
||||||
|
}
|
||||||
@ -1,11 +1,18 @@
|
|||||||
export const id = a => a;
|
export const id = a => a;
|
||||||
|
|
||||||
export function hashSet(_a) {
|
export const registerSubscriptions = subscriptionsArray => fn => {
|
||||||
if (_a instanceof Set) {
|
subscriptionsArray.push(fn);
|
||||||
return Array.from(_a.keys())
|
return () => {
|
||||||
.sort()
|
const idx = subscriptionsArray.indexOf(fn);
|
||||||
.map(k => `${(typeof k).substr(0, 1)}:${encodeURIComponent(k)}/`)
|
if (idx !== -1) {
|
||||||
.join('?');
|
subscriptionsArray.splice(idx, 1);
|
||||||
}
|
}
|
||||||
return _a;
|
return subscriptionsArray.length;
|
||||||
}
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const call = a => (typeof a === 'function' ? a() : a);
|
||||||
|
|
||||||
|
export const registerFire = subscriptionsArray => val => {
|
||||||
|
subscriptionsArray.map(s => s(val)).forEach(call);
|
||||||
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user