Skip to content

Commit

Permalink
chore: add retries test
Browse files Browse the repository at this point in the history
  • Loading branch information
Complexlity committed Aug 25, 2024
1 parent c965e4d commit 047020f
Show file tree
Hide file tree
Showing 3 changed files with 223 additions and 66 deletions.
5 changes: 3 additions & 2 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const service = new uniFarcasterSdk({
});

const mockUser = { fid: 123, username: "testuser" };
const DUMMY_CAST_HASH = "0x59821dAf7b797D926440C9088bb91e018d6556B8";

runBasicTests(service);

Expand Down Expand Up @@ -184,7 +185,7 @@ describe("main cache", () => {

test("should use cache for getCastByHash", async () => {
const mockCast = {
hash: "0x59821dAf7b797D926440C9088bb91e018d6556B8",
hash: DUMMY_CAST_HASH,
url: "https://example.com/cast/mockUser.fid",
};
mockService.getCastByHash.mockResolvedValueOnce({
Expand All @@ -211,7 +212,7 @@ describe("main cache", () => {
test("should use cache for getCastByUrl", async () => {
const mockCast = {
url: "https://example.com/cast/mockUser.fid",
hash: "0x59821dAf7b797D926440C9088bb91e018d6556B8",
hash: DUMMY_CAST_HASH,
};
mockService.getCastByUrl.mockResolvedValueOnce({
data: mockCast,
Expand Down
90 changes: 26 additions & 64 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
import { Cache, type CacheKeys, type CacheTypes } from "@/lib/cache";
import { DEFAULTS } from "@/lib/constants";
import { LogLevel, Logger, Noop } from "@/lib/logger";
import type {
Cast,
Config,
DataOrError,
Service,
User,
UserWithOptionalViewerContext,
} from "@/lib/types";
import type { Cast, Config, DataOrError, Service, User } from "@/lib/types";
import { isAddress } from "@/lib/utils";
import { type TService, services } from "@/services";

Expand Down Expand Up @@ -122,23 +115,27 @@ class uniFarcasterSdk implements Omit<Service, "name"> {
}
}

// Add this helper method
private async retryWrapper<T>(
fn: (...args: any[]) => Promise<DataOrError<T>>,
...args: any[]
): Promise<DataOrError<T>> {
let attempts = 0;

// console.log(this.retries)
while (attempts <= this.retries) {
const result = await fn(...args);

if (!result.error) {
return result;
}
attempts++;
if (attempts <= this.retries) {
this.logger().warning(`Retry attempt ${attempts} of ${this.retries}`);
this.logger({ name: "retrying..." }).warning(
`attmept ${attempts} of ${this.retries}`,
);
}
}
return await fn(...args); // Return the last attempt result
return await fn(...args);
}

private async withCache<T extends CacheKeys>(
Expand All @@ -161,7 +158,10 @@ class uniFarcasterSdk implements Omit<Service, "name"> {
};
}
this.logger({ name: "cache miss" }).warning(`${description}`);
const result = await fn.apply(thisArg || this.activeService, params);
const result = await this.retryWrapper(
async () => fn.apply(thisArg || this.activeService, params),
...params,
);
const { data } = result;
if (data) {
//First params is the fid or username and we don't want to add that since we would get that from data
Expand Down Expand Up @@ -207,13 +207,12 @@ class uniFarcasterSdk implements Omit<Service, "name"> {
) {
if (!this.airstackApiKey) throw new Error("No airstack api key provided");
const airstackService = new services.airstack(this.airstackApiKey);
const res = await this.withCache(
return await this.withCache(
"custom",
airstackService.customQuery<T>,
[query, variables],
airstackService,
);
return res;
}

public async neynar<T = unknown>(
Expand All @@ -222,47 +221,28 @@ class uniFarcasterSdk implements Omit<Service, "name"> {
) {
if (!this.neynarApiKey) throw new Error("No neynar api key provided");
const neynarService = new services.neynar(this.neynarApiKey);
const res = await this.withCache(

return await this.withCache(
"custom",
neynarService.customQuery<T>,
[endpoint, params],
neynarService,
);
return res;
}

public async getUserByFid(fid: number, viewerFid: number = DEFAULTS.fid) {
return this.retryWrapper(
async () =>
(await this.withCache("user", this.activeService.getUserByFid, [
fid,
viewerFid,
])) as DataOrError<User>,
fid,
viewerFid,
);

// const res = (await this.withCache(
// "user",
// this.activeService?.getUserByFid,
// [fid, viewerFid]
// )) as DataOrError<User>;
// return res;
const res = (await this.withCache(
"user",
this.activeService?.getUserByFid,
[fid, viewerFid],
)) as DataOrError<User>;
return res;
}

public async getUserByUsername(
username: string,
viewerFid: number = DEFAULTS.fid,
) {
return this.retryWrapper(
async () =>
(await this.withCache("user", this.activeService.getUserByUsername, [
username,
viewerFid,
])) as DataOrError<UserWithOptionalViewerContext>,
username,
viewerFid,
);
const res = await this.withCache(
"user",
this.activeService.getUserByUsername,
Expand All @@ -277,38 +257,20 @@ class uniFarcasterSdk implements Omit<Service, "name"> {
if (!isValidHash) {
res = { data: null, error: { message: "Invalid hash" } };
} else {
res = await this.retryWrapper(
async () =>
(await this.withCache("cast", this.activeService.getCastByHash, [
hash,
viewerFid,
])) as DataOrError<Cast>,
res = await this.withCache("cast", this.activeService?.getCastByHash, [
hash,
viewerFid,
);
// res = await this.withCache("cast", this.activeService?.getCastByHash, [
// hash,
// viewerFid,
// ]);
]);
}
return res;
}

public async getCastByUrl(url: string, viewerFid: number = DEFAULTS.fid) {
return this.retryWrapper(
async () =>
(await this.withCache("cast", this.activeService.getCastByUrl, [
url,
viewerFid,
])) as DataOrError<Cast>,
const res = await this.withCache("cast", this.activeService?.getCastByUrl, [
url,
viewerFid,
);
// const res = await this.withCache("cast", this.activeService?.getCastByUrl, [
// url,
// viewerFid,
// ]);
// return res;
]);
return res;
}
}

Expand Down
194 changes: 194 additions & 0 deletions src/retries.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import { type Mock, beforeEach, describe, expect, test, vi } from "vitest";
import uniFarcasterSdk from ".";
import { DataOrError } from "./lib/types";
import { services } from "./services";

vi.mock("./services", () => ({
services: {
neynar: vi.fn(),
airstack: vi.fn(),
},
}));

const DUMMY_CAST_HASH = "0x59821dAf7b797D926440C9088bb91e018d6556B8";

describe("uniFarcasterSdk with retries", () => {
let sdk: uniFarcasterSdk;
let mockService: {
name: string;
getUserByFid: Mock;
getUserByUsername: Mock;
getCastByHash: Mock;
getCastByUrl: Mock;
};

beforeEach(() => {
vi.clearAllMocks();
mockService = {
name: "mockService",
getUserByFid: vi.fn(),
getUserByUsername: vi.fn(),
getCastByHash: vi.fn(),
getCastByUrl: vi.fn(),
};

sdk = new uniFarcasterSdk({
neynarApiKey: "mock-key",
airstackApiKey: "mock-airstack-key",
retries: 2,
});
// @ts-expect-error Accessing private property for testing
sdk.activeService = mockService;
});

test("should retry getUserByFid on error", async () => {
const mockUser = { fid: 123, username: "testuser" };
const errorResponse: DataOrError<typeof mockUser> = {
data: null,
error: { message: "Error" },
};
const successResponse: DataOrError<typeof mockUser> = {
data: mockUser,
error: null,
};

mockService.getUserByFid
.mockResolvedValueOnce(errorResponse)
.mockResolvedValueOnce(errorResponse)
.mockResolvedValueOnce(successResponse);

const result = await sdk.getUserByFid(mockUser.fid);

expect(result).toEqual(successResponse);
expect(mockService.getUserByFid).toHaveBeenCalledTimes(3);
});

test("should return last error if all retries fail", async () => {
const mockUser = { fid: 123, username: "testuser" };
const errorResponse: DataOrError<typeof mockUser> = {
data: null,
error: { message: "Error" },
};

mockService.getUserByFid.mockResolvedValue(errorResponse);

const result = await sdk.getUserByFid(mockUser.fid);

expect(result).toEqual(errorResponse);
});

test("should not retry if first attempt is successful", async () => {
const mockUser = { fid: 123, username: "testuser" };
const successResponse: DataOrError<typeof mockUser> = {
data: mockUser,
error: null,
};

mockService.getUserByFid.mockResolvedValue(successResponse);

const result = await sdk.getUserByFid(mockUser.fid);

expect(result).toEqual(successResponse);
expect(mockService.getUserByFid).toHaveBeenCalledTimes(1);
});

test("should retry getUserByUsername on error", async () => {
const mockUser = { fid: 123, username: "testuser" };
const errorResponse: DataOrError<typeof mockUser> = {
data: null,
error: { message: "Error" },
};
const successResponse: DataOrError<typeof mockUser> = {
data: mockUser,
error: null,
};

mockService.getUserByUsername
.mockResolvedValueOnce(errorResponse)
.mockResolvedValueOnce(errorResponse)
.mockResolvedValueOnce(successResponse);

const result = await sdk.getUserByUsername(mockUser.username);

expect(result).toEqual(successResponse);
expect(mockService.getUserByUsername).toHaveBeenCalledTimes(3);
});

test("should retry getCastByHash on error", async () => {
const mockCast = { hash: DUMMY_CAST_HASH, text: "Test cast" };
const errorResponse: DataOrError<typeof mockCast> = {
data: null,
error: { message: "Error" },
};
const successResponse: DataOrError<typeof mockCast> = {
data: mockCast,
error: null,
};

mockService.getCastByHash
.mockResolvedValueOnce(errorResponse)
.mockResolvedValueOnce(errorResponse)
.mockResolvedValueOnce(successResponse);

const result = await sdk.getCastByHash(mockCast.hash);

expect(result).toEqual(successResponse);
expect(mockService.getCastByHash).toHaveBeenCalledTimes(3);
});

test("should retry getCastByUrl on error", async () => {
const mockCast = { url: "https://example.com/cast/123", text: "Test cast" };
const errorResponse: DataOrError<typeof mockCast> = {
data: null,
error: { message: "Error" },
};
const successResponse: DataOrError<typeof mockCast> = {
data: mockCast,
error: null,
};

mockService.getCastByUrl
.mockResolvedValueOnce(errorResponse)
.mockResolvedValueOnce(errorResponse)
.mockResolvedValueOnce(successResponse);

const result = await sdk.getCastByUrl(mockCast.url);

expect(result).toEqual(successResponse);
expect(mockService.getCastByUrl).toHaveBeenCalledTimes(3);
});

test("should retry neynar custom query on error", async () => {
const mockCustomQuery = vi.fn();
mockCustomQuery
.mockResolvedValueOnce({ data: null, error: { message: "Error 1" } })
.mockResolvedValueOnce({ data: null, error: { message: "Error 2" } })
.mockResolvedValueOnce({ data: { message: "success" }, error: null });

vi.mocked(services.neynar).mockImplementation(() => ({
customQuery: mockCustomQuery,
}));

const result = await sdk.neynar("/test-endpoint", { param1: "value" });

expect(result).toEqual({ data: { message: "success" }, error: null });
expect(mockCustomQuery).toHaveBeenCalledTimes(3);
});

test("should retry airstack custom query on error", async () => {
const mockCustomQuery = vi.fn();
mockCustomQuery
.mockResolvedValueOnce({ data: null, error: { message: "Error 1" } })
.mockResolvedValueOnce({ data: null, error: { message: "Error 2" } })
.mockResolvedValueOnce({ data: { message: "success" }, error: null });

vi.mocked(services.airstack).mockImplementation(() => ({
customQuery: mockCustomQuery,
}));

const result = await sdk.airstack("query { example }", { param1: "value" });

expect(result).toEqual({ data: { message: "success" }, error: null });
expect(mockCustomQuery).toHaveBeenCalledTimes(3);
});
});

0 comments on commit 047020f

Please sign in to comment.