@solana/web3.js 1.41.1 → 1.41.4

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/src/connection.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import bs58 from 'bs58';
2
2
  import {Buffer} from 'buffer';
3
3
  import crossFetch from 'cross-fetch';
4
+ // @ts-ignore
5
+ import fastStableStringify from 'fast-stable-stringify';
4
6
  import {
5
7
  type as pick,
6
8
  number,
@@ -22,7 +24,6 @@ import {
22
24
  import type {Struct} from 'superstruct';
23
25
  import {Client as RpcWebSocketClient} from 'rpc-websockets';
24
26
  import RpcClient from 'jayson/lib/client/browser';
25
- import {IWSRequestParams} from 'rpc-websockets/dist/lib/client';
26
27
 
27
28
  import {AgentManager} from './agent-manager';
28
29
  import {EpochSchedule} from './epoch-schedule';
@@ -63,6 +64,130 @@ const BufferFromRawAccountData = coerce(
63
64
  */
64
65
  export const BLOCKHASH_CACHE_TIMEOUT_MS = 30 * 1000;
65
66
 
67
+ /**
68
+ * HACK.
69
+ * Copied from rpc-websockets/dist/lib/client.
70
+ * Otherwise, `yarn build` fails with:
71
+ * https://gist.github.com/steveluscher/c057eca81d479ef705cdb53162f9971d
72
+ */
73
+ interface IWSRequestParams {
74
+ [x: string]: any;
75
+ [x: number]: any;
76
+ }
77
+
78
+ type ClientSubscriptionId = number;
79
+ /** @internal */ type ServerSubscriptionId = number;
80
+ /** @internal */ type SubscriptionConfigHash = string;
81
+ /** @internal */ type SubscriptionDisposeFn = () => Promise<void>;
82
+ /**
83
+ * @internal
84
+ * Every subscription contains the args used to open the subscription with
85
+ * the server, and a list of callers interested in notifications.
86
+ */
87
+ type BaseSubscription<TMethod = SubscriptionConfig['method']> = Readonly<{
88
+ args: IWSRequestParams;
89
+ callbacks: Set<Extract<SubscriptionConfig, {method: TMethod}>['callback']>;
90
+ }>;
91
+ /**
92
+ * @internal
93
+ * A subscription may be in various states of connectedness. Only when it is
94
+ * fully connected will it have a server subscription id associated with it.
95
+ * This id can be returned to the server to unsubscribe the client entirely.
96
+ */
97
+ type StatefulSubscription = Readonly<
98
+ // New subscriptions that have not yet been
99
+ // sent to the server start in this state.
100
+ | {
101
+ state: 'pending';
102
+ }
103
+ // These subscriptions have been sent to the server
104
+ // and are waiting for the server to acknowledge them.
105
+ | {
106
+ state: 'subscribing';
107
+ }
108
+ // These subscriptions have been acknowledged by the
109
+ // server and have been assigned server subscription ids.
110
+ | {
111
+ serverSubscriptionId: ServerSubscriptionId;
112
+ state: 'subscribed';
113
+ }
114
+ // These subscriptions are intended to be torn down and
115
+ // are waiting on an acknowledgement from the server.
116
+ | {
117
+ serverSubscriptionId: ServerSubscriptionId;
118
+ state: 'unsubscribing';
119
+ }
120
+ // The request to tear down these subscriptions has been
121
+ // acknowledged by the server. The `serverSubscriptionId`
122
+ // is the id of the now-dead subscription.
123
+ | {
124
+ serverSubscriptionId: ServerSubscriptionId;
125
+ state: 'unsubscribed';
126
+ }
127
+ >;
128
+ /**
129
+ * A type that encapsulates a subscription's RPC method
130
+ * names and notification (callback) signature.
131
+ */
132
+ type SubscriptionConfig = Readonly<
133
+ | {
134
+ callback: AccountChangeCallback;
135
+ method: 'accountSubscribe';
136
+ unsubscribeMethod: 'accountUnsubscribe';
137
+ }
138
+ | {
139
+ callback: LogsCallback;
140
+ method: 'logsSubscribe';
141
+ unsubscribeMethod: 'logsUnsubscribe';
142
+ }
143
+ | {
144
+ callback: ProgramAccountChangeCallback;
145
+ method: 'programSubscribe';
146
+ unsubscribeMethod: 'programUnsubscribe';
147
+ }
148
+ | {
149
+ callback: RootChangeCallback;
150
+ method: 'rootSubscribe';
151
+ unsubscribeMethod: 'rootUnsubscribe';
152
+ }
153
+ | {
154
+ callback: SignatureSubscriptionCallback;
155
+ method: 'signatureSubscribe';
156
+ unsubscribeMethod: 'signatureUnsubscribe';
157
+ }
158
+ | {
159
+ callback: SlotChangeCallback;
160
+ method: 'slotSubscribe';
161
+ unsubscribeMethod: 'slotUnsubscribe';
162
+ }
163
+ | {
164
+ callback: SlotUpdateCallback;
165
+ method: 'slotsUpdatesSubscribe';
166
+ unsubscribeMethod: 'slotsUpdatesUnsubscribe';
167
+ }
168
+ >;
169
+ /**
170
+ * @internal
171
+ * Utility type that keeps tagged unions intact while omitting properties.
172
+ */
173
+ type DistributiveOmit<T, K extends PropertyKey> = T extends unknown
174
+ ? Omit<T, K>
175
+ : never;
176
+ /**
177
+ * @internal
178
+ * This type represents a single subscribable 'topic.' It's made up of:
179
+ *
180
+ * - The args used to open the subscription with the server,
181
+ * - The state of the subscription, in terms of its connectedness, and
182
+ * - The set of callbacks to call when the server publishes notifications
183
+ *
184
+ * This record gets indexed by `SubscriptionConfigHash` and is used to
185
+ * set up subscriptions, fan out notifications, and track subscription state.
186
+ */
187
+ type Subscription = BaseSubscription &
188
+ StatefulSubscription &
189
+ DistributiveOmit<SubscriptionConfig, 'callback'>;
190
+
66
191
  type RpcRequest = (methodName: string, args: Array<any>) => any;
67
192
 
68
193
  type RpcBatchRequest = (requests: RpcParams[]) => any;
@@ -1863,21 +1988,6 @@ export type AccountChangeCallback = (
1863
1988
  context: Context,
1864
1989
  ) => void;
1865
1990
 
1866
- /**
1867
- * @internal
1868
- */
1869
- type SubscriptionId = 'subscribing' | number;
1870
-
1871
- /**
1872
- * @internal
1873
- */
1874
- type AccountSubscriptionInfo = {
1875
- publicKey: string; // PublicKey of the account as a base 58 string
1876
- callback: AccountChangeCallback;
1877
- commitment?: Commitment;
1878
- subscriptionId: SubscriptionId | null; // null when there's no current server subscription id
1879
- };
1880
-
1881
1991
  /**
1882
1992
  * Callback function for program account change notifications
1883
1993
  */
@@ -1886,43 +1996,16 @@ export type ProgramAccountChangeCallback = (
1886
1996
  context: Context,
1887
1997
  ) => void;
1888
1998
 
1889
- /**
1890
- * @internal
1891
- */
1892
- type ProgramAccountSubscriptionInfo = {
1893
- programId: string; // PublicKey of the program as a base 58 string
1894
- callback: ProgramAccountChangeCallback;
1895
- commitment?: Commitment;
1896
- subscriptionId: SubscriptionId | null; // null when there's no current server subscription id
1897
- filters?: GetProgramAccountsFilter[];
1898
- };
1899
-
1900
1999
  /**
1901
2000
  * Callback function for slot change notifications
1902
2001
  */
1903
2002
  export type SlotChangeCallback = (slotInfo: SlotInfo) => void;
1904
2003
 
1905
- /**
1906
- * @internal
1907
- */
1908
- type SlotSubscriptionInfo = {
1909
- callback: SlotChangeCallback;
1910
- subscriptionId: SubscriptionId | null; // null when there's no current server subscription id
1911
- };
1912
-
1913
2004
  /**
1914
2005
  * Callback function for slot update notifications
1915
2006
  */
1916
2007
  export type SlotUpdateCallback = (slotUpdate: SlotUpdate) => void;
1917
2008
 
1918
- /**
1919
- * @private
1920
- */
1921
- type SlotUpdateSubscriptionInfo = {
1922
- callback: SlotUpdateCallback;
1923
- subscriptionId: SubscriptionId | null; // null when there's no current server subscription id
1924
- };
1925
-
1926
2009
  /**
1927
2010
  * Callback function for signature status notifications
1928
2011
  */
@@ -1962,29 +2045,11 @@ export type SignatureSubscriptionOptions = {
1962
2045
  enableReceivedNotification?: boolean;
1963
2046
  };
1964
2047
 
1965
- /**
1966
- * @internal
1967
- */
1968
- type SignatureSubscriptionInfo = {
1969
- signature: TransactionSignature; // TransactionSignature as a base 58 string
1970
- callback: SignatureSubscriptionCallback;
1971
- options?: SignatureSubscriptionOptions;
1972
- subscriptionId: SubscriptionId | null; // null when there's no current server subscription id
1973
- };
1974
-
1975
2048
  /**
1976
2049
  * Callback function for root change notifications
1977
2050
  */
1978
2051
  export type RootChangeCallback = (root: number) => void;
1979
2052
 
1980
- /**
1981
- * @internal
1982
- */
1983
- type RootSubscriptionInfo = {
1984
- callback: RootChangeCallback;
1985
- subscriptionId: SubscriptionId | null; // null when there's no current server subscription id
1986
- };
1987
-
1988
2053
  /**
1989
2054
  * @internal
1990
2055
  */
@@ -2021,16 +2086,6 @@ export type LogsFilter = PublicKey | 'all' | 'allWithVotes';
2021
2086
  */
2022
2087
  export type LogsCallback = (logs: Logs, ctx: Context) => void;
2023
2088
 
2024
- /**
2025
- * @private
2026
- */
2027
- type LogsSubscriptionInfo = {
2028
- callback: LogsCallback;
2029
- filter: LogsFilter;
2030
- subscriptionId: SubscriptionId | null; // null when there's no current server subscription id
2031
- commitment?: Commitment;
2032
- };
2033
-
2034
2089
  /**
2035
2090
  * Signature result
2036
2091
  */
@@ -2120,13 +2175,6 @@ export type ConnectionConfig = {
2120
2175
  confirmTransactionInitialTimeout?: number;
2121
2176
  };
2122
2177
 
2123
- function createSubscriptionWarningMessage(id: number, label: string): string {
2124
- return (
2125
- 'Ignored unsubscribe request because an active subscription ' +
2126
- `with id \`${id}\` for '${label}' events could not be found.`
2127
- );
2128
- }
2129
-
2130
2178
  /**
2131
2179
  * A connection to a fullnode JSON RPC endpoint
2132
2180
  */
@@ -2146,6 +2194,13 @@ export class Connection {
2146
2194
  /** @internal */ _rpcWebSocketIdleTimeout: ReturnType<
2147
2195
  typeof setTimeout
2148
2196
  > | null = null;
2197
+ /** @internal
2198
+ * A number that we increment every time an active connection closes.
2199
+ * Used to determine whether the same socket connection that was open
2200
+ * when an async operation started is the same one that's active when
2201
+ * its continuation fires.
2202
+ *
2203
+ */ private _rpcWebSocketGeneration: number = 0;
2149
2204
 
2150
2205
  /** @internal */ _disableBlockhashCaching: boolean = false;
2151
2206
  /** @internal */ _pollingBlockhash: boolean = false;
@@ -2161,40 +2216,35 @@ export class Connection {
2161
2216
  simulatedSignatures: [],
2162
2217
  };
2163
2218
 
2164
- /** @internal */ _accountChangeSubscriptionCounter: number = 0;
2165
- /** @internal */ _accountChangeSubscriptions: {
2166
- [id: number]: AccountSubscriptionInfo;
2167
- } = {};
2168
-
2169
- /** @internal */ _programAccountChangeSubscriptionCounter: number = 0;
2170
- /** @internal */ _programAccountChangeSubscriptions: {
2171
- [id: number]: ProgramAccountSubscriptionInfo;
2172
- } = {};
2173
-
2174
- /** @internal */ _rootSubscriptionCounter: number = 0;
2175
- /** @internal */ _rootSubscriptions: {
2176
- [id: number]: RootSubscriptionInfo;
2219
+ /** @internal */ private _nextClientSubscriptionId: ClientSubscriptionId = 0;
2220
+ /** @internal */ private _subscriptionDisposeFunctionsByClientSubscriptionId: {
2221
+ [clientSubscriptionId: ClientSubscriptionId]:
2222
+ | SubscriptionDisposeFn
2223
+ | undefined;
2177
2224
  } = {};
2178
-
2179
- /** @internal */ _signatureSubscriptionCounter: number = 0;
2180
- /** @internal */ _signatureSubscriptions: {
2181
- [id: number]: SignatureSubscriptionInfo;
2182
- } = {};
2183
-
2184
- /** @internal */ _slotSubscriptionCounter: number = 0;
2185
- /** @internal */ _slotSubscriptions: {
2186
- [id: number]: SlotSubscriptionInfo;
2187
- } = {};
2188
-
2189
- /** @internal */ _logsSubscriptionCounter: number = 0;
2190
- /** @internal */ _logsSubscriptions: {
2191
- [id: number]: LogsSubscriptionInfo;
2225
+ /** @internal */ private _subscriptionCallbacksByServerSubscriptionId: {
2226
+ [serverSubscriptionId: ServerSubscriptionId]:
2227
+ | Set<SubscriptionConfig['callback']>
2228
+ | undefined;
2192
2229
  } = {};
2193
-
2194
- /** @internal */ _slotUpdateSubscriptionCounter: number = 0;
2195
- /** @internal */ _slotUpdateSubscriptions: {
2196
- [id: number]: SlotUpdateSubscriptionInfo;
2230
+ /** @internal */ private _subscriptionsByHash: {
2231
+ [hash: SubscriptionConfigHash]: Subscription | undefined;
2197
2232
  } = {};
2233
+ /**
2234
+ * Special case.
2235
+ * After a signature is processed, RPCs automatically dispose of the
2236
+ * subscription on the server side. We need to track which of these
2237
+ * subscriptions have been disposed in such a way, so that we know
2238
+ * whether the client is dealing with a not-yet-processed signature
2239
+ * (in which case we must tear down the server subscription) or an
2240
+ * already-processed signature (in which case the client can simply
2241
+ * clear out the subscription locally without telling the server).
2242
+ *
2243
+ * NOTE: There is a proposal to eliminate this special case, here:
2244
+ * https://github.com/solana-labs/solana/issues/18892
2245
+ */
2246
+ /** @internal */ private _subscriptionsAutoDisposedByRpc: Set<ServerSubscriptionId> =
2247
+ new Set();
2198
2248
 
2199
2249
  /**
2200
2250
  * Establish a JSON RPC connection
@@ -4131,6 +4181,7 @@ export class Connection {
4131
4181
  * @internal
4132
4182
  */
4133
4183
  _wsOnClose(code: number) {
4184
+ this._rpcWebSocketGeneration++;
4134
4185
  if (this._rpcWebSocketHeartbeat) {
4135
4186
  clearInterval(this._rpcWebSocketHeartbeat);
4136
4187
  this._rpcWebSocketHeartbeat = null;
@@ -4143,114 +4194,22 @@ export class Connection {
4143
4194
  }
4144
4195
 
4145
4196
  // implicit close, prepare subscriptions for auto-reconnect
4146
- this._resetSubscriptions();
4147
- }
4148
-
4149
- /**
4150
- * @internal
4151
- */
4152
- async _subscribe(
4153
- sub: {subscriptionId: SubscriptionId | null},
4154
- rpcMethod: string,
4155
- rpcArgs: IWSRequestParams,
4156
- ) {
4157
- if (sub.subscriptionId == null) {
4158
- sub.subscriptionId = 'subscribing';
4159
- try {
4160
- const id = await this._rpcWebSocket.call(rpcMethod, rpcArgs);
4161
- if (typeof id === 'number' && sub.subscriptionId === 'subscribing') {
4162
- // eslint-disable-next-line require-atomic-updates
4163
- sub.subscriptionId = id;
4164
- }
4165
- } catch (err) {
4166
- if (sub.subscriptionId === 'subscribing') {
4167
- // eslint-disable-next-line require-atomic-updates
4168
- sub.subscriptionId = null;
4169
- }
4170
- if (err instanceof Error) {
4171
- console.error(
4172
- `${rpcMethod} error for argument`,
4173
- rpcArgs,
4174
- err.message,
4175
- );
4176
- }
4177
- }
4178
- }
4179
- }
4180
-
4181
- /**
4182
- * @internal
4183
- */
4184
- async _unsubscribe(
4185
- sub: {subscriptionId: SubscriptionId | null},
4186
- rpcMethod: string,
4187
- ) {
4188
- const subscriptionId = sub.subscriptionId;
4189
- if (subscriptionId != null && typeof subscriptionId != 'string') {
4190
- const unsubscribeId: number = subscriptionId;
4191
- try {
4192
- await this._rpcWebSocket.call(rpcMethod, [unsubscribeId]);
4193
- } catch (err) {
4194
- if (err instanceof Error) {
4195
- console.error(`${rpcMethod} error:`, err.message);
4196
- }
4197
- }
4198
- }
4199
- }
4200
-
4201
- /**
4202
- * @internal
4203
- */
4204
- _resetSubscriptions() {
4205
- Object.values(this._accountChangeSubscriptions).forEach(
4206
- s => (s.subscriptionId = null),
4207
- );
4208
- Object.values(this._logsSubscriptions).forEach(
4209
- s => (s.subscriptionId = null),
4210
- );
4211
- Object.values(this._programAccountChangeSubscriptions).forEach(
4212
- s => (s.subscriptionId = null),
4213
- );
4214
- Object.values(this._rootSubscriptions).forEach(
4215
- s => (s.subscriptionId = null),
4216
- );
4217
- Object.values(this._signatureSubscriptions).forEach(
4218
- s => (s.subscriptionId = null),
4219
- );
4220
- Object.values(this._slotSubscriptions).forEach(
4221
- s => (s.subscriptionId = null),
4222
- );
4223
- Object.values(this._slotUpdateSubscriptions).forEach(
4224
- s => (s.subscriptionId = null),
4225
- );
4197
+ this._subscriptionCallbacksByServerSubscriptionId = {};
4198
+ Object.entries(
4199
+ this._subscriptionsByHash as Record<SubscriptionConfigHash, Subscription>,
4200
+ ).forEach(([hash, subscription]) => {
4201
+ this._subscriptionsByHash[hash] = {
4202
+ ...subscription,
4203
+ state: 'pending',
4204
+ };
4205
+ });
4226
4206
  }
4227
4207
 
4228
4208
  /**
4229
4209
  * @internal
4230
4210
  */
4231
- _updateSubscriptions() {
4232
- const accountKeys = Object.keys(this._accountChangeSubscriptions).map(
4233
- Number,
4234
- );
4235
- const programKeys = Object.keys(
4236
- this._programAccountChangeSubscriptions,
4237
- ).map(Number);
4238
- const slotKeys = Object.keys(this._slotSubscriptions).map(Number);
4239
- const slotUpdateKeys = Object.keys(this._slotUpdateSubscriptions).map(
4240
- Number,
4241
- );
4242
- const signatureKeys = Object.keys(this._signatureSubscriptions).map(Number);
4243
- const rootKeys = Object.keys(this._rootSubscriptions).map(Number);
4244
- const logsKeys = Object.keys(this._logsSubscriptions).map(Number);
4245
- if (
4246
- accountKeys.length === 0 &&
4247
- programKeys.length === 0 &&
4248
- slotKeys.length === 0 &&
4249
- slotUpdateKeys.length === 0 &&
4250
- signatureKeys.length === 0 &&
4251
- rootKeys.length === 0 &&
4252
- logsKeys.length === 0
4253
- ) {
4211
+ async _updateSubscriptions() {
4212
+ if (Object.keys(this._subscriptionsByHash).length === 0) {
4254
4213
  if (this._rpcWebSocketConnected) {
4255
4214
  this._rpcWebSocketConnected = false;
4256
4215
  this._rpcWebSocketIdleTimeout = setTimeout(() => {
@@ -4281,75 +4240,255 @@ export class Connection {
4281
4240
  return;
4282
4241
  }
4283
4242
 
4284
- for (let id of accountKeys) {
4285
- const sub = this._accountChangeSubscriptions[id];
4286
- this._subscribe(
4287
- sub,
4288
- 'accountSubscribe',
4289
- this._buildArgs([sub.publicKey], sub.commitment, 'base64'),
4290
- );
4291
- }
4292
-
4293
- for (let id of programKeys) {
4294
- const sub = this._programAccountChangeSubscriptions[id];
4295
- this._subscribe(
4296
- sub,
4297
- 'programSubscribe',
4298
- this._buildArgs([sub.programId], sub.commitment, 'base64', {
4299
- filters: sub.filters,
4300
- }),
4301
- );
4302
- }
4303
-
4304
- for (let id of slotKeys) {
4305
- const sub = this._slotSubscriptions[id];
4306
- this._subscribe(sub, 'slotSubscribe', []);
4307
- }
4308
-
4309
- for (let id of slotUpdateKeys) {
4310
- const sub = this._slotUpdateSubscriptions[id];
4311
- this._subscribe(sub, 'slotsUpdatesSubscribe', []);
4312
- }
4243
+ const activeWebSocketGeneration = this._rpcWebSocketGeneration;
4244
+ const isCurrentConnectionStillActive = () => {
4245
+ return activeWebSocketGeneration === this._rpcWebSocketGeneration;
4246
+ };
4313
4247
 
4314
- for (let id of signatureKeys) {
4315
- const sub = this._signatureSubscriptions[id];
4316
- const args: any[] = [sub.signature];
4317
- if (sub.options) args.push(sub.options);
4318
- this._subscribe(sub, 'signatureSubscribe', args);
4319
- }
4248
+ await Promise.all(
4249
+ // Don't be tempted to change this to `Object.entries`. We call
4250
+ // `_updateSubscriptions` recursively when processing the state,
4251
+ // so it's important that we look up the *current* version of
4252
+ // each subscription, every time we process a hash.
4253
+ Object.keys(this._subscriptionsByHash).map(async hash => {
4254
+ const subscription = this._subscriptionsByHash[hash];
4255
+ if (subscription === undefined) {
4256
+ // This entry has since been deleted. Skip.
4257
+ return;
4258
+ }
4259
+ switch (subscription.state) {
4260
+ case 'pending':
4261
+ case 'unsubscribed':
4262
+ if (subscription.callbacks.size === 0) {
4263
+ /**
4264
+ * You can end up here when:
4265
+ *
4266
+ * - a subscription has recently unsubscribed
4267
+ * without having new callbacks added to it
4268
+ * while the unsubscribe was in flight, or
4269
+ * - when a pending subscription has its
4270
+ * listeners removed before a request was
4271
+ * sent to the server.
4272
+ *
4273
+ * Being that nobody is interested in this
4274
+ * subscription any longer, delete it.
4275
+ */
4276
+ delete this._subscriptionsByHash[hash];
4277
+ if (subscription.state === 'unsubscribed') {
4278
+ delete this._subscriptionCallbacksByServerSubscriptionId[
4279
+ subscription.serverSubscriptionId
4280
+ ];
4281
+ }
4282
+ await this._updateSubscriptions();
4283
+ return;
4284
+ }
4285
+ await (async () => {
4286
+ const {args, method} = subscription;
4287
+ try {
4288
+ this._subscriptionsByHash[hash] = {
4289
+ ...subscription,
4290
+ state: 'subscribing',
4291
+ };
4292
+ const serverSubscriptionId: ServerSubscriptionId =
4293
+ (await this._rpcWebSocket.call(method, args)) as number;
4294
+ this._subscriptionsByHash[hash] = {
4295
+ ...subscription,
4296
+ serverSubscriptionId,
4297
+ state: 'subscribed',
4298
+ };
4299
+ this._subscriptionCallbacksByServerSubscriptionId[
4300
+ serverSubscriptionId
4301
+ ] = subscription.callbacks;
4302
+ await this._updateSubscriptions();
4303
+ } catch (e) {
4304
+ if (e instanceof Error) {
4305
+ console.error(
4306
+ `${method} error for argument`,
4307
+ args,
4308
+ e.message,
4309
+ );
4310
+ }
4311
+ if (!isCurrentConnectionStillActive()) {
4312
+ return;
4313
+ }
4314
+ // TODO: Maybe add an 'errored' state or a retry limit?
4315
+ this._subscriptionsByHash[hash] = {
4316
+ ...subscription,
4317
+ state: 'pending',
4318
+ };
4319
+ await this._updateSubscriptions();
4320
+ }
4321
+ })();
4322
+ break;
4323
+ case 'subscribed':
4324
+ if (subscription.callbacks.size === 0) {
4325
+ // By the time we successfully set up a subscription
4326
+ // with the server, the client stopped caring about it.
4327
+ // Tear it down now.
4328
+ await (async () => {
4329
+ const {serverSubscriptionId, unsubscribeMethod} = subscription;
4330
+ if (
4331
+ this._subscriptionsAutoDisposedByRpc.has(serverSubscriptionId)
4332
+ ) {
4333
+ /**
4334
+ * Special case.
4335
+ * If we're dealing with a subscription that has been auto-
4336
+ * disposed by the RPC, then we can skip the RPC call to
4337
+ * tear down the subscription here.
4338
+ *
4339
+ * NOTE: There is a proposal to eliminate this special case, here:
4340
+ * https://github.com/solana-labs/solana/issues/18892
4341
+ */
4342
+ this._subscriptionsAutoDisposedByRpc.delete(
4343
+ serverSubscriptionId,
4344
+ );
4345
+ } else {
4346
+ this._subscriptionsByHash[hash] = {
4347
+ ...subscription,
4348
+ state: 'unsubscribing',
4349
+ };
4350
+ try {
4351
+ await this._rpcWebSocket.call(unsubscribeMethod, [
4352
+ serverSubscriptionId,
4353
+ ]);
4354
+ } catch (e) {
4355
+ if (e instanceof Error) {
4356
+ console.error(`${unsubscribeMethod} error:`, e.message);
4357
+ }
4358
+ if (!isCurrentConnectionStillActive()) {
4359
+ return;
4360
+ }
4361
+ // TODO: Maybe add an 'errored' state or a retry limit?
4362
+ this._subscriptionsByHash[hash] = {
4363
+ ...subscription,
4364
+ state: 'subscribed',
4365
+ };
4366
+ await this._updateSubscriptions();
4367
+ return;
4368
+ }
4369
+ }
4370
+ this._subscriptionsByHash[hash] = {
4371
+ ...subscription,
4372
+ state: 'unsubscribed',
4373
+ };
4374
+ await this._updateSubscriptions();
4375
+ })();
4376
+ }
4377
+ break;
4378
+ case 'subscribing':
4379
+ case 'unsubscribing':
4380
+ break;
4381
+ }
4382
+ }),
4383
+ );
4384
+ }
4320
4385
 
4321
- for (let id of rootKeys) {
4322
- const sub = this._rootSubscriptions[id];
4323
- this._subscribe(sub, 'rootSubscribe', []);
4386
+ /**
4387
+ * @internal
4388
+ */
4389
+ private _handleServerNotification<
4390
+ TCallback extends SubscriptionConfig['callback'],
4391
+ >(
4392
+ serverSubscriptionId: ServerSubscriptionId,
4393
+ callbackArgs: Parameters<TCallback>,
4394
+ ): void {
4395
+ const callbacks =
4396
+ this._subscriptionCallbacksByServerSubscriptionId[serverSubscriptionId];
4397
+ if (callbacks === undefined) {
4398
+ return;
4324
4399
  }
4325
-
4326
- for (let id of logsKeys) {
4327
- const sub = this._logsSubscriptions[id];
4328
- let filter;
4329
- if (typeof sub.filter === 'object') {
4330
- filter = {mentions: [sub.filter.toString()]};
4331
- } else {
4332
- filter = sub.filter;
4400
+ callbacks.forEach(cb => {
4401
+ try {
4402
+ cb(
4403
+ // I failed to find a way to convince TypeScript that `cb` is of type
4404
+ // `TCallback` which is certainly compatible with `Parameters<TCallback>`.
4405
+ // See https://github.com/microsoft/TypeScript/issues/47615
4406
+ // @ts-ignore
4407
+ ...callbackArgs,
4408
+ );
4409
+ } catch (e) {
4410
+ console.error(e);
4333
4411
  }
4334
- this._subscribe(
4335
- sub,
4336
- 'logsSubscribe',
4337
- this._buildArgs([filter], sub.commitment),
4338
- );
4339
- }
4412
+ });
4340
4413
  }
4341
4414
 
4342
4415
  /**
4343
4416
  * @internal
4344
4417
  */
4345
4418
  _wsOnAccountNotification(notification: object) {
4346
- const res = create(notification, AccountNotificationResult);
4347
- for (const sub of Object.values(this._accountChangeSubscriptions)) {
4348
- if (sub.subscriptionId === res.subscription) {
4349
- sub.callback(res.result.value, res.result.context);
4350
- return;
4351
- }
4419
+ const {result, subscription} = create(
4420
+ notification,
4421
+ AccountNotificationResult,
4422
+ );
4423
+ this._handleServerNotification<AccountChangeCallback>(subscription, [
4424
+ result.value,
4425
+ result.context,
4426
+ ]);
4427
+ }
4428
+
4429
+ /**
4430
+ * @internal
4431
+ */
4432
+ private _makeSubscription(
4433
+ subscriptionConfig: SubscriptionConfig,
4434
+ /**
4435
+ * When preparing `args` for a call to `_makeSubscription`, be sure
4436
+ * to carefully apply a default `commitment` property, if necessary.
4437
+ *
4438
+ * - If the user supplied a `commitment` use that.
4439
+ * - Otherwise, if the `Connection::commitment` is set, use that.
4440
+ * - Otherwise, set it to the RPC server default: `finalized`.
4441
+ *
4442
+ * This is extremely important to ensure that these two fundamentally
4443
+ * identical subscriptions produce the same identifying hash:
4444
+ *
4445
+ * - A subscription made without specifying a commitment.
4446
+ * - A subscription made where the commitment specified is the same
4447
+ * as the default applied to the subscription above.
4448
+ *
4449
+ * Example; these two subscriptions must produce the same hash:
4450
+ *
4451
+ * - An `accountSubscribe` subscription for `'PUBKEY'`
4452
+ * - An `accountSubscribe` subscription for `'PUBKEY'` with commitment
4453
+ * `'finalized'`.
4454
+ *
4455
+ * See the 'making a subscription with defaulted params omitted' test
4456
+ * in `connection-subscriptions.ts` for more.
4457
+ */
4458
+ args: IWSRequestParams,
4459
+ ): ClientSubscriptionId {
4460
+ const clientSubscriptionId = this._nextClientSubscriptionId++;
4461
+ const hash = fastStableStringify(
4462
+ [subscriptionConfig.method, args],
4463
+ true /* isArrayProp */,
4464
+ );
4465
+ const existingSubscription = this._subscriptionsByHash[hash];
4466
+ if (existingSubscription === undefined) {
4467
+ this._subscriptionsByHash[hash] = {
4468
+ ...subscriptionConfig,
4469
+ args,
4470
+ callbacks: new Set([subscriptionConfig.callback]),
4471
+ state: 'pending',
4472
+ };
4473
+ } else {
4474
+ existingSubscription.callbacks.add(subscriptionConfig.callback);
4352
4475
  }
4476
+ this._subscriptionDisposeFunctionsByClientSubscriptionId[
4477
+ clientSubscriptionId
4478
+ ] = async () => {
4479
+ delete this._subscriptionDisposeFunctionsByClientSubscriptionId[
4480
+ clientSubscriptionId
4481
+ ];
4482
+ const subscription = this._subscriptionsByHash[hash];
4483
+ assert(
4484
+ subscription !== undefined,
4485
+ `Could not find a \`Subscription\` when tearing down client subscription #${clientSubscriptionId}`,
4486
+ );
4487
+ subscription.callbacks.delete(subscriptionConfig.callback);
4488
+ await this._updateSubscriptions();
4489
+ };
4490
+ this._updateSubscriptions();
4491
+ return clientSubscriptionId;
4353
4492
  }
4354
4493
 
4355
4494
  /**
@@ -4364,52 +4503,51 @@ export class Connection {
4364
4503
  publicKey: PublicKey,
4365
4504
  callback: AccountChangeCallback,
4366
4505
  commitment?: Commitment,
4367
- ): number {
4368
- const id = ++this._accountChangeSubscriptionCounter;
4369
- this._accountChangeSubscriptions[id] = {
4370
- publicKey: publicKey.toBase58(),
4371
- callback,
4372
- commitment,
4373
- subscriptionId: null,
4374
- };
4375
- this._updateSubscriptions();
4376
- return id;
4506
+ ): ClientSubscriptionId {
4507
+ const args = this._buildArgs(
4508
+ [publicKey.toBase58()],
4509
+ commitment || this._commitment || 'finalized', // Apply connection/server default.
4510
+ 'base64',
4511
+ );
4512
+ return this._makeSubscription(
4513
+ {
4514
+ callback,
4515
+ method: 'accountSubscribe',
4516
+ unsubscribeMethod: 'accountUnsubscribe',
4517
+ },
4518
+ args,
4519
+ );
4377
4520
  }
4378
4521
 
4379
4522
  /**
4380
4523
  * Deregister an account notification callback
4381
4524
  *
4382
- * @param id subscription id to deregister
4525
+ * @param id client subscription id to deregister
4383
4526
  */
4384
- async removeAccountChangeListener(id: number): Promise<void> {
4385
- if (this._accountChangeSubscriptions[id]) {
4386
- const subInfo = this._accountChangeSubscriptions[id];
4387
- delete this._accountChangeSubscriptions[id];
4388
- await this._unsubscribe(subInfo, 'accountUnsubscribe');
4389
- this._updateSubscriptions();
4390
- } else {
4391
- console.warn(createSubscriptionWarningMessage(id, 'account change'));
4392
- }
4527
+ async removeAccountChangeListener(
4528
+ clientSubscriptionId: ClientSubscriptionId,
4529
+ ): Promise<void> {
4530
+ await this._unsubscribeClientSubscription(
4531
+ clientSubscriptionId,
4532
+ 'account change',
4533
+ );
4393
4534
  }
4394
4535
 
4395
4536
  /**
4396
4537
  * @internal
4397
4538
  */
4398
4539
  _wsOnProgramAccountNotification(notification: Object) {
4399
- const res = create(notification, ProgramAccountNotificationResult);
4400
- for (const sub of Object.values(this._programAccountChangeSubscriptions)) {
4401
- if (sub.subscriptionId === res.subscription) {
4402
- const {value, context} = res.result;
4403
- sub.callback(
4404
- {
4405
- accountId: value.pubkey,
4406
- accountInfo: value.account,
4407
- },
4408
- context,
4409
- );
4410
- return;
4411
- }
4412
- }
4540
+ const {result, subscription} = create(
4541
+ notification,
4542
+ ProgramAccountNotificationResult,
4543
+ );
4544
+ this._handleServerNotification<ProgramAccountChangeCallback>(subscription, [
4545
+ {
4546
+ accountId: result.value.pubkey,
4547
+ accountInfo: result.value.account,
4548
+ },
4549
+ result.context,
4550
+ ]);
4413
4551
  }
4414
4552
 
4415
4553
  /**
@@ -4427,35 +4565,35 @@ export class Connection {
4427
4565
  callback: ProgramAccountChangeCallback,
4428
4566
  commitment?: Commitment,
4429
4567
  filters?: GetProgramAccountsFilter[],
4430
- ): number {
4431
- const id = ++this._programAccountChangeSubscriptionCounter;
4432
- this._programAccountChangeSubscriptions[id] = {
4433
- programId: programId.toBase58(),
4434
- callback,
4435
- commitment,
4436
- subscriptionId: null,
4437
- filters,
4438
- };
4439
- this._updateSubscriptions();
4440
- return id;
4568
+ ): ClientSubscriptionId {
4569
+ const args = this._buildArgs(
4570
+ [programId.toBase58()],
4571
+ commitment || this._commitment || 'finalized', // Apply connection/server default.
4572
+ 'base64' /* encoding */,
4573
+ filters ? {filters: filters} : undefined /* extra */,
4574
+ );
4575
+ return this._makeSubscription(
4576
+ {
4577
+ callback,
4578
+ method: 'programSubscribe',
4579
+ unsubscribeMethod: 'programUnsubscribe',
4580
+ },
4581
+ args,
4582
+ );
4441
4583
  }
4442
4584
 
4443
4585
  /**
4444
4586
  * Deregister an account notification callback
4445
4587
  *
4446
- * @param id subscription id to deregister
4588
+ * @param id client subscription id to deregister
4447
4589
  */
4448
- async removeProgramAccountChangeListener(id: number): Promise<void> {
4449
- if (this._programAccountChangeSubscriptions[id]) {
4450
- const subInfo = this._programAccountChangeSubscriptions[id];
4451
- delete this._programAccountChangeSubscriptions[id];
4452
- await this._unsubscribe(subInfo, 'programUnsubscribe');
4453
- this._updateSubscriptions();
4454
- } else {
4455
- console.warn(
4456
- createSubscriptionWarningMessage(id, 'program account change'),
4457
- );
4458
- }
4590
+ async removeProgramAccountChangeListener(
4591
+ clientSubscriptionId: ClientSubscriptionId,
4592
+ ): Promise<void> {
4593
+ await this._unsubscribeClientSubscription(
4594
+ clientSubscriptionId,
4595
+ 'program account change',
4596
+ );
4459
4597
  }
4460
4598
 
4461
4599
  /**
@@ -4465,60 +4603,49 @@ export class Connection {
4465
4603
  filter: LogsFilter,
4466
4604
  callback: LogsCallback,
4467
4605
  commitment?: Commitment,
4468
- ): number {
4469
- const id = ++this._logsSubscriptionCounter;
4470
- this._logsSubscriptions[id] = {
4471
- filter,
4472
- callback,
4473
- commitment,
4474
- subscriptionId: null,
4475
- };
4476
- this._updateSubscriptions();
4477
- return id;
4606
+ ): ClientSubscriptionId {
4607
+ const args = this._buildArgs(
4608
+ [typeof filter === 'object' ? {mentions: [filter.toString()]} : filter],
4609
+ commitment || this._commitment || 'finalized', // Apply connection/server default.
4610
+ );
4611
+ return this._makeSubscription(
4612
+ {
4613
+ callback,
4614
+ method: 'logsSubscribe',
4615
+ unsubscribeMethod: 'logsUnsubscribe',
4616
+ },
4617
+ args,
4618
+ );
4478
4619
  }
4479
4620
 
4480
4621
  /**
4481
4622
  * Deregister a logs callback.
4482
4623
  *
4483
- * @param id subscription id to deregister.
4624
+ * @param id client subscription id to deregister.
4484
4625
  */
4485
- async removeOnLogsListener(id: number): Promise<void> {
4486
- if (this._logsSubscriptions[id]) {
4487
- const subInfo = this._logsSubscriptions[id];
4488
- delete this._logsSubscriptions[id];
4489
- await this._unsubscribe(subInfo, 'logsUnsubscribe');
4490
- this._updateSubscriptions();
4491
- } else {
4492
- console.warn(createSubscriptionWarningMessage(id, 'logs'));
4493
- }
4626
+ async removeOnLogsListener(
4627
+ clientSubscriptionId: ClientSubscriptionId,
4628
+ ): Promise<void> {
4629
+ await this._unsubscribeClientSubscription(clientSubscriptionId, 'logs');
4494
4630
  }
4495
4631
 
4496
4632
  /**
4497
4633
  * @internal
4498
4634
  */
4499
4635
  _wsOnLogsNotification(notification: Object) {
4500
- const res = create(notification, LogsNotificationResult);
4501
- const keys = Object.keys(this._logsSubscriptions).map(Number);
4502
- for (let id of keys) {
4503
- const sub = this._logsSubscriptions[id];
4504
- if (sub.subscriptionId === res.subscription) {
4505
- sub.callback(res.result.value, res.result.context);
4506
- return;
4507
- }
4508
- }
4636
+ const {result, subscription} = create(notification, LogsNotificationResult);
4637
+ this._handleServerNotification<LogsCallback>(subscription, [
4638
+ result.value,
4639
+ result.context,
4640
+ ]);
4509
4641
  }
4510
4642
 
4511
4643
  /**
4512
4644
  * @internal
4513
4645
  */
4514
4646
  _wsOnSlotNotification(notification: Object) {
4515
- const res = create(notification, SlotNotificationResult);
4516
- for (const sub of Object.values(this._slotSubscriptions)) {
4517
- if (sub.subscriptionId === res.subscription) {
4518
- sub.callback(res.result);
4519
- return;
4520
- }
4521
- }
4647
+ const {result, subscription} = create(notification, SlotNotificationResult);
4648
+ this._handleServerNotification<SlotChangeCallback>(subscription, [result]);
4522
4649
  }
4523
4650
 
4524
4651
  /**
@@ -4527,43 +4654,40 @@ export class Connection {
4527
4654
  * @param callback Function to invoke whenever the slot changes
4528
4655
  * @return subscription id
4529
4656
  */
4530
- onSlotChange(callback: SlotChangeCallback): number {
4531
- const id = ++this._slotSubscriptionCounter;
4532
- this._slotSubscriptions[id] = {
4533
- callback,
4534
- subscriptionId: null,
4535
- };
4536
- this._updateSubscriptions();
4537
- return id;
4657
+ onSlotChange(callback: SlotChangeCallback): ClientSubscriptionId {
4658
+ return this._makeSubscription(
4659
+ {
4660
+ callback,
4661
+ method: 'slotSubscribe',
4662
+ unsubscribeMethod: 'slotUnsubscribe',
4663
+ },
4664
+ [] /* args */,
4665
+ );
4538
4666
  }
4539
4667
 
4540
4668
  /**
4541
4669
  * Deregister a slot notification callback
4542
4670
  *
4543
- * @param id subscription id to deregister
4671
+ * @param id client subscription id to deregister
4544
4672
  */
4545
- async removeSlotChangeListener(id: number): Promise<void> {
4546
- if (this._slotSubscriptions[id]) {
4547
- const subInfo = this._slotSubscriptions[id];
4548
- delete this._slotSubscriptions[id];
4549
- await this._unsubscribe(subInfo, 'slotUnsubscribe');
4550
- this._updateSubscriptions();
4551
- } else {
4552
- console.warn(createSubscriptionWarningMessage(id, 'slot change'));
4553
- }
4673
+ async removeSlotChangeListener(
4674
+ clientSubscriptionId: ClientSubscriptionId,
4675
+ ): Promise<void> {
4676
+ await this._unsubscribeClientSubscription(
4677
+ clientSubscriptionId,
4678
+ 'slot change',
4679
+ );
4554
4680
  }
4555
4681
 
4556
4682
  /**
4557
4683
  * @internal
4558
4684
  */
4559
4685
  _wsOnSlotUpdatesNotification(notification: Object) {
4560
- const res = create(notification, SlotUpdateNotificationResult);
4561
- for (const sub of Object.values(this._slotUpdateSubscriptions)) {
4562
- if (sub.subscriptionId === res.subscription) {
4563
- sub.callback(res.result);
4564
- return;
4565
- }
4566
- }
4686
+ const {result, subscription} = create(
4687
+ notification,
4688
+ SlotUpdateNotificationResult,
4689
+ );
4690
+ this._handleServerNotification<SlotUpdateCallback>(subscription, [result]);
4567
4691
  }
4568
4692
 
4569
4693
  /**
@@ -4573,29 +4697,51 @@ export class Connection {
4573
4697
  * @param callback Function to invoke whenever the slot updates
4574
4698
  * @return subscription id
4575
4699
  */
4576
- onSlotUpdate(callback: SlotUpdateCallback): number {
4577
- const id = ++this._slotUpdateSubscriptionCounter;
4578
- this._slotUpdateSubscriptions[id] = {
4579
- callback,
4580
- subscriptionId: null,
4581
- };
4582
- this._updateSubscriptions();
4583
- return id;
4700
+ onSlotUpdate(callback: SlotUpdateCallback): ClientSubscriptionId {
4701
+ return this._makeSubscription(
4702
+ {
4703
+ callback,
4704
+ method: 'slotsUpdatesSubscribe',
4705
+ unsubscribeMethod: 'slotsUpdatesUnsubscribe',
4706
+ },
4707
+ [] /* args */,
4708
+ );
4584
4709
  }
4585
4710
 
4586
4711
  /**
4587
4712
  * Deregister a slot update notification callback
4588
4713
  *
4589
- * @param id subscription id to deregister
4714
+ * @param id client subscription id to deregister
4590
4715
  */
4591
- async removeSlotUpdateListener(id: number): Promise<void> {
4592
- if (this._slotUpdateSubscriptions[id]) {
4593
- const subInfo = this._slotUpdateSubscriptions[id];
4594
- delete this._slotUpdateSubscriptions[id];
4595
- await this._unsubscribe(subInfo, 'slotsUpdatesUnsubscribe');
4596
- this._updateSubscriptions();
4716
+ async removeSlotUpdateListener(
4717
+ clientSubscriptionId: ClientSubscriptionId,
4718
+ ): Promise<void> {
4719
+ await this._unsubscribeClientSubscription(
4720
+ clientSubscriptionId,
4721
+ 'slot update',
4722
+ );
4723
+ }
4724
+
4725
+ /**
4726
+ * @internal
4727
+ */
4728
+
4729
+ private async _unsubscribeClientSubscription(
4730
+ clientSubscriptionId: ClientSubscriptionId,
4731
+ subscriptionName: string,
4732
+ ) {
4733
+ const dispose =
4734
+ this._subscriptionDisposeFunctionsByClientSubscriptionId[
4735
+ clientSubscriptionId
4736
+ ];
4737
+ if (dispose) {
4738
+ await dispose();
4597
4739
  } else {
4598
- console.warn(createSubscriptionWarningMessage(id, 'slot update'));
4740
+ console.warn(
4741
+ 'Ignored unsubscribe request because an active subscription with id ' +
4742
+ `\`${clientSubscriptionId}\` for '${subscriptionName}' events ` +
4743
+ 'could not be found.',
4744
+ );
4599
4745
  }
4600
4746
  }
4601
4747
 
@@ -4646,32 +4792,32 @@ export class Connection {
4646
4792
  * @internal
4647
4793
  */
4648
4794
  _wsOnSignatureNotification(notification: Object) {
4649
- const res = create(notification, SignatureNotificationResult);
4650
- for (const [id, sub] of Object.entries(this._signatureSubscriptions)) {
4651
- if (sub.subscriptionId === res.subscription) {
4652
- if (res.result.value === 'receivedSignature') {
4653
- sub.callback(
4654
- {
4655
- type: 'received',
4656
- },
4657
- res.result.context,
4658
- );
4659
- } else {
4660
- // Signatures subscriptions are auto-removed by the RPC service so
4661
- // no need to explicitly send an unsubscribe message
4662
- delete this._signatureSubscriptions[Number(id)];
4663
- this._updateSubscriptions();
4664
- sub.callback(
4665
- {
4666
- type: 'status',
4667
- result: res.result.value,
4668
- },
4669
- res.result.context,
4670
- );
4671
- }
4672
- return;
4673
- }
4795
+ const {result, subscription} = create(
4796
+ notification,
4797
+ SignatureNotificationResult,
4798
+ );
4799
+ if (result.value !== 'receivedSignature') {
4800
+ /**
4801
+ * Special case.
4802
+ * After a signature is processed, RPCs automatically dispose of the
4803
+ * subscription on the server side. We need to track which of these
4804
+ * subscriptions have been disposed in such a way, so that we know
4805
+ * whether the client is dealing with a not-yet-processed signature
4806
+ * (in which case we must tear down the server subscription) or an
4807
+ * already-processed signature (in which case the client can simply
4808
+ * clear out the subscription locally without telling the server).
4809
+ *
4810
+ * NOTE: There is a proposal to eliminate this special case, here:
4811
+ * https://github.com/solana-labs/solana/issues/18892
4812
+ */
4813
+ this._subscriptionsAutoDisposedByRpc.add(subscription);
4674
4814
  }
4815
+ this._handleServerNotification<SignatureSubscriptionCallback>(
4816
+ subscription,
4817
+ result.value === 'receivedSignature'
4818
+ ? [{type: 'received'}, result.context]
4819
+ : [{type: 'status', result: result.value}, result.context],
4820
+ );
4675
4821
  }
4676
4822
 
4677
4823
  /**
@@ -4686,20 +4832,32 @@ export class Connection {
4686
4832
  signature: TransactionSignature,
4687
4833
  callback: SignatureResultCallback,
4688
4834
  commitment?: Commitment,
4689
- ): number {
4690
- const id = ++this._signatureSubscriptionCounter;
4691
- this._signatureSubscriptions[id] = {
4692
- signature,
4693
- callback: (notification, context) => {
4694
- if (notification.type === 'status') {
4695
- callback(notification.result, context);
4696
- }
4835
+ ): ClientSubscriptionId {
4836
+ const args = this._buildArgs(
4837
+ [signature],
4838
+ commitment || this._commitment || 'finalized', // Apply connection/server default.
4839
+ );
4840
+ const clientSubscriptionId = this._makeSubscription(
4841
+ {
4842
+ callback: (notification, context) => {
4843
+ if (notification.type === 'status') {
4844
+ callback(notification.result, context);
4845
+ // Signatures subscriptions are auto-removed by the RPC service
4846
+ // so no need to explicitly send an unsubscribe message.
4847
+ try {
4848
+ this.removeSignatureListener(clientSubscriptionId);
4849
+ // eslint-disable-next-line no-empty
4850
+ } catch {
4851
+ // Already removed.
4852
+ }
4853
+ }
4854
+ },
4855
+ method: 'signatureSubscribe',
4856
+ unsubscribeMethod: 'signatureUnsubscribe',
4697
4857
  },
4698
- options: {commitment},
4699
- subscriptionId: null,
4700
- };
4701
- this._updateSubscriptions();
4702
- return id;
4858
+ args,
4859
+ );
4860
+ return clientSubscriptionId;
4703
4861
  }
4704
4862
 
4705
4863
  /**
@@ -4716,45 +4874,59 @@ export class Connection {
4716
4874
  signature: TransactionSignature,
4717
4875
  callback: SignatureSubscriptionCallback,
4718
4876
  options?: SignatureSubscriptionOptions,
4719
- ): number {
4720
- const id = ++this._signatureSubscriptionCounter;
4721
- this._signatureSubscriptions[id] = {
4722
- signature,
4723
- callback,
4724
- options,
4725
- subscriptionId: null,
4877
+ ): ClientSubscriptionId {
4878
+ const {commitment, ...extra} = {
4879
+ ...options,
4880
+ commitment:
4881
+ (options && options.commitment) || this._commitment || 'finalized', // Apply connection/server default.
4726
4882
  };
4727
- this._updateSubscriptions();
4728
- return id;
4883
+ const args = this._buildArgs(
4884
+ [signature],
4885
+ commitment,
4886
+ undefined /* encoding */,
4887
+ extra,
4888
+ );
4889
+ const clientSubscriptionId = this._makeSubscription(
4890
+ {
4891
+ callback: (notification, context) => {
4892
+ callback(notification, context);
4893
+ // Signatures subscriptions are auto-removed by the RPC service
4894
+ // so no need to explicitly send an unsubscribe message.
4895
+ try {
4896
+ this.removeSignatureListener(clientSubscriptionId);
4897
+ // eslint-disable-next-line no-empty
4898
+ } catch {
4899
+ // Already removed.
4900
+ }
4901
+ },
4902
+ method: 'signatureSubscribe',
4903
+ unsubscribeMethod: 'signatureUnsubscribe',
4904
+ },
4905
+ args,
4906
+ );
4907
+ return clientSubscriptionId;
4729
4908
  }
4730
4909
 
4731
4910
  /**
4732
4911
  * Deregister a signature notification callback
4733
4912
  *
4734
- * @param id subscription id to deregister
4913
+ * @param id client subscription id to deregister
4735
4914
  */
4736
- async removeSignatureListener(id: number): Promise<void> {
4737
- if (this._signatureSubscriptions[id]) {
4738
- const subInfo = this._signatureSubscriptions[id];
4739
- delete this._signatureSubscriptions[id];
4740
- await this._unsubscribe(subInfo, 'signatureUnsubscribe');
4741
- this._updateSubscriptions();
4742
- } else {
4743
- console.warn(createSubscriptionWarningMessage(id, 'signature result'));
4744
- }
4915
+ async removeSignatureListener(
4916
+ clientSubscriptionId: ClientSubscriptionId,
4917
+ ): Promise<void> {
4918
+ await this._unsubscribeClientSubscription(
4919
+ clientSubscriptionId,
4920
+ 'signature result',
4921
+ );
4745
4922
  }
4746
4923
 
4747
4924
  /**
4748
4925
  * @internal
4749
4926
  */
4750
4927
  _wsOnRootNotification(notification: Object) {
4751
- const res = create(notification, RootNotificationResult);
4752
- for (const sub of Object.values(this._rootSubscriptions)) {
4753
- if (sub.subscriptionId === res.subscription) {
4754
- sub.callback(res.result);
4755
- return;
4756
- }
4757
- }
4928
+ const {result, subscription} = create(notification, RootNotificationResult);
4929
+ this._handleServerNotification<RootChangeCallback>(subscription, [result]);
4758
4930
  }
4759
4931
 
4760
4932
  /**
@@ -4763,29 +4935,28 @@ export class Connection {
4763
4935
  * @param callback Function to invoke whenever the root changes
4764
4936
  * @return subscription id
4765
4937
  */
4766
- onRootChange(callback: RootChangeCallback): number {
4767
- const id = ++this._rootSubscriptionCounter;
4768
- this._rootSubscriptions[id] = {
4769
- callback,
4770
- subscriptionId: null,
4771
- };
4772
- this._updateSubscriptions();
4773
- return id;
4938
+ onRootChange(callback: RootChangeCallback): ClientSubscriptionId {
4939
+ return this._makeSubscription(
4940
+ {
4941
+ callback,
4942
+ method: 'rootSubscribe',
4943
+ unsubscribeMethod: 'rootUnsubscribe',
4944
+ },
4945
+ [] /* args */,
4946
+ );
4774
4947
  }
4775
4948
 
4776
4949
  /**
4777
4950
  * Deregister a root notification callback
4778
4951
  *
4779
- * @param id subscription id to deregister
4952
+ * @param id client subscription id to deregister
4780
4953
  */
4781
- async removeRootChangeListener(id: number): Promise<void> {
4782
- if (this._rootSubscriptions[id]) {
4783
- const subInfo = this._rootSubscriptions[id];
4784
- delete this._rootSubscriptions[id];
4785
- await this._unsubscribe(subInfo, 'rootUnsubscribe');
4786
- this._updateSubscriptions();
4787
- } else {
4788
- console.warn(createSubscriptionWarningMessage(id, 'root change'));
4789
- }
4954
+ async removeRootChangeListener(
4955
+ clientSubscriptionId: ClientSubscriptionId,
4956
+ ): Promise<void> {
4957
+ await this._unsubscribeClientSubscription(
4958
+ clientSubscriptionId,
4959
+ 'root change',
4960
+ );
4790
4961
  }
4791
4962
  }