Skip to content
Open
1 change: 1 addition & 0 deletions src/builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ const getGlobalConfig = (config) => ({
idField: 'id',
enableDeduplication: true,
useProductionBuild: process.NODE_ENV === 'production',
strictMode: true,
...(config.__config || {})
});

Expand Down
73 changes: 70 additions & 3 deletions src/builder.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,24 @@ deleteUser.operation = 'DELETE';
const noopUser = () => Promise.resolve(['a', 'b']);
noopUser.operation = 'NO_OPERATION';

const config = () => ({
const updateUser = (user) => Promise.resolve(user);
updateUser.operation = 'UPDATE';

const config = (globals = {}) => ({
user: {
ttl: 300,
api: {
getUser,
getUsers,
deleteUser,
noopUser
noopUser,
updateUser
},
invalidates: ['alles']
},
__config: {
useProductionBuild: true
useProductionBuild: true,
...globals
}
});

Expand Down Expand Up @@ -568,5 +573,67 @@ describe('builder', () => {
});
});
});

describe('immutability', () => {
it('does return the same object twice when cache is hit', () => {
const myConfig = config();
const api = build(myConfig);

return api.user.getUser('1').then(firstUser => {
return api.user.getUser('1').then(secondUser => {
expect(firstUser).to.equal(secondUser);
});
});
});

it('does not return the same object after a cache update', () => {
const myConfig = config();
const api = build(myConfig);

return api.user.getUser('1').then(firstUser => {
return api.user.updateUser({ id: '1' }).then(() => {
return api.user.getUser('1').then(secondUser => {
expect(firstUser).not.to.equal(secondUser);
});
});
});
});
});

describe('strict mode', () => {
const testFreeze = () => {
const myConfig = config();
const api = build(myConfig);

return api.user.getUser('1').then(firstUser => {
return api.user.updateUser({ id: '1', data: { x: 'a' } }).then((secondUser) => {
expect(() => { firstUser.id = '2'; }).to.throw('read only');
expect(() => { secondUser.id = '2'; }).to.throw('read only');
expect(() => { secondUser.data.x = 'b'; }).to.throw('read only');
});
});
};

it('returns deep-frozen objects when in strict mode', () => {
testFreeze();
});

it('defaults to strict mode', () => {
testFreeze();
});

it('does not return frozen objects when not in strict mode', () => {
const myConfig = config({ strictMode: false });
const api = build(myConfig);

return api.user.getUser('1').then(firstUser => {
return api.user.updateUser({ id: '1', data: { x: 'a' } }).then((secondUser) => {
expect(() => { firstUser.id = '2'; }).not.to.throw('read only');
expect(() => { secondUser.id = '2'; }).not.to.throw('read only');
expect(() => { secondUser.data.x = 'b'; }).not.to.throw('read only');
});
});
});
});
});

6 changes: 3 additions & 3 deletions src/plugins/cache/cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import {curry} from 'ladda-fp';
import * as QueryCache from './query-cache';
import * as EntityStore from './entity-store';

export const createCache = (entityConfigs, onChange) => {
const entityStore = EntityStore.createEntityStore(entityConfigs, onChange);
const queryCache = QueryCache.createQueryCache(entityStore, onChange);
export const createCache = (entityConfigs, globalConfig = {}) => {
const entityStore = EntityStore.createEntityStore(entityConfigs, globalConfig);
const queryCache = QueryCache.createQueryCache(entityStore);
return {entityStore, queryCache};
};

Expand Down
62 changes: 45 additions & 17 deletions src/plugins/cache/entity-store.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,42 @@
* Of course, this also requiers the view to truly be a subset of the entity.
*/

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

// Value -> StoreValue
const toStoreValue = v => ({value: v, timestamp: Date.now()});
const deepFreeze = o => {
if (Array.isArray(o)) {
return Object.freeze(map(deepFreeze, o));
}
if (typeof o === 'object') {
return Object.freeze(reduce(
(m, k) => {
m[k] = deepFreeze(o[k]);
return m;
},
{},
Object.keys(o)
));
}
return o;
};

// Bool -> Value -> StoreValue
const toStoreValue = (strictMode, v) => ({
value: strictMode ? { ...v, item: deepFreeze(v.item) } : v,
timestamp: Date.now()
});

// EntityStore -> String -> Value
const read = ([_, s], k) => (s[k] ? {...s[k], value: clone(s[k].value)} : s[k]);
const read = ([_, s], k) => (s[k] ? {...s[k], value: s[k].value} : s[k]);

// EntityStore -> String -> Value -> ()
const set = ([eMap, s], k, v) => { s[k] = toStoreValue(clone(v)); };
// EntityStore -> String -> Value -> Value
const set = ([eMap, s, c], k, v) => {
const storeValue = toStoreValue(c.strictMode, v);
s[k] = storeValue;
return storeValue.value.item;
};

// EntityStore -> String -> ()
const rm = curry(([_, s], k) => delete s[k]);
Expand Down Expand Up @@ -67,8 +91,7 @@ const setEntityValue = (s, e, v) => {
throw new Error(`Value is missing id, tried to add to entity ${e.name}`);
}
const k = createEntityKey(e, v);
set(s, k, v);
return v;
return set(s, k, v);
};

