harb/web-app/src/composables/useVersionCheck.ts

190 lines
5.2 KiB
TypeScript
Raw Normal View History

import { ref, onMounted } from 'vue';
import { DEFAULT_CHAIN_ID } from '@/config';
import { resolveGraphqlEndpoint } from '@/utils/graphqlRetry';
import { KRAIKEN_LIB_VERSION, STACK_META_ID } from 'kraiken-lib/version';
const WEB_APP_VERSION = __APP_VERSION__;
type GraphqlError = { message?: string | null };
type GraphqlResponse<T> = {
data?: T;
errors?: GraphqlError[];
};
interface StackMetaRecord {
contractVersion?: number | null;
ponderVersion?: string | null;
kraikenLibVersion?: number | null;
updatedAt?: string | null;
}
interface StackMetaResult {
stackMeta?: StackMetaRecord | null;
}
export interface StackVersions {
contractVersion: number | null;
ponderVersion: string | null;
indexerLibVersion: number | null;
kraikenLibVersion: number;
webAppVersion: string;
updatedAt: bigint | null;
}
export interface VersionStatus {
isValid: boolean;
error?: string;
contractVersion?: number;
indexerLibVersion?: number;
ponderVersion?: string | null;
libVersion: number;
webAppVersion: string;
}
const STACK_META_QUERY = `
query StackMeta($id: String!) {
stackMeta(id: $id) {
contractVersion
ponderVersion
kraikenLibVersion
updatedAt
}
}
`;
const versionStatus = ref<VersionStatus>({
isValid: true,
libVersion: KRAIKEN_LIB_VERSION,
webAppVersion: WEB_APP_VERSION,
});
const stackVersions = ref<StackVersions>({
contractVersion: null,
ponderVersion: null,
indexerLibVersion: null,
kraikenLibVersion: KRAIKEN_LIB_VERSION,
webAppVersion: WEB_APP_VERSION,
updatedAt: null,
});
const isChecking = ref(false);
const hasChecked = ref(false);
function formatError(error: unknown): string {
if (error instanceof Error && error.message) {
return error.message;
}
if (typeof error === 'string' && error.length > 0) {
return error;
}
return 'Failed to load stack version metadata';
}
function sanitizeNumber(value: unknown): number | null {
const parsed = typeof value === 'number' ? value : Number(value ?? Number.NaN);
return Number.isFinite(parsed) ? parsed : null;
}
export function useVersionCheck() {
async function checkVersions(graphqlEndpointOverride?: string) {
if (isChecking.value) return versionStatus.value;
isChecking.value = true;
try {
const endpoint = resolveGraphqlEndpoint(DEFAULT_CHAIN_ID, graphqlEndpointOverride);
const response = await fetch(endpoint, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
query: STACK_META_QUERY,
variables: { id: STACK_META_ID },
}),
});
if (!response.ok) {
throw new Error(`GraphQL stackMeta request failed (HTTP ${response.status})`);
}
const payload = (await response.json()) as GraphqlResponse<StackMetaResult>;
const apiErrors = payload.errors?.filter(err => err?.message);
if (apiErrors && apiErrors.length > 0) {
throw new Error(apiErrors.map(err => err.message).join(', '));
}
const meta = payload.data?.stackMeta;
if (!meta) {
throw new Error('Stack metadata unavailable (stackMeta query returned null)');
}
const contractVersion = sanitizeNumber(meta.contractVersion);
const ponderVersion = meta.ponderVersion ?? null;
const indexerLibVersion = sanitizeNumber(meta.kraikenLibVersion);
const updatedAt = meta.updatedAt ? BigInt(meta.updatedAt) : null;
const libMismatch = indexerLibVersion !== null && indexerLibVersion !== KRAIKEN_LIB_VERSION;
stackVersions.value = {
contractVersion,
ponderVersion,
indexerLibVersion,
kraikenLibVersion: KRAIKEN_LIB_VERSION,
webAppVersion: WEB_APP_VERSION,
updatedAt,
};
versionStatus.value = {
isValid: !libMismatch,
error: libMismatch
? `Stack mismatch: Ponder reports kraiken-lib v${indexerLibVersion} (frontend expects v${KRAIKEN_LIB_VERSION}). Restart the stack to rebuild containers.`
: undefined,
contractVersion: contractVersion ?? undefined,
indexerLibVersion: indexerLibVersion ?? undefined,
ponderVersion,
libVersion: KRAIKEN_LIB_VERSION,
webAppVersion: WEB_APP_VERSION,
};
} catch (error) {
versionStatus.value = {
isValid: false,
error: formatError(error),
contractVersion: stackVersions.value.contractVersion ?? undefined,
indexerLibVersion: stackVersions.value.indexerLibVersion ?? undefined,
ponderVersion: stackVersions.value.ponderVersion,
libVersion: KRAIKEN_LIB_VERSION,
webAppVersion: WEB_APP_VERSION,
};
} finally {
hasChecked.value = true;
isChecking.value = false;
}
return versionStatus.value;
}
return {
versionStatus,
stackVersions,
checkVersions,
isChecking,
hasChecked,
};
}
export function useVersionCheckOnMount(graphqlUrl?: string) {
const { versionStatus, stackVersions, checkVersions, isChecking, hasChecked } = useVersionCheck();
onMounted(async () => {
await checkVersions(graphqlUrl);
});
return {
versionStatus,
stackVersions,
isChecking,
hasChecked,
checkVersions,
};
}