UNPKG

@nosana/kit

Version:

Nosana KIT

180 lines 7.34 kB
import { parseBase64RpcAccount } from '@solana/kit'; import { JobState } from '../JobsProgram.js'; import { MonitorEventType } from './types.js'; /** * Set up WebSocket subscription for program notifications */ async function setupSubscription(deps, programId, abortController) { try { // Set up the subscription using the correct API pattern const subscriptionIterable = await deps.solana.rpcSubscriptions .programNotifications(programId, { encoding: 'base64' }) .subscribe({ abortSignal: abortController.signal }); return subscriptionIterable; } catch (error) { throw new Error(`Failed to setup subscription: ${error}`); } } /** * Handle JobAccount updates */ async function handleJobAccount(encodedAccount, autoMerge, runs, monitorDeps) { const { client, transformJobAccount, mergeRunIntoJob, deps } = monitorDeps; const jobAccount = client.decodeJobAccount(encodedAccount); let job = transformJobAccount(jobAccount); // If auto-merge is enabled, check for run accounts if (autoMerge && job.state === JobState.QUEUED) { try { const runAccounts = await runs({ job: job.address }); if (runAccounts.length > 0) { job = mergeRunIntoJob(job, runAccounts[0]); } } catch (error) { deps.logger.error(`Error checking run account for job ${job.address}: ${error}`); } } return { type: MonitorEventType.JOB, data: job }; } /** * Handle MarketAccount updates */ function handleMarketAccount(encodedAccount, monitorDeps) { const { client, transformMarketAccount } = monitorDeps; const marketAccount = client.decodeMarketAccount(encodedAccount); const market = transformMarketAccount(marketAccount); return { type: MonitorEventType.MARKET, data: market }; } /** * Handle RunAccount updates */ async function handleRunAccount(encodedAccount, autoMerge, get, monitorDeps) { const { client, transformRunAccount, mergeRunIntoJob, deps } = monitorDeps; const runAccount = client.decodeRunAccount(encodedAccount); const run = transformRunAccount(runAccount); if (autoMerge) { // For auto-merge, fetch the job and merge run data, then yield as job event try { const job = await get(run.job, false); const mergedJob = mergeRunIntoJob(job, run); return { type: MonitorEventType.JOB, data: mergedJob }; } catch (error) { deps.logger.error(`Error fetching job ${run.job} for run account ${runAccount.address}: ${error}`); // Skip this event if we can't fetch the job return null; } } else { // For detailed monitoring, yield run event as-is return { type: MonitorEventType.RUN, data: run }; } } /** * Create an async generator that yields monitor events from subscription notifications */ async function* createEventStream(subscriptionIterable, isMonitoring, autoMerge, get, runs, monitorDeps) { const { deps, client } = monitorDeps; try { for await (const notification of subscriptionIterable) { // Check if monitoring should continue if (!isMonitoring()) { deps.logger.info('Monitoring stopped, exiting subscription processing'); break; } try { const { value } = notification; const { account, pubkey } = value; const encodedAccount = parseBase64RpcAccount(pubkey, account); const accountType = client.identifyNosanaJobsAccount(encodedAccount); let event = null; switch (accountType) { case client.NosanaJobsAccount.JobAccount: event = await handleJobAccount(encodedAccount, autoMerge, runs, monitorDeps); break; case client.NosanaJobsAccount.MarketAccount: event = handleMarketAccount(encodedAccount, monitorDeps); break; case client.NosanaJobsAccount.RunAccount: event = await handleRunAccount(encodedAccount, autoMerge, get, monitorDeps); break; default: deps.logger.error(`No support yet for account type: ${accountType}`); break; } if (event) { yield event; } } catch (error) { deps.logger.error(`Error handling account update notification: ${error}`); // Continue processing other events } } } catch (error) { deps.logger.error(`Subscription error: ${error}`); // Throw the error so the calling function can restart the subscription throw error; } } async function createMonitorStream(get, runs, autoMerge, monitorDeps) { const { deps, config } = monitorDeps; const programId = config.jobsAddress; let abortController = null; let isMonitoring = true; // Function to stop all monitoring const stopMonitoring = () => { isMonitoring = false; if (abortController) { abortController.abort(); } deps.logger.info(`Stopped monitoring job program account updates`); }; // Create async generator that handles reconnection const eventStream = (async function* () { while (isMonitoring) { try { deps.logger.info('Attempting to establish WebSocket subscription...'); abortController = new AbortController(); const subscriptionIterable = await setupSubscription(deps, programId, abortController); deps.logger.info('Successfully established WebSocket subscription'); // Yield events from the subscription yield* createEventStream(subscriptionIterable, () => isMonitoring, autoMerge, get, runs, monitorDeps); } catch (error) { if (!isMonitoring) { // Monitoring was stopped, exit gracefully return; } deps.logger.warn(`WebSocket subscription failed: ${error}`); // Clean up current subscription if (abortController) { abortController.abort(); abortController = null; } if (isMonitoring) { deps.logger.info('Retrying WebSocket subscription in 5 seconds...'); await new Promise((resolve) => setTimeout(resolve, 5000)); } } } })(); deps.logger.info(`Successfully started monitoring job program account updates`); return [eventStream, stopMonitoring]; } /** * Factory function to create monitor functions */ export function createMonitorFunctions(get, runs, monitorDeps) { return { async monitor() { return createMonitorStream(get, runs, true, monitorDeps); }, async monitorDetailed() { return createMonitorStream(get, runs, false, monitorDeps); }, }; } //# sourceMappingURL=monitor.js.map