/** * WebSocket Streaming Resource * * Price updates streamed through the OilPriceAPI ActionCable endpoint * (`wss://api.oilpriceapi.com/cable`). * * Streaming availability depends on account entitlement; review * https://www.oilpriceapi.com/pricing. Connections authenticate with your API * key and subscribe to the `EnergyPricesChannel`, which pushes an initial * `welcome` snapshot followed by `price_update` and, for eligible accounts, * `rig_count_update` messages. * * The implementation speaks the raw ActionCable JSON subprotocol over the * `ws` package: it performs the `welcome` -> `subscribe` -> * `confirm_subscription` handshake, answers server `ping` frames, and * surfaces decoded channel messages as typed events. Auto-reconnect with * exponential backoff keeps the stream alive across transient network drops. * * @example * ```typescript * const sub = client.stream.prices({}, (update) => { * console.log(update.prices.oil.wti?.original_price); * }); * * sub.on("rig_count_update", (m) => console.log(m.rig_count.region, m.rig_count.count)); * sub.on("error", (err) => console.error(err)); * * // later * sub.close(); * ``` */ import { EventEmitter } from "node:events"; import WebSocket from "ws"; import type { OilPriceAPI } from "../client.js"; /** * The ActionCable channel exposed by the OilPriceAPI server. * * Confirmed from `app/channels/energy_prices_channel.rb` * (`class EnergyPricesChannel`). */ export declare const ENERGY_PRICES_CHANNEL = "EnergyPricesChannel"; /** * A single normalized price point as broadcast by the server. * * Mirrors the shape produced by `BroadcastEnergyPricesJob#normalized_price_for_cached` * and `EnergyPricesChannel#normalize_price`. */ export interface StreamedPrice { /** Price converted to the common base unit (USD / MMBtu). */ normalized_price: number | null; /** Original price in its native unit/currency. */ original_price: number | null; /** Native unit (e.g. `"barrel_oil"`, `"mmbtu"`). */ original_unit: string; /** Native currency (e.g. `"USD"`, `"GBP"`, `"EUR"`). */ original_currency: string; /** ISO-8601 timestamp of the underlying price record. */ timestamp: string; /** 24h absolute change (present on the initial `welcome` snapshot). */ change_24h?: number; /** 24h percent change (present on the initial `welcome` snapshot). */ change_24h_percent?: number; } /** * The `prices` map carried by `welcome` and `price_update` messages. */ export interface StreamedPriceMap { oil: { brent: StreamedPrice | null; wti: StreamedPrice | null; }; natural_gas: { uk: StreamedPrice | null; us: StreamedPrice | null; eu: StreamedPrice | null; }; } /** * Live `price_update` broadcast (the most common streamed message). */ export interface PriceUpdateMessage { type: "price_update"; timestamp: string; base_currency: string; base_unit: string; prices: StreamedPriceMap; } /** * Initial snapshot transmitted immediately on subscription confirmation. * * Note: the server uses `type: "welcome"` for this *channel* message. It is * distinct from the ActionCable transport-level `welcome` frame, which the * client consumes internally and never surfaces. */ export interface WelcomeMessage { type: "welcome"; data: { timestamp?: string; base_currency?: string; base_unit?: string; prices?: StreamedPriceMap; /** Present only for drilling-tier accounts. */ drilling_intelligence?: Record; /** Present when the initial snapshot could not be built. */ error?: string; }; } /** * Rig-count update broadcast for accounts with drilling access. */ export interface RigCountUpdateMessage { type: "rig_count_update"; timestamp: string; rig_count: { code: string; region: string; count: number; source: string; updated_at: string; }; } /** * Any decoded channel message. Unknown `type` values are passed through so * forward-compatible servers don't break older SDKs. */ export type StreamMessage = WelcomeMessage | PriceUpdateMessage | RigCountUpdateMessage | { type: string; [key: string]: unknown; }; /** * Options for {@link StreamingResource.prices}. */ export interface StreamPricesOptions { /** * Optional client-side filter. When provided, only `price_update` messages * whose normalized map contains at least one of these commodity slugs are * delivered to `onUpdate` / the `price_update` event. * * Accepts the streamed slugs (`"brent"`, `"wti"`, `"uk"`, `"us"`, `"eu"`) * or the upstream codes (`"BRENT_CRUDE_USD"`, `"WTI_USD"`, * `"NATURAL_GAS_GBP"`, `"NATURAL_GAS_USD"`, `"DUTCH_TTF_EUR"`). * * The server broadcasts the full map regardless; filtering is applied * locally so callers can scope updates without extra config. */ commodities?: string[]; /** Disable automatic reconnection (default: reconnect enabled). */ autoReconnect?: boolean; /** Base reconnect delay in ms (default: 1000). */ reconnectDelay?: number; /** Maximum reconnect delay in ms for the exponential backoff (default: 30000). */ maxReconnectDelay?: number; /** * Maximum number of consecutive reconnect attempts before giving up and * emitting a terminal `error`. `Infinity` to retry forever (default: 10). */ maxReconnectAttempts?: number; } /** Callback invoked for each delivered `price_update` message. */ export type PriceUpdateHandler = (update: PriceUpdateMessage) => void; /** * Handle for an active price stream. * * Extends `EventEmitter`. Emitted events: * - `"connected"` — transport connected and subscription confirmed * - `"welcome"` — initial snapshot ({@link WelcomeMessage}) * - `"price_update"` — live price broadcast ({@link PriceUpdateMessage}) * - `"rig_count_update"` — drilling broadcast ({@link RigCountUpdateMessage}) * - `"message"` — every decoded channel message ({@link StreamMessage}) * - `"reconnecting"` — a reconnect attempt is scheduled (`{ attempt, delay }`) * - `"disconnected"` — transport closed (`{ code, reason }`) * - `"error"` — an `Error` (transport error, unauthorized, or retries exhausted) * - `"close"` — the subscription was closed via {@link PriceStreamSubscription.close} */ export declare class PriceStreamSubscription extends EventEmitter { private readonly wsImpl; private ws; private readonly url; private readonly apiKey; private readonly identifier; private readonly options; private readonly commodityFilter; private closed; private subscribed; private reconnectAttempts; private reconnectTimer; /** * @param url - The `wss://.../cable` endpoint. * @param apiKey - API key sent as the ActionCable `Authorization: Token ` header. * @param options - Reconnect + filter options. * @param wsImpl - Injectable WebSocket constructor (used by tests to mock). * @internal Construct via {@link StreamingResource.prices}. */ constructor(url: string, apiKey: string, options: StreamPricesOptions, wsImpl?: typeof WebSocket); /** * Open the transport and begin the ActionCable handshake. * @internal Called once by {@link StreamingResource.prices}. */ connect(): void; private handleRaw; private dispatch; private matchesFilter; private scheduleReconnect; private send; /** Whether the channel subscription has been confirmed by the server. */ get isSubscribed(): boolean; /** * Cleanly tear down the stream: cancels any pending reconnect, unsubscribes * from the channel, and closes the socket. Safe to call multiple times. * Emits `"close"` once. */ close(): void; } /** * Streaming resource: entry point for price-update subscriptions. * * Accessed via `client.stream`. */ export declare class StreamingResource { private client; /** * Injectable WebSocket implementation. Defaults to the `ws` package; * tests pass a mock constructor. * @internal */ private wsImpl; constructor(client: OilPriceAPI, /** * Injectable WebSocket implementation. Defaults to the `ws` package; * tests pass a mock constructor. * @internal */ wsImpl?: typeof WebSocket); /** * Derive the `wss://.../cable` endpoint from the client's REST base URL. * * `https://api.oilpriceapi.com` -> `wss://api.oilpriceapi.com/cable` * `http://localhost:5000` -> `ws://localhost:5000/cable` */ private cableUrl; /** * Open a price-update stream over the `EnergyPricesChannel`. * * Returns a {@link PriceStreamSubscription} handle (an `EventEmitter`) you * can attach further listeners to and `.close()` when done. The optional * `onUpdate` callback is a convenience wired to the `price_update` event. * * @param options - Filtering and reconnect options. * @param onUpdate - Optional callback for each `price_update` message. * @returns The subscription handle. * * @throws {OilPriceAPIError} If no API key is configured on the client. * * @example * ```typescript * const client = new OilPriceAPI({ apiKey: process.env.OILPRICEAPI_KEY }); * * const sub = client.stream.prices( * { commodities: ["WTI_USD", "BRENT_CRUDE_USD"] }, * (update) => { * const wti = update.prices.oil.wti; * if (wti) console.log(`WTI ${wti.original_price} @ ${update.timestamp}`); * }, * ); * * sub.on("connected", () => console.log("streaming live")); * sub.on("error", (err) => console.error("stream error:", err)); * * process.on("SIGINT", () => sub.close()); * ``` */ prices(options?: StreamPricesOptions, onUpdate?: PriceUpdateHandler): PriceStreamSubscription; }