Skip to content

Commit

Permalink
feat(rest): Add more fine-grained options for rate limit control
Browse files Browse the repository at this point in the history
  • Loading branch information
Sidnioulz committed Sep 30, 2024
1 parent 88a78f4 commit d1369f5
Show file tree
Hide file tree
Showing 2 changed files with 45 additions and 12 deletions.
41 changes: 35 additions & 6 deletions packages/rest/src/__tests__/client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,30 +305,59 @@ describe('@figmarine/rest - client', () => {
});

describe('Options - rateLimit', () => {
it('rate limits by default', async ({ mockedEnv }) => {
// Note that we cannot easily spy on axiosRetry because it's a default export.
it('only applies 429 exponential retry by default', async ({ mockedEnv }) => {
const logSpy = vi.spyOn(loggerModule, 'log');
const rlSpy = vi.spyOn(interceptorsModule, 'rateLimitRequestInterceptor');
mockedEnv({});
await Client();
expect(logSpy).toHaveBeenCalledWith('Applying rate limit proxy to API client.');
expect(rlSpy).toHaveBeenCalled();
expect(logSpy).toHaveBeenCalledWith('Applying rate limit safeguards to API client.');
expect(logSpy).toHaveBeenCalledWith('Applying exponential retry on Error 429.');
expect(logSpy).not.toHaveBeenCalledWith('Applying proactive rate limit (limiting req/s).');
expect(rlSpy).not.toHaveBeenCalled();
});

it('does not rate limit when false', async ({ mockedEnv }) => {
const logSpy = vi.spyOn(loggerModule, 'log');
const rlSpy = vi.spyOn(interceptorsModule, 'rateLimitRequestInterceptor');
mockedEnv({});
await Client({ rateLimit: false });
expect(logSpy).not.toHaveBeenCalledWith('Applying rate limit proxy to API client.');
expect(logSpy).not.toHaveBeenCalledWith('Applying rate limit safeguards to API client.');
expect(logSpy).not.toHaveBeenCalledWith('Applying exponential retry on Error 429.');
expect(logSpy).not.toHaveBeenCalledWith('Applying proactive rate limit (limiting req/s).');
expect(rlSpy).not.toHaveBeenCalled();
});

it('does rate limit when true', async ({ mockedEnv }) => {
it('does rate limit with all options when true', async ({ mockedEnv }) => {
const logSpy = vi.spyOn(loggerModule, 'log');
const rlSpy = vi.spyOn(interceptorsModule, 'rateLimitRequestInterceptor');
mockedEnv({});
await Client({ rateLimit: true });
expect(logSpy).toHaveBeenCalledWith('Applying rate limit proxy to API client.');
expect(logSpy).toHaveBeenCalledWith('Applying rate limit safeguards to API client.');
expect(logSpy).toHaveBeenCalledWith('Applying exponential retry on Error 429.');
expect(logSpy).toHaveBeenCalledWith('Applying proactive rate limit (limiting req/s).');
expect(rlSpy).toHaveBeenCalled();
});

it('only applies 429 exponential retry when set to "reactive"', async ({ mockedEnv }) => {
const logSpy = vi.spyOn(loggerModule, 'log');
const rlSpy = vi.spyOn(interceptorsModule, 'rateLimitRequestInterceptor');
mockedEnv({});
await Client({ rateLimit: 'reactive' });
expect(logSpy).toHaveBeenCalledWith('Applying rate limit safeguards to API client.');
expect(logSpy).toHaveBeenCalledWith('Applying exponential retry on Error 429.');
expect(logSpy).not.toHaveBeenCalledWith('Applying proactive rate limit (limiting req/s).');
expect(rlSpy).not.toHaveBeenCalled();
});

it('does rate limit with all options when set to "proactive"', async ({ mockedEnv }) => {
const logSpy = vi.spyOn(loggerModule, 'log');
const rlSpy = vi.spyOn(interceptorsModule, 'rateLimitRequestInterceptor');
mockedEnv({});
await Client({ rateLimit: 'proactive' });
expect(logSpy).toHaveBeenCalledWith('Applying rate limit safeguards to API client.');
expect(logSpy).toHaveBeenCalledWith('Applying exponential retry on Error 429.');
expect(logSpy).toHaveBeenCalledWith('Applying proactive rate limit (limiting req/s).');
expect(rlSpy).toHaveBeenCalled();
});
});
Expand Down
16 changes: 10 additions & 6 deletions packages/rest/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export interface ClientOptions {
* Whether to slow down API calls so that the Figma REST API's rate limit
* policy isn't violated. Prevents errors when doing multiple consecutive calls.
*/
rateLimit?: boolean;
rateLimit?: boolean | 'reactive' | 'proactive';
}

export type ClientInterface = Omit<ApiInterface<ClientOptions>, 'setSecurityData'> & {
Expand All @@ -50,7 +50,7 @@ export async function Client(opts: ClientOptions = {}): Promise<ClientInterface>
mode = process.env.NODE_ENV,
oauthToken = process.env.FIGMA_OAUTH_TOKEN,
personalAccessToken = process.env.FIGMA_PERSONAL_ACCESS_TOKEN,
rateLimit = true,
rateLimit = 'reactive',
} = opts;

const isDevelopment = mode === 'development';
Expand Down Expand Up @@ -103,13 +103,17 @@ export async function Client(opts: ClientOptions = {}): Promise<ClientInterface>
* We do this without a loop because of TypeScript limitations,
* so this must be maintained on every major API version release. */
if (rateLimit) {
log(`Applying rate limit proxy to API client.`);
log('Applying rate limit safeguards to API client.');

api.instance.interceptors.request.use(
rateLimitRequestInterceptor(defaultKeyGenerator, cacheInstance?.getCache()),
);
if (rateLimit === true || rateLimit === 'proactive') {
log('Applying proactive rate limit (limiting req/s).');
api.instance.interceptors.request.use(
rateLimitRequestInterceptor(defaultKeyGenerator, cacheInstance?.getCache()),
);
}

// Add response interceptor for 429 handling.
log('Applying exponential retry on Error 429.');
const rlConfig = get429Config();
axiosRetry(api.instance, {
onMaxRetryTimesExceeded: (error) => {
Expand Down

0 comments on commit d1369f5

Please sign in to comment.