Skip to content

Commit b515fe6

Browse files
committed
refactor: 💡 Improved NodeRequestManager
See related PR for more dateails
1 parent 26f354b commit b515fe6

File tree

1 file changed

+158
-46
lines changed

1 file changed

+158
-46
lines changed

‎src/node/requestManager.ts

Lines changed: 158 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,35 @@ import { NoncePeriodOption } from '../shared/options.js';
44
import { isExpired, nowSec, parseSeconds } from '../utils/time.js';
55
import { createLogger } from '../utils/logger.js';
66

7-
const logger = createLogger('RequestManager');
7+
const logger = createLogger('NodeRequestManager');
88

99
/**
10-
* Request manager (of the protocol node) initialization options type
10+
* Type for initialization options of the request manager in the protocol node.
1111
*/
1212
export type RequestManagerOptions = NoncePeriodOption;
1313

14+
/**
15+
* Type for custom event for request
16+
*/
1417
export interface RequestEvent<CustomRequestQuery extends GenericQuery> {
1518
topic: string;
1619
data: RequestData<CustomRequestQuery>;
1720
}
1821

19-
export interface RequestManagerEvents<CustomRequestQuery extends GenericQuery> {
22+
/**
23+
* Type of request item in the cache
24+
*/
25+
interface RequestCacheItem<CustomRequestQuery extends GenericQuery> {
26+
topic: string;
27+
data: RequestData<CustomRequestQuery>;
28+
}
29+
30+
/**
31+
* NodeRequestManager events interface
32+
*/
33+
export interface NodeRequestManagerEvents<
34+
CustomRequestQuery extends GenericQuery,
35+
> {
2036
/**
2137
* @example
2238
*
@@ -27,70 +43,166 @@ export interface RequestManagerEvents<CustomRequestQuery extends GenericQuery> {
2743
* ```
2844
*/
2945
request: CustomEvent<RequestEvent<CustomRequestQuery>>;
46+
47+
/**
48+
* @example
49+
*
50+
* ```js
51+
* request.addEventListener('request', () => {
52+
* // ... request is ready
53+
* })
54+
* ```
55+
*/
56+
error: CustomEvent<Error>;
3057
}
3158

32-
export class RequestManager<CustomRequestQuery extends GenericQuery> extends EventEmitter<
33-
RequestManagerEvents<CustomRequestQuery>
34-
> {
59+
/**
60+
* Class for managing requests in a node
61+
*
62+
* @export
63+
* @class NodeRequestManager
64+
* @extends {EventEmitter<NodeRequestManagerEvents<CustomRequestQuery>>}
65+
* @template CustomRequestQuery
66+
*/
67+
export class NodeRequestManager<
68+
CustomRequestQuery extends GenericQuery = GenericQuery,
69+
> extends EventEmitter<NodeRequestManagerEvents<CustomRequestQuery>> {
70+
/** The period of time the manager waits for messages with a higher nonce. */
3571
private noncePeriod: number;
36-
private cache: Map<string, RequestData<CustomRequestQuery>>;
37-
private cacheTopic: Map<string, string>;
72+
/** In-memory cache for messages. */
73+
private cache: Map<string, RequestCacheItem<CustomRequestQuery>>;
3874

75+
/**
76+
* Creates an instance of NodeRequestManager.
77+
* @param {RequestManagerOptions} options
78+
* @memberof NodeRequestManager
79+
*/
3980
constructor(options: RequestManagerOptions) {
4081
super();
4182

4283
const { noncePeriod } = options;
4384

4485
// @todo Validate RequestManagerOptions
4586

46-
this.cache = new Map<string, RequestData<CustomRequestQuery>>(); // requestId => request
47-
this.cacheTopic = new Map<string, string>(); // requestId => topic
87+
// requestId => RequestCacheItem
88+
this.cache = new Map<string, RequestCacheItem<CustomRequestQuery>>();
89+
90+
this.noncePeriod = Number(parseSeconds(noncePeriod));
91+
}
92+
93+
/**
94+
* Sets a new value of the `noncePeriod`
95+
*
96+
* @param {(number | string)} noncePeriod
97+
* @memberof NodeRequestManager
98+
*/
99+
setNoncePeriod(noncePeriod: number | string) {
48100
this.noncePeriod = Number(parseSeconds(noncePeriod));
49101
}
50102

51-
add(topic: string, data: string) {
52-
const requestData = JSON.parse(data) as RequestData<CustomRequestQuery>;
103+
/**
104+
* Clears the requests cache
105+
*
106+
* @memberof NodeRequestManager
107+
*/
108+
clear() {
109+
this.cache.clear();
110+
}
53111

54-
if (isExpired(requestData.expire)) {
55-
logger.trace(`Request #${requestData.id} is expired`);
56-
return;
112+
/**
113+
* Deletes expired requests from the cache
114+
*
115+
* @memberof NodeRequestManager
116+
*/
117+
prune() {
118+
const now = Math.ceil(Date.now() / 1000);
119+
for (const [id, record] of this.cache.entries()) {
120+
try {
121+
if (record.data.expire < now) {
122+
this.cache.delete(id);
123+
}
124+
} catch (error) {
125+
logger.error('Cache prune error', error);
126+
}
57127
}
128+
}
58129

59-
if (BigInt(nowSec() + this.noncePeriod) > BigInt(requestData.expire)) {
60-
logger.trace(`Request #${requestData.id} will expire before it can bee processed`);
61-
return;
62-
}
130+
/**
131+
* Adds a request to cache
132+
*
133+
* @param {string} requestTopic
134+
* @param {string} data
135+
* @memberof NodeRequestManager
136+
*/
137+
add(requestTopic: string, data: string) {
138+
try {
139+
const requestData = JSON.parse(data) as RequestData<CustomRequestQuery>;
140+
141+
// TODO: Implement validation of `data` type and `requestTopic`
142+
143+
// Check if request is expired
144+
if (isExpired(requestData.expire)) {
145+
logger.trace(`Request #${requestData.id} is expired`);
146+
return;
147+
}
148+
149+
// Check if request will expire before it can be processed
150+
if (BigInt(nowSec() + this.noncePeriod) > BigInt(requestData.expire)) {
151+
logger.trace(
152+
`Request #${requestData.id} will expire before it can bee processed`,
153+
);
154+
return;
155+
}
156+
157+
// If request is new, add to cache and set a timeout to dispatch event
158+
if (!this.cache.has(requestData.id)) {
159+
// New request
160+
this.cache.set(requestData.id, {
161+
data: requestData,
162+
topic: requestTopic,
163+
});
63164

64-
if (!this.cache.has(requestData.id)) {
65-
// New request
66-
this.cache.set(requestData.id, requestData);
67-
this.cacheTopic.set(requestData.id, topic);
68-
setTimeout(() => {
69-
try {
70-
this.dispatchEvent(
71-
new CustomEvent('request', {
72-
detail: {
73-
topic,
74-
data: requestData,
75-
},
76-
}),
77-
);
78-
79-
if (!this.cache.delete(requestData.id) || !this.cacheTopic.delete(requestData.id)) {
80-
throw new Error(`Unable to remove request #${requestData.id} from cache`);
165+
// Wait until the nonce period ends
166+
setTimeout(() => {
167+
try {
168+
const cacheItem = this.cache.get(requestData.id);
169+
170+
if (cacheItem) {
171+
this.dispatchEvent(
172+
new CustomEvent('request', {
173+
detail: {
174+
topic: cacheItem.topic,
175+
data: cacheItem.data,
176+
},
177+
}),
178+
);
179+
180+
if (!this.cache.delete(requestData.id)) {
181+
throw new Error(
182+
`Unable to remove request #${requestData.id} from cache`,
183+
);
184+
}
185+
}
186+
} catch (error) {
187+
logger.error(error);
81188
}
82-
} catch (error) {
83-
logger.error(error);
189+
}, this.noncePeriod * 1000);
190+
} else {
191+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
192+
const { topic, data } = this.cache.get(requestData.id)!;
193+
194+
// If request is known, only update if nonce is higher and the same topic
195+
if (requestTopic === topic && requestData.nonce > data.nonce) {
196+
this.cache.set(requestData.id, { data: requestData, topic });
84197
}
85-
}, this.noncePeriod * 1000);
86-
} else {
87-
// Known request
88-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
89-
const knownRequest = this.cache.get(requestData.id)!;
90-
91-
if (knownRequest.nonce < requestData.nonce) {
92-
this.cache.set(requestData.id, requestData);
93198
}
199+
} catch (error) {
200+
logger.error(error);
201+
this.dispatchEvent(
202+
new CustomEvent('error', {
203+
detail: new Error('Unable to add request to cache due to error'),
204+
}),
205+
);
94206
}
95207
}
96208
}

0 commit comments

Comments
 (0)