@visionfi/desktop-sdk
Version:
Desktop SDK for VisionFI Cloud Run services with Azure AD authentication
266 lines (229 loc) • 7.19 kB
text/typescript
/**
* VisionFI Desktop Client
* Copyright (c) 2024-2025 VisionFI. All Rights Reserved.
*/
import { EventEmitter } from 'eventemitter3';
import axios, { AxiosInstance } from 'axios';
import {
// Import all types from core
PackageInfo,
CreatePackageOptions,
CreatePackageResponse,
ListPackagesOptions,
ListPackagesResponse,
AnalyzeDocumentOptions,
AnalysisJob,
AnalysisResult,
VisionFiError
} from '@visionfi/core';
import {
VisionFIDesktopConfig,
ConnectionStatus,
UploadProgress
} from './types.js';
import {
PackageClient,
AdminClient
} from './clients/index.js';
/**
* Desktop client for VisionFI Cloud Run services
* Provides Azure AD authentication and desktop-specific features
*/
export class VisionFIDesktop extends EventEmitter {
private config: VisionFIDesktopConfig;
private apiClient: AxiosInstance;
private connectionStatus: ConnectionStatus = 'offline';
// Client instances for nested API
readonly packages: PackageClient;
readonly admin: AdminClient;
constructor(config: VisionFIDesktopConfig) {
super();
this.config = config;
// Create axios instance
this.apiClient = axios.create({
baseURL: config.tenantApiUrl,
timeout: config.timeout || 30000,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
// Add authentication interceptor
this.apiClient.interceptors.request.use(async (request) => {
try {
const token = await this.config.getAccessToken();
request.headers.Authorization = `Bearer ${token}`;
} catch (error) {
this.emit('auth:error', error);
throw new VisionFiError('Failed to get access token', 401, 'auth_error');
}
return request;
});
// Add response interceptor for error handling
this.apiClient.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 401) {
this.emit('auth:expired');
if (this.config.onTokenExpired) {
this.config.onTokenExpired();
}
}
return Promise.reject(error);
}
);
// Initialize client instances
this.packages = new PackageClient(this.apiClient);
this.admin = new AdminClient(this.apiClient);
}
/**
* Connect to the Cloud Run service
*/
async connect(): Promise<void> {
this.connectionStatus = 'connecting';
this.emit('connection:connecting');
try {
// Test connection with a simple request
await this.apiClient.get('/health');
this.connectionStatus = 'online';
this.emit('connection:online');
} catch (error) {
this.connectionStatus = 'offline';
this.emit('connection:offline', error);
throw new VisionFiError('Failed to connect to Cloud Run service', 503, 'connection_error');
}
}
/**
* Disconnect from the service
*/
async disconnect(): Promise<void> {
this.connectionStatus = 'offline';
this.emit('connection:offline');
}
/**
* Get current connection status
*/
getConnectionStatus(): ConnectionStatus {
return this.connectionStatus;
}
// Package Management Methods (delegated to managers for consistency)
/**
* Create a new package
* @deprecated Use client.packages.create() instead
*/
async createPackage(options: CreatePackageOptions): Promise<CreatePackageResponse> {
return this.packages.create(options);
}
/**
* List packages with optional filtering
* @deprecated Use client.packages.list() instead
*/
async listPackages(options?: ListPackagesOptions): Promise<ListPackagesResponse> {
return this.packages.list(options);
}
/**
* Get a specific package by ID
* @deprecated Use client.packages.get() instead
*/
async getPackage(packageId: string): Promise<PackageInfo> {
return this.packages.get(packageId);
}
// Document Analysis Methods
/**
* Analyze a document with upload progress tracking
*/
async analyzeDocument(
fileBuffer: Buffer,
options: AnalyzeDocumentOptions,
onProgress?: (progress: UploadProgress) => void
): Promise<AnalysisJob> {
try {
const fileBase64 = fileBuffer.toString('base64');
const totalBytes = fileBuffer.length;
let bytesUploaded = 0;
const startTime = Date.now();
// Simulate progress for demo (in real implementation, use axios upload progress)
if (onProgress) {
const progressInterval = setInterval(() => {
bytesUploaded = Math.min(bytesUploaded + totalBytes / 10, totalBytes);
const percentage = Math.round((bytesUploaded / totalBytes) * 100);
const elapsedTime = (Date.now() - startTime) / 1000;
const speed = bytesUploaded / elapsedTime;
const remainingTime = (totalBytes - bytesUploaded) / speed;
onProgress({
fileName: options.fileName,
bytesUploaded,
totalBytes,
percentage,
speed,
remainingTime
});
if (bytesUploaded >= totalBytes) {
clearInterval(progressInterval);
}
}, 100);
}
const response = await this.apiClient.post('/analyze', {
fileBase64,
...options
});
return response.data;
} catch (error) {
throw this.handleError(error, 'Failed to analyze document');
}
}
/**
* Get analysis results
*/
async getResults(jobUuid: string, pollInterval?: number, maxAttempts?: number): Promise<AnalysisResult> {
if (!pollInterval) {
try {
const response = await this.apiClient.get(`/results/${jobUuid}`);
return response.data;
} catch (error) {
throw this.handleError(error, 'Failed to get results');
}
}
// Polling implementation
const interval = pollInterval || 3000;
const attempts = maxAttempts || 20;
return new Promise((resolve, reject) => {
let attemptCount = 0;
const poll = async () => {
attemptCount++;
try {
const response = await this.apiClient.get(`/results/${jobUuid}`);
const result = response.data as AnalysisResult;
if (result.found === false && attemptCount < attempts) {
setTimeout(poll, interval);
return;
}
resolve(result);
} catch (error) {
reject(this.handleError(error, 'Error while polling for results'));
}
};
poll();
});
}
// Administrative Methods
/**
* Get available product types
* @deprecated Use client.admin.getProductTypes() instead
*/
async getProductTypes() {
return this.admin.getProductTypes();
}
/**
* Handle errors consistently
*/
private handleError(error: any, defaultMessage: string): Error {
if (error instanceof VisionFiError) {
return error;
}
const message = error.response?.data?.message || error.message || defaultMessage;
const statusCode = error.response?.status;
const code = error.response?.data?.code || 'unknown_error';
return new VisionFiError(message, statusCode, code);
}
}