# @aichatkit/storage-adapter

Base storage adapter abstract class for Hypermode ChatKit with backend synchronization support.

## Installation

```bash
npm install @aichatkit/storage-adapter
```

## Usage

This package provides the base abstract class for implementing storage adapters. You typically won't
use this package directly, but rather create or use an implementation like
`@aichatkit/localstorage-adapter`.

```typescript
import { StorageAdapter } from "@aichatkit/storage-adapter"
import { Conversation, Message } from "@aichatkit/types"

// Example: Create a custom adapter implementation
class CustomStorageAdapter extends StorageAdapter {
  private conversations: Map<string, Conversation> = new Map()
  private agentMapping: Map<string, string> = new Map()

  async saveConversation(conversation: Conversation): Promise<void> {
    this.conversations.set(conversation.id, { ...conversation })
  }

  async getConversation(id: string): Promise<Conversation | null> {
    return this.conversations.get(id) || null
  }

  async getAllConversations(): Promise<Conversation[]> {
    return Array.from(this.conversations.values())
  }

  async deleteConversation(id: string): Promise<boolean> {
    const deleted = this.conversations.delete(id)
    this.agentMapping.delete(id)
    return deleted
  }

  async addMessage(conversationId: string, message: Message): Promise<Conversation | null> {
    const conversation = this.conversations.get(conversationId)
    if (!conversation) return null

    conversation.messages.push(message)
    await this.saveConversation(conversation)
    return conversation
  }

  async getConversationHistory(conversationId: string): Promise<Message[]> {
    // Check if we have network callbacks for backend sync
    const agentId = await this.getConversationAgent(conversationId)

    if (agentId && this.callbacks?.getConversationHistory) {
      try {
        const backendMessages = await this.callbacks.getConversationHistory(agentId)

        // Update local storage with backend data
        const conversation = await this.getConversation(conversationId)
        if (conversation) {
          conversation.messages = backendMessages
          await this.saveConversation(conversation)
        }

        return backendMessages
      } catch (error) {
        console.error("Backend sync failed, using local data:", error)
      }
    }

    // Fall back to local data
    const conversation = await this.getConversation(conversationId)
    return conversation?.messages || []
  }

  async clearConversationHistory(conversationId: string): Promise<void> {
    const agentId = await this.getConversationAgent(conversationId)

    // Clear on backend if possible
    if (agentId && this.callbacks?.clearConversationHistory) {
      try {
        await this.callbacks.clearConversationHistory(agentId)
      } catch (error) {
        console.error("Failed to clear backend history:", error)
      }
    }

    // Clear locally
    const conversation = await this.getConversation(conversationId)
    if (conversation) {
      conversation.messages = []
      await this.saveConversation(conversation)
    }
  }

  async setConversationAgent(conversationId: string, agentId: string): Promise<void> {
    this.agentMapping.set(conversationId, agentId)
  }

  async getConversationAgent(conversationId: string): Promise<string | null> {
    return this.agentMapping.get(conversationId) || null
  }
}
```

## Abstract Methods

All storage adapters must implement these core methods:

### saveConversation(conversation: Conversation): Promise<void>

Saves a conversation to storage.

```typescript
abstract saveConversation(conversation: Conversation): Promise<void>;
```

**Parameters**:

- `conversation`: The conversation object to save

**Example**:

```typescript
await adapter.saveConversation({
  id: "conv-1",
  title: "My Chat",
  messages: [],
})
```

### getConversation(id: string): Promise<Conversation | null>

Retrieves a conversation from storage by ID.

```typescript
abstract getConversation(id: string): Promise<Conversation | null>;
```

**Parameters**:

- `id`: ID of the conversation to retrieve

**Returns**: Promise resolving to the conversation or null if not found

### getAllConversations(): Promise<Conversation[]>

Retrieves all conversations from storage.

```typescript
abstract getAllConversations(): Promise<Conversation[]>;
```

**Returns**: Promise resolving to an array of all conversations

### deleteConversation(id: string): Promise<boolean>

Deletes a conversation from storage.

```typescript
abstract deleteConversation(id: string): Promise<boolean>;
```

**Parameters**:

- `id`: ID of the conversation to delete

**Returns**: Promise resolving to true if successful, false otherwise

### addMessage(conversationId: string, message: Message): Promise<Conversation | null>

Adds a message to a conversation.

```typescript
abstract addMessage(conversationId: string, message: Message): Promise<Conversation | null>;
```

**Parameters**:

- `conversationId`: ID of the conversation
- `message`: Message to add

**Returns**: Promise resolving to the updated conversation or null if not found

## Backend Synchronization Methods

These methods enable synchronization with backend agents:

