Skip to content

Commit f89b0a8

Browse files
committed
✨ Add SlidingTaskMap
1 parent b65526b commit f89b0a8

File tree

4 files changed

+123
-1
lines changed

4 files changed

+123
-1
lines changed

README.md

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ multiple requests to do time-consuming operations simultaneously.
88
With this approach, one request will take care of creating the final data and other processes
99
will asynchronously wait for the completion.
1010

11-
**NOTE:** This library is useful if you need a lihtweight solution without an extra service like Redis / PubSub etc.
11+
**NOTE:** This library is useful if you need a lightweight solution without an extra service like Redis / PubSub etc.
1212

1313
Does this library help you? Please give it a ⭐️!
1414

@@ -17,6 +17,7 @@ Does this library help you? Please give it a ⭐️!
1717
- Later resolving of a task in a `Promise` way
1818
- Auto rejecting promises removed from the shared data structure (`TaskMap`, which is an extension of a `Map` data structure)
1919
- Zero dependencies
20+
- Map with limited space (sliding-window)
2021

2122
## 🚀 Installation
2223

@@ -108,3 +109,29 @@ app.get('/observations/:date', async function (req, res) {
108109
downloadDataFromS3(date).pipe(res)
109110
})
110111
```
112+
113+
**Sliding window**
114+
115+
When your map size reaches a specified threshold, the oldest values will be
116+
removed. You can be then sure that the size of the map will never overflow your memory.
117+
118+
```typescript
119+
import { Task, SlidingTaskMap } from 'promise-based-task'
120+
121+
const WINDOW_SIZE = 10
122+
const tasks = new SlidingTaskMap<string, number[]>(WINDOW_SIZE)
123+
124+
app.get('/calculation/:date', async function () {
125+
const date = req.params.date
126+
127+
if (!tasks.has(date)) {
128+
const task = new Task<void>()
129+
tasks.set(date, task)
130+
131+
const data = await fetchData(date)
132+
task.resolve(data)
133+
}
134+
135+
return tasks.get(date)
136+
})
137+
```

src/SlidingTaskMap.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { Deletable } from './types';
2+
import TaskMap from './TaskMap';
3+
4+
class SlidingTaskMap<K, V extends Deletable> extends TaskMap<K, V> {
5+
private keysByTime: K[] = [];
6+
7+
constructor(private readonly windowSize: number) {
8+
super();
9+
if (windowSize < 1 || isNaN(Number(windowSize))) {
10+
throw new TypeError(`windowSize cannot be less than 1!`);
11+
}
12+
}
13+
14+
set(key: K, value: V): this {
15+
if (this.has(key)) {
16+
return super.set(key, value);
17+
}
18+
19+
if (this.size + 1 > this.windowSize) {
20+
this.delete(this.keysByTime[0]);
21+
}
22+
this.keysByTime.push(key);
23+
return super.set(key, value);
24+
}
25+
26+
delete(key: K): boolean {
27+
const didDelete = super.delete(key);
28+
if (didDelete) {
29+
const deleteIndex = this.keysByTime.indexOf(key);
30+
this.keysByTime.splice(deleteIndex, 1);
31+
}
32+
return didDelete;
33+
}
34+
35+
clear() {
36+
super.clear();
37+
this.keysByTime.length = 0;
38+
}
39+
}
40+
41+
export default SlidingTaskMap;

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export { default as Task } from './Task';
22
export { default as TaskMap } from './TaskMap';
3+
export { default as SlidingTaskMap } from './SlidingTaskMap';
34
export * from './types';
45
export * from './error';

test/SlidingTaskMap.test.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { Task, SlidingTaskMap } from '../src';
2+
3+
describe('SlidingTaskMap', () => {
4+
it('Counts', async () => {
5+
const map = new SlidingTaskMap<string, Task<number>>(5);
6+
7+
expect(map.size).toBe(0);
8+
map.set('1', new Task<number>(1));
9+
expect(map.size).toBe(1);
10+
map.delete('1');
11+
expect(map.size).toBe(0);
12+
map.set('2', new Task<number>(2));
13+
expect(map.size).toBe(1);
14+
map.set('2', new Task<number>(2));
15+
expect(map.size).toBe(1);
16+
map.clear();
17+
expect(map.size).toBe(0);
18+
});
19+
20+
it('Slides', async () => {
21+
const WINDOW_SIZE = 5;
22+
const map = new SlidingTaskMap<string, Task<string>>(WINDOW_SIZE);
23+
24+
const keys = Array.from({ length: 50 }).map((_, i) => String(i + 1));
25+
for (const key of keys) {
26+
const prevSize = Number(map.size);
27+
28+
const task = new Task<string>(key);
29+
map.set(key, task);
30+
31+
expect(map.has(key)).toBe(true);
32+
expect(map.get(key)).toEqual(task);
33+
expect(prevSize).toBeLessThanOrEqual(map.size);
34+
expect(map.size).toBeLessThanOrEqual(WINDOW_SIZE);
35+
}
36+
37+
const resolvedValues = await Promise.all(Array.from(map.values()));
38+
expect(resolvedValues).toEqual(keys.slice(-WINDOW_SIZE));
39+
});
40+
41+
it('Throws with invalid window size', async () => {
42+
expect(() => new SlidingTaskMap(0)).toThrow();
43+
44+
// @ts-expect-error simulating no type safety env
45+
expect(() => new SlidingTaskMap()).toThrow();
46+
47+
// @ts-expect-error simulating no type safety env
48+
expect(() => new SlidingTaskMap([])).toThrow();
49+
50+
// @ts-expect-error simulating no type safety env
51+
expect(() => new SlidingTaskMap('x')).toThrow();
52+
});
53+
});

0 commit comments

Comments
 (0)