Skip to content

Commit 2001059

Browse files
committed
enforce_rl
Signed-off-by: Finbarrs Oketunji <f@finbarrs.eu>
1 parent afe9e13 commit 2001059

File tree

3 files changed

+94
-25
lines changed

3 files changed

+94
-25
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Changelog
22

3+
## 1.0.2 - 2024-09-25 - @0xnu
4+
* Rate limits
5+
36
## 1.0.1 - 2024-09-21 - @0xnu
47
* Clean up
58

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "mot-js-sdk",
3-
"version": "1.0.1",
3+
"version": "1.0.2",
44
"description": "A TypeScript SDK for the MOT History API.",
55
"main": "dist/mot-js-sdk.js",
66
"types": "dist/mot-js-sdk.d.ts",

src/mot-js-sdk.ts

+90-24
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ class MotApiSdk extends EventEmitter {
1717
private token: string | null = null;
1818
private tokenExpiry: number = 0;
1919

20+
// Rate limiting properties
21+
private dailyQuota: number = 500000;
22+
private dailyQuotaReset: number = 0;
23+
private burstLimit: number = 10;
24+
private burstTokens: number = 10;
25+
private rpsLimit: number = 15;
26+
private requestTimestamps: number[] = [];
27+
2028
private static readonly BASE_URL =
2129
"https://history.mot.api.gov.uk/v1/trade/vehicles";
2230
private static readonly TOKEN_URL =
@@ -31,76 +39,134 @@ class MotApiSdk extends EventEmitter {
3139
[404, "Not Found - The requested data is not found"],
3240
[
3341
405,
34-
"Method Not Allowed - The HTTP method is not supported for this endpoint",
42+
"Method Not Allowed - The HTTP method is not supported for this endpoint"
3543
],
3644
[406, "Not Acceptable - The requested media type is not supported"],
3745
[
3846
409,
39-
"Conflict - The request could not be completed due to a conflict with the current state of the target resource",
47+
"Conflict - The request could not be completed due to a conflict with the current state of the target resource"
4048
],
4149
[
4250
412,
43-
"Precondition Failed - Could not complete request because a constraint was not met",
51+
"Precondition Failed - Could not complete request because a constraint was not met"
4452
],
4553
[
4654
415,
47-
"Unsupported Media Type - The media type of the request is not supported",
55+
"Unsupported Media Type - The media type of the request is not supported"
4856
],
4957
[
5058
422,
51-
"Unprocessable Entity - The request was well-formed but contains semantic errors",
59+
"Unprocessable Entity - The request was well-formed but contains semantic errors"
5260
],
5361
[
5462
429,
55-
"Too Many Requests - The user has sent too many requests in a given amount of time",
63+
"Too Many Requests - The user has sent too many requests in a given amount of time"
5664
],
5765
[500, "Internal Server Error - An unexpected error has occurred"],
5866
[
5967
502,
60-
"Bad Gateway - The server received an invalid response from an upstream server",
68+
"Bad Gateway - The server received an invalid response from an upstream server"
6169
],
6270
[
6371
503,
64-
"Service Unavailable - The server is currently unable to handle the request",
72+
"Service Unavailable - The server is currently unable to handle the request"
6573
],
6674
[
6775
504,
68-
"Gateway Timeout - The upstream server failed to send a request in the time allowed by the server",
69-
],
70-
],
76+
"Gateway Timeout - The upstream server failed to send a request in the time allowed by the server"
77+
]
78+
]
7179
);
7280

7381
constructor(
7482
private readonly clientId: string,
7583
private readonly clientSecret: string,
76-
private readonly apiKey: string,
84+
private readonly apiKey: string
7785
) {
7886
super();
7987
this.axiosInstance = axios.create({
8088
baseURL: MotApiSdk.BASE_URL,
8189
headers: {
82-
"X-API-Key": this.apiKey,
83-
},
90+
"X-API-Key": this.apiKey
91+
}
8492
});
8593

8694
this.setupInterceptors();
95+
this.resetDailyQuota();
8796
}
8897

