@visionfi/desktop-sdk
Version:
Desktop SDK for VisionFI Cloud Run services with Azure AD authentication
209 lines (208 loc) • 7.26 kB
JavaScript
/**
* VisionFI Desktop Client
* Copyright (c) 2024-2025 VisionFI. All Rights Reserved.
*/
import { EventEmitter } from 'eventemitter3';
import axios from 'axios';
import { VisionFiError } from '@visionfi/core';
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 {
config;
apiClient;
connectionStatus = 'offline';
// Client instances for nested API
packages;
admin;
constructor(config) {
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() {
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() {
this.connectionStatus = 'offline';
this.emit('connection:offline');
}
/**
* Get current connection status
*/
getConnectionStatus() {
return this.connectionStatus;
}
// Package Management Methods (delegated to managers for consistency)
/**
* Create a new package
* @deprecated Use client.packages.create() instead
*/
async createPackage(options) {
return this.packages.create(options);
}
/**
* List packages with optional filtering
* @deprecated Use client.packages.list() instead
*/
async listPackages(options) {
return this.packages.list(options);
}
/**
* Get a specific package by ID
* @deprecated Use client.packages.get() instead
*/
async getPackage(packageId) {
return this.packages.get(packageId);
}
// Document Analysis Methods
/**
* Analyze a document with upload progress tracking
*/
async analyzeDocument(fileBuffer, options, onProgress) {
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, pollInterval, maxAttempts) {
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;
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
*/
handleError(error, defaultMessage) {
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);
}
}