Skip to content

Commit

Permalink
feat: watchStatus action (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
horsefacts authored Dec 20, 2023
1 parent 483c7f3 commit 216aa3c
Show file tree
Hide file tree
Showing 9 changed files with 176 additions and 8 deletions.
6 changes: 4 additions & 2 deletions apps/relay/src/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export async function authenticate(request: FastifyRequest<{ Body: AuthenticateR
signature,
});
if (update.isOk()) {
reply.send(update.value);
reply.code(201).send(update.value);
} else {
reply.code(500).send({ error: update.error.message });
}
Expand All @@ -94,8 +94,10 @@ export async function status(request: FastifyRequest, reply: FastifyReply) {
if (close.isErr()) {
reply.code(500).send({ error: close.error.message });
}
reply.code(200).send(res);
} else {
reply.code(202).send(res);
}
reply.send(res);
} else {
if (channel.error.errCode === "not_found") reply.code(401).send({ error: "Unauthorized" });
reply.code(500).send({ error: channel.error.message });
Expand Down
8 changes: 4 additions & 4 deletions apps/relay/src/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ describe("relay server", () => {
const response = await http.post(getFullUrl("/v1/connect/authenticate"), authenticateParams, {
headers: { Authorization: `Bearer ${channelToken}` },
});
expect(response.status).toBe(200);
expect(response.status).toBe(201);
});

test("POST with invalid token", async () => {
Expand Down Expand Up @@ -314,7 +314,7 @@ describe("relay server", () => {
const response = await http.get(getFullUrl("/v1/connect/status"), {
headers: { Authorization: `Bearer ${channelToken}` },
});
expect(response.status).toBe(200);
expect(response.status).toBe(202);

const { state, nonce, ...rest } = response.data;
expect(state).toBe("pending");
Expand Down Expand Up @@ -370,11 +370,11 @@ describe("relay server", () => {
};

response = await http.get(getFullUrl("/v1/connect/status"), authHeaders);
expect(response.status).toBe(200);
expect(response.status).toBe(202);
expect(response.data.state).toBe("pending");

response = await http.post(getFullUrl("/v1/connect/authenticate"), authenticateParams, authHeaders);
expect(response.status).toBe(200);
expect(response.status).toBe(201);

response = await http.get(getFullUrl("/v1/connect/status"), authHeaders);
expect(response.status).toBe(200);
Expand Down
2 changes: 1 addition & 1 deletion packages/connect/src/actions/app/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,5 @@ interface StatusAPIResponse {
const path = "connect/status";

export const status = async (client: Client, { channelToken }: StatusArgs): StatusResponse => {
return get(client, path, { authToken: channelToken });
return get<StatusAPIResponse>(client, path, { authToken: channelToken });
};
41 changes: 41 additions & 0 deletions packages/connect/src/actions/app/watchStatus.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { createAppClient } from "../../clients/createAppClient";
import { jest } from "@jest/globals";
import { viem } from "../../clients/ethereum/viem";

describe("status", () => {
const client = createAppClient({
relayURI: "https://connect.farcaster.xyz",
ethereum: viem(),
});

afterEach(() => {
jest.restoreAllMocks();
});

test("", async () => {
const pending1 = new Response(JSON.stringify({ state: "pending" }), {
status: 202,
});
const pending2 = new Response(JSON.stringify({ state: "pending" }), {
status: 202,
});
const completed = new Response(JSON.stringify({ state: "completed" }));
const fetchSpy = jest
.spyOn(global, "fetch")
.mockResolvedValueOnce(pending1)
.mockResolvedValueOnce(pending2)
.mockResolvedValueOnce(completed);

const callbackSpy = jest.fn();

const res = await client.watchStatus({
channelToken: "some-channel-token",
onResponse: callbackSpy,
});

expect(res.response.status).toEqual(200);
expect(res.data).toEqual({ state: "completed" });
expect(fetchSpy).toHaveBeenCalledTimes(3);
expect(callbackSpy).toHaveBeenCalledTimes(2);
});
});
41 changes: 41 additions & 0 deletions packages/connect/src/actions/app/watchStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Client } from "../../clients/createClient";
import { AsyncHttpResponse, poll, HttpResponse } from "../../clients/transports/http";

export interface WatchStatusArgs {
channelToken: string;
timeout?: number;
interval?: number;
onResponse?: (response: HttpResponse<StatusAPIResponse>) => void;
}

export type WatchStatusResponse = AsyncHttpResponse<StatusAPIResponse>;

interface StatusAPIResponse {
state: "pending" | "completed";
nonce: string;
connectURI: string;
message?: string;
signature?: `0x${string}`;
fid?: number;
username?: string;
bio?: string;
displayName?: string;
pfpUrl?: string;
}

const path = "connect/status";

const voidCallback = () => {};

export const watchStatus = async (client: Client, args: WatchStatusArgs): WatchStatusResponse => {
return poll<StatusAPIResponse>(
client,
path,
{
timeout: args?.timeout ?? 10000,
interval: args?.interval ?? 1000,
onResponse: args?.onResponse ?? voidCallback,
},
{ authToken: args.channelToken },
);
};
3 changes: 3 additions & 0 deletions packages/connect/src/clients/createAppClient.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { connect, ConnectArgs, ConnectResponse } from "../actions/app/connect";
import { status, StatusArgs, StatusResponse } from "../actions/app/status";
import { watchStatus, WatchStatusArgs, WatchStatusResponse } from "../actions/app/watchStatus";
import {
verifySignInMessage,
VerifySignInMessageArgs,
Expand All @@ -10,6 +11,7 @@ import { Client, CreateClientArgs, createClient } from "./createClient";
export interface AppClient extends Client {
connect: (args: ConnectArgs) => ConnectResponse;
status: (args: StatusArgs) => StatusResponse;
watchStatus: (args: WatchStatusArgs) => WatchStatusResponse;
verifySignInMessage: (args: VerifySignInMessageArgs) => VerifySignInMessageResponse;
}

Expand All @@ -19,6 +21,7 @@ export const createAppClient = (config: CreateClientArgs): AppClient => {
...client,
connect: (args: ConnectArgs) => connect(client, args),
status: (args: StatusArgs) => status(client, args),
watchStatus: (args: WatchStatusArgs) => watchStatus(client, args),
verifySignInMessage: (args: VerifySignInMessageArgs) => verifySignInMessage(client, args),
};
};
4 changes: 4 additions & 0 deletions packages/connect/src/clients/createClient.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { createClient, Client } from "./createClient";
import { viem } from "./ethereum/viem";

describe("createClient", () => {
const ethereum = viem();
const config = {
relayURI: "https://connect.farcaster.xyz",
ethereum,
};

let client: Client;
Expand Down Expand Up @@ -41,6 +44,7 @@ describe("createClient", () => {
relayURI: "https://connect.farcaster.xyz",
version: "v2",
},
ethereum,
});
});
});
40 changes: 39 additions & 1 deletion packages/connect/src/clients/transports/http.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { createClient } from "../createClient";
import { get, post } from "./http";
import { viem } from "../ethereum/viem";
import { get, poll, post } from "./http";
import { jest } from "@jest/globals";

describe("http", () => {
const config = {
relayURI: "https://connect.farcaster.xyz",
ethereum: viem(),
};

const client = createClient(config);
Expand Down Expand Up @@ -107,4 +109,40 @@ describe("http", () => {
});
});
});

describe("poll", () => {
test("polls for success response", async () => {
const accepted1 = new Response(JSON.stringify({ state: "pending" }), {
status: 202,
});
const accepted2 = new Response(JSON.stringify({ state: "pending" }), {
status: 202,
});
const ok = new Response(JSON.stringify({ state: "completed" }), {
status: 200,
});

const spy = jest
.spyOn(global, "fetch")
.mockResolvedValueOnce(accepted1)
.mockResolvedValueOnce(accepted2)
.mockResolvedValueOnce(ok);

const res = await poll(client, "path");

expect(spy).toHaveBeenCalledTimes(3);
expect(res.response.status).toBe(200);
expect(res.data).toEqual({ state: "completed" });
});

test("times out", async () => {
const accepted = new Response(JSON.stringify({ state: "pending" }), {
status: 202,
});

jest.spyOn(global, "fetch").mockResolvedValue(accepted);

await expect(poll(client, "path", { timeout: 1, interval: 1 })).rejects.toThrow("Polling timed out after 1ms");
});
});
});
39 changes: 39 additions & 0 deletions packages/connect/src/clients/transports/http.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,32 @@
import { time } from "console";
import { Client } from "../createClient";

