diff --git a/factory/createClient.ts b/factory/createClient.ts index 95e4698..a147f78 100644 --- a/factory/createClient.ts +++ b/factory/createClient.ts @@ -1,71 +1,115 @@ -import { TradingAPI } from "../api/trading.ts"; +import marketData from "../api/marketData.ts"; +import trade from "../api/trade.ts"; + import { TokenBucketOptions, createTokenBucket } from "./createTokenBucket.ts"; -type ClientOptions = { - apiKey: string; - apiSecret: string; +export type Trade = ReturnType; + +export type MarketData = ReturnType; + +// Infer the client type based on the base URL +type ClientFactoryMap = { + "https://paper-api.alpaca.markets": Trade; + "https://data.alpaca.markets": MarketData; +}; + +export type Client = Trade | MarketData; + +type RequestOptions = { + method?: string; + path: string; + params?: Record; + data?: object; +}; + +type CreateClientOptions = { + keyId: string; + secretKey: string; baseURL: string; tokenBucket?: TokenBucketOptions; }; -type Endpoint = keyof TradingAPI; -type Method = TradingAPI[Endpoint]["method"]; -type Params = TradingAPI[E]["params"]; -type Response = TradingAPI[E]["response"]; - -function createClient(options: ClientOptions) { - const tokenBucket = options.tokenBucket - ? createTokenBucket(options.tokenBucket) - : undefined; - - const call = async ( - method: Method, - endpoint: E, - params?: Params - ): Promise> => { - if (tokenBucket && !tokenBucket.take(1)) { - throw new Error("Rate limit exceeded"); - } +export type ClientContext = { + options: CreateClientOptions; + request: (options: RequestOptions) => Promise; +}; + +export function createClient( + options: CreateClientOptions & { baseURL: T } +): ClientFactoryMap[T] { + // Create a token bucket for rate limiting + const bucket = createTokenBucket(options.tokenBucket); - const url = new URL(`${options.baseURL}${endpoint}`); - if (params && method === "GET") { - Object.entries(params as Record).forEach( - ([key, value]) => { - url.searchParams.append(key, value); + // Throttled request function that respects the token bucket + const request = async ({ + method = "GET", + path, + params, + data, + }: RequestOptions): Promise => { + await new Promise((resolve) => { + // Poll the token bucket every second + const timer = setInterval(() => { + // If a token is available, resolve the promise + if (bucket.take(1)) { + clearInterval(timer); + resolve(true); } - ); + }, 1000); + }); + + // Hold the final path + let modified = path; + + if (params) { + // Replace path parameters with actual values + for (const [key, value] of Object.entries(params)) { + modified = modified.replace(`{${key}}`, encodeURIComponent(value)); + } } - const response = await fetch(url.toString(), { + // Construct the full URL + const url = `${options.baseURL}${modified}`; + + // Construct the headers + const headers = new Headers({ + "APCA-API-KEY-ID": options.keyId, + "APCA-API-SECRET-KEY": options.secretKey, + "Content-Type": "application/json", + }); + + // Make the request and parse the JSON response + return fetch(url, { method, - headers: { - "APCA-API-KEY-ID": options.apiKey, - "APCA-API-SECRET-KEY": options.apiSecret, - "Content-Type": "application/json", - }, - body: method !== "GET" ? JSON.stringify(params) : undefined, + headers, + body: data ? JSON.stringify(data) : null, + }).then((response) => { + if (!response.ok) { + throw new Error( + `Failed to ${method} ${url}: ${response.status} ${response.statusText}` + ); + } + + return response.json(); }); + }; - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } + // Create a context object to pass to the client factory + const context: ClientContext = { + options, + request, + }; - return (await response.json()) as Response; + // Conditonally return client based on the base URL + const factory = (context: ClientContext): ClientFactoryMap[T] => { + if (options.baseURL === "https://paper-api.alpaca.markets") { + return trade(context) as ClientFactoryMap[T]; + } else if (options.baseURL === "https://data.alpaca.markets") { + return marketData(context) as ClientFactoryMap[T]; + } else { + throw new Error("invalid base URL"); + } }; - return { call }; + return factory(context); } - -const client = createClient({ - apiKey: "your-api-key", - apiSecret: "your-api-secret", - baseURL: "https://api.example.com", - tokenBucket: { - capacity: 200, - fillRate: 3, - }, -}); - -client.call("GET", "/v2/account/activities").then((response) => { - console.log(response); -});