Skip to content

Commit

Permalink
Merge pull request #23 from ladda-js/dedup-docs
Browse files Browse the repository at this point in the history
Dedup docs
  • Loading branch information
LFDM committed Apr 24, 2017
2 parents f5c97a4 + e9a39e2 commit 182c5cc
Show file tree
Hide file tree
Showing 6 changed files with 178 additions and 25 deletions.
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
* [Advanced](/docs/advanced/README.md)
* [Invalidation](/docs/advanced/Invalidation.md)
* [Views](/docs/advanced/ViewOf.md)
* [Deduplication](/docs/advanced/Deduplication.md)
* [Custom ID](/docs/advanced/CustomId.md)
* [Plugins](/docs/advanced/Plugins.md)
* [Cheat Sheet](/docs/advanced/Plugins-CheatSheet.md)
Expand Down
30 changes: 30 additions & 0 deletions docs/advanced/Deduplication.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Deduplication

Ladda tries to optimize "READ" operations by deduplicating identical
simultaneous requests and therefore reduce the load both on server and
client.

*Identical* means calling the same function with identical arguments.
<br>
*Simultaneous* means that another call has been made before the first
call has been resolved or rejected.

Given the following code, where `getUsers` is a "READ" operation:

```javascript
// Component 1
api.user.getUsers();

// Component 2
api.user.getUsers();

// Component 3
api.user.getUsers();
```

Ladda will only make a single call to `getUsers` and distribute its
result to all callers.


This feature can be disabled on a global, an entity and a function
level. Check the [Configuration Reference](/docs/basics/Configuration.md) for details.
9 changes: 9 additions & 0 deletions docs/basics/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ There are a handful optional options to configure Ladda. In a minimal configurat

* **api** (required): An object of ApiFunctions, functions that communicate with an external service and return a Promise. The function name as key and function as value.

* **noDedup**: `true | false` Disable [deduplication](/docs/advanced/Deduplication.md)
of all "READ" operations for this entity. Defaults to false.

## Method Configuration

* **operation**: `"CREATE" | "READ" | "UPDATE" | "DELETE" | "NO_OPERATION"`. Default is `"NO_OPERATION"`.
Expand All @@ -29,6 +32,12 @@ There are a handful optional options to configure Ladda. In a minimal configurat
call (if any), looking up items from the cache and only calling for
items that are not yet present. Defaults to false.

* **noDedup**: `true | false` Disable [deduplication](/docs/advanced/Deduplication.md)
a "READ" operation. Defaults to false.

## Ladda Configuration

* **idField**: Specify the default property that contains the ID. By default this is `"id"`.

* **noDedup**: `true | false` Disable [deduplication](/docs/advanced/Deduplication.md)
of "READ" operation for all entities. Defaults to false.
5 changes: 4 additions & 1 deletion src/builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ const setEntityConfigDefaults = ec => {
ttl: 300,
invalidates: [],
invalidatesOn: ['CREATE', 'UPDATE', 'DELETE'],
noDedup: false,
...ec
};
};
Expand All @@ -84,7 +85,8 @@ const setApiConfigDefaults = ec => {
invalidates: [],
idFrom: 'ENTITY',
byId: false,
byIds: false
byIds: false,
noDedup: false
};

