diff --git a/packages/frptools/README.md b/packages/frptools/README.md index 17f4ce7..fa78c27 100644 --- a/packages/frptools/README.md +++ b/packages/frptools/README.md @@ -40,10 +40,12 @@ inViewport(false); ### Subscribe to changes Call the `subscribe` method with a callback that will be called when the observable is changed to a -different value. The returned function can be called to unsubscribe from the observable. +different value. The returned function can be called to unsubscribe from the observable. When called +it will provide the count of remaining subscriptions. ```js const unsubscribe = inViewport.subscribe(console.log.bind(console)); +const remainingSubscriptionCount = unsubscribe(); ``` # [computed](./src/computed.js) @@ -81,10 +83,12 @@ 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 observable. +different value. The returned function can be called to unsubscribe from the observable. 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 diff --git a/packages/frptools/package.json b/packages/frptools/package.json index ba2c2cd..11cedaf 100644 --- a/packages/frptools/package.json +++ b/packages/frptools/package.json @@ -1,6 +1,6 @@ { "name": "frptools", - "version": "1.0.0", + "version": "1.1.0", "description": "Observable and Computed data streams", "main": "lib/index.js", "jsnext:main": "src/index.js", diff --git a/packages/frptools/spec/computed.spec.js b/packages/frptools/spec/computed.spec.js index 1c7dfd6..7d909aa 100644 --- a/packages/frptools/spec/computed.spec.js +++ b/packages/frptools/spec/computed.spec.js @@ -40,17 +40,107 @@ describe('computed', () => { a(1); expect(runCount).toEqual(0); + // b evaluates expect(b()).toEqual(1); expect(runCount).toEqual(1); + // b does not evaluate expect(b()).toEqual(1); expect(runCount).toEqual(1); currentValue = 3; + // b does not evaluate a(3); expect(runCount).toEqual(1); + // b evaluates expect(b()).toEqual(9); expect(runCount).toEqual(2); }); + it('computes automatically when subscribed', () => { + let runCount = 0; + let subRunCount = 0; + let currentValue = 1; + const a = observable(0); + const b = computed( + val => { + runCount += 1; + expect(val).toEqual(currentValue); + return val * val; + }, + [a] + ); + + // b does not evaluate + a(1); + expect(runCount).toEqual(0); + // b evaluates + expect(b()).toEqual(1); + expect(runCount).toEqual(1); + // b does not evaluate + expect(b()).toEqual(1); + expect(runCount).toEqual(1); + + const cancelSubscription = b.subscribe(val => { + subRunCount += 1; + expect(val).toEqual(currentValue * currentValue); + }); + + currentValue = 3; + // b evaluates + a(3); + expect(runCount).toEqual(2); + expect(subRunCount).toEqual(1); + // b does not evaluate + expect(b()).toEqual(9); + expect(runCount).toEqual(2); + expect(subRunCount).toEqual(1); + }); + + it('honors cancelled subscriptions', () => { + let runCount = 0; + let subRunCount = 0; + let currentValue = 1; + const a = observable(0); + const b = computed( + val => { + runCount += 1; + expect(val).toEqual(currentValue); + return val * val; + }, + [a] + ); + const cancelSubscription = b.subscribe(val => { + subRunCount += 1; + expect(val).toEqual(currentValue * currentValue); + }); + + const cancelSubscription2 = b.subscribe(val => { + subRunCount += 1; + }); + + // b evaluates + a(1); + expect(runCount).toEqual(1); + expect(subRunCount).toEqual(2); + // b does not evaluate + expect(b()).toEqual(1); + expect(runCount).toEqual(1); + expect(subRunCount).toEqual(2); + + expect(cancelSubscription()).toEqual(1); + + currentValue = 3; + // b evaluates + a(3); + expect(runCount).toEqual(2); + expect(subRunCount).toEqual(3); + // b does not evaluate + expect(b()).toEqual(9); + expect(runCount).toEqual(2); + expect(subRunCount).toEqual(3); + + expect(cancelSubscription2()).toEqual(0); + }); + it('can be detached', () => { const a = observable(2); const b = computed(square, [a]); diff --git a/packages/frptools/spec/observable.spec.js b/packages/frptools/spec/observable.spec.js index b789382..78d6bae 100644 --- a/packages/frptools/spec/observable.spec.js +++ b/packages/frptools/spec/observable.spec.js @@ -44,12 +44,21 @@ describe('observable', () => { runCount += 1; expect(val).toEqual(currentValue); }); + const cancelSubscription2 = a.subscribe(val => { + runCount += 1; + expect(val).toEqual(currentValue); + }); expect(a(1)).toEqual(1); - expect(runCount).toEqual(1); + expect(runCount).toEqual(2); expect(a(1)).toEqual(1); - expect(runCount).toEqual(1); - cancelSubscription(); + expect(runCount).toEqual(2); + expect(cancelSubscription()).toEqual(1); + currentValue = 3; expect(a(3)).toEqual(3); - expect(runCount).toEqual(1); + expect(runCount).toEqual(3); + expect(cancelSubscription2()).toEqual(0); + currentValue = 4; + expect(a(4)).toEqual(4); + expect(runCount).toEqual(3); }); }); diff --git a/packages/frptools/src/computed.js b/packages/frptools/src/computed.js index f479db4..dbfff1d 100644 --- a/packages/frptools/src/computed.js +++ b/packages/frptools/src/computed.js @@ -31,7 +31,10 @@ export function computed(fn, dependencies = []) { accessor.subscribe = fn => { subscribers.add(fn); - return () => subscribers.delete(fn); + return () => { + subscribers.delete(fn); + return subscribers.size; + }; }; accessor._d = fn => { diff --git a/packages/frptools/src/observable.js b/packages/frptools/src/observable.js index 6d159db..66d57ce 100644 --- a/packages/frptools/src/observable.js +++ b/packages/frptools/src/observable.js @@ -11,7 +11,10 @@ export function observable(store) { accessor.subscribe = accessor._d = fn => { subscribers.add(fn); - return () => subscribers.delete(fn); + return () => { + subscribers.delete(fn); + return subscribers.size; + }; }; accessor.unsubscribeAll = () => subscribers.clear();