export interface HttpOpts {
authToken?: string;
headers?: Record<string, string>;
}

export interface PollOpts<ResponseDataType> {
interval?: number;
timeout?: number;
successCode?: number;
onResponse?: (response: HttpResponse<ResponseDataType>) => void;
}

export interface HttpResponse<ResponseDataType> {
response: Response;
data: ResponseDataType;
}

export type AsyncHttpResponse<T> = Promise<HttpResponse<T>>;

const defaultPollOpts = {
interval: 1000,
timeout: 10000,
successCode: 200,
onResponse: () => {},
};

export const get = async <ResponseDataType>(
client: Client,
path: string,
Expand Down Expand Up @@ -39,6 +54,30 @@ export const post = async <BodyType, ResponseDataType>(
return { response, data };
};

export const poll = async <ResponseDataType>(
client: Client,
path: string,
pollOpts?: PollOpts<ResponseDataType>,
opts?: HttpOpts,
): Promise<HttpResponse<ResponseDataType>> => {
const { timeout, interval, successCode, onResponse } = {
...defaultPollOpts,
...pollOpts,
};

const deadline = Date.now() + timeout;

while (Date.now() < deadline) {
const res = await get<ResponseDataType>(client, path, opts);
if (res.response.status === successCode) {
return res;
}
onResponse(res);
await new Promise((resolve) => setTimeout(resolve, interval));
}
throw new Error(`Polling timed out after ${timeout}ms`);
};

const getURI = (client: Client, path: string) => {
return `${client.config.relayURI}/${client.config.version}/${path}`;
};
Expand Down

0 comments on commit 216aa3c

Please sign in to comment.