@drift-labs/sdk 2.145.0 → 2.146.0-alpha.13

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.
Files changed (99) hide show
  1. package/.env +4 -0
  2. package/VERSION +1 -1
  3. package/lib/browser/accounts/grpcMultiUserAccountSubscriber.js +8 -1
  4. package/lib/browser/accounts/webSocketProgramAccountSubscriberV2.d.ts +99 -7
  5. package/lib/browser/accounts/webSocketProgramAccountSubscriberV2.js +435 -144
  6. package/lib/browser/adminClient.d.ts +5 -1
  7. package/lib/browser/adminClient.js +57 -23
  8. package/lib/browser/constants/numericConstants.d.ts +2 -0
  9. package/lib/browser/constants/numericConstants.js +5 -1
  10. package/lib/browser/constants/perpMarkets.js +0 -2
  11. package/lib/browser/decode/user.js +4 -0
  12. package/lib/browser/driftClient.d.ts +25 -10
  13. package/lib/browser/driftClient.js +238 -41
  14. package/lib/browser/driftClientConfig.d.ts +7 -2
  15. package/lib/browser/idl/drift.json +245 -22
  16. package/lib/browser/index.d.ts +4 -0
  17. package/lib/browser/index.js +9 -1
  18. package/lib/browser/marginCalculation.d.ts +86 -0
  19. package/lib/browser/marginCalculation.js +209 -0
  20. package/lib/browser/math/margin.d.ts +1 -1
  21. package/lib/browser/math/margin.js +8 -1
  22. package/lib/browser/math/position.d.ts +1 -0
  23. package/lib/browser/math/position.js +10 -2
  24. package/lib/browser/math/spotPosition.d.ts +1 -1
  25. package/lib/browser/math/spotPosition.js +3 -2
  26. package/lib/browser/math/superStake.d.ts +3 -2
  27. package/lib/browser/types.d.ts +13 -0
  28. package/lib/browser/types.js +12 -1
  29. package/lib/browser/user.d.ts +59 -11
  30. package/lib/browser/user.js +348 -43
  31. package/lib/node/accounts/grpcMultiUserAccountSubscriber.d.ts.map +1 -1
  32. package/lib/node/accounts/grpcMultiUserAccountSubscriber.js +8 -1
  33. package/lib/node/accounts/webSocketProgramAccountSubscriberV2.d.ts +99 -7
  34. package/lib/node/accounts/webSocketProgramAccountSubscriberV2.d.ts.map +1 -1
  35. package/lib/node/accounts/webSocketProgramAccountSubscriberV2.js +435 -144
  36. package/lib/node/adminClient.d.ts +5 -1
  37. package/lib/node/adminClient.d.ts.map +1 -1
  38. package/lib/node/adminClient.js +57 -23
  39. package/lib/node/constants/numericConstants.d.ts +2 -0
  40. package/lib/node/constants/numericConstants.d.ts.map +1 -1
  41. package/lib/node/constants/numericConstants.js +5 -1
  42. package/lib/node/constants/perpMarkets.d.ts.map +1 -1
  43. package/lib/node/constants/perpMarkets.js +0 -2
  44. package/lib/node/decode/user.d.ts.map +1 -1
  45. package/lib/node/decode/user.js +4 -0
  46. package/lib/node/driftClient.d.ts +25 -10
  47. package/lib/node/driftClient.d.ts.map +1 -1
  48. package/lib/node/driftClient.js +238 -41
  49. package/lib/node/driftClientConfig.d.ts +7 -2
  50. package/lib/node/driftClientConfig.d.ts.map +1 -1
  51. package/lib/node/idl/drift.json +245 -22
  52. package/lib/node/index.d.ts +4 -0
  53. package/lib/node/index.d.ts.map +1 -1
  54. package/lib/node/index.js +9 -1
  55. package/lib/node/marginCalculation.d.ts +87 -0
  56. package/lib/node/marginCalculation.d.ts.map +1 -0
  57. package/lib/node/marginCalculation.js +209 -0
  58. package/lib/node/math/margin.d.ts +1 -1
  59. package/lib/node/math/margin.d.ts.map +1 -1
  60. package/lib/node/math/margin.js +8 -1
  61. package/lib/node/math/position.d.ts +1 -0
  62. package/lib/node/math/position.d.ts.map +1 -1
  63. package/lib/node/math/position.js +10 -2
  64. package/lib/node/math/spotPosition.d.ts +1 -1
  65. package/lib/node/math/spotPosition.d.ts.map +1 -1
  66. package/lib/node/math/spotPosition.js +3 -2
  67. package/lib/node/math/superStake.d.ts +3 -2
  68. package/lib/node/math/superStake.d.ts.map +1 -1
  69. package/lib/node/types.d.ts +13 -0
  70. package/lib/node/types.d.ts.map +1 -1
  71. package/lib/node/types.js +12 -1
  72. package/lib/node/user.d.ts +59 -11
  73. package/lib/node/user.d.ts.map +1 -1
  74. package/lib/node/user.js +348 -43
  75. package/package.json +1 -1
  76. package/scripts/deposit-isolated-positions.ts +110 -0
  77. package/scripts/single-grpc-client-test.ts +71 -21
  78. package/scripts/withdraw-isolated-positions.ts +174 -0
  79. package/src/accounts/grpcMultiUserAccountSubscriber.ts +8 -1
  80. package/src/accounts/webSocketProgramAccountSubscriberV2.ts +566 -167
  81. package/src/adminClient.ts +74 -25
  82. package/src/constants/numericConstants.ts +5 -0
  83. package/src/constants/perpMarkets.ts +0 -3
  84. package/src/decode/user.ts +7 -1
  85. package/src/driftClient.ts +465 -52
  86. package/src/driftClientConfig.ts +15 -8
  87. package/src/idl/drift.json +246 -23
  88. package/src/index.ts +4 -0
  89. package/src/margin/README.md +143 -0
  90. package/src/marginCalculation.ts +306 -0
  91. package/src/math/margin.ts +13 -1
  92. package/src/math/position.ts +12 -2
  93. package/src/math/spotPosition.ts +6 -2
  94. package/src/types.ts +16 -0
  95. package/src/user.ts +623 -81
  96. package/tests/amm/test.ts +1 -1
  97. package/tests/dlob/helpers.ts +6 -3
  98. package/tests/user/getMarginCalculation.ts +405 -0
  99. package/tests/user/test.ts +0 -7
