batchloader
Advanced tools
Comparing version
@@ -13,4 +13,5 @@ import { IBatchLoader } from './types'; | ||
loadMany(keys: Key[]): Promise<Value[]>; | ||
createMapppedLoader<MappedValue>(mapFn: (value: Value) => MappedValue, batchDelay?: number): BatchLoader<Key, MappedValue>; | ||
protected triggerBatch(): Promise<Value[]>; | ||
protected runBatchNow(): Promise<Value[]>; | ||
} |
@@ -35,2 +35,16 @@ "use strict"; | ||
} | ||
createMapppedLoader(mapFn, batchDelay = this.batchDelay) { | ||
return new BatchLoader((keys) => __awaiter(this, void 0, void 0, function* () { | ||
const values = yield this.batchFn(keys); | ||
const mapped = values.map(mapFn); | ||
const len = mapped.length; | ||
for (let i = 0; i < len; i += 1) { | ||
const item = mapped[i]; | ||
if (item != null && typeof item.then === 'function') { | ||
return Promise.all(mapped); | ||
} | ||
} | ||
return mapped; | ||
}), this.keyToUniqueId, batchDelay); | ||
} | ||
triggerBatch() { | ||
@@ -37,0 +51,0 @@ return (this.batchPromise || |
@@ -18,2 +18,7 @@ "use strict"; | ||
}; | ||
if (typeof process !== 'undefined' && | ||
process.env && | ||
process.env.NODE_ENV !== 'production') { | ||
console.log('Deprecated! Use BatchLoader.createMapppedLoader instead!'); | ||
} | ||
} | ||
@@ -20,0 +25,0 @@ load(key) { |
{ | ||
"name": "batchloader", | ||
"version": "0.0.1", | ||
"version": "0.0.2", | ||
"description": "BatchLoader is a utility for data fetching layer to reduce requests via batching written in TypeScript. Inspired by Facebook's DataLoader", | ||
@@ -17,2 +17,3 @@ "main": "lib/index.js", | ||
"test:coverage": "jest --coverage", | ||
"test:coverage:report": "jest --coverage && cat ./coverage/lcov.info | coveralls", | ||
"test:watch": "jest --watch" | ||
@@ -39,2 +40,3 @@ }, | ||
"@types/node": "^10.11.4", | ||
"coveralls": "^3.0.2", | ||
"jest": "^23.5.0", | ||
@@ -41,0 +43,0 @@ "prettier": "^1.14.2", |
147
README.md
@@ -1,2 +0,145 @@ | ||
# batchloader | ||
DataLoader is a utility for data fetching layer to reduce requests via batching written in TypeScript. Inspired by Facebook's DataLoader | ||
# BatchLoader | ||
BatchLoader is a batching utility for data fetching layer to reduce requests round trips, inspired by [Facebook's DataLoader](https://github.com/facebook/dataloader), written in [TypeScript](https://www.typescriptlang.org/index.html). | ||
BatchLoader is a simplified version of Facebook's DataLoader and can be used with any database such as MongoDB or with GraphQL. | ||
[](https://travis-ci.org/joonhocho/batchloader) | ||
[](https://coveralls.io/github/joonhocho/batchloader?branch=master) | ||
[](https://badge.fury.io/js/batchloader) | ||
[](https://david-dm.org/joonhocho/batchloader) | ||
[](http://doge.mit-license.org) | ||
## Comparison to DataLoader | ||
\+ written in TypeScript | ||
\+ Further reduces data fetching requests by filtering out duplicate keys | ||
\+ Similar api as DataLoader | ||
\+ Smaller in size (0 dependencies) | ||
\+ MappedBatchLoader can be used to compose a new loader using existing loaders. | ||
\- Removed caching functionalities. Leave caching to better cache libraries. | ||
It is a very simple batcher that only does batching, and it does it very well. | ||
## Getting Started | ||
First, install BatchLoader using npm. | ||
```sh | ||
npm install --save batchloader | ||
``` | ||
or with Yarn, | ||
```sh | ||
yarn add batchloader | ||
``` | ||
> Note: BatchLoader assumes a JavaScript environment with global ES6 `Promise`, available in all supported versions of Node.js. | ||
## Batching | ||
Create loaders by providing a batch loading function and key transformation function (used for finding duplicate keys). | ||
```typescript | ||
import { BatchLoader } from 'batchloader'; | ||
const userLoader = new BatchLoader( | ||
(_ids: ObjectId[]) => User.getByIds(_ids), // [required] batch function. | ||
(_id: ObjectId) => _id.toString(), // [optional] key to unique id function. must return string. used for finding duplicate keys. | ||
100 // [optional = 0] batch delay in ms. default 0 ms. | ||
); | ||
const user1 = await userLoader.load(id1); | ||
const [user1, user2] = await userLoader.loadMany([id1, id2]); | ||
const [user1, user1, user1] = await userLoader.loadMany([id1, id1, id1]); // batch function receives only one id1 since duplicate ids. Still returs three items just as requested. | ||
const [user1, user2, user3, user2, user1] = await Promise.all([ | ||
userLoader.load(id1), | ||
userLoader.load(id2), | ||
userLoader.load(id3), | ||
userLoader.load(id2), | ||
userLoader.load(id1), | ||
]); // batch function receives [id1, id2, id3] only without duplicate ids. | ||
``` | ||
#### Batch Function | ||
A batch loading function must be of the following type: | ||
```typescript | ||
(keys: Key[]) => Value[] | Promise<Value[]> // keys.length === values.length | ||
``` | ||
Constraints | ||
- keys.length === values.length | ||
- keys[i] => values[i] | ||
- keys.length > 0 | ||
#### KeyToUniqueId Function | ||
A function must return string value given a key: | ||
```typescript | ||
(key: Key) => string | ||
``` | ||
If key is not uniquely identifiable, simply pass `null` instead. This will disable filtering out duplicate keys, and still work the same way. | ||
```typescript | ||
const loader = new BatchLoader( | ||
(keys: Key[]) => loadValues(keys), | ||
null // keys cannot be transformed into string. no duplicates filtering. | ||
); | ||
const v1 = await loader.load(k1); | ||
const [v1, v2, v1] = await loader.loadMany([k1, k2, k1]); // batch function receives [k1, k2, k1] as keys | ||
``` | ||
## MappedBatchLoader | ||
You can map a loader to create another loader. | ||
```typescript | ||
import { MappedBatchLoader } from 'batchloader'; | ||
const usernameLoader = new MappedBatchLoader( | ||
userLoader, // previously defined loader | ||
(user) => user && user.username // mapping function | ||
); | ||
// same APIs as BatchLoader | ||
const username = await usernameLoader.load(userId); | ||
const [username1, username2] = await usernameLoader.loadMany([userId1, userId2]); | ||
const [user1, username1] = await Promise.all([ | ||
userLoader.load(id1), | ||
usernameLoader.load(id1), | ||
]) // one round-trip request with keys being [id1], since usernameLoader is using userLoader internally and id1 is duplicate. | ||
const anotherMappedLoader = new MappedBatchLoader( | ||
usernameLoader, // MappedBatchLoader can be mapped, too. | ||
... | ||
); | ||
``` | ||
## Caching | ||
Unlike DataLoader, BatchLoader does not do any caching. | ||
This is intentional, because you may want to use your favorite cache library that is best suited for your own use case. | ||
You can add caching ability easily like so: | ||
```typescript | ||
let userCache = {}; | ||
const cachedUserLoader = new BatchLoader( | ||
async (ids) => { | ||
const uncachedIds = ids.filter(id => !userCache[id]); | ||
const users = await getUsersByIds(uncachedIds); | ||
uncachedIds.forEach((id, i) => { userCache[id] = users[i]; }); | ||
return ids.map(id => userCache[id]); | ||
}, | ||
... | ||
); | ||
delete userCache[id1]; // delete cache by key | ||
userCache[id2] = user2; // add cache by key | ||
userCache = {}; // empty cache | ||
``` | ||
Choose whatever caching library you like and simply add it like above. |
@@ -98,2 +98,78 @@ import { BatchLoader } from './batchloader'; | ||
}); | ||
test('sync createMapppedLoader', async () => { | ||
const idss = [] as number[][]; | ||
const loader = new BatchLoader( | ||
(ids: number[]): Promise<number[]> => | ||
new Promise( | ||
(resolve): void => { | ||
idss.push(ids); | ||
setTimeout(() => resolve(ids.map((i) => i * 2)), 10); | ||
} | ||
), | ||
String | ||
).createMapppedLoader(String); | ||
expect(await loader.load(3)).toBe('6'); | ||
expect(await loader.load(4)).toBe('8'); | ||
expect(await loader.load(5)).toBe('10'); | ||
expect(await loader.loadMany([])).toEqual([]); | ||
expect(await loader.loadMany([1, 2, 3])).toEqual([2, 4, 6].map(String)); | ||
expect(await loader.loadMany([1, 2, 3, 2, 3, 2, 1])).toEqual( | ||
[2, 4, 6, 4, 6, 4, 2].map(String) | ||
); | ||
expect( | ||
await Promise.all([ | ||
loader.load(1), | ||
loader.load(2), | ||
loader.load(3), | ||
loader.load(2), | ||
loader.load(1), | ||
loader.load(2), | ||
loader.load(3), | ||
]) | ||
).toEqual([2, 4, 6, 4, 2, 4, 6].map(String)); | ||
expect(idss).toEqual([[3], [4], [5], [1, 2, 3], [1, 2, 3], [1, 2, 3]]); | ||
}); | ||
test('async createMapppedLoader', async () => { | ||
const idss = [] as number[][]; | ||
const loader = new BatchLoader( | ||
(ids: number[]): Promise<number[]> => | ||
new Promise( | ||
(resolve): void => { | ||
idss.push(ids); | ||
setTimeout(() => resolve(ids.map((i) => i * 2)), 10); | ||
} | ||
), | ||
String | ||
).createMapppedLoader((x): Promise<string> => Promise.resolve(String(x))); | ||
expect(await loader.load(3)).toBe('6'); | ||
expect(await loader.load(4)).toBe('8'); | ||
expect(await loader.load(5)).toBe('10'); | ||
expect(await loader.loadMany([])).toEqual([]); | ||
expect(await loader.loadMany([1, 2, 3])).toEqual([2, 4, 6].map(String)); | ||
expect(await loader.loadMany([1, 2, 3, 2, 3, 2, 1])).toEqual( | ||
[2, 4, 6, 4, 6, 4, 2].map(String) | ||
); | ||
expect( | ||
await Promise.all([ | ||
loader.load(1), | ||
loader.load(2), | ||
loader.load(3), | ||
loader.load(2), | ||
loader.load(1), | ||
loader.load(2), | ||
loader.load(3), | ||
]) | ||
).toEqual([2, 4, 6, 4, 2, 4, 6].map(String)); | ||
expect(idss).toEqual([[3], [4], [5], [1, 2, 3], [1, 2, 3], [1, 2, 3]]); | ||
}); | ||
}); |
@@ -39,2 +39,25 @@ import { IBatchLoader } from 'src/types'; | ||
public createMapppedLoader<MappedValue>( | ||
mapFn: (value: Value) => MappedValue, | ||
batchDelay = this.batchDelay | ||
): BatchLoader<Key, MappedValue> { | ||
return new BatchLoader( | ||
async (keys: Key[]): Promise<MappedValue[]> => { | ||
const values = await this.batchFn(keys); | ||
const mapped = values.map(mapFn); | ||
const len = mapped.length; | ||
for (let i = 0; i < len; i += 1) { | ||
const item = mapped[i]; | ||
if (item != null && typeof (item as any).then === 'function') { | ||
// has at least one promise | ||
return Promise.all(mapped); | ||
} | ||
} | ||
return mapped as MappedValue[]; | ||
}, | ||
this.keyToUniqueId, | ||
batchDelay | ||
); | ||
} | ||
protected triggerBatch(): Promise<Value[]> { | ||
@@ -41,0 +64,0 @@ return ( |
@@ -9,3 +9,12 @@ import { IBatchLoader } from 'src/types'; | ||
protected mapFn: (v: Value) => MappedValue | Promise<MappedValue> | ||
) {} | ||
) { | ||
if ( | ||
typeof process !== 'undefined' && | ||
process.env && | ||
process.env.NODE_ENV !== 'production' | ||
) { | ||
// tslint:disable-next-line no-console | ||
console.log('Deprecated! Use BatchLoader.createMapppedLoader instead!'); | ||
} | ||
} | ||
@@ -12,0 +21,0 @@ public load(key: Key): Promise<MappedValue> { |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
30255
50.31%22
4.76%523
28.82%146
4766.67%11
10%