Quantumbraid's picture
download
raw
3.95 kB
import { apiClient } from './api';
export type ScriptArgs = (string | number | boolean)[] | Record<string, string | number | boolean | null | undefined>;
export interface ScriptResult {
ok: boolean;
exitCode: number | null;
signal: string | null;
stdout: string;
stderr: string;
result: unknown;
timedOut: boolean;
error?: string;
}
export interface StreamEvent {
type: 'stdout' | 'stderr' | 'exit' | 'error';
data?: string;
message?: string;
exitCode?: number | null;
signal?: string | null;
ok?: boolean;
timedOut?: boolean;
result?: unknown;
stderr?: string;
}
export interface CallScriptOptions {
args?: ScriptArgs;
signal?: AbortSignal;
// Match the API's 20-minute ceiling by default so axios doesn't bail early.
timeoutMs?: number;
}
export interface StreamCallScriptOptions extends CallScriptOptions {
onEvent?: (event: StreamEvent) => void;
onStdout?: (chunk: string) => void;
onStderr?: (chunk: string) => void;
}
const DEFAULT_TIMEOUT_MS = 20 * 60 * 1000;
// Buffered call: resolves with full stdout/stderr after the script exits.
export const callScript = async (
script: string,
options: CallScriptOptions = {},
): Promise<ScriptResult> => {
const response = await apiClient.post<ScriptResult>(
'/api/scripts',
{ script, args: options.args },
{
timeout: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
signal: options.signal,
// Don't throw on non-2xx so callers can inspect the structured failure.
validateStatus: () => true,
},
);
return response.data;
};
// Streaming call: invokes the callbacks for each NDJSON event as it arrives,
// then resolves with the final exit event.
export const callScriptStream = async (
script: string,
options: StreamCallScriptOptions = {},
): Promise<StreamEvent | null> => {
const token = typeof window !== 'undefined' ? localStorage.getItem('AI_TOOLKIT_AUTH') : null;
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
if (token) headers['Authorization'] = `Bearer ${token}`;
const controller = new AbortController();
const onAbort = () => controller.abort();
if (options.signal) {
if (options.signal.aborted) controller.abort();
else options.signal.addEventListener('abort', onAbort);
}
const timeout = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
const timer = setTimeout(() => controller.abort(), timeout);
let finalEvent: StreamEvent | null = null;
try {
const response = await fetch('/api/scripts', {
method: 'POST',
headers,
body: JSON.stringify({ script, args: options.args, stream: true }),
signal: controller.signal,
});
if (!response.ok || !response.body) {
const text = await response.text().catch(() => '');
throw new Error(`Script stream failed: HTTP ${response.status} ${text}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
let buffer = '';
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
let newlineIdx: number;
while ((newlineIdx = buffer.indexOf('\n')) >= 0) {
const line = buffer.slice(0, newlineIdx).trim();
buffer = buffer.slice(newlineIdx + 1);
if (!line) continue;
try {
const event = JSON.parse(line) as StreamEvent;
options.onEvent?.(event);
if (event.type === 'stdout' && event.data) options.onStdout?.(event.data);
if (event.type === 'stderr' && event.data) options.onStderr?.(event.data);
if (event.type === 'exit' || event.type === 'error') finalEvent = event;
} catch {
// Ignore malformed lines and keep streaming.
}
}
}
} finally {
clearTimeout(timer);
if (options.signal) options.signal.removeEventListener('abort', onAbort);
}
return finalEvent;
};

Xet Storage Details

Size:
3.95 kB
·
Xet hash:
cbf875fcc1b5d373397307e0ef4bed25e9b15546768a9b093720f2abfb3c5866

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.