8998
private setupInterceptors(): void {
9099
this.axiosInstance.interceptors.request.use(
91-
async (config) => {
100+
async config => {
101+
await this.waitForRateLimit();
92102
config.headers["Authorization"] = `Bearer ${await this.getToken()}`;
93103
return config;
94104
},
95-
(error) => Promise.reject(error),
105+
error => Promise.reject(error)
96106
);
97107

98108
this.axiosInstance.interceptors.response.use(
99-
(response) => response,
100-
(error) => this.handleApiError(error),
109+
response => response,
110+
error => this.handleApiError(error)
101111
);
102112
}
103113

114+
private async waitForRateLimit(): Promise<void> {
115+
while (!this.checkRateLimits()) {
116+
await new Promise(resolve => setTimeout(resolve, 100));
117+
}
118+
this.updateRateLimits();
119+
}
120+
121+
private checkRateLimits(): boolean {
122+
const now = Date.now();
123+
124+
// Check daily quota
125+
if (this.dailyQuota <= 0 && now < this.dailyQuotaReset) {
126+
return false;
127+
}
128+
129+
// Check burst limit
130+
if (this.burstTokens <= 0) {
131+
return false;
132+
}
133+
134+
// Check RPS limit
135+
const oneSecondAgo = now - 1000;
136+
const requestsLastSecond = this.requestTimestamps.filter(
137+
t => t > oneSecondAgo
138+
).length;
139+
if (requestsLastSecond >= this.rpsLimit) {
140+
return false;
141+
}
142+
143+
return true;
144+
}
145+
146+
private updateRateLimits(): void {
147+
const now = Date.now();
148+
149+
// Update daily quota
150+
if (now >= this.dailyQuotaReset) {
151+
this.resetDailyQuota();
152+
}
153+
this.dailyQuota--;
154+
155+
// Update burst tokens
156+
this.burstTokens = Math.min(this.burstTokens + 1, this.burstLimit);
157+
this.burstTokens--;
158+
159+
// Update RPS tracking
160+
this.requestTimestamps.push(now);
161+
this.requestTimestamps = this.requestTimestamps.filter(t => t > now - 1000);
162+
}
163+
164+
private resetDailyQuota(): void {
165+
const now = Date.now();
166+
this.dailyQuota = 500000;
167+
this.dailyQuotaReset = now + 24 * 60 * 60 * 1000; // Reset after 24 hours
168+
}
169+
104170
private async getToken(): Promise<string> {
105171
if (this.token && this.tokenExpiry > Date.now()) {
106172
return this.token;
@@ -111,17 +177,17 @@ class MotApiSdk extends EventEmitter {
111177
grant_type: "client_credentials",
112178
client_id: this.clientId,
113179
client_secret: this.clientSecret,
114-
scope: MotApiSdk.SCOPE_URL,
180+
scope: MotApiSdk.SCOPE_URL
115181
});
116182

117183
const tokenResponse = await axios.post<TokenResponse>(
118184
MotApiSdk.TOKEN_URL,
119185
params,
120186
{
121187
headers: {
122-
"Content-Type": "application/x-www-form-urlencoded",
123-
},
124-
},
188+
"Content-Type": "application/x-www-form-urlencoded"
189+
}
190+
}
125191
);
126192

127193
this.token = tokenResponse.data.access_token;
@@ -151,7 +217,7 @@ class MotApiSdk extends EventEmitter {
151217
private async makeRequest<T>(
152218
endpoint: string,
153219
method: "GET" | "PUT" = "GET",
154-
data?: any,
220+
data?: any
155221
): Promise<T> {
156222
try {
157223
const response: AxiosResponse<T> = await this.axiosInstance.request({
@@ -161,7 +227,7 @@ class MotApiSdk extends EventEmitter {
161227
headers:
162228
method === "PUT"
163229
? { "Content-Type": "application/x-www-form-urlencoded" }
164-
: {},
230+
: {}
165231
});
166232
this.emit("requestSuccess", { endpoint, method });
167233
return response.data;

0 commit comments

Comments
 (0)