Skip to content
184 changes: 184 additions & 0 deletions app/(main)/instance/_lib/instance.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { Instance } from '../../instances/_lib/instances.d';

const OPERATION_POLL_MAX_ATTEMPTS = 20;
const OPERATION_POLL_DELAY_MS = 500;

export type InstanceAction = 'start' | 'stop' | 'restart' | 'freeze';

export async function performInstanceAction({
Expand Down Expand Up @@ -34,3 +37,184 @@ export async function performInstanceAction({
);
}
}

export async function updateInstance({
name,
description,
project,
}: {
name: string;
description: string;
project?: string;
}) {
const projectSuffix = project
? `?project=${encodeURIComponent(project)}`
: '';
const res = await fetch(
`/1.0/instances/${encodeURIComponent(name)}${projectSuffix}`,
{
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
description,
}),
},
);
if (!res.ok) {
const payload = await res.json().catch(() => ({ error: res.statusText }));
throw new Error(payload?.error || `Unable to update instance ${name}`);
}
}

export async function renameInstance({
name,
newName,
project,
}: {
name: string;
newName: string;
project?: string;
}) {
const projectSuffix = project
? `?project=${encodeURIComponent(project)}`
: '';
const res = await fetch(
`/1.0/instances/${encodeURIComponent(name)}${projectSuffix}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: newName,
}),
},
);
if (!res.ok) {
const payload = await res.json().catch(() => ({ error: res.statusText }));
throw new Error(payload?.error || `Unable to rename instance ${name}`);
}

const data = await res.json();
if (data.type === 'async' && data.operation) {
await waitForOperation(data.operation);
}
}

async function waitForOperation(operationUrl: string) {
// Poll the operation URL until it's done
for (let i = 0; i < OPERATION_POLL_MAX_ATTEMPTS; i++) {
const res = await fetch(operationUrl);
if (!res.ok) {
// If we can't check the operation, assume it failed or network issue
throw new Error('Failed to check operation status');
}
const data = await res.json();
// Operation status: Running, Pending, Success, Failure, Cancelled
if (data.metadata?.status === 'Success') {
return;
}
if (data.metadata?.status === 'Failure') {
throw new Error(data.metadata.err || 'Operation failed');
}
if (data.metadata?.status === 'Cancelled') {
throw new Error('Operation cancelled');
}

// Wait before next poll
await new Promise((resolve) => setTimeout(resolve, OPERATION_POLL_DELAY_MS));
}

const operationId = operationUrl.split('/').pop() ?? operationUrl;
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
const socketUrl = `${protocol}://${window.location.host}/1.0/events?type=operation&operation=${encodeURIComponent(
operationId,
)}`;

await new Promise<void>((resolve, reject) => {
let settled = false;
const socket = new WebSocket(socketUrl);
const timeoutId = window.setTimeout(() => {
if (settled) return;
settled = true;
socket.close();
reject(new Error('Operation timed out'));
}, OPERATION_EVENT_TIMEOUT_MS);
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The constant OPERATION_EVENT_TIMEOUT_MS is referenced but not defined anywhere in this file. This will cause a ReferenceError at runtime when the WebSocket fallback is triggered.

Copilot uses AI. Check for mistakes.

const cleanup = () => {
window.clearTimeout(timeoutId);
socket.onopen = null;
socket.onmessage = null;
socket.onerror = null;
socket.onclose = null;
};

socket.onerror = () => {
if (settled) return;
settled = true;
cleanup();
reject(new Error('Failed to check operation status'));
};

socket.onclose = () => {
if (settled) return;
settled = true;
cleanup();
reject(new Error('Operation socket closed before completion'));
};

socket.onmessage = (event) => {
try {
const data = JSON.parse(event.data) as OperationEvent;
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type OperationEvent is not defined or imported in this file. This will cause a TypeScript compilation error.

Copilot uses AI. Check for mistakes.
if (data.type !== 'operation') {
return;
}

const metadata = data.metadata;
if (!metadata?.status) {
return;
}

if (metadata.id && metadata.id !== operationId) {
return;
}

if (metadata.status === 'Success') {
if (settled) return;
settled = true;
cleanup();
socket.close();
resolve();
}

if (metadata.status === 'Failure') {
if (settled) return;
settled = true;
cleanup();
socket.close();
reject(new Error(metadata.err || 'Operation failed'));
}

if (metadata.status === 'Cancelled') {
if (settled) return;
settled = true;
cleanup();
socket.close();
reject(new Error('Operation cancelled'));
}
} catch (error) {
if (settled) return;
settled = true;
cleanup();
socket.close();
reject(
error instanceof Error
? error
: new Error('Failed to parse operation status'),
);
}
};
});
}
Loading
Loading