diff --git a/README.md b/README.md index 080b250..e750ee6 100644 --- a/README.md +++ b/README.md @@ -51,22 +51,24 @@ const remainingSubscriptionCount = unsubscribe(); inViewport.unsubscribeAll(); // Call unsubscribeAll to remove child property/computed subscriptions. ``` -### Provide a comparator 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 -function to determine in the new provided value should be propagated to dependents. +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 setEquals(a, b) { - return ( - a instanceof Set && - b instanceof Set && - [...a].reduce((acc, d) => acc && b.has(d), true) && - [...b].reduce((acc, d) => acc && a.has(d), true) - ); +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]), setEquals); +const a = prop(new Set([1, 2]), hashSet); ``` # [computed](./src/computed.js) @@ -137,28 +139,30 @@ showDialog.detach(); // Call detach to remove this computed from the logic tree. showDialog.unsubscribeAll(); // Call unsubscribeAll to remove child property/computed subscriptions. ``` -### Provide a comparator for complex types +### 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 function to determine in the new provided value should be propagated to dependents. +provide a hash function to be used for simple comparison to determine if the new provided value +should be propagated to dependents. ```js -function setEquals(a, b) { - return ( - a instanceof Set && - b instanceof Set && - [...a].reduce((acc, d) => acc && b.has(d), true) && - [...b].reduce((acc, d) => acc && a.has(d), true) - ); +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]), setEquals); -const b = prop(new Set([2, 3]), setEquals); -const intersection = computed(_intersection, [a, b], setEquals); +const a = prop(new Set([1, 2]), hashSet); +const b = prop(new Set([2, 3]), hashSet); +const intersection = computed(_intersection, [a, b], hashSet); ``` # [bundle](./src/bundle.js) diff --git a/package.json b/package.json index 1b9f4de..f71a7ca 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "frptools", - "version": "2.0.0", + "version": "2.1.0", "description": "Observable Property and Computed data streams", "main": "lib/index.js", "jsnext:main": "src/index.js", diff --git a/spec/computed.spec.js b/spec/computed.spec.js index cf6cb50..8b69bde 100644 --- a/spec/computed.spec.js +++ b/spec/computed.spec.js @@ -1,4 +1,5 @@ const { prop, computed } = require('../lib/index.js'); +const { hashSet } = require('../lib/util.js'); describe('computed', () => { const add = (a, b) => a + b; @@ -157,15 +158,6 @@ describe('computed', () => { }); it('uses a comparator', () => { - function setEquals(a, b) { - return ( - a instanceof Set && - b instanceof Set && - [...a].reduce((acc, d) => acc && b.has(d), true) && - [...b].reduce((acc, d) => acc && a.has(d), true) - ); - } - let runCount = 0; function intersection(a, b) { @@ -173,9 +165,9 @@ describe('computed', () => { return new Set([...a].filter(x => b.has(x))); } - const a = prop(new Set([1, 2]), setEquals); - const b = prop(new Set([2, 3]), setEquals); - const ABintersection = computed(intersection, [a, b], setEquals); + const a = prop(new Set([1, 2]), hashSet); + const b = prop(new Set([2, 3]), hashSet); + const ABintersection = computed(intersection, [a, b], hashSet); expect(runCount).toEqual(0); expect([...ABintersection()]).toEqual([2]); diff --git a/spec/property.spec.js b/spec/property.spec.js index 05a49e0..f42d60d 100644 --- a/spec/property.spec.js +++ b/spec/property.spec.js @@ -1,4 +1,5 @@ const { prop } = require('../lib/index.js'); +const { hashSet } = require('../lib/util.js'); describe('A property', () => { it('returns its initialized value', () => { @@ -62,19 +63,10 @@ describe('A property', () => { expect(runCount).toEqual(3); }); - it('uses a comparator', () => { - function setEquals(a, b) { - return ( - a instanceof Set && - b instanceof Set && - [...a].reduce((acc, d) => acc && b.has(d), true) && - [...b].reduce((acc, d) => acc && a.has(d), true) - ); - } - + it('uses a hash function', () => { let runCount = 0; - const a = prop(new Set([1, 2]), setEquals); + const a = prop(new Set([1, 2]), hashSet); a.subscribe(() => (runCount += 1)); expect([...a()]).toEqual([1, 2]); expect(runCount).toEqual(0); diff --git a/src/computed.js b/src/computed.js index b622652..40e25a9 100644 --- a/src/computed.js +++ b/src/computed.js @@ -1,10 +1,11 @@ -import { eq } from './util.js'; +import { id } from './util.js'; -export function computed(fn, dependencies = [], comparator = eq) { +export function computed(fn, dependencies = [], hash = id) { const subscribers = new Set(); const dependents = new Set(); let isDirty = true; let val; + let id; // Receive dirty flag from parent logic node (dependency). Pass it down. function _computedDirtyReporter(_, skipPropagation) { @@ -25,7 +26,9 @@ export function computed(fn, dependencies = [], comparator = eq) { if (isDirty) { const newVal = fn.apply(null, dependencies.map(runParam)); isDirty = false; - if (!comparator(val, newVal)) { + const newId = hash(newVal); + if (id !== newId) { + id = newId; val = newVal; subscribers.forEach(s => s(val)); } diff --git a/src/property.js b/src/property.js index c3719c3..3f431c1 100644 --- a/src/property.js +++ b/src/property.js @@ -1,10 +1,13 @@ -import { eq } from './util.js'; +import { id } from './util.js'; -export function prop(store, comparator = eq) { +export function prop(store, hash = id) { const subscribers = new Set(); + let id = hash(store); const accessor = function _prop(newVal) { - if (newVal !== undefined && !comparator(store, newVal)) { + const newId = hash(newVal); + if (newVal !== undefined && id !== newId) { + id = newId; store = newVal; subscribers.forEach(s => s(store)); } diff --git a/src/util.js b/src/util.js index 4ce0d59..251a894 100644 --- a/src/util.js +++ b/src/util.js @@ -1 +1,11 @@ -export const eq = (a, b) => a === b; +export const id = a => a; + +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; +}