Skip to content

Commit 901c009

Browse files
authored
feat: use Symbol to hold the method batcher on decorator + simplified README.md usage (#4)
* feat: use Symbol to hold the method batcher on decorator + simplified README.md usage * improved README.md * NPM_TOKEN
1 parent 3f7bea3 commit 901c009

File tree

7 files changed

+80
-87
lines changed

7 files changed

+80
-87
lines changed

.github/workflows/npm-publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,4 @@ jobs:
3131
- run: npm run build
3232
- run: npm publish
3333
env:
34-
NODE_AUTH_TOKEN: ${{secrets.npm_token}}
34+
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}

README.md

Lines changed: 52 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,18 @@ backends and reduce requests to those backends via batching.
99
This library is especially useful for scenarios where you need to perform multiple asynchronous operations efficiently,
1010
such as when making network requests or performing database queries.
1111

12-
Heavily inspired by [graphql/dataloader](https://github.com/graphql/dataloader) but using classes and decorators 😜
12+
Heavily inspired by [graphql/dataloader](https://github.com/graphql/dataloader) but simpler using decorators (😜 really
13+
decoupled). Because of that the
14+
rest of your application doesn't event need to know about the batching/dataloader, it just works!
1315

1416
## Table of Contents
1517

1618
- [Installation](#installation)
1719
- [Usage](#usage)
18-
- [Basic Usage](#basic-usage)
19-
- [Using the `@InBatches` Decorator](#using-the-inbatches-decorator)
20+
- [Basic usage with `@InBatches` Decorator](#basic-usage-with-inbatches-decorator)
21+
- [Advanced usage with custom `Batcher` class](#advanced-usage-with-custom-batcher-class)
2022
- [API](#api)
2123
- [`BatcherOptions`](#batcheroptions)
22-
- [`Batcher<K, V>` Class](#batcherk-v-class)
23-
- [`InBatches<K, V>` Decorator](#inbatches-decorator)
2424
- [Contributing](#contributing)
2525
- [License](#license)
2626

@@ -30,29 +30,45 @@ Heavily inspired by [graphql/dataloader](https://github.com/graphql/dataloader)
3030
npm install inbatches
3131
```
3232

33+
or
34+
35+
```bash
36+
yarn add inbatches
37+
```
38+
3339
## Usage
3440

35-
### Using the `Batcher` Class
41+
### Basic usage with `@InBatches` Decorator
42+
43+
The simplest way to get the grown running is to use the `@InBatches` decorator. This decorator will wrap your method
44+
and will batch-enable it, like magic! 🧙‍♂️
3645

3746
```typescript
38-
import { Batcher } from 'inbatches';
47+
import { InBatches } from 'inbatches';
3948

40-
// Define a class that extends Batcher and implements the `run` method
41-
// the `run` method will be called with an array of keys collected from the `enqueue` method
42-
class MyBatcher extends Batcher<number, string> {
43-
async run(ids: number[]): Promise<string[]> {
44-
// Perform asynchronous operations using the keys
45-
// you must return an array of results in the same order as the keys
46-
return this.db.getMany(ids);
49+
class MyService {
50+
51+
// (optional) overloaded method, where you define the keys as `number` and the return type as `User` for typings
52+
async fetch(key: number): Promise<User>;
53+
54+
// This method is now batch-enabled
55+
@InBatches()
56+
async fetch(keys: number | number[]): Promise<User | User[]> {
57+
if (Array.isArray(keys)) return await this.db.getMany(keys);
58+
59+
// in reality the Decorator will wrap this method and it will never be called with a single key :)
60+
throw new Error('It will never be called with a single key 😉');
4761
}
4862
}
63+
```
4964

50-
// Create an instance of your batcher
51-
const batcher = new MyBatcher();
65+
Profit! 🤑
66+
67+
```typescript
68+
const service = new MyService();
5269

53-
// Enqueue keys for batched execution
5470
const result = [1, 2, 3, 4, 5].map(async id => {
55-
return await batcher.enqueue(id);
71+
return await service.fetch(id);
5672
});
5773

5874
// The result will be an array of results in the same order as the keys
@@ -61,34 +77,33 @@ result.then(results => {
6177
});
6278
```
6379

64-
### Using the `@InBatches` Decorator
80+
### Advanced usage with custom `Batcher` class
6581

66-
The library also provides a decorator called `InBatches` that you can use to batch-enable methods of your class.
82+
Another way to use the library is to create a class that extends the `Batcher` class and implement the `run` method.
83+
This class will provide a `enqueue` method that you can use to enqueue keys for batched execution.
6784

6885
```typescript
69-
import { InBatches } from 'inbatches';
70-
71-
class MyService {
72-
73-
// (optional) overloaded method, where you define the keys as `number` and the return type as `string` for typings
74-
async fetch(keys: number): Promise<string>;
75-
76-
// in reality the Decorator will wrap this method and it will never be called with a single key :)
77-
@InBatches() // This method is now batch-enabled
78-
async fetch(keys: number | number[]): Promise<string | string[]> {
79-
if (Array.isArray(keys)) {
80-
return this.db.getMany(keys);
81-
}
86+
import { Batcher } from 'inbatches';
8287

83-
// the Decorator will wrap this method and because of that it will never be called with a single key
84-
throw new Error('It will never be called with a single key 😉');
88+
// The `run` method will be called with an array of keys collected from the `enqueue` method
89+
class MyBatcher extends Batcher<number, string> {
90+
async run(ids: number[]): Promise<string[]> {
91+
// Perform asynchronous operations using the keys
92+
// you must return an array of results in the same order as the keys
93+
return this.db.getMany(ids);
8594
}
8695
}
96+
```
8797

88-
const service = new MyService();
98+
then
8999

100+
```typescript
101+
// Create an instance of your batcher
102+
const batcher = new MyBatcher();
103+
104+
// Enqueue keys for batched execution
90105
const result = [1, 2, 3, 4, 5].map(async id => {
91-
return await service.fetch(id);
106+
return await batcher.enqueue(id);
92107
});
93108

94109
// The result will be an array of results in the same order as the keys
@@ -108,33 +123,6 @@ An interface to specify options for the batcher.
108123
is `undefined` and will use `process.nextTick` to dispatch the batch, which is highly efficient and fast. Only use
109124
this if you really want to accumulate promises calls in a window of time before dispatching the batch.
110125

111-
### `Batcher<K, V>` Class
112-
113-
An abstract class that provides the core functionality for batching and executing asynchronous operations.
114-
115-
- `enqueue(key: K): Promise<V>`: Enqueues a key for batching and returns a promise that resolves to the result when
116-
available.
117-
118-
### `InBatches` Decorator
119-
120-
A decorator function that can be applied to methods to enable batching.
121-
122-
- Usage: `@InBatches(options?: BatcherOptions)`
123-
- Example:
124-
125-
```typescript
126-
class MyService {
127-
128-
// (optional) overloaded method, where you define the keys as `number` and the return type as `string` for typings
129-
async fetchResults(keys: number): Promise<string>
130-
131-
@InBatches({ maxBatchSize: 10 })
132-
async fetchResults(keys: number | number[]): Promise<string | string[]> {
133-
// Batch-enabled method logic
134-
}
135-
}
136-
```
137-
138126
## Contributing
139127

140128
Contributions are welcome! Feel free to open issues or submit pull requests on

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "inbatches",
3-
"version": "0.0.7",
3+
"version": "0.0.8",
44
"private": false,
55
"license": "MIT",
66
"repository": {

src/batcher.spec.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Batcher, BatcherOptions } from './batcher';
22

3-
class BatcherSpec extends Batcher<string, string> {
3+
class RunInBatches extends Batcher<string, string> {
44
constructor(options?: BatcherOptions) {
55
super(options);
66
}
@@ -16,7 +16,7 @@ class BatcherSpec extends Batcher<string, string> {
1616

1717
describe('Batcher', () => {
1818
it('should call run in batch', async () => {
19-
const batcher = new BatcherSpec();
19+
const batcher = new RunInBatches();
2020

2121
const promises = ['a', 'b', 'c'].map(key => {
2222
return batcher.enqueue(key);
@@ -27,7 +27,7 @@ describe('Batcher', () => {
2727
});
2828

2929
it('should call run in batch with max size', async () => {
30-
const batcher = new BatcherSpec({ maxBatchSize: 2 });
30+
const batcher = new RunInBatches({ maxBatchSize: 2 });
3131

3232
const promises = ['batch1.1', 'batch1.2', 'batch2.1', 'batch2.2'].map(key => {
3333
return batcher.enqueue(key);
@@ -38,7 +38,7 @@ describe('Batcher', () => {
3838
});
3939

4040
it('should call run method with unique keys if duplicates', async () => {
41-
const batcher = new BatcherSpec();
41+
const batcher = new RunInBatches();
4242

4343
const promises = ['a', 'b', 'a', 'c'].map(key => {
4444
return batcher.enqueue(key);
@@ -49,7 +49,7 @@ describe('Batcher', () => {
4949
});
5050

5151
it('should reject all with same error when run method failed', async () => {
52-
const batcher = new BatcherSpec();
52+
const batcher = new RunInBatches();
5353

5454
const promises = ['a', 'throw', 'c'].map(key => {
5555
return batcher.enqueue(key);
@@ -63,7 +63,7 @@ describe('Batcher', () => {
6363
});
6464

6565
it('should reject single with returned error when returning error', async () => {
66-
const batcher = new BatcherSpec();
66+
const batcher = new RunInBatches();
6767

6868
const promises = ['a', 'error', 'c'].map(key => {
6969
return batcher.enqueue(key);
@@ -76,7 +76,7 @@ describe('Batcher', () => {
7676
});
7777

7878
it('should call in batches with delay', cb => {
79-
const batcher = new BatcherSpec({ delayWindowInMs: 100 });
79+
const batcher = new RunInBatches({ delayWindowInMs: 100 });
8080

8181
const promises = ['a', 'b', 'c'].map(key => {
8282
return batcher.enqueue(key);

src/batcher.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,19 @@ interface Callback<K> {
1111

1212
class Batch<K, V> {
1313
public active = true;
14-
public readonly cache = new Map<K, Promise<V>>();
14+
public readonly uniques = new Map<K, Promise<V>>();
1515
public readonly callbacks: Callback<K>[] = [];
1616

1717
append(key: K) {
18-
if (this.cache.has(key)) {
19-
return this.cache.get(key);
18+
if (this.uniques.has(key)) {
19+
return this.uniques.get(key);
2020
}
2121

2222
const promise = new Promise<V>((resolve, reject) => {
2323
this.callbacks.push({ key, resolve, reject });
2424
});
2525

26-
this.cache.set(key, promise);
26+
this.uniques.set(key, promise);
2727
return promise;
2828
}
2929
}

src/decorator.spec.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ class RunInBatches {
44
constructor(private id: string = '') {
55
}
66

7-
87
async getAll(keys: string): Promise<string>;
98

109
@InBatches()

src/decorator.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,28 @@ class MethodBatcher<I, K, V> extends Batcher<K, V> {
1616
}
1717
}
1818

19-
function getInstanceBatcher<K, V>(self: any, property: string, fn: Method<K, V>, options?: BatcherOptions) {
20-
const bkey = `${property}_____batcher`;
19+
// this Symbol is used to store the MethodBatcher instances in the instance of the class that is using the decorator
20+
// this way we can have a unique batcher for each instance and method of the class decorated with @InBatches
21+
const holder = Symbol('__inbatches__');
2122

22-
// check if the instance already has a batcher for this method
23-
if (self[bkey]) return self[bkey];
23+
function getInstanceBatcher<I, K, V>(instance: I, property: string, descriptor: Method<K, V>, options?: BatcherOptions) {
24+
// check if the instance already has a holder for all the batchers in the class
25+
instance[holder] = instance[holder] ?? new Map<string, MethodBatcher<I, K, V>>();
2426

25-
// otherwise, create a new batcher and store it in the instance so it is unique for that instance
26-
self[bkey] = new MethodBatcher(self, fn, options);
27-
return self[bkey];
27+
// check if the instance already has a method matcher for this specific method
28+
if (instance[holder].has(property)) return instance[holder].get(property);
29+
30+
// otherwise, create a new batcher and store it in the instance batchers holder
31+
const batcher = new MethodBatcher<I, K, V>(instance, descriptor, options);
32+
instance[holder].set(property, batcher);
33+
return batcher;
2834
}
2935

3036
export function InBatches<K, V>(options?: BatcherOptions) {
3137
return function (_: any, property: string, descriptor: PropertyDescriptor) {
32-
const fn = descriptor.value;
38+
const method = descriptor.value;
3339
descriptor.value = function (...args: any[]) {
34-
const batcher = getInstanceBatcher<K, V>(this, property, fn, options);
40+
const batcher = getInstanceBatcher<any, K, V>(this, property, method, options);
3541
return batcher.enqueue(args);
3642
};
3743

0 commit comments

Comments
 (0)