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