@nosana/node 1.1.16 → 1.1.19-rc
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/package.json +1 -1
- package/dist/src/NodeManager/errors/NodeNotQualifiedError.d.ts +1 -1
- package/dist/src/NodeManager/errors/NodeNotQualifiedError.js +11 -2
- package/dist/src/NodeManager/node/job/JobRegistry.d.ts +3 -0
- package/dist/src/NodeManager/node/job/JobRegistry.js +41 -12
- package/dist/src/NodeManager/node/job/jobExternalUtil.d.ts +1 -0
- package/dist/src/NodeManager/node/job/jobExternalUtil.js +15 -3
- package/dist/src/NodeManager/node/job/jobHandler.d.ts +2 -0
- package/dist/src/NodeManager/node/job/jobHandler.js +29 -4
- package/dist/src/NodeManager/node/market/benchmark.d.ts +0 -1
- package/dist/src/NodeManager/node/market/benchmark.js +2 -23
- package/dist/src/NodeManager/node/market/hostManager.d.ts +6 -3
- package/dist/src/NodeManager/node/market/hostManager.js +16 -0
- package/dist/src/NodeManager/node/market/marketHandler.d.ts +1 -0
- package/dist/src/NodeManager/node/market/marketHandler.js +84 -16
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/dist/package.json
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
export class NodeNotQualifiedError extends Error {
|
|
2
|
-
constructor() {
|
|
3
|
-
|
|
2
|
+
constructor(nextTestAt) {
|
|
3
|
+
const nextTestSuffix = formatNextTestSuffix(nextTestAt);
|
|
4
|
+
super(`Node does not meet the minimum requirements for any market.${nextTestSuffix} Shutting down.`);
|
|
4
5
|
this.name = 'NodeNotQualifiedError';
|
|
5
6
|
}
|
|
6
7
|
}
|
|
8
|
+
function formatNextTestSuffix(nextTestAt) {
|
|
9
|
+
if (!nextTestAt)
|
|
10
|
+
return '';
|
|
11
|
+
const next = new Date(nextTestAt);
|
|
12
|
+
if (Number.isNaN(next.getTime()) || next.getTime() <= Date.now())
|
|
13
|
+
return '';
|
|
14
|
+
return ` Next eligible retest at ${next.toISOString()}.`;
|
|
15
|
+
}
|
|
@@ -10,5 +10,8 @@ export declare class JobRegistry {
|
|
|
10
10
|
get(jobId: string): Job | undefined;
|
|
11
11
|
remove(jobId: string): void;
|
|
12
12
|
has(jobId: string): boolean;
|
|
13
|
+
private getErrorMessage;
|
|
14
|
+
private isStaleCleanupError;
|
|
15
|
+
private logCleanupError;
|
|
13
16
|
stop(sdk: SDK, repository: NodeRepository): Promise<void>;
|
|
14
17
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ipfsHashToSolBytesArray } from '@nosana/ipfs';
|
|
2
|
+
import chalk from 'chalk';
|
|
2
3
|
import { JobExternalUtil } from './jobExternalUtil.js';
|
|
3
4
|
import { IpfsClientSingleton } from '../../../ipfs/IpfsClient.js';
|
|
4
5
|
export class JobRegistry {
|
|
@@ -26,25 +27,53 @@ export class JobRegistry {
|
|
|
26
27
|
has(jobId) {
|
|
27
28
|
return this.registry.has(jobId);
|
|
28
29
|
}
|
|
30
|
+
getErrorMessage(error) {
|
|
31
|
+
if (error instanceof Error) {
|
|
32
|
+
return error.message;
|
|
33
|
+
}
|
|
34
|
+
return String(error);
|
|
35
|
+
}
|
|
36
|
+
isStaleCleanupError(error) {
|
|
37
|
+
const message = this.getErrorMessage(error);
|
|
38
|
+
return (message.includes('AccountNotInitialized') ||
|
|
39
|
+
message.includes('The program expected this account to be already initialized') ||
|
|
40
|
+
message.includes('Account does not exist or has no data'));
|
|
41
|
+
}
|
|
42
|
+
logCleanupError(jobId, error) {
|
|
43
|
+
if (this.isStaleCleanupError(error)) {
|
|
44
|
+
console.warn(chalk.yellow(`Skipping cleanup result submission for stale job ${jobId}: run is already closed on-chain.`));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
console.warn(chalk.yellow(`Cleanup could not submit result for job ${jobId}: ${this.getErrorMessage(error)}`));
|
|
48
|
+
}
|
|
29
49
|
async stop(sdk, repository) {
|
|
30
50
|
const stopPromises = [];
|
|
31
51
|
for (const [jobId, job] of this.registry.entries()) {
|
|
32
52
|
const promise = (async () => {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
53
|
+
try {
|
|
54
|
+
const jobExternalUtil = new JobExternalUtil(repository);
|
|
55
|
+
const result = await jobExternalUtil.resolveResult(jobId, job);
|
|
56
|
+
const ipfsResult = await IpfsClientSingleton.pin(result);
|
|
57
|
+
const bytesArray = ipfsHashToSolBytesArray(ipfsResult);
|
|
58
|
+
await sdk.jobs.submitResult(bytesArray, this.runs.get(jobId), job.market);
|
|
59
|
+
this.remove(jobId);
|
|
60
|
+
return { jobId, status: 'submitted' };
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (this.isStaleCleanupError(error)) {
|
|
64
|
+
this.remove(jobId);
|
|
65
|
+
return { jobId, status: 'stale', error };
|
|
66
|
+
}
|
|
67
|
+
return { jobId, status: 'retryable-error', error };
|
|
68
|
+
}
|
|
40
69
|
})();
|
|
41
70
|
stopPromises.push(promise);
|
|
42
71
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
72
|
+
const results = await Promise.all(stopPromises);
|
|
73
|
+
for (const result of results) {
|
|
74
|
+
if (result.status !== 'submitted') {
|
|
75
|
+
this.logCleanupError(result.jobId, result.error);
|
|
76
|
+
}
|
|
48
77
|
}
|
|
49
78
|
}
|
|
50
79
|
}
|
|
@@ -3,6 +3,7 @@ import { NodeRepository } from '../../repository/NodeRepository.js';
|
|
|
3
3
|
export declare class JobExternalUtil {
|
|
4
4
|
private repository;
|
|
5
5
|
constructor(repository: NodeRepository);
|
|
6
|
+
private retrieveJobDefinitionWithRetry;
|
|
6
7
|
resolveJobDefinition(id: string, job: Job): Promise<JobDefinition | null>;
|
|
7
8
|
resolveResult(id: string, job: Job): Promise<FlowState>;
|
|
8
9
|
validate(id: string, jobDefinition: JobDefinition): Promise<boolean>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { validateJobDefinition } from '@nosana/sdk';
|
|
1
|
+
import { validateJobDefinition, sleep } from '@nosana/sdk';
|
|
2
2
|
import { IpfsClientSingleton } from '../../../ipfs/IpfsClient.js';
|
|
3
3
|
import { JobDefinitionStrategySelector } from './defination/JobDefinitionStrategy.js';
|
|
4
4
|
import { ResultReturnStrategySelector } from './result/ResultReturnStrategy.js';
|
|
@@ -7,8 +7,20 @@ export class JobExternalUtil {
|
|
|
7
7
|
constructor(repository) {
|
|
8
8
|
this.repository = repository;
|
|
9
9
|
}
|
|
10
|
+
async retrieveJobDefinitionWithRetry(ipfsHash, attempts = 0) {
|
|
11
|
+
try {
|
|
12
|
+
return await IpfsClientSingleton.retrieve(ipfsHash);
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
if (attempts >= 2) {
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
await sleep(10);
|
|
19
|
+
return this.retrieveJobDefinitionWithRetry(ipfsHash, attempts + 1);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
10
22
|
async resolveJobDefinition(id, job) {
|
|
11
|
-
let jobDefinition = await
|
|
23
|
+
let jobDefinition = await this.retrieveJobDefinitionWithRetry(job.ipfsJob);
|
|
12
24
|
if (jobDefinition && jobDefinition.logistics?.send?.type) {
|
|
13
25
|
const strategySelector = new JobDefinitionStrategySelector();
|
|
14
26
|
const strategy = strategySelector.selectStrategy(jobDefinition.logistics.send.type);
|
|
@@ -28,7 +40,7 @@ export class JobExternalUtil {
|
|
|
28
40
|
}
|
|
29
41
|
async resolveResult(id, job) {
|
|
30
42
|
let result = this.repository.getFlowState(id);
|
|
31
|
-
let jobDefinition = await
|
|
43
|
+
let jobDefinition = await this.retrieveJobDefinitionWithRetry(job.ipfsJob);
|
|
32
44
|
if (result) {
|
|
33
45
|
const orginalStatus = result.status;
|
|
34
46
|
if (jobDefinition.logistics?.receive?.type) {
|
|
@@ -29,6 +29,8 @@ export declare class JobHandler {
|
|
|
29
29
|
private jobId;
|
|
30
30
|
private getJobOrThrow;
|
|
31
31
|
clearJob(): void;
|
|
32
|
+
private getErrorMessage;
|
|
33
|
+
private formatErrorDetails;
|
|
32
34
|
claim(jobAddress: string): Promise<Job>;
|
|
33
35
|
register(jobAddress: string, run: Run, job: Job): void;
|
|
34
36
|
deregister(jobAddress: string): void;
|
|
@@ -53,6 +53,31 @@ export class JobHandler {
|
|
|
53
53
|
this.job = undefined;
|
|
54
54
|
this.finishing = false;
|
|
55
55
|
}
|
|
56
|
+
getErrorMessage(error) {
|
|
57
|
+
if (error instanceof Error) {
|
|
58
|
+
return error.message;
|
|
59
|
+
}
|
|
60
|
+
return String(error);
|
|
61
|
+
}
|
|
62
|
+
formatErrorDetails(error) {
|
|
63
|
+
if (!(error instanceof Error)) {
|
|
64
|
+
return String(error);
|
|
65
|
+
}
|
|
66
|
+
const details = [error.message];
|
|
67
|
+
const cause = error.cause;
|
|
68
|
+
if (cause instanceof Error) {
|
|
69
|
+
let causeDetails = cause.message;
|
|
70
|
+
const causeCode = cause.code;
|
|
71
|
+
if (causeCode) {
|
|
72
|
+
causeDetails += ` [${causeCode}]`;
|
|
73
|
+
}
|
|
74
|
+
details.push(`cause: ${causeDetails}`);
|
|
75
|
+
}
|
|
76
|
+
else if (cause !== undefined) {
|
|
77
|
+
details.push(`cause: ${String(cause)}`);
|
|
78
|
+
}
|
|
79
|
+
return details.join(' | ');
|
|
80
|
+
}
|
|
56
81
|
async claim(jobAddress) {
|
|
57
82
|
try {
|
|
58
83
|
const job = await this.sdk.jobs.get(jobAddress);
|
|
@@ -60,8 +85,8 @@ export class JobHandler {
|
|
|
60
85
|
this.job = job;
|
|
61
86
|
return job;
|
|
62
87
|
}
|
|
63
|
-
catch (
|
|
64
|
-
throw new Error(
|
|
88
|
+
catch (error) {
|
|
89
|
+
throw new Error(`could not start job: ${this.formatErrorDetails(error)}`);
|
|
65
90
|
}
|
|
66
91
|
}
|
|
67
92
|
register(jobAddress, run, job) {
|
|
@@ -187,8 +212,8 @@ export class JobHandler {
|
|
|
187
212
|
await this.sdk.jobs.submitResult(bytesArray, run, this.getJobOrThrow().market.toString());
|
|
188
213
|
}
|
|
189
214
|
}
|
|
190
|
-
catch (
|
|
191
|
-
throw new Error(`Failed to finish job: ${
|
|
215
|
+
catch (error) {
|
|
216
|
+
throw new Error(`Failed to finish job: ${this.formatErrorDetails(error)}`);
|
|
192
217
|
}
|
|
193
218
|
}
|
|
194
219
|
async clearOldJobs() {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
1
|
import { HostManager } from './hostManager.js';
|
|
3
2
|
import TaskManager, { OperationProgressStatuses } from "../task/TaskManager.js";
|
|
4
3
|
import { createLoggingProxy, logEmitter } from '../../monitoring/proxy/loggingProxy.js';
|
|
@@ -12,8 +11,7 @@ export class Benchmark {
|
|
|
12
11
|
}
|
|
13
12
|
async run() {
|
|
14
13
|
await this.executeBenchmark();
|
|
15
|
-
|
|
16
|
-
await this.reportResults(results);
|
|
14
|
+
await this.submitResults();
|
|
17
15
|
}
|
|
18
16
|
async executeBenchmark() {
|
|
19
17
|
for (const op of this.jobDefinition.ops) {
|
|
@@ -60,25 +58,6 @@ export class Benchmark {
|
|
|
60
58
|
if (!flowResults)
|
|
61
59
|
throw new Error(`Cannot find results for flow with id ${this.benchmarkId}`);
|
|
62
60
|
this.repository.deleteflow(this.benchmarkId);
|
|
63
|
-
|
|
64
|
-
return response.report?.metrics ?? [];
|
|
65
|
-
}
|
|
66
|
-
reportResults(metrics) {
|
|
67
|
-
const displayMetrics = metrics.filter((metric) => metric.measuredValue !== undefined);
|
|
68
|
-
const nameWidth = displayMetrics.length > 0
|
|
69
|
-
? Math.max(20, ...displayMetrics.map((metric) => metric.metricKey.length))
|
|
70
|
-
: 20;
|
|
71
|
-
const width = nameWidth + 20;
|
|
72
|
-
console.log();
|
|
73
|
-
console.log(' ' + chalk.cyan('━'.repeat(width)));
|
|
74
|
-
console.log(' ' + chalk.bold.cyan('LATEST MEASUREMENTS'));
|
|
75
|
-
console.log(' ' + chalk.cyan('━'.repeat(width)));
|
|
76
|
-
console.log();
|
|
77
|
-
for (const metric of displayMetrics) {
|
|
78
|
-
const name = chalk.bold(metric.metricKey.padEnd(nameWidth));
|
|
79
|
-
const details = chalk.cyan(`measured: ${metric.measuredValue}`);
|
|
80
|
-
console.log(` ${chalk.cyan('•')} ${name} ${details}`);
|
|
81
|
-
}
|
|
82
|
-
console.log();
|
|
61
|
+
await HostManager.submitBenchmarkResults(this.benchmarkId, flowResults.state);
|
|
83
62
|
}
|
|
84
63
|
}
|
|
@@ -3,6 +3,7 @@ import type { operations } from "../../clients/hostManager/schema.d.ts";
|
|
|
3
3
|
type RequestMarketResponse = operations["getNodesRequest-market"]["responses"][200]["content"]["application/json"];
|
|
4
4
|
export type SubmitBenchmarkBody = operations["postBenchmarksByIdSubmit-results"]["requestBody"]["content"]["application/json"];
|
|
5
5
|
type GetNodeResponse = operations["getNodesById"]["responses"][200]["content"]["application/json"];
|
|
6
|
+
export type NodeMetricsResponse = operations["getNodesByIdMetrics"]["responses"][200]["content"]["application/json"];
|
|
6
7
|
type RequiredResourcesResponse = operations["getMarketsByIdRequired-resources"]["responses"][200]["content"]["application/json"];
|
|
7
8
|
type HeartbeatResponse = operations["postNodesHeartbeat"]["responses"][200]["content"]["application/json"];
|
|
8
9
|
type ErrorReportBody = operations["postErrorsReport"]["requestBody"]["content"]["application/json"];
|
|
@@ -13,6 +14,7 @@ export type RequestMarketResult = Pick<RequestMarketResponse, "status" | "market
|
|
|
13
14
|
id: string;
|
|
14
15
|
};
|
|
15
16
|
benchmarkId?: string;
|
|
17
|
+
nextTestAt?: string;
|
|
16
18
|
};
|
|
17
19
|
export declare class HostManager {
|
|
18
20
|
static register(email: string, discord?: string, twitter?: string): Promise<void>;
|
|
@@ -31,7 +33,7 @@ export declare class HostManager {
|
|
|
31
33
|
passed: boolean;
|
|
32
34
|
metrics: {
|
|
33
35
|
metricKey: string;
|
|
34
|
-
measuredValue?: string | number | boolean | undefined;
|
|
36
|
+
measuredValue?: string | number | boolean | (string | number | boolean | null)[] | null | undefined;
|
|
35
37
|
ruleDescription: string;
|
|
36
38
|
passed: boolean;
|
|
37
39
|
failureMessage?: string | undefined;
|
|
@@ -46,7 +48,7 @@ export declare class HostManager {
|
|
|
46
48
|
passed: boolean;
|
|
47
49
|
metrics: {
|
|
48
50
|
metricKey: string;
|
|
49
|
-
measuredValue?: string | number | boolean | undefined;
|
|
51
|
+
measuredValue?: string | number | boolean | (string | number | boolean | null)[] | null | undefined;
|
|
50
52
|
ruleDescription: string;
|
|
51
53
|
passed: boolean;
|
|
52
54
|
failureMessage?: string | undefined;
|
|
@@ -61,7 +63,7 @@ export declare class HostManager {
|
|
|
61
63
|
passed: boolean;
|
|
62
64
|
metrics: {
|
|
63
65
|
metricKey: string;
|
|
64
|
-
measuredValue?: string | number | boolean | undefined;
|
|
66
|
+
measuredValue?: string | number | boolean | (string | number | boolean | null)[] | null | undefined;
|
|
65
67
|
ruleDescription: string;
|
|
66
68
|
passed: boolean;
|
|
67
69
|
failureMessage?: string | undefined;
|
|
@@ -73,6 +75,7 @@ export declare class HostManager {
|
|
|
73
75
|
}, `${string}/${string}`>>;
|
|
74
76
|
static syncNodeAfterMint(address: string): Promise<void>;
|
|
75
77
|
static getNode(address: string): Promise<GetNodeResponse | null>;
|
|
78
|
+
static getNodeMetrics(address: string): Promise<NodeMetricsResponse | null>;
|
|
76
79
|
static getMarketRequiredResources(marketId: string): Promise<RequiredResourcesResponse | null>;
|
|
77
80
|
static sendHeartbeat(): Promise<HeartbeatResponse>;
|
|
78
81
|
static getRpcUrl(): Promise<string | undefined>;
|
|
@@ -45,6 +45,10 @@ export class HostManager {
|
|
|
45
45
|
sftTx: data.market.sftTx ?? undefined,
|
|
46
46
|
};
|
|
47
47
|
}
|
|
48
|
+
const nextTestAt = data.nextTestAt;
|
|
49
|
+
if (typeof nextTestAt === "string") {
|
|
50
|
+
result.nextTestAt = nextTestAt;
|
|
51
|
+
}
|
|
48
52
|
return result;
|
|
49
53
|
}
|
|
50
54
|
static async submitBenchmarkResults(benchmarkId, body) {
|
|
@@ -77,6 +81,18 @@ export class HostManager {
|
|
|
77
81
|
}
|
|
78
82
|
return data;
|
|
79
83
|
}
|
|
84
|
+
static async getNodeMetrics(address) {
|
|
85
|
+
const { data, error, response } = await hostManagerClientSelector().GET("/nodes/{id}/metrics", {
|
|
86
|
+
params: { path: { id: address }, query: {} },
|
|
87
|
+
});
|
|
88
|
+
if (error || !data) {
|
|
89
|
+
if (response?.status === 404 || response?.status === 'NotFound') {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
throw new Error(`Error getting node metrics: ${JSON.stringify(error)}`);
|
|
93
|
+
}
|
|
94
|
+
return data;
|
|
95
|
+
}
|
|
80
96
|
static async getMarketRequiredResources(marketId) {
|
|
81
97
|
const { data, error } = await hostManagerClientSelector().GET("/markets/{id}/required-resources", {
|
|
82
98
|
params: { path: { id: marketId } },
|
|
@@ -17,6 +17,7 @@ export declare class MarketHandler {
|
|
|
17
17
|
setInMarket(): void;
|
|
18
18
|
request(requestedMarket?: string): Promise<Market>;
|
|
19
19
|
private logFeedbackReport;
|
|
20
|
+
private logLatestMeasurements;
|
|
20
21
|
check(market: string): Promise<Market>;
|
|
21
22
|
stopMarket(): Promise<boolean>;
|
|
22
23
|
getJobMarket(market: string): Promise<Market>;
|
|
@@ -46,11 +46,12 @@ export class MarketHandler {
|
|
|
46
46
|
await sleep(30);
|
|
47
47
|
await HostManager.syncNodeAfterMint(this.address.toString());
|
|
48
48
|
}
|
|
49
|
+
await this.logLatestMeasurements();
|
|
49
50
|
if (result.feedbackReport) {
|
|
50
51
|
this.logFeedbackReport(result.feedbackReport);
|
|
51
52
|
}
|
|
52
53
|
if (!result.market?.address) {
|
|
53
|
-
throw new NodeNotQualifiedError();
|
|
54
|
+
throw new NodeNotQualifiedError(result.nextTestAt);
|
|
54
55
|
}
|
|
55
56
|
const onchainMarket = await this.sdk.jobs.getMarket(result.market.address);
|
|
56
57
|
if (!onchainMarket) {
|
|
@@ -64,37 +65,80 @@ export class MarketHandler {
|
|
|
64
65
|
}
|
|
65
66
|
logFeedbackReport(report) {
|
|
66
67
|
const targetMarketLabel = report.marketName ?? report.marketAddress;
|
|
67
|
-
const
|
|
68
|
-
|
|
68
|
+
const requiredMetrics = report.metrics.filter((m) => !m.isOptional);
|
|
69
|
+
const optionalMetrics = report.metrics.filter((m) => m.isOptional);
|
|
69
70
|
let nameWidth = 24;
|
|
71
|
+
let passedRequired = 0;
|
|
70
72
|
for (const metric of report.metrics) {
|
|
71
|
-
if (metric.passed)
|
|
72
|
-
passedCount++;
|
|
73
73
|
if (metric.metricKey.length > nameWidth)
|
|
74
74
|
nameWidth = metric.metricKey.length;
|
|
75
75
|
}
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
76
|
+
for (const metric of requiredMetrics) {
|
|
77
|
+
if (metric.passed)
|
|
78
|
+
passedRequired++;
|
|
79
|
+
}
|
|
80
|
+
const totalRequired = requiredMetrics.length;
|
|
81
|
+
const failedRequired = totalRequired - passedRequired;
|
|
82
|
+
const printMetric = (metric) => {
|
|
81
83
|
const icon = metric.passed ? chalk.green(" ✔ ") : chalk.red(" ✖ ");
|
|
82
|
-
const measuredStr = metric.measuredValue !== undefined
|
|
83
|
-
? ` ${chalk.cyan(
|
|
84
|
+
const measuredStr = metric.measuredValue !== undefined && metric.measuredValue !== null
|
|
85
|
+
? ` ${chalk.cyan(metric.measuredValue)}`
|
|
84
86
|
: "";
|
|
85
87
|
console.log(icon + chalk.bold(metric.metricKey.padEnd(nameWidth)) + measuredStr);
|
|
86
88
|
console.log(chalk.gray(` rule: ${metric.ruleDescription}`));
|
|
87
|
-
|
|
88
|
-
|
|
89
|
+
};
|
|
90
|
+
console.log("\n" + chalk.bgCyan.black.bold(" TARGET MARKET ") + "\n");
|
|
91
|
+
console.log(` ${chalk.bold(targetMarketLabel)}`);
|
|
92
|
+
console.log();
|
|
93
|
+
for (const metric of requiredMetrics) {
|
|
94
|
+
printMetric(metric);
|
|
95
|
+
}
|
|
96
|
+
if (optionalMetrics.length > 0) {
|
|
97
|
+
console.log();
|
|
98
|
+
console.log(` ${chalk.bold.underline("Optional thresholds")}`);
|
|
99
|
+
console.log(chalk.gray(` (do not block market access)`));
|
|
100
|
+
console.log();
|
|
101
|
+
for (const metric of optionalMetrics) {
|
|
102
|
+
printMetric(metric);
|
|
89
103
|
}
|
|
90
104
|
}
|
|
105
|
+
console.log();
|
|
91
106
|
if (report.passed) {
|
|
92
|
-
console.log(chalk.bgGreen.black.bold(` Node currently meets all ${
|
|
107
|
+
console.log(chalk.bgGreen.black.bold(` Node currently meets all ${totalRequired} market requirements for ${targetMarketLabel} `) + "\n");
|
|
93
108
|
}
|
|
94
109
|
else {
|
|
95
|
-
console.log(chalk.bgYellow.black.bold(` Node currently meets ${
|
|
110
|
+
console.log(chalk.bgYellow.black.bold(` Node currently meets ${passedRequired} of ${totalRequired} market requirements for ${targetMarketLabel} — ${failedRequired} still need improvement `) + "\n");
|
|
96
111
|
}
|
|
97
112
|
}
|
|
113
|
+
async logLatestMeasurements() {
|
|
114
|
+
let response;
|
|
115
|
+
try {
|
|
116
|
+
response = await HostManager.getNodeMetrics(this.address.toString());
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
console.warn(chalk.yellow("Could not fetch latest measurements:"), error);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const rawMetrics = response?.metrics;
|
|
123
|
+
if (!rawMetrics)
|
|
124
|
+
return;
|
|
125
|
+
const flat = flattenMetrics(rawMetrics);
|
|
126
|
+
if (flat.length === 0)
|
|
127
|
+
return;
|
|
128
|
+
const nameWidth = Math.max(20, ...flat.map(([key]) => key.length));
|
|
129
|
+
const width = nameWidth + 30;
|
|
130
|
+
console.log();
|
|
131
|
+
console.log(" " + chalk.cyan("━".repeat(width)));
|
|
132
|
+
console.log(" " + chalk.bold.cyan("LATEST MEASUREMENTS"));
|
|
133
|
+
console.log(" " + chalk.cyan("━".repeat(width)));
|
|
134
|
+
console.log();
|
|
135
|
+
for (const [key, value] of flat) {
|
|
136
|
+
const name = chalk.bold(key.padEnd(nameWidth));
|
|
137
|
+
const details = chalk.cyan(value);
|
|
138
|
+
console.log(` ${chalk.cyan("•")} ${name} ${details}`);
|
|
139
|
+
}
|
|
140
|
+
console.log();
|
|
141
|
+
}
|
|
98
142
|
async check(market) {
|
|
99
143
|
try {
|
|
100
144
|
return await this.sdk.jobs.getMarket(market);
|
|
@@ -217,3 +261,27 @@ export class MarketHandler {
|
|
|
217
261
|
this.clear();
|
|
218
262
|
}
|
|
219
263
|
}
|
|
264
|
+
function flattenMetrics(obj, prefix = "") {
|
|
265
|
+
const result = [];
|
|
266
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
267
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
268
|
+
if (Array.isArray(value)) {
|
|
269
|
+
value.forEach((item, i) => {
|
|
270
|
+
const indexed = `${fullKey}[${i}]`;
|
|
271
|
+
if (item && typeof item === "object") {
|
|
272
|
+
result.push(...flattenMetrics(item, indexed));
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
result.push([indexed, String(item)]);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
else if (value && typeof value === "object") {
|
|
280
|
+
result.push(...flattenMetrics(value, fullKey));
|
|
281
|
+
}
|
|
282
|
+
else if (value !== null && value !== undefined) {
|
|
283
|
+
result.push([fullKey, String(value)]);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return result;
|
|
287
|
+
}
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nosana/node",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.19-rc",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@nosana/node",
|
|
9
|
-
"version": "1.1.
|
|
9
|
+
"version": "1.1.19-rc",
|
|
10
10
|
"license": "ISC",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@coral-xyz/anchor": "^0.28.1-beta.1",
|