Skip to content

Commit

Permalink
Update dependencies and add new tests for createClient function
Browse files Browse the repository at this point in the history
  • Loading branch information
117 committed Mar 25, 2024
1 parent e5ef624 commit 795088c
Show file tree
Hide file tree
Showing 3 changed files with 136 additions and 71 deletions.
178 changes: 116 additions & 62 deletions factory/createClient.test.ts
Original file line number Diff line number Diff line change
@@ -1,70 +1,124 @@
import { assert } from "https://deno.land/[email protected]/assert/mod.ts";
import { websocket } from "../api/marketData.ts";
import { assert } from "https://deno.land/[email protected]/assert/assert.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("creates a client with trade and marketData APIs", () => {
const { trade, marketData } = createClient({
keyId: "test",
secretKey: "test",
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(trade !== undefined, "trade");
assert(websocket !== undefined, "marketData");
assert(
typeof trade.rest.v2.account.get === "function",
"trade.v2.account.get"
);
assert(
typeof marketData.rest.v2.stocks.getConditionCodes === "function",
"marketData.v2.stocks.getConditionCodes"
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", () => {
assertThrows(
() => {
createClient({
// deno-lint-ignore ban-ts-comment
// @ts-expect-error
baseURL: "https://invalid-url.com",
keyId: "EXAMPLE_KEY_ID",
secretKey: "EXAMPLE_KEY_SECRET",
});
},
Error,
"invalid base URL"
);
});

// Deno.test("createClient - request handles parameters and data", async () => {
// const client = createClient({
// keyId: "testKeyId",
// secretKey: "testSecretKey",
// baseURL: "https://example.com",
// });

// const response = await client.rest.trade({
// path: "/test/{param}",
// params: { param: "value" },
// data: { test: "data" },
// });

// assertEquals(response.finalPath, "/test/value");
// assertEquals(response.data, { test: "data" });
// });

// Deno.test("createClient - throws error on API call failure", async () => {
// const client = createClient({
// keyId: "testKeyId",
// secretKey: "testSecretKey",
// baseURL: "https://example.com",
// });

// await assertRejects(
// () =>
// client.rest.trade({
// path: "/invalid-endpoint",
// }),
// Error,
// "API call failed"
// );
// });

// Deno.test("createClient - constructs websocket clients correctly", () => {
// const { websocket } = createClient({
// keyId: "PK1OHDJBZQ6J5HQJZBXX",
// secretKey: "7ntdrZayazQkRxINbLWcn4ib0Nv58AlTQH0IqzbQ",
// baseURL: "https://paper-api.alpaca.markets",
// });

// assert(websocket.trade instanceof TradeWebSocket);
// assert(websocket.marketData.stock instanceof StockDataWebSocket);
// assert(websocket.marketData.crypto instanceof CryptoWebSocket);
// assert(websocket.marketData.news instanceof NewsWebSocket);
// assert(websocket.marketData.options instanceof OptionsWebSocket);
// });
Deno.test("should use the provided token bucket options", () => {
const tokenBucketOptions = {
capacity: 100,
fillRate: 2,
};

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

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;
});
21 changes: 12 additions & 9 deletions factory/createClient.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import marketData from "../api/marketData.ts";
import trade from "../api/trade.ts";

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

export type Trade = ReturnType<typeof trade>;

export type MarketData = ReturnType<typeof marketData>;

// Infer the client type based on the base URL
Expand Down Expand Up @@ -34,9 +32,14 @@ export type ClientContext = {
request: <T>(options: RequestOptions) => Promise<T>;
};

export type ClientWithContext<T extends keyof ClientFactoryMap> =
ClientFactoryMap[T] & {
_context: ClientContext;
};

export function createClient<T extends keyof ClientFactoryMap>(
options: CreateClientOptions & { baseURL: T }
): ClientFactoryMap[T] {
): ClientWithContext<T> {
// Create a token bucket for rate limiting
const bucket = createTokenBucket(options.tokenBucket);

Expand All @@ -60,7 +63,6 @@ export function createClient<T extends keyof ClientFactoryMap>(

// Hold the final path
let modified = path;

if (params) {
// Replace path parameters with actual values
for (const [key, value] of Object.entries(params)) {
Expand Down Expand Up @@ -89,7 +91,6 @@ export function createClient<T extends keyof ClientFactoryMap>(
`Failed to ${method} ${url}: ${response.status} ${response.statusText}`
);
}

return response.json();
});
};
Expand All @@ -100,15 +101,17 @@ export function createClient<T extends keyof ClientFactoryMap>(
request,
};

// Conditonally return client based on the base URL
const factory = (context: ClientContext): ClientFactoryMap[T] => {
// Conditionally return client based on the base URL
const factory = (context: ClientContext): ClientWithContext<T> => {
let client: ClientFactoryMap[T];
if (options.baseURL === "https://paper-api.alpaca.markets") {
return trade(context) as ClientFactoryMap[T];
client = trade(context) as ClientFactoryMap[T];
} else if (options.baseURL === "https://data.alpaca.markets") {
return marketData(context) as ClientFactoryMap[T];
client = marketData(context) as ClientFactoryMap[T];
} else {
throw new Error("invalid base URL");
}
return Object.assign(client, { _context: context });
};

return factory(context);
Expand Down
8 changes: 8 additions & 0 deletions utils/mockFetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const mockFetch =
// deno-lint-ignore no-explicit-any
(response: any) => (_url: string, _init?: RequestInit) => {
return Promise.resolve({
ok: true,
json: () => Promise.resolve(response),
} as Response);
};

0 comments on commit 795088c

Please sign in to comment.