@drift-labs/sdk-browser
Version:
SDK for Drift Protocol
204 lines (203 loc) • 8.44 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.grpcMultiUserAccountSubscriber = void 0;
const types_1 = require("./types");
const events_1 = require("events");
const web3_js_1 = require("@solana/web3.js");
const grpcMultiAccountSubscriber_1 = require("./grpcMultiAccountSubscriber");
class grpcMultiUserAccountSubscriber {
constructor(program, grpcConfigs, resubOpts, multiSubscriber) {
this.userData = new Map();
this.listeners = new Map();
this.keyToPk = new Map();
this.pendingAddKeys = new Set();
this.debounceMs = 20;
this.isMultiSubscribed = false;
this.userAccountSubscribers = new Map();
this.handleAccountChange = (accountId, data, context, _buffer, _accountProps) => {
const k = accountId.toBase58();
this.userData.set(k, { data, slot: context.slot });
const setForKey = this.listeners.get(k);
if (setForKey) {
for (const emitter of setForKey) {
emitter.emit('userAccountUpdate', data);
emitter.emit('update');
}
}
};
this.program = program;
this.multiSubscriber = multiSubscriber;
this.grpcConfigs = grpcConfigs;
this.resubOpts = resubOpts;
}
async subscribe() {
if (!this.multiSubscriber) {
this.multiSubscriber =
await grpcMultiAccountSubscriber_1.grpcMultiAccountSubscriber.create(this.grpcConfigs, 'user', this.program, undefined, this.resubOpts);
}
// Subscribe all per-user subscribers first
await Promise.all(Array.from(this.userAccountSubscribers.values()).map((subscriber) => subscriber.subscribe()));
// Ensure we immediately register any pending keys and kick off underlying subscription/fetch
await this.flushPending();
// Proactively fetch once to populate data for all subscribed accounts
await this.multiSubscriber.fetch();
// Wait until the underlying multi-subscriber has data for every registered user key
const targetKeys = Array.from(this.listeners.keys());
if (targetKeys.length === 0)
return;
// Poll until all keys are present in dataMap
// Use debounceMs as the polling cadence to avoid introducing new magic numbers
// eslint-disable-next-line no-constant-condition
while (true) {
const map = this.multiSubscriber.getAccountDataMap();
let allPresent = true;
for (const k of targetKeys) {
if (!map.has(k)) {
allPresent = false;
break;
}
}
if (allPresent)
break;
await new Promise((resolve) => setTimeout(resolve, this.debounceMs));
}
}
forUser(userAccountPublicKey) {
if (this.userAccountSubscribers.has(userAccountPublicKey.toBase58())) {
return this.userAccountSubscribers.get(userAccountPublicKey.toBase58());
}
const key = userAccountPublicKey.toBase58();
const perUserEmitter = new events_1.EventEmitter();
// eslint-disable-next-line @typescript-eslint/no-this-alias
const parent = this;
let isSubscribed = false;
const registerHandlerIfNeeded = async () => {
if (!this.listeners.has(key)) {
this.listeners.set(key, new Set());
this.keyToPk.set(key, userAccountPublicKey);
this.pendingAddKeys.add(key);
if (this.isMultiSubscribed) {
// only schedule flush if already subscribed to the multi-subscriber
this.scheduleFlush();
}
}
};
const perUser = {
get eventEmitter() {
return perUserEmitter;
},
set eventEmitter(_v) { },
get isSubscribed() {
return isSubscribed;
},
set isSubscribed(_v) {
isSubscribed = _v;
},
async subscribe(userAccount) {
if (isSubscribed)
return true;
if (userAccount) {
this.updateData(userAccount, 0);
}
await registerHandlerIfNeeded();
const setForKey = parent.listeners.get(key);
setForKey.add(perUserEmitter);
isSubscribed = true;
return true;
},
async fetch() {
if (!isSubscribed) {
throw new types_1.NotSubscribedError('Must subscribe before fetching account updates');
}
const account = (await parent.program.account.user.fetch(userAccountPublicKey));
this.updateData(account, 0);
},
updateData(userAccount, slot) {
const existingData = parent.userData.get(key);
if (existingData && existingData.slot > slot) {
return;
}
parent.userData.set(key, { data: userAccount, slot });
perUserEmitter.emit('userAccountUpdate', userAccount);
perUserEmitter.emit('update');
},
async unsubscribe() {
if (!isSubscribed)
return;
const setForKey = parent.listeners.get(key);
if (setForKey) {
setForKey.delete(perUserEmitter);
if (setForKey.size === 0) {
parent.listeners.delete(key);
await parent.multiSubscriber.removeAccounts([userAccountPublicKey]);
parent.userData.delete(key);
parent.keyToPk.delete(key);
parent.pendingAddKeys.delete(key);
}
}
isSubscribed = false;
},
getUserAccountAndSlot() {
const das = parent.userData.get(key);
if (!das) {
throw new types_1.NotSubscribedError('Must subscribe before getting user account data');
}
return das;
},
};
this.userAccountSubscribers.set(userAccountPublicKey.toBase58(), perUser);
return perUser;
}
scheduleFlush() {
if (this.debounceTimer)
return;
this.debounceTimer = setTimeout(() => {
void this.flushPending();
}, this.debounceMs);
}
async flushPending() {
const hasPending = this.pendingAddKeys.size > 0;
if (!hasPending) {
this.debounceTimer = undefined;
return;
}
const allPks = [];
for (const k of this.listeners.keys()) {
const pk = this.keyToPk.get(k);
if (pk)
allPks.push(pk);
}
if (allPks.length === 0) {
this.pendingAddKeys.clear();
this.debounceTimer = undefined;
return;
}
if (!this.isMultiSubscribed) {
await this.multiSubscriber.subscribe(allPks, this.handleAccountChange);
this.isMultiSubscribed = true;
await this.multiSubscriber.fetch();
for (const k of this.pendingAddKeys) {
const pk = this.keyToPk.get(k);
if (pk) {
const data = this.multiSubscriber.getAccountData(k);
if (data) {
this.handleAccountChange(pk, data.data, { slot: data.slot }, undefined, undefined);
}
}
}
}
else {
const ms = this.multiSubscriber;
for (const k of this.pendingAddKeys) {
ms.onChangeMap.set(k, (data, ctx, buffer, accountProps) => {
this.multiSubscriber.setAccountData(k, data, ctx.slot);
this.handleAccountChange(new web3_js_1.PublicKey(k), data, ctx, buffer, accountProps);
});
}
await this.multiSubscriber.addAccounts(allPks);
}
this.pendingAddKeys.clear();
this.debounceTimer = undefined;
}
}
exports.grpcMultiUserAccountSubscriber = grpcMultiUserAccountSubscriber;