diff --git a/package/package.json b/package/package.json index aaac7ca..4590c97 100644 --- a/package/package.json +++ b/package/package.json @@ -40,12 +40,15 @@ "peerDependencies": { "astro": "^4.4.1" }, + "devDependencies": { + "@octokit/types": "^12.6.0" + }, "dependencies": { "@expressive-code/plugin-line-numbers": "^0.33.4", - "@octokit/types": "^12.6.0", "astro-integration-kit": "^0.5.1", "expressive-code": "^0.33.4", "hast-util-to-html": "8.0.4", + "p-retry": "6.2.0", "octokit": "^3.1.2", "vite": "^5.1.6" } diff --git a/package/src/lib/gist-processor.ts b/package/src/lib/gist-processor.ts deleted file mode 100644 index 259f6e9..0000000 --- a/package/src/lib/gist-processor.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { requestRetry } from './octokit'; - -// Get a Gist by ID -export const getGist = async (gistId: string) => { - /** @ts-ignore-error */ - const { data: response } = await requestRetry('GET /gists/{gist_id}', { gist_id: gistId }); - const data = response as { - // biome-ignore lint/suspicious/noExplicitAny: we don't know the shape of the data returned from the API -files: any; data: unknown -}; - return data; -}; - -// Get a file from a Gist by ID and filename -export const getGistFile = async ( - gistId: string, - filename: string - ) => { - const gist = await getGist(gistId); - if (gist?.files) { - const file = gist.files[filename]; - return file ? file : null; - } - return null; -}; - -export const getGistGroup = async (gistId: string) => { - const gist = await getGist(gistId); - return gist; -}; \ No newline at end of file diff --git a/package/src/lib/index.ts b/package/src/lib/index.ts index b7e2afd..3ded18b 100644 --- a/package/src/lib/index.ts +++ b/package/src/lib/index.ts @@ -1,2 +1 @@ -export * from "./gist-processor"; export * from "./octokit"; \ No newline at end of file diff --git a/package/src/lib/octokit.ts b/package/src/lib/octokit.ts index d75a5e5..7269f8d 100644 --- a/package/src/lib/octokit.ts +++ b/package/src/lib/octokit.ts @@ -1,10 +1,12 @@ import { Octokit } from "octokit"; +import type { OctokitResponse } from "@octokit/types"; import { loadEnv } from "vite"; -import type { Route, RequestParameters, OctokitResponse } from "@octokit/types" +import pRretry from 'p-retry'; // Load environment variables const { GITHUB_PERSONAL_TOKEN } = loadEnv("all", process.cwd(), "GITHUB_"); +// Check if there is a GitHub Personal Token export const isThereAToken = () => { if (!GITHUB_PERSONAL_TOKEN) { return false; @@ -12,32 +14,59 @@ export const isThereAToken = () => { return true; } +// Error message if the token is missing export const TOKEN_MISSING_ERROR = "GITHUB_PERSONAL_TOKEN not found. Please add it to your .env file. Without it, you will be limited to 60 requests per hour."; // Create an Octokit instance const octokit = new Octokit({ auth: GITHUB_PERSONAL_TOKEN }); -// Retry requests if rate limited -export async function requestRetry(route: Route, parameters: RequestParameters) { - try { - const response: OctokitResponse = await octokit.request(route, parameters); - return response; - } catch (error) { - /** @ts-ignore-error */ - if (error.response && error.status === 403 && error.response.headers['x-ratelimit-remaining'] === '0') { - /** @ts-ignore-error */ - const resetTimeEpochSeconds = error.response.headers['x-ratelimit-reset']; - const currentTimeEpochSeconds = Math.floor(new Date().getTime() / 1000); - const secondsToWait = resetTimeEpochSeconds - currentTimeEpochSeconds; - console.log(`Rate limit reached. Waiting ${secondsToWait} seconds before retrying.`); - return new Promise((resolve) => { - setTimeout(async () => { - const retryResponse = await requestRetry(route, parameters); - resolve(retryResponse); - }, secondsToWait * 1000); - }); - } - // Return a rejected Promise - return Promise.reject(error); +// Retry failed requests +const retry: typeof pRretry = (fn, opts) => + pRretry(fn, { + onFailedAttempt: (e) => + console.log(`[Astro-Gists] Attempt ${e.attemptNumber} failed. There are ${e.retriesLeft} retries left.\n `, + e.message + ), + retries: 3, + ...opts, + }); + +// Handle the response from the Octokit API +// biome-ignore lint/suspicious/noExplicitAny: any is used to handle the response from the Octokit API +function handleResponse(response: OctokitResponse) { + switch (response.status) { + case 200: + return response.data; + case 404: + return "Gist not found."; + case 403: + return "You have exceeded the rate limit for requests to the GitHub API. Please try again later."; + case 500: + return "An internal server error occurred. Please try again later."; + default: + return "An error occurred. Please try again later."; } -} \ No newline at end of file +} + +// Gist Grabber +const gistGrabber = async (gistId: string) => { + const response = await retry(() => octokit.request('GET /gists/{gist_id}', { gist_id: gistId })); + + return handleResponse(response); +} + +// Get a file from a Gist by ID and filename +export const getGistFile = async ( + gistId: string, + filename: string + ) => { + const gist = await gistGrabber(gistId); + const file = gist.files[filename]; + return file ? file : null; +}; + +// Get a Group of Gist files by ID +export const getGistGroup = async (gistId: string) => { + const gist = await gistGrabber(gistId); + return gist; +}; \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index afc3eb5..a808d2e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,9 +20,6 @@ importers: '@expressive-code/plugin-line-numbers': specifier: ^0.33.4 version: 0.33.4 - '@octokit/types': - specifier: ^12.6.0 - version: 12.6.0 astro: specifier: ^4.4.1 version: 4.4.11 @@ -38,9 +35,16 @@ importers: octokit: specifier: ^3.1.2 version: 3.1.2 + p-retry: + specifier: 6.2.0 + version: 6.2.0 vite: specifier: ^5.1.6 version: 5.1.6(@types/node@20.11.25) + devDependencies: + '@octokit/types': + specifier: ^12.6.0 + version: 12.6.0 playground: dependencies: @@ -1186,7 +1190,6 @@ packages: /@octokit/openapi-types@20.0.0: resolution: {integrity: sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==} - dev: false /@octokit/plugin-paginate-graphql@4.0.0(@octokit/core@5.1.0): resolution: {integrity: sha512-7HcYW5tP7/Z6AETAPU14gp5H5KmCPT3hmJrS/5tO7HIgbwenYmgw4OY9Ma54FDySuxMwD+wsJlxtuGWwuZuItA==} @@ -1263,7 +1266,6 @@ packages: resolution: {integrity: sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==} dependencies: '@octokit/openapi-types': 20.0.0 - dev: false /@octokit/webhooks-methods@4.1.0: resolution: {integrity: sha512-zoQyKw8h9STNPqtm28UGOYFE7O6D4Il8VJwhAtMHFt2C4L0VQT1qGKLeefUOqHNs1mNRYSadVv7x0z8U2yyeWQ==} @@ -1486,6 +1488,10 @@ packages: resolution: {integrity: sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==} dev: false + /@types/retry@0.12.2: + resolution: {integrity: sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==} + dev: false + /@types/semver@7.5.7: resolution: {integrity: sha512-/wdoPq1QqkSj9/QOeKkFquEuPzQbHTWAMPH/PaUMB+JuR31lXhlWXRZ52IpfDYVlDOUBvX09uBrPwxGT1hjNBg==} dev: true @@ -3341,6 +3347,11 @@ packages: engines: {node: '>= 0.4'} dev: true + /is-network-error@1.0.1: + resolution: {integrity: sha512-OwQXkwBJeESyhFw+OumbJVD58BFBJJI5OM5S1+eyrDKlgDZPX2XNT5gXS56GSD3NPbbwUuMlR1Q71SRp5SobuQ==} + engines: {node: '>=16'} + dev: false + /is-number-object@1.0.7: resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} engines: {node: '>= 0.4'} @@ -4395,6 +4406,15 @@ packages: eventemitter3: 5.0.1 p-timeout: 6.1.2 + /p-retry@6.2.0: + resolution: {integrity: sha512-JA6nkq6hKyWLLasXQXUrO4z8BUZGUt/LjlJxx8Gb2+2ntodU/SS63YZ8b0LUTbQ8ZB9iwOfhEPhg4ykKnn2KsA==} + engines: {node: '>=16.17'} + dependencies: + '@types/retry': 0.12.2 + is-network-error: 1.0.1 + retry: 0.13.1 + dev: false + /p-timeout@6.1.2: resolution: {integrity: sha512-UbD77BuZ9Bc9aABo74gfXhNvzC9Tx7SxtHSh1fxvx3jTLLYvmVhiQZZrJzqqU0jKbN32kb5VOKiLEQI/3bIjgQ==} engines: {node: '>=14.16'} @@ -4838,6 +4858,11 @@ packages: retext-stringify: 3.1.0 unified: 10.1.2 + /retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + dev: false + /reusify@1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'}