@drift-labs/sdk
Version:
SDK for Drift Protocol
223 lines (222 loc) • 8.35 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.BulkAccountLoader = void 0;
const uuid_1 = require("uuid");
const promiseTimeout_1 = require("../util/promiseTimeout");
const numericConstants_1 = require("../constants/numericConstants");
const oneMinute = 60 * 1000;
class BulkAccountLoader {
constructor(connection, commitment, pollingFrequency) {
this.accountsToLoad = new Map();
this.bufferAndSlotMap = new Map();
this.errorCallbacks = new Map();
this.lastTimeLoadingPromiseCleared = Date.now();
this.mostRecentSlot = 0;
this.connection = connection;
this.commitment = commitment;
this.pollingFrequency = pollingFrequency;
}
async addAccount(publicKey, callback) {
if (!publicKey) {
console.trace(`Caught adding blank publickey to bulkAccountLoader`);
}
const existingSize = this.accountsToLoad.size;
const callbackId = (0, uuid_1.v4)();
const existingAccountToLoad = this.accountsToLoad.get(publicKey.toString());
if (existingAccountToLoad) {
existingAccountToLoad.callbacks.set(callbackId, callback);
}
else {
const callbacks = new Map();
callbacks.set(callbackId, callback);
const newAccountToLoad = {
publicKey,
callbacks,
};
this.accountsToLoad.set(publicKey.toString(), newAccountToLoad);
}
if (existingSize === 0) {
this.startPolling();
}
// resolve the current loadPromise in case client wants to call load
await this.loadPromise;
return callbackId;
}
removeAccount(publicKey, callbackId) {
const existingAccountToLoad = this.accountsToLoad.get(publicKey.toString());
if (existingAccountToLoad) {
existingAccountToLoad.callbacks.delete(callbackId);
if (existingAccountToLoad.callbacks.size === 0) {
this.bufferAndSlotMap.delete(publicKey.toString());
this.accountsToLoad.delete(existingAccountToLoad.publicKey.toString());
}
}
if (this.accountsToLoad.size === 0) {
this.stopPolling();
}
}
addErrorCallbacks(callback) {
const callbackId = (0, uuid_1.v4)();
this.errorCallbacks.set(callbackId, callback);
return callbackId;
}
removeErrorCallbacks(callbackId) {
this.errorCallbacks.delete(callbackId);
}
chunks(array, size) {
return new Array(Math.ceil(array.length / size))
.fill(null)
.map((_, index) => index * size)
.map((begin) => array.slice(begin, begin + size));
}
async load() {
if (this.loadPromise) {
const now = Date.now();
if (now - this.lastTimeLoadingPromiseCleared > oneMinute) {
this.loadPromise = undefined;
}
else {
return this.loadPromise;
}
}
this.loadPromise = new Promise((resolver) => {
this.loadPromiseResolver = resolver;
});
this.lastTimeLoadingPromiseCleared = Date.now();
try {
const chunks = this.chunks(this.chunks(Array.from(this.accountsToLoad.values()), numericConstants_1.GET_MULTIPLE_ACCOUNTS_CHUNK_SIZE), 10);
await Promise.all(chunks.map((chunk) => {
return this.loadChunk(chunk);
}));
}
catch (e) {
console.error(`Error in bulkAccountLoader.load()`);
console.error(e);
for (const [_, callback] of this.errorCallbacks) {
callback(e);
}
}
finally {
this.loadPromiseResolver();
this.loadPromise = undefined;
}
}
async loadChunk(accountsToLoadChunks) {
if (accountsToLoadChunks.length === 0) {
return;
}
const requests = new Array();
for (const accountsToLoadChunk of accountsToLoadChunks) {
const args = [
accountsToLoadChunk
.filter((accountToLoad) => accountToLoad.callbacks.size > 0)
.map((accountToLoad) => {
return accountToLoad.publicKey.toBase58();
}),
{ commitment: this.commitment },
];
requests.push({
methodName: 'getMultipleAccounts',
args,
});
}
const rpcResponses = await (0, promiseTimeout_1.promiseTimeout)(
// @ts-ignore
this.connection._rpcBatchRequest(requests), 10 * 1000 // 30 second timeout
);
if (rpcResponses === null) {
this.log('request to rpc timed out');
return;
}
rpcResponses.forEach((rpcResponse, i) => {
if (!rpcResponse.result) {
console.error('rpc response missing result:');
console.log(JSON.stringify(rpcResponse));
return;
}
const newSlot = rpcResponse.result.context.slot;
if (newSlot > this.mostRecentSlot) {
this.mostRecentSlot = newSlot;
}
const accountsToLoad = accountsToLoadChunks[i];
accountsToLoad.forEach((accountToLoad, j) => {
if (accountToLoad.callbacks.size === 0) {
return;
}
const key = accountToLoad.publicKey.toBase58();
const oldRPCResponse = this.bufferAndSlotMap.get(key);
if (oldRPCResponse && newSlot < oldRPCResponse.slot) {
return;
}
let newBuffer = undefined;
if (rpcResponse.result.value[j]) {
const raw = rpcResponse.result.value[j].data[0];
const dataType = rpcResponse.result.value[j].data[1];
newBuffer = Buffer.from(raw, dataType);
}
if (!oldRPCResponse) {
this.bufferAndSlotMap.set(key, {
slot: newSlot,
buffer: newBuffer,
});
this.handleAccountCallbacks(accountToLoad, newBuffer, newSlot);
return;
}
const oldBuffer = oldRPCResponse.buffer;
if (newBuffer && (!oldBuffer || !newBuffer.equals(oldBuffer))) {
this.bufferAndSlotMap.set(key, {
slot: newSlot,
buffer: newBuffer,
});
this.handleAccountCallbacks(accountToLoad, newBuffer, newSlot);
}
});
});
}
handleAccountCallbacks(accountToLoad, buffer, slot) {
for (const [_, callback] of accountToLoad.callbacks) {
try {
callback(buffer, slot);
}
catch (e) {
console.log('Bulk account load: error in account callback');
console.log('accounto to load', accountToLoad.publicKey.toString());
console.log('buffer', buffer.toString('base64'));
for (const callback of accountToLoad.callbacks.values()) {
console.log('account to load cb', callback);
}
throw e;
}
}
}
getBufferAndSlot(publicKey) {
return this.bufferAndSlotMap.get(publicKey.toString());
}
getSlot() {
return this.mostRecentSlot;
}
startPolling() {
if (this.intervalId) {
return;
}
if (this.pollingFrequency !== 0)
this.intervalId = setInterval(this.load.bind(this), this.pollingFrequency);
}
stopPolling() {
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = undefined;
}
}
log(msg) {
console.log(msg);
}
updatePollingFrequency(pollingFrequency) {
this.stopPolling();
this.pollingFrequency = pollingFrequency;
if (this.accountsToLoad.size > 0) {
this.startPolling();
}
}
}
exports.BulkAccountLoader = BulkAccountLoader;