@solana/rpc-subscriptions 2.0.0-rc.0 → 2.0.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -4
- package/dist/index.browser.cjs +268 -283
- package/dist/index.browser.cjs.map +1 -1
- package/dist/index.browser.mjs +267 -285
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.native.mjs +250 -274
- package/dist/index.native.mjs.map +1 -1
- package/dist/index.node.cjs +251 -272
- package/dist/index.node.cjs.map +1 -1
- package/dist/index.node.mjs +250 -274
- package/dist/index.node.mjs.map +1 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/rpc-subscriptions-autopinger.d.ts +5 -4
- package/dist/types/rpc-subscriptions-autopinger.d.ts.map +1 -1
- package/dist/types/rpc-subscriptions-channel-pool-internal.d.ts +13 -0
- package/dist/types/rpc-subscriptions-channel-pool-internal.d.ts.map +1 -0
- package/dist/types/rpc-subscriptions-channel-pool.d.ts +8 -0
- package/dist/types/rpc-subscriptions-channel-pool.d.ts.map +1 -0
- package/dist/types/rpc-subscriptions-channel.d.ts +12 -0
- package/dist/types/rpc-subscriptions-channel.d.ts.map +1 -0
- package/dist/types/rpc-subscriptions-clusters.d.ts +23 -1
- package/dist/types/rpc-subscriptions-clusters.d.ts.map +1 -1
- package/dist/types/rpc-subscriptions-coalescer.d.ts +2 -9
- package/dist/types/rpc-subscriptions-coalescer.d.ts.map +1 -1
- package/dist/types/rpc-subscriptions-json-bigint.d.ts +3 -0
- package/dist/types/rpc-subscriptions-json-bigint.d.ts.map +1 -0
- package/dist/types/rpc-subscriptions-json.d.ts +3 -0
- package/dist/types/rpc-subscriptions-json.d.ts.map +1 -0
- package/dist/types/rpc-subscriptions-transport.d.ts +6 -12
- package/dist/types/rpc-subscriptions-transport.d.ts.map +1 -1
- package/dist/types/rpc-subscriptions.d.ts +5 -4
- package/dist/types/rpc-subscriptions.d.ts.map +1 -1
- package/package.json +24 -10
- package/dist/types/cached-abortable-iterable.d.ts +0 -10
- package/dist/types/cached-abortable-iterable.d.ts.map +0 -1
- package/dist/types/rpc-subscriptions-connection-sharding.d.ts +0 -13
- package/dist/types/rpc-subscriptions-connection-sharding.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -6,12 +6,57 @@
|
|
|
6
6
|
|
|
7
7
|
[code-style-prettier-image]: https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square
|
|
8
8
|
[code-style-prettier-url]: https://github.com/prettier/prettier
|
|
9
|
-
[npm-downloads-image]: https://img.shields.io/npm/dm/@solana/rpc-subscriptions/
|
|
10
|
-
[npm-image]: https://img.shields.io/npm/v/@solana/rpc-subscriptions/
|
|
11
|
-
[npm-url]: https://www.npmjs.com/package/@solana/rpc-subscriptions/v/
|
|
9
|
+
[npm-downloads-image]: https://img.shields.io/npm/dm/@solana/rpc-subscriptions/rc.svg?style=flat
|
|
10
|
+
[npm-image]: https://img.shields.io/npm/v/@solana/rpc-subscriptions/rc.svg?style=flat
|
|
11
|
+
[npm-url]: https://www.npmjs.com/package/@solana/rpc-subscriptions/v/rc
|
|
12
12
|
[semantic-release-image]: https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg
|
|
13
13
|
[semantic-release-url]: https://github.com/semantic-release/semantic-release
|
|
14
14
|
|
|
15
15
|
# @solana/rpc-subscriptions
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
This package contains types that implement RPC subscriptions as required by the Solana RPC. Additionally, it incorporates some useful defaults that make working with subscriptions easier, more performant, and more reliable. It can be used standalone, but it is also exported as part of the Solana JavaScript SDK [`@solana/web3.js@rc`](https://github.com/solana-labs/solana-web3.js/tree/master/packages/library).
|
|
18
|
+
|
|
19
|
+
## Functions
|
|
20
|
+
|
|
21
|
+
### `createDefaultRpcSubscriptionsChannelCreator(config)`
|
|
22
|
+
|
|
23
|
+
Creates a function that returns new subscription channels when called.
|
|
24
|
+
|
|
25
|
+
### `createDefaultSolanaRpcSubscriptionsChannelCreator(config)`
|
|
26
|
+
|
|
27
|
+
Similar to `createDefaultRpcSubscriptionsChannelCreator` with some Solana-specific defaults. For instance, it safely handles `BigInt` values in JSON messages since Solana RPC servers accept and return integers larger than `Number.MAX_SAFE_INTEGER`.
|
|
28
|
+
|
|
29
|
+
#### Arguments
|
|
30
|
+
|
|
31
|
+
A config object with the following properties:
|
|
32
|
+
|
|
33
|
+
- `intervalMs`: The number of milliseconds to wait since the last message sent or received over the channel before sending a ping message to keep the channel open.
|
|
34
|
+
- `maxSubscriptionsPerChannel`: The number of subscribers that may share a channel before a new channel must be created (default: 100). It is important that you set this to the maximum number of subscriptions that your RPC provider recommends making over a single connection; the default is set deliberately low, so as to comply with the restrictive limits of the public mainnet RPC node.
|
|
35
|
+
- `minChannels`: The number of channels to create before reusing a channel for a new subscription.
|
|
36
|
+
- `sendBufferHighWatermark`: The number of bytes of data to admint into the `WebSocket` buffer before buffering data on the client. -`url`: The URL of the web socket server. Must use the `ws` or `wss` protocols.
|
|
37
|
+
|
|
38
|
+
### `getChannelPoolingChannelCreator(createChannel, { maxSubscriptionsPerChannel, minChannels })`
|
|
39
|
+
|
|
40
|
+
Given a channel creator, will return a new channel creator with the following behavior.
|
|
41
|
+
|
|
42
|
+
1. When called, returns an `RpcSubscriptionsChannel`. Adds that channel to a pool.
|
|
43
|
+
2. When called again, creates and returns new `RpcSubscriptionChannels` up to the number specified by `minChannels`.
|
|
44
|
+
3. When `minChannels` channels have been created, subsequent calls vend whichever existing channel from the pool has the fewest subscribers, or the next one in rotation in the event of a tie.
|
|
45
|
+
4. Once all channels carry the number of subscribers specified by the number `maxSubscriptionsPerChannel`, new channels in excess of `minChannel` will be created, returned, and added to the pool.
|
|
46
|
+
5. A channel will be destroyed once all of its subscribers' abort signals fire.
|
|
47
|
+
|
|
48
|
+
### `getRpcSubscriptionsChannelWithJSONSerialization(channel)`
|
|
49
|
+
|
|
50
|
+
Given an `RpcSubscriptionsChannel`, will return a new channel that parses data published to the `'message'` channel as JSON, and JSON-stringifies messages sent via the `send(message)` method.
|
|
51
|
+
|
|
52
|
+
### `getRpcSubscriptionsChannelWithBigIntJSONSerialization(channel)`
|
|
53
|
+
|
|
54
|
+
Similarly, to `getRpcSubscriptionsChannelWithJSONSerialization`, this function will stringify and parse JSON message to and from the given `string` channel. However, this function parses any integer value as a `BigInt` in order to safely handle numbers that exceed the JavaScript `Number.MAX_SAFE_INTEGER` value.
|
|
55
|
+
|
|
56
|
+
### `getRpcSubscriptionsChannelWithAutoping(channel)`
|
|
57
|
+
|
|
58
|
+
Given an `RpcSubscriptionsChannel`, will return a new channel that sends a ping message to the inner channel if a message has not been sent or received in the last `intervalMs`. In web browsers, this implementation sends no ping when the network is down, and sends a ping immediately upon the network coming back up.
|
|
59
|
+
|
|
60
|
+
### `getRpcSubscriptionsTransportWithSubscriptionCoalescing(transport)`
|
|
61
|
+
|
|
62
|
+
Given an `RpcSubscriptionsTransport`, will return a new transport that coalesces identical subscriptions into a single subscription request to the server. The determination of whether a subscription is the same as another is based on the `rpcRequest` returned by its `RpcSubscriptionsPlan`. The subscription will only be aborted once all subscribers abort, or there is an error.
|
package/dist/index.browser.cjs
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
var rpcSubscriptionsApi = require('@solana/rpc-subscriptions-api');
|
|
4
4
|
var rpcSubscriptionsSpec = require('@solana/rpc-subscriptions-spec');
|
|
5
5
|
var errors = require('@solana/errors');
|
|
6
|
-
var
|
|
6
|
+
var rpcSubscriptionsChannelWebsocket = require('@solana/rpc-subscriptions-channel-websocket');
|
|
7
7
|
var functional = require('@solana/functional');
|
|
8
|
-
var
|
|
8
|
+
var rpcSpecTypes = require('@solana/rpc-spec-types');
|
|
9
|
+
var fastStableStringify = require('@solana/fast-stable-stringify');
|
|
9
10
|
|
|
10
11
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
11
12
|
|
|
@@ -46,332 +47,316 @@ function createSolanaJsonRpcIntegerOverflowError(methodName, keyPath, value) {
|
|
|
46
47
|
// src/rpc-default-config.ts
|
|
47
48
|
var DEFAULT_RPC_SUBSCRIPTIONS_CONFIG = {
|
|
48
49
|
defaultCommitment: "confirmed",
|
|
49
|
-
onIntegerOverflow(
|
|
50
|
-
throw createSolanaJsonRpcIntegerOverflowError(methodName, keyPath, value);
|
|
50
|
+
onIntegerOverflow(request, keyPath, value) {
|
|
51
|
+
throw createSolanaJsonRpcIntegerOverflowError(request.methodName, keyPath, value);
|
|
51
52
|
}
|
|
52
53
|
};
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
})();
|
|
62
|
-
}
|
|
63
|
-
function getCachedAbortableIterableFactory({
|
|
64
|
-
getAbortSignalFromInputArgs,
|
|
65
|
-
getCacheKeyFromInputArgs,
|
|
66
|
-
onCacheHit,
|
|
67
|
-
onCreateIterable
|
|
54
|
+
var PING_PAYLOAD = {
|
|
55
|
+
jsonrpc: "2.0",
|
|
56
|
+
method: "ping"
|
|
57
|
+
};
|
|
58
|
+
function getRpcSubscriptionsChannelWithAutoping({
|
|
59
|
+
abortSignal: callerAbortSignal,
|
|
60
|
+
channel,
|
|
61
|
+
intervalMs
|
|
68
62
|
}) {
|
|
69
|
-
|
|
70
|
-
function
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
cacheKey: cacheKey.toString()
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
return currentCacheEntry;
|
|
78
|
-
}
|
|
79
|
-
return async (...args) => {
|
|
80
|
-
const cacheKey = getCacheKeyFromInputArgs(...args);
|
|
81
|
-
const signal = getAbortSignalFromInputArgs(...args);
|
|
82
|
-
if (cacheKey === void 0) {
|
|
83
|
-
return await onCreateIterable(signal, ...args);
|
|
84
|
-
}
|
|
85
|
-
const cleanup = () => {
|
|
86
|
-
cache.delete(cacheKey);
|
|
87
|
-
signal.removeEventListener("abort", handleAbort);
|
|
88
|
-
};
|
|
89
|
-
const handleAbort = () => {
|
|
90
|
-
const cacheEntry = getCacheEntryOrThrow(cacheKey);
|
|
91
|
-
if (cacheEntry.purgeScheduled !== true) {
|
|
92
|
-
cacheEntry.purgeScheduled = true;
|
|
93
|
-
globalThis.queueMicrotask(() => {
|
|
94
|
-
cacheEntry.purgeScheduled = false;
|
|
95
|
-
if (cacheEntry.referenceCount === 0) {
|
|
96
|
-
cacheEntry.abortController.abort();
|
|
97
|
-
cleanup();
|
|
98
|
-
}
|
|
99
|
-
});
|
|
63
|
+
let intervalId;
|
|
64
|
+
function sendPing() {
|
|
65
|
+
channel.send(PING_PAYLOAD).catch((e) => {
|
|
66
|
+
if (errors.isSolanaError(e, errors.SOLANA_ERROR__RPC_SUBSCRIPTIONS__CHANNEL_CONNECTION_CLOSED)) {
|
|
67
|
+
pingerAbortController.abort();
|
|
100
68
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
function restartPingTimer() {
|
|
72
|
+
clearInterval(intervalId);
|
|
73
|
+
intervalId = setInterval(sendPing, intervalMs);
|
|
74
|
+
}
|
|
75
|
+
const pingerAbortController = new AbortController();
|
|
76
|
+
pingerAbortController.signal.addEventListener("abort", () => {
|
|
77
|
+
clearInterval(intervalId);
|
|
78
|
+
});
|
|
79
|
+
callerAbortSignal.addEventListener("abort", () => {
|
|
80
|
+
pingerAbortController.abort();
|
|
81
|
+
});
|
|
82
|
+
channel.on(
|
|
83
|
+
"error",
|
|
84
|
+
() => {
|
|
85
|
+
pingerAbortController.abort();
|
|
86
|
+
},
|
|
87
|
+
{ signal: pingerAbortController.signal }
|
|
88
|
+
);
|
|
89
|
+
channel.on("message", restartPingTimer, { signal: pingerAbortController.signal });
|
|
90
|
+
if (globalThis.navigator.onLine) {
|
|
91
|
+
restartPingTimer();
|
|
92
|
+
}
|
|
93
|
+
{
|
|
94
|
+
globalThis.window.addEventListener(
|
|
95
|
+
"offline",
|
|
96
|
+
function handleOffline() {
|
|
97
|
+
clearInterval(intervalId);
|
|
98
|
+
},
|
|
99
|
+
{ signal: pingerAbortController.signal }
|
|
100
|
+
);
|
|
101
|
+
globalThis.window.addEventListener(
|
|
102
|
+
"online",
|
|
103
|
+
function handleOnline() {
|
|
104
|
+
sendPing();
|
|
105
|
+
restartPingTimer();
|
|
106
|
+
},
|
|
107
|
+
{ signal: pingerAbortController.signal }
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
...channel,
|
|
112
|
+
send(...args) {
|
|
113
|
+
if (!pingerAbortController.signal.aborted) {
|
|
114
|
+
restartPingTimer();
|
|
126
115
|
}
|
|
127
|
-
|
|
128
|
-
cleanup();
|
|
129
|
-
throw e;
|
|
116
|
+
return channel.send(...args);
|
|
130
117
|
}
|
|
131
118
|
};
|
|
132
119
|
}
|
|
133
120
|
|
|
134
|
-
// src/rpc-subscriptions-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
121
|
+
// src/rpc-subscriptions-channel-pool-internal.ts
|
|
122
|
+
function createChannelPool() {
|
|
123
|
+
return {
|
|
124
|
+
entries: [],
|
|
125
|
+
freeChannelIndex: -1
|
|
126
|
+
};
|
|
140
127
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
128
|
+
|
|
129
|
+
// src/rpc-subscriptions-channel-pool.ts
|
|
130
|
+
function getChannelPoolingChannelCreator(createChannel, { maxSubscriptionsPerChannel, minChannels }) {
|
|
131
|
+
const pool = createChannelPool();
|
|
132
|
+
function recomputeFreeChannelIndex() {
|
|
133
|
+
if (pool.entries.length < minChannels) {
|
|
134
|
+
pool.freeChannelIndex = -1;
|
|
135
|
+
return;
|
|
148
136
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
get(target, p, receiver) {
|
|
164
|
-
const subscriptionMethod = Reflect.get(target, p, receiver);
|
|
165
|
-
if (typeof subscriptionMethod !== "function") {
|
|
166
|
-
return subscriptionMethod;
|
|
167
|
-
}
|
|
168
|
-
return function(...rawParams) {
|
|
169
|
-
const deduplicationKey = getDeduplicationKey(p, rawParams);
|
|
170
|
-
if (deduplicationKey === void 0) {
|
|
171
|
-
return subscriptionMethod(...rawParams);
|
|
172
|
-
}
|
|
173
|
-
if (cache.has(deduplicationKey)) {
|
|
174
|
-
return cache.get(deduplicationKey);
|
|
175
|
-
}
|
|
176
|
-
const iterableFactory = getCachedAbortableIterableFactory({
|
|
177
|
-
getAbortSignalFromInputArgs: ({ abortSignal }) => abortSignal,
|
|
178
|
-
getCacheKeyFromInputArgs: () => deduplicationKey,
|
|
179
|
-
async onCacheHit(_iterable, _config) {
|
|
180
|
-
},
|
|
181
|
-
async onCreateIterable(abortSignal, config) {
|
|
182
|
-
const pendingSubscription2 = subscriptionMethod(
|
|
183
|
-
...rawParams
|
|
184
|
-
);
|
|
185
|
-
const iterable = await pendingSubscription2.subscribe({
|
|
186
|
-
...config,
|
|
187
|
-
abortSignal
|
|
188
|
-
});
|
|
189
|
-
registerIterableCleanup2(iterable, () => {
|
|
190
|
-
cache.delete(deduplicationKey);
|
|
191
|
-
});
|
|
192
|
-
return iterable;
|
|
193
|
-
}
|
|
194
|
-
});
|
|
195
|
-
const pendingSubscription = {
|
|
196
|
-
async subscribe(...args) {
|
|
197
|
-
const iterable = await iterableFactory(...args);
|
|
198
|
-
const { abortSignal } = args[0];
|
|
199
|
-
let abortPromise;
|
|
200
|
-
return {
|
|
201
|
-
...iterable,
|
|
202
|
-
async *[Symbol.asyncIterator]() {
|
|
203
|
-
abortPromise ||= abortSignal.aborted ? Promise.reject(EXPLICIT_ABORT_TOKEN ||= createExplicitAbortToken()) : new Promise((_, reject) => {
|
|
204
|
-
abortSignal.addEventListener("abort", () => {
|
|
205
|
-
reject(EXPLICIT_ABORT_TOKEN ||= createExplicitAbortToken());
|
|
206
|
-
});
|
|
207
|
-
});
|
|
208
|
-
try {
|
|
209
|
-
const iterator = iterable[Symbol.asyncIterator]();
|
|
210
|
-
while (true) {
|
|
211
|
-
const iteratorResult = await Promise.race([iterator.next(), abortPromise]);
|
|
212
|
-
if (iteratorResult.done) {
|
|
213
|
-
return;
|
|
214
|
-
} else {
|
|
215
|
-
yield iteratorResult.value;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
} catch (e) {
|
|
219
|
-
if (e === (EXPLICIT_ABORT_TOKEN ||= createExplicitAbortToken())) {
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
cache.delete(deduplicationKey);
|
|
223
|
-
throw e;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
};
|
|
227
|
-
}
|
|
137
|
+
let mostFreeChannel;
|
|
138
|
+
for (let ii = 0; ii < pool.entries.length; ii++) {
|
|
139
|
+
const nextPoolIndex = (pool.freeChannelIndex + ii + 2) % pool.entries.length;
|
|
140
|
+
const nextPoolEntry = (
|
|
141
|
+
// Start from the item two positions after the current item. This way, the
|
|
142
|
+
// search will finish on the item after the current one. This ensures that, if
|
|
143
|
+
// any channels tie for having the most capacity, the one that will be chosen is
|
|
144
|
+
// the one immediately to the current one's right (wrapping around).
|
|
145
|
+
pool.entries[nextPoolIndex]
|
|
146
|
+
);
|
|
147
|
+
if (nextPoolEntry.subscriptionCount < maxSubscriptionsPerChannel && (!mostFreeChannel || mostFreeChannel.subscriptionCount >= nextPoolEntry.subscriptionCount)) {
|
|
148
|
+
mostFreeChannel = {
|
|
149
|
+
poolIndex: nextPoolIndex,
|
|
150
|
+
subscriptionCount: nextPoolEntry.subscriptionCount
|
|
228
151
|
};
|
|
229
|
-
|
|
230
|
-
return pendingSubscription;
|
|
231
|
-
};
|
|
152
|
+
}
|
|
232
153
|
}
|
|
233
|
-
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
intervalMs,
|
|
243
|
-
transport
|
|
244
|
-
}) {
|
|
245
|
-
const pingableConnections = /* @__PURE__ */ new Map();
|
|
246
|
-
return async (...args) => {
|
|
247
|
-
const connection = await transport(...args);
|
|
248
|
-
let intervalId;
|
|
249
|
-
function sendPing() {
|
|
250
|
-
connection.send_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(PING_PAYLOAD);
|
|
154
|
+
pool.freeChannelIndex = mostFreeChannel?.poolIndex ?? -1;
|
|
155
|
+
}
|
|
156
|
+
return function getExistingChannelWithMostCapacityOrCreateChannel({ abortSignal }) {
|
|
157
|
+
let poolEntry;
|
|
158
|
+
function destroyPoolEntry() {
|
|
159
|
+
const index = pool.entries.findIndex((entry) => entry === poolEntry);
|
|
160
|
+
pool.entries.splice(index, 1);
|
|
161
|
+
poolEntry.dispose();
|
|
162
|
+
recomputeFreeChannelIndex();
|
|
251
163
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
164
|
+
if (pool.freeChannelIndex === -1) {
|
|
165
|
+
const abortController = new AbortController();
|
|
166
|
+
const newChannelPromise = createChannel({ abortSignal: abortController.signal });
|
|
167
|
+
newChannelPromise.then((newChannel) => {
|
|
168
|
+
newChannel.on("error", destroyPoolEntry, { signal: abortController.signal });
|
|
169
|
+
}).catch(destroyPoolEntry);
|
|
170
|
+
poolEntry = {
|
|
171
|
+
channel: newChannelPromise,
|
|
172
|
+
dispose() {
|
|
173
|
+
abortController.abort();
|
|
174
|
+
},
|
|
175
|
+
subscriptionCount: 0
|
|
176
|
+
};
|
|
177
|
+
pool.entries.push(poolEntry);
|
|
178
|
+
} else {
|
|
179
|
+
poolEntry = pool.entries[pool.freeChannelIndex];
|
|
255
180
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
(async () => {
|
|
265
|
-
try {
|
|
266
|
-
for await (const _ of connection) {
|
|
267
|
-
restartPingTimer();
|
|
268
|
-
}
|
|
269
|
-
} catch {
|
|
270
|
-
} finally {
|
|
271
|
-
pingableConnections.delete(connection);
|
|
272
|
-
clearInterval(intervalId);
|
|
273
|
-
if (handleOffline) {
|
|
274
|
-
globalThis.window.removeEventListener("offline", handleOffline);
|
|
275
|
-
}
|
|
276
|
-
if (handleOnline) {
|
|
277
|
-
globalThis.window.removeEventListener("online", handleOnline);
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
})();
|
|
281
|
-
if (globalThis.navigator.onLine) {
|
|
282
|
-
restartPingTimer();
|
|
181
|
+
poolEntry.subscriptionCount++;
|
|
182
|
+
abortSignal.addEventListener("abort", function destroyConsumer() {
|
|
183
|
+
poolEntry.subscriptionCount--;
|
|
184
|
+
if (poolEntry.subscriptionCount === 0) {
|
|
185
|
+
destroyPoolEntry();
|
|
186
|
+
} else if (pool.freeChannelIndex !== -1) {
|
|
187
|
+
pool.freeChannelIndex--;
|
|
188
|
+
recomputeFreeChannelIndex();
|
|
283
189
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
handleOffline = () => {
|
|
288
|
-
clearInterval(intervalId);
|
|
289
|
-
};
|
|
290
|
-
handleOnline = () => {
|
|
291
|
-
sendPing();
|
|
292
|
-
restartPingTimer();
|
|
293
|
-
};
|
|
294
|
-
globalThis.window.addEventListener("offline", handleOffline);
|
|
295
|
-
globalThis.window.addEventListener("online", handleOnline);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
return pingableConnections.get(connection);
|
|
190
|
+
});
|
|
191
|
+
recomputeFreeChannelIndex();
|
|
192
|
+
return poolEntry.channel;
|
|
299
193
|
};
|
|
300
194
|
}
|
|
195
|
+
function getRpcSubscriptionsChannelWithJSONSerialization(channel) {
|
|
196
|
+
return functional.pipe(
|
|
197
|
+
channel,
|
|
198
|
+
(c) => rpcSubscriptionsSpec.transformChannelInboundMessages(c, JSON.parse),
|
|
199
|
+
(c) => rpcSubscriptionsSpec.transformChannelOutboundMessages(c, JSON.stringify)
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
function getRpcSubscriptionsChannelWithBigIntJSONSerialization(channel) {
|
|
203
|
+
return functional.pipe(
|
|
204
|
+
channel,
|
|
205
|
+
(c) => rpcSubscriptionsSpec.transformChannelInboundMessages(c, rpcSpecTypes.parseJsonWithBigInts),
|
|
206
|
+
(c) => rpcSubscriptionsSpec.transformChannelOutboundMessages(c, rpcSpecTypes.stringifyJsonWithBigints)
|
|
207
|
+
);
|
|
208
|
+
}
|
|
301
209
|
|
|
302
|
-
// src/rpc-subscriptions-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
210
|
+
// src/rpc-subscriptions-channel.ts
|
|
211
|
+
function createDefaultSolanaRpcSubscriptionsChannelCreator(config) {
|
|
212
|
+
return createDefaultRpcSubscriptionsChannelCreatorImpl({
|
|
213
|
+
...config,
|
|
214
|
+
jsonSerializer: getRpcSubscriptionsChannelWithBigIntJSONSerialization
|
|
215
|
+
});
|
|
306
216
|
}
|
|
307
|
-
function
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
217
|
+
function createDefaultRpcSubscriptionsChannelCreator(config) {
|
|
218
|
+
return createDefaultRpcSubscriptionsChannelCreatorImpl({
|
|
219
|
+
...config,
|
|
220
|
+
jsonSerializer: getRpcSubscriptionsChannelWithJSONSerialization
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
function createDefaultRpcSubscriptionsChannelCreatorImpl(config) {
|
|
224
|
+
if (/^wss?:/i.test(config.url) === false) {
|
|
225
|
+
const protocolMatch = config.url.match(/^([^:]+):/);
|
|
226
|
+
throw new DOMException(
|
|
227
|
+
protocolMatch ? `Failed to construct 'WebSocket': The URL's scheme must be either 'ws' or 'wss'. '${protocolMatch[1]}:' is not allowed.` : `Failed to construct 'WebSocket': The URL '${config.url}' is invalid.`
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
const { intervalMs, ...rest } = config;
|
|
231
|
+
const createDefaultRpcSubscriptionsChannel = ({ abortSignal }) => {
|
|
232
|
+
return rpcSubscriptionsChannelWebsocket.createWebSocketChannel({
|
|
233
|
+
...rest,
|
|
234
|
+
sendBufferHighWatermark: config.sendBufferHighWatermark ?? // Let 128KB of data into the WebSocket buffer before buffering it in the app.
|
|
235
|
+
131072,
|
|
317
236
|
signal: abortSignal
|
|
318
|
-
})
|
|
237
|
+
}).then(config.jsonSerializer).then(
|
|
238
|
+
(channel) => getRpcSubscriptionsChannelWithAutoping({
|
|
239
|
+
abortSignal,
|
|
240
|
+
channel,
|
|
241
|
+
intervalMs: intervalMs ?? 5e3
|
|
242
|
+
})
|
|
243
|
+
);
|
|
244
|
+
};
|
|
245
|
+
return getChannelPoolingChannelCreator(createDefaultRpcSubscriptionsChannel, {
|
|
246
|
+
maxSubscriptionsPerChannel: config.maxSubscriptionsPerChannel ?? /**
|
|
247
|
+
* A note about this default. The idea here is that, because some RPC providers impose
|
|
248
|
+
* an upper limit on the number of subscriptions you can make per channel, we must
|
|
249
|
+
* choose a number low enough to avoid hitting that limit. Without knowing what provider
|
|
250
|
+
* a given person is using, or what their limit is, we have to choose the lowest of all
|
|
251
|
+
* known limits. As of this writing (October 2024) that is the public mainnet RPC node
|
|
252
|
+
* (api.mainnet-beta.solana.com) at 100 subscriptions.
|
|
253
|
+
*/
|
|
254
|
+
100,
|
|
255
|
+
minChannels: config.minChannels ?? 1
|
|
319
256
|
});
|
|
320
257
|
}
|
|
258
|
+
function getRpcSubscriptionsTransportWithSubscriptionCoalescing(transport) {
|
|
259
|
+
const cache = /* @__PURE__ */ new Map();
|
|
260
|
+
return function rpcSubscriptionsTransportWithSubscriptionCoalescing(config) {
|
|
261
|
+
const { request, signal } = config;
|
|
262
|
+
const subscriptionConfigurationHash = fastStableStringify__default.default([request.methodName, request.params]);
|
|
263
|
+
let cachedDataPublisherPromise = cache.get(subscriptionConfigurationHash);
|
|
264
|
+
if (!cachedDataPublisherPromise) {
|
|
265
|
+
const abortController = new AbortController();
|
|
266
|
+
const dataPublisherPromise = transport({
|
|
267
|
+
...config,
|
|
268
|
+
signal: abortController.signal
|
|
269
|
+
});
|
|
270
|
+
dataPublisherPromise.then((dataPublisher) => {
|
|
271
|
+
dataPublisher.on(
|
|
272
|
+
"error",
|
|
273
|
+
() => {
|
|
274
|
+
cache.delete(subscriptionConfigurationHash);
|
|
275
|
+
abortController.abort();
|
|
276
|
+
},
|
|
277
|
+
{ signal: abortController.signal }
|
|
278
|
+
);
|
|
279
|
+
}).catch(() => {
|
|
280
|
+
});
|
|
281
|
+
cache.set(
|
|
282
|
+
subscriptionConfigurationHash,
|
|
283
|
+
cachedDataPublisherPromise = {
|
|
284
|
+
abortController,
|
|
285
|
+
dataPublisherPromise,
|
|
286
|
+
numSubscribers: 0
|
|
287
|
+
}
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
cachedDataPublisherPromise.numSubscribers++;
|
|
291
|
+
signal.addEventListener(
|
|
292
|
+
"abort",
|
|
293
|
+
() => {
|
|
294
|
+
cachedDataPublisherPromise.numSubscribers--;
|
|
295
|
+
if (cachedDataPublisherPromise.numSubscribers === 0) {
|
|
296
|
+
queueMicrotask(() => {
|
|
297
|
+
if (cachedDataPublisherPromise.numSubscribers === 0) {
|
|
298
|
+
cache.delete(subscriptionConfigurationHash);
|
|
299
|
+
cachedDataPublisherPromise.abortController.abort();
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
{ signal: cachedDataPublisherPromise.abortController.signal }
|
|
305
|
+
);
|
|
306
|
+
return cachedDataPublisherPromise.dataPublisherPromise;
|
|
307
|
+
};
|
|
308
|
+
}
|
|
321
309
|
|
|
322
310
|
// src/rpc-subscriptions-transport.ts
|
|
323
|
-
function createDefaultRpcSubscriptionsTransport(
|
|
324
|
-
|
|
311
|
+
function createDefaultRpcSubscriptionsTransport({
|
|
312
|
+
createChannel
|
|
313
|
+
}) {
|
|
325
314
|
return functional.pipe(
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
}),
|
|
331
|
-
(transport) => getWebSocketTransportWithAutoping({
|
|
332
|
-
intervalMs: intervalMs ?? 5e3,
|
|
333
|
-
transport
|
|
334
|
-
}),
|
|
335
|
-
(transport) => getWebSocketTransportWithConnectionSharding({
|
|
336
|
-
getShard,
|
|
337
|
-
transport
|
|
338
|
-
})
|
|
315
|
+
createRpcSubscriptionsTransportFromChannelCreator(
|
|
316
|
+
createChannel
|
|
317
|
+
),
|
|
318
|
+
(transport) => getRpcSubscriptionsTransportWithSubscriptionCoalescing(transport)
|
|
339
319
|
);
|
|
340
320
|
}
|
|
321
|
+
function createRpcSubscriptionsTransportFromChannelCreator(createChannel) {
|
|
322
|
+
return async ({ execute, signal }) => {
|
|
323
|
+
const channel = await createChannel({ abortSignal: signal });
|
|
324
|
+
return await execute({ channel, signal });
|
|
325
|
+
};
|
|
326
|
+
}
|
|
341
327
|
|
|
342
328
|
// src/rpc-subscriptions.ts
|
|
343
|
-
function
|
|
344
|
-
const transport = createDefaultRpcSubscriptionsTransport({
|
|
329
|
+
function createSolanaRpcSubscriptionsImpl(clusterUrl, config) {
|
|
330
|
+
const transport = createDefaultRpcSubscriptionsTransport({
|
|
331
|
+
createChannel: createDefaultSolanaRpcSubscriptionsChannelCreator({ ...config, url: clusterUrl })
|
|
332
|
+
});
|
|
345
333
|
return createSolanaRpcSubscriptionsFromTransport(transport);
|
|
346
334
|
}
|
|
335
|
+
function createSolanaRpcSubscriptions(clusterUrl, config) {
|
|
336
|
+
return createSolanaRpcSubscriptionsImpl(clusterUrl, config);
|
|
337
|
+
}
|
|
347
338
|
function createSolanaRpcSubscriptions_UNSTABLE(clusterUrl, config) {
|
|
348
|
-
return
|
|
339
|
+
return createSolanaRpcSubscriptionsImpl(
|
|
349
340
|
clusterUrl,
|
|
350
341
|
config
|
|
351
342
|
);
|
|
352
343
|
}
|
|
353
344
|
function createSolanaRpcSubscriptionsFromTransport(transport) {
|
|
354
|
-
return
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
}),
|
|
359
|
-
(rpcSubscriptions) => getRpcSubscriptionsWithSubscriptionCoalescing({
|
|
360
|
-
getDeduplicationKey: (...args) => fastStableStringify__default.default(args),
|
|
361
|
-
rpcSubscriptions
|
|
362
|
-
})
|
|
363
|
-
);
|
|
364
|
-
}
|
|
365
|
-
function createSolanaRpcSubscriptionsFromTransport_UNSTABLE(transport) {
|
|
366
|
-
return createSolanaRpcSubscriptionsFromTransport(transport);
|
|
345
|
+
return rpcSubscriptionsSpec.createSubscriptionRpc({
|
|
346
|
+
api: rpcSubscriptionsApi.createSolanaRpcSubscriptionsApi(DEFAULT_RPC_SUBSCRIPTIONS_CONFIG),
|
|
347
|
+
transport
|
|
348
|
+
});
|
|
367
349
|
}
|
|
368
350
|
|
|
369
351
|
exports.DEFAULT_RPC_SUBSCRIPTIONS_CONFIG = DEFAULT_RPC_SUBSCRIPTIONS_CONFIG;
|
|
352
|
+
exports.createDefaultRpcSubscriptionsChannelCreator = createDefaultRpcSubscriptionsChannelCreator;
|
|
370
353
|
exports.createDefaultRpcSubscriptionsTransport = createDefaultRpcSubscriptionsTransport;
|
|
354
|
+
exports.createDefaultSolanaRpcSubscriptionsChannelCreator = createDefaultSolanaRpcSubscriptionsChannelCreator;
|
|
355
|
+
exports.createRpcSubscriptionsTransportFromChannelCreator = createRpcSubscriptionsTransportFromChannelCreator;
|
|
371
356
|
exports.createSolanaRpcSubscriptions = createSolanaRpcSubscriptions;
|
|
372
357
|
exports.createSolanaRpcSubscriptionsFromTransport = createSolanaRpcSubscriptionsFromTransport;
|
|
373
|
-
exports.createSolanaRpcSubscriptionsFromTransport_UNSTABLE = createSolanaRpcSubscriptionsFromTransport_UNSTABLE;
|
|
374
358
|
exports.createSolanaRpcSubscriptions_UNSTABLE = createSolanaRpcSubscriptions_UNSTABLE;
|
|
359
|
+
exports.getRpcSubscriptionsChannelWithJSONSerialization = getRpcSubscriptionsChannelWithJSONSerialization;
|
|
375
360
|
Object.keys(rpcSubscriptionsApi).forEach(function (k) {
|
|
376
361
|
if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
|
|
377
362
|
enumerable: true,
|