Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 55 additions & 1 deletion lazer/sdk/js/README.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
2 changes: 1 addition & 1 deletion lazer/sdk/js/examples/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import { PythLazerClient } from "../src/index.js";

// Ignore debug messages
console.debug = () => {};
console.debug = () => { };

const client = await PythLazerClient.create({
urls: [
Expand Down
2 changes: 1 addition & 1 deletion lazer/sdk/js/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@pythnetwork/pyth-lazer-sdk",
"version": "2.0.0",
"version": "2.1.0",
"description": "Pyth Lazer SDK",
"publishConfig": {
"access": "public"
Expand Down
96 changes: 89 additions & 7 deletions lazer/sdk/js/src/socket/websocket-pool.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is available as a query param, why use bearer auth at all? Seems like if you have to support query param auth it's simpler to just always use the query param and never set the header...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeahh, considered that, but we prefer people use the header whenever possible to avoid leaking the token in logs/some nginx somewhere/etc.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah yeah that's fair


// 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<string, string> | undefined),
Authorization: `Bearer ${token}`,
},
},
};
}
}

const DEFAULT_NUM_CONNECTIONS = 4;

export type WebSocketPoolConfig = {
Expand Down Expand Up @@ -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,
});
Expand Down
Loading