Skip to content

Commit

Permalink
Merge 716b51d into bfdc1b0
Browse files Browse the repository at this point in the history
  • Loading branch information
LFDM committed Apr 18, 2017
2 parents bfdc1b0 + 716b51d commit 6b959fc
Show file tree
Hide file tree
Showing 18 changed files with 942 additions and 40 deletions.
3 changes: 2 additions & 1 deletion package.json
Expand Up @@ -21,7 +21,8 @@
"mocha": "^2.5.3",
"nyc": "^10.1.2",
"sinon": "^1.17.7",
"sinon-chai": "^2.8.0"
"sinon-chai": "^2.8.0",
"webpack": "^1.14.0"
},
"scripts": {
"docs:prepare": "gitbook install",
Expand Down
21 changes: 13 additions & 8 deletions src/builder.js
@@ -1,7 +1,11 @@
import {mapObject, mapValues, compose, toObject, reduce, toPairs,
prop, filterObject, isEqual, not, curry, copyFunction} from './fp';
prop, filterObject, isEqual, not, curry, copyFunction,
set
} from './fp';

import {decorator} from './decorator';
import {dedup} from './dedup';
import {createListenerStore} from './listener-store';

// [[EntityName, EntityConfig]] -> Entity
const toEntity = ([name, c]) => ({
Expand Down Expand Up @@ -60,10 +64,8 @@ export const mapApiFunctions = (fn, entityConfigs) => {
}, entityConfigs);
};

const stripMetaData = (fn) => (...args) => fn(...args);

// EntityConfig -> Api
const toApi = mapValues(compose(mapValues(stripMetaData), prop('api')));
const toApi = mapValues(prop('api'));