const writeToObjectIfNotSet = curry((o, [k, v]) => {
Expand Down Expand Up @@ -115,6 +117,7 @@ export const getEntityConfigs = compose( // exported for testing

const getGlobalConfig = (config) => ({
idField: 'id',
noDedup: false,
useProductionBuild: process.NODE_ENV === 'production',
...(config.__config || {})
});
Expand Down
47 changes: 38 additions & 9 deletions src/validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const isValidLogger = (logger) => logger && typeof logger.error === 'function';

const getEntityNames = (entityConfigs) => Object.keys(entityConfigs);

const checkApiDeclaration = (logger, entityConfigs, config, entityName, entity) => {
const checkApiDeclaration = (logger, entityConfigs, entityName, entity) => {
if (typeof entity.api !== 'object') {
warn(logger, `No api definition found for entity ${entityName}`);
return;
Expand All @@ -30,7 +30,7 @@ const checkApiDeclaration = (logger, entityConfigs, config, entityName, entity)
compose(
// eslint-disable-next-line no-unused-vars
map_(([fnName, fn]) => {
const { operation, invalidates, idFrom, byId, byIds } = fn;
const { operation, invalidates, idFrom, byId, byIds, noDedup } = fn;
const fullName = `${entityName}.${fnName}`;
if (!isOperation(operation)) {
warnApi(
Expand All @@ -51,6 +51,12 @@ const checkApiDeclaration = (logger, entityConfigs, config, entityName, entity)
);
}

if (typeof noDedup !== 'boolean') {
warnApi(
`${fullName}'s noDedup needs to be a boolean, was ${typeof noDedup}'`
);
}

if (typeof idFrom !== 'function' && !isIdFromString(idFrom)) {
warnApi(
`${fullName} defines illegal idFrom. Use 'ENTITY', 'ARGS', or a function (Entity => id)`
Expand All @@ -70,7 +76,7 @@ const checkApiDeclaration = (logger, entityConfigs, config, entityName, entity)
)(entity.api);
};

const checkViewOf = (logger, entityConfigs, config, entityName, entity) => {
const checkViewOf = (logger, entityConfigs, entityName, entity) => {
const { viewOf } = entity;
if (viewOf && !isConfigured(viewOf, entityConfigs)) {
warn(
Expand All @@ -81,7 +87,7 @@ const checkViewOf = (logger, entityConfigs, config, entityName, entity) => {
}
};

const checkInvalidations = (logger, entityConfigs, config, entityName, entity) => {
const checkInvalidations = (logger, entityConfigs, entityName, entity) => {
const { invalidates, invalidatesOn } = entity;
map_((entityToInvalidate) => {
if (!isConfigured(entityToInvalidate, entityConfigs)) {
Expand All @@ -104,7 +110,7 @@ const checkInvalidations = (logger, entityConfigs, config, entityName, entity) =
}, invalidatesOn);
};

const checkTTL = (logger, entityConfigs, config, entityName, entity) => {
const checkTTL = (logger, entityConfigs, entityName, entity) => {
if (typeof entity.ttl !== 'number') {
warn(
logger,
Expand All @@ -113,22 +119,43 @@ const checkTTL = (logger, entityConfigs, config, entityName, entity) => {
}
};

const checkEntities = (logger, entityConfigs, config) => {
const checkNoDedup = (logger, entityConfigs, entityName, entity) => {
if (typeof entity.noDedup !== 'boolean') {
warn(
logger,
`Entity ${entityName} specified noDedup as ${typeof entity.noDedup}, needs to be a boolean`
);
}
};

const checkEntities = (logger, entityConfigs) => {
const checks = [
checkApiDeclaration,
checkViewOf,
checkInvalidations,
checkTTL
checkTTL,
checkNoDedup
];

compose(
map_(([entityName, entity]) => {
map_((check) => check(logger, entityConfigs, config, entityName, entity), checks);
map_((check) => check(logger, entityConfigs, entityName, entity), checks);
}),
toPairs
)(entityConfigs);
};

const checkGlobalConfig = (logger, config) => {
const { noDedup, idField } = config;
if (typeof noDedup !== 'boolean') {
warn(logger, 'noDedup needs to be a boolean, was string');
}

if (typeof idField !== 'string') {
warn(logger, 'idField needs to be a string, was boolean');
}
};


export const validateConfig = (logger, entityConfigs, config) => {
// do not remove the process.NODE_ENV check here - allows uglifiers
Expand All @@ -137,5 +164,7 @@ export const validateConfig = (logger, entityConfigs, config) => {
return;
}

checkEntities(logger, entityConfigs, config);

checkGlobalConfig(logger, config);
checkEntities(logger, entityConfigs);
};

0 comments on commit 182c5cc

Please sign in to comment.