### getConversationHistory(conversationId: string): Promise<Message[]>

Gets conversation history with backend synchronization.

```typescript
abstract getConversationHistory(conversationId: string): Promise<Message[]>;
```

This method should:

1. Check if a backend agent exists for the conversation
2. Sync with backend if available
3. Fall back to local storage if backend sync fails

### clearConversationHistory(conversationId: string): Promise<void>

Clears conversation history both locally and on backend.

```typescript
abstract clearConversationHistory(conversationId: string): Promise<void>;
```

### setConversationAgent(conversationId: string, agentId: string): Promise<void>

Maps a conversation to a backend agent ID.

```typescript
abstract setConversationAgent(conversationId: string, agentId: string): Promise<void>;
```

### getConversationAgent(conversationId: string): Promise<string | null>

Gets the backend agent ID for a conversation.

```typescript
abstract getConversationAgent(conversationId: string): Promise<string | null>;
```

## Optional Methods

### initialize(config?: Record<string, any>): Promise<void>

Optional initialization method.

```typescript
async initialize(config?: Record<string, any>): Promise<void> {
  return Promise.resolve();
}
```

### syncAllConversationsWithBackend(): Promise<void>

Syncs all conversations with backend (called on app load).

```typescript
async syncAllConversationsWithBackend?(): Promise<void> {
  return Promise.resolve();
}
```

## Network Callbacks

Storage adapters can receive network callbacks for backend synchronization:

### StorageAdapterCallbacks Interface

```typescript
interface StorageAdapterCallbacks {
  getConversationHistory?: (agentId: string) => Promise<Message[]>
  clearConversationHistory?: (agentId: string) => Promise<void>
}
```

### setNetworkCallbacks(callbacks: StorageAdapterCallbacks): void

Sets network callbacks for backend communication.

```typescript
import { NetworkAdapter } from "@aichatkit/network-adapter"

const networkAdapter = new SomeNetworkAdapter()
const storageAdapter = new SomeStorageAdapter()

// Connect storage with network for syncing
storageAdapter.setNetworkCallbacks({
  getConversationHistory: (agentId) => networkAdapter.getConversationHistory(agentId),
  clearConversationHistory: (agentId) => networkAdapter.clearConversationHistory(agentId),
})
```

## Data Types

### Conversation

```typescript
interface Conversation {
  id: string
  title: string
  messages: Message[]
}
```

### Message

```typescript
interface Message {
  id: string | number
  content: string
  role: "user" | "assistant"
  timestamp?: string
}
```

## Agent-Conversation Mapping

Storage adapters maintain a mapping between conversations and backend agents:

```typescript
// When creating a new conversation with an agent
await storageAdapter.setConversationAgent("conv-1", "agent-123")

// When retrieving the agent for a conversation
const agentId = await storageAdapter.getConversationAgent("conv-1")

// When deleting a conversation, also remove the agent mapping
await storageAdapter.deleteConversation("conv-1") // Should also remove agent mapping
```

## Synchronization Flow

1. **App Initialization**: Call `syncAllConversationsWithBackend()` to get latest state
2. **Conversation Switch**: Sync specific conversation when user switches to it
3. **Message History**: Always try backend first, fall back to local storage
4. **Background Sync**: Periodically sync to keep data fresh

### Example Sync Implementation

```typescript
async getConversationHistory(conversationId: string): Promise<Message[]> {
  const agentId = await this.getConversationAgent(conversationId);

  if (agentId && this.callbacks?.getConversationHistory) {
    try {
      // Try to get latest from backend
      const backendMessages = await this.callbacks.getConversationHistory(agentId);

      // Update local storage with backend data
      const conversation = await this.getConversation(conversationId);
      if (conversation) {
        conversation.messages = backendMessages;
        await this.saveConversation(conversation);
      }

      return backendMessages;
    } catch (error) {
      console.error('Backend sync failed:', error);
      // Continue to local fallback
    }
  }

  // Fall back to local storage
  const conversation = await this.getConversation(conversationId);
  return conversation?.messages || [];
}
```

## Available Implementations

- **[@aichatkit/localstorage-adapter](../localstorage-adapter)**: Browser localStorage
  implementation
- **Custom implementations**: Create your own for databases, cloud storage, etc.

## Testing

Mock storage adapters for testing:

```typescript
class MockStorageAdapter extends StorageAdapter {
  private data: Map<string, Conversation> = new Map()

  async saveConversation(conversation: Conversation): Promise<void> {
    this.data.set(conversation.id, { ...conversation })
  }

  async getConversation(id: string): Promise<Conversation | null> {
    return this.data.get(id) || null
  }

  // ... implement other methods
}
```

## License

[MIT](../../LICENSE) © Hypermode
