@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.
Files changed (77) hide show
  1. package/VERSION +1 -1
  2. package/lib/browser/accounts/types.d.ts +2 -0
  3. package/lib/browser/accounts/webSocketAccountSubscriberV2.d.ts +76 -3
  4. package/lib/browser/accounts/webSocketAccountSubscriberV2.js +211 -39
  5. package/lib/browser/accounts/webSocketDriftClientAccountSubscriberV2.d.ts +87 -0
  6. package/lib/browser/accounts/webSocketDriftClientAccountSubscriberV2.js +444 -0
  7. package/lib/browser/accounts/webSocketProgramAccountsSubscriberV2.d.ts +145 -0
  8. package/lib/browser/accounts/webSocketProgramAccountsSubscriberV2.js +744 -0
  9. package/lib/browser/accounts/websocketProgramUserAccountSubscriber.d.ts +22 -0
  10. package/lib/browser/accounts/websocketProgramUserAccountSubscriber.js +54 -0
  11. package/lib/browser/driftClient.js +22 -18
  12. package/lib/browser/driftClientConfig.d.ts +7 -2
  13. package/lib/browser/factory/bigNum.d.ts +2 -2
  14. package/lib/browser/factory/bigNum.js +20 -5
  15. package/lib/browser/index.d.ts +4 -0
  16. package/lib/browser/index.js +9 -1
  17. package/lib/browser/memcmp.d.ts +2 -0
  18. package/lib/browser/memcmp.js +19 -1
  19. package/lib/browser/oracles/oracleId.d.ts +5 -0
  20. package/lib/browser/oracles/oracleId.js +46 -1
  21. package/lib/browser/user.js +12 -5
  22. package/lib/browser/userConfig.d.ts +3 -0
  23. package/lib/node/accounts/types.d.ts +2 -0
  24. package/lib/node/accounts/types.d.ts.map +1 -1
  25. package/lib/node/accounts/webSocketAccountSubscriberV2.d.ts +76 -3
  26. package/lib/node/accounts/webSocketAccountSubscriberV2.d.ts.map +1 -1
  27. package/lib/node/accounts/webSocketAccountSubscriberV2.js +211 -39
  28. package/lib/node/accounts/webSocketDriftClientAccountSubscriberV2.d.ts +88 -0
  29. package/lib/node/accounts/webSocketDriftClientAccountSubscriberV2.d.ts.map +1 -0
  30. package/lib/node/accounts/webSocketDriftClientAccountSubscriberV2.js +444 -0
  31. package/lib/node/accounts/webSocketProgramAccountsSubscriberV2.d.ts +146 -0
  32. package/lib/node/accounts/webSocketProgramAccountsSubscriberV2.d.ts.map +1 -0
  33. package/lib/node/accounts/webSocketProgramAccountsSubscriberV2.js +744 -0
  34. package/lib/node/accounts/websocketProgramUserAccountSubscriber.d.ts +23 -0
  35. package/lib/node/accounts/websocketProgramUserAccountSubscriber.d.ts.map +1 -0
  36. package/lib/node/accounts/websocketProgramUserAccountSubscriber.js +54 -0
  37. package/lib/node/driftClient.d.ts.map +1 -1
  38. package/lib/node/driftClient.js +22 -18
  39. package/lib/node/driftClientConfig.d.ts +7 -2
  40. package/lib/node/driftClientConfig.d.ts.map +1 -1
  41. package/lib/node/factory/bigNum.d.ts +2 -2
  42. package/lib/node/factory/bigNum.d.ts.map +1 -1
  43. package/lib/node/factory/bigNum.js +20 -5
  44. package/lib/node/index.d.ts +4 -0
  45. package/lib/node/index.d.ts.map +1 -1
  46. package/lib/node/index.js +9 -1
  47. package/lib/node/memcmp.d.ts +2 -0
  48. package/lib/node/memcmp.d.ts.map +1 -1
  49. package/lib/node/memcmp.js +19 -1
  50. package/lib/node/oracles/oracleId.d.ts +5 -0
  51. package/lib/node/oracles/oracleId.d.ts.map +1 -1
  52. package/lib/node/oracles/oracleId.js +46 -1
  53. package/lib/node/user.d.ts.map +1 -1
  54. package/lib/node/user.js +12 -5
  55. package/lib/node/userConfig.d.ts +3 -0
  56. package/lib/node/userConfig.d.ts.map +1 -1
  57. package/package.json +1 -1
  58. package/src/accounts/README_WebSocketAccountSubscriberV2.md +41 -0
  59. package/src/accounts/types.ts +3 -0
  60. package/src/accounts/webSocketAccountSubscriberV2.ts +243 -42
  61. package/src/accounts/webSocketDriftClientAccountSubscriberV2.ts +745 -0
  62. package/src/accounts/webSocketProgramAccountsSubscriberV2.ts +995 -0
  63. package/src/accounts/websocketProgramUserAccountSubscriber.ts +94 -0
  64. package/src/driftClient.ts +13 -7
  65. package/src/driftClientConfig.ts +15 -8
  66. package/src/factory/bigNum.ts +22 -5
  67. package/src/index.ts +4 -0
  68. package/src/memcmp.ts +17 -0
  69. package/src/oracles/oracleId.ts +34 -0
  70. package/src/user.ts +21 -9
  71. package/src/userConfig.ts +3 -0
  72. package/lib/browser/accounts/webSocketProgramAccountSubscriberV2.d.ts +0 -53
  73. package/lib/browser/accounts/webSocketProgramAccountSubscriberV2.js +0 -453
  74. package/lib/node/accounts/webSocketProgramAccountSubscriberV2.d.ts +0 -54
  75. package/lib/node/accounts/webSocketProgramAccountSubscriberV2.d.ts.map +0 -1
  76. package/lib/node/accounts/webSocketProgramAccountSubscriberV2.js +0 -453
  77. package/src/accounts/webSocketProgramAccountSubscriberV2.ts +0 -596
