UNPKG

@drift-labs/sdk-browser

Version:
204 lines (203 loc) 8.44 kB
"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;