diff --git a/package-lock.json b/package-lock.json index e7602a8..cc1f02d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "@pulumi/github": "^6.7.3", + "@pulumi/github": "^6.11.0", "@pulumi/googleworkspace": "file:sdks/googleworkspace", "@pulumi/pulumi": "^3.218.0", "@pulumi/random": "^4.14.0" @@ -655,9 +655,9 @@ "license": "BSD-3-Clause" }, "node_modules/@pulumi/github": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/@pulumi/github/-/github-6.9.1.tgz", - "integrity": "sha512-B7FcceruXfMClws4poIyUfJoy2dqcdhi8P5+ahfArIeaDjFDlcRNU9SZbfFRbkdoXflMJi+AI3rWabyvqS2+YQ==", + "version": "6.12.1", + "resolved": "https://registry.npmjs.org/@pulumi/github/-/github-6.12.1.tgz", + "integrity": "sha512-OsbQgF8go/tA1PuMhFq7O0XVfYbwRk0/LHaZTH6OFO395SufdFFa1go3IeXvJCSA5oDCbdULerGg4n9sP8vTLg==", "license": "Apache-2.0", "dependencies": { "@pulumi/pulumi": "^3.142.0" diff --git a/src/discord.ts b/src/discord.ts index d08f17a..99896c2 100644 --- a/src/discord.ts +++ b/src/discord.ts @@ -20,31 +20,63 @@ interface DiscordApiError { message: string; } +interface DiscordRateLimitResponse { + message: string; + retry_after: number; + global: boolean; +} + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + async function discordFetch( token: string, endpoint: string, - options: RequestInit = {} + options: RequestInit = {}, + maxRetries = 5 ): Promise { - const response = await fetch(`${DISCORD_API_BASE}${endpoint}`, { - ...options, - headers: { - Authorization: `Bot ${token}`, - 'Content-Type': 'application/json', - ...options.headers, - }, - }); + let lastError: Error | undefined; + + for (let attempt = 0; attempt <= maxRetries; attempt++) { + const response = await fetch(`${DISCORD_API_BASE}${endpoint}`, { + ...options, + headers: { + Authorization: `Bot ${token}`, + 'Content-Type': 'application/json', + ...options.headers, + }, + }); - if (!response.ok) { - const error = (await response.json()) as DiscordApiError; - throw new Error(`Discord API error: ${error.message} (code: ${error.code})`); - } + if (response.status === 429) { + const body = (await response.json()) as DiscordRateLimitResponse; + const retryAfterMs = Math.ceil(body.retry_after * 1000) + Math.random() * 250; + lastError = new Error( + `Discord API rate limited on ${endpoint} (retry_after=${body.retry_after}s, global=${body.global})` + ); + if (attempt < maxRetries) { + await sleep(retryAfterMs); + continue; + } + throw lastError; + } + + if (!response.ok) { + const error = (await response.json()) as DiscordApiError; + throw new Error(`Discord API error: ${error.message} (code: ${error.code})`); + } - // Handle 204 No Content - if (response.status === 204) { - return undefined as T; + // Handle 204 No Content + if (response.status === 204) { + return undefined as T; + } + + return response.json() as Promise; } - return response.json() as Promise; + throw ( + lastError ?? new Error(`Discord API request to ${endpoint} failed after ${maxRetries} retries`) + ); } // Discord API response types @@ -122,8 +154,8 @@ const discordRoleProvider: pulumi.dynamic.ResourceProvider = { roleId: role.id, }, }; - } catch { - throw new Error(`Failed to read role ${id}`); + } catch (error) { + throw new Error(`Failed to read role ${id}: ${error}`); } }, @@ -303,7 +335,7 @@ const discordMemberRoleSyncProvider: pulumi.dynamic.ResourceProvider = { }, }; } - throw new Error(`Failed to read member roles for ${id}`); + throw new Error(`Failed to read member roles for ${id}: ${error}`); } const currentRoles = new Set(member.roles);