@@ -3,11 +3,70 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.WebSocketProgramAccountSubscriberV2 = void 0;
6
+ exports.WebSocketProgramAccountsSubscriberV2 = void 0;
7
7
  const web3_js_1 = require("@solana/web3.js");
8
8
  const gill_1 = require("gill");
9
9
  const bs58_1 = __importDefault(require("bs58"));
10
- class WebSocketProgramAccountSubscriberV2 {
10
+ /**
11
+ * WebSocketProgramAccountsSubscriberV2
12
+ *
13
+ * High-level overview
14
+ * - WebSocket-first subscriber for Solana program accounts that also layers in
15
+ * targeted polling to detect missed updates reliably.
16
+ * - Emits decoded account updates via the provided `onChange` callback.
17
+ * - Designed to focus extra work on the specific accounts the consumer cares
18
+ * about ("monitored accounts") while keeping baseline WS behavior for the
19
+ * full program subscription.
20
+ *
21
+ * Why polling if this is a WebSocket subscriber?
22
+ * - WS infra can stall, drop, or reorder notifications under network stress or
23
+ * provider hiccups. When that happens, critical account changes can be missed.
24
+ * - To mitigate this, the class accepts a set of accounts (provided via constructor) to monitor
25
+ * and uses light polling to verify whether a WS change was missed.
26
+ * - If polling detects a newer slot with different data than the last seen
27
+ * buffer, a centralized resubscription is triggered to restore a clean stream.
28
+ *
29
+ * Initial fetch (on subscribe)
30
+ * - On `subscribe()`, we first perform a single batched fetch of all monitored
31
+ * accounts ("initial monitor fetch").
32
+ * - Purpose: seed the internal `bufferAndSlotMap` and emit the latest state so
33
+ * consumers have up-to-date data immediately, even before WS events arrive.
34
+ * - This step does not decide resubscription; it only establishes ground truth.
35
+ *
36
+ * Continuous polling (only for monitored accounts)
37
+ * - After seeding, each monitored account is put into a monitoring cycle:
38
+ * 1) If no WS notification for an account is observed for `pollingIntervalMs`,
39
+ * we enqueue it for a batched fetch (buffered for a short window).
40
+ * 2) Once an account enters the "currently polling" set, a shared batch poll
41
+ * runs every `pollingIntervalMs` across all such accounts.
42
+ * 3) If WS notifications resume for an account, that account is removed from
43
+ * the polling set and returns to passive monitoring.
44
+ * - Polling compares the newly fetched buffer with the last stored buffer at a
45
+ * later slot. A difference indicates a missed update; we schedule a single
46
+ * resubscription (coalesced across accounts) to re-sync.
47
+ *
48
+ * Accounts the consumer cares about
49
+ * - Provide accounts up-front via the constructor `accountsToMonitor`, or add
50
+ * them dynamically with `addAccountToMonitor()` and remove with
51
+ * `removeAccountFromMonitor()`.
52
+ * - Only these accounts incur additional polling safeguards; other accounts are
53
+ * still processed from the WS stream normally.
54
+ *
55
+ * Resubscription strategy
56
+ * - Missed updates from any monitored account are coalesced and trigger a single
57
+ * resubscription after a short delay. This avoids rapid churn.
58
+ * - If `resubOpts.resubTimeoutMs` is set, an inactivity timer also performs a
59
+ * batch check of monitored accounts. If a missed update is found, the same
60
+ * centralized resubscription flow is used.
61
+ *
62
+ * Tuning knobs
63
+ * - `setPollingInterval(ms)`: adjust how often monitoring/polling runs
64
+ * (default 30s). Shorter = faster detection, higher RPC load.
65
+ * - Debounced immediate poll (~100ms): batches accounts added to polling right after inactivity.
66
+ * - Batch size for `getMultipleAccounts` is limited to 100, requests are chunked
67
+ * and processed concurrently.
68
+ */
69
+ class WebSocketProgramAccountsSubscriberV2 {
11
70
  constructor(subscriptionName, accountDiscriminator, program, decodeBufferFn, options = {
12
71
  filters: [],
13
72
  }, resubOpts, accountsToMonitor // Optional list of accounts to poll
@@ -22,11 +81,19 @@ class WebSocketProgramAccountSubscriberV2 {
22
81
  this.pollingTimeouts = new Map();
23
82
  this.lastWsNotificationTime = new Map(); // Track last WS notification time per account
24
83
  this.accountsCurrentlyPolling = new Set(); // Track which accounts are being polled
84
+ this.debouncedImmediatePollMs = 100; // configurable short window
85
+ // Centralized resubscription handling
86
+ this.missedChangeDetected = false; // Flag to track if any missed change was detected
87
+ this.accountsWithMissedUpdates = new Set(); // Track which accounts had missed updates
25
88
  this.subscriptionName = subscriptionName;
26
89
  this.accountDiscriminator = accountDiscriminator;
27
90
  this.program = program;
28
91
  this.decodeBuffer = decodeBufferFn;
29
- this.resubOpts = resubOpts;
92
+ this.resubOpts = resubOpts !== null && resubOpts !== void 0 ? resubOpts : {
93
+ resubTimeoutMs: 30000,
94
+ usePollingInsteadOfResub: true,
95
+ logResubMessages: false,
96
+ };
30
97
  if (((_a = this.resubOpts) === null || _a === void 0 ? void 0 : _a.resubTimeoutMs) < 1000) {
31
98
  console.log('resubTimeoutMs should be at least 1000ms to avoid spamming resub');
32
99
  }
@@ -47,52 +114,101 @@ class WebSocketProgramAccountSubscriberV2 {
47
114
  this.rpc = rpc;
48
115
  this.rpcSubscriptions = rpcSubscriptions;
49
116
  }
117
+ async handleNotificationLoop(notificationPromise) {
118
+ var _a;
119
+ try {
120
+ const subscriptionIterable = await notificationPromise;
121
+ for await (const notification of subscriptionIterable) {
122
+ try {
123
+ if ((_a = this.resubOpts) === null || _a === void 0 ? void 0 : _a.resubTimeoutMs) {
124
+ this.receivingData = true;
125
+ clearTimeout(this.timeoutId);
126
+ this.handleRpcResponse(notification.context, notification.value.pubkey, notification.value.account.data);
127
+ this.setTimeout();
128
+ }
129
+ else {
130
+ this.handleRpcResponse(notification.context, notification.value.pubkey, notification.value.account.data);
131
+ }
132
+ }
133
+ catch (error) {
134
+ console.error(`Error handling RPC response for pubkey ${notification.value.pubkey}:`, error);
135
+ }
136
+ }
137
+ }
138
+ catch (error) {
139
+ console.error(`[${this.subscriptionName}] Error in notification loop:`, error);
140
+ }
141
+ }
50
142
  async subscribe(onChange) {
51
143
  var _a, _b;
144
+ /**
145
+ * Start the WebSocket subscription and initialize polling safeguards.
146
+ *
147
+ * Flow
148
+ * - Seeds all monitored accounts with a single batched RPC fetch and emits
149
+ * their current state.
150
+ * - Subscribes to program notifications via WS using gill.
151
+ * - If `resubOpts.resubTimeoutMs` is set, starts an inactivity timer that
152
+ * batch-checks monitored accounts when WS goes quiet.
153
+ * - Begins monitoring for accounts that may need polling when WS
154
+ * notifications are not observed within `pollingIntervalMs`.
155
+ *
156
+ * @param onChange Callback invoked with decoded account data when an update
157
+ * is detected (via WS or batch RPC fetch).
158
+ */
159
+ const startTime = performance.now();
52
160
  if (this.listenerId != null || this.isUnsubscribing) {
53
161
  return;
54
162
  }
163
+ if ((_a = this.resubOpts) === null || _a === void 0 ? void 0 : _a.logResubMessages) {
164
+ console.log(`[${this.subscriptionName}] initializing subscription. This many monitored accounts: ${this.accountsToMonitor.size}`);
165
+ }
55
166
  this.onChange = onChange;
167
+ // initial fetch of monitored data - only fetch and populate, don't check for missed changes
168
+ await this.fetchAndPopulateAllMonitoredAccounts();
56
169
  // Create abort controller for proper cleanup
57
170
  const abortController = new AbortController();
58
171
  this.abortController = abortController;
172
+ this.listenerId = Math.random(); // Unique ID for logging purposes
173
+ if ((_b = this.resubOpts) === null || _b === void 0 ? void 0 : _b.resubTimeoutMs) {
174
+ this.receivingData = true;
175
+ this.setTimeout();
176
+ }
59
177
  // Subscribe to program account changes using gill's rpcSubscriptions
60
178
  const programId = this.program.programId.toBase58();
61
179
  if ((0, gill_1.isAddress)(programId)) {
62
- const subscription = await this.rpcSubscriptions
180
+ const subscriptionPromise = this.rpcSubscriptions
63
181
  .programNotifications(programId, {
64
182
  commitment: this.options.commitment,
65
183
  encoding: 'base64',
66
- filters: this.options.filters.map((filter) => ({
67
- memcmp: {
68
- offset: BigInt(filter.memcmp.offset),
69
- bytes: filter.memcmp.bytes,
70
- encoding: 'base64',
71
- },
72
- })),
184
+ filters: this.options.filters.map((filter) => {
185
+ // Convert filter bytes from base58 to base64 if needed
186
+ let bytes = filter.memcmp.bytes;
187
+ if (typeof bytes === 'string' &&
188
+ /^[1-9A-HJ-NP-Za-km-z]+$/.test(bytes)) {
189
+ // Looks like base58 - convert to base64
190
+ const decoded = bs58_1.default.decode(bytes);
191
+ bytes = Buffer.from(decoded).toString('base64');
192
+ }
193
+ return {
194
+ memcmp: {
195
+ offset: BigInt(filter.memcmp.offset),
196
+ bytes: bytes,
197
+ encoding: 'base64',
198
+ },
199
+ };
200
+ }),
73
201
  })
74
202
  .subscribe({
75
203
  abortSignal: abortController.signal,
76
204
  });
77
- for await (const notification of subscription) {
78
- if ((_a = this.resubOpts) === null || _a === void 0 ? void 0 : _a.resubTimeoutMs) {
79
- this.receivingData = true;
80
- clearTimeout(this.timeoutId);
81
- this.handleRpcResponse(notification.context, notification.value.account);
82
- this.setTimeout();
83
- }
84
- else {
85
- this.handleRpcResponse(notification.context, notification.value.account);
86
- }
87
- }
88
- }
89
- this.listenerId = Math.random(); // Unique ID for logging purposes
90
- if ((_b = this.resubOpts) === null || _b === void 0 ? void 0 : _b.resubTimeoutMs) {
91
- this.receivingData = true;
92
- this.setTimeout();
205
+ // Start notification loop without awaiting
206
+ this.handleNotificationLoop(subscriptionPromise);
207
+ // Start monitoring for accounts that may need polling if no WS event is received
208
+ this.startMonitoringForAccounts();
93
209
  }
94
- // Start monitoring for accounts that may need polling if no WS event is received
95
- this.startMonitoringForAccounts();
210
+ const endTime = performance.now();
211
+ console.log(`[PROFILING] ${this.subscriptionName}.subscribe() completed in ${endTime - startTime}ms`);
96
212
  }
97
213
  setTimeout() {
98
214
  var _a;
@@ -107,44 +223,46 @@ class WebSocketProgramAccountSubscriberV2 {
107
223
  }
108
224
  if (this.receivingData) {
109
225
  if ((_a = this.resubOpts) === null || _a === void 0 ? void 0 : _a.logResubMessages) {
110
- console.log(`No ws data from ${this.subscriptionName} in ${(_b = this.resubOpts) === null || _b === void 0 ? void 0 : _b.resubTimeoutMs}ms, resubscribing`);
226
+ console.log(`No ws data from ${this.subscriptionName} in ${(_b = this.resubOpts) === null || _b === void 0 ? void 0 : _b.resubTimeoutMs}ms, checking for missed changes`);
227
+ }
228
+ // Check for missed changes in monitored accounts
229
+ const missedChangeDetected = await this.fetchAllMonitoredAccounts();
230
+ if (missedChangeDetected) {
231
+ // Signal missed change with a generic identifier since we don't have specific account IDs from this context
232
+ this.signalMissedChange('timeout-check');
233
+ }
234
+ else {
235
+ // No missed changes, continue monitoring
236
+ this.receivingData = false;
237
+ this.setTimeout();
111
238
  }
112
- await this.unsubscribe(true);
113
- this.receivingData = false;
114
- await this.subscribe(this.onChange);
115
239
  }
116
240
  }, (_a = this.resubOpts) === null || _a === void 0 ? void 0 : _a.resubTimeoutMs);
117
241
  }
118
- handleRpcResponse(context, accountInfo) {
242
+ handleRpcResponse(context, accountId, accountInfo) {
119
243
  const newSlot = Number(context.slot);
120
244
  let newBuffer = undefined;
121
245
  if (accountInfo) {
122
- // Extract data from gill response
123
- if (accountInfo.data) {
124
- // Handle different data formats from gill
125
- if (Array.isArray(accountInfo.data)) {
126
- // If it's a tuple [data, encoding]
127
- const [data, encoding] = accountInfo.data;
128
- if (encoding === 'base58') {
129
- // Convert base58 to buffer using bs58
130
- newBuffer = Buffer.from(bs58_1.default.decode(data));
131
- }
132
- else {
133
- newBuffer = Buffer.from(data, 'base64');
134
- }
246
+ // Handle different data formats from gill
247
+ if (Array.isArray(accountInfo)) {
248
+ // If it's a tuple [data, encoding]
249
+ const [data, encoding] = accountInfo;
250
+ if (encoding === 'base58') {
251
+ // Convert base58 to buffer using bs58
252
+ newBuffer = Buffer.from(bs58_1.default.decode(data));
253
+ }
254
+ else {
255
+ newBuffer = Buffer.from(data, 'base64');
135
256
  }
136
257
  }
137
258
  }
138
- // Convert gill's account key to PublicKey
139
- // Note: accountInfo doesn't have a key property, we need to get it from the notification
140
- // For now, we'll use a placeholder - this needs to be fixed based on the actual gill API
141
- const accountId = new web3_js_1.PublicKey('11111111111111111111111111111111'); // Placeholder
142
- const accountIdString = accountId.toBase58();
259
+ const accountIdString = accountId.toString();
143
260
  const existingBufferAndSlot = this.bufferAndSlotMap.get(accountIdString);
144
261
  // Track WebSocket notification time for this account
145
262
  this.lastWsNotificationTime.set(accountIdString, Date.now());
146
- // If this account was being polled, stop polling it
147
- if (this.accountsCurrentlyPolling.has(accountIdString)) {
263
+ // If this account was being polled, stop polling it if the buffer has changed
264
+ if (this.accountsCurrentlyPolling.has(accountIdString) &&
265
+ !(existingBufferAndSlot === null || existingBufferAndSlot === void 0 ? void 0 : existingBufferAndSlot.buffer.equals(newBuffer))) {
148
266
  this.accountsCurrentlyPolling.delete(accountIdString);
149
267
  // If no more accounts are being polled, stop batch polling
150
268
  if (this.accountsCurrentlyPolling.size === 0 &&
@@ -155,12 +273,7 @@ class WebSocketProgramAccountSubscriberV2 {
155
273
  }
156
274
  if (!existingBufferAndSlot) {
157
275
  if (newBuffer) {
158
- this.bufferAndSlotMap.set(accountIdString, {
159
- buffer: newBuffer,
160
- slot: newSlot,
161
- });
162
- const account = this.decodeBuffer(this.accountDiscriminator, newBuffer);
163
- this.onChange(accountId, account, { slot: newSlot }, newBuffer);
276
+ this.updateBufferAndHandleChange(newBuffer, newSlot, accountIdString);
164
277
  }
165
278
  return;
166
279
  }
@@ -169,12 +282,7 @@ class WebSocketProgramAccountSubscriberV2 {
169
282
  }
170
283
  const oldBuffer = existingBufferAndSlot.buffer;
171
284
  if (newBuffer && (!oldBuffer || !newBuffer.equals(oldBuffer))) {
172
- this.bufferAndSlotMap.set(accountIdString, {
173
- buffer: newBuffer,
174
- slot: newSlot,
175
- });
176
- const account = this.decodeBuffer(this.accountDiscriminator, newBuffer);
177
- this.onChange(accountId, account, { slot: newSlot }, newBuffer);
285
+ this.updateBufferAndHandleChange(newBuffer, newSlot, accountIdString);
178
286
  }
179
287
  }
180
288
  startMonitoringForAccounts() {
@@ -193,15 +301,18 @@ class WebSocketProgramAccountSubscriberV2 {
193
301
  }
194
302
  // Set up monitoring timeout - only start polling if no WS notification in 30s
195
303
  const timeoutId = setTimeout(async () => {
304
+ var _a;
196
305
  // Check if we've received a WS notification for this account recently
197
- const lastNotificationTime = this.lastWsNotificationTime.get(accountIdString);
306
+ const lastNotificationTime = this.lastWsNotificationTime.get(accountIdString) || 0;
198
307
  const currentTime = Date.now();
199
308
  if (!lastNotificationTime ||
200
309
  currentTime - lastNotificationTime >= this.pollingIntervalMs) {
201
- // No recent WS notification, start polling
202
- await this.pollAccount(accountIdString);
203
- // Schedule next poll
204
- this.startPollingForAccount(accountIdString);
310
+ if ((_a = this.resubOpts) === null || _a === void 0 ? void 0 : _a.logResubMessages) {
311
+ console.debug(`[${this.subscriptionName}] No recent WS notification for ${accountIdString}, adding to polling set`);
312
+ }
313
+ // No recent WS notification: add to polling and schedule debounced poll
314
+ this.accountsCurrentlyPolling.add(accountIdString);
315
+ this.scheduleDebouncedImmediatePoll();
205
316
  }
206
317
  else {
207
318
  // We received a WS notification recently, continue monitoring
@@ -210,15 +321,32 @@ class WebSocketProgramAccountSubscriberV2 {
210
321
  }, this.pollingIntervalMs);
211
322
  this.pollingTimeouts.set(accountIdString, timeoutId);
212
323
  }
213
- startPollingForAccount(accountIdString) {
214
- // Add account to polling set
215
- this.accountsCurrentlyPolling.add(accountIdString);
216
- // If this is the first account being polled, start batch polling
217
- if (this.accountsCurrentlyPolling.size === 1) {
218
- this.startBatchPolling();
324
+ scheduleDebouncedImmediatePoll() {
325
+ if (this.debouncedImmediatePollTimeout) {
326
+ clearTimeout(this.debouncedImmediatePollTimeout);
219
327
  }
328
+ this.debouncedImmediatePollTimeout = setTimeout(async () => {
329
+ var _a;
330
+ try {
331
+ await this.pollAllAccounts();
332
+ // After the immediate poll, ensure continuous batch polling is active
333
+ if (!this.batchPollingTimeout &&
334
+ this.accountsCurrentlyPolling.size > 0) {
335
+ this.startBatchPolling();
336
+ }
337
+ }
338
+ catch (e) {
339
+ if ((_a = this.resubOpts) === null || _a === void 0 ? void 0 : _a.logResubMessages) {
340
+ console.log(`[${this.subscriptionName}] Error during debounced immediate poll:`, e);
341
+ }
342
+ }
343
+ }, this.debouncedImmediatePollMs);
220
344
  }
221
345
  startBatchPolling() {
346
+ var _a;
347
+ if ((_a = this.resubOpts) === null || _a === void 0 ? void 0 : _a.logResubMessages) {
348
+ console.debug(`[${this.subscriptionName}] Scheduling batch polling`);
349
+ }
222
350
  // Clear existing batch polling timeout
223
351
  if (this.batchPollingTimeout) {
224
352
  clearTimeout(this.batchPollingTimeout);
@@ -238,8 +366,32 @@ class WebSocketProgramAccountSubscriberV2 {
238
366
  if (accountsToPoll.length === 0) {
239
367
  return;
240
368
  }
369
+ if ((_a = this.resubOpts) === null || _a === void 0 ? void 0 : _a.logResubMessages) {
370
+ console.debug(`[${this.subscriptionName}] Polling all accounts`, accountsToPoll.length, 'accounts');
371
+ }
372
+ // Use the shared batch fetch method
373
+ await this.fetchAccountsBatch(accountsToPoll);
374
+ }
375
+ catch (error) {
376
+ if ((_b = this.resubOpts) === null || _b === void 0 ? void 0 : _b.logResubMessages) {
377
+ console.log(`[${this.subscriptionName}] Error batch polling accounts:`, error);
378
+ }
379
+ }
380
+ }
381
+ /**
382
+ * Fetches and populates all monitored accounts data without checking for missed changes
383
+ * This is used during initial subscription to populate data
384
+ */
385
+ async fetchAndPopulateAllMonitoredAccounts() {
386
+ var _a;
387
+ try {
388
+ // Get all accounts currently being polled
389
+ const accountsToMonitor = Array.from(this.accountsToMonitor);
390
+ if (accountsToMonitor.length === 0) {
391
+ return;
392
+ }
241
393
  // Fetch all accounts in a single batch request
242
- const accountAddresses = accountsToPoll.map((accountId) => accountId);
394
+ const accountAddresses = accountsToMonitor.map((accountId) => accountId);
243
395
  const rpcResponse = await this.rpc
244
396
  .getMultipleAccounts(accountAddresses, {
245
397
  commitment: this.options.commitment,
@@ -248,8 +400,8 @@ class WebSocketProgramAccountSubscriberV2 {
248
400
  .send();
249
401
  const currentSlot = Number(rpcResponse.context.slot);
250
402
  // Process each account response
251
- for (let i = 0; i < accountsToPoll.length; i++) {
252
- const accountIdString = accountsToPoll[i];
403
+ for (let i = 0; i < accountsToMonitor.length; i++) {
404
+ const accountIdString = accountsToMonitor[i];
253
405
  const accountInfo = rpcResponse.value[i];
254
406
  if (!accountInfo) {
255
407
  continue;
@@ -258,24 +410,18 @@ class WebSocketProgramAccountSubscriberV2 {
258
410
  if (!existingBufferAndSlot) {
259
411
  // Account not in our map yet, add it
260
412
  let newBuffer = undefined;
261
- if (accountInfo.data) {
413
+ if (accountInfo) {
262
414
  if (Array.isArray(accountInfo.data)) {
263
415
  const [data, encoding] = accountInfo.data;
264
416
  newBuffer = Buffer.from(data, encoding);
265
417
  }
266
418
  }
267
419
  if (newBuffer) {
268
- this.bufferAndSlotMap.set(accountIdString, {
269
- buffer: newBuffer,
270
- slot: currentSlot,
271
- });
272
- const account = this.decodeBuffer(this.accountDiscriminator, newBuffer);
273
- const accountId = new web3_js_1.PublicKey(accountIdString);
274
- this.onChange(accountId, account, { slot: currentSlot }, newBuffer);
420
+ this.updateBufferAndHandleChange(newBuffer, currentSlot, accountIdString);
275
421
  }
276
422
  continue;
277
423
  }
278
- // Check if we missed an update
424
+ // For initial population, just update the slot if we have newer data
279
425
  if (currentSlot > existingBufferAndSlot.slot) {
280
426
  let newBuffer = undefined;
281
427
  if (accountInfo.data) {
@@ -289,70 +435,68 @@ class WebSocketProgramAccountSubscriberV2 {
289
435
  }
290
436
  }
291
437
  }
292
- // Check if buffer has changed
293
- if (newBuffer &&
294
- (!existingBufferAndSlot.buffer ||
295
- !newBuffer.equals(existingBufferAndSlot.buffer))) {
296
- if ((_a = this.resubOpts) === null || _a === void 0 ? void 0 : _a.logResubMessages) {
297
- console.log(`[${this.subscriptionName}] Batch polling detected missed update for account ${accountIdString}, resubscribing`);
298
- }
299
- // We missed an update, resubscribe
300
- await this.unsubscribe(true);
301
- this.receivingData = false;
302
- await this.subscribe(this.onChange);
303
- return;
438
+ // Update with newer data if available
439
+ if (newBuffer) {
440
+ this.updateBufferAndHandleChange(newBuffer, currentSlot, accountIdString);
304
441
  }
305
442
  }
306
443
  }
307
444
  }
308
445
  catch (error) {
309
- if ((_b = this.resubOpts) === null || _b === void 0 ? void 0 : _b.logResubMessages) {
310
- console.log(`[${this.subscriptionName}] Error batch polling accounts:`, error);
446
+ if ((_a = this.resubOpts) === null || _a === void 0 ? void 0 : _a.logResubMessages) {
447
+ console.log(`[${this.subscriptionName}] Error fetching and populating monitored accounts:`, error);
311
448
  }
312
449
  }
313
450
  }
314
- async pollAccount(accountIdString) {
451
+ /**
452
+ * Fetches all monitored accounts and checks for missed changes
453
+ * Returns true if a missed change was detected and resubscription is needed
454
+ */
455
+ async fetchAllMonitoredAccounts() {
315
456
  var _a, _b;
316
457
  try {
317
- // Fetch current account data using gill's rpc
318
- const accountAddress = accountIdString;
458
+ // Get all accounts currently being polled
459
+ const accountsToMonitor = Array.from(this.accountsToMonitor);
460
+ if (accountsToMonitor.length === 0) {
461
+ return false;
462
+ }
463
+ // Fetch all accounts in a single batch request
464
+ const accountAddresses = accountsToMonitor.map((accountId) => accountId);
319
465
  const rpcResponse = await this.rpc
320
- .getAccountInfo(accountAddress, {
466
+ .getMultipleAccounts(accountAddresses, {
321
467
  commitment: this.options.commitment,
322
468
  encoding: 'base64',
323
469
  })
324
470
  .send();
325
471
  const currentSlot = Number(rpcResponse.context.slot);
326
- const existingBufferAndSlot = this.bufferAndSlotMap.get(accountIdString);
327
- if (!existingBufferAndSlot) {
328
- // Account not in our map yet, add it
329
- if (rpcResponse.value) {
472
+ // Process each account response
473
+ for (let i = 0; i < accountsToMonitor.length; i++) {
474
+ const accountIdString = accountsToMonitor[i];
475
+ const accountInfo = rpcResponse.value[i];
476
+ if (!accountInfo) {
477
+ continue;
478
+ }
479
+ const existingBufferAndSlot = this.bufferAndSlotMap.get(accountIdString);
480
+ if (!existingBufferAndSlot) {
481
+ // Account not in our map yet, add it
330
482
  let newBuffer = undefined;
331
- if (rpcResponse.value.data) {
332
- if (Array.isArray(rpcResponse.value.data)) {
333
- const [data, encoding] = rpcResponse.value.data;
483
+ if (accountInfo.data) {
484
+ if (Array.isArray(accountInfo.data)) {
485
+ const [data, encoding] = accountInfo.data;
334
486
  newBuffer = Buffer.from(data, encoding);
335
487
  }
336
488
  }
337
489
  if (newBuffer) {
338
- this.bufferAndSlotMap.set(accountIdString, {
339
- buffer: newBuffer,
340
- slot: currentSlot,
341
- });
342
- const account = this.decodeBuffer(this.accountDiscriminator, newBuffer);
343
- const accountId = new web3_js_1.PublicKey(accountIdString);
344
- this.onChange(accountId, account, { slot: currentSlot }, newBuffer);
490
+ this.updateBufferAndHandleChange(newBuffer, currentSlot, accountIdString);
345
491
  }
492
+ continue;
346
493
  }
347
- return;
348
- }
349
- // Check if we missed an update
350
- if (currentSlot > existingBufferAndSlot.slot) {
351
- let newBuffer = undefined;
352
- if (rpcResponse.value) {
353
- if (rpcResponse.value.data) {
354
- if (Array.isArray(rpcResponse.value.data)) {
355
- const [data, encoding] = rpcResponse.value.data;
494
+ // Check if we missed an update
495
+ if (currentSlot > existingBufferAndSlot.slot) {
496
+ let newBuffer = undefined;
497
+ if (accountInfo.data) {
498
+ if (Array.isArray(accountInfo.data)) {
499
+ const [data, encoding] = accountInfo.data;
356
500
  if (encoding === 'base58') {
357
501
  newBuffer = Buffer.from(bs58_1.default.decode(data));
358
502
  }
@@ -361,25 +505,102 @@ class WebSocketProgramAccountSubscriberV2 {
361
505
  }
362
506
  }
363
507
  }
364
- }
365
- // Check if buffer has changed
366
- if (newBuffer &&
367
- (!existingBufferAndSlot.buffer ||
368
- !newBuffer.equals(existingBufferAndSlot.buffer))) {
369
- if ((_a = this.resubOpts) === null || _a === void 0 ? void 0 : _a.logResubMessages) {
370
- console.log(`[${this.subscriptionName}] Polling detected missed update for account ${accountIdString}, resubscribing`);
508
+ // Check if buffer has changed
509
+ if (newBuffer &&
510
+ (!existingBufferAndSlot.buffer ||
511
+ !newBuffer.equals(existingBufferAndSlot.buffer))) {
512
+ if ((_a = this.resubOpts) === null || _a === void 0 ? void 0 : _a.logResubMessages) {
513
+ console.log(`[${this.subscriptionName}] Batch polling detected missed update for account ${accountIdString}, resubscribing`);
514
+ }
515
+ // We missed an update, return true to indicate resubscription is needed
516
+ return true;
371
517
  }
372
- // We missed an update, resubscribe
373
- await this.unsubscribe(true);
374
- this.receivingData = false;
375
- await this.subscribe(this.onChange);
376
- return;
377
518
  }
378
519
  }
520
+ // No missed changes detected
521
+ return false;
379
522
  }
380
523
  catch (error) {
381
524
  if ((_b = this.resubOpts) === null || _b === void 0 ? void 0 : _b.logResubMessages) {
382
- console.log(`[${this.subscriptionName}] Error polling account ${accountIdString}:`, error);
525
+ console.log(`[${this.subscriptionName}] Error batch polling accounts:`, error);
526
+ }
527
+ return false;
528
+ }
529
+ }
530
+ async fetchAccountsBatch(accountIds) {
531
+ var _a;
532
+ try {
533
+ // Chunk account IDs into groups of 100 (getMultipleAccounts limit)
534
+ const chunkSize = 100;
535
+ const chunks = [];
536
+ for (let i = 0; i < accountIds.length; i += chunkSize) {
537
+ chunks.push(accountIds.slice(i, i + chunkSize));
538
+ }
539
+ // Process all chunks concurrently
540
+ await Promise.all(chunks.map(async (chunk) => {
541
+ var _a;
542
+ const accountAddresses = chunk.map((accountId) => accountId);
543
+ const rpcResponse = await this.rpc
544
+ .getMultipleAccounts(accountAddresses, {
545
+ commitment: this.options.commitment,
546
+ encoding: 'base64',
547
+ })
548
+ .send();
549
+ const currentSlot = Number(rpcResponse.context.slot);
550
+ // Process each account response in this chunk
551
+ for (let i = 0; i < chunk.length; i++) {
552
+ const accountIdString = chunk[i];
553
+ const accountInfo = rpcResponse.value[i];
554
+ if (!accountInfo) {
555
+ continue;
556
+ }
557
+ const existingBufferAndSlot = this.bufferAndSlotMap.get(accountIdString);
558
+ if (!existingBufferAndSlot) {
559
+ // Account not in our map yet, add it
560
+ let newBuffer = undefined;
561
+ if (accountInfo.data) {
562
+ if (Array.isArray(accountInfo.data)) {
563
+ const [data, encoding] = accountInfo.data;
564
+ newBuffer = Buffer.from(data, encoding);
565
+ }
566
+ }
567
+ if (newBuffer) {
568
+ this.updateBufferAndHandleChange(newBuffer, currentSlot, accountIdString);
569
+ }
570
+ continue;
571
+ }
572
+ // Check if we missed an update
573
+ if (currentSlot > existingBufferAndSlot.slot) {
574
+ let newBuffer = undefined;
575
+ if (accountInfo.data) {
576
+ if (Array.isArray(accountInfo.data)) {
577
+ const [data, encoding] = accountInfo.data;
578
+ if (encoding === 'base58') {
579
+ newBuffer = Buffer.from(bs58_1.default.decode(data));
580
+ }
581
+ else {
582
+ newBuffer = Buffer.from(data, 'base64');
583
+ }
584
+ }
585
+ }
586
+ // Check if buffer has changed
587
+ if (newBuffer &&
588
+ (!existingBufferAndSlot.buffer ||
589
+ !newBuffer.equals(existingBufferAndSlot.buffer))) {
590
+ if ((_a = this.resubOpts) === null || _a === void 0 ? void 0 : _a.logResubMessages) {
591
+ console.log(`[${this.subscriptionName}] Batch polling detected missed update for account ${accountIdString}, signaling resubscription`);
592
+ }
593
+ // Signal missed change instead of immediately resubscribing
594
+ this.signalMissedChange(accountIdString);
595
+ return;
596
+ }
597
+ }
598
+ }
599
+ }));
600
+ }
601
+ catch (error) {
602
+ if ((_a = this.resubOpts) === null || _a === void 0 ? void 0 : _a.logResubMessages) {
603
+ console.log(`[${this.subscriptionName}] Error fetching accounts batch:`, error);
383
604
  }
384
605
  }
385
606
  }
@@ -393,8 +614,60 @@ class WebSocketProgramAccountSubscriberV2 {
393
614
  clearTimeout(this.batchPollingTimeout);
394
615
  this.batchPollingTimeout = undefined;
395
616
  }
617
+ // Clear initial fetch timeout
618
+ // if (this.initialFetchTimeout) {
619
+ // clearTimeout(this.initialFetchTimeout);
620
+ // this.initialFetchTimeout = undefined;
621
+ // }
622
+ // Clear resubscription timeout
623
+ if (this.resubscriptionTimeout) {
624
+ clearTimeout(this.resubscriptionTimeout);
625
+ this.resubscriptionTimeout = undefined;
626
+ }
396
627
  // Clear accounts currently polling
397
628
  this.accountsCurrentlyPolling.clear();
629
+ // Clear accounts pending initial monitor fetch
630
+ // this.accountsPendingInitialMonitorFetch.clear();
631
+ // Reset missed change flag and clear accounts with missed updates
632
+ this.missedChangeDetected = false;
633
+ this.accountsWithMissedUpdates.clear();
634
+ }
635
+ /**
636
+ * Centralized resubscription handler that only resubscribes once after checking all accounts
637
+ */
638
+ async handleResubscription() {
639
+ var _a;
640
+ if (this.missedChangeDetected) {
641
+ if ((_a = this.resubOpts) === null || _a === void 0 ? void 0 : _a.logResubMessages) {
642
+ console.log(`[${this.subscriptionName}] Missed change detected for ${this.accountsWithMissedUpdates.size} accounts: ${Array.from(this.accountsWithMissedUpdates).join(', ')}, resubscribing`);
643
+ }
644
+ await this.unsubscribe(true);
645
+ this.receivingData = false;
646
+ await this.subscribe(this.onChange);
647
+ this.missedChangeDetected = false;
648
+ this.accountsWithMissedUpdates.clear();
649
+ }
650
+ }
651
+ /**
652
+ * Signal that a missed change was detected and schedule resubscription
653
+ */
654
+ signalMissedChange(accountIdString) {
655
+ if (!this.missedChangeDetected) {
656
+ this.missedChangeDetected = true;
657
+ this.accountsWithMissedUpdates.add(accountIdString);
658
+ // Clear any existing resubscription timeout
659
+ if (this.resubscriptionTimeout) {
660
+ clearTimeout(this.resubscriptionTimeout);
661
+ }
662
+ // Schedule resubscription after a short delay to allow for batch processing
663
+ this.resubscriptionTimeout = setTimeout(async () => {
664
+ await this.handleResubscription();
665
+ }, 100); // 100ms delay to allow for batch processing
666
+ }
667
+ else {
668
+ // If already detected, just add the account to the set
669
+ this.accountsWithMissedUpdates.add(accountIdString);
670
+ }
398
671
  }
399
672
  unsubscribe(onResub = false) {
400
673
  if (!onResub) {
@@ -415,6 +688,11 @@ class WebSocketProgramAccountSubscriberV2 {
415
688
  return Promise.resolve();
416
689
  }
417
690
  // Method to add accounts to the polling list
691
+ /**
692
+ * Add an account to the monitored set.
693
+ * - Monitored accounts are subject to initial fetch and periodic batch polls
694
+ * if WS notifications are not observed within `pollingIntervalMs`.
695
+ */
418
696
  addAccountToMonitor(accountId) {
419
697
  const accountIdString = accountId.toBase58();
420
698
  this.accountsToMonitor.add(accountIdString);
@@ -442,6 +720,10 @@ class WebSocketProgramAccountSubscriberV2 {
442
720
  }
443
721
  }
444
722
  // Method to set polling interval
723
+ /**
724
+ * Set the monitoring/polling interval for monitored accounts.
725
+ * Shorter intervals detect missed updates sooner but increase RPC load.
726
+ */
445
727
  setPollingInterval(intervalMs) {
446
728
  this.pollingIntervalMs = intervalMs;
447
729
  // Restart monitoring with new interval if already subscribed
@@ -449,5 +731,14 @@ class WebSocketProgramAccountSubscriberV2 {
449
731
  this.startMonitoringForAccounts();
450
732
  }
451
733
  }
734
+ updateBufferAndHandleChange(newBuffer, newSlot, accountIdString) {
735
+ this.bufferAndSlotMap.set(accountIdString, {
736
+ buffer: newBuffer,
737
+ slot: newSlot,
738
+ });
739
+ const account = this.decodeBuffer(this.accountDiscriminator, newBuffer);
740
+ const accountIdPubkey = new web3_js_1.PublicKey(accountIdString);
741
+ this.onChange(accountIdPubkey, account, { slot: newSlot }, newBuffer);
742
+ }
452
743
  }
453
- exports.WebSocketProgramAccountSubscriberV2 = WebSocketProgramAccountSubscriberV2;
744
+ exports.WebSocketProgramAccountsSubscriberV2 = WebSocketProgramAccountsSubscriberV2;