Skip to content

Commit

Permalink
Refactor fetch function and add responseType parameter
Browse files Browse the repository at this point in the history
  • Loading branch information
117 committed Mar 25, 2024
1 parent 795088c commit 7448e4d
Show file tree
Hide file tree
Showing 5 changed files with 213 additions and 133 deletions.
195 changes: 106 additions & 89 deletions factory/createClient.test.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,38 @@
import { assert } from "https://deno.land/[email protected]/assert/assert.ts";
import { assertEquals } from "https://deno.land/[email protected]/assert/assert_equals.ts";
import { assertThrows } from "https://deno.land/[email protected]/assert/assert_throws.ts";
import { mockFetch } from "../utils/mockFetch.ts";
import { createClient } from "./createClient.ts";

Deno.test("should create a trade client with valid options", () => {
const client = createClient({
baseURL: "https://paper-api.alpaca.markets",
keyId: "EXAMPLE_KEY_ID",
secretKey: "EXAMPLE_KEY_SECRET",
});

assert(client.account !== undefined);
assert(client.orders.create !== undefined);
});

Deno.test("should create a market data client with valid options", () => {
const client = createClient({
baseURL: "https://data.alpaca.markets",
keyId: "EXAMPLE_KEY_ID",
secretKey: "EXAMPLE_KEY_SECRET",
});

assert(client.rest.v1beta1 !== undefined);
assert(client.rest.v1beta3 !== undefined);
});

