@ganaka/sdk 1.9.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +147 -4
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +147 -4
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -8121,6 +8121,144 @@ const logger = pino({
|
|
|
8121
8121
|
target: "pino-pretty"
|
|
8122
8122
|
}
|
|
8123
8123
|
});
|
|
8124
|
+
class RateLimiter {
|
|
8125
|
+
constructor(config2) {
|
|
8126
|
+
this.queue = [];
|
|
8127
|
+
this.secondTimestamps = [];
|
|
8128
|
+
this.minuteTimestamps = [];
|
|
8129
|
+
this.processing = false;
|
|
8130
|
+
this.inFlight = 0;
|
|
8131
|
+
this.maxPerSecond = config2.maxPerSecond;
|
|
8132
|
+
this.maxPerMinute = config2.maxPerMinute;
|
|
8133
|
+
this.maxConcurrency = config2.maxConcurrency ?? 10;
|
|
8134
|
+
this.requestTimeoutMs = config2.requestTimeoutMs ?? 3e4;
|
|
8135
|
+
}
|
|
8136
|
+
/**
|
|
8137
|
+
* Execute a function with rate limiting.
|
|
8138
|
+
* The function will be queued and executed when rate limits allow.
|
|
8139
|
+
*/
|
|
8140
|
+
async execute(fn) {
|
|
8141
|
+
return new Promise((resolve, reject) => {
|
|
8142
|
+
this.queue.push({ execute: fn, resolve, reject });
|
|
8143
|
+
this.processQueue();
|
|
8144
|
+
});
|
|
8145
|
+
}
|
|
8146
|
+
/**
|
|
8147
|
+
* Process the queue of pending requests.
|
|
8148
|
+
* Dispatches requests concurrently up to maxConcurrency while respecting rate limits.
|
|
8149
|
+
*/
|
|
8150
|
+
async processQueue() {
|
|
8151
|
+
if (this.processing) {
|
|
8152
|
+
return;
|
|
8153
|
+
}
|
|
8154
|
+
this.processing = true;
|
|
8155
|
+
while (this.queue.length > 0) {
|
|
8156
|
+
this.cleanupTimestamps();
|
|
8157
|
+
if (this.inFlight < this.maxConcurrency && this.canMakeRequest()) {
|
|
8158
|
+
const request = this.queue.shift();
|
|
8159
|
+
if (!request) {
|
|
8160
|
+
break;
|
|
8161
|
+
}
|
|
8162
|
+
const now = Date.now();
|
|
8163
|
+
this.secondTimestamps.push(now);
|
|
8164
|
+
this.minuteTimestamps.push(now);
|
|
8165
|
+
this.inFlight++;
|
|
8166
|
+
this.executeWithTimeout(request.execute, this.requestTimeoutMs).then((result) => {
|
|
8167
|
+
request.resolve(result);
|
|
8168
|
+
}).catch((error) => {
|
|
8169
|
+
request.reject(error instanceof Error ? error : new Error(String(error)));
|
|
8170
|
+
}).finally(() => {
|
|
8171
|
+
this.inFlight--;
|
|
8172
|
+
this.processQueue();
|
|
8173
|
+
});
|
|
8174
|
+
} else {
|
|
8175
|
+
if (this.inFlight >= this.maxConcurrency) {
|
|
8176
|
+
break;
|
|
8177
|
+
} else {
|
|
8178
|
+
const waitTime = this.getWaitTime();
|
|
8179
|
+
if (waitTime > 0) {
|
|
8180
|
+
await this.sleep(waitTime);
|
|
8181
|
+
} else {
|
|
8182
|
+
await this.sleep(10);
|
|
8183
|
+
}
|
|
8184
|
+
}
|
|
8185
|
+
}
|
|
8186
|
+
}
|
|
8187
|
+
this.processing = false;
|
|
8188
|
+
}
|
|
8189
|
+
/**
|
|
8190
|
+
* Check if a new request can be made based on current rate limits.
|
|
8191
|
+
*/
|
|
8192
|
+
canMakeRequest() {
|
|
8193
|
+
const now = Date.now();
|
|
8194
|
+
const oneSecondAgo = now - 1e3;
|
|
8195
|
+
const oneMinuteAgo = now - 6e4;
|
|
8196
|
+
const recentSecondCount = this.secondTimestamps.filter((ts) => ts > oneSecondAgo).length;
|
|
8197
|
+
const recentMinuteCount = this.minuteTimestamps.filter((ts) => ts > oneMinuteAgo).length;
|
|
8198
|
+
return recentSecondCount < this.maxPerSecond && recentMinuteCount < this.maxPerMinute;
|
|
8199
|
+
}
|
|
8200
|
+
/**
|
|
8201
|
+
* Remove timestamps that are outside the tracking windows.
|
|
8202
|
+
*/
|
|
8203
|
+
cleanupTimestamps() {
|
|
8204
|
+
const now = Date.now();
|
|
8205
|
+
const oneSecondAgo = now - 1e3;
|
|
8206
|
+
const oneMinuteAgo = now - 6e4;
|
|
8207
|
+
this.secondTimestamps = this.secondTimestamps.filter((ts) => ts > oneSecondAgo);
|
|
8208
|
+
this.minuteTimestamps = this.minuteTimestamps.filter((ts) => ts > oneMinuteAgo);
|
|
8209
|
+
}
|
|
8210
|
+
/**
|
|
8211
|
+
* Calculate the minimum wait time needed before the next request can be made.
|
|
8212
|
+
*/
|
|
8213
|
+
getWaitTime() {
|
|
8214
|
+
const now = Date.now();
|
|
8215
|
+
const oneSecondAgo = now - 1e3;
|
|
8216
|
+
const oneMinuteAgo = now - 6e4;
|
|
8217
|
+
let waitTime = 0;
|
|
8218
|
+
const recentSecondCount = this.secondTimestamps.filter((ts) => ts > oneSecondAgo).length;
|
|
8219
|
+
if (recentSecondCount >= this.maxPerSecond) {
|
|
8220
|
+
const oldestInSecond = Math.min(...this.secondTimestamps.filter((ts) => ts > oneSecondAgo));
|
|
8221
|
+
const waitForSecond = oldestInSecond + 1e3 - now;
|
|
8222
|
+
waitTime = Math.max(waitTime, waitForSecond);
|
|
8223
|
+
}
|
|
8224
|
+
const recentMinuteCount = this.minuteTimestamps.filter((ts) => ts > oneMinuteAgo).length;
|
|
8225
|
+
if (recentMinuteCount >= this.maxPerMinute) {
|
|
8226
|
+
const oldestInMinute = Math.min(...this.minuteTimestamps.filter((ts) => ts > oneMinuteAgo));
|
|
8227
|
+
const waitForMinute = oldestInMinute + 6e4 - now;
|
|
8228
|
+
waitTime = Math.max(waitTime, waitForMinute);
|
|
8229
|
+
}
|
|
8230
|
+
return Math.max(waitTime, 0);
|
|
8231
|
+
}
|
|
8232
|
+
/**
|
|
8233
|
+
* Execute a function with a timeout.
|
|
8234
|
+
* If the function doesn't complete within the timeout, it will be rejected.
|
|
8235
|
+
*/
|
|
8236
|
+
async executeWithTimeout(fn, timeoutMs) {
|
|
8237
|
+
return Promise.race([
|
|
8238
|
+
fn(),
|
|
8239
|
+
new Promise(
|
|
8240
|
+
(_2, reject) => setTimeout(
|
|
8241
|
+
() => reject(new Error(`Request timed out after ${timeoutMs}ms`)),
|
|
8242
|
+
timeoutMs
|
|
8243
|
+
)
|
|
8244
|
+
)
|
|
8245
|
+
]);
|
|
8246
|
+
}
|
|
8247
|
+
/**
|
|
8248
|
+
* Sleep for a given number of milliseconds.
|
|
8249
|
+
*/
|
|
8250
|
+
sleep(ms) {
|
|
8251
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
8252
|
+
}
|
|
8253
|
+
}
|
|
8254
|
+
const limiters = /* @__PURE__ */ new Map();
|
|
8255
|
+
limiters.set("groww", new RateLimiter({
|
|
8256
|
+
maxPerSecond: 10,
|
|
8257
|
+
maxPerMinute: 300,
|
|
8258
|
+
maxConcurrency: 10,
|
|
8259
|
+
requestTimeoutMs: 3e4
|
|
8260
|
+
}));
|
|
8261
|
+
const growwRateLimiter = limiters.get("groww");
|
|
8124
8262
|
dayjs.extend(utc);
|
|
8125
8263
|
dayjs.extend(timezone);
|
|
8126
8264
|
const fetchCandles = ({
|
|
@@ -8149,10 +8287,15 @@ const fetchCandles = ({
|
|
|
8149
8287
|
if (currentTimezone) {
|
|
8150
8288
|
headers["X-Current-Timezone"] = currentTimezone;
|
|
8151
8289
|
}
|
|
8152
|
-
const response = await
|
|
8153
|
-
|
|
8154
|
-
|
|
8155
|
-
|
|
8290
|
+
const response = await growwRateLimiter.execute(
|
|
8291
|
+
() => axios.get(
|
|
8292
|
+
`${apiDomain}/v1/developer/historical-candles`,
|
|
8293
|
+
{
|
|
8294
|
+
params: validatedParams,
|
|
8295
|
+
headers
|
|
8296
|
+
}
|
|
8297
|
+
)
|
|
8298
|
+
);
|
|
8156
8299
|
return response.data.data;
|
|
8157
8300
|
} catch (error) {
|
|
8158
8301
|
if (axios.isAxiosError(error)) {
|