Services Layer
Na tej stronie
Organizacja serwisów
Dział zatytułowany „Organizacja serwisów”Vista zawiera 50 serwisów zorganizowanych domenowo:
src/services/├── ai/ # 8 serwisów AI├── audio/ # 6 serwisów audio├── patients/ # 5 serwisów pacjentów├── visits/ # 7 serwisów wizyt├── auth/ # 4 serwisy auth├── calendar/ # 5 serwisów kalendarza├── settings/ # 3 serwisy ustawień├── utils/ # 12 utility services└── index.ts # Re-exportsKluczowe serwisy
Dział zatytułowany „Kluczowe serwisy”UnifiedAIClient
Dział zatytułowany „UnifiedAIClient”Centralny klient AI - abstrakcja nad wszystkimi providerami AI.
export class UnifiedAIClient { private serviceResolver: ServiceResolver; private retryConfig: RetryConfig; private cacheManager: ResponseCache;
/** * Chat completion - główna metoda LLM */ async chat(request: ChatRequest): Promise<ChatResponse> { // 1. Request validation const validatedRequest = this.validateChatRequest(request);
// 2. Provider selection with health check const provider = await this.serviceResolver.selectProvider('llm');
// 3. Request execution with retry logic const response = await this.executeWithRetry( () => provider.chat(validatedRequest), this.retryConfig );
// 4. Response caching and metrics await this.cacheManager.store(request, response); this.metrics.recordRequest(provider.name, response.latencyMs);
return response; }
/** * Speech-to-Text - transkrypcja audio */ async transcribe(audioFile: File): Promise<TranscriptionResponse> { const provider = await this.serviceResolver.selectProvider('stt'); return provider.transcribe(audioFile); }
/** * Text-to-Speech - synteza mowy */ async synthesize(text: string, voice?: string): Promise<AudioBuffer> { const provider = await this.serviceResolver.selectProvider('tts'); return provider.synthesize(text, voice); }}
// Singleton instanceexport const aiClient = new UnifiedAIClient();Provider Priority
Dział zatytułowany „Provider Priority”sequenceDiagram participant UI as Frontend UI participant UAI as Unified AI Client participant SR as Service Resolver participant LIB as LibraxisAI Cloud participant MLX as Local MLX participant OAI as OpenAI API
UI->>UAI: AI Request UAI->>SR: Resolve Provider Chain
Note over SR: Provider Priority:<br/>1. LibraxisAI<br/>2. Local MLX<br/>3. OpenAI
SR->>LIB: Try Primary alt LibraxisAI Success LIB->>SR: Response SR->>UAI: Success else LibraxisAI Failure SR->>MLX: Try Secondary alt MLX Success MLX->>SR: Response SR->>UAI: Success else MLX Failure SR->>OAI: Try Tertiary OAI->>SR: Response SR->>UAI: Success end end UAI->>UI: ResultpatientService
Dział zatytułowany „patientService”Zarządzanie pacjentami - CRUD + wyszukiwanie.
export const patientService = { /** * Pobierz pacjenta po ID */ async getById(patientId: string): Promise<Patient> { return safeInvoke('get_patient', { patientId }); },
/** * Lista pacjentów z paginacją */ async list(options: ListOptions): Promise<PaginatedResult<Patient>> { return safeInvoke('list_patients', { page: options.page, limit: options.limit, sortBy: options.sortBy, sortOrder: options.sortOrder, }); },
/** * Wyszukiwanie pacjentów */ async search(query: string): Promise<Patient[]> { if (query.length < 2) return []; return safeInvoke('search_patients', { query }); },
/** * Utwórz nowego pacjenta */ async create(data: CreatePatientData): Promise<Patient> { const validated = PatientSchema.parse(data); return safeInvoke('create_patient', validated); },
/** * Aktualizuj pacjenta */ async update(patientId: string, updates: Partial<Patient>): Promise<Patient> { return safeInvoke('update_patient', { patientId, updates }); },
/** * Usuń pacjenta */ async delete(patientId: string): Promise<void> { return safeInvoke('delete_patient', { patientId }); },
/** * Historia wizyt pacjenta */ async getVisitHistory(patientId: string): Promise<Visit[]> { return safeInvoke('get_patient_visits', { patientId }); },
/** * Sprawdź duplikaty (identity matching) */ async checkDuplicates(data: PatientIdentity): Promise<Patient[]> { return safeInvoke('find_duplicate_patients', data); },};visitService
Dział zatytułowany „visitService”Zarządzanie wizytami - główny workflow medyczny.
export const visitService = { /** * Pobierz wizytę po ID */ async getById(visitId: string): Promise<Visit> { return safeInvoke('get_visit', { visitId }); },
/** * Lista wizyt użytkownika */ async listForUser(userId: string, filters?: VisitFilters): Promise<Visit[]> { return safeInvoke('list_user_visits', { userId, filters }); },
/** * Utwórz nową wizytę */ async create(data: CreateVisitData): Promise<Visit> { const validated = CreateVisitSchema.parse(data); return safeInvoke('create_visit', validated); },
/** * Aktualizuj SOAP z optimistic locking */ async updateSOAP( visitId: string, soapData: SOAPData, expectedVersion: number ): Promise<Visit> { return safeInvoke('update_visit_soap', { visitId, soapData, expectedVersion, }); },
/** * Finalizuj wizytę */ async finalize(visitId: string): Promise<Visit> { return safeInvoke('finalize_visit', { visitId }); },
/** * Eksportuj wizytę do PDF */ async exportToPDF(visitId: string): Promise<string> { return safeInvoke('export_visit_pdf', { visitId }); },
/** * Wyślij wizytę emailem */ async sendEmail(visitId: string, recipientEmail: string): Promise<void> { return safeInvoke('send_visit_email', { visitId, recipientEmail }); },};audioService
Dział zatytułowany „audioService”Obsługa audio - nagrywanie, transkrypcja, storage.
export const audioService = { /** * Zapisz nagranie audio */ async saveRecording( visitId: string, audioBlob: Blob ): Promise<RecordingMetadata> { const base64 = await blobToBase64(audioBlob); return safeInvoke('save_audio_file', { visitId, audioData: base64, format: audioBlob.type, }); },
/** * Rozpocznij transkrypcję */ async startTranscription( recordingId: string, options?: TranscriptionOptions ): Promise<TranscriptionJob> { return safeInvoke('start_transcription', { recordingId, provider: options?.provider ?? 'auto', language: options?.language ?? 'pl', }); },
/** * Pobierz status transkrypcji */ async getTranscriptionStatus(jobId: string): Promise<TranscriptionStatus> { return safeInvoke('get_transcription_status', { jobId }); },
/** * Pobierz plik audio */ async getAudioFile(recordingId: string): Promise<ArrayBuffer> { return safeInvoke('get_audio_file', { recordingId }); },
/** * Usuń nagranie */ async deleteRecording(recordingId: string): Promise<void> { return safeInvoke('delete_recording', { recordingId }); },};authService
Dział zatytułowany „authService”Autoryzacja i sesje.
export const authService = { /** * Logowanie hasłem */ async login(email: string, password: string): Promise<LoginResponse> { return safeInvoke('login', { email, password }); },
/** * Logowanie biometrią */ async loginWithBiometric(): Promise<LoginResponse> { return safeInvoke('login_biometric', {}); },
/** * Wylogowanie */ async logout(): Promise<void> { return safeInvoke('logout', {}); },
/** * Odśwież sesję */ async refreshSession(): Promise<Session> { return safeInvoke('refresh_session', {}); },
/** * Zmiana hasła */ async changePassword( currentPassword: string, newPassword: string ): Promise<void> { return safeInvoke('change_password', { currentPassword, newPassword }); },
/** * Sprawdź aktywną sesję */ async getActiveSession(): Promise<Session | null> { try { return await safeInvoke('get_active_session', {}); } catch { return null; } },};appointmentService
Dział zatytułowany „appointmentService”Zarządzanie wizytami w kalendarzu.
export const appointmentService = { /** * Lista wizyt dla weterynarza */ async listForVet( vetId: string, dateRange: DateRange ): Promise<Appointment[]> { return safeInvoke('list_vet_appointments', { vetId, startDate: dateRange.start.toISOString(), endDate: dateRange.end.toISOString(), }); },
/** * Utwórz wizytę */ async create(data: CreateAppointmentData): Promise<Appointment> { // Sprawdź konflikty przed utworzeniem const conflicts = await this.checkConflicts( data.veterinarianId, data.scheduledDate, data.scheduledTime, data.durationMinutes );
if (conflicts.hasConflicts) { throw new ConflictError(conflicts); }
return safeInvoke('create_appointment', data); },
/** * Sprawdź konflikty czasowe */ async checkConflicts( vetId: string, date: string, time: string, duration: number ): Promise<ConflictResult> { return safeInvoke('check_appointment_conflicts', { veterinarianId: vetId, date, startTime: time, durationMinutes: duration, }); },
/** * Zmień status wizyty */ async updateStatus( appointmentId: string, status: AppointmentStatus ): Promise<Appointment> { return safeInvoke('update_appointment_status', { appointmentId, status, }); },
/** * Konwertuj wizytę na konsultację */ async convertToVisit(appointmentId: string): Promise<Visit> { return safeInvoke('convert_appointment_to_visit', { appointmentId }); },};Utility Services
Dział zatytułowany „Utility Services”errorHandlingService
Dział zatytułowany „errorHandlingService”Centralne zarządzanie błędami.
interface ErrorContext { category: 'auth' | 'database' | 'ai' | 'audio' | 'network'; severity: 'low' | 'medium' | 'high' | 'critical'; context: Record<string, any>; showToast?: boolean; logToFile?: boolean;}
export const handleError = (error: Error, context: ErrorContext) => { // 1. Error classification const classification = classifyError(error, context);
// 2. Logging strategy if (classification.severity >= 'medium') { secureLogger.logError(error, context); }
// 3. User notification if (context.showToast !== false) { notificationService.showError( getLocalizedErrorMessage(error, context) ); }
// 4. Recovery suggestions if (classification.canRecover) { return classification.recoveryActions; }
return null;};secureLogger
Dział zatytułowany „secureLogger”Bezpieczne logowanie (bez danych wrażliwych).
export const secureLogger = { /** * Loguj info (bez danych wrażliwych) */ info(message: string, context?: object): void { const sanitized = sanitizeLogData(context); console.info(`[INFO] ${message}`, sanitized); },
/** * Loguj błąd */ error(error: Error, context?: ErrorContext): void { const sanitized = sanitizeLogData(context); console.error(`[ERROR] ${error.message}`, sanitized);
// Opcjonalnie: zapisz do pliku if (context?.logToFile) { safeInvoke('log_error_to_file', { message: error.message, stack: error.stack, context: sanitized, }); } },
/** * Sanityzuj dane przed logowaniem */ sanitize<T extends object>(data: T): T { return sanitizeLogData(data); },};
function sanitizeLogData<T extends object>(data?: T): T | undefined { if (!data) return data;
const sensitiveKeys = [ 'password', 'token', 'api_key', 'apiKey', 'secret', 'authorization', 'cookie', ];
const sanitized = { ...data };
for (const key of Object.keys(sanitized)) { if (sensitiveKeys.some(sk => key.toLowerCase().includes(sk))) { (sanitized as any)[key] = '[REDACTED]'; } }
return sanitized;}Service Patterns
Dział zatytułowany „Service Patterns”Safe Invoke Pattern
Dział zatytułowany „Safe Invoke Pattern”export async function safeInvoke<T>( command: string, args?: Record<string, unknown>): Promise<T> { try { return await invoke<T>(command, args); } catch (error) { // Log error secureLogger.error(error as Error, { category: 'network', severity: 'medium', context: { command }, });
// Transform to user-friendly message throw new Error(getLocalizedErrorMessage(error)); }}Retry Pattern
Dział zatytułowany „Retry Pattern”interface RetryConfig { maxAttempts: number; delayMs: number; backoffMultiplier: number; retryOn: (error: Error) => boolean;}
export async function withRetry<T>( fn: () => Promise<T>, config: RetryConfig): Promise<T> { let lastError: Error; let delay = config.delayMs;
for (let attempt = 1; attempt <= config.maxAttempts; attempt++) { try { return await fn(); } catch (error) { lastError = error as Error;
if (!config.retryOn(lastError) || attempt === config.maxAttempts) { throw lastError; }
await sleep(delay); delay *= config.backoffMultiplier; } }
throw lastError!;}Cache Pattern
Dział zatytułowany „Cache Pattern”interface CacheEntry<T> { data: T; timestamp: number; ttl: number;}
export class ServiceCache<T> { private cache = new Map<string, CacheEntry<T>>();
get(key: string): T | null { const entry = this.cache.get(key);
if (!entry) return null;
if (Date.now() - entry.timestamp > entry.ttl) { this.cache.delete(key); return null; }
return entry.data; }
set(key: string, data: T, ttlMs: number): void { this.cache.set(key, { data, timestamp: Date.now(), ttl: ttlMs, }); }
invalidate(key: string): void { this.cache.delete(key); }
clear(): void { this.cache.clear(); }}Testing Services
Dział zatytułowany „Testing Services”import { patientService } from '../patients/patientService';import { vi } from 'vitest';
vi.mock('@tauri-apps/api/core', () => ({ invoke: vi.fn(),}));
describe('patientService', () => { describe('search', () => { it('returns empty array for short queries', async () => { const result = await patientService.search('B'); expect(result).toEqual([]); });
it('calls backend for valid queries', async () => { const mockPatients = [{ patient_id: '1', name: 'Burek' }]; vi.mocked(invoke).mockResolvedValue(mockPatients);
const result = await patientService.search('Burek');
expect(invoke).toHaveBeenCalledWith('search_patients', { query: 'Burek', }); expect(result).toEqual(mockPatients); }); });});