Deno.test("should throw an error with an invalid base URL", () => {
Deno.test(
"createClient should create a trade client with valid options",
() => {
const client = createClient({
baseURL: "https://paper-api.alpaca.markets",
keyId: "EXAMPLE_KEY_ID",
secretKey: "EXAMPLE_KEY_SECRET",
});

assert(client.account !== undefined);
assert(client.orders.create !== undefined);
}
);

Deno.test(
"createClient should create a market data client with valid options",
() => {
const client = createClient({
baseURL: "https://data.alpaca.markets",
keyId: "EXAMPLE_KEY_ID",
secretKey: "EXAMPLE_KEY_SECRET",
});

assert(client.rest.v1beta1 !== undefined);
assert(client.rest.v1beta3 !== undefined);
}
);

Deno.test("createClient should throw an error with an invalid base URL", () => {
assertThrows(
() => {
createClient({
Expand All @@ -41,7 +48,7 @@ Deno.test("should throw an error with an invalid base URL", () => {
);
});

Deno.test("should use the provided token bucket options", () => {
Deno.test("createClient should use the provided token bucket options", () => {
const tokenBucketOptions = {
capacity: 100,
fillRate: 2,
Expand All @@ -57,68 +64,78 @@ Deno.test("should use the provided token bucket options", () => {
assert(client._context.options.tokenBucket === tokenBucketOptions);
});

Deno.test("should use default token bucket options if not provided", () => {
const client = createClient({
baseURL: "https://paper-api.alpaca.markets",
keyId: "EXAMPLE_KEY_ID",
secretKey: "EXAMPLE_KEY_SECRET",
});

assert(client._context.options.tokenBucket === undefined);
});

Deno.test("should make a request with the correct options", async () => {
const mockResponse = { mock: "data" };
const originalFetch = globalThis.fetch;
// deno-lint-ignore ban-ts-comment
// @ts-expect-error
globalThis.fetch = mockFetch(mockResponse);

const client = createClient({
baseURL: "https://paper-api.alpaca.markets",
keyId: "EXAMPLE_KEY_ID",
secretKey: "EXAMPLE_KEY_SECRET",
});

const response = await client._context.request({
path: "/v2/account",
});

assert(response === mockResponse);

globalThis.fetch = originalFetch;
});

Deno.test("should throttle requests based on token bucket", async () => {
const mockResponse = { mock: "data" };
const originalFetch = globalThis.fetch;

// deno-lint-ignore ban-ts-comment
// @ts-expect-error
globalThis.fetch = mockFetch(mockResponse);

const client = createClient({
baseURL: "https://paper-api.alpaca.markets",
keyId: "EXAMPLE_KEY_ID",
secretKey: "EXAMPLE_KEY_SECRET",
tokenBucket: {
capacity: 2,
fillRate: 1,
},
});

const startTime = Date.now();

await Promise.all([
client._context.request({ path: "/v2/account" }),
client._context.request({ path: "/v2/account" }),
client._context.request({ path: "/v2/account" }),
]);

const endTime = Date.now();
const elapsedTime = endTime - startTime;

assert(elapsedTime >= 2000, "Requests should be throttled");

globalThis.fetch = originalFetch;
});
Deno.test(
"createClient should use default token bucket options if not provided",
() => {
const client = createClient({
baseURL: "https://paper-api.alpaca.markets",
keyId: "EXAMPLE_KEY_ID",
secretKey: "EXAMPLE_KEY_SECRET",
});

assert(client._context.options.tokenBucket === undefined);
}
);

Deno.test(
"createClient should make a request with the correct options",
async () => {
const mockResponse = { mock: "data" };
const originalFetch = globalThis.fetch;
// deno-lint-ignore ban-ts-comment
// @ts-expect-error
globalThis.fetch = mockFetch(mockResponse);

const client = createClient({
baseURL: "https://paper-api.alpaca.markets",
keyId: "EXAMPLE_KEY_ID",
secretKey: "EXAMPLE_KEY_SECRET",
});

const response = await client._context.request<typeof mockResponse>({
path: "/v2/account",
});

assertEquals(response.ok, true);
assertEquals(response.data, mockResponse);

globalThis.fetch = originalFetch;
}
);

Deno.test(
"createClient should throttle requests based on token bucket",
async () => {
const mockResponse = { mock: "data" };
const originalFetch = globalThis.fetch;

// deno-lint-ignore ban-ts-comment
// @ts-expect-error
globalThis.fetch = mockFetch(mockResponse);

const client = createClient({
baseURL: "https://paper-api.alpaca.markets",
keyId: "EXAMPLE_KEY_ID",
secretKey: "EXAMPLE_KEY_SECRET",
tokenBucket: {
capacity: 2,
fillRate: 1,
},
});

const startTime = Date.now();

await Promise.all([
client._context.request({ path: "/v2/account" }),
client._context.request({ path: "/v2/account" }),
client._context.request({ path: "/v2/account" }),
]);

const endTime = Date.now();
const elapsedTime = endTime - startTime;

assert(elapsedTime >= 2000, "Requests should be throttled");

globalThis.fetch = originalFetch;
}
);
17 changes: 11 additions & 6 deletions factory/createClient.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import marketData from "../api/marketData.ts";
import trade from "../api/trade.ts";

import { TokenBucketOptions, createTokenBucket } from "./createTokenBucket.ts";

export type Trade = ReturnType<typeof trade>;
Expand All @@ -13,11 +14,13 @@ type ClientFactoryMap = {

export type Client = Trade | MarketData;

type RequestOptions = {
type RequestOptions<T> = {
method?: string;
path: string;
// deno-lint-ignore no-explicit-any
params?: Record<string, any>;
data?: object;
responseType?: T;
};

type CreateClientOptions = {
Expand All @@ -29,7 +32,7 @@ type CreateClientOptions = {

export type ClientContext = {
options: CreateClientOptions;
request: <T>(options: RequestOptions) => Promise<T>;
request: <T>(options: RequestOptions<T>) => Promise<Response & { data: T }>;
};

export type ClientWithContext<T extends keyof ClientFactoryMap> =
Expand All @@ -49,7 +52,8 @@ export function createClient<T extends keyof ClientFactoryMap>(
path,
params,
data,
}: RequestOptions): Promise<T> => {
responseType,
}: RequestOptions<T>): Promise<Response & { data: T }> => {
await new Promise((resolve) => {
// Poll the token bucket every second
const timer = setInterval(() => {
Expand Down Expand Up @@ -80,18 +84,19 @@ export function createClient<T extends keyof ClientFactoryMap>(
"Content-Type": "application/json",
});

// Make the request and parse the JSON response
// Make the request and return the Response object
return fetch(url, {
method,
headers,
body: data ? JSON.stringify(data) : null,
}).then((response) => {
}).then(async (response) => {
if (!response.ok) {
throw new Error(
`Failed to ${method} ${url}: ${response.status} ${response.statusText}`
);
}
return response.json();
const responseData = await response.json();
return Object.assign(response, { data: responseData as T });
});
};

Expand Down
78 changes: 45 additions & 33 deletions factory/createTokenBucket.test.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,59 @@
import { assert } from "https://deno.land/[email protected]/assert/assert.ts";
import { createTokenBucket } from "./createTokenBucket.ts";

Deno.test("should allow taking tokens within capacity", () => {
const tokenBucket = createTokenBucket({
capacity: 200,
fillRate: 3,
});

assert(tokenBucket.take(50) === true);
});
Deno.test(
"createTokenBucket should allow taking tokens within capacity",
() => {
const tokenBucket = createTokenBucket({
capacity: 200,
fillRate: 3,
});

assert(tokenBucket.take(50) === true);
}
);

Deno.test("should reject taking tokens beyond capacity", () => {
const tokenBucket = createTokenBucket({
capacity: 200,
fillRate: 3,
});
Deno.test(
"createTokenBucket should reject taking tokens beyond capacity",
() => {
const tokenBucket = createTokenBucket({
capacity: 200,
fillRate: 3,
});

assert(tokenBucket.take(300) === false);
});
assert(tokenBucket.take(300) === false);
}
);

Deno.test("should refill tokens based on fill rate", async () => {
const tokenBucket = createTokenBucket({
capacity: 200,
fillRate: 3,
});
Deno.test(
"createTokenBucket should refill tokens based on fill rate",
async () => {
const tokenBucket = createTokenBucket({
capacity: 200,
fillRate: 3,
});

tokenBucket.take(50);
tokenBucket.take(50);

await new Promise((resolve) => setTimeout(resolve, 3000));
await new Promise((resolve) => setTimeout(resolve, 3000));

assert(tokenBucket.take(50) === true);
});
assert(tokenBucket.take(50) === true);
}
);

Deno.test("should support 200 requests per 60 seconds", async () => {
const tokenBucket = createTokenBucket({ capacity: 200, fillRate: 3 });
Deno.test(
"createTokenBucket should support 200 requests per 60 seconds",
() => {
const tokenBucket = createTokenBucket({ capacity: 200, fillRate: 3 });

let successfulRequests = 0;
let successfulRequests = 0;

for (let i = 0; i < 200; i++) {
if (tokenBucket.take(1)) {
successfulRequests++;
for (let i = 0; i < 200; i++) {
if (tokenBucket.take(1)) {
successfulRequests++;
}
}
}

assert(successfulRequests === 200);
});
assert(successfulRequests === 200);
}
);
Loading

0 comments on commit 7448e4d

Please sign in to comment.