@nosana/kit
Version:
Nosana KIT
180 lines • 7.34 kB
JavaScript
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