// EntityStore -> Entity -> Value -> ()
Expand All @@ -79,23 +102,21 @@ const setViewValue = (s, e, v) => {

if (entityValueExist(s, e, v)) {
const eValue = read(s, createEntityKey(e, v)).value;
setEntityValue(s, e, merge(v, eValue));
rmViews(s, e); // all views will prefer entity cache since it is newer
} else {
const k = createViewKey(e, v);
set(s, k, v);
return setEntityValue(s, e, merge(v, eValue));
}

return v;
const k = createViewKey(e, v);
return set(s, k, v);
};

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

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

// EntityStore -> Entity -> String -> Value
const getEntityValue = (s, e, id) => {
Expand Down Expand Up @@ -152,5 +173,12 @@ const registerEntity = ([eMap, ...other], e) => {
// 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);
// GlobalConfig -> EntityStoreGlobalConfig
const getGlobalConfig = ({ strictMode }) => ({ strictMode });

// [Entity] -> GlobalConfig -> EntityStore
export const createEntityStore = (es, c = {}) => reduce(
updateIndex,
[{}, {}, getGlobalConfig(c)],
es
);
33 changes: 12 additions & 21 deletions src/plugins/cache/entity-store.spec.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable no-unused-expressions */

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

const config = [
{
Expand Down Expand Up @@ -62,24 +62,15 @@ describe('EntityStore', () => {
const e = { name: 'user'};
put(s, e, addId({}, undefined, undefined, v));
const r = get(s, e, v.id);
expect(r.value).to.deep.equal({...v, __ladda__id: 'hello'});
});
it('altering an added value does not alter the stored value when doing a get later', () => {
const s = createEntityStore(config);
const v = {id: 'hello', name: 'kalle'};
const e = { name: 'user'};
put(s, e, addId({}, undefined, undefined, v));
v.name = 'ingvar';
const r = get(s, e, v.id);
expect(r.value.name).to.equal('kalle');
expect(r.value).to.deep.equal(withId('hello', v));
});
it('an added value to a view is later returned when calling get for view', () => {
const s = createEntityStore(config);
const v = {id: 'hello'};
const e = { name: 'user'};
put(s, e, addId({}, undefined, undefined, v));
const r = get(s, e, v.id);
expect(r.value).to.deep.equal({...v, __ladda__id: 'hello'});
expect(r.value).to.deep.equal(withId('hello', v));
});
it('merges view into entity value', () => {
const s = createEntityStore(config);
Expand All @@ -89,7 +80,7 @@ describe('EntityStore', () => {
put(s, e, addId({}, undefined, undefined, {...v, name: 'kalle'}));
put(s, eView, addId({}, undefined, undefined, {...v, name: 'ingvar'}));
const r = get(s, eView, v.id);
expect(r.value).to.be.deep.equal({__ladda__id: 'hello', id: 'hello', name: 'ingvar'});
expect(r.value).to.be.deep.equal(withId('hello', { id: 'hello', name: 'ingvar' }));
});
it('writing view value without id throws error', () => {
const s = createEntityStore(config);
Expand Down Expand Up @@ -118,8 +109,8 @@ describe('EntityStore', () => {
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'});
expect(r1.value).to.deep.equal(withId('hello', v1));
expect(r2.value).to.deep.equal(withId('there', v2));
});
});
describe('get', () => {
Expand All @@ -131,15 +122,15 @@ describe('EntityStore', () => {
const r = get(s, e, v.id);
expect(r.timestamp).to.not.be.undefined;
});
it('altering retrieved value does not alter the stored value', () => {
xit('altering retrieved value does not alter the stored value', () => {
const s = createEntityStore(config);
const v = {id: 'hello', name: 'kalle'};
const e = { name: 'user'};
put(s, e, addId({}, undefined, undefined, v));
const r = get(s, e, v.id);
r.value.name = 'ingvar';
const r2 = get(s, e, v.id);
expect(r2.value.name).to.equal(v.name);
expect(r2.value.item.name).to.equal(v.name);
});
it('gets undefined if value does not exist', () => {
const s = createEntityStore(config);
Expand All @@ -162,15 +153,15 @@ describe('EntityStore', () => {
put(s, e, addId({}, undefined, undefined, v));
const eView = {name: 'userPreview', viewOf: 'user'};
const r = get(s, eView, v.id);
expect(r.value).to.be.deep.equal({...v, __ladda__id: 'hello'});
expect(r.value).to.be.deep.equal(withId('hello', v));
});
it('gets view if only it exist', () => {
const s = createEntityStore(config);
const v = {id: 'hello'};
const eView = {name: 'userPreview', viewOf: 'user'};
put(s, eView, addId({}, undefined, undefined, v));
const r = get(s, eView, v.id);
expect(r.value).to.be.deep.equal({...v, __ladda__id: 'hello'});
expect(r.value).to.be.deep.equal(withId('hello', v));
});
it('gets entity value if same timestamp as view value', () => {
const s = createEntityStore(config);
Expand All @@ -180,7 +171,7 @@ describe('EntityStore', () => {
put(s, eView, addId({}, undefined, undefined, v));
put(s, e, addId({}, undefined, undefined, {...v, name: 'kalle'}));
const r = get(s, eView, v.id);
expect(r.value).to.be.deep.equal({...v, name: 'kalle', __ladda__id: 'hello'});
expect(r.value).to.be.deep.equal(withId('hello', {...v, name: 'kalle' }));
});
it('gets entity value if newer than view value', (done) => {
const s = createEntityStore(config);
Expand All @@ -191,7 +182,7 @@ describe('EntityStore', () => {
setTimeout(() => {
put(s, e, addId({}, undefined, undefined, {...v, name: 'kalle'}));
const r = get(s, eView, v.id);
expect(r.value).to.be.deep.equal({...v, name: 'kalle', __ladda__id: 'hello'});
expect(r.value).to.be.deep.equal(withId('hello', {...v, name: 'kalle' }));
done();
}, 1);
});
Expand Down
26 changes: 8 additions & 18 deletions src/plugins/cache/id-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,18 @@ export const getId = curry((c, aFn, args, o) => {
return getIdGetter(c, aFn)(o);
});

export const withId = (id, item) => ({ __ladda__id: id, item });
export const withoutId = (itemWithId) => itemWithId.item;

export const addId = curry((c, aFn, args, o) => {
if (aFn && aFn.idFrom === 'ARGS') {
return {
...o,
__ladda__id: createIdFromArgs(args)
};
return withId(createIdFromArgs(args), o);
}
const getId_ = getIdGetter(c, aFn);
if (Array.isArray(o)) {
return map(x => ({
...x,
__ladda__id: getId_(x)
}), o);
return map(x => withId(getId_(x), x), o);
}
return {
...o,
__ladda__id: getId_(o)
};
return withId(getId_(o), o);
});

export const removeId = (o) => {
Expand All @@ -45,11 +39,7 @@ export const removeId = (o) => {
}

if (Array.isArray(o)) {
return map(x => {
delete x.__ladda__id;
return x;
}, o);
return map(withoutId, o);
}
delete o.__ladda__id;
return o;
return withoutId(o);
};
Loading