Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion packages/gridproxy_client/src/builders/abstract_builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,23 @@ export abstract class AbstractBuilder<T> {
}
}

public async build(path: string, timeout = 10000): Promise<Response> {
public async build(path: string, timeout = 10000, signal?: AbortSignal): Promise<Response> {
assertString(path);
assertPattern(path, /^\//);

const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);

// Combine external signal with timeout signal
if (signal) {
// If signal is already aborted, abort immediately
if (signal.aborted) {
controller.abort();
} else {
signal.addEventListener("abort", () => controller.abort());
}
}

try {
const out: string[] = [];

Expand Down
4 changes: 2 additions & 2 deletions packages/gridproxy_client/src/modules/farms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ export class FarmsClient extends AbstractClient<FarmsBuilder, FarmsQuery> {
});
}

public async list(queries: Partial<FarmsQuery> = {}) {
const res = await this.builder(queries).build("/farms");
public async list(queries: Partial<FarmsQuery> = {}, signal?: AbortSignal) {
const res = await this.builder(queries).build("/farms", 10000, signal);
return resolvePaginator<Farm[]>(res);
}

Expand Down
42 changes: 24 additions & 18 deletions packages/gridproxy_client/src/modules/nodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,22 +32,28 @@ export class NodesClient extends AbstractClient<NodesBuilder, NodesQuery> {
this.setTwin = this.setTwin.bind(this);
}

public async list(queries: Partial<NodesQuery> = {}, extraOptions: NodesExtractOptions = {}) {
const res = await this.builder(queries).build("/nodes");
public async list(queries: Partial<NodesQuery> = {}, extraOptions: NodesExtractOptions = {}, signal?: AbortSignal) {
const res = await this.builder(queries).build("/nodes", 10000, signal);
const nodes = await resolvePaginator<GridNode[]>(res);

if (extraOptions.loadFarm) {
await this.loadFarms(nodes.data.map(n => n.farmId));
await this.loadFarms(
nodes.data.map(n => n.farmId),
signal,
);
nodes.data = nodes.data.map(this.setFarm);
}

if (extraOptions.loadTwin) {
await this.loadTwins(nodes.data.map(n => n.twinId));
await this.loadTwins(
nodes.data.map(n => n.twinId),
signal,
);
nodes.data = nodes.data.map(this.setTwin);
}

if (extraOptions.loadStats) {
const nodesStats = await Promise.all(nodes.data.map(n => this.statsById(n.nodeId)));
const nodesStats = await Promise.all(nodes.data.map(n => this.statsById(n.nodeId, signal)));
nodes.data = nodes.data.map((n, index) => {
n.stats = nodesStats[index];
return n;
Expand All @@ -57,8 +63,8 @@ export class NodesClient extends AbstractClient<NodesBuilder, NodesQuery> {
return nodes;
}

public async byId(nodeId: number, extraOptions: NodesExtractOptions = {}): Promise<GridNode> {
const res = await this.builder({}).build(`/nodes/${nodeId}`);
public async byId(nodeId: number, extraOptions: NodesExtractOptions = {}, signal?: AbortSignal): Promise<GridNode> {
const res = await this.builder({}).build(`/nodes/${nodeId}`, 10000, signal);
let node: GridNode = await res.json();

const capacity = Reflect.get(node, "capacity");
Expand All @@ -68,37 +74,37 @@ export class NodesClient extends AbstractClient<NodesBuilder, NodesQuery> {
}

if (extraOptions.loadFarm && node) {
await this.loadFarms([node.farmId]);
await this.loadFarms([node.farmId], signal);
node = this.setFarm(node);
}

if (extraOptions.loadTwin) {
await this.loadTwins([node.twinId]);
await this.loadTwins([node.twinId], signal);
node = this.setTwin(node);
}

if (extraOptions.loadStats) {
node.stats = await this.statsById(node.nodeId);
node.stats = await this.statsById(node.nodeId, signal);
}

return node;
}

public async statsById(nodeId: number): Promise<NodeStats> {
const res = await this.builder({}).build(`/nodes/${nodeId}/statistics`);
public async statsById(nodeId: number, signal?: AbortSignal): Promise<NodeStats> {
const res = await this.builder({}).build(`/nodes/${nodeId}/statistics`, 10000, signal);
return res.json();
}

public async gpuById(nodeId: number): Promise<GPUCard[] | null | { error: string }> {
const res = await this.builder({}).build(`/nodes/${nodeId}/gpu`);
public async gpuById(nodeId: number, signal?: AbortSignal): Promise<GPUCard[] | null | { error: string }> {
const res = await this.builder({}).build(`/nodes/${nodeId}/gpu`, 10000, signal);
return res.json();
}

private async loadFarms(farmIds: number[]): Promise<void> {
private async loadFarms(farmIds: number[], signal?: AbortSignal): Promise<void> {
farmIds = farmIds.filter(id => !this.farms.has(id));
const ids = Array.from(new Set(farmIds));
if (!ids.length) return;
const farms = await Promise.all(ids.map(farmId => this.__farmsClient.list({ farmId })));
const farms = await Promise.all(ids.map(farmId => this.__farmsClient.list({ farmId }, signal)));
for (const { data } of farms) {
const [farm] = data;
this.farms = this.farms.set(farm.farmId, farm);
Expand All @@ -124,11 +130,11 @@ export class NodesClient extends AbstractClient<NodesBuilder, NodesQuery> {
};
}

private async loadTwins(twinIds: number[]): Promise<void> {
private async loadTwins(twinIds: number[], signal?: AbortSignal): Promise<void> {
twinIds = twinIds.filter(id => !this.twins.has(id));
const ids = Array.from(new Set(twinIds));
if (!ids.length) return;
const twins = await Promise.all(ids.map(twinId => this.__twinsClient.list({ twinId })));
const twins = await Promise.all(ids.map(twinId => this.__twinsClient.list({ twinId }, signal)));
for (const { data } of twins) {
const [twin] = data;
this.twins = this.twins.set(twin.twinId, twin);
Expand Down
4 changes: 2 additions & 2 deletions packages/gridproxy_client/src/modules/stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ export class StatsClient extends AbstractClient<StatsBuilder, StatsQuery> {
});
}

public async get(queries: Partial<StatsQuery> = {}): Promise<Stats> {
const res = await this.builder(queries).build("/stats");
public async get(queries: Partial<StatsQuery> = {}, signal?: AbortSignal): Promise<Stats> {
const res = await this.builder(queries).build("/stats", 10000, signal);
return res.json();
}
}
4 changes: 2 additions & 2 deletions packages/gridproxy_client/src/modules/twins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ export class TwinsClient extends AbstractClient<TwinsBuilder, TwinsQuery> {
});
}

public async list(queries: Partial<TwinsQuery> = {}) {
const res = await this.builder(queries).build("/twins");
public async list(queries: Partial<TwinsQuery> = {}, signal?: AbortSignal) {
const res = await this.builder(queries).build("/twins", 10000, signal);
return resolvePaginator<Twin[]>(res);
}

Expand Down
2 changes: 1 addition & 1 deletion packages/playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"dev": "vite",
"build": "run-p type-check build-only",
"preview": "vite preview",
"test:unit": "vitest",
"test:unit": "vitest --run",
"build-only": "vite build",
"type-check": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false"
},
Expand Down
32 changes: 19 additions & 13 deletions packages/playground/src/components/app_info.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,7 @@

<v-divider class="mb-2" />
<v-card-actions class="d-flex justify-end">
<v-btn color="anchor" class="mr-2 my-1" @click="setOpenInfo(false)">
Close
</v-btn>
<v-btn color="anchor" class="mr-2 my-1" @click="setOpenInfo(false)"> Close </v-btn>
</v-card-actions>
</v-card>
</v-dialog>
Expand All @@ -41,6 +39,7 @@ import { marked } from "marked";
import { computed, type ComputedRef, ref } from "vue";
import { useRoute } from "vue-router";

import { isAbortError, useFetch } from "../hooks/useAbortController";
import type { InfoMeta } from "../router";

export interface InfoFileMeta {
Expand All @@ -59,22 +58,29 @@ export default {
const title = ref("");
const subtitle = ref("");
const html = ref("");
const fetchWithAbort = useFetch();

async function setOpenInfo(value: boolean) {
openInfo.value = value;

if (value) {
loading.value = true;
const res = await fetch(import.meta.env.BASE_URL + info.value.page);
const markdown = await res.text();

const { attributes, body } = fm<InfoFileMeta>(markdown);

title.value = attributes.title || "";
subtitle.value = attributes.subtitle || "";
html.value = await marked.parse(body);

loading.value = false;
try {
const res = await fetchWithAbort(import.meta.env.BASE_URL + info.value.page);
const markdown = await res.text();

const { attributes, body } = fm<InfoFileMeta>(markdown);

title.value = attributes.title || "";
subtitle.value = attributes.subtitle || "";
html.value = await marked.parse(body);

loading.value = false;
} catch (error) {
if (isAbortError(error)) return;
loading.value = false;
throw error;
}
}
}

Expand Down
33 changes: 17 additions & 16 deletions packages/playground/src/dashboard/components/user_nodes.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
<template v-if="nodes">
<div class="my-6">
<v-card color="primary rounded-0">
<v-card-title class="py-1 text-subtitle-1 text-center">
Your Nodes
</v-card-title>
<v-card-title class="py-1 text-subtitle-1 text-center"> Your Nodes </v-card-title>
</v-card>
<v-data-table-server
v-model:page="page"
Expand Down Expand Up @@ -46,9 +44,7 @@

<v-card class="mt-4">
<v-alert class="pa-5" style="height: 20px">
<h4 class="text-center font-weight-medium">
Resource Units Reserved
</h4>
<h4 class="text-center font-weight-medium">Resource Units Reserved</h4>
</v-alert>
<v-card-text class="pb-8">
<NodeResources :node="item" />
Expand All @@ -57,9 +53,7 @@

<v-card v-if="network == 'main'" class="mt-4" focusable single model-value>
<v-alert class="pa-5" style="height: 20px">
<h4 class="text-center font-weight-medium">
Node Statistics
</h4>
<h4 class="text-center font-weight-medium">Node Statistics</h4>
</v-alert>
<v-card-item>
<NodeMintingDetails :node="item" />
Expand Down Expand Up @@ -107,6 +101,7 @@ import CardDetails from "@/components/node_details_cards/card_details.vue";
import { useProfileManager } from "@/stores";
import type { NodeDetailsCard } from "@/types";
import { createCustomToast, ToastType } from "@/utils/custom_toast";
import { useAbortController, useWithAbortSignal } from "@/hooks/useAbortController";
import { getNodeStatusColor } from "@/utils/get_nodes";
import { calculateUptime, getNodeAvailability, getNodeMintingFixupReceipts, type NodeInterface } from "@/utils/node";

Expand All @@ -126,6 +121,8 @@ export default {
},
setup() {
const profileManager = useProfileManager();
const getReceiptsWithAbort = useWithAbortSignal(getNodeMintingFixupReceipts);
const signal = useAbortController();
const loading = ref(false);
const page = ref<number>(1);
const pageSize = ref(10);
Expand Down Expand Up @@ -187,12 +184,16 @@ export default {
loading.value = true;
const twinId = profileManager.profile!.twinId;

const { data, count } = await gridProxyClient.nodes.list({
retCount: true,
page: page.value,
size: pageSize.value,
ownedBy: twinId as number,
});
const { data, count } = await gridProxyClient.nodes.list(
{
retCount: true,
page: page.value,
size: pageSize.value,
ownedBy: twinId as number,
},
{},
signal,
);

const _nodes = data as unknown as NodeInterface[];
nodesCount.value = count ?? 0;
Expand All @@ -201,7 +202,7 @@ export default {
const network = process.env.NETWORK || (window as any).env.NETWORK;
node.receipts = [];
try {
if (network == "main") node.receipts = await getNodeMintingFixupReceipts(node.nodeId);
if (network == "main") node.receipts = await getReceiptsWithAbort(node.nodeId);
} catch {
createCustomToast(`Failed to get node ${node.nodeId} minting receipts!`, ToastType.danger);
}
Expand Down
68 changes: 68 additions & 0 deletions packages/playground/src/hooks/useAbortController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { onUnmounted } from "vue";

/**
* Composable that provides an AbortController that automatically aborts on component unmount.
* @returns AbortController signal
*/
export function useAbortController() {
const abortController = new AbortController();

onUnmounted(() => {
abortController.abort();
});

return abortController.signal;
}

/**
* Type guard to detect AbortError in catch blocks.
*/
export function isAbortError(error: unknown): error is Error {
return error instanceof Error && error.name === "AbortError";
}

/**
* Composable that wraps fetch calls with automatic abort signal.
* Reduces boilerplate by automatically injecting signal.
*/
export function useFetch() {
const signal = useAbortController();

return async (url: string, options?: RequestInit) => {
return fetch(url, { ...options, signal });
};
}

/**
* Composable that wraps API utility functions with automatic abort signal.
* Automatically appends abort signal as the last parameter.
* Creates a fresh signal for each component instance that aborts on unmount.
*
* Usage:
* - const getNodesWithSignal = useWithAbortSignal(requestNodes);
* - const listWithSignal = useWithAbortSignal((q, opts) => gridProxyClient.nodes.list(q, opts));
*/
export function useWithAbortSignal<T extends (...args: any[]) => Promise<any>>(
fn: T,
): (...args: Parameters<T>) => Promise<ReturnType<T>> {
// Get the signal that will be aborted when component unmounts
// This creates a NEW signal each time setup() runs (on each mount)
const signal = useAbortController();

return async (...args: Parameters<T>) => {
// Check if signal is already aborted (component unmounted)
if (signal.aborted) {
const error = new Error("Request aborted");
error.name = "AbortError";
throw error;
}

// Check if last argument is already an AbortSignal
const lastArg = args[args.length - 1];
if (lastArg instanceof AbortSignal) {
return fn(...args);
}
// Append signal as last parameter
return fn(...args, signal as any);
};
}
Loading