@drift-labs/sdk 2.49.0-beta.1 → 2.49.0-beta.11

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 (54) hide show
  1. package/VERSION +1 -1
  2. package/lib/accounts/{mockUserAccountSubscriber.d.ts → basicUserAccountSubscriber.d.ts} +2 -2
  3. package/lib/accounts/{mockUserAccountSubscriber.js → basicUserAccountSubscriber.js} +9 -6
  4. package/lib/accounts/pollingInsuranceFundStakeAccountSubscriber.d.ts +29 -0
  5. package/lib/accounts/pollingInsuranceFundStakeAccountSubscriber.js +110 -0
  6. package/lib/accounts/types.d.ts +14 -1
  7. package/lib/accounts/webSocketInsuranceFundStakeAccountSubscriber.d.ts +23 -0
  8. package/lib/accounts/webSocketInsuranceFundStakeAccountSubscriber.js +65 -0
  9. package/lib/dlob/DLOB.d.ts +6 -2
  10. package/lib/dlob/DLOB.js +37 -12
  11. package/lib/driftClient.d.ts +66 -66
  12. package/lib/driftClient.js +208 -194
  13. package/lib/events/eventSubscriber.js +2 -1
  14. package/lib/events/sort.d.ts +2 -2
  15. package/lib/events/sort.js +6 -23
  16. package/lib/examples/loadDlob.js +10 -5
  17. package/lib/index.d.ts +3 -1
  18. package/lib/index.js +3 -1
  19. package/lib/math/superStake.d.ts +43 -0
  20. package/lib/math/superStake.js +64 -22
  21. package/lib/orderSubscriber/OrderSubscriber.js +4 -0
  22. package/lib/orderSubscriber/WebsocketSubscription.d.ts +1 -1
  23. package/lib/orderSubscriber/WebsocketSubscription.js +8 -6
  24. package/lib/types.d.ts +0 -2
  25. package/lib/userMap/PollingSubscription.d.ts +15 -0
  26. package/lib/userMap/PollingSubscription.js +26 -0
  27. package/lib/userMap/WebsocketSubscription.d.ts +19 -0
  28. package/lib/userMap/WebsocketSubscription.js +40 -0
  29. package/lib/userMap/userMap.d.ts +15 -18
  30. package/lib/userMap/userMap.js +62 -31
  31. package/lib/userMap/userMapConfig.d.ts +20 -0
  32. package/lib/userMap/userMapConfig.js +2 -0
  33. package/package.json +1 -1
  34. package/src/accounts/{mockUserAccountSubscriber.ts → basicUserAccountSubscriber.ts} +8 -6
  35. package/src/accounts/pollingInsuranceFundStakeAccountSubscriber.ts +185 -0
  36. package/src/accounts/types.ts +21 -0
  37. package/src/accounts/webSocketInsuranceFundStakeAccountSubscriber.ts +127 -0
  38. package/src/dlob/DLOB.ts +55 -15
  39. package/src/driftClient.ts +429 -272
  40. package/src/events/eventSubscriber.ts +2 -1
  41. package/src/events/sort.ts +7 -29
  42. package/src/examples/loadDlob.ts +11 -6
  43. package/src/index.ts +3 -1
  44. package/src/math/superStake.ts +108 -20
  45. package/src/orderSubscriber/OrderSubscriber.ts +4 -0
  46. package/src/orderSubscriber/WebsocketSubscription.ts +19 -16
  47. package/src/types.ts +0 -2
  48. package/src/userMap/PollingSubscription.ts +46 -0
  49. package/src/userMap/WebsocketSubscription.ts +74 -0
  50. package/src/userMap/userMap.ts +88 -60
  51. package/src/userMap/userMapConfig.ts +31 -0
  52. package/tests/amm/test.ts +6 -3
  53. package/tests/dlob/helpers.ts +2 -6
  54. package/tests/dlob/test.ts +194 -0
@@ -47,7 +47,7 @@ export class EventSubscriber {
47
47
  new EventList(
48
48
  eventType,
49
49
  this.options.maxEventsPerType,
50
- getSortFn(this.options.orderBy, this.options.orderDir, eventType),
50
+ getSortFn(this.options.orderBy, this.options.orderDir),
51
51
  this.options.orderDir
52
52
  )
53
53
  );
