@solana/rpc-subscriptions
Version:
A library for subscribing to Solana RPC notifications
378 lines (365 loc) • 14 kB
JavaScript
var rpcSubscriptionsApi = require('@solana/rpc-subscriptions-api');
var rpcSubscriptionsSpec = require('@solana/rpc-subscriptions-spec');
var errors = require('@solana/errors');
var rpcSubscriptionsChannelWebsocket = require('@solana/rpc-subscriptions-channel-websocket');
var functional = require('@solana/functional');
var rpcSpecTypes = require('@solana/rpc-spec-types');
var fastStableStringify = require('@solana/fast-stable-stringify');
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
var fastStableStringify__default = /*#__PURE__*/_interopDefault(fastStableStringify);
// src/index.ts
function createSolanaJsonRpcIntegerOverflowError(methodName, keyPath, value) {
let argumentLabel = "";
if (typeof keyPath[0] === "number") {
const argPosition = keyPath[0] + 1;
const lastDigit = argPosition % 10;
const lastTwoDigits = argPosition % 100;
if (lastDigit == 1 && lastTwoDigits != 11) {
argumentLabel = argPosition + "st";
} else if (lastDigit == 2 && lastTwoDigits != 12) {
argumentLabel = argPosition + "nd";
} else if (lastDigit == 3 && lastTwoDigits != 13) {
argumentLabel = argPosition + "rd";
} else {
argumentLabel = argPosition + "th";
}
} else {
argumentLabel = `\`${keyPath[0].toString()}\``;
}
const path = keyPath.length > 1 ? keyPath.slice(1).map((pathPart) => typeof pathPart === "number" ? `[${pathPart}]` : pathPart).join(".") : void 0;
const error = new errors.SolanaError(errors.SOLANA_ERROR__RPC__INTEGER_OVERFLOW, {
argumentLabel,
keyPath,
methodName,
optionalPathLabel: path ? ` at path \`${path}\`` : "",
value,
...path !== void 0 ? { path } : void 0
});
errors.safeCaptureStackTrace(error, createSolanaJsonRpcIntegerOverflowError);
return error;
}
// src/rpc-default-config.ts
var DEFAULT_RPC_SUBSCRIPTIONS_CONFIG = {
defaultCommitment: "confirmed",
onIntegerOverflow(request, keyPath, value) {
throw createSolanaJsonRpcIntegerOverflowError(request.methodName, keyPath, value);
}
};
// ../event-target-impl/dist/index.browser.mjs
var o = globalThis.AbortController;
// src/rpc-subscriptions-autopinger.ts
var PING_PAYLOAD = {
jsonrpc: "2.0",
method: "ping"
};
function getRpcSubscriptionsChannelWithAutoping({
abortSignal: callerAbortSignal,
channel,
intervalMs
}) {
let intervalId;
function sendPing() {
channel.send(PING_PAYLOAD).catch((e) => {
if (errors.isSolanaError(e, errors.SOLANA_ERROR__RPC_SUBSCRIPTIONS__CHANNEL_CONNECTION_CLOSED)) {
pingerAbortController.abort();
}
});
}
function restartPingTimer() {
clearInterval(intervalId);
intervalId = setInterval(sendPing, intervalMs);
}
const pingerAbortController = new o();
pingerAbortController.signal.addEventListener("abort", () => {
clearInterval(intervalId);
});
callerAbortSignal.addEventListener("abort", () => {
pingerAbortController.abort();
});
channel.on(
"error",
() => {
pingerAbortController.abort();
},
{ signal: pingerAbortController.signal }
);
channel.on("message", restartPingTimer, { signal: pingerAbortController.signal });
if (globalThis.navigator.onLine) {
restartPingTimer();
}
{
globalThis.addEventListener(
"offline",
function handleOffline() {
clearInterval(intervalId);
},
{ signal: pingerAbortController.signal }
);
globalThis.addEventListener(
"online",
function handleOnline() {
sendPing();
restartPingTimer();
},
{ signal: pingerAbortController.signal }
);
}
return {
...channel,
send(...args) {
if (!pingerAbortController.signal.aborted) {
restartPingTimer();
}
return channel.send(...args);
}
};
}
// src/rpc-subscriptions-channel-pool-internal.ts
function createChannelPool() {
return {
entries: [],
freeChannelIndex: -1
};
}
// src/rpc-subscriptions-channel-pool.ts
function getChannelPoolingChannelCreator(createChannel, { maxSubscriptionsPerChannel, minChannels }) {
const pool = createChannelPool();
function recomputeFreeChannelIndex() {
if (pool.entries.length < minChannels) {
pool.freeChannelIndex = -1;
return;
}
let mostFreeChannel;
for (let ii = 0; ii < pool.entries.length; ii++) {
const nextPoolIndex = (pool.freeChannelIndex + ii + 2) % pool.entries.length;
const nextPoolEntry = (
// Start from the item two positions after the current item. This way, the
// search will finish on the item after the current one. This ensures that, if
// any channels tie for having the most capacity, the one that will be chosen is
// the one immediately to the current one's right (wrapping around).
pool.entries[nextPoolIndex]
);
if (nextPoolEntry.subscriptionCount < maxSubscriptionsPerChannel && (!mostFreeChannel || mostFreeChannel.subscriptionCount >= nextPoolEntry.subscriptionCount)) {
mostFreeChannel = {
poolIndex: nextPoolIndex,
subscriptionCount: nextPoolEntry.subscriptionCount
};
}
}
pool.freeChannelIndex = mostFreeChannel?.poolIndex ?? -1;
}
return function getExistingChannelWithMostCapacityOrCreateChannel({ abortSignal }) {
let poolEntry;
function destroyPoolEntry() {
const index = pool.entries.findIndex((entry) => entry === poolEntry);
pool.entries.splice(index, 1);
poolEntry.dispose();
recomputeFreeChannelIndex();
}
if (pool.freeChannelIndex === -1) {
const abortController = new o();
const newChannelPromise = createChannel({ abortSignal: abortController.signal });
newChannelPromise.then((newChannel) => {
newChannel.on("error", destroyPoolEntry, { signal: abortController.signal });
}).catch(destroyPoolEntry);
poolEntry = {
channel: newChannelPromise,
dispose() {
abortController.abort();
},
subscriptionCount: 0
};
pool.entries.push(poolEntry);
} else {
poolEntry = pool.entries[pool.freeChannelIndex];
}
poolEntry.subscriptionCount++;
abortSignal.addEventListener("abort", function destroyConsumer() {
poolEntry.subscriptionCount--;
if (poolEntry.subscriptionCount === 0) {
destroyPoolEntry();
} else if (pool.freeChannelIndex !== -1) {
pool.freeChannelIndex--;
recomputeFreeChannelIndex();
}
});
recomputeFreeChannelIndex();
return poolEntry.channel;
};
}
function getRpcSubscriptionsChannelWithJSONSerialization(channel) {
return functional.pipe(
channel,
(c) => rpcSubscriptionsSpec.transformChannelInboundMessages(c, JSON.parse),
(c) => rpcSubscriptionsSpec.transformChannelOutboundMessages(c, JSON.stringify)
);
}
function getRpcSubscriptionsChannelWithBigIntJSONSerialization(channel) {
return functional.pipe(
channel,
(c) => rpcSubscriptionsSpec.transformChannelInboundMessages(c, rpcSpecTypes.parseJsonWithBigInts),
(c) => rpcSubscriptionsSpec.transformChannelOutboundMessages(c, rpcSpecTypes.stringifyJsonWithBigints)
);
}
// src/rpc-subscriptions-channel.ts
function createDefaultSolanaRpcSubscriptionsChannelCreator(config) {
return createDefaultRpcSubscriptionsChannelCreatorImpl({
...config,
jsonSerializer: getRpcSubscriptionsChannelWithBigIntJSONSerialization
});
}
function createDefaultRpcSubscriptionsChannelCreator(config) {
return createDefaultRpcSubscriptionsChannelCreatorImpl({
...config,
jsonSerializer: getRpcSubscriptionsChannelWithJSONSerialization
});
}
function createDefaultRpcSubscriptionsChannelCreatorImpl(config) {
if (/^wss?:/i.test(config.url) === false) {
const protocolMatch = config.url.match(/^([^:]+):/);
throw new DOMException(
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.`
);
}
const { intervalMs, ...rest } = config;
const createDefaultRpcSubscriptionsChannel = ({ abortSignal }) => {
return rpcSubscriptionsChannelWebsocket.createWebSocketChannel({
...rest,
sendBufferHighWatermark: config.sendBufferHighWatermark ?? // Let 128KB of data into the WebSocket buffer before buffering it in the app.
131072,
signal: abortSignal
}).then(config.jsonSerializer).then(
(channel) => getRpcSubscriptionsChannelWithAutoping({
abortSignal,
channel,
intervalMs: intervalMs ?? 5e3
})
);
};
return getChannelPoolingChannelCreator(createDefaultRpcSubscriptionsChannel, {
maxSubscriptionsPerChannel: config.maxSubscriptionsPerChannel ?? /**
* A note about this default. The idea here is that, because some RPC providers impose
* an upper limit on the number of subscriptions you can make per channel, we must
* choose a number low enough to avoid hitting that limit. Without knowing what provider
* a given person is using, or what their limit is, we have to choose the lowest of all
* known limits. As of this writing (October 2024) that is the public mainnet RPC node
* (api.mainnet-beta.solana.com) at 100 subscriptions.
*/
100,
minChannels: config.minChannels ?? 1
});
}
function getRpcSubscriptionsTransportWithSubscriptionCoalescing(transport) {
const cache = /* @__PURE__ */ new Map();
return function rpcSubscriptionsTransportWithSubscriptionCoalescing(config) {
const { request, signal } = config;
const subscriptionConfigurationHash = fastStableStringify__default.default([request.methodName, request.params]);
let cachedDataPublisherPromise = cache.get(subscriptionConfigurationHash);
if (!cachedDataPublisherPromise) {
const abortController = new o();
const dataPublisherPromise = transport({
...config,
signal: abortController.signal
});
dataPublisherPromise.then((dataPublisher) => {
dataPublisher.on(
"error",
() => {
cache.delete(subscriptionConfigurationHash);
abortController.abort();
},
{ signal: abortController.signal }
);
}).catch(() => {
});
cache.set(
subscriptionConfigurationHash,
cachedDataPublisherPromise = {
abortController,
dataPublisherPromise,
numSubscribers: 0
}
);
}
cachedDataPublisherPromise.numSubscribers++;
signal.addEventListener(
"abort",
() => {
cachedDataPublisherPromise.numSubscribers--;
if (cachedDataPublisherPromise.numSubscribers === 0) {
queueMicrotask(() => {
if (cachedDataPublisherPromise.numSubscribers === 0) {
cache.delete(subscriptionConfigurationHash);
cachedDataPublisherPromise.abortController.abort();
}
});
}
},
{ signal: cachedDataPublisherPromise.abortController.signal }
);
return cachedDataPublisherPromise.dataPublisherPromise;
};
}
// src/rpc-subscriptions-transport.ts
function createDefaultRpcSubscriptionsTransport({
createChannel
}) {
return functional.pipe(
createRpcSubscriptionsTransportFromChannelCreator(
createChannel
),
(transport) => getRpcSubscriptionsTransportWithSubscriptionCoalescing(transport)
);
}
function createRpcSubscriptionsTransportFromChannelCreator(createChannel) {
return async ({ execute, signal }) => {
const channel = await createChannel({ abortSignal: signal });
return await execute({ channel, signal });
};
}
// src/rpc-subscriptions.ts
function createSolanaRpcSubscriptionsImpl(clusterUrl, config) {
const transport = createDefaultRpcSubscriptionsTransport({
createChannel: createDefaultSolanaRpcSubscriptionsChannelCreator({ ...config, url: clusterUrl })
});
return createSolanaRpcSubscriptionsFromTransport(transport);
}
function createSolanaRpcSubscriptions(clusterUrl, config) {
return createSolanaRpcSubscriptionsImpl(clusterUrl, config);
}
function createSolanaRpcSubscriptions_UNSTABLE(clusterUrl, config) {
return createSolanaRpcSubscriptionsImpl(
clusterUrl,
config
);
}
function createSolanaRpcSubscriptionsFromTransport(transport) {
return rpcSubscriptionsSpec.createSubscriptionRpc({
api: rpcSubscriptionsApi.createSolanaRpcSubscriptionsApi(DEFAULT_RPC_SUBSCRIPTIONS_CONFIG),
transport
});
}
exports.DEFAULT_RPC_SUBSCRIPTIONS_CONFIG = DEFAULT_RPC_SUBSCRIPTIONS_CONFIG;
exports.createDefaultRpcSubscriptionsChannelCreator = createDefaultRpcSubscriptionsChannelCreator;
exports.createDefaultRpcSubscriptionsTransport = createDefaultRpcSubscriptionsTransport;
exports.createDefaultSolanaRpcSubscriptionsChannelCreator = createDefaultSolanaRpcSubscriptionsChannelCreator;
exports.createRpcSubscriptionsTransportFromChannelCreator = createRpcSubscriptionsTransportFromChannelCreator;
exports.createSolanaRpcSubscriptions = createSolanaRpcSubscriptions;
exports.createSolanaRpcSubscriptionsFromTransport = createSolanaRpcSubscriptionsFromTransport;
exports.createSolanaRpcSubscriptions_UNSTABLE = createSolanaRpcSubscriptions_UNSTABLE;
exports.getRpcSubscriptionsChannelWithJSONSerialization = getRpcSubscriptionsChannelWithJSONSerialization;
Object.keys(rpcSubscriptionsApi).forEach(function (k) {
if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
enumerable: true,
get: function () { return rpcSubscriptionsApi[k]; }
});
});
Object.keys(rpcSubscriptionsSpec).forEach(function (k) {
if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
enumerable: true,
get: function () { return rpcSubscriptionsSpec[k]; }
});
});
//# sourceMappingURL=index.browser.cjs.map
//# sourceMappingURL=index.browser.cjs.map
;