@@ -8,16 +8,67 @@ import { AnchorProvider, Program } from '@coral-xyz/anchor';
8
8
  import { capitalize } from './utils';
9
9
  import {
10
10
  AccountInfoBase,
11
- AccountInfoWithBase58EncodedData,
12
11
  AccountInfoWithBase64EncodedData,
12
+ AccountInfoWithBase58EncodedData,
13
13
  createSolanaClient,
14
14
  isAddress,
15
+ Rpc,
16
+ RpcSubscriptions,
17
+ SolanaRpcSubscriptionsApi,
15
18
  type Address,
16
19
  type Commitment,
17
20
  } from 'gill';
18
21
  import { PublicKey } from '@solana/web3.js';
19
22
  import bs58 from 'bs58';
20
23
 
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
+ */
21
72
  export class WebSocketAccountSubscriberV2<T> implements AccountSubscriber<T> {
22
73
  dataAndSlot?: DataAndSlot<T>;
23
74
  bufferAndSlot?: BufferAndSlot;
@@ -29,12 +80,13 @@ export class WebSocketAccountSubscriberV2<T> implements AccountSubscriber<T> {
29
80
  onChange: (data: T) => void;
30
81
  listenerId?: number;
31
82
 
32
- resubOpts?: ResubOpts;
83
+ resubOpts: ResubOpts;
33
84
 
34
85
  commitment?: Commitment;
35
86
  isUnsubscribing = false;
36
87
 
37
88
  timeoutId?: ReturnType<typeof setTimeout>;
89
+ pollingTimeoutId?: ReturnType<typeof setTimeout>;
38
90
 
39
91
  receivingData: boolean;
40
92
 
@@ -45,21 +97,40 @@ export class WebSocketAccountSubscriberV2<T> implements AccountSubscriber<T> {
45
97
  >['rpcSubscriptions'];
46
98
  private abortController?: AbortController;
47
99
 
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
+ */
48
113
  public constructor(
49
114
  accountName: string,
50
115
  program: Program,
51
116
  accountPublicKey: PublicKey,
52
117
  decodeBuffer?: (buffer: Buffer) => T,
53
118
  resubOpts?: ResubOpts,
54
- commitment?: Commitment
119
+ commitment?: Commitment,
120
+ rpcSubscriptions?: RpcSubscriptions<SolanaRpcSubscriptionsApi> & string,
121
+ rpc?: Rpc<any>
55
122
  ) {
56
123
  this.accountName = accountName;
57
124
  this.logAccountName = `${accountName}-${accountPublicKey.toBase58()}-ws-acct-subscriber-v2`;
58
125
  this.program = program;
59
126
  this.accountPublicKey = accountPublicKey;
60
127
  this.decodeBufferFn = decodeBuffer;
61
- this.resubOpts = resubOpts;
62
- if (this.resubOpts?.resubTimeoutMs < 1000) {
128
+ this.resubOpts = resubOpts ?? {
129
+ resubTimeoutMs: 30000,
130
+ usePollingInsteadOfResub: true,
131
+ logResubMessages: false,
132
+ };
133
+ if (this.resubOpts.resubTimeoutMs < 1000) {
63
134
  console.log(
64
135
  `resubTimeoutMs should be at least 1000ms to avoid spamming resub ${this.logAccountName}`
65
136
  );
@@ -81,31 +152,67 @@ export class WebSocketAccountSubscriberV2<T> implements AccountSubscriber<T> {
81
152
  ((this.program.provider as AnchorProvider).opts.commitment as Commitment);
82
153
 
83
154
  // Initialize gill client using the same RPC URL as the program provider
84
- const rpcUrl = (this.program.provider as AnchorProvider).connection
85
- .rpcEndpoint;
86
- const { rpc, rpcSubscriptions } = createSolanaClient({
87
- urlOrMoniker: rpcUrl,
88
- });
89
- this.rpc = rpc;
90
- this.rpcSubscriptions = rpcSubscriptions;
155
+
156
+ this.rpc = rpc
157
+ ? rpc
158
+ : (() => {
159
+ const rpcUrl = (this.program.provider as AnchorProvider).connection
160
+ .rpcEndpoint;
161
+ const { rpc } = createSolanaClient({
162
+ urlOrMoniker: rpcUrl,
163
+ });
164
+ return rpc;
165
+ })();
166
+ this.rpcSubscriptions = rpcSubscriptions
167
+ ? rpcSubscriptions
168
+ : (() => {
169
+ const rpcUrl = (this.program.provider as AnchorProvider).connection
170
+ .rpcEndpoint;
171
+ const { rpcSubscriptions } = createSolanaClient({
172
+ urlOrMoniker: rpcUrl,
173
+ });
174
+ return rpcSubscriptions;
175
+ })();
91
176
  }
92
177
 
93
- private async handleNotificationLoop(subscription: AsyncIterable<any>) {
178
+ private async handleNotificationLoop(
179
+ subscriptionPromise: Promise<AsyncIterable<any>>
180
+ ) {
181
+ const subscription = await subscriptionPromise;
94
182
  for await (const notification of subscription) {
95
- if (this.resubOpts?.resubTimeoutMs) {
96
- this.receivingData = true;
97
- clearTimeout(this.timeoutId);
98
- this.handleRpcResponse(notification.context, notification.value);
99
- this.setTimeout();
100
- } else {
101
- this.handleRpcResponse(notification.context, notification.value);
183
+ // If we're currently polling and receive a WebSocket event, stop polling
184
+ if (this.pollingTimeoutId) {
185
+ if (this.resubOpts.logResubMessages) {
186
+ console.log(
187
+ `[${this.logAccountName}] Received WebSocket event while polling, stopping polling`
188
+ );
189
+ }
190
+ this.stopPolling();
102
191
  }
192
+
193
+ this.receivingData = true;
194
+ clearTimeout(this.timeoutId);
195
+ this.handleRpcResponse(notification.context, notification.value);
196
+ this.setTimeout();
103
197
  }
104
198
  }
105
199
 
106
200
  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
+ */
107
214
  if (this.listenerId != null || this.isUnsubscribing) {
108
- if (this.resubOpts?.logResubMessages) {
215
+ if (this.resubOpts.logResubMessages) {
109
216
  console.log(
110
217
  `[${this.logAccountName}] Subscribe returning early - listenerId=${this.listenerId}, isUnsubscribing=${this.isUnsubscribing}`
111
218
  );
@@ -124,7 +231,7 @@ export class WebSocketAccountSubscriberV2<T> implements AccountSubscriber<T> {
124
231
 
125
232
  this.listenerId = Math.random(); // Unique ID for logging purposes
126
233
 
127
- if (this.resubOpts?.resubTimeoutMs) {
234
+ if (this.resubOpts.resubTimeoutMs) {
128
235
  this.receivingData = true;
129
236
  this.setTimeout();
130
237
  }
@@ -132,7 +239,7 @@ export class WebSocketAccountSubscriberV2<T> implements AccountSubscriber<T> {
132
239
  // Subscribe to account changes using gill's rpcSubscriptions
133
240
  const pubkey = this.accountPublicKey.toBase58();
134
241
  if (isAddress(pubkey)) {
135
- const subscription = await this.rpcSubscriptions
242
+ const subscriptionPromise = this.rpcSubscriptions
136
243
  .accountNotifications(pubkey, {
137
244
  commitment: this.commitment,
138
245
  encoding: 'base64',
@@ -141,8 +248,10 @@ export class WebSocketAccountSubscriberV2<T> implements AccountSubscriber<T> {
141
248
  abortSignal: abortController.signal,
142
249
  });
143
250
 
144
- // Start notification loop without awaiting
145
- this.handleNotificationLoop(subscription);
251
+ // Start notification loop with the subscription promise
252
+ this.handleNotificationLoop(subscriptionPromise);
253
+ } else {
254
+ throw new Error('Invalid account public key');
146
255
  }
147
256
  }
148
257
 
@@ -159,23 +268,37 @@ export class WebSocketAccountSubscriberV2<T> implements AccountSubscriber<T> {
159
268
  }
160
269
 
161
270
  protected setTimeout(): void {
271
+ /**
272
+ * Schedule inactivity handling. If WS is quiet for
273
+ * `resubOpts.resubTimeoutMs` and `receivingData` is true, trigger either
274
+ * a polling loop or a resubscribe depending on options.
275
+ */
162
276
  if (!this.onChange) {
163
277
  throw new Error('onChange callback function must be set');
164
278
  }
165
- this.timeoutId = setTimeout(
166
- async () => {
167
- if (this.isUnsubscribing) {
168
- // If we are in the process of unsubscribing, do not attempt to resubscribe
169
- if (this.resubOpts?.logResubMessages) {
279
+ this.timeoutId = setTimeout(async () => {
280
+ if (this.isUnsubscribing) {
281
+ // If we are in the process of unsubscribing, do not attempt to resubscribe
282
+ if (this.resubOpts.logResubMessages) {
283
+ console.log(
284
+ `[${this.logAccountName}] Timeout fired but isUnsubscribing=true, skipping resubscribe`
285
+ );
286
+ }
287
+ return;
288
+ }
289
+
290
+ if (this.receivingData) {
291
+ if (this.resubOpts.usePollingInsteadOfResub) {
292
+ // Use polling instead of resubscribing
293
+ if (this.resubOpts.logResubMessages) {
170
294
  console.log(
171
- `[${this.logAccountName}] Timeout fired but isUnsubscribing=true, skipping resubscribe`
295
+ `[${this.logAccountName}] No ws data in ${this.resubOpts.resubTimeoutMs}ms, starting polling - listenerId=${this.listenerId}`
172
296
  );
173
297
  }
174
- return;
175
- }
176
-
177
- if (this.receivingData) {
178
- if (this.resubOpts?.logResubMessages) {
298
+ this.startPolling();
299
+ } else {
300
+ // Original resubscribe behavior
301
+ if (this.resubOpts.logResubMessages) {
179
302
  console.log(
180
303
  `No ws data from ${this.logAccountName} in ${this.resubOpts.resubTimeoutMs}ms, resubscribing - listenerId=${this.listenerId}, isUnsubscribing=${this.isUnsubscribing}`
181
304
  );
@@ -183,23 +306,93 @@ export class WebSocketAccountSubscriberV2<T> implements AccountSubscriber<T> {
183
306
  await this.unsubscribe(true);
184
307
  this.receivingData = false;
185
308
  await this.subscribe(this.onChange);
186
- if (this.resubOpts?.logResubMessages) {
309
+ if (this.resubOpts.logResubMessages) {
187
310
  console.log(
188
311
  `[${this.logAccountName}] Resubscribe completed - receivingData=${this.receivingData}, listenerId=${this.listenerId}, isUnsubscribing=${this.isUnsubscribing}`
189
312
  );
190
313
  }
314
+ }
315
+ } else {
316
+ if (this.resubOpts.logResubMessages) {
317
+ console.log(
318
+ `[${this.logAccountName}] Timeout fired but receivingData=false, skipping resubscribe`
319
+ );
320
+ }
321
+ }
322
+ }, this.resubOpts.resubTimeoutMs);
323
+ }
324
+
325
+ /**
326
+ * Start the polling loop (single-account).
327
+ * - Periodically calls `fetch()` and compares buffers to detect changes.
328
+ * - On detected change, stops polling and resubscribes to WS.
329
+ */
330
+ private startPolling(): void {
331
+ const pollingInterval = this.resubOpts.pollingIntervalMs || 30000; // Default to 30s
332
+
333
+ const poll = async () => {
334
+ if (this.isUnsubscribing) {
335
+ return;
336
+ }
337
+
338
+ try {
339
+ // Store current data and buffer before polling
340
+ const currentBuffer = this.bufferAndSlot?.buffer;
341
+
342
+ // Fetch latest account data
343
+ await this.fetch();
344
+
345
+ // Check if we got new data by comparing buffers
346
+ const newBuffer = this.bufferAndSlot?.buffer;
347
+ const hasNewData =
348
+ newBuffer && (!currentBuffer || !newBuffer.equals(currentBuffer));
349
+
350
+ if (hasNewData) {
351
+ // New data received, stop polling and resubscribe to websocket
352
+ if (this.resubOpts.logResubMessages) {
353
+ console.log(
354
+ `[${this.logAccountName}] Polling detected account data change, resubscribing to websocket`
355
+ );
356
+ }
357
+ await this.unsubscribe(true);
358
+ this.receivingData = false;
359
+ await this.subscribe(this.onChange);
191
360
  } else {
192
- if (this.resubOpts?.logResubMessages) {
361
+ // No new data, continue polling
362
+ if (this.resubOpts.logResubMessages) {
193
363
  console.log(
194
- `[${this.logAccountName}] Timeout fired but receivingData=false, skipping resubscribe`
364
+ `[${this.logAccountName}] Polling found no account changes, continuing to poll every ${pollingInterval}ms`
195
365
  );
196
366
  }
367
+ this.pollingTimeoutId = setTimeout(poll, pollingInterval);
368
+ }
369
+ } catch (error) {
370
+ if (this.resubOpts.logResubMessages) {
371
+ console.error(
372
+ `[${this.logAccountName}] Error during polling:`,
373
+ error
374
+ );
197
375
  }
198
- },
199
- this.resubOpts?.resubTimeoutMs
200
- );
376
+ // On error, continue polling
377
+ this.pollingTimeoutId = setTimeout(poll, pollingInterval);
378
+ }
379
+ };
380
+
381
+ // Start polling immediately
382
+ poll();
201
383
  }
202
384
 
385
+ private stopPolling(): void {
386
+ if (this.pollingTimeoutId) {
387
+ clearTimeout(this.pollingTimeoutId);
388
+ this.pollingTimeoutId = undefined;
389
+ }
390
+ }
391
+
392
+ /**
393
+ * Fetch the current account state via RPC and process it through the same
394
+ * decoding and update pathway as WS notifications.
395
+ */
203
396
  async fetch(): Promise<void> {
204
397
  // Use gill's rpc for fetching account info
205
398
  const accountAddress = this.accountPublicKey.toBase58() as Address;
@@ -294,6 +487,11 @@ export class WebSocketAccountSubscriberV2<T> implements AccountSubscriber<T> {
294
487
  }
295
488
 
296
489
  unsubscribe(onResub = false): Promise<void> {
490
+ /**
491
+ * Stop timers, polling, and WS subscription.
492
+ * - When called during a resubscribe (`onResub=true`), we preserve
493
+ * `resubOpts.resubTimeoutMs` for the restarted subscription.
494
+ */
297
495
  if (!onResub && this.resubOpts) {
298
496
  this.resubOpts.resubTimeoutMs = undefined;
299
497
  }
@@ -301,6 +499,9 @@ export class WebSocketAccountSubscriberV2<T> implements AccountSubscriber<T> {
301
499
  clearTimeout(this.timeoutId);
302
500
  this.timeoutId = undefined;
303
501
 
502
+ // Stop polling if active
503
+ this.stopPolling();
504
+
304
505
  // Abort the WebSocket subscription
305
506
  if (this.abortController) {
306
507
  this.abortController.abort('unsubscribing');