@@ -21,6 +21,54 @@ import {
2121import { PublicKey } from '@solana/web3.js' ;
2222import bs58 from 'bs58' ;
2323
24+ /**
25+ * WebSocketAccountSubscriberV2
26+ *
27+ * High-level overview
28+ * - WebSocket-first subscriber for a single Solana account with optional
29+ * polling safeguards when the WS feed goes quiet.
30+ * - Emits decoded updates via `onChange` and maintains the latest
31+ * `{buffer, slot}` and decoded `{data, slot}` internally.
32+ *
33+ * Why polling if this is a WebSocket subscriber?
34+ * - Under real-world conditions, WS notifications can stall or get dropped.
35+ * - When `resubOpts.resubTimeoutMs` elapses without WS data, you can either:
36+ * - resubscribe to the WS stream (default), or
37+ * - enable `resubOpts.usePollingInsteadOfResub` to start polling this single
38+ * account via RPC to check for missed changes.
39+ * - Polling compares the fetched buffer to the last known buffer. If different
40+ * at an equal-or-later slot, it indicates a missed update and we resubscribe
41+ * to WS to restore a clean stream.
42+ *
43+ * Initial fetch (on subscribe)
44+ * - On `subscribe()`, we do a one-time RPC `fetch()` to seed internal state and
45+ * emit the latest account state, ensuring consumers start from ground truth
46+ * even before WS events arrive.
47+ *
48+ * Continuous polling (opt-in)
49+ * - If `usePollingInsteadOfResub` is set, the inactivity timeout triggers a
50+ * polling loop that periodically `fetch()`es the account and checks for
51+ * changes. On change, polling stops and we resubscribe to WS.
52+ * - If not set (default), the inactivity timeout immediately triggers a WS
53+ * resubscription (no polling loop).
54+ *
55+ * Account focus
56+ * - This class tracks exactly one account — the one passed to the constructor —
57+ * which is by definition the account the consumer cares about. The extra
58+ * logic is narrowly scoped to this account to minimize overhead.
59+ *
60+ * Tuning knobs
61+ * - `resubOpts.resubTimeoutMs`: WS inactivity threshold before fallback.
62+ * - `resubOpts.usePollingInsteadOfResub`: toggle polling vs immediate resub.
63+ * - `resubOpts.pollingIntervalMs`: polling cadence (default 30s).
64+ * - `resubOpts.logResubMessages`: verbose logs for diagnostics.
65+ * - `commitment`: WS/RPC commitment used for reads and notifications.
66+ * - `decodeBufferFn`: optional custom decode; defaults to Anchor coder.
67+ *
68+ * Implementation notes
69+ * - Uses `gill` for both WS (`rpcSubscriptions`) and RPC (`rpc`) to match the
70+ * program provider’s RPC endpoint. Handles base58/base64 encoded data.
71+ */
2472export class WebSocketAccountSubscriberV2 < T > implements AccountSubscriber < T > {
2573 dataAndSlot ?: DataAndSlot < T > ;
2674 bufferAndSlot ?: BufferAndSlot ;
@@ -49,6 +97,19 @@ export class WebSocketAccountSubscriberV2<T> implements AccountSubscriber<T> {
4997 > [ 'rpcSubscriptions' ] ;
5098 private abortController ?: AbortController ;
5199
100+ /**
101+ * Create a single-account WebSocket subscriber with optional polling fallback.
102+ *
103+ * @param accountName Name of the Anchor account type (used for default decode).
104+ * @param program Anchor `Program` used for decoding and provider access.
105+ * @param accountPublicKey Public key of the account to track.
106+ * @param decodeBuffer Optional custom decode function; if omitted, uses
107+ * program coder to decode `accountName`.
108+ * @param resubOpts Resubscription/polling options. See class docs.
109+ * @param commitment Commitment for WS and RPC operations.
110+ * @param rpcSubscriptions Optional override/injection for testing.
111+ * @param rpc Optional override/injection for testing.
112+ */
52113 public constructor (
53114 accountName : string ,
54115 program : Program ,
@@ -137,6 +198,19 @@ export class WebSocketAccountSubscriberV2<T> implements AccountSubscriber<T> {
137198 }
138199
139200 async subscribe ( onChange : ( data : T ) => void ) : Promise < void > {
201+ /**
202+ * Start the WebSocket subscription and (optionally) setup inactivity
203+ * fallback.
204+ *
205+ * Flow
206+ * - If we do not have initial state, perform a one-time `fetch()` to seed
207+ * internal buffers and emit current data.
208+ * - Subscribe to account notifications via WS.
209+ * - If `resubOpts.resubTimeoutMs` is set, schedule an inactivity timeout.
210+ * When it fires:
211+ * - if `usePollingInsteadOfResub` is true, start polling loop;
212+ * - otherwise, resubscribe to WS immediately.
213+ */
140214 if ( this . listenerId != null || this . isUnsubscribing ) {
141215 if ( this . resubOpts ?. logResubMessages ) {
142216 console . log (
@@ -192,6 +266,11 @@ export class WebSocketAccountSubscriberV2<T> implements AccountSubscriber<T> {
192266 }
193267
194268 protected setTimeout ( ) : void {
269+ /**
270+ * Schedule inactivity handling. If WS is quiet for
271+ * `resubOpts.resubTimeoutMs` and `receivingData` is true, trigger either
272+ * a polling loop or a resubscribe depending on options.
273+ */
195274 if ( ! this . onChange ) {
196275 throw new Error ( 'onChange callback function must be set' ) ;
197276 }
@@ -244,6 +323,11 @@ export class WebSocketAccountSubscriberV2<T> implements AccountSubscriber<T> {
244323 ) ;
245324 }
246325
326+ /**
327+ * Start the polling loop (single-account).
328+ * - Periodically calls `fetch()` and compares buffers to detect changes.
329+ * - On detected change, stops polling and resubscribes to WS.
330+ */
247331 private startPolling ( ) : void {
248332 const pollingInterval = this . resubOpts ?. pollingIntervalMs || 30000 ; // Default to 30s
249333
@@ -306,6 +390,10 @@ export class WebSocketAccountSubscriberV2<T> implements AccountSubscriber<T> {
306390 }
307391 }
308392
393+ /**
394+ * Fetch the current account state via RPC and process it through the same
395+ * decoding and update pathway as WS notifications.
396+ */
309397 async fetch ( ) : Promise < void > {
310398 // Use gill's rpc for fetching account info
311399 const accountAddress = this . accountPublicKey . toBase58 ( ) as Address ;
@@ -400,6 +488,11 @@ export class WebSocketAccountSubscriberV2<T> implements AccountSubscriber<T> {
400488 }
401489
402490 unsubscribe ( onResub = false ) : Promise < void > {
491+ /**
492+ * Stop timers, polling, and WS subscription.
493+ * - When called during a resubscribe (`onResub=true`), we preserve
494+ * `resubOpts.resubTimeoutMs` for the restarted subscription.
495+ */
403496 if ( ! onResub && this . resubOpts ) {
404497 this . resubOpts . resubTimeoutMs = undefined ;
405498 }
0 commit comments