Break subscription management out to properly handle unsubscribing
This commit is contained in:
parent
52d8c44a22
commit
278ec30827
@ -2,13 +2,13 @@
|
|||||||
|
|
||||||
Reactimal provides a set of primitives that can be used to express your code as a logic graph using
|
Reactimal provides a set of primitives that can be used to express your code as a logic graph using
|
||||||
reactive programming methods. Reactive programming inverts the dependency relationship between
|
reactive programming methods. Reactive programming inverts the dependency relationship between
|
||||||
functions and their parameters. No longer do callers need to know the signature of the functions
|
functions and their inputs. No longer do callers need to know the signature of the functions
|
||||||
they call. Instead computed values subscribe to scalar properties or other computed values. The
|
they call. Instead computed values subscribe to scalar properties or other computed values. The
|
||||||
reactive method improves code readability and reduces the tendency toward spaghetti code.
|
reactive method improves code readability and reduces the tendency toward spaghetti code.
|
||||||
|
|
||||||
Reactimal provides the following reactive programming primitives. Each type of primitive is
|
Reactimal provides the following reactive programming primitives. Each type of primitive is
|
||||||
[subscribable](./docs/subscribable.md) and provides a single output value, but differ in how they
|
[subscribable](./docs/subscribable.md) and provides a single output value (or promise), but differ
|
||||||
receive their input.
|
in how they receive their input.
|
||||||
|
|
||||||
## Input Primitives
|
## Input Primitives
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { id, registerFire, registerSubscriptions, call } from './util.js';
|
import { id, call } from './util.js';
|
||||||
|
import { subscribable } from './subscribable.js';
|
||||||
|
|
||||||
|
|
||||||
export const hashableComputed = hash => (fn, dependencies = []) => {
|
export const hashableComputed = hash => (fn, dependencies = []) => {
|
||||||
let subscribers = [];
|
|
||||||
let isDirty = true;
|
let isDirty = true;
|
||||||
let val;
|
let val;
|
||||||
let oldId;
|
let oldId;
|
||||||
@ -25,29 +26,23 @@ export const hashableComputed = hash => (fn, dependencies = []) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Add child nodes to the logic graph (value-based)
|
// Add child nodes to the logic graph (value-based)
|
||||||
accessor.subscribe = registerSubscriptions(subscribers);
|
Object.assign(accessor, subscribable());
|
||||||
accessor._fire = registerFire(subscribers);
|
|
||||||
|
|
||||||
// Receive dirty flag from parent logic node (dependency). Pass it down.
|
// Receive dirty flag from parent logic node (dependency). Pass it down.
|
||||||
accessor._setDirty = function setDirty() {
|
accessor._setDirty = function setDirty() {
|
||||||
if (!isDirty) {
|
if (!isDirty) {
|
||||||
isDirty = true;
|
isDirty = true;
|
||||||
subscribers.forEach(s => s._setDirty && s._setDirty());
|
accessor._forEachSubscription(s => s._setDirty && s._setDirty());
|
||||||
}
|
}
|
||||||
return subscribers.length && accessor;
|
return accessor._hasSubscribers() && accessor;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Remove this node from the logic graph completely
|
// Remove this node from the logic graph completely
|
||||||
accessor.detach = () => {
|
accessor.detach = () => {
|
||||||
subscribers = [];
|
accessor.unsubscribeAll();
|
||||||
dependentSubscriptions.forEach(call);
|
dependentSubscriptions.forEach(call);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Remove child nodes from the logic graph
|
|
||||||
accessor.unsubscribeAll = () => {
|
|
||||||
subscribers = [];
|
|
||||||
};
|
|
||||||
|
|
||||||
const dependentSubscriptions = dependencies.map(d => d.subscribe(accessor._setDirty));
|
const dependentSubscriptions = dependencies.map(d => d.subscribe(accessor._setDirty));
|
||||||
|
|
||||||
return accessor;
|
return accessor;
|
||||||
|
|||||||
@ -1,16 +1,10 @@
|
|||||||
import { registerSubscriptions, registerFire } from './util.js';
|
import { subscribable } from './subscribable.js';
|
||||||
|
|
||||||
export const hashableContainer = hash => store => {
|
export const hashableContainer = hash => store => {
|
||||||
let subscribers = [];
|
|
||||||
let id = hash && hash(store);
|
let id = hash && hash(store);
|
||||||
let lockCount = 0;
|
let lockCount = 0;
|
||||||
|
|
||||||
const containerMethods = {
|
const containerMethods = {
|
||||||
subscribe: registerSubscriptions(subscribers),
|
|
||||||
_fire: registerFire(subscribers),
|
|
||||||
unsubscribeAll: () => {
|
|
||||||
subscribers = [];
|
|
||||||
},
|
|
||||||
_lock: () => {
|
_lock: () => {
|
||||||
lockCount += 1;
|
lockCount += 1;
|
||||||
return p;
|
return p;
|
||||||
@ -19,7 +13,8 @@ export const hashableContainer = hash => store => {
|
|||||||
if (lockCount && --lockCount === 0) {
|
if (lockCount && --lockCount === 0) {
|
||||||
checkUpdate(store);
|
checkUpdate(store);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
...subscribable()
|
||||||
};
|
};
|
||||||
|
|
||||||
function checkUpdate(target) {
|
function checkUpdate(target) {
|
||||||
|
|||||||
@ -137,4 +137,21 @@ describe('A container', () => {
|
|||||||
a.add(1);
|
a.add(1);
|
||||||
expect(order).toEqual('abc');
|
expect(order).toEqual('abc');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('unsubscribeAll clears subscriptions', () => {
|
||||||
|
let order = '';
|
||||||
|
|
||||||
|
const a = containedSet(new Set());
|
||||||
|
a.subscribe(() => (order += 'a'));
|
||||||
|
a.subscribe(() => (order += 'b'));
|
||||||
|
a.subscribe(() => (order += 'c'));
|
||||||
|
a.unsubscribeAll()
|
||||||
|
a.add(1);
|
||||||
|
expect(order).toEqual('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('protects method properties.', () => {
|
||||||
|
const a = containedSet(new Set());
|
||||||
|
expect(() => a.subscribe = 'test').toThrow(new ReferenceError('Cannot set subscribe in [object Set]'));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
import { id, registerSubscriptions, registerFire } from './util.js';
|
import { id } from './util.js';
|
||||||
|
import { subscribable } from './subscribable.js';
|
||||||
|
|
||||||
|
|
||||||
export const hashableProperty = hash => store => {
|
export const hashableProperty = hash => store => {
|
||||||
let subscribers = [];
|
let subscribers = [];
|
||||||
@ -16,9 +18,8 @@ export const hashableProperty = hash => store => {
|
|||||||
}
|
}
|
||||||
return store;
|
return store;
|
||||||
};
|
};
|
||||||
accessor.subscribe = registerSubscriptions(subscribers);
|
// Add child nodes to the logic graph (value-based)
|
||||||
accessor.unsubscribeAll = () => (subscribers = []);
|
Object.assign(accessor, subscribable());
|
||||||
accessor._fire = registerFire(subscribers);
|
|
||||||
accessor._lock = () => {
|
accessor._lock = () => {
|
||||||
lockCount += 1;
|
lockCount += 1;
|
||||||
return accessor();
|
return accessor();
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
import { id, registerFire, registerSubscriptions, call } from './util.js';
|
import { id, call } from './util.js';
|
||||||
|
import { subscribable } from './subscribable.js';
|
||||||
|
|
||||||
|
|
||||||
export const hashableStream = hash => (fn, dependencies = []) => {
|
export const hashableStream = hash => (fn, dependencies = []) => {
|
||||||
let subscribers = [];
|
let subscribers = [];
|
||||||
@ -37,29 +39,23 @@ export const hashableStream = hash => (fn, dependencies = []) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Add child nodes to the logic graph (value-based)
|
// Add child nodes to the logic graph (value-based)
|
||||||
accessor.subscribe = registerSubscriptions(subscribers);
|
Object.assign(accessor, subscribable());
|
||||||
accessor._fire = registerFire(subscribers);
|
|
||||||
|
|
||||||
// Receive dirty flag from parent logic node (dependency). Pass it down.
|
// Receive dirty flag from parent logic node (dependency). Pass it down.
|
||||||
accessor._setDirty = function setDirty() {
|
accessor._setDirty = function setDirty() {
|
||||||
if (!isDirty) {
|
if (!isDirty) {
|
||||||
isDirty = true;
|
isDirty = true;
|
||||||
subscribers.forEach(s => s._setDirty && s._setDirty());
|
accessor._forEachSubscription(s => s._setDirty && s._setDirty());
|
||||||
}
|
}
|
||||||
return subscribers.length && accessor;
|
return accessor._hasSubscribers() && accessor;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Remove this node from the logic graph completely
|
// Remove this node from the logic graph completely
|
||||||
accessor.detach = () => {
|
accessor.detach = () => {
|
||||||
subscribers = [];
|
accessor.unsubscribeAll();
|
||||||
dependentSubscriptions.forEach(call);
|
dependentSubscriptions.forEach(call);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Remove child nodes from the logic graph
|
|
||||||
accessor.unsubscribeAll = () => {
|
|
||||||
subscribers = [];
|
|
||||||
};
|
|
||||||
|
|
||||||
const dependentSubscriptions = dependencies.map(d => d.subscribe(accessor._setDirty));
|
const dependentSubscriptions = dependencies.map(d => d.subscribe(accessor._setDirty));
|
||||||
|
|
||||||
return accessor;
|
return accessor;
|
||||||
|
|||||||
@ -155,4 +155,28 @@ describe('A stream', () => {
|
|||||||
await aDep();
|
await aDep();
|
||||||
expect(aCount).toBe(2);
|
expect(aCount).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('detach breaks the logic chain', async () => {
|
||||||
|
const a = prop(0);
|
||||||
|
const b = computed(square, [a]);
|
||||||
|
const c = stream(delaySquare, [a]);
|
||||||
|
const d = stream(delayAdd, [a, c]);
|
||||||
|
const e = stream(delaySquare, [b]);
|
||||||
|
|
||||||
|
expect(await c()).toEqual(0);
|
||||||
|
expect(await d()).toEqual(0);
|
||||||
|
expect(await e()).toEqual(0);
|
||||||
|
|
||||||
|
a(1);
|
||||||
|
expect(await c()).toEqual(1);
|
||||||
|
expect(await d()).toEqual(2);
|
||||||
|
expect(await e()).toEqual(1);
|
||||||
|
|
||||||
|
c.detach();
|
||||||
|
|
||||||
|
a(2);
|
||||||
|
expect(await c()).toEqual(1);
|
||||||
|
expect(await d()).toEqual(3);
|
||||||
|
expect(await e()).toEqual(16);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
30
src/subscribable.js
Normal file
30
src/subscribable.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { call } from './util.js';
|
||||||
|
|
||||||
|
export const subscribable = () => {
|
||||||
|
const subscriptions = [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe(fn) {
|
||||||
|
subscriptions.push(fn);
|
||||||
|
return () => {
|
||||||
|
const idx = subscriptions.indexOf(fn);
|
||||||
|
if (idx !== -1) {
|
||||||
|
subscriptions.splice(idx, 1);
|
||||||
|
}
|
||||||
|
return subscriptions.length;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
unsubscribeAll() {
|
||||||
|
subscriptions.splice(0, Infinity);
|
||||||
|
},
|
||||||
|
_fire(val) {
|
||||||
|
return subscriptions.map(s => s(val)).forEach(call);
|
||||||
|
},
|
||||||
|
_forEachSubscription(fn) {
|
||||||
|
subscriptions.forEach(fn);
|
||||||
|
},
|
||||||
|
_hasSubscribers() {
|
||||||
|
return subscriptions.length > 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
17
src/util.js
17
src/util.js
@ -3,20 +3,3 @@ export const id = a => a;
|
|||||||
export const pick = (id, def) => doc => (doc && doc.hasOwnProperty(id) ? doc[id] : def);
|
export const pick = (id, def) => doc => (doc && doc.hasOwnProperty(id) ? doc[id] : def);
|
||||||
|
|
||||||
export const call = a => (typeof a === 'function' ? a() : a);
|
export const call = a => (typeof a === 'function' ? a() : a);
|
||||||
|
|
||||||
// internal utilities
|
|
||||||
|
|
||||||
export const registerSubscriptions = subscriptionsArray => fn => {
|
|
||||||
subscriptionsArray.push(fn);
|
|
||||||
return () => {
|
|
||||||
const idx = subscriptionsArray.indexOf(fn);
|
|
||||||
if (idx !== -1) {
|
|
||||||
subscriptionsArray.splice(idx, 1);
|
|
||||||
}
|
|
||||||
return subscriptionsArray.length;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const registerFire = subscriptionsArray => val => {
|
|
||||||
subscriptionsArray.map(s => s(val)).forEach(call);
|
|
||||||
};
|
|
||||||
|
|||||||
Reference in New Issue
Block a user