Skip to content

Commit

Permalink
Merge 6bcd534 into 90a8679
Browse files Browse the repository at this point in the history
  • Loading branch information
LFDM committed Mar 16, 2017
2 parents 90a8679 + 6bcd534 commit f0273d6
Show file tree
Hide file tree
Showing 9 changed files with 147 additions and 15 deletions.
3 changes: 2 additions & 1 deletion src/builder.js
Expand Up @@ -68,7 +68,8 @@ const setApiConfigDefaults = ec => {
operation: 'NO_OPERATION',
invalidates: [],
idFrom: 'ENTITY',
byId: false
byId: false,
byIds: false
};

const writeToObjectIfNotSet = curry((o, [k, v]) => {
Expand Down
53 changes: 47 additions & 6 deletions src/decorator/read.js
@@ -1,12 +1,13 @@
import {get as getFromEs,
put as putInEs,
mPut as mPutInEs,
contains as inEs} from '../entity-store';
import {get as getFromQc,
invalidate,
put as putInQc,
contains as inQc,
getValue} from '../query-cache';
import {passThrough, compose} from '../fp';
import {passThrough, compose, curry, reduce, toIdMap, map, concat, zip} from '../fp';
import {addId, removeId} from '../id-helper';

const getTtl = e => e.ttl * 1000;
Expand All @@ -16,13 +17,21 @@ const hasExpired = (e, timestamp) => {
return (Date.now() - timestamp) > getTtl(e);
};

const readFromCache = curry((es, e, aFn, id) => {
if (inEs(es, e, id) && !aFn.alwaysGetFreshData) {
const v = getFromEs(es, e, id);
if (!hasExpired(e, v.timestamp)) {
return removeId(v.value);
}
}
return undefined;
});

const decorateReadSingle = (c, es, qc, e, aFn) => {
return (id) => {
if (inEs(es, e, id) && !aFn.alwaysGetFreshData) {
const v = getFromEs(es, e, id);
if (!hasExpired(e, v.timestamp)) {
return Promise.resolve(removeId(v.value));
}
const fromCache = readFromCache(es, e, aFn, id);
if (fromCache) {
return Promise.resolve(fromCache);
}

return aFn(id)
Expand All @@ -31,6 +40,35 @@ const decorateReadSingle = (c, es, qc, e, aFn) => {
};
};

const decorateReadSome = (c, es, qc, e, aFn) => {
return (ids) => {
const readFromCache_ = readFromCache(es, e, aFn);
const [cached, remaining] = reduce(([c_, r], id) => {
const fromCache = readFromCache_(id);
if (fromCache) {
c_.push(fromCache);
} else {
r.push(id);
}
return [c_, r];
}, [[], []], ids);

if (!remaining.length) {
return Promise.resolve(cached);
}

const addIds = map(([id, item]) => addId(c, aFn, id, item));

return aFn(remaining)
.then(passThrough(compose(mPutInEs(es, e), addIds, zip(remaining))))
.then(passThrough(() => invalidate(qc, e, aFn)))
.then((other) => {
const asMap = compose(toIdMap, concat)(cached, other);
return map((id) => asMap[id], ids);
});
};
};

const decorateReadQuery = (c, es, qc, e, aFn) => {
return (...args) => {
if (inQc(qc, e, aFn, args) && !aFn.alwaysGetFreshData) {
Expand All @@ -50,5 +88,8 @@ export function decorateRead(c, es, qc, e, aFn) {
if (aFn.byId) {
return decorateReadSingle(c, es, qc, e, aFn);
}
if (aFn.byIds) {
return decorateReadSome(c, es, qc, e, aFn);
}
return decorateReadQuery(c, es, qc, e, aFn);
}
76 changes: 76 additions & 0 deletions src/decorator/read.spec.js
Expand Up @@ -5,6 +5,7 @@ import {decorateRead} from './read';
import {createEntityStore} from '../entity-store';
import {createQueryCache} from '../query-cache';
import {createSampleConfig, createApiFunction} from '../test-helper';
import {map} from '../fp';

const config = createSampleConfig();

Expand Down Expand Up @@ -78,6 +79,81 @@ describe('Read', () => {
done();
});
});
fdescribe('with byIds', () => {
const users = {
a: { id: 'a' },
b: { id: 'b' },
c: { id: 'c' }
};

const fn = (ids) => Promise.resolve(map((id) => users[id], ids));
const decoratedFn = createApiFunction(fn, { byIds: true });

it('calls api fn if nothing in cache', () => {
const es = createEntityStore(config);
const qc = createQueryCache(es);
const e = config[0];
const fnWithSpy = sinon.spy(decoratedFn);
const apiFn = decorateRead({}, es, qc, e, fnWithSpy);
return apiFn(['a', 'b']).then((res) => {
expect(res).to.deep.equal([users.a, users.b]);
});
});

it('calls api fn if nothing in cache', () => {
const es = createEntityStore(config);
const qc = createQueryCache(es);
const e = config[0];
const fnWithSpy = sinon.spy(decoratedFn);
const apiFn = decorateRead({}, es, qc, e, fnWithSpy);
return apiFn(['a', 'b']).then((res) => {
expect(res).to.deep.equal([users.a, users.b]);
});
});

it('puts item in the cache and can read them again', () => {
const es = createEntityStore(config);
const qc = createQueryCache(es);
const e = config[0];
const fnWithSpy = sinon.spy(decoratedFn);
const apiFn = decorateRead({}, es, qc, e, fnWithSpy);
const args = ['a', 'b'];
return apiFn(args).then(() => {
return apiFn(args).then((res) => {
expect(fnWithSpy).to.have.been.calledOnce;
expect(res).to.deep.equal([users.a, users.b]);
});
});
});

it('only makes additional request for uncached items', () => {
const es = createEntityStore(config);
const qc = createQueryCache(es);
const e = config[0];
const fnWithSpy = sinon.spy(decoratedFn);
const apiFn = decorateRead({}, es, qc, e, fnWithSpy);
return apiFn(['a', 'b']).then(() => {
return apiFn(['b', 'c']).then(() => {
expect(fnWithSpy).to.have.been.calledTwice;
expect(fnWithSpy).to.have.been.calledWith(['a', 'b']);
expect(fnWithSpy).to.have.been.calledWith(['c']);
});
});
});

it('returns all items in correct order when making partial requests', () => {
const es = createEntityStore(config);
const qc = createQueryCache(es);
const e = config[0];
const fnWithSpy = sinon.spy(decoratedFn);
const apiFn = decorateRead({}, es, qc, e, fnWithSpy);
return apiFn(['a', 'b']).then(() => {
return apiFn(['a', 'b', 'c']).then((res) => {
expect(res).to.deep.equal([users.a, users.b, users.c]);
});
});
});
});
it('calls api fn if not in cache', (done) => {
const es = createEntityStore(config);
const qc = createQueryCache(es);
Expand Down
3 changes: 3 additions & 0 deletions src/entity-store.js
Expand Up @@ -97,6 +97,9 @@ const setViewValue = (s, e, v) => {
// EntityStore -> Entity -> Value -> ()
export const put = handle(setViewValue, setEntityValue);

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

// EntityStore -> Entity -> String -> Value
const getEntityValue = (s, e, id) => {
const k = createEntityKey(e, {__ladda__id: id});
Expand Down
2 changes: 2 additions & 0 deletions src/fp.js
Expand Up @@ -182,3 +182,5 @@ export const uniq = (arr) => [...new Set(arr)];
export const fst = (arr) => arr[0];
export const snd = (arr) => arr[1];

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

14 changes: 13 additions & 1 deletion src/fp.spec.js
Expand Up @@ -6,7 +6,7 @@ import {debug, identity, curry, passThrough,
on2, init, tail, last, head, map, map_, reverse,
reduce, compose, prop, zip, flip, toPairs, fromPairs,
mapObject, mapValues, toObject, filter, clone, filterObject,
copyFunction, get, set, concat, flatten, uniq} from './fp';
copyFunction, get, set, concat, flatten, uniq, toIdMap} from './fp';

describe('fp', () => {
describe('debug', () => {
Expand Down Expand Up @@ -347,4 +347,16 @@ describe('fp', () => {
expect(actual).to.deep.equal(expected);
});
});

describe('toIdMap', () => {
it('returns a map with ids as keys from a list of entities', () => {
const a = { id: 'a' };
const b = { id: 'b' };
const c = { id: 'c' };

const expected = { a, b, c };
const actual = toIdMap([a, b, c]);
expect(actual).to.deep.equal(expected);
});
});
});
6 changes: 2 additions & 4 deletions src/plugins/denormalizer/index.js
@@ -1,7 +1,7 @@
import {
compose, curry, head, map, mapObject, mapValues,
prop, reduce, fromPairs, toPairs, toObject, values,
uniq, flatten, get, set, snd
prop, reduce, fromPairs, toPairs, values,
uniq, flatten, get, set, snd, toIdMap
} from '../../fp';

/* TYPES
Expand All @@ -23,8 +23,6 @@ export const NAME = 'denormalizer';

const def = curry((a, b) => b || a);

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

const getApi = curry((configs, entityName) => compose(prop('api'), prop(entityName))(configs));

const getPluginConf_ = curry((config) => compose(prop(NAME), def({}), prop('plugins'))(config));
Expand Down
4 changes: 1 addition & 3 deletions src/plugins/denormalizer/index.spec.js
Expand Up @@ -3,11 +3,9 @@
import sinon from 'sinon';

import { build } from '../../builder';
import { curry, prop, head, last, toObject, values } from '../../fp';
import { curry, head, last, toIdMap, values } from '../../fp';
import { denormalizer, extractAccessors } from '.';

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

const peter = { id: 'peter' };
const gernot = { id: 'gernot' };
const robin = { id: 'robin' };
Expand Down
1 change: 1 addition & 0 deletions src/test-helper.js
Expand Up @@ -6,6 +6,7 @@ export const createApiFunction = (fn, config = {}) => {
fnCopy.invalidates = config.invalidates || [];
fnCopy.idFrom = config.idFrom || 'ENTITY';
fnCopy.byId = config.byId || false;
fnCopy.byIds = config.byIds || false;
return fnCopy;
};

Expand Down

0 comments on commit f0273d6

Please sign in to comment.