This guide covers LiteChat's service layer, which encapsulates business logic and provides a clean interface between the UI and data layers. Services handle complex operations, coordinate between different parts of the system, and maintain separation of concerns.
Services in LiteChat follow these principles:
- Single Responsibility: Each service handles a specific domain area
- Event-Driven: Services communicate through the event system
- Stateless: Services don't maintain state; they read from stores and emit events
- Async by Default: Most service methods are asynchronous and return Promises
All services are located in src/services/ and follow the naming convention *.service.ts.
The PersistenceService provides a clean abstraction over the database layer (Dexie.js/IndexedDB).
- CRUD Operations: Create, read, update, delete for all entity types
- Transaction Support: Database transactions for data consistency
- Error Handling: Centralized error logging and handling
- Date Field Handling: Automatic conversion between Date objects and ISO strings
export class PersistenceService {
// Conversations
static async loadConversations(): Promise<Conversation[]>
static async saveConversation(conversation: Conversation): Promise<string>
static async deleteConversation(id: string): Promise<void>
// Interactions
static async loadInteractions(conversationId: string): Promise<Interaction[]>
static async saveInteraction(interaction: Interaction): Promise<string>
static async deleteInteraction(id: string): Promise<void>
// Projects
static async loadProjects(): Promise<Project[]>
static async saveProject(project: Project): Promise<string>
static async deleteProject(id: string): Promise<void>
// Provider Configurations
static async loadProviderConfigs(): Promise<DbProviderConfig[]>
static async saveProviderConfig(config: DbProviderConfig): Promise<string>
// Rules and Tags
static async loadRules(): Promise<DbRule[]>
static async saveRule(rule: DbRule): Promise<string>
static async loadTags(): Promise<DbTag[]>
static async saveTag(tag: DbTag): Promise<string>
// Import/Export
static async exportAllData(): Promise<FullExportData>
static async importAllData(data: FullExportData, options: FullImportOptions): Promise<void>
}// Services read state and trigger events, never modify state directly
class SomeService {
static async performOperation() {
try {
// Read current state
const currentData = someStore.getState().data
// Perform business logic
const result = await PersistenceService.saveData(processedData)
// Emit success event
eventEmitter.emit(someEvent.operationCompleted, { result })
} catch (error) {
// Emit error event
eventEmitter.emit(someEvent.operationFailed, { error: error.message })
}
}
}The AIService handles direct interaction with AI providers through the Vercel AI SDK.
- Streaming Responses: Real-time text generation with streaming
- Tool Execution: Coordinate AI tool calls with system tools
- Multi-Provider Support: Abstract interface for different AI providers
- Error Handling: Graceful handling of API errors and timeouts
export class AIService {
/**
* Stream a chat completion with tool support
*/
static async streamChatCompletion(
payload: AIPayload,
options: StreamingOptions
): Promise<void>
/**
* Generate a single completion (non-streaming)
*/
static async generateCompletion(
payload: AIPayload
): Promise<AIResponse>
/**
* Handle tool execution during streaming
*/
static async executeTools(
toolCalls: ToolCall[],
interactionId: string
): Promise<ToolResult[]>
}The ConversationService orchestrates conversation management and prompt submission.
- Prompt Processing: Apply rules, handle attachments, process context
- Title Generation: Automatic conversation titles
- Rule Application: Apply prompt engineering rules and templates
- File Context: Include file contents and VFS context in prompts
export class ConversationService {
/**
* Submit a new prompt, creating conversation and interaction
*/
static async submitPrompt(params: SubmitPromptParams): Promise<string>
/**
* Process prompt with rules and context
*/
static async processPrompt(
originalPrompt: string,
options: PromptProcessingOptions
): Promise<ProcessedPrompt>
/**
* Generate conversation title from first interaction
*/
static async generateConversationTitle(
conversationId: string
): Promise<void>
/**
* Apply rules to prompt content
*/
static applyRulesToPrompt(
prompt: string,
rules: DbRule[]
): string
}// Conversation service coordinates multiple concerns
const result = await ConversationService.submitPrompt({
prompt: userInput,
projectId: currentProject.id,
conversationId: selectedConversation?.id,
attachments: selectedFiles,
ruleIds: projectDefaults.ruleIds,
modelOverride: tempModelSelection
})The InteractionService manages the lifecycle of AI interactions from start to completion.
- Streaming Management: Handle real-time text streaming
- Tool Coordination: Execute and track tool calls
- State Tracking: Monitor interaction progress and completion
- Error Recovery: Handle failures and partial completions
export class InteractionService {
/**
* Start a new interaction with streaming
*/
static async startInteraction(
params: StartInteractionParams
): Promise<string>
/**
* Append streaming text chunk
*/
static appendTextChunk(
interactionId: string,
chunk: string
): void
/**
* Execute AI tool call
*/
static async executeToolCall(
interactionId: string,
toolCall: ToolCall
): Promise<ToolResult>
/**
* Mark interaction as complete
*/
static markInteractionComplete(
interactionId: string
): Promise<void>
/**
* Handle interaction errors
*/
static markInteractionAsError(
interactionId: string,
error: Error
): Promise<void>
}The ImportExportService handles data portability and backup functionality.
- Full Data Export: Complete application state export
- Selective Import: Choose which data types to import
- Conversation Export: Individual conversation exports
- Format Validation: Validate import data structure
export class ImportExportService {
/**
* Export all application data
*/
static async exportAllData(): Promise<FullExportData>
/**
* Import full configuration from file
*/
static async importFullConfiguration(
file: File,
options: FullImportOptions
): Promise<void>
/**
* Export single conversation with interactions
*/
static async exportConversation(
conversationId: string
): Promise<ConversationExport>
/**
* Import conversations from export file
*/
static async importConversations(
file: File,
targetProjectId?: string
): Promise<void>
}Handles Git repository synchronization with conversations.
export class SyncService {
/**
* Sync conversation to Git repository
*/
static async syncConversationToGit(
conversationId: string,
repoId: string
): Promise<void>
/**
* Pull latest changes from Git repository
*/
static async pullFromRepository(repoId: string): Promise<void>
/**
* Push local changes to Git repository
*/
static async pushToRepository(repoId: string): Promise<void>
}Handles batch synchronization operations.
export class BulkSyncService {
/**
* Perform bulk synchronization of multiple repos
*/
static async performBulkSync(options: BulkSyncOptions): Promise<void>
/**
* Stop any running bulk sync operation
*/
static abort(): void
/**
* Check if bulk sync is currently running
*/
static isActive(): boolean
}Fetches available models from AI providers.
export class ModelFetcher {
/**
* Fetch models for a provider configuration
*/
static async fetchModelsForProvider(
config: DbProviderConfig
): Promise<ModelListItem[]>
/**
* Update model cache for provider
*/
static async updateProviderModels(
providerId: string
): Promise<void>
}Handles image generation requests.
export interface ImageGenerationResult {
image: string // URL or base64 encoded image
finishReason: "stop" | "length" | "content-filter" | "other"
}
export class AIImageGenerationService {
static async generateImage(
prompt: string,
options: ImageGenerationOptions
): Promise<ImageGenerationResult>
}Central hub for routing action request events to store handlers.
export class EventActionCoordinatorService {
/**
* Register store action handlers
*/
static registerStore(handlers: RegisteredActionHandler[]): void
/**
* Initialize event listeners for all registered handlers
*/
static initialize(): void
/**
* Clean up event listeners
*/
static cleanup(): void
}Services use events for communication rather than direct method calls:
// Instead of direct calls
// someStore.updateData(newData) // DON'T DO THIS
// Use events
eventEmitter.emit(someEvent.updateDataRequest, { data: newData })
// Service listens for request events and emits result events
class SomeService {
static async handleUpdateRequest(payload: { data: any }) {
try {
const result = await this.processData(payload.data)
eventEmitter.emit(someEvent.dataUpdated, { result })
} catch (error) {
eventEmitter.emit(someEvent.updateFailed, { error: error.message })
}
}
}export class ServiceBase {
protected static async executeWithErrorHandling<T>(
operation: () => Promise<T>,
errorEvent: string,
context: string
): Promise<T | null> {
try {
return await operation()
} catch (error) {
console.error(`${context}:`, error)
eventEmitter.emit(errorEvent, {
error: error instanceof Error ? error.message : String(error)
})
return null
}
}
}
// Usage in services
class ExampleService extends ServiceBase {
static async performOperation(id: string): Promise<void> {
await this.executeWithErrorHandling(
() => this.doActualWork(id),
someEvent.operationFailed,
'ExampleService.performOperation'
)
}
}export class AsyncServicePattern {
/**
* Standard pattern for async operations with loading states
*/
static async performAsyncOperation(params: any): Promise<void> {
// Emit loading start
eventEmitter.emit(someEvent.loadingStateChanged, {
isLoading: true,
error: null
})
try {
// Perform actual work
const result = await this.doWork(params)
// Emit success
eventEmitter.emit(someEvent.operationCompleted, { result })
} catch (error) {
// Emit error
eventEmitter.emit(someEvent.operationFailed, {
error: error instanceof Error ? error.message : String(error)
})
} finally {
// Always emit loading end
eventEmitter.emit(someEvent.loadingStateChanged, {
isLoading: false
})
}
}
}// Services coordinate complex operations involving multiple domains
export class CoordinatedService {
static async performComplexOperation(params: ComplexParams): Promise<void> {
// Step 1: Prepare data
const processedData = await DataProcessingService.processInput(params.input)
// Step 2: Validate with another service
const isValid = await ValidationService.validate(processedData)
if (!isValid) {
throw new Error('Validation failed')
}
// Step 3: Persist changes
const savedId = await PersistenceService.saveData(processedData)
// Step 4: Trigger downstream operations
eventEmitter.emit(dataEvent.dataCreated, { id: savedId, data: processedData })
// Step 5: Optional cleanup or notifications
await NotificationService.notifyCompletion(savedId)
}
}import { describe, it, expect, beforeEach, jest } from '@jest/globals'
import { AIService } from './ai.service'
import { eventEmitter } from '../lib/event-emitter'
describe('AIService', () => {
beforeEach(() => {
jest.clearAllMocks()
})
it('should handle streaming responses', async () => {
const mockPayload = {
messages: [{ role: 'user', content: 'Hello' }],
model: 'gpt-3.5-turbo'
}
const onTextChunk = jest.fn()
const streamingOptions = { onTextChunk }
await AIService.streamChatCompletion(mockPayload, streamingOptions)
expect(onTextChunk).toHaveBeenCalled()
})
it('should emit error events on failure', async () => {
const errorSpy = jest.fn()
eventEmitter.on('ai.error', errorSpy)
// Test error scenario
await AIService.streamChatCompletion(invalidPayload, options)
expect(errorSpy).toHaveBeenCalledWith({
error: expect.stringContaining('Failed')
})
})
})describe('Service Integration', () => {
it('should coordinate conversation creation flow', async () => {
// Test full flow from prompt submission to completion
const conversationId = await ConversationService.submitPrompt({
prompt: 'Test prompt',
projectId: 'test-project'
})
expect(conversationId).toBeDefined()
// Verify conversation was created
const conversation = await PersistenceService.loadConversation(conversationId)
expect(conversation).toBeDefined()
expect(conversation.projectId).toBe('test-project')
})
})- Single Responsibility: Each service should handle one domain area
- Stateless: Services don't maintain state; they read from stores
- Event-Driven: Use events for communication, not direct method calls
- Error Handling: Always handle errors gracefully and emit appropriate events
- Async by Default: Most service methods should be async
- Clear Interfaces: Use TypeScript interfaces for parameters and return types
- Validation: Validate inputs and provide clear error messages
- Documentation: Document complex business logic and edge cases
- Loose Coupling: Services should depend on interfaces, not implementations
- Event Coordination: Use events to coordinate between services
- Error Isolation: One service failure shouldn't cascade to others
- Testing: Design services to be easily unit testable
- Batch Operations: Group related operations when possible
- Caching: Cache expensive computations appropriately
- Cancellation: Support operation cancellation for long-running tasks
- Resource Management: Clean up resources properly
This service layer architecture ensures clean separation of concerns, maintainable business logic, and efficient coordination between different parts of the LiteChat application.