Skip to content

Commit

Permalink
Checkpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
117 committed Mar 27, 2024
1 parent ae5c6f6 commit 77ea842
Show file tree
Hide file tree
Showing 10 changed files with 647 additions and 602 deletions.
Empty file removed src/api/marketData.test.ts
Empty file.
707 changes: 355 additions & 352 deletions src/api/marketData.ts

Large diffs are not rendered by default.

Empty file removed src/api/trade.test.ts
Empty file.
374 changes: 190 additions & 184 deletions src/api/trade.ts

Large diffs are not rendered by default.

Empty file removed src/api/websocket.test.ts
Empty file.
Empty file removed src/api/websocket.ts
Empty file.
31 changes: 15 additions & 16 deletions src/factory/createClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ Deno.test(
() => {
const client = createClient({
baseURL: "https://paper-api.alpaca.markets",
keyId: "EXAMPLE_KEY_ID",
secretKey: "EXAMPLE_KEY_SECRET",
key: "EXAMPLE_KEY_ID",
secret: "EXAMPLE_KEY_SECRET",
});

assert(client.v2.account !== undefined);
Expand All @@ -23,8 +23,8 @@ Deno.test(
() => {
const client = createClient({
baseURL: "https://data.alpaca.markets",
keyId: "EXAMPLE_KEY_ID",
secretKey: "EXAMPLE_KEY_SECRET",
key: "EXAMPLE_KEY_ID",
secret: "EXAMPLE_KEY_SECRET",
});

assert(client.v2.stocks !== undefined);
Expand All @@ -39,8 +39,8 @@ Deno.test("createClient should throw an error with an invalid base URL", () => {
// deno-lint-ignore ban-ts-comment
// @ts-expect-error
baseURL: "https://invalid-url.com",
keyId: "EXAMPLE_KEY_ID",
secretKey: "EXAMPLE_KEY_SECRET",
key: "EXAMPLE_KEY_ID",
secret: "EXAMPLE_KEY_SECRET",
});
},
Error,
Expand All @@ -56,8 +56,8 @@ Deno.test("createClient should use the provided token bucket options", () => {

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

Expand All @@ -69,8 +69,8 @@ Deno.test(
() => {
const client = createClient({
baseURL: "https://paper-api.alpaca.markets",
keyId: "EXAMPLE_KEY_ID",
secretKey: "EXAMPLE_KEY_SECRET",
key: "EXAMPLE_KEY_ID",
secret: "EXAMPLE_KEY_SECRET",
});

assert(client._context.options.tokenBucket === undefined);
Expand All @@ -82,22 +82,22 @@ Deno.test(
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",
key: "EXAMPLE_KEY_ID",
secret: "EXAMPLE_KEY_SECRET",
});

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

assertEquals(response, mockResponse);

globalThis.fetch = originalFetch;
}
);
Expand All @@ -114,8 +114,8 @@ Deno.test(

const client = createClient({
baseURL: "https://paper-api.alpaca.markets",
keyId: "EXAMPLE_KEY_ID",
secretKey: "EXAMPLE_KEY_SECRET",
key: "EXAMPLE_KEY_ID",
secret: "EXAMPLE_KEY_SECRET",
tokenBucket: {
capacity: 2,
fillRate: 1,
Expand All @@ -134,7 +134,6 @@ Deno.test(
const elapsedTime = endTime - startTime;

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

globalThis.fetch = originalFetch;
}
);
115 changes: 68 additions & 47 deletions src/factory/createClient.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,58 @@
import { data, trading } from "../api/trade.ts";
import marketData from "../api/marketData.ts";
import trade from "../api/trade.ts";

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

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

export type CreateClientOptions = {
key?: string;
secret?: string;
baseURL?: string;
accessToken?: string;
token?: string;
baseURL: string;
tokenBucket?: TokenBucketOptions;
};

export type Client = Trade | MarketData;

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

const clientFactoryMap = {
// REST (JSON)
"https://api.alpaca.markets": trade.api,
"https://paper-api.alpaca.markets": trade.api,
"https://data.alpaca.markets": marketData.api,
// WebSocket (binary)
"wss://paper-api.alpaca.markets/stream": trade.websocket,
"wss://api.alpaca.markets/stream": trade.websocket,
// WebSocket (JSON)
"wss://data.alpaca.markets/stream": marketData.websocket,
} as const;

export type ClientFactoryMap = {
[K in keyof typeof clientFactoryMap]: ReturnType<
(typeof clientFactoryMap)[K]
>;
};

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

export type Trade = ReturnType<typeof trading>;
export type MarketData = ReturnType<typeof data>;

// Infer the client type based on the base URL
export type ClientFactoryMap = {
"https://paper-api.alpaca.markets": Trade;
"https://data.alpaca.markets": MarketData;
};

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

Expand All @@ -71,42 +78,63 @@ export function createClient<T extends keyof ClientFactoryMap>(
const url = new URL(path, options.baseURL);

if (params) {
// Add query parameters to the URL
for (const [key, value] of Object.entries(params)) {
url.searchParams.append(key, value);
}
// Append query parameters to the URL
url.search = new URLSearchParams(
Object.entries(params) as [string, string][]
).toString();
}

const {
key = "",
secret = "",
token = "",
} = {
key: options.key || Deno.env.get("APCA_KEY_ID"),
secret: options.secret || Deno.env.get("APCA_KEY_SECRET"),
token: options.token || Deno.env.get("APCA_ACCESS_TOKEN"),
};

// Check if credentials are provided
if (!token && (!key || !secret)) {
throw new Error("Missing credentials (need accessToken or key/secret)");
}

// Construct the headers
const headers = new Headers({
"APCA-API-KEY-ID": options.key || Deno.env.get("APCA_KEY_ID") || "",
"APCA-API-SECRET-KEY":
options.secret || Deno.env.get("APCA_KEY_SECRET") || "",
"Content-Type": "application/json",
});

if (token) {
// Use the access token for authentication
headers.set("Authorization", `Bearer ${token}`);
} else {
// Use the API key and secret for authentication
headers.set("APCA-API-KEY-ID", key);
headers.set("APCA-API-SECRET-KEY", secret);
}

// Make the request
return fetch(url, {
method,
headers,
body: data ? JSON.stringify(data) : null,
}).then(async (response) => {
}).then((response) => {
if (!response.ok) {
throw new Error(
`Failed to ${method} ${url}: ${response.status} ${response.statusText}`
);
}

try {
const jsonData = await response.json();
return Object.assign(response, { data: jsonData as T }).data;
// Parse the response and cast it to the expected type
return response.json() as Promise<T>;
} catch (error) {
if (
error instanceof SyntaxError &&
error.message.includes("Unexpected end of JSON input")
) {
// Return an empty object or a default value instead of throwing an error
return Object.assign(response, { data: {} as T }).data;
return {} as T;
}

// Re-throw other errors
Expand All @@ -121,22 +149,15 @@ export function createClient<T extends keyof ClientFactoryMap>(
request,
};

// 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") {
client = trading(context) as ClientFactoryMap[T];
} else if (options.baseURL === "https://data.alpaca.markets") {
client = data(context) as ClientFactoryMap[T];
} else {
throw new Error("invalid base URL");
}
// Get the client factory function based on the base URL
const clientFactory = clientFactoryMap[options.baseURL as T];

return Object.assign(client, { _context: context });
};
if (!clientFactory) {
throw new Error("Invalid base URL");
}

return factory(context);
}
// Create the client using the client factory function
const client = clientFactory(context) as ClientFactoryMap[T];

// client.<resource>.<method>(options)
return Object.assign(client, { _context: context });
};
16 changes: 15 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,15 @@
// export everything here
export { type TokenBucketOptions } from "./factory/createTokenBucket.ts";

// api/marketData.test.ts
// api/marketData.ts
// api/trade.test.ts
// api/trade.ts
// api/websocket.test.ts
// api/websocket.ts
// factory/createClient.test.ts
// factory/createClient.ts
// factory/createTokenBucket.test.ts ✅
// factory/createTokenBucket.ts ✅
// util/mockFetch.test.ts ✅
// util/mockFetch.ts ✅
// index.ts (this file)
6 changes: 4 additions & 2 deletions src/util/mockFetch.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
export type MockResponse = Response | object | string;
type MockResponse = Response | object | string;

export type MockFetch = (url: string, init?: RequestInit) => Promise<Response>;
type MockFetch = (url: string, init?: RequestInit) => Promise<Response>;

// Used to mock the fetch function in tests
export const mockFetch: (response: MockResponse) => MockFetch =
(response) => (_url, _init?) =>
// Return a promise that resolves with a response
Promise.resolve(
new Response(
typeof response === "object"
Expand Down

0 comments on commit 77ea842

Please sign in to comment.