diff --git a/lazer/sdk/js/README.md b/lazer/sdk/js/README.md index 65e587c6c9..d5cae31334 100644 --- a/lazer/sdk/js/README.md +++ b/lazer/sdk/js/README.md @@ -1,4 +1,58 @@ -# pyth-lazer-sdk - Readme +# Pyth Lazer JavaScript SDK + +A JavaScript/TypeScript SDK for connecting to the Pyth Lazer service, supporting both Node and browser environments. + + +## Quick Start +Install with: +```sh +npm install @pythnetwork/pyth-lazer-sdk +``` + +Connect to Lazer and process the messages: +```javascript +import { PythLazerClient } from '@pythnetwork/pyth-lazer-sdk'; + +const client = await PythLazerClient.create({ + urls: ['wss://your-lazer-endpoint/v1/stream'], + token: 'your-access-token', + numConnections: 3 +}); + +// Register an event handler for each price update message. +client.addMessageListener((message) => { + console.log('Received:', message); +}); + +// Subscribe to a feed. You can call subscribe() multiple times. +client.subscribe({ + type: "subscribe", + subscriptionId: 1, + priceFeedIds: [1, 2], + properties: ["price"], + formats: ["solana"], + deliveryFormat: "binary", + channel: "fixed_rate@200ms", + parsed: false, + jsonBinaryEncoding: "base64", +}); +``` + +For a full demo, run the example in `examples/index.ts` with: +``` +pnpm run example +``` +### Build locally + +Build ESM and CJS packages with: + +```sh +pnpm turbo build -F @pythnetwork/pyth-lazer-sdk +``` + +## API Reference + +For detailed API documentation, see the [TypeDoc documentation](docs/typedoc/). ## Contributing & Development diff --git a/lazer/sdk/js/examples/index.ts b/lazer/sdk/js/examples/index.ts index f6170b542e..e86febb4cb 100644 --- a/lazer/sdk/js/examples/index.ts +++ b/lazer/sdk/js/examples/index.ts @@ -4,7 +4,7 @@ import { PythLazerClient } from "../src/index.js"; // Ignore debug messages -console.debug = () => {}; +console.debug = () => { }; const client = await PythLazerClient.create({ urls: [ diff --git a/lazer/sdk/js/package.json b/lazer/sdk/js/package.json index 6c412d5281..7f209389df 100644 --- a/lazer/sdk/js/package.json +++ b/lazer/sdk/js/package.json @@ -1,6 +1,6 @@ { "name": "@pythnetwork/pyth-lazer-sdk", - "version": "2.0.0", + "version": "2.1.0", "description": "Pyth Lazer SDK", "publishConfig": { "access": "public" diff --git a/lazer/sdk/js/src/socket/websocket-pool.ts b/lazer/sdk/js/src/socket/websocket-pool.ts index 6e57811c0c..23a6b0c701 100644 --- a/lazer/sdk/js/src/socket/websocket-pool.ts +++ b/lazer/sdk/js/src/socket/websocket-pool.ts @@ -1,3 +1,5 @@ +import type { ClientRequestArgs } from "node:http"; + import TTLCache from "@isaacs/ttlcache"; import type { ErrorEvent } from "isomorphic-ws"; import WebSocket from "isomorphic-ws"; @@ -8,6 +10,84 @@ import type { Request, Response } from "../protocol.js"; import type { ResilientWebSocketConfig } from "./resilient-websocket.js"; import { ResilientWebSocket } from "./resilient-websocket.js"; +/** + * Browser global interface for proper typing + */ +type BrowserGlobal = { + window?: { + document?: unknown; + }; + importScripts?: (...urls: string[]) => void; +} + +/** + * Detects if the code is running in a regular DOM or Web Worker context. + * @returns true if running in a DOM or Web Worker context, false if running in Node.js + */ +function isBrowser(): boolean { + try { + // Check for browser's window object (DOM context) + if (typeof globalThis !== "undefined") { + const global = globalThis as BrowserGlobal; + if (global.window?.document) { + return true; + } + + // Check for Web Worker context (has importScripts but no window) + if (typeof global.importScripts === "function" && !global.window) { + return true; + } + } + + // Node.js environment + return false; + } catch { + // If any error occurs, assume Node.js environment + return false; + } +} + +/** + * Adds authentication to either the URL as a query parameter or as an Authorization header. + * + * Browsers don't support custom headers for WebSocket connections, so if a browser is detected, + * the token is added as a query parameter instead. Else, the token is added as an Authorization header. + * + * @param url - The WebSocket URL + * @param token - The authentication token + * @param wsOptions - Existing WebSocket options + * @returns Object containing the modified endpoint and wsOptions + */ +function addAuthentication( + url: string, + token: string, + wsOptions: WebSocket.ClientOptions | ClientRequestArgs | undefined = {} +): { endpoint: string; wsOptions: WebSocket.ClientOptions | ClientRequestArgs | undefined } { + if (isBrowser()) { + // Browser: Add token as query parameter + const urlObj = new URL(url); + urlObj.searchParams.set("ACCESS_TOKEN", token); + + // For browsers, filter out wsOptions since headers aren't supported + return { + endpoint: urlObj.toString(), + wsOptions: undefined, + }; + } else { + // Node.js: Add Authorization header + return { + endpoint: url, + wsOptions: { + ...wsOptions, + headers: { + ...(wsOptions.headers as Record | undefined), + Authorization: `Bearer ${token}`, + }, + }, + }; + } +} + const DEFAULT_NUM_CONNECTIONS = 4; export type WebSocketPoolConfig = { @@ -63,15 +143,17 @@ export class WebSocketPool { if (!url) { throw new Error(`URLs must not be null or empty`); } - const wsOptions = { - ...config.rwsConfig?.wsOptions, - headers: { - Authorization: `Bearer ${config.token}`, - }, - }; + + // Apply authentication based on environment (browser vs Node.js) + const { endpoint, wsOptions } = addAuthentication( + url, + config.token, + config.rwsConfig?.wsOptions + ); + const rws = new ResilientWebSocket({ ...config.rwsConfig, - endpoint: url, + endpoint, wsOptions, logger, });