@drift-labs/sdk 2.136.0-beta.0 → 2.136.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/VERSION +1 -1
- package/lib/browser/accounts/types.d.ts +2 -0
- package/lib/browser/accounts/webSocketAccountSubscriberV2.d.ts +76 -3
- package/lib/browser/accounts/webSocketAccountSubscriberV2.js +211 -39
- package/lib/browser/accounts/webSocketDriftClientAccountSubscriberV2.d.ts +87 -0
- package/lib/browser/accounts/webSocketDriftClientAccountSubscriberV2.js +444 -0
- package/lib/browser/accounts/webSocketProgramAccountsSubscriberV2.d.ts +145 -0
- package/lib/browser/accounts/webSocketProgramAccountsSubscriberV2.js +744 -0
- package/lib/browser/accounts/websocketProgramUserAccountSubscriber.d.ts +22 -0
- package/lib/browser/accounts/websocketProgramUserAccountSubscriber.js +54 -0
- package/lib/browser/driftClient.js +22 -18
- package/lib/browser/driftClientConfig.d.ts +7 -2
- package/lib/browser/factory/bigNum.d.ts +2 -2
- package/lib/browser/factory/bigNum.js +20 -5
- package/lib/browser/index.d.ts +4 -0
- package/lib/browser/index.js +9 -1
- package/lib/browser/memcmp.d.ts +2 -0
- package/lib/browser/memcmp.js +19 -1
- package/lib/browser/oracles/oracleId.d.ts +5 -0
- package/lib/browser/oracles/oracleId.js +46 -1
- package/lib/browser/user.js +12 -5
- package/lib/browser/userConfig.d.ts +3 -0
- package/lib/node/accounts/types.d.ts +2 -0
- package/lib/node/accounts/types.d.ts.map +1 -1
- package/lib/node/accounts/webSocketAccountSubscriberV2.d.ts +76 -3
- package/lib/node/accounts/webSocketAccountSubscriberV2.d.ts.map +1 -1
- package/lib/node/accounts/webSocketAccountSubscriberV2.js +211 -39
- package/lib/node/accounts/webSocketDriftClientAccountSubscriberV2.d.ts +88 -0
- package/lib/node/accounts/webSocketDriftClientAccountSubscriberV2.d.ts.map +1 -0
- package/lib/node/accounts/webSocketDriftClientAccountSubscriberV2.js +444 -0
- package/lib/node/accounts/webSocketProgramAccountsSubscriberV2.d.ts +146 -0
- package/lib/node/accounts/webSocketProgramAccountsSubscriberV2.d.ts.map +1 -0
- package/lib/node/accounts/webSocketProgramAccountsSubscriberV2.js +744 -0
- package/lib/node/accounts/websocketProgramUserAccountSubscriber.d.ts +23 -0
- package/lib/node/accounts/websocketProgramUserAccountSubscriber.d.ts.map +1 -0
- package/lib/node/accounts/websocketProgramUserAccountSubscriber.js +54 -0
- package/lib/node/driftClient.d.ts.map +1 -1
- package/lib/node/driftClient.js +22 -18
- package/lib/node/driftClientConfig.d.ts +7 -2
- package/lib/node/driftClientConfig.d.ts.map +1 -1
- package/lib/node/factory/bigNum.d.ts +2 -2
- package/lib/node/factory/bigNum.d.ts.map +1 -1
- package/lib/node/factory/bigNum.js +20 -5
- package/lib/node/index.d.ts +4 -0
- package/lib/node/index.d.ts.map +1 -1
- package/lib/node/index.js +9 -1
- package/lib/node/memcmp.d.ts +2 -0
- package/lib/node/memcmp.d.ts.map +1 -1
- package/lib/node/memcmp.js +19 -1
- package/lib/node/oracles/oracleId.d.ts +5 -0
- package/lib/node/oracles/oracleId.d.ts.map +1 -1
- package/lib/node/oracles/oracleId.js +46 -1
- package/lib/node/user.d.ts.map +1 -1
- package/lib/node/user.js +12 -5
- package/lib/node/userConfig.d.ts +3 -0
- package/lib/node/userConfig.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/accounts/README_WebSocketAccountSubscriberV2.md +41 -0
- package/src/accounts/types.ts +3 -0
- package/src/accounts/webSocketAccountSubscriberV2.ts +243 -42
- package/src/accounts/webSocketDriftClientAccountSubscriberV2.ts +745 -0
- package/src/accounts/webSocketProgramAccountsSubscriberV2.ts +995 -0
- package/src/accounts/websocketProgramUserAccountSubscriber.ts +94 -0
- package/src/driftClient.ts +13 -7
- package/src/driftClientConfig.ts +15 -8
- package/src/factory/bigNum.ts +22 -5
- package/src/index.ts +4 -0
- package/src/memcmp.ts +17 -0
- package/src/oracles/oracleId.ts +34 -0
- package/src/user.ts +21 -9
- package/src/userConfig.ts +3 -0
- package/lib/browser/accounts/webSocketProgramAccountSubscriberV2.d.ts +0 -53
- package/lib/browser/accounts/webSocketProgramAccountSubscriberV2.js +0 -453
- package/lib/node/accounts/webSocketProgramAccountSubscriberV2.d.ts +0 -54
- package/lib/node/accounts/webSocketProgramAccountSubscriberV2.d.ts.map +0 -1
- package/lib/node/accounts/webSocketProgramAccountSubscriberV2.js +0 -453
- package/src/accounts/webSocketProgramAccountSubscriberV2.ts +0 -596
|
@@ -0,0 +1,995 @@
|
|
|
1
|
+
import { BufferAndSlot, ProgramAccountSubscriber, ResubOpts } from './types';
|
|
2
|
+
import { AnchorProvider, Program } from '@coral-xyz/anchor';
|
|
3
|
+
import { Commitment, Context, MemcmpFilter, PublicKey } from '@solana/web3.js';
|
|
4
|
+
import {
|
|
5
|
+
AccountInfoBase,
|
|
6
|
+
AccountInfoWithBase58EncodedData,
|
|
7
|
+
AccountInfoWithBase64EncodedData,
|
|
8
|
+
createSolanaClient,
|
|
9
|
+
isAddress,
|
|
10
|
+
Lamports,
|
|
11
|
+
Slot,
|
|
12
|
+
type Address,
|
|
13
|
+
type Commitment as GillCommitment,
|
|
14
|
+
} from 'gill';
|
|
15
|
+
import bs58 from 'bs58';
|
|
16
|
+
|
|
17
|
+
type ProgramAccountSubscriptionAsyncIterable = AsyncIterable<
|
|
18
|
+
Readonly<{
|
|
19
|
+
context: Readonly<{
|
|
20
|
+
slot: Slot;
|
|
21
|
+
}>;
|
|
22
|
+
value: Readonly<{
|
|
23
|
+
account: Readonly<{
|
|
24
|
+
executable: boolean;
|
|
25
|
+
lamports: Lamports;
|
|
26
|
+
owner: Address;
|
|
27
|
+
rentEpoch: bigint;
|
|
28
|
+
space: bigint;
|
|
29
|
+
}> &
|
|
30
|
+
Readonly<any>;
|
|
31
|
+
pubkey: Address;
|
|
32
|
+
}>;
|
|
33
|
+
}>
|
|
34
|
+
>;
|
|
35
|
+
/**
|
|
36
|
+
* WebSocketProgramAccountsSubscriberV2
|
|
37
|
+
*
|
|
38
|
+
* High-level overview
|
|
39
|
+
* - WebSocket-first subscriber for Solana program accounts that also layers in
|
|
40
|
+
* targeted polling to detect missed updates reliably.
|
|
41
|
+
* - Emits decoded account updates via the provided `onChange` callback.
|
|
42
|
+
* - Designed to focus extra work on the specific accounts the consumer cares
|
|
43
|
+
* about ("monitored accounts") while keeping baseline WS behavior for the
|
|
44
|
+
* full program subscription.
|
|
45
|
+
*
|
|
46
|
+
* Why polling if this is a WebSocket subscriber?
|
|
47
|
+
* - WS infra can stall, drop, or reorder notifications under network stress or
|
|
48
|
+
* provider hiccups. When that happens, critical account changes can be missed.
|
|
49
|
+
* - To mitigate this, the class accepts a set of accounts (provided via constructor) to monitor
|
|
50
|
+
* and uses light polling to verify whether a WS change was missed.
|
|
51
|
+
* - If polling detects a newer slot with different data than the last seen
|
|
52
|
+
* buffer, a centralized resubscription is triggered to restore a clean stream.
|
|
53
|
+
*
|
|
54
|
+
* Initial fetch (on subscribe)
|
|
55
|
+
* - On `subscribe()`, we first perform a single batched fetch of all monitored
|
|
56
|
+
* accounts ("initial monitor fetch").
|
|
57
|
+
* - Purpose: seed the internal `bufferAndSlotMap` and emit the latest state so
|
|
58
|
+
* consumers have up-to-date data immediately, even before WS events arrive.
|
|
59
|
+
* - This step does not decide resubscription; it only establishes ground truth.
|
|
60
|
+
*
|
|
61
|
+
* Continuous polling (only for monitored accounts)
|
|
62
|
+
* - After seeding, each monitored account is put into a monitoring cycle:
|
|
63
|
+
* 1) If no WS notification for an account is observed for `pollingIntervalMs`,
|
|
64
|
+
* we enqueue it for a batched fetch (buffered for a short window).
|
|
65
|
+
* 2) Once an account enters the "currently polling" set, a shared batch poll
|
|
66
|
+
* runs every `pollingIntervalMs` across all such accounts.
|
|
67
|
+
* 3) If WS notifications resume for an account, that account is removed from
|
|
68
|
+
* the polling set and returns to passive monitoring.
|
|
69
|
+
* - Polling compares the newly fetched buffer with the last stored buffer at a
|
|
70
|
+
* later slot. A difference indicates a missed update; we schedule a single
|
|
71
|
+
* resubscription (coalesced across accounts) to re-sync.
|
|
72
|
+
*
|
|
73
|
+
* Accounts the consumer cares about
|
|
74
|
+
* - Provide accounts up-front via the constructor `accountsToMonitor`, or add
|
|
75
|
+
* them dynamically with `addAccountToMonitor()` and remove with
|
|
76
|
+
* `removeAccountFromMonitor()`.
|
|
77
|
+
* - Only these accounts incur additional polling safeguards; other accounts are
|
|
78
|
+
* still processed from the WS stream normally.
|
|
79
|
+
*
|
|
80
|
+
* Resubscription strategy
|
|
81
|
+
* - Missed updates from any monitored account are coalesced and trigger a single
|
|
82
|
+
* resubscription after a short delay. This avoids rapid churn.
|
|
83
|
+
* - If `resubOpts.resubTimeoutMs` is set, an inactivity timer also performs a
|
|
84
|
+
* batch check of monitored accounts. If a missed update is found, the same
|
|
85
|
+
* centralized resubscription flow is used.
|
|
86
|
+
*
|
|
87
|
+
* Tuning knobs
|
|
88
|
+
* - `setPollingInterval(ms)`: adjust how often monitoring/polling runs
|
|
89
|
+
* (default 30s). Shorter = faster detection, higher RPC load.
|
|
90
|
+
* - Debounced immediate poll (~100ms): batches accounts added to polling right after inactivity.
|
|
91
|
+
* - Batch size for `getMultipleAccounts` is limited to 100, requests are chunked
|
|
92
|
+
* and processed concurrently.
|
|
93
|
+
*/
|
|
94
|
+
|
|
95
|
+
export class WebSocketProgramAccountsSubscriberV2<T>
|
|
96
|
+
implements ProgramAccountSubscriber<T>
|
|
97
|
+
{
|
|
98
|
+
subscriptionName: string;
|
|
99
|
+
accountDiscriminator: string;
|
|
100
|
+
bufferAndSlotMap: Map<string, BufferAndSlot> = new Map();
|
|
101
|
+
program: Program;
|
|
102
|
+
decodeBuffer: (accountName: string, ix: Buffer) => T;
|
|
103
|
+
onChange: (
|
|
104
|
+
accountId: PublicKey,
|
|
105
|
+
data: T,
|
|
106
|
+
context: Context,
|
|
107
|
+
buffer: Buffer
|
|
108
|
+
) => void;
|
|
109
|
+
listenerId?: number;
|
|
110
|
+
resubOpts: ResubOpts;
|
|
111
|
+
isUnsubscribing = false;
|
|
112
|
+
timeoutId?: ReturnType<typeof setTimeout>;
|
|
113
|
+
options: { filters: MemcmpFilter[]; commitment?: Commitment };
|
|
114
|
+
|
|
115
|
+
receivingData = false;
|
|
116
|
+
|
|
117
|
+
// Gill client components
|
|
118
|
+
private rpc: ReturnType<typeof createSolanaClient>['rpc'];
|
|
119
|
+
private rpcSubscriptions: ReturnType<
|
|
120
|
+
typeof createSolanaClient
|
|
121
|
+
>['rpcSubscriptions'];
|
|
122
|
+
private abortController?: AbortController;
|
|
123
|
+
|
|
124
|
+
// Polling logic for specific accounts
|
|
125
|
+
private accountsToMonitor: Set<string> = new Set();
|
|
126
|
+
private pollingIntervalMs: number = 30000; // 30 seconds
|
|
127
|
+
private pollingTimeouts: Map<string, ReturnType<typeof setTimeout>> =
|
|
128
|
+
new Map();
|
|
129
|
+
private lastWsNotificationTime: Map<string, number> = new Map(); // Track last WS notification time per account
|
|
130
|
+
private accountsCurrentlyPolling: Set<string> = new Set(); // Track which accounts are being polled
|
|
131
|
+
private batchPollingTimeout?: ReturnType<typeof setTimeout>; // Single timeout for batch polling
|
|
132
|
+
|
|
133
|
+
// Debounced immediate poll to batch multiple additions within a short window
|
|
134
|
+
private debouncedImmediatePollTimeout?: ReturnType<typeof setTimeout>;
|
|
135
|
+
private debouncedImmediatePollMs: number = 100; // configurable short window
|
|
136
|
+
|
|
137
|
+
// Centralized resubscription handling
|
|
138
|
+
private missedChangeDetected = false; // Flag to track if any missed change was detected
|
|
139
|
+
private resubscriptionTimeout?: ReturnType<typeof setTimeout>; // Timeout for delayed resubscription
|
|
140
|
+
private accountsWithMissedUpdates: Set<string> = new Set(); // Track which accounts had missed updates
|
|
141
|
+
|
|
142
|
+
public constructor(
|
|
143
|
+
subscriptionName: string,
|
|
144
|
+
accountDiscriminator: string,
|
|
145
|
+
program: Program,
|
|
146
|
+
decodeBufferFn: (accountName: string, ix: Buffer) => T,
|
|
147
|
+
options: { filters: MemcmpFilter[]; commitment?: Commitment } = {
|
|
148
|
+
filters: [],
|
|
149
|
+
},
|
|
150
|
+
resubOpts?: ResubOpts,
|
|
151
|
+
accountsToMonitor?: PublicKey[] // Optional list of accounts to poll
|
|
152
|
+
) {
|
|
153
|
+
this.subscriptionName = subscriptionName;
|
|
154
|
+
this.accountDiscriminator = accountDiscriminator;
|
|
155
|
+
this.program = program;
|
|
156
|
+
this.decodeBuffer = decodeBufferFn;
|
|
157
|
+
this.resubOpts = resubOpts ?? {
|
|
158
|
+
resubTimeoutMs: 30000,
|
|
159
|
+
usePollingInsteadOfResub: true,
|
|
160
|
+
logResubMessages: false,
|
|
161
|
+
};
|
|
162
|
+
if (this.resubOpts?.resubTimeoutMs < 1000) {
|
|
163
|
+
console.log(
|
|
164
|
+
'resubTimeoutMs should be at least 1000ms to avoid spamming resub'
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
this.options = options;
|
|
168
|
+
this.receivingData = false;
|
|
169
|
+
|
|
170
|
+
// Initialize accounts to monitor
|
|
171
|
+
if (accountsToMonitor) {
|
|
172
|
+
accountsToMonitor.forEach((account) => {
|
|
173
|
+
this.accountsToMonitor.add(account.toBase58());
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Initialize gill client using the same RPC URL as the program provider
|
|
178
|
+
const rpcUrl = (this.program.provider as AnchorProvider).connection
|
|
179
|
+
.rpcEndpoint;
|
|
180
|
+
const { rpc, rpcSubscriptions } = createSolanaClient({
|
|
181
|
+
urlOrMoniker: rpcUrl,
|
|
182
|
+
});
|
|
183
|
+
this.rpc = rpc;
|
|
184
|
+
this.rpcSubscriptions = rpcSubscriptions;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private async handleNotificationLoop(
|
|
188
|
+
notificationPromise: Promise<ProgramAccountSubscriptionAsyncIterable>
|
|
189
|
+
) {
|
|
190
|
+
try {
|
|
191
|
+
const subscriptionIterable = await notificationPromise;
|
|
192
|
+
for await (const notification of subscriptionIterable) {
|
|
193
|
+
try {
|
|
194
|
+
if (this.resubOpts?.resubTimeoutMs) {
|
|
195
|
+
this.receivingData = true;
|
|
196
|
+
clearTimeout(this.timeoutId);
|
|
197
|
+
this.handleRpcResponse(
|
|
198
|
+
notification.context,
|
|
199
|
+
notification.value.pubkey,
|
|
200
|
+
notification.value.account.data
|
|
201
|
+
);
|
|
202
|
+
this.setTimeout();
|
|
203
|
+
} else {
|
|
204
|
+
this.handleRpcResponse(
|
|
205
|
+
notification.context,
|
|
206
|
+
notification.value.pubkey,
|
|
207
|
+
notification.value.account.data
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
} catch (error) {
|
|
211
|
+
console.error(
|
|
212
|
+
`Error handling RPC response for pubkey ${notification.value.pubkey}:`,
|
|
213
|
+
error
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
} catch (error) {
|
|
218
|
+
console.error(
|
|
219
|
+
`[${this.subscriptionName}] Error in notification loop:`,
|
|
220
|
+
error
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async subscribe(
|
|
226
|
+
onChange: (
|
|
227
|
+
accountId: PublicKey,
|
|
228
|
+
data: T,
|
|
229
|
+
context: Context,
|
|
230
|
+
buffer: Buffer
|
|
231
|
+
) => void
|
|
232
|
+
): Promise<void> {
|
|
233
|
+
/**
|
|
234
|
+
* Start the WebSocket subscription and initialize polling safeguards.
|
|
235
|
+
*
|
|
236
|
+
* Flow
|
|
237
|
+
* - Seeds all monitored accounts with a single batched RPC fetch and emits
|
|
238
|
+
* their current state.
|
|
239
|
+
* - Subscribes to program notifications via WS using gill.
|
|
240
|
+
* - If `resubOpts.resubTimeoutMs` is set, starts an inactivity timer that
|
|
241
|
+
* batch-checks monitored accounts when WS goes quiet.
|
|
242
|
+
* - Begins monitoring for accounts that may need polling when WS
|
|
243
|
+
* notifications are not observed within `pollingIntervalMs`.
|
|
244
|
+
*
|
|
245
|
+
* @param onChange Callback invoked with decoded account data when an update
|
|
246
|
+
* is detected (via WS or batch RPC fetch).
|
|
247
|
+
*/
|
|
248
|
+
const startTime = performance.now();
|
|
249
|
+
if (this.listenerId != null || this.isUnsubscribing) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (this.resubOpts?.logResubMessages) {
|
|
254
|
+
console.log(
|
|
255
|
+
`[${this.subscriptionName}] initializing subscription. This many monitored accounts: ${this.accountsToMonitor.size}`
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
this.onChange = onChange;
|
|
260
|
+
|
|
261
|
+
// initial fetch of monitored data - only fetch and populate, don't check for missed changes
|
|
262
|
+
await this.fetchAndPopulateAllMonitoredAccounts();
|
|
263
|
+
|
|
264
|
+
// Create abort controller for proper cleanup
|
|
265
|
+
const abortController = new AbortController();
|
|
266
|
+
this.abortController = abortController;
|
|
267
|
+
|
|
268
|
+
this.listenerId = Math.random(); // Unique ID for logging purposes
|
|
269
|
+
|
|
270
|
+
if (this.resubOpts?.resubTimeoutMs) {
|
|
271
|
+
this.receivingData = true;
|
|
272
|
+
this.setTimeout();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Subscribe to program account changes using gill's rpcSubscriptions
|
|
276
|
+
const programId = this.program.programId.toBase58();
|
|
277
|
+
if (isAddress(programId)) {
|
|
278
|
+
const subscriptionPromise = this.rpcSubscriptions
|
|
279
|
+
.programNotifications(programId, {
|
|
280
|
+
commitment: this.options.commitment as GillCommitment,
|
|
281
|
+
encoding: 'base64',
|
|
282
|
+
filters: this.options.filters.map((filter) => {
|
|
283
|
+
// Convert filter bytes from base58 to base64 if needed
|
|
284
|
+
let bytes = filter.memcmp.bytes;
|
|
285
|
+
if (
|
|
286
|
+
typeof bytes === 'string' &&
|
|
287
|
+
/^[1-9A-HJ-NP-Za-km-z]+$/.test(bytes)
|
|
288
|
+
) {
|
|
289
|
+
// Looks like base58 - convert to base64
|
|
290
|
+
const decoded = bs58.decode(bytes);
|
|
291
|
+
bytes = Buffer.from(decoded).toString('base64');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
memcmp: {
|
|
296
|
+
offset: BigInt(filter.memcmp.offset),
|
|
297
|
+
bytes: bytes as any,
|
|
298
|
+
encoding: 'base64' as const,
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
}),
|
|
302
|
+
})
|
|
303
|
+
.subscribe({
|
|
304
|
+
abortSignal: abortController.signal,
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// Start notification loop without awaiting
|
|
308
|
+
this.handleNotificationLoop(subscriptionPromise);
|
|
309
|
+
// Start monitoring for accounts that may need polling if no WS event is received
|
|
310
|
+
this.startMonitoringForAccounts();
|
|
311
|
+
}
|
|
312
|
+
const endTime = performance.now();
|
|
313
|
+
console.log(
|
|
314
|
+
`[PROFILING] ${this.subscriptionName}.subscribe() completed in ${
|
|
315
|
+
endTime - startTime
|
|
316
|
+
}ms`
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
protected setTimeout(): void {
|
|
321
|
+
if (!this.onChange) {
|
|
322
|
+
throw new Error('onChange callback function must be set');
|
|
323
|
+
}
|
|
324
|
+
this.timeoutId = setTimeout(
|
|
325
|
+
async () => {
|
|
326
|
+
if (this.isUnsubscribing) {
|
|
327
|
+
// If we are in the process of unsubscribing, do not attempt to resubscribe
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (this.receivingData) {
|
|
332
|
+
if (this.resubOpts?.logResubMessages) {
|
|
333
|
+
console.log(
|
|
334
|
+
`No ws data from ${this.subscriptionName} in ${this.resubOpts?.resubTimeoutMs}ms, checking for missed changes`
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Check for missed changes in monitored accounts
|
|
339
|
+
const missedChangeDetected = await this.fetchAllMonitoredAccounts();
|
|
340
|
+
|
|
341
|
+
if (missedChangeDetected) {
|
|
342
|
+
// Signal missed change with a generic identifier since we don't have specific account IDs from this context
|
|
343
|
+
this.signalMissedChange('timeout-check');
|
|
344
|
+
} else {
|
|
345
|
+
// No missed changes, continue monitoring
|
|
346
|
+
this.receivingData = false;
|
|
347
|
+
this.setTimeout();
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
this.resubOpts?.resubTimeoutMs
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
handleRpcResponse(
|
|
356
|
+
context: { slot: bigint },
|
|
357
|
+
accountId: Address,
|
|
358
|
+
accountInfo?: AccountInfoBase &
|
|
359
|
+
(
|
|
360
|
+
| AccountInfoWithBase58EncodedData
|
|
361
|
+
| AccountInfoWithBase64EncodedData
|
|
362
|
+
)['data']
|
|
363
|
+
): void {
|
|
364
|
+
const newSlot = Number(context.slot);
|
|
365
|
+
let newBuffer: Buffer | undefined = undefined;
|
|
366
|
+
|
|
367
|
+
if (accountInfo) {
|
|
368
|
+
// Handle different data formats from gill
|
|
369
|
+
if (Array.isArray(accountInfo)) {
|
|
370
|
+
// If it's a tuple [data, encoding]
|
|
371
|
+
const [data, encoding] = accountInfo;
|
|
372
|
+
|
|
373
|
+
if (encoding === ('base58' as any)) {
|
|
374
|
+
// Convert base58 to buffer using bs58
|
|
375
|
+
newBuffer = Buffer.from(bs58.decode(data));
|
|
376
|
+
} else {
|
|
377
|
+
newBuffer = Buffer.from(data, 'base64');
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const accountIdString = accountId.toString();
|
|
383
|
+
const existingBufferAndSlot = this.bufferAndSlotMap.get(accountIdString);
|
|
384
|
+
|
|
385
|
+
// Track WebSocket notification time for this account
|
|
386
|
+
this.lastWsNotificationTime.set(accountIdString, Date.now());
|
|
387
|
+
|
|
388
|
+
// If this account was being polled, stop polling it if the buffer has changed
|
|
389
|
+
if (
|
|
390
|
+
this.accountsCurrentlyPolling.has(accountIdString) &&
|
|
391
|
+
!existingBufferAndSlot?.buffer.equals(newBuffer)
|
|
392
|
+
) {
|
|
393
|
+
this.accountsCurrentlyPolling.delete(accountIdString);
|
|
394
|
+
|
|
395
|
+
// If no more accounts are being polled, stop batch polling
|
|
396
|
+
if (
|
|
397
|
+
this.accountsCurrentlyPolling.size === 0 &&
|
|
398
|
+
this.batchPollingTimeout
|
|
399
|
+
) {
|
|
400
|
+
clearTimeout(this.batchPollingTimeout);
|
|
401
|
+
this.batchPollingTimeout = undefined;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (!existingBufferAndSlot) {
|
|
406
|
+
if (newBuffer) {
|
|
407
|
+
this.updateBufferAndHandleChange(newBuffer, newSlot, accountIdString);
|
|
408
|
+
}
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (newSlot < existingBufferAndSlot.slot) {
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const oldBuffer = existingBufferAndSlot.buffer;
|
|
417
|
+
if (newBuffer && (!oldBuffer || !newBuffer.equals(oldBuffer))) {
|
|
418
|
+
this.updateBufferAndHandleChange(newBuffer, newSlot, accountIdString);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
private startMonitoringForAccounts(): void {
|
|
423
|
+
// Clear any existing polling timeouts
|
|
424
|
+
this.clearPollingTimeouts();
|
|
425
|
+
|
|
426
|
+
// Start monitoring for each account in the accountsToMonitor set
|
|
427
|
+
this.accountsToMonitor.forEach((accountIdString) => {
|
|
428
|
+
this.startMonitoringForAccount(accountIdString);
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
private startMonitoringForAccount(accountIdString: string): void {
|
|
433
|
+
// Clear existing timeout for this account
|
|
434
|
+
const existingTimeout = this.pollingTimeouts.get(accountIdString);
|
|
435
|
+
if (existingTimeout) {
|
|
436
|
+
clearTimeout(existingTimeout);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Set up monitoring timeout - only start polling if no WS notification in 30s
|
|
440
|
+
const timeoutId = setTimeout(async () => {
|
|
441
|
+
// Check if we've received a WS notification for this account recently
|
|
442
|
+
const lastNotificationTime =
|
|
443
|
+
this.lastWsNotificationTime.get(accountIdString) || 0;
|
|
444
|
+
const currentTime = Date.now();
|
|
445
|
+
|
|
446
|
+
if (
|
|
447
|
+
!lastNotificationTime ||
|
|
448
|
+
currentTime - lastNotificationTime >= this.pollingIntervalMs
|
|
449
|
+
) {
|
|
450
|
+
if (this.resubOpts?.logResubMessages) {
|
|
451
|
+
console.debug(
|
|
452
|
+
`[${this.subscriptionName}] No recent WS notification for ${accountIdString}, adding to polling set`
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
// No recent WS notification: add to polling and schedule debounced poll
|
|
456
|
+
this.accountsCurrentlyPolling.add(accountIdString);
|
|
457
|
+
this.scheduleDebouncedImmediatePoll();
|
|
458
|
+
} else {
|
|
459
|
+
// We received a WS notification recently, continue monitoring
|
|
460
|
+
this.startMonitoringForAccount(accountIdString);
|
|
461
|
+
}
|
|
462
|
+
}, this.pollingIntervalMs);
|
|
463
|
+
|
|
464
|
+
this.pollingTimeouts.set(accountIdString, timeoutId);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
private scheduleDebouncedImmediatePoll(): void {
|
|
468
|
+
if (this.debouncedImmediatePollTimeout) {
|
|
469
|
+
clearTimeout(this.debouncedImmediatePollTimeout);
|
|
470
|
+
}
|
|
471
|
+
this.debouncedImmediatePollTimeout = setTimeout(async () => {
|
|
472
|
+
try {
|
|
473
|
+
await this.pollAllAccounts();
|
|
474
|
+
// After the immediate poll, ensure continuous batch polling is active
|
|
475
|
+
if (
|
|
476
|
+
!this.batchPollingTimeout &&
|
|
477
|
+
this.accountsCurrentlyPolling.size > 0
|
|
478
|
+
) {
|
|
479
|
+
this.startBatchPolling();
|
|
480
|
+
}
|
|
481
|
+
} catch (e) {
|
|
482
|
+
if (this.resubOpts?.logResubMessages) {
|
|
483
|
+
console.log(
|
|
484
|
+
`[${this.subscriptionName}] Error during debounced immediate poll:`,
|
|
485
|
+
e
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}, this.debouncedImmediatePollMs);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
private startBatchPolling(): void {
|
|
493
|
+
if (this.resubOpts?.logResubMessages) {
|
|
494
|
+
console.debug(`[${this.subscriptionName}] Scheduling batch polling`);
|
|
495
|
+
}
|
|
496
|
+
// Clear existing batch polling timeout
|
|
497
|
+
if (this.batchPollingTimeout) {
|
|
498
|
+
clearTimeout(this.batchPollingTimeout);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Set up batch polling interval
|
|
502
|
+
this.batchPollingTimeout = setTimeout(async () => {
|
|
503
|
+
await this.pollAllAccounts();
|
|
504
|
+
// Schedule next batch poll
|
|
505
|
+
this.startBatchPolling();
|
|
506
|
+
}, this.pollingIntervalMs);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
private async pollAllAccounts(): Promise<void> {
|
|
510
|
+
try {
|
|
511
|
+
// Get all accounts currently being polled
|
|
512
|
+
const accountsToPoll = Array.from(this.accountsCurrentlyPolling);
|
|
513
|
+
if (accountsToPoll.length === 0) {
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (this.resubOpts?.logResubMessages) {
|
|
518
|
+
console.debug(
|
|
519
|
+
`[${this.subscriptionName}] Polling all accounts`,
|
|
520
|
+
accountsToPoll.length,
|
|
521
|
+
'accounts'
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Use the shared batch fetch method
|
|
526
|
+
await this.fetchAccountsBatch(accountsToPoll);
|
|
527
|
+
} catch (error) {
|
|
528
|
+
if (this.resubOpts?.logResubMessages) {
|
|
529
|
+
console.log(
|
|
530
|
+
`[${this.subscriptionName}] Error batch polling accounts:`,
|
|
531
|
+
error
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Fetches and populates all monitored accounts data without checking for missed changes
|
|
539
|
+
* This is used during initial subscription to populate data
|
|
540
|
+
*/
|
|
541
|
+
private async fetchAndPopulateAllMonitoredAccounts(): Promise<void> {
|
|
542
|
+
try {
|
|
543
|
+
// Get all accounts currently being polled
|
|
544
|
+
const accountsToMonitor = Array.from(this.accountsToMonitor);
|
|
545
|
+
if (accountsToMonitor.length === 0) {
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Fetch all accounts in a single batch request
|
|
550
|
+
const accountAddresses = accountsToMonitor.map(
|
|
551
|
+
(accountId) => accountId as Address
|
|
552
|
+
);
|
|
553
|
+
const rpcResponse = await this.rpc
|
|
554
|
+
.getMultipleAccounts(accountAddresses, {
|
|
555
|
+
commitment: this.options.commitment as GillCommitment,
|
|
556
|
+
encoding: 'base64',
|
|
557
|
+
})
|
|
558
|
+
.send();
|
|
559
|
+
|
|
560
|
+
const currentSlot = Number(rpcResponse.context.slot);
|
|
561
|
+
|
|
562
|
+
// Process each account response
|
|
563
|
+
for (let i = 0; i < accountsToMonitor.length; i++) {
|
|
564
|
+
const accountIdString = accountsToMonitor[i];
|
|
565
|
+
const accountInfo = rpcResponse.value[i];
|
|
566
|
+
|
|
567
|
+
if (!accountInfo) {
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const existingBufferAndSlot =
|
|
572
|
+
this.bufferAndSlotMap.get(accountIdString);
|
|
573
|
+
|
|
574
|
+
if (!existingBufferAndSlot) {
|
|
575
|
+
// Account not in our map yet, add it
|
|
576
|
+
let newBuffer: Buffer | undefined = undefined;
|
|
577
|
+
if (accountInfo) {
|
|
578
|
+
if (Array.isArray(accountInfo.data)) {
|
|
579
|
+
const [data, encoding] = accountInfo.data;
|
|
580
|
+
newBuffer = Buffer.from(data, encoding);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (newBuffer) {
|
|
585
|
+
this.updateBufferAndHandleChange(
|
|
586
|
+
newBuffer,
|
|
587
|
+
currentSlot,
|
|
588
|
+
accountIdString
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// For initial population, just update the slot if we have newer data
|
|
595
|
+
if (currentSlot > existingBufferAndSlot.slot) {
|
|
596
|
+
let newBuffer: Buffer | undefined = undefined;
|
|
597
|
+
if (accountInfo.data) {
|
|
598
|
+
if (Array.isArray(accountInfo.data)) {
|
|
599
|
+
const [data, encoding] = accountInfo.data;
|
|
600
|
+
if (encoding === ('base58' as any)) {
|
|
601
|
+
newBuffer = Buffer.from(bs58.decode(data));
|
|
602
|
+
} else {
|
|
603
|
+
newBuffer = Buffer.from(data, 'base64');
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Update with newer data if available
|
|
609
|
+
if (newBuffer) {
|
|
610
|
+
this.updateBufferAndHandleChange(
|
|
611
|
+
newBuffer,
|
|
612
|
+
currentSlot,
|
|
613
|
+
accountIdString
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
} catch (error) {
|
|
619
|
+
if (this.resubOpts?.logResubMessages) {
|
|
620
|
+
console.log(
|
|
621
|
+
`[${this.subscriptionName}] Error fetching and populating monitored accounts:`,
|
|
622
|
+
error
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Fetches all monitored accounts and checks for missed changes
|
|
630
|
+
* Returns true if a missed change was detected and resubscription is needed
|
|
631
|
+
*/
|
|
632
|
+
private async fetchAllMonitoredAccounts(): Promise<boolean> {
|
|
633
|
+
try {
|
|
634
|
+
// Get all accounts currently being polled
|
|
635
|
+
const accountsToMonitor = Array.from(this.accountsToMonitor);
|
|
636
|
+
if (accountsToMonitor.length === 0) {
|
|
637
|
+
return false;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Fetch all accounts in a single batch request
|
|
641
|
+
const accountAddresses = accountsToMonitor.map(
|
|
642
|
+
(accountId) => accountId as Address
|
|
643
|
+
);
|
|
644
|
+
const rpcResponse = await this.rpc
|
|
645
|
+
.getMultipleAccounts(accountAddresses, {
|
|
646
|
+
commitment: this.options.commitment as GillCommitment,
|
|
647
|
+
encoding: 'base64',
|
|
648
|
+
})
|
|
649
|
+
.send();
|
|
650
|
+
|
|
651
|
+
const currentSlot = Number(rpcResponse.context.slot);
|
|
652
|
+
|
|
653
|
+
// Process each account response
|
|
654
|
+
for (let i = 0; i < accountsToMonitor.length; i++) {
|
|
655
|
+
const accountIdString = accountsToMonitor[i];
|
|
656
|
+
const accountInfo = rpcResponse.value[i];
|
|
657
|
+
|
|
658
|
+
if (!accountInfo) {
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const existingBufferAndSlot =
|
|
663
|
+
this.bufferAndSlotMap.get(accountIdString);
|
|
664
|
+
|
|
665
|
+
if (!existingBufferAndSlot) {
|
|
666
|
+
// Account not in our map yet, add it
|
|
667
|
+
let newBuffer: Buffer | undefined = undefined;
|
|
668
|
+
if (accountInfo.data) {
|
|
669
|
+
if (Array.isArray(accountInfo.data)) {
|
|
670
|
+
const [data, encoding] = accountInfo.data;
|
|
671
|
+
newBuffer = Buffer.from(data, encoding);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
if (newBuffer) {
|
|
676
|
+
this.updateBufferAndHandleChange(
|
|
677
|
+
newBuffer,
|
|
678
|
+
currentSlot,
|
|
679
|
+
accountIdString
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Check if we missed an update
|
|
686
|
+
if (currentSlot > existingBufferAndSlot.slot) {
|
|
687
|
+
let newBuffer: Buffer | undefined = undefined;
|
|
688
|
+
if (accountInfo.data) {
|
|
689
|
+
if (Array.isArray(accountInfo.data)) {
|
|
690
|
+
const [data, encoding] = accountInfo.data;
|
|
691
|
+
if (encoding === ('base58' as any)) {
|
|
692
|
+
newBuffer = Buffer.from(bs58.decode(data));
|
|
693
|
+
} else {
|
|
694
|
+
newBuffer = Buffer.from(data, 'base64');
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// Check if buffer has changed
|
|
700
|
+
if (
|
|
701
|
+
newBuffer &&
|
|
702
|
+
(!existingBufferAndSlot.buffer ||
|
|
703
|
+
!newBuffer.equals(existingBufferAndSlot.buffer))
|
|
704
|
+
) {
|
|
705
|
+
if (this.resubOpts?.logResubMessages) {
|
|
706
|
+
console.log(
|
|
707
|
+
`[${this.subscriptionName}] Batch polling detected missed update for account ${accountIdString}, resubscribing`
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
// We missed an update, return true to indicate resubscription is needed
|
|
711
|
+
return true;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// No missed changes detected
|
|
717
|
+
return false;
|
|
718
|
+
} catch (error) {
|
|
719
|
+
if (this.resubOpts?.logResubMessages) {
|
|
720
|
+
console.log(
|
|
721
|
+
`[${this.subscriptionName}] Error batch polling accounts:`,
|
|
722
|
+
error
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
return false;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
private async fetchAccountsBatch(accountIds: string[]): Promise<void> {
|
|
730
|
+
try {
|
|
731
|
+
// Chunk account IDs into groups of 100 (getMultipleAccounts limit)
|
|
732
|
+
const chunkSize = 100;
|
|
733
|
+
const chunks: string[][] = [];
|
|
734
|
+
for (let i = 0; i < accountIds.length; i += chunkSize) {
|
|
735
|
+
chunks.push(accountIds.slice(i, i + chunkSize));
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// Process all chunks concurrently
|
|
739
|
+
await Promise.all(
|
|
740
|
+
chunks.map(async (chunk) => {
|
|
741
|
+
const accountAddresses = chunk.map(
|
|
742
|
+
(accountId) => accountId as Address
|
|
743
|
+
);
|
|
744
|
+
const rpcResponse = await this.rpc
|
|
745
|
+
.getMultipleAccounts(accountAddresses, {
|
|
746
|
+
commitment: this.options.commitment as GillCommitment,
|
|
747
|
+
encoding: 'base64',
|
|
748
|
+
})
|
|
749
|
+
.send();
|
|
750
|
+
|
|
751
|
+
const currentSlot = Number(rpcResponse.context.slot);
|
|
752
|
+
|
|
753
|
+
// Process each account response in this chunk
|
|
754
|
+
for (let i = 0; i < chunk.length; i++) {
|
|
755
|
+
const accountIdString = chunk[i];
|
|
756
|
+
const accountInfo = rpcResponse.value[i];
|
|
757
|
+
|
|
758
|
+
if (!accountInfo) {
|
|
759
|
+
continue;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const existingBufferAndSlot =
|
|
763
|
+
this.bufferAndSlotMap.get(accountIdString);
|
|
764
|
+
|
|
765
|
+
if (!existingBufferAndSlot) {
|
|
766
|
+
// Account not in our map yet, add it
|
|
767
|
+
let newBuffer: Buffer | undefined = undefined;
|
|
768
|
+
if (accountInfo.data) {
|
|
769
|
+
if (Array.isArray(accountInfo.data)) {
|
|
770
|
+
const [data, encoding] = accountInfo.data;
|
|
771
|
+
newBuffer = Buffer.from(data, encoding);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
if (newBuffer) {
|
|
776
|
+
this.updateBufferAndHandleChange(
|
|
777
|
+
newBuffer,
|
|
778
|
+
currentSlot,
|
|
779
|
+
accountIdString
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
continue;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// Check if we missed an update
|
|
786
|
+
if (currentSlot > existingBufferAndSlot.slot) {
|
|
787
|
+
let newBuffer: Buffer | undefined = undefined;
|
|
788
|
+
if (accountInfo.data) {
|
|
789
|
+
if (Array.isArray(accountInfo.data)) {
|
|
790
|
+
const [data, encoding] = accountInfo.data;
|
|
791
|
+
if (encoding === ('base58' as any)) {
|
|
792
|
+
newBuffer = Buffer.from(bs58.decode(data));
|
|
793
|
+
} else {
|
|
794
|
+
newBuffer = Buffer.from(data, 'base64');
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// Check if buffer has changed
|
|
800
|
+
if (
|
|
801
|
+
newBuffer &&
|
|
802
|
+
(!existingBufferAndSlot.buffer ||
|
|
803
|
+
!newBuffer.equals(existingBufferAndSlot.buffer))
|
|
804
|
+
) {
|
|
805
|
+
if (this.resubOpts?.logResubMessages) {
|
|
806
|
+
console.log(
|
|
807
|
+
`[${this.subscriptionName}] Batch polling detected missed update for account ${accountIdString}, signaling resubscription`
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
// Signal missed change instead of immediately resubscribing
|
|
811
|
+
this.signalMissedChange(accountIdString);
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
})
|
|
817
|
+
);
|
|
818
|
+
} catch (error) {
|
|
819
|
+
if (this.resubOpts?.logResubMessages) {
|
|
820
|
+
console.log(
|
|
821
|
+
`[${this.subscriptionName}] Error fetching accounts batch:`,
|
|
822
|
+
error
|
|
823
|
+
);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
private clearPollingTimeouts(): void {
|
|
829
|
+
this.pollingTimeouts.forEach((timeoutId) => {
|
|
830
|
+
clearTimeout(timeoutId);
|
|
831
|
+
});
|
|
832
|
+
this.pollingTimeouts.clear();
|
|
833
|
+
|
|
834
|
+
// Clear batch polling timeout
|
|
835
|
+
if (this.batchPollingTimeout) {
|
|
836
|
+
clearTimeout(this.batchPollingTimeout);
|
|
837
|
+
this.batchPollingTimeout = undefined;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// Clear initial fetch timeout
|
|
841
|
+
// if (this.initialFetchTimeout) {
|
|
842
|
+
// clearTimeout(this.initialFetchTimeout);
|
|
843
|
+
// this.initialFetchTimeout = undefined;
|
|
844
|
+
// }
|
|
845
|
+
|
|
846
|
+
// Clear resubscription timeout
|
|
847
|
+
if (this.resubscriptionTimeout) {
|
|
848
|
+
clearTimeout(this.resubscriptionTimeout);
|
|
849
|
+
this.resubscriptionTimeout = undefined;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// Clear accounts currently polling
|
|
853
|
+
this.accountsCurrentlyPolling.clear();
|
|
854
|
+
|
|
855
|
+
// Clear accounts pending initial monitor fetch
|
|
856
|
+
// this.accountsPendingInitialMonitorFetch.clear();
|
|
857
|
+
|
|
858
|
+
// Reset missed change flag and clear accounts with missed updates
|
|
859
|
+
this.missedChangeDetected = false;
|
|
860
|
+
this.accountsWithMissedUpdates.clear();
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/**
|
|
864
|
+
* Centralized resubscription handler that only resubscribes once after checking all accounts
|
|
865
|
+
*/
|
|
866
|
+
private async handleResubscription(): Promise<void> {
|
|
867
|
+
if (this.missedChangeDetected) {
|
|
868
|
+
if (this.resubOpts?.logResubMessages) {
|
|
869
|
+
console.log(
|
|
870
|
+
`[${this.subscriptionName}] Missed change detected for ${
|
|
871
|
+
this.accountsWithMissedUpdates.size
|
|
872
|
+
} accounts: ${Array.from(this.accountsWithMissedUpdates).join(
|
|
873
|
+
', '
|
|
874
|
+
)}, resubscribing`
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
await this.unsubscribe(true);
|
|
878
|
+
this.receivingData = false;
|
|
879
|
+
await this.subscribe(this.onChange);
|
|
880
|
+
this.missedChangeDetected = false;
|
|
881
|
+
this.accountsWithMissedUpdates.clear();
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/**
|
|
886
|
+
* Signal that a missed change was detected and schedule resubscription
|
|
887
|
+
*/
|
|
888
|
+
private signalMissedChange(accountIdString: string): void {
|
|
889
|
+
if (!this.missedChangeDetected) {
|
|
890
|
+
this.missedChangeDetected = true;
|
|
891
|
+
this.accountsWithMissedUpdates.add(accountIdString);
|
|
892
|
+
|
|
893
|
+
// Clear any existing resubscription timeout
|
|
894
|
+
if (this.resubscriptionTimeout) {
|
|
895
|
+
clearTimeout(this.resubscriptionTimeout);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// Schedule resubscription after a short delay to allow for batch processing
|
|
899
|
+
this.resubscriptionTimeout = setTimeout(async () => {
|
|
900
|
+
await this.handleResubscription();
|
|
901
|
+
}, 100); // 100ms delay to allow for batch processing
|
|
902
|
+
} else {
|
|
903
|
+
// If already detected, just add the account to the set
|
|
904
|
+
this.accountsWithMissedUpdates.add(accountIdString);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
unsubscribe(onResub = false): Promise<void> {
|
|
909
|
+
if (!onResub) {
|
|
910
|
+
this.resubOpts.resubTimeoutMs = undefined;
|
|
911
|
+
}
|
|
912
|
+
this.isUnsubscribing = true;
|
|
913
|
+
clearTimeout(this.timeoutId);
|
|
914
|
+
this.timeoutId = undefined;
|
|
915
|
+
|
|
916
|
+
// Clear polling timeouts
|
|
917
|
+
this.clearPollingTimeouts();
|
|
918
|
+
|
|
919
|
+
// Abort the WebSocket subscription
|
|
920
|
+
if (this.abortController) {
|
|
921
|
+
this.abortController.abort('unsubscribing');
|
|
922
|
+
this.abortController = undefined;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
this.listenerId = undefined;
|
|
926
|
+
this.isUnsubscribing = false;
|
|
927
|
+
|
|
928
|
+
return Promise.resolve();
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// Method to add accounts to the polling list
|
|
932
|
+
/**
|
|
933
|
+
* Add an account to the monitored set.
|
|
934
|
+
* - Monitored accounts are subject to initial fetch and periodic batch polls
|
|
935
|
+
* if WS notifications are not observed within `pollingIntervalMs`.
|
|
936
|
+
*/
|
|
937
|
+
addAccountToMonitor(accountId: PublicKey): void {
|
|
938
|
+
const accountIdString = accountId.toBase58();
|
|
939
|
+
this.accountsToMonitor.add(accountIdString);
|
|
940
|
+
|
|
941
|
+
// If already subscribed, start monitoring for this account
|
|
942
|
+
if (this.listenerId != null && !this.isUnsubscribing) {
|
|
943
|
+
this.startMonitoringForAccount(accountIdString);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// Method to remove accounts from the polling list
|
|
948
|
+
removeAccountFromMonitor(accountId: PublicKey): void {
|
|
949
|
+
const accountIdString = accountId.toBase58();
|
|
950
|
+
this.accountsToMonitor.delete(accountIdString);
|
|
951
|
+
|
|
952
|
+
// Clear monitoring timeout for this account
|
|
953
|
+
const timeoutId = this.pollingTimeouts.get(accountIdString);
|
|
954
|
+
if (timeoutId) {
|
|
955
|
+
clearTimeout(timeoutId);
|
|
956
|
+
this.pollingTimeouts.delete(accountIdString);
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// Remove from currently polling set if it was being polled
|
|
960
|
+
this.accountsCurrentlyPolling.delete(accountIdString);
|
|
961
|
+
|
|
962
|
+
// If no more accounts are being polled, stop batch polling
|
|
963
|
+
if (this.accountsCurrentlyPolling.size === 0 && this.batchPollingTimeout) {
|
|
964
|
+
clearTimeout(this.batchPollingTimeout);
|
|
965
|
+
this.batchPollingTimeout = undefined;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// Method to set polling interval
|
|
970
|
+
/**
|
|
971
|
+
* Set the monitoring/polling interval for monitored accounts.
|
|
972
|
+
* Shorter intervals detect missed updates sooner but increase RPC load.
|
|
973
|
+
*/
|
|
974
|
+
setPollingInterval(intervalMs: number): void {
|
|
975
|
+
this.pollingIntervalMs = intervalMs;
|
|
976
|
+
// Restart monitoring with new interval if already subscribed
|
|
977
|
+
if (this.listenerId != null && !this.isUnsubscribing) {
|
|
978
|
+
this.startMonitoringForAccounts();
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
private updateBufferAndHandleChange(
|
|
983
|
+
newBuffer: Buffer,
|
|
984
|
+
newSlot: number,
|
|
985
|
+
accountIdString: string
|
|
986
|
+
) {
|
|
987
|
+
this.bufferAndSlotMap.set(accountIdString, {
|
|
988
|
+
buffer: newBuffer,
|
|
989
|
+
slot: newSlot,
|
|
990
|
+
});
|
|
991
|
+
const account = this.decodeBuffer(this.accountDiscriminator, newBuffer);
|
|
992
|
+
const accountIdPubkey = new PublicKey(accountIdString);
|
|
993
|
+
this.onChange(accountIdPubkey, account, { slot: newSlot }, newBuffer);
|
|
994
|
+
}
|
|
995
|
+
}
|