@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 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 axios.get(`${apiDomain}/v1/developer/historical-candles`, {
8153
- params: validatedParams,
8154
- headers
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)) {