Skip to content

Commit

Permalink
Merge pull request #19 from shiftyp/feature/#17-state-shape
Browse files Browse the repository at this point in the history
#17: Allowed for adding and removing nodes from objects. Added tests
  • Loading branch information
shiftyp authored Jul 13, 2016
2 parents fcd1a9a + c288b00 commit 07ce810
Show file tree
Hide file tree
Showing 3 changed files with 161 additions and 40 deletions.
30 changes: 23 additions & 7 deletions lib/state/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ export const createChildAccessor = (addChildrenSubject, getChildrenSubject) => {
const nodeSubject = beforeChildren[key];
if (!nodeSubject || nodeSubject.getValue().provisional) {
if (typeof value !== 'undefined') {
addChildrenSubject.onNext({ key, value, provisional: false });
addChildrenSubject.onNext({ action: 'add', key, value, provisional: false });
} else if (!nodeSubject) {
addChildrenSubject.onNext({ key, value: null, provisional: true });
addChildrenSubject.onNext({ action: 'add', key, value: null, provisional: true });
}
}
return getChildrenSubject.getValue()[key].getValue();
Expand All @@ -41,7 +41,8 @@ export const createChildAccessor = (addChildrenSubject, getChildrenSubject) => {
export const createFinalNodeFromProvisionalNode = ({
observable,
provisionalNode,
setNextState
setNextState,
setCompleted
}) => {
const { observableSubject, nodeSubject } = provisionalNode;

Expand All @@ -56,6 +57,9 @@ export const createFinalNodeFromProvisionalNode = ({
if (typeof setNextState === 'function') {
nodeProps.setNextState = setNextState;
}
if (typeof setCompleted === 'function') {
nodeProps.setCompleted = setCompleted;
}

const node = Object.assign(
createNodeAccessor(nodeProps),
Expand All @@ -74,6 +78,7 @@ export const createInitialNode = ({
observable,
hookMap,
setNextState,
setCompleted,
provisional
}) => {
const observableSubject = new Rx.ReplaySubject(1);
Expand All @@ -91,19 +96,19 @@ export const createInitialNode = ({
hookMap,
pauser
);
const connectDisposable = new Rx.CompositeDisposable();
const connect = () => {
const allDisposable = new Rx.CompositeDisposable();
const children = getChildrenSubject && getChildrenSubject.getValue();
if (children) {
const keys = Object.keys(children);
keys.forEach(key => allDisposable.add(
keys.forEach(key => connectDisposable.add(
children[key].getValue().connect()
));
}
allDisposable.add(
connectDisposable.add(
asObservable().connect()
);
return allDisposable;
return connectDisposable;
};

const setInitialState = (initialState) => {
Expand All @@ -122,6 +127,13 @@ export const createInitialNode = ({
return publishNode(nodeSubject.getValue());
}
};
const setNodeCompleted = () => {
setCompleted();
observableSubject.onCompleted();
// Dispose on next tick so onComplete handlers
// will be invoked.
setTimeout(() => connectDisposable.dispose());
};

if (observable) {
observableSubject.onNext(observable);
Expand All @@ -133,6 +145,7 @@ export const createInitialNode = ({
reduce,
child,
setNextState,
setCompleted: setNodeCompleted,
nodeSubject,
provisional: !!provisional,
provisionalNode: !!provisional && node,
Expand Down Expand Up @@ -161,13 +174,15 @@ export const createNode = ({
observable,
hookMap,
setNextState,
setCompleted,
provisional,
provisionalNode
}) => {
if (provisionalNode) {
return createFinalNodeFromProvisionalNode({
observable,
setNextState,
setCompleted,
provisionalNode
});
} else {
Expand All @@ -178,6 +193,7 @@ export const createNode = ({
observable,
hookMap,
setNextState,
setCompleted,
provisional
});
}
Expand Down
101 changes: 68 additions & 33 deletions lib/state/tree.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,35 @@ import {
throwFinalizedError
} from '../errors';

export const createTreeSetNextState = (childrenObservable) => {
export const createTreeSetNextState = (
childrenObservable,
addChildrenSubject,
pauser
) => {
const newStateSubject = new Rx.Subject();
newStateSubject
.withLatestFrom(childrenObservable)
.subscribe(([newState, children]) => {
const keys = Object.keys(children);
const newKeys = Object.keys(newState);
const additionalState = {};
const pruneState = {};

if (newKeys.length !== keys.length) {
throwShapeError();
}

keys.forEach(key => {
if (key in newState) {
newKeys.forEach(key => {
const child = children[key];
if (child && !child.getValue().provisional) {
children[key].getValue().setNextState(newState[key]);
} else {
throwShapeError();
Object.assign(additionalState, { [key]: newState[key] })
}
});
keys.forEach(key => {
if (!(key in newState)) {
Object.assign(pruneState, { [key]: true });
}
});

updateAddChildrenSubject(additionalState, pruneState, addChildrenSubject, pauser)
});

return (newState) => {
Expand Down Expand Up @@ -74,37 +84,52 @@ export const createChildrenObservable = ({
}) => {
return addChildrenSubject
.withLatestFrom(getChildrenSubject, (child, acc) => {
const { key, value, provisional } = child;
const oldNode = acc[key] && acc[key].getValue();
if(!oldNode || oldNode.provisional) {
const newNode = createTree({
initialState: value,
pauser,
hookMap,
createNode,
provisional,
provisionalNode: oldNode
});
return Object.assign({}, acc, {
[key]: newNode.nodeSubject
});
} else if (!oldNode.provisional) {
throwFinalizedError(key);
const { action, key, value, provisional } = child;
if (action === 'add') {
const oldNode = acc[key] && acc[key].getValue();
if(!oldNode || oldNode.provisional) {
const newNode = createTree({
initialState: value,
pauser,
hookMap,
createNode,
provisional,
provisionalNode: oldNode
});
return Object.assign({}, acc, {
[key]: newNode.nodeSubject
});
} else if (!oldNode.provisional) {
throwFinalizedError(key);
} else {
return acc;
}
} else {
const node = acc[key] && acc[key].getValue();
if (node) {
node.setCompleted();
delete acc[node];
}
return acc;
}
})
.shareReplay(1);
}
};

export const updateAddChildrenSubject = (initialState, addChildrenSubject, pauser) => {
const keys = Object.keys(initialState);
export const updateAddChildrenSubject = (addState, pruneState, addChildrenSubject, pauser) => {
const addKeys = Object.keys(addState);
pauser.onNext(false);
keys.forEach((key) => {
addKeys.forEach((key) => {
addChildrenSubject.onNext(
{ key, value: initialState[key], provisional: false }
{ action: 'add', key, value: addState[key], provisional: false }
);
});
if (pruneState) {
const pruneKeys = Object.keys(pruneState);
pruneKeys.forEach(key => {
addChildrenSubject.onNext({ action: 'prune', key });
})
}
pauser.onNext(true);
};

Expand All @@ -117,7 +142,7 @@ export const createFinalTreeFromProvisionalNode = ({
addChildrenSubject,
pauser
} = provisionalNode;
updateAddChildrenSubject(initialState, addChildrenSubject, pauser);
updateAddChildrenSubject(initialState, null, addChildrenSubject, pauser);
return createNode({
provisional: false,
provisionalNode
Expand Down Expand Up @@ -154,12 +179,17 @@ const createInitialTree = ({
childrenObservable.subscribeOnError(err => { throw err });

if (!provisional) {
updateAddChildrenSubject(initialState, addChildrenSubject, pauser);
updateAddChildrenSubject(initialState, null, addChildrenSubject, pauser);
}

const valueObservable = createTreeObservable(childrenObservable)
.pausable(pauser);
const setNextState = createTreeSetNextState(childrenObservable, pauser);
const setNextState = createTreeSetNextState(childrenObservable, addChildrenSubject, pauser);
const setCompleted = () => {
addChildrenSubject.onCompleted();
getChildrenSubject.onCompleted();
valueSubject.onCompleted();
};

valueObservable.subscribe(valueSubject);

Expand All @@ -170,6 +200,7 @@ const createInitialTree = ({
observable: valueSubject.asObservable(),
hookMap,
setNextState,
setCompleted,
provisional,
provisionalNode
});
Expand Down Expand Up @@ -201,13 +232,17 @@ export const createLeaf = ({
.distinctUntilChanged();
const setNextState = (newState) => {
subject.onNext(newState);
}
};
const setCompleted = () => {
subject.onCompleted();
};

return createNode({
observable,
hookMap,
pauser,
setNextState,
setCompleted,
provisional: false,
provisionalNode
});
Expand Down
70 changes: 70 additions & 0 deletions test/state/tree.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import {
import {
createNode
} from '../../dist/state/node';
import {
createAction
} from '../../dist/action';

test('createLeaf observable should have initial value', t => {
const testVal = 2;
Expand Down Expand Up @@ -166,3 +169,70 @@ test('separate hooks into a single action should lead to one update on parent',
singleAction.onNext(1);
singleAction.onNext(2);
});

test.cb('children should be pruned if excluded from reduced state', t => {
const state = createState({ foo: 1, bar: 1 });
const pruneAction = createAction();
const testAction = createAction();
const pruneState = state('bar');

// The 2 is significant, if changed change
// pruneState subscription
pruneState.reduce(testAction, () => 2);

pruneState.asObservable().subscribe((val) => {
// Val will equal 2 if the subscription
// is active when testAction is called.
// Initial state of 'bar' is expected to
// be not 2
if (val === 2) {
t.fail()
}
// completed will be called when a node is
// pruned, all subscriptions to the node's
// observable will be disposed as well on
// the next tick.
}, null, () => t.pass());

state.reduce(pruneAction, state => ({ foo: 1 }));

state.connect();

t.plan(1);

pruneAction();

// Test that subscriptions have been disposed
setTimeout(() => testAction() || t.end());
});

test('reducers should be able to add children dynamically if in reduced state', t => {
const state = createState({ foo: 1 });
const addAction = createAction();

state.reduce(addAction, () => ({ foo: 1, bar: 1, baz: 1 }));

t.plan(2);

// This will create a provisional node to be
// populated by the reducer.
state('bar')
.asObservable()
.subscribe(val => t.is(val, 1));

state.connect();

addAction();

// This node didn't have a provisional node,
// but we should be able to access the one
// created by reduce.
state('baz')
.asObservable()
.subscribe(val => t.is(val, 1));

// Because no provisional node existed when
// we connected the state, we have to connect
// the new node here.
state('baz').connect();
});

0 comments on commit 07ce810

Please sign in to comment.