@@ -159,6 +159,7 @@ export class EventSubscriber {
159
159
 
160
160
  if (!this.lastSeenSlot || slot > this.lastSeenSlot) {
161
161
  this.lastSeenTxSig = txSig;
162
+ this.lastSeenSlot = slot;
162
163
  }
163
164
 
164
165
  if (
@@ -4,10 +4,7 @@ import {
4
4
  EventSubscriptionOrderDirection,
5
5
  EventType,
6
6
  SortFn,
7
- Event,
8
7
  } from './types';
9
- import { OrderActionRecord } from '../types';
10
- import { ZERO } from '../index';
11
8
 
12
9
  function clientSortAscFn(): 'less than' {
13
10
  return 'less than';
@@ -17,45 +14,26 @@ function clientSortDescFn(): 'greater than' {
17
14
  return 'greater than';
18
15
  }
19
16
 
20
- function defaultBlockchainSortFn(
17
+ function blockchainSortFn(
21
18
  currentEvent: EventMap[EventType],
22
19
  newEvent: EventMap[EventType]
23
20
  ): 'less than' | 'greater than' {
24
- return currentEvent.slot <= newEvent.slot ? 'less than' : 'greater than';
25
- }
26
-
27
- function orderActionRecordSortFn(
28
- currentEvent: Event<OrderActionRecord>,
29
- newEvent: Event<OrderActionRecord>
30
- ): 'less than' | 'greater than' {
31
- const currentEventMarketIndex = currentEvent.marketIndex;
32
- const newEventMarketIndex = newEvent.marketIndex;
33
- if (currentEventMarketIndex !== newEventMarketIndex) {
34
- return currentEvent.ts.lte(newEvent.ts) ? 'less than' : 'greater than';
35
- }
36
-
37
- if (currentEvent.fillRecordId?.gt(ZERO) && newEvent.fillRecordId?.gt(ZERO)) {
38
- return currentEvent.fillRecordId.lte(newEvent.fillRecordId)
21
+ if (currentEvent.slot == newEvent.slot) {
22
+ return currentEvent.txSigIndex < newEvent.txSigIndex
39
23
  ? 'less than'
40
24
  : 'greater than';
41
- } else {
42
- return currentEvent.ts.lte(newEvent.ts) ? 'less than' : 'greater than';
43
25
  }
26
+
27
+ return currentEvent.slot < newEvent.slot ? 'less than' : 'greater than';
44
28
  }
45
29
 
46
30
  export function getSortFn(
47
31
  orderBy: EventSubscriptionOrderBy,
48
- orderDir: EventSubscriptionOrderDirection,
49
- eventType: EventType
32
+ orderDir: EventSubscriptionOrderDirection
50
33
  ): SortFn {
51
34
  if (orderBy === 'client') {
52
35
  return orderDir === 'asc' ? clientSortAscFn : clientSortDescFn;
53
36
  }
54
37
 
55
- switch (eventType) {
56
- case 'OrderActionRecord':
57
- return orderActionRecordSortFn;
58
- default:
59
- return defaultBlockchainSortFn;
60
- }
38
+ return blockchainSortFn;
61
39
  }
@@ -1,5 +1,5 @@
1
1
  import { AnchorProvider } from '@coral-xyz/anchor';
2
- import { DLOB, UserMap, Wallet } from '..';
2
+ import { UserMap, Wallet } from '..';
3
3
  import { Connection, Keypair, PublicKey } from '@solana/web3.js';
4
4
  import {
5
5
  DriftClient,
@@ -55,17 +55,22 @@ const main = async () => {
55
55
  await driftClient.subscribe();
56
56
 
57
57
  console.log('Loading user map...');
58
- const userMap = new UserMap(driftClient, {
59
- type: 'polling',
60
- accountLoader: bulkAccountLoader,
58
+ const userMap = new UserMap({
59
+ driftClient,
60
+ subscriptionConfig: {
61
+ type: 'websocket',
62
+ commitment: 'processed',
63
+ },
64
+ skipInitialLoad: false,
65
+ includeIdle: false,
61
66
  });
62
67
 
63
68
  // fetches all users and subscribes for updates
64
69
  await userMap.subscribe();
65
70
 
66
71
  console.log('Loading dlob from user map...');
67
- const dlob = new DLOB();
68
- await dlob.initFromUserMap(userMap, bulkAccountLoader.mostRecentSlot);
72
+ const slot = await driftClient.connection.getSlot();
73
+ const dlob = await userMap.getDLOB(slot);
69
74
 
70
75
  console.log('number of orders', dlob.getDLOBOrders().length);
71
76
 
package/src/index.ts CHANGED
@@ -10,6 +10,7 @@ export * from './types';
10
10
  export * from './constants/perpMarkets';
11
11
  export * from './accounts/fetch';
12
12
  export * from './accounts/webSocketDriftClientAccountSubscriber';
13
+ export * from './accounts/webSocketInsuranceFundStakeAccountSubscriber';
13
14
  export * from './accounts/bulkAccountLoader';
14
15
  export * from './accounts/bulkUserSubscription';
15
16
  export * from './accounts/bulkUserStatsSubscription';
@@ -18,7 +19,8 @@ export * from './accounts/pollingOracleAccountSubscriber';
18
19
  export * from './accounts/pollingTokenAccountSubscriber';
19
20
  export * from './accounts/pollingUserAccountSubscriber';
20
21
  export * from './accounts/pollingUserStatsAccountSubscriber';
21
- export * from './accounts/mockUserAccountSubscriber';
22
+ export * from './accounts/pollingInsuranceFundStakeAccountSubscriber';
23
+ export * from './accounts/basicUserAccountSubscriber';
22
24
  export * from './accounts/types';
23
25
  export * from './addresses/pda';
24
26
  export * from './adminClient';
@@ -14,6 +14,38 @@ import { LAMPORTS_PRECISION, ZERO } from '../constants/numericConstants';
14
14
  import fetch from 'node-fetch';
15
15
  import { checkSameDate } from './utils';
16
16
 
17
+ export type BSOL_STATS_API_RESPONSE = {
18
+ success: boolean;
19
+ stats?: {
20
+ conversion: {
21
+ bsol_to_sol: number;
22
+ sol_to_bsol: number;
23
+ };
24
+ apy: {
25
+ base: number;
26
+ blze: number;
27
+ total: number;
28
+ lending: number;
29
+ liquidity: number;
30
+ };
31
+ };
32
+ };
33
+
34
+ export type BSOL_EMISSIONS_API_RESPONSE = {
35
+ success: boolean;
36
+ emissions?: {
37
+ lend: number;
38
+ };
39
+ };
40
+
41
+ export async function fetchBSolMetrics() {
42
+ return await fetch('https://stake.solblaze.org/api/v1/stats');
43
+ }
44
+
45
+ export async function fetchBSolDriftEmissions() {
46
+ return await fetch('https://stake.solblaze.org/api/v1/drift_emissions');
47
+ }
48
+
17
49
  export async function findBestSuperStakeIxs({
18
50
  marketIndex,
19
51
  amount,
@@ -56,6 +88,16 @@ export async function findBestSuperStakeIxs({
56
88
  userAccountPublicKey,
57
89
  onlyDirectRoutes,
58
90
  });
91
+ } else if (marketIndex === 8) {
92
+ return findBestLstSuperStakeIxs({
93
+ amount,
94
+ lstMint: driftClient.getSpotMarketAccount(8).mint,
95
+ lstMarketIndex: 8,
96
+ jupiterClient,
97
+ driftClient,
98
+ userAccountPublicKey,
99
+ onlyDirectRoutes,
100
+ });
59
101
  } else {
60
102
  throw new Error(`Unsupported superstake market index: ${marketIndex}`);
61
103
  }
@@ -153,16 +195,53 @@ export async function findBestJitoSolSuperStakeIxs({
153
195
  lookupTables: AddressLookupTableAccount[];
154
196
  method: 'jupiter' | 'marinade';
155
197
  price: number;
198
+ }> {
199
+ return await findBestLstSuperStakeIxs({
200
+ amount,
201
+ jupiterClient,
202
+ driftClient,
203
+ userAccountPublicKey,
204
+ onlyDirectRoutes,
205
+ lstMint: driftClient.getSpotMarketAccount(6).mint,
206
+ lstMarketIndex: 6,
207
+ });
208
+ }
209
+
210
+ /**
211
+ * Finds best Jupiter Swap instructions for a generic lstMint
212
+ *
213
+ * Without doing any extra steps like checking if you can get a better rate by staking directly with that LST platform
214
+ */
215
+ export async function findBestLstSuperStakeIxs({
216
+ amount,
217
+ lstMint,
218
+ jupiterClient,
219
+ driftClient,
220
+ userAccountPublicKey,
221
+ onlyDirectRoutes,
222
+ lstMarketIndex,
223
+ }: {
224
+ amount: BN;
225
+ lstMint: PublicKey;
226
+ lstMarketIndex: number;
227
+ jupiterClient: JupiterClient;
228
+ driftClient: DriftClient;
229
+ userAccountPublicKey?: PublicKey;
230
+ onlyDirectRoutes?: boolean;
231
+ }): Promise<{
232
+ ixs: TransactionInstruction[];
233
+ lookupTables: AddressLookupTableAccount[];
234
+ method: 'jupiter' | 'marinade';
235
+ price: number;
156
236
  }> {
157
237
  const solMint = driftClient.getSpotMarketAccount(1).mint;
158
- const JitoSolMint = driftClient.getSpotMarketAccount(6).mint;
159
238
 
160
239
  let jupiterPrice;
161
240
  let bestRoute;
162
241
  try {
163
242
  const jupiterRoutes = await jupiterClient.getRoutes({
164
243
  inputMint: solMint,
165
- outputMint: JitoSolMint,
244
+ outputMint: lstMint,
166
245
  amount,
167
246
  onlyDirectRoutes,
168
247
  });
@@ -176,7 +255,7 @@ export async function findBestJitoSolSuperStakeIxs({
176
255
 
177
256
  const { ixs, lookupTables } = await driftClient.getJupiterSwapIx({
178
257
  inMarketIndex: 1,
179
- outMarketIndex: 6,
258
+ outMarketIndex: lstMarketIndex,
180
259
  route: bestRoute,
181
260
  jupiterClient,
182
261
  amount,
@@ -322,10 +401,27 @@ export async function calculateSolEarned({
322
401
  }
323
402
  };
324
403
 
404
+ const getBSolPrice = async (timestamps: number[]) => {
405
+ // Currently there's only one bSOL price, no timestamped data
406
+ // So just use the same price for every timestamp for now
407
+ const response = await fetchBSolMetrics();
408
+ if (response.status === 200) {
409
+ const data = (await response.json()) as BSOL_STATS_API_RESPONSE;
410
+ const bSolRatio = data?.stats?.conversion?.bsol_to_sol;
411
+ if (bSolRatio) {
412
+ timestamps.forEach((timestamp) => lstRatios.set(timestamp, bSolRatio));
413
+ }
414
+ }
415
+ };
416
+
417
+ // This block kind of assumes the record are all from the same market
418
+ // Otherwise the following code that checks the record.marketIndex would break
325
419
  if (marketIndex === 2) {
326
420
  await Promise.all(timestamps.map(getMsolPrice));
327
421
  } else if (marketIndex === 6) {
328
422
  lstRatios = await getJitoSolHistoricalPriceMap(timestamps);
423
+ } else if (marketIndex === 8) {
424
+ await getBSolPrice(timestamps);
329
425
  }
330
426
 
331
427
  let solEarned = ZERO;
@@ -336,23 +432,15 @@ export async function calculateSolEarned({
336
432
  } else {
337
433
  solEarned = solEarned.add(record.amount);
338
434
  }
339
- } else if (record.marketIndex === 2) {
340
- const msolRatio = lstRatios.get(record.ts.toNumber());
341
- const msolRatioBN = new BN(msolRatio * LAMPORTS_PER_SOL);
342
-
343
- const solAmount = record.amount.mul(msolRatioBN).div(LAMPORTS_PRECISION);
344
- if (isVariant(record.direction, 'deposit')) {
345
- solEarned = solEarned.sub(solAmount);
346
- } else {
347
- solEarned = solEarned.add(solAmount);
348
- }
349
- } else if (record.marketIndex === 6) {
350
- const jitoSolRatio = lstRatios.get(record.ts.toNumber());
351
- const jitoSolRatioBN = new BN(jitoSolRatio * LAMPORTS_PER_SOL);
352
-
353
- const solAmount = record.amount
354
- .mul(jitoSolRatioBN)
355
- .div(LAMPORTS_PRECISION);
435
+ } else if (
436
+ record.marketIndex === 2 ||
437
+ record.marketIndex === 6 ||
438
+ record.marketIndex === 8
439
+ ) {
440
+ const lstRatio = lstRatios.get(record.ts.toNumber());
441
+ const lstRatioBN = new BN(lstRatio * LAMPORTS_PER_SOL);
442
+
443
+ const solAmount = record.amount.mul(lstRatioBN).div(LAMPORTS_PRECISION);
356
444
  if (isVariant(record.direction, 'deposit')) {
357
445
  solEarned = solEarned.sub(solAmount);
358
446
  } else {
@@ -94,12 +94,16 @@ export class OrderSubscriber {
94
94
  programAccount.account.data,
95
95
  slot
96
96
  );
97
+ // give event loop a chance to breathe
98
+ await new Promise((resolve) => setTimeout(resolve, 0));
97
99
  }
98
100
 
99
101
  for (const key of this.usersAccounts.keys()) {
100
102
  if (!programAccountSet.has(key)) {
101
103
  this.usersAccounts.delete(key);
102
104
  }
105
+ // give event loop a chance to breathe
106
+ await new Promise((resolve) => setTimeout(resolve, 0));
103
107
  }
104
108
  } catch (e) {
105
109
  console.error(e);
@@ -10,7 +10,7 @@ export class WebsocketSubscription {
10
10
  private skipInitialLoad: boolean;
11
11
  private resubTimeoutMs?: number;
12
12
 
13
- private subscriber: WebSocketProgramAccountSubscriber<UserAccount>;
13
+ private subscriber?: WebSocketProgramAccountSubscriber<UserAccount>;
14
14
 
15
15
  constructor({
16
16
  orderSubscriber,
@@ -30,22 +30,24 @@ export class WebsocketSubscription {
30
30
  }
31
31
 
32
32
  public async subscribe(): Promise<void> {
33
- if (!this.subscriber) {
34
- this.subscriber = new WebSocketProgramAccountSubscriber<UserAccount>(
35
- 'OrderSubscriber',
36
- 'User',
37
- this.orderSubscriber.driftClient.program,
38
- this.orderSubscriber.driftClient.program.account.user.coder.accounts.decode.bind(
39
- this.orderSubscriber.driftClient.program.account.user.coder.accounts
40
- ),
41
- {
42
- filters: [getUserFilter(), getNonIdleUserFilter()],
43
- commitment: this.commitment,
44
- },
45
- this.resubTimeoutMs
46
- );
33
+ if (this.subscriber) {
34
+ return;
47
35
  }
48
36
 
37
+ this.subscriber = new WebSocketProgramAccountSubscriber<UserAccount>(
38
+ 'OrderSubscriber',
39
+ 'User',
40
+ this.orderSubscriber.driftClient.program,
41
+ this.orderSubscriber.driftClient.program.account.user.coder.accounts.decodeUnchecked.bind(
42
+ this.orderSubscriber.driftClient.program.account.user.coder.accounts
43
+ ),
44
+ {
45
+ filters: [getUserFilter(), getNonIdleUserFilter()],
46
+ commitment: this.commitment,
47
+ },
48
+ this.resubTimeoutMs
49
+ );
50
+
49
51
  await this.subscriber.subscribe(
50
52
  (accountId: PublicKey, account: UserAccount, context: Context) => {
51
53
  const userKey = accountId.toBase58();
@@ -65,6 +67,7 @@ export class WebsocketSubscription {
65
67
 
66
68
  public async unsubscribe(): Promise<void> {
67
69
  if (!this.subscriber) return;
68
- this.subscriber.unsubscribe();
70
+ await this.subscriber.unsubscribe();
71
+ this.subscriber = undefined;
69
72
  }
70
73
  }
package/src/types.ts CHANGED
@@ -1000,8 +1000,6 @@ export interface IVersionedWallet {
1000
1000
 
1001
1001
  export type FeeStructure = {
1002
1002
  feeTiers: FeeTier[];
1003
- makerRebateNumerator: BN;
1004
- makerRebateDenominator: BN;
1005
1003
  fillerRewardStructure: OrderFillerRewardStructure;
1006
1004
  flatFillerFee: BN;
1007
1005
  referrerRewardEpochUpperBound: BN;
@@ -0,0 +1,46 @@
1
+ import { UserMap } from './userMap';
2
+
3
+ export class PollingSubscription {
4
+ private userMap: UserMap;
5
+ private frequency: number;
6
+ private skipInitialLoad: boolean;
7
+
8
+ intervalId?: ReturnType<typeof setTimeout>;
9
+
10
+ constructor({
11
+ userMap,
12
+ frequency,
13
+ skipInitialLoad = false,
14
+ }: {
15
+ userMap: UserMap;
16
+ frequency: number;
17
+ skipInitialLoad?: boolean;
18
+ includeIdle?: boolean;
19
+ }) {
20
+ this.userMap = userMap;
21
+ this.frequency = frequency;
22
+ this.skipInitialLoad = skipInitialLoad;
23
+ }
24
+
25
+ public async subscribe(): Promise<void> {
26
+ if (this.intervalId) {
27
+ return;
28
+ }
29
+
30
+ this.intervalId = setInterval(
31
+ this.userMap.sync.bind(this.userMap),
32
+ this.frequency
33
+ );
34
+
35
+ if (!this.skipInitialLoad) {
36
+ await this.userMap.sync();
37
+ }
38
+ }
39
+
40
+ public async unsubscribe(): Promise<void> {
41
+ if (this.intervalId) {
42
+ clearInterval(this.intervalId);
43
+ this.intervalId = undefined;
44
+ }
45
+ }
46
+ }
@@ -0,0 +1,74 @@
1
+ import { UserMap } from './userMap';
2
+ import { getNonIdleUserFilter, getUserFilter } from '../memcmp';
3
+ import { WebSocketProgramAccountSubscriber } from '../accounts/webSocketProgramAccountSubscriber';
4
+ import { UserAccount } from '../types';
5
+ import { Commitment, Context, PublicKey } from '@solana/web3.js';
6
+
7
+ export class WebsocketSubscription {
8
+ private userMap: UserMap;
9
+ private commitment: Commitment;
10
+ private skipInitialLoad: boolean;
11
+ private resubTimeoutMs?: number;
12
+ private includeIdle?: boolean;
13
+
14
+ private subscriber: WebSocketProgramAccountSubscriber<UserAccount>;
15
+
16
+ constructor({
17
+ userMap,
18
+ commitment,
19
+ skipInitialLoad = false,
20
+ resubTimeoutMs,
21
+ includeIdle = false,
22
+ }: {
23
+ userMap: UserMap;
24
+ commitment: Commitment;
25
+ skipInitialLoad?: boolean;
26
+ resubTimeoutMs?: number;
27
+ includeIdle?: boolean;
28
+ }) {
29
+ this.userMap = userMap;
30
+ this.commitment = commitment;
31
+ this.skipInitialLoad = skipInitialLoad;
32
+ this.resubTimeoutMs = resubTimeoutMs;
33
+ this.includeIdle = includeIdle || false;
34
+ }
35
+
36
+ public async subscribe(): Promise<void> {
37
+ if (!this.subscriber) {
38
+ const filters = [getUserFilter()];
39
+ if (!this.includeIdle) {
40
+ filters.push(getNonIdleUserFilter());
41
+ }
42
+ this.subscriber = new WebSocketProgramAccountSubscriber<UserAccount>(
43
+ 'UserMap',
44
+ 'User',
45
+ this.userMap.driftClient.program,
46
+ this.userMap.driftClient.program.account.user.coder.accounts.decodeUnchecked.bind(
47
+ this.userMap.driftClient.program.account.user.coder.accounts
48
+ ),
49
+ {
50
+ filters,
51
+ commitment: this.commitment,
52
+ },
53
+ this.resubTimeoutMs
54
+ );
55
+ }
56
+
57
+ await this.subscriber.subscribe(
58
+ (accountId: PublicKey, account: UserAccount, context: Context) => {
59
+ const userKey = accountId.toBase58();
60
+ this.userMap.updateUserAccount(userKey, account, context.slot);
61
+ }
62
+ );
63
+
64
+ if (!this.skipInitialLoad) {
65
+ await this.userMap.sync();
66
+ }
67
+ }
68
+
69
+ public async unsubscribe(): Promise<void> {
70
+ if (!this.subscriber) return;
71
+ await this.subscriber.unsubscribe();
72
+ this.subscriber = undefined;
73
+ }
74
+ }