// EntityConfig -> EntityConfig
const setEntityConfigDefaults = ec => {
Expand Down Expand Up @@ -111,14 +113,17 @@ const getEntityConfigs = compose(
filterObject(compose(not, isEqual('__config')))
);

const applyPlugin = curry((config, entityConfigs, plugin) => {
const pluginDecorator = plugin({ config, entityConfigs });
const applyPlugin = curry((addListener, config, entityConfigs, plugin) => {
const pluginDecorator = plugin({ addListener, config, entityConfigs });
return mapApiFunctions(pluginDecorator, entityConfigs);
});

// Config -> Api
export const build = (c, ps = []) => {
const config = c.__config || {idField: 'id'};
const createApi = compose(toApi, reduce(applyPlugin(config), getEntityConfigs(c)));
return createApi([decorator, ...ps, dedup]);
const listenerStore = createListenerStore(config);
const addListener = set(['__addListener'], listenerStore.addListener);
const applyPlugins = reduce(applyPlugin(listenerStore.addListener, config), getEntityConfigs(c));
const createApi = compose(addListener, toApi, applyPlugins);
return createApi([decorator(listenerStore.onChange), ...ps, dedup]);
};
49 changes: 48 additions & 1 deletion src/builder.spec.js
Expand Up @@ -4,7 +4,8 @@ import sinon from 'sinon';
import {build} from './builder';
import {curry} from './fp';

const getUsers = () => Promise.resolve([{id: 1}, {id: 2}]);
const users = [{ id: 1 }, { id: 2 }];
const getUsers = () => Promise.resolve(users);
getUsers.operation = 'READ';

const deleteUser = () => Promise.resolve();
Expand Down Expand Up @@ -147,4 +148,50 @@ describe('builder', () => {
.then(expectACall)
.then(() => done());
});

it('exposes Ladda\'s listener/onChange interface', () => {
const api = build(config());
expect(api.__addListener).to.be;
});

describe('__addListener', () => {
it('allows to add a listener, which gets notified on all cache changes', () => {
const api = build(config());
const spy = sinon.spy();
api.__addListener(spy);

return api.user.getUsers().then(() => {
expect(spy).to.have.been.calledOnce;
const changeObject = spy.args[0][0];
expect(changeObject.entity).to.equal('user');
expect(changeObject.type).to.equal('UPDATE');
expect(changeObject.entities).to.deep.equal(users);
});
});

it('does not trigger when a pure cache hit is made', () => {
const api = build(config());
const spy = sinon.spy();
api.__addListener(spy);

return api.user.getUsers().then(() => {
expect(spy).to.have.been.calledOnce;

return api.user.getUsers().then(() => {
expect(spy).to.have.been.calledOnce;
});
});
});

it('returns a deregistration function to remove the listener', () => {
const api = build(config());
const spy = sinon.spy();
const deregister = api.__addListener(spy);
deregister();

return api.user.getUsers().then(() => {
expect(spy).not.to.have.been.called;
});
});
});
});
4 changes: 2 additions & 2 deletions src/decorator/create.js
Expand Up @@ -6,7 +6,7 @@ import {addId} from '../id-helper';
export function decorateCreate(c, es, qc, e, aFn) {
return (...args) => {
return aFn(...args)
.then(passThrough(compose(put(es, e), addId(c, aFn, args))))
.then(passThrough(() => invalidate(qc, e, aFn)));
.then(passThrough(() => invalidate(qc, e, aFn)))
.then(passThrough(compose(put(es, e), addId(c, aFn, args))));
};
}
4 changes: 2 additions & 2 deletions src/decorator/delete.js
Expand Up @@ -5,8 +5,8 @@ import {serialize} from '../serializer';

export function decorateDelete(c, es, qc, e, aFn) {
return (...args) => {
remove(es, e, serialize(args));
return aFn(...args)
.then(passThrough(() => invalidate(qc, e, aFn)));
.then(passThrough(() => invalidate(qc, e, aFn)))
.then(() => remove(es, e, serialize(args)));
};
}
6 changes: 3 additions & 3 deletions src/decorator/index.js
Expand Up @@ -15,9 +15,9 @@ const HANDLERS = {
NO_OPERATION: decorateNoOperation
};

export const decorator = ({ config, entityConfigs }) => {
const entityStore = compose(createEntityStore, values)(entityConfigs);
const queryCache = createQueryCache(entityStore);
export const decorator = (onChange) => ({ config, entityConfigs }) => {
const entityStore = compose((c) => createEntityStore(c, onChange), values)(entityConfigs);
const queryCache = createQueryCache(entityStore, onChange);
return ({ entity, fn }) => {
const handler = HANDLERS[fn.operation];
return handler(config, entityStore, queryCache, entity, fn);
Expand Down
4 changes: 2 additions & 2 deletions src/decorator/update.js
Expand Up @@ -5,8 +5,8 @@ import {addId} from '../id-helper';

export function decorateUpdate(c, es, qc, e, aFn) {
return (eValue, ...args) => {
put(es, e, addId(c, undefined, undefined, eValue));
return aFn(eValue, ...args)
.then(passThrough(() => invalidate(qc, e, aFn)));
.then(passThrough(() => invalidate(qc, e, aFn)))
.then(passThrough(() => put(es, e, addId(c, undefined, undefined, eValue))));
};
}
48 changes: 33 additions & 15 deletions src/entity-store.js
Expand Up @@ -12,7 +12,8 @@
*/

import {merge} from './merger';
import {curry, reduce, map_, clone} from './fp';
import {curry, reduce, map_, clone, noop} from './fp';
import {removeId} from './id-helper';

// Value -> StoreValue
const toStoreValue = v => ({value: v, timestamp: Date.now()});
Expand Down Expand Up @@ -49,11 +50,15 @@ const createViewKey = (e, v) => {
// Entity -> Bool
const isView = e => !!e.viewOf;

// EntityStore -> Entity -> String -> ()
export const remove = (es, e, id) => {
rm(es, createEntityKey(e, {__ladda__id: id}));
rmViews(es, e);
};
// EntityStore -> Hook
const getHook = (es) => es[2];

// EntityStore -> Type -> [Entity] -> ()
const triggerHook = curry((es, e, type, xs) => getHook(es)({
type,
entity: e.name, // real name, not getEntityType, which takes views into account!
entities: removeId(xs)
}));

// Function -> Function -> EntityStore -> Entity -> Value -> a
const handle = curry((viewHandler, entityHandler, s, e, v) => {
Expand Down Expand Up @@ -94,11 +99,14 @@ const setViewValue = (s, e, v) => {
return v;
};

// EntityStore -> Entity -> Value -> ()
export const put = handle(setViewValue, setEntityValue);

// EntityStore -> Entity -> [Value] -> ()
export const mPut = curry((es, e) => map_(put(es, e)));
export const mPut = curry((es, e, xs) => {
map_(handle(setViewValue, setEntityValue)(es, e))(xs);
triggerHook(es, e, 'UPDATE', xs);
});

// EntityStore -> Entity -> Value -> ()
export const put = curry((es, e, x) => mPut(es, e, [x]));

// EntityStore -> Entity -> String -> Value
const getEntityValue = (s, e, id) => {
Expand All @@ -121,28 +129,38 @@ const getViewValue = (s, e, id) => {
// EntityStore -> Entity -> String -> ()
export const get = handle(getViewValue, getEntityValue);

// EntityStore -> Entity -> String -> ()
export const remove = (es, e, id) => {
const x = get(es, e, id);
rm(es, createEntityKey(e, {__ladda__id: id}));
rmViews(es, e);
if (x) {
triggerHook(es, e, 'DELETE', [x.value]);
}
};

// EntityStore -> Entity -> String -> Bool
export const contains = (es, e, id) => !!handle(getViewValue, getEntityValue)(es, e, id);

// EntityStore -> Entity -> EntityStore
const registerView = ([eMap, store], e) => {
const registerView = ([eMap, ...other], e) => {
if (!eMap[e.viewOf]) {
eMap[e.viewOf] = [];
}
eMap[e.viewOf].push(e.name);
return [eMap, store];
return [eMap, ...other];
};

// EntityStore -> Entity -> EntityStore
const registerEntity = ([eMap, store], e) => {
const registerEntity = ([eMap, ...other], e) => {
if (!eMap[e.name]) {
eMap[e.name] = [];
}
return [eMap, store];
return [eMap, ...other];
};

// EntityStore -> Entity -> EntityStore
const updateIndex = (m, e) => { return isView(e) ? registerView(m, e) : registerEntity(m, e); };

// [Entity] -> EntityStore
export const createEntityStore = c => reduce(updateIndex, [{}, {}], c);
export const createEntityStore = (c, hook = noop) => reduce(updateIndex, [{}, {}, hook], c);
74 changes: 73 additions & 1 deletion src/entity-store.spec.js
@@ -1,6 +1,7 @@
/* eslint-disable no-unused-expressions */

import {createEntityStore, put, get, contains, remove} from './entity-store';
import sinon from 'sinon';
import {createEntityStore, put, mPut, get, contains, remove} from './entity-store';
import {addId} from './id-helper';

const config = [
Expand Down Expand Up @@ -106,6 +107,22 @@ describe('EntityStore', () => {
expect(write).to.throw(Error);
});
});

describe('mPut', () => {
it('adds values which are later returned when calling get', () => {
const s = createEntityStore(config);
const v1 = {id: 'hello'};
const v2 = {id: 'there'};
const e = { name: 'user'};
const v1WithId = addId({}, undefined, undefined, v1);
const v2WithId = addId({}, undefined, undefined, v2);
mPut(s, e, [v1WithId, v2WithId]);
const r1 = get(s, e, v1.id);
const r2 = get(s, e, v2.id);
expect(r1.value).to.deep.equal({...v1, __ladda__id: 'hello'});
expect(r2.value).to.deep.equal({...v2, __ladda__id: 'there'});
});
});
describe('get', () => {
it('gets value with timestamp', () => {
const s = createEntityStore(config);
Expand Down Expand Up @@ -215,4 +232,59 @@ describe('EntityStore', () => {
expect(fn).to.not.throw();
});
});

describe('with a hook', () => {
describe('put', () => {
it('notifies with the put entity as singleton list', () => {
const hook = sinon.spy();
const s = createEntityStore(config, hook);
const v = {id: 'hello'};
const e = { name: 'user'};
put(s, e, addId({}, undefined, undefined, v));
expect(hook).to.have.been.called;

expect(hook).to.have.been.calledWith({
type: 'UPDATE',
entity: 'user',
entities: [v]
});
});
});

describe('mPut', () => {
it('notifies with the put entities', () => {
const hook = sinon.spy();
const s = createEntityStore(config, hook);
const v1 = {id: 'hello'};
const v2 = {id: 'there'};
const e = { name: 'user'};
const v1WithId = addId({}, undefined, undefined, v1);
const v2WithId = addId({}, undefined, undefined, v2);
mPut(s, e, [v1WithId, v2WithId]);

expect(hook).to.have.been.called;

const arg = hook.args[0][0];
expect(arg.type).to.equal('UPDATE');
expect(arg.entities).to.deep.equal([v1, v2]);
});
});

describe('rm', () => {
it('notifies with the removed entity as a singleton list', () => {
const hook = sinon.spy();
const s = createEntityStore(config, hook);
const v = {id: 'hello'};
const e = { name: 'user'};
put(s, e, addId({}, undefined, undefined, v));
remove(s, e, v.id);

expect(hook).to.have.been.calledTwice; // we also put!

const arg = hook.args[1][0];
expect(arg.type).to.equal('DELETE');
expect(arg.entities).to.deep.equal([v]);
});
});
});
});
9 changes: 9 additions & 0 deletions src/fp.js → src/fp/index.js
Expand Up @@ -184,3 +184,12 @@ export const snd = (arr) => arr[1];

export const toIdMap = toObject(prop('id'));

export const noop = () => {};

export const removeAtIndex = curry((i, list) => {
const isOutOfBounds = i === -1 || i > list.length - 1;
return isOutOfBounds ? list : [...list.slice(0, i), ...list.slice(i + 1)];
});

export const removeElement = curry((el, list) => removeAtIndex(list.indexOf(el), list));

0 comments on commit 6b959fc

Please sign in to comment.