diff --git a/packages/rest/src/__tests__/client.spec.ts b/packages/rest/src/__tests__/client.spec.ts index babb3ea..5e00d27 100644 --- a/packages/rest/src/__tests__/client.spec.ts +++ b/packages/rest/src/__tests__/client.spec.ts @@ -305,13 +305,16 @@ 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 }) => { @@ -319,16 +322,42 @@ describe('@figmarine/rest - client', () => { 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(); }); }); diff --git a/packages/rest/src/client.ts b/packages/rest/src/client.ts index 58ad85e..b27184e 100644 --- a/packages/rest/src/client.ts +++ b/packages/rest/src/client.ts @@ -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, 'setSecurityData'> & { @@ -50,7 +50,7 @@ export async function Client(opts: ClientOptions = {}): Promise 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'; @@ -103,13 +103,17 @@ export async function Client(opts: ClientOptions = {}): Promise * 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) => {