-
Notifications
You must be signed in to change notification settings - Fork 17
feat(js-client): create multi-queued rate limits #380
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
…configured, and server headers
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR is being reviewed by Cursor Bugbot
Details
Your team is on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle for each member of your team.
To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.
| const hasFindBy = 'find_by' in params; | ||
|
|
||
| return (isCdnStories && hasSpecificPath) || hasFindBy; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: Trailing slash incorrectly identified as single story
The isSingleStoryRequest function incorrectly identifies listing endpoints with trailing slashes (like /cdn/stories/) as single story requests. When the URL is /cdn/stories/, the function returns true because url.split('/').length is 4 and !url.endsWith('/cdn/stories') is true (it ends with /cdn/stories/ not /cdn/stories). This causes listings to use the 50 req/s single-story rate limit instead of the appropriate tier based on per_page, potentially causing rate limit issues for users who include trailing slashes in their API calls.
maoberlehner
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some comments are technically wrong and some other minor comments. I let you decide if you want to fix them or not.
packages/js-client/src/rateLimit.ts
Outdated
| } | ||
|
|
||
| // For listings, determine tier based on per_page | ||
| const perPage = params!.per_page || 25; // Default per_page is 25 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question (non-blocking): do we have this magic number 25 in some constant already that we could use here?
packages/js-client/src/rateLimit.ts
Outdated
| SINGLE_OR_SMALL: 50, // Single entries or listings ≤25 entries | ||
| MEDIUM: 15, // 25-50 entries | ||
| LARGE: 10, // 50-75 entries | ||
| VERY_LARGE: 6, // 75-100 entries |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nitpick (non-blocking): I think it should be 26-50, 51-75, and 76-100
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And it would be nice to have the values as constants here, too, instead of hardcoded below. Comments can get old, constants not.
packages/js-client/src/index.test.ts
Outdated
|
|
||
| // Should have created multiple queues for different rate limits (1000, 50, 15) | ||
| // @ts-expect-error - accessing private property for testing | ||
| expect(client.throttleManager.getQueueCount()).toBe(3); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion (non-blocking): instead of inspecting the throttleManager itself, we could use vi.advanceTimersByTime(N) and check how often get was actually called for the different kinds of requests. That way we remove the coupling of the test to the concrete implementation.
it.only('should independently throttle different request types to their specific limits', async () => {
vi.useFakeTimers();
const limits = {
published: 1000,
small: 50,
medium: 15,
large: 10,
veryLarge: 6,
} as const;
const attempts = {
published: limits.published + 10,
small: limits.small + 10,
medium: limits.medium + 10,
large: limits.large + 10,
veryLarge: limits.veryLarge + 10,
} as const;
const promises = [];
for (let i = 0; i < attempts.veryLarge; i++) {
promises.push(
client.get('cdn/stories', { version: 'draft', per_page: 76 }).catch(() => {}),
);
}
for (let i = 0; i < attempts.large; i++) {
promises.push(
client.get('cdn/stories', { version: 'draft', per_page: 51 }).catch(() => {}),
);
}
for (let i = 0; i < attempts.medium; i++) {
promises.push(
client.get('cdn/stories', { version: 'draft', per_page: 26 }).catch(() => {}),
);
}
for (let i = 0; i < attempts.small; i++) {
promises.push(
client.get('cdn/stories', { version: 'draft', per_page: 25 }).catch(() => {}),
);
}
for (let i = 0; i < attempts.published; i++) {
promises.push(
client.get('cdn/stories', { version: 'published' }).catch(() => {}),
);
}
await vi.advanceTimersByTimeAsync(999);
const countCallsContaining = (param: string, value: string | number) => {
return client.client.get.mock.calls.filter(call => call[1][param] === value).length;
};
const veryLargeCalls = countCallsContaining('per_page', 76);
expect(veryLargeCalls).toBe(limits.veryLarge);
const largeCalls = countCallsContaining('per_page', 51);
expect(largeCalls).toBe(limits.large);
const mediumCalls = countCallsContaining('per_page', 26);
expect(mediumCalls).toBe(limits.medium);
const smallCalls = countCallsContaining('per_page', 25);
expect(smallCalls).toBe(limits.small);
const medium = countCallsContaining('version', 'published');
expect(medium).toBe(limits.published);
vi.useRealTimers();
});There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Totally agree. Def better when they're more input-output based. Thanks for the suggestion
packages/js-client/README.md
Outdated
| - (`region` String, optional) | ||
| - (`https` Boolean, optional) | ||
| - (`rateLimit` Integer, optional, defaults to 3 for management api and 5 for cdn api) | ||
| - (`rateLimit` Integer, optional - Custom rate limit for **uncached requests only** (requests with `version=draft`). When not provided, the client automatically uses optimal rate limits based on the request type: 50 req/s for single entries or small listings (≤25 items), 15 req/s for medium listings (25-50 items), 10 req/s for large listings (50-75 items), and 6 req/s for very large listings (75-100 items). Cached requests (version=published) always use 1000 req/s. The maximum rate limit is capped at 1000 req/s. For Management API with `oauthToken`, defaults to 3 req/s for backward compatibility.) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nitpick (non-blocking): I think the ranges here are also not quite correct (26-50 and so on).
@storyblok/astro
storyblok
@storyblok/eslint-config
@storyblok/js
storyblok-js-client
@storyblok/management-api-client
@storyblok/nuxt
@storyblok/react
@storyblok/region-helper
@storyblok/richtext
@storyblok/svelte
@storyblok/vue
commit: |
Note
Implements a multi-queue throttle manager with dynamic rate limits (CDN/Management), parses server rate limit headers, integrates across client methods, and updates docs/tests.
ThrottleQueueManagerto run separate throttle queues per rate (e.g.,1000,50,15,10,6req/s).rateLimit.tswithdetermineRateLimit,parseRateLimitHeaders,createRateLimitConfig, andMANAGEMENT_API_DEFAULT_RATE_LIMIT.DEFAULT_PER_PAGE,PER_PAGE_THRESHOLDSfor tiered uncached limits.src/index.ts):throttlewiththrottleManagerand per-request rate selection viadetermineRateLimit.get(viacacheResponse) and Management API methods (post,put,delete).X-RateLimit(-Policy)response headers to adjust future limits.rateLimitconfig option description and behavior notes.rateLimit.test.ts,throttleQueueManager.test.ts).throttleManager.execute; adds timing-based throttling scenarios inindex.test.ts.abortinthrottlePromise.test.ts.Written by Cursor Bugbot for commit 227642a. This will update automatically on new commits. Configure here.