Skip to content

Commit

Permalink
feat: add retry logic to fetch for 3 retries
Browse files Browse the repository at this point in the history
  • Loading branch information
mvantellingen committed Jan 23, 2025
1 parent 7c7800f commit 56d6acf
Show file tree
Hide file tree
Showing 6 changed files with 94 additions and 8 deletions.
5 changes: 5 additions & 0 deletions .changeset/tough-fireants-build.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@labdigital/apollo-trusted-documents": patch
---

Add logic to retry fetching documents from remote store
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"@apollo/server": "^4.11.0",
"@biomejs/biome": "^1.9.4",
"@changesets/cli": "^2.27.1",
"@types/node": "^20.12.7",
"@types/node": "^20.17.14",
"@vitest/coverage-v8": "3.0.2",
"tsup": "8.3.5",
"typescript": "5.7.3",
Expand Down
2 changes: 1 addition & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 34 additions & 0 deletions src/fetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
export const fetchWithRetry = async (
url: string,
options: RequestInit = {},
retries = 3,
initialDelay = 100,
): Promise<Response> => {
let delay = initialDelay;

for (let attempt = 0; attempt <= retries; attempt++) {
try {
const response = await fetch(url, options);

if (!response.ok) {
console.error(
`Failed to fetch data from ${url} status: ${response.status}`,
);
throw new Error(`HTTP error! status: ${response.status}`);
}
return response;
} catch (err: unknown) {
if (attempt >= retries) {
throw err;
}

// Wait for the current delay before retrying
await new Promise((resolve) => setTimeout(resolve, delay));

// Increase the delay (exponential backoff)
delay = Math.min(delay * 2, 1000); // Cap at 1000ms
}
}

throw new Error("Internal error");
};
52 changes: 49 additions & 3 deletions src/store-hive.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import { Keyv } from "keyv";
import { http, HttpResponse } from "msw";
import { setupServer } from "msw/node";
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
import {
afterAll,
afterEach,
beforeAll,
beforeEach,
describe,
expect,
it,
vi,
} from "vitest";
import { HiveStore } from "./store-hive";

const server = setupServer();
Expand All @@ -10,7 +19,11 @@ describe("HiveStore", () => {
beforeAll(() => {
server.listen({ onUnhandledRequest: "error" });
});
beforeEach(() => {
vi.spyOn(console, "error").mockImplementation(() => {});
});
afterEach(() => {
vi.restoreAllMocks();
server.resetHandlers();
});
afterAll(() => {
Expand Down Expand Up @@ -58,8 +71,34 @@ describe("HiveStore", () => {
const result = await hiveStore.get(documentId);
expect(result).toBe(fetchedDocument);
});
it("should return undefined if the fetch has a network error", async () => {
const cache = new Keyv<string | null>({ store: new Map() });
const hiveStore = new HiveStore({
endpoint: "https://example.com",
accessToken: "test-access-token",
cache: cache,
});

const documentId = "my-app/version-1/hash";
const fetchedDocument = "Fetched Document Content";

let count = 0;
server.use(
http.get(`https://example.com/apps/${documentId}`, ({ request }) => {
count += 1;
if (count < 2) {
return HttpResponse.error();
}
return HttpResponse.text(fetchedDocument);
}),
);

const result = await hiveStore.get(documentId);

expect(result).toBe(fetchedDocument);
});

it.skip("should return undefined if the fetch fails", async () => {
it("should return undefined if the fetch fails", async () => {
const cache = new Keyv<string | null>({ store: new Map() });
const hiveStore = new HiveStore({
endpoint: "https://example.com",
Expand All @@ -73,7 +112,14 @@ describe("HiveStore", () => {
server.use(
http.get(`https://example.com/apps/${documentId}`, ({ request }) => {
if (request.headers.get("X-Hive-CDN-Key") !== "other-access-token") {
return HttpResponse.error();
return HttpResponse.json(
{
message: "Unauthorized",
},
{
status: 401,
},
);
}
return HttpResponse.text(fetchedDocument);
}),
Expand Down
7 changes: 4 additions & 3 deletions src/store-hive.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { fetchWithRetry } from "./fetch";
import type { DocumentCache, DocumentStore } from "./store-base";

type HiveOptions = {
Expand Down Expand Up @@ -32,7 +33,7 @@ export class HiveStore implements DocumentStore {
}

const url = `${this.options.endpoint}/apps/${documentId}`;
document = await fetch(url, {
document = await fetchWithRetry(url, {
headers: {
"X-Hive-CDN-Key": this.options.accessToken,
},
Expand All @@ -43,8 +44,8 @@ export class HiveStore implements DocumentStore {
}
return null;
})
.catch(() => {
console.error("failed to fetch document from hive");
.catch((error) => {
console.error("failed to fetch document from hive: ", error);
return null;
});

Expand Down

0 comments on commit 56d6acf

Please sign in to comment.