Skip to content

Commit bf709a7

Browse files
committed
feat: get rid of dependencies
1 parent fc9fe4b commit bf709a7

12 files changed

+293
-308
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# projected
22

3-
Collections of objects that rely on remote data sources. Hides the complexity of fetching and caching data from a remote source regardless of the number of consumers.
3+
Collections of utilities. Hides the complexity of fetching and caching data from a remote source regardless of the number of consumers.
44

55
## Installation
66

package-lock.json

-13
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

-4
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
"eslint-plugin-import": "^2.31.0",
3232
"eslint-plugin-prettier": "^5.2.1",
3333
"npm-run-all": "^4.1.5",
34-
"rxjs": "^7.8.1",
3534
"semantic-release": "^24.2.0",
3635
"typescript": "^5.7.2",
3736
"typescript-eslint": "^8.18.0",
@@ -52,8 +51,5 @@
5251
"publishConfig": {
5352
"access": "public",
5453
"registry": "https://registry.npmjs.org"
55-
},
56-
"peerDependencies": {
57-
"rxjs": "^7.8.1"
5854
}
5955
}

src/projected-lazy-map/buffered-request.ts

-87
This file was deleted.

src/projected-lazy-map/dispatcher.ts

+151
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import type { MaybePromise } from '../types/maybe-promise.js';
2+
import type { Maybe } from '../types/maybe.js';
3+
import { Deferred } from '../utils/deferred.js';
4+
5+
export type ResolverOptions<K, V> = {
6+
/**
7+
* Function that returns key of an entity
8+
* @param item Entity
9+
* @returns Key of the entity
10+
*/
11+
key: (item: V) => K;
12+
13+
/**
14+
* Function that fetches multiple entities by keys
15+
* @param keys Array of keys
16+
* @returns Promise that resolves to an array of entities
17+
*/
18+
values: (keys: K[]) => MaybePromise<Maybe<V>[]>;
19+
20+
/**
21+
* Delay in ms that is used to buffer requests
22+
* @default 50
23+
*/
24+
delay?: number;
25+
26+
/**
27+
* Maximum number of keys that can be buffered
28+
* @default 1000
29+
*/
30+
maxChunkSize?: number;
31+
};
32+
33+
class ConsumerMap<K, V> extends Map<K, ConsumerValue<V>> {}
34+
type ConsumerValue<V> = { value: Maybe<V> } | { error: any };
35+
type Consumer<K, V> = (values: ConsumerMap<K, Maybe<V>>) => void;
36+
37+
/**
38+
* Utility class that helps to reduce the number of requests to the backend.
39+
*/
40+
export class Resolver<K, V> {
41+
private readonly values: (keys: K[]) => MaybePromise<Maybe<V>[]>;
42+
private readonly key: (item: V) => K;
43+
private readonly delay: number;
44+
private readonly maxChunkSize: number;
45+
46+
private readonly queue = new Set<K>();
47+
48+
private timeout: any | null = null;
49+
50+
private consumers = new Set<Consumer<K, V>>();
51+
52+
constructor(options: ResolverOptions<K, V>) {
53+
const { values, key, delay, maxChunkSize } = options;
54+
55+
this.values = values;
56+
this.key = key;
57+
this.delay = delay ?? 50;
58+
this.maxChunkSize = maxChunkSize ?? 1000;
59+
}
60+
61+
async resolve(keys: K[]): Promise<Map<K, Maybe<V>>> {
62+
const deferred = new Deferred<Map<K, Maybe<V>>>();
63+
64+
const pending = new Set(keys);
65+
const resolved = new Map<K, Maybe<V>>();
66+
67+
const consume: Consumer<K, V> = (results) => {
68+
results.forEach((value, key) => {
69+
if ('error' in value) {
70+
this.consumers.delete(consume);
71+
deferred.reject(value.error);
72+
return;
73+
}
74+
75+
resolved.set(key, value.value);
76+
pending.delete(key);
77+
});
78+
79+
if (!pending.size) {
80+
this.consumers.delete(consume);
81+
deferred.resolve(resolved);
82+
}
83+
};
84+
85+
this.consumers.add(consume);
86+
this.enqueue(keys);
87+
this.schedule();
88+
89+
return deferred.promise;
90+
}
91+
92+
private async enqueue(keys: K[]) {
93+
// if present, the key will remain on the same position in the queue even if it is added multiple times
94+
keys.forEach((key) => this.queue.add(key));
95+
}
96+
97+
private async schedule() {
98+
// if the buffer is full, dispatch immediately
99+
while (this.queue.size > this.maxChunkSize) {
100+
this.clearTimer();
101+
await this.dispatch();
102+
}
103+
104+
// already scheduled
105+
if (this.timeout) {
106+
return;
107+
}
108+
109+
this.timeout = setTimeout(async () => {
110+
this.clearTimer();
111+
await this.dispatch();
112+
}, this.delay);
113+
}
114+
115+
private async dispatch() {
116+
// get relevant chunk of keys from the queue
117+
const keys = [...this.queue].slice(0, this.maxChunkSize);
118+
119+
if (!keys.length) {
120+
return;
121+
}
122+
123+
// remove dispatched keys from the queue
124+
keys.forEach((key) => this.queue.delete(key));
125+
126+
const results = new ConsumerMap<K, V>(keys.map((key) => [key, { value: undefined }]));
127+
128+
try {
129+
const values = await this.values(keys);
130+
131+
values.forEach((value) => {
132+
if (value) {
133+
const key = this.key(value);
134+
135+
results.set(key, { value });
136+
}
137+
});
138+
} catch (error) {
139+
results.forEach((_, key) => results.set(key, { error }));
140+
}
141+
142+
this.consumers.forEach((consume) => consume(results));
143+
}
144+
145+
private clearTimer() {
146+
if (this.timeout) {
147+
clearTimeout(this.timeout);
148+
this.timeout = null;
149+
}
150+
}
151+
}

0 commit comments

Comments
 (0)