@prmichaelsen/acp-visualizer 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -0
- package/agent/commands/acp.clarification-address.md +417 -0
- package/agent/commands/acp.clarification-capture.md +386 -0
- package/agent/commands/acp.clarification-create.md +437 -0
- package/agent/commands/acp.clarifications-research.md +326 -0
- package/agent/commands/acp.command-create.md +432 -0
- package/agent/commands/acp.design-create.md +286 -0
- package/agent/commands/acp.design-reference.md +355 -0
- package/agent/commands/acp.handoff.md +270 -0
- package/agent/commands/acp.index.md +423 -0
- package/agent/commands/acp.init.md +546 -0
- package/agent/commands/acp.package-create.md +895 -0
- package/agent/commands/acp.package-info.md +212 -0
- package/agent/commands/acp.package-install.md +539 -0
- package/agent/commands/acp.package-list.md +280 -0
- package/agent/commands/acp.package-publish.md +541 -0
- package/agent/commands/acp.package-remove.md +293 -0
- package/agent/commands/acp.package-search.md +307 -0
- package/agent/commands/acp.package-update.md +361 -0
- package/agent/commands/acp.package-validate.md +540 -0
- package/agent/commands/acp.pattern-create.md +386 -0
- package/agent/commands/acp.plan.md +587 -0
- package/agent/commands/acp.proceed.md +882 -0
- package/agent/commands/acp.project-create.md +675 -0
- package/agent/commands/acp.project-info.md +312 -0
- package/agent/commands/acp.project-list.md +226 -0
- package/agent/commands/acp.project-remove.md +379 -0
- package/agent/commands/acp.project-set.md +227 -0
- package/agent/commands/acp.project-update.md +307 -0
- package/agent/commands/acp.projects-restore.md +228 -0
- package/agent/commands/acp.projects-sync.md +347 -0
- package/agent/commands/acp.report.md +407 -0
- package/agent/commands/acp.resume.md +239 -0
- package/agent/commands/acp.sessions.md +301 -0
- package/agent/commands/acp.status.md +293 -0
- package/agent/commands/acp.sync.md +364 -0
- package/agent/commands/acp.task-create.md +500 -0
- package/agent/commands/acp.update.md +302 -0
- package/agent/commands/acp.validate.md +466 -0
- package/agent/commands/acp.version-check-for-updates.md +276 -0
- package/agent/commands/acp.version-check.md +191 -0
- package/agent/commands/acp.version-update.md +289 -0
- package/agent/commands/command.template.md +339 -0
- package/agent/commands/git.commit.md +526 -0
- package/agent/commands/git.init.md +514 -0
- package/agent/commands/tanstack-cloudflare.deploy.md +272 -0
- package/agent/commands/tanstack-cloudflare.tail.md +275 -0
- package/agent/design/.gitkeep +0 -0
- package/agent/design/design.template.md +154 -0
- package/agent/design/local.dashboard-layout-routing.md +288 -0
- package/agent/design/local.data-model-yaml-parsing.md +310 -0
- package/agent/design/local.search-filtering.md +331 -0
- package/agent/design/local.server-api-auto-refresh.md +235 -0
- package/agent/design/local.table-tree-views.md +299 -0
- package/agent/design/local.visualizer-requirements.md +349 -0
- package/agent/design/requirements.template.md +387 -0
- package/agent/index/.gitkeep +0 -0
- package/agent/index/acp.core.yaml +137 -0
- package/agent/index/local.main.template.yaml +37 -0
- package/agent/manifest.template.yaml +13 -0
- package/agent/manifest.yaml +302 -0
- package/agent/milestones/.gitkeep +0 -0
- package/agent/milestones/milestone-1-project-scaffold-data-pipeline.md +67 -0
- package/agent/milestones/milestone-1-{title}.template.md +206 -0
- package/agent/milestones/milestone-2-dashboard-views-interaction.md +79 -0
- package/agent/package.template.yaml +86 -0
- package/agent/patterns/.gitkeep +0 -0
- package/agent/patterns/bootstrap.template.md +1237 -0
- package/agent/patterns/pattern.template.md +382 -0
- package/agent/patterns/tanstack-cloudflare.acl-permissions.md +332 -0
- package/agent/patterns/tanstack-cloudflare.action-bar-item.md +416 -0
- package/agent/patterns/tanstack-cloudflare.api-route-handlers.md +401 -0
- package/agent/patterns/tanstack-cloudflare.auth-session-management.md +387 -0
- package/agent/patterns/tanstack-cloudflare.card-and-list.md +271 -0
- package/agent/patterns/tanstack-cloudflare.chat-engine.md +353 -0
- package/agent/patterns/tanstack-cloudflare.confirmation-tokens.md +346 -0
- package/agent/patterns/tanstack-cloudflare.durable-objects-websocket.md +516 -0
- package/agent/patterns/tanstack-cloudflare.email-service.md +431 -0
- package/agent/patterns/tanstack-cloudflare.expander.md +98 -0
- package/agent/patterns/tanstack-cloudflare.fcm-push.md +115 -0
- package/agent/patterns/tanstack-cloudflare.firebase-anonymous-sessions.md +441 -0
- package/agent/patterns/tanstack-cloudflare.firebase-auth.md +348 -0
- package/agent/patterns/tanstack-cloudflare.firebase-firestore.md +550 -0
- package/agent/patterns/tanstack-cloudflare.firebase-storage.md +369 -0
- package/agent/patterns/tanstack-cloudflare.form-controls.md +145 -0
- package/agent/patterns/tanstack-cloudflare.global-search-context.md +93 -0
- package/agent/patterns/tanstack-cloudflare.image-carousel.md +126 -0
- package/agent/patterns/tanstack-cloudflare.library-services.md +553 -0
- package/agent/patterns/tanstack-cloudflare.lightbox.md +169 -0
- package/agent/patterns/tanstack-cloudflare.markdown-content.md +115 -0
- package/agent/patterns/tanstack-cloudflare.mention-suggestions.md +98 -0
- package/agent/patterns/tanstack-cloudflare.modal.md +156 -0
- package/agent/patterns/tanstack-cloudflare.nextjs-to-tanstack-routing.md +461 -0
- package/agent/patterns/tanstack-cloudflare.notifications-engine.md +151 -0
- package/agent/patterns/tanstack-cloudflare.oauth-token-refresh.md +90 -0
- package/agent/patterns/tanstack-cloudflare.og-metadata.md +296 -0
- package/agent/patterns/tanstack-cloudflare.pagination.md +442 -0
- package/agent/patterns/tanstack-cloudflare.pill-input.md +220 -0
- package/agent/patterns/tanstack-cloudflare.provider-adapter.md +401 -0
- package/agent/patterns/tanstack-cloudflare.rate-limiting.md +323 -0
- package/agent/patterns/tanstack-cloudflare.scheduled-tasks.md +338 -0
- package/agent/patterns/tanstack-cloudflare.searchable-settings.md +375 -0
- package/agent/patterns/tanstack-cloudflare.slide-over.md +129 -0
- package/agent/patterns/tanstack-cloudflare.ssr-preload.md +571 -0
- package/agent/patterns/tanstack-cloudflare.third-party-api-integration.md +508 -0
- package/agent/patterns/tanstack-cloudflare.toast-system.md +142 -0
- package/agent/patterns/tanstack-cloudflare.unified-header.md +280 -0
- package/agent/patterns/tanstack-cloudflare.user-scoped-collections.md +628 -0
- package/agent/patterns/tanstack-cloudflare.websocket-manager.md +237 -0
- package/agent/patterns/tanstack-cloudflare.wrangler-configuration.md +358 -0
- package/agent/patterns/tanstack-cloudflare.zod-schema-validation.md +336 -0
- package/agent/progress.template.yaml +161 -0
- package/agent/progress.yaml +145 -0
- package/agent/schemas/package.schema.yaml +276 -0
- package/agent/scripts/acp.common.sh +1781 -0
- package/agent/scripts/acp.install.sh +333 -0
- package/agent/scripts/acp.package-create.sh +924 -0
- package/agent/scripts/acp.package-info.sh +288 -0
- package/agent/scripts/acp.package-install.sh +893 -0
- package/agent/scripts/acp.package-list.sh +311 -0
- package/agent/scripts/acp.package-publish.sh +420 -0
- package/agent/scripts/acp.package-remove.sh +348 -0
- package/agent/scripts/acp.package-search.sh +156 -0
- package/agent/scripts/acp.package-update.sh +517 -0
- package/agent/scripts/acp.package-validate.sh +1018 -0
- package/agent/scripts/acp.uninstall.sh +85 -0
- package/agent/scripts/acp.version-check-for-updates.sh +98 -0
- package/agent/scripts/acp.version-check.sh +47 -0
- package/agent/scripts/acp.version-update.sh +176 -0
- package/agent/scripts/acp.yaml-parser.sh +985 -0
- package/agent/scripts/acp.yaml-validate.sh +205 -0
- package/agent/tasks/.gitkeep +0 -0
- package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-1-initialize-tanstack-start-project.md +210 -0
- package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-2-implement-data-model-yaml-parser.md +294 -0
- package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-3-build-server-api-data-loading.md +193 -0
- package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-4-add-auto-refresh-sse.md +262 -0
- package/agent/tasks/milestone-2-dashboard-views-interaction/task-10-polish-integration-testing.md +156 -0
- package/agent/tasks/milestone-2-dashboard-views-interaction/task-5-build-dashboard-layout-routing.md +178 -0
- package/agent/tasks/milestone-2-dashboard-views-interaction/task-6-build-overview-page.md +141 -0
- package/agent/tasks/milestone-2-dashboard-views-interaction/task-7-implement-milestone-table-view.md +153 -0
- package/agent/tasks/milestone-2-dashboard-views-interaction/task-8-implement-milestone-tree-view.md +174 -0
- package/agent/tasks/milestone-2-dashboard-views-interaction/task-9-implement-search-filtering.md +233 -0
- package/agent/tasks/task-1-{title}.template.md +244 -0
- package/bin/visualize.mjs +84 -0
- package/package.json +48 -0
- package/src/components/ExtraFieldsBadge.tsx +15 -0
- package/src/components/FilterBar.tsx +33 -0
- package/src/components/Header.tsx +23 -0
- package/src/components/MilestoneTable.tsx +167 -0
- package/src/components/MilestoneTree.tsx +84 -0
- package/src/components/ProgressBar.tsx +20 -0
- package/src/components/SearchInput.tsx +22 -0
- package/src/components/Sidebar.tsx +54 -0
- package/src/components/StatusBadge.tsx +23 -0
- package/src/components/StatusDot.tsx +12 -0
- package/src/components/TaskList.tsx +36 -0
- package/src/components/ViewToggle.tsx +31 -0
- package/src/lib/config.ts +8 -0
- package/src/lib/file-watcher.ts +43 -0
- package/src/lib/search.ts +48 -0
- package/src/lib/types.ts +73 -0
- package/src/lib/useAutoRefresh.ts +31 -0
- package/src/lib/useCollapse.ts +31 -0
- package/src/lib/useFilteredData.ts +55 -0
- package/src/lib/yaml-loader-real.spec.ts +47 -0
- package/src/lib/yaml-loader.spec.ts +201 -0
- package/src/lib/yaml-loader.ts +265 -0
- package/src/routeTree.gen.ts +140 -0
- package/src/router.tsx +10 -0
- package/src/routes/__root.tsx +75 -0
- package/src/routes/api/watch.ts +29 -0
- package/src/routes/index.tsx +115 -0
- package/src/routes/milestones.tsx +50 -0
- package/src/routes/search.tsx +84 -0
- package/src/routes/tasks.tsx +63 -0
- package/src/services/progress-database.service.ts +46 -0
- package/src/styles.css +25 -0
- package/tsconfig.json +24 -0
- package/vite.config.ts +16 -0
- package/vitest.config.ts +27 -0
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
# Provider Adapter Pattern
|
|
2
|
+
|
|
3
|
+
**Category**: Architecture
|
|
4
|
+
**Applicable To**: TanStack Start + Cloudflare Workers applications with pluggable backends
|
|
5
|
+
**Status**: Stable
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
The Provider Adapter pattern defines TypeScript interfaces for external dependencies (AI providers, storage backends, messaging protocols, vision APIs) and injects concrete implementations at construction time. This enables the core business logic to remain portable, testable, and provider-agnostic while allowing implementations to be swapped without code changes.
|
|
12
|
+
|
|
13
|
+
By defining contracts as interfaces and injecting implementations, you can:
|
|
14
|
+
- Swap AI providers (Bedrock → OpenAI) without changing chat logic
|
|
15
|
+
- Swap storage (Firebase → Postgres) without changing services
|
|
16
|
+
- Mock all dependencies for unit testing
|
|
17
|
+
- Extract core logic into reusable packages
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## When to Use This Pattern
|
|
22
|
+
|
|
23
|
+
✅ **Use this pattern when:**
|
|
24
|
+
- Building features that depend on external services (AI, storage, messaging)
|
|
25
|
+
- Want to swap implementations without changing business logic
|
|
26
|
+
- Need testable code with mockable dependencies
|
|
27
|
+
- Building portable libraries that could be extracted into packages
|
|
28
|
+
- Working with multiple providers for the same capability
|
|
29
|
+
|
|
30
|
+
❌ **Don't use this pattern when:**
|
|
31
|
+
- Only one implementation will ever exist (over-engineering)
|
|
32
|
+
- The dependency is trivial and unlikely to change
|
|
33
|
+
- Performance is critical and interface indirection adds measurable overhead
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Core Principles
|
|
38
|
+
|
|
39
|
+
1. **Interface First**: Define the interface before writing any implementation
|
|
40
|
+
2. **Single Responsibility**: Each interface covers one capability (AI, storage, MCP, vision)
|
|
41
|
+
3. **Constructor Injection**: Implementations injected at construction time, not imported directly
|
|
42
|
+
4. **Param Objects**: Methods accept typed parameter objects, not positional arguments
|
|
43
|
+
5. **Callback-Based Streaming**: Use `onMessage` callbacks for streaming data rather than returning streams
|
|
44
|
+
6. **Optional Methods**: Use `method?` syntax for capabilities not all providers support
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Implementation
|
|
49
|
+
|
|
50
|
+
### Structure
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
src/
|
|
54
|
+
├── lib/
|
|
55
|
+
│ ├── chat/
|
|
56
|
+
│ │ ├── chat-engine.ts # Core logic — depends on interfaces only
|
|
57
|
+
│ │ ├── types.ts # Shared types
|
|
58
|
+
│ │ └── interfaces/
|
|
59
|
+
│ │ ├── ai-provider.ts # IAIProvider interface
|
|
60
|
+
│ │ ├── storage-provider.ts # IStorageProvider interface
|
|
61
|
+
│ │ ├── mcp-provider.ts # IMCPProvider interface
|
|
62
|
+
│ │ ├── vision-provider.ts # IVisionProvider interface
|
|
63
|
+
│ │ └── logger.ts # ILogger interface
|
|
64
|
+
│ └── chat-providers/
|
|
65
|
+
│ ├── bedrock-ai-provider.ts # IAIProvider → Bedrock
|
|
66
|
+
│ ├── firebase-storage-provider.ts # IStorageProvider → Firebase
|
|
67
|
+
│ ├── mcp-provider.ts # IMCPProvider → MCP SDK
|
|
68
|
+
│ └── google-vision-provider.ts # IVisionProvider → Google
|
|
69
|
+
└── durable-objects/
|
|
70
|
+
└── ChatRoom.ts # Assembles and injects providers
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Code Example
|
|
74
|
+
|
|
75
|
+
#### Step 1: Define Provider Interfaces
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
// src/lib/chat/interfaces/ai-provider.ts
|
|
79
|
+
import type { ChatEngineMessage } from '../types'
|
|
80
|
+
|
|
81
|
+
export interface IAIProvider {
|
|
82
|
+
streamChat(params: StreamChatParams): Promise<void>
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface StreamChatParams {
|
|
86
|
+
messages: ChatMessage[]
|
|
87
|
+
systemPrompt: string
|
|
88
|
+
tools: Tool[]
|
|
89
|
+
onMessage: (message: ChatEngineMessage) => void
|
|
90
|
+
executeTool: (toolName: string, toolInput: any) => Promise<any>
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface ChatMessage {
|
|
94
|
+
role: 'user' | 'assistant'
|
|
95
|
+
content: string | ContentBlock[]
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface Tool {
|
|
99
|
+
name: string
|
|
100
|
+
description: string
|
|
101
|
+
input_schema: {
|
|
102
|
+
type: 'object'
|
|
103
|
+
properties: Record<string, any>
|
|
104
|
+
required?: string[]
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
// src/lib/chat/interfaces/storage-provider.ts
|
|
111
|
+
|
|
112
|
+
export interface IStorageProvider {
|
|
113
|
+
saveMessage(params: SaveMessageParams): Promise<Message>
|
|
114
|
+
loadMessages(params: LoadMessagesParams): Promise<Message[]>
|
|
115
|
+
ensureConversation(params: EnsureConversationParams): Promise<string>
|
|
116
|
+
getConversation(params: GetConversationParams): Promise<Conversation | null>
|
|
117
|
+
updateConversation(params: UpdateConversationParams): Promise<void>
|
|
118
|
+
generateTitle?(params: GenerateTitleParams): Promise<string> // Optional
|
|
119
|
+
addToolCall(params: AddToolCallParams): Promise<string>
|
|
120
|
+
updateToolCall(params: UpdateToolCallParams): Promise<void>
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface SaveMessageParams {
|
|
124
|
+
userId: string
|
|
125
|
+
conversationId: string
|
|
126
|
+
message: Message
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface LoadMessagesParams {
|
|
130
|
+
userId: string
|
|
131
|
+
conversationId: string
|
|
132
|
+
limit?: number
|
|
133
|
+
startAfter?: string
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
// src/lib/chat/interfaces/mcp-provider.ts
|
|
139
|
+
|
|
140
|
+
export interface IMCPProvider {
|
|
141
|
+
getAvailableServers(params: GetAvailableServersParams): Promise<MCPServer[]>
|
|
142
|
+
connectToServers(params: ConnectToServersParams): Promise<MCPConnection[]>
|
|
143
|
+
getTools(connections: MCPConnection[]): Promise<Tool[]>
|
|
144
|
+
executeTool(params: ExecuteToolParams): Promise<any>
|
|
145
|
+
disconnect(connections: MCPConnection[]): Promise<void>
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
// src/lib/chat/interfaces/vision-provider.ts
|
|
151
|
+
|
|
152
|
+
export interface IVisionProvider {
|
|
153
|
+
analyzeImage(params: AnalyzeImageParams): Promise<VisionResult>
|
|
154
|
+
processImagesInMessage(params: ProcessImagesParams): Promise<MessageContent>
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
#### Step 2: Create Engine with DI
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
// src/lib/chat/chat-engine.ts
|
|
162
|
+
import type { IAIProvider } from './interfaces/ai-provider'
|
|
163
|
+
import type { IStorageProvider } from './interfaces/storage-provider'
|
|
164
|
+
import type { IMCPProvider } from './interfaces/mcp-provider'
|
|
165
|
+
import type { IVisionProvider } from './interfaces/vision-provider'
|
|
166
|
+
import type { ILogger } from './interfaces/logger'
|
|
167
|
+
|
|
168
|
+
interface ChatEngineConfig {
|
|
169
|
+
aiProvider: IAIProvider
|
|
170
|
+
storageProvider: IStorageProvider
|
|
171
|
+
mcpProvider?: IMCPProvider // Optional — not all apps need MCP
|
|
172
|
+
visionProvider?: IVisionProvider // Optional — not all apps need vision
|
|
173
|
+
logger: ILogger
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export class ChatEngine {
|
|
177
|
+
private ai: IAIProvider
|
|
178
|
+
private storage: IStorageProvider
|
|
179
|
+
private mcp?: IMCPProvider
|
|
180
|
+
private vision?: IVisionProvider
|
|
181
|
+
private logger: ILogger
|
|
182
|
+
|
|
183
|
+
constructor(config: ChatEngineConfig) {
|
|
184
|
+
this.ai = config.aiProvider
|
|
185
|
+
this.storage = config.storageProvider
|
|
186
|
+
this.mcp = config.mcpProvider
|
|
187
|
+
this.vision = config.visionProvider
|
|
188
|
+
this.logger = config.logger
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async processMessage(params: ProcessMessageParams): Promise<void> {
|
|
192
|
+
const { userId, conversationId, message, onMessage } = params
|
|
193
|
+
|
|
194
|
+
// 1. Save user message
|
|
195
|
+
const savedMessage = await this.storage.saveMessage({
|
|
196
|
+
userId, conversationId, message: { ...message, role: 'user' }
|
|
197
|
+
})
|
|
198
|
+
onMessage({ type: 'user_message_saved', message: savedMessage })
|
|
199
|
+
|
|
200
|
+
// 2. Load history
|
|
201
|
+
const history = await this.storage.loadMessages({ userId, conversationId })
|
|
202
|
+
|
|
203
|
+
// 3. Get tools (if MCP available)
|
|
204
|
+
let tools: Tool[] = []
|
|
205
|
+
if (this.mcp) {
|
|
206
|
+
const servers = await this.mcp.getAvailableServers({ userId })
|
|
207
|
+
const connections = await this.mcp.connectToServers({ servers, userId })
|
|
208
|
+
tools = await this.mcp.getTools(connections)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// 4. Stream AI response
|
|
212
|
+
await this.ai.streamChat({
|
|
213
|
+
messages: history,
|
|
214
|
+
systemPrompt: this.buildSystemPrompt(userId),
|
|
215
|
+
tools,
|
|
216
|
+
onMessage,
|
|
217
|
+
executeTool: async (name, input) => {
|
|
218
|
+
if (!this.mcp) throw new Error('No MCP provider')
|
|
219
|
+
return this.mcp.executeTool({ toolName: name, toolInput: input, connections: [] })
|
|
220
|
+
}
|
|
221
|
+
})
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async loadMessages(params: LoadMessagesParams) {
|
|
225
|
+
return this.storage.loadMessages(params)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
#### Step 3: Inject Implementations in Durable Object
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
// src/durable-objects/ChatRoom.ts
|
|
234
|
+
import { ChatEngine } from '@/lib/chat'
|
|
235
|
+
import {
|
|
236
|
+
BedrockAIProvider,
|
|
237
|
+
FirebaseStorageProvider,
|
|
238
|
+
MCPProvider,
|
|
239
|
+
GoogleVisionProvider
|
|
240
|
+
} from '@/lib/chat-providers'
|
|
241
|
+
|
|
242
|
+
export class ChatRoom extends DurableObject {
|
|
243
|
+
private chatEngine: ChatEngine
|
|
244
|
+
|
|
245
|
+
constructor(state: DurableObjectState, env: Env) {
|
|
246
|
+
super(state, env)
|
|
247
|
+
|
|
248
|
+
// Wire up all dependencies
|
|
249
|
+
this.chatEngine = new ChatEngine({
|
|
250
|
+
aiProvider: new BedrockAIProvider(),
|
|
251
|
+
storageProvider: new FirebaseStorageProvider(),
|
|
252
|
+
mcpProvider: new MCPProvider(),
|
|
253
|
+
visionProvider: new GoogleVisionProvider(),
|
|
254
|
+
logger: chatLogger
|
|
255
|
+
})
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
#### Step 4: Mock for Testing
|
|
261
|
+
|
|
262
|
+
```typescript
|
|
263
|
+
// src/__tests__/chat-engine.spec.ts
|
|
264
|
+
import { ChatEngine } from '@/lib/chat/chat-engine'
|
|
265
|
+
|
|
266
|
+
describe('ChatEngine', () => {
|
|
267
|
+
const mockAI: IAIProvider = {
|
|
268
|
+
streamChat: jest.fn(async ({ onMessage }) => {
|
|
269
|
+
onMessage({ type: 'chunk', content: 'Hello!' })
|
|
270
|
+
onMessage({ type: 'complete' })
|
|
271
|
+
})
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const mockStorage: IStorageProvider = {
|
|
275
|
+
saveMessage: jest.fn(async (params) => ({ ...params.message, id: 'msg-1' })),
|
|
276
|
+
loadMessages: jest.fn(async () => []),
|
|
277
|
+
ensureConversation: jest.fn(async () => 'conv-1'),
|
|
278
|
+
getConversation: jest.fn(async () => null),
|
|
279
|
+
updateConversation: jest.fn(),
|
|
280
|
+
addToolCall: jest.fn(async () => 'tc-1'),
|
|
281
|
+
updateToolCall: jest.fn(),
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
it('should process message through providers', async () => {
|
|
285
|
+
const engine = new ChatEngine({
|
|
286
|
+
aiProvider: mockAI,
|
|
287
|
+
storageProvider: mockStorage,
|
|
288
|
+
logger: { info: jest.fn(), error: jest.fn(), warn: jest.fn(), debug: jest.fn() }
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
const messages: any[] = []
|
|
292
|
+
await engine.processMessage({
|
|
293
|
+
userId: 'user1',
|
|
294
|
+
conversationId: 'conv1',
|
|
295
|
+
message: { content: 'Hi', role: 'user' },
|
|
296
|
+
onMessage: (msg) => messages.push(msg)
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
expect(mockStorage.saveMessage).toHaveBeenCalled()
|
|
300
|
+
expect(mockAI.streamChat).toHaveBeenCalled()
|
|
301
|
+
expect(messages).toContainEqual({ type: 'chunk', content: 'Hello!' })
|
|
302
|
+
})
|
|
303
|
+
})
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
## Benefits
|
|
309
|
+
|
|
310
|
+
### 1. Portable Business Logic
|
|
311
|
+
Core engine depends on interfaces, not implementations — extractable to npm package.
|
|
312
|
+
|
|
313
|
+
### 2. Provider Swapping
|
|
314
|
+
Change `BedrockAIProvider` to `OpenAIProvider` in one place (the constructor).
|
|
315
|
+
|
|
316
|
+
### 3. Full Testability
|
|
317
|
+
All external dependencies are mockable via interface substitution.
|
|
318
|
+
|
|
319
|
+
### 4. Incremental Adoption
|
|
320
|
+
Optional providers (MCP, vision) can be added later without changing existing code.
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
## Trade-offs
|
|
325
|
+
|
|
326
|
+
### 1. Interface Overhead
|
|
327
|
+
**Downside**: Additional files and indirection for each provider type.
|
|
328
|
+
**Mitigation**: Only create interfaces for dependencies you actually need to swap or mock.
|
|
329
|
+
|
|
330
|
+
### 2. Param Object Verbosity
|
|
331
|
+
**Downside**: Parameter objects are more verbose than positional arguments.
|
|
332
|
+
**Mitigation**: Provides better readability and extensibility (add fields without breaking callers).
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
## Anti-Patterns
|
|
337
|
+
|
|
338
|
+
### ❌ Anti-Pattern 1: Direct Imports in Engine
|
|
339
|
+
|
|
340
|
+
```typescript
|
|
341
|
+
// ❌ BAD: Engine directly imports implementation
|
|
342
|
+
import { BedrockAIProvider } from '@/lib/chat-providers'
|
|
343
|
+
|
|
344
|
+
export class ChatEngine {
|
|
345
|
+
private ai = new BedrockAIProvider() // Tight coupling!
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// ✅ GOOD: Engine accepts interface
|
|
349
|
+
export class ChatEngine {
|
|
350
|
+
constructor(config: { aiProvider: IAIProvider }) {
|
|
351
|
+
this.ai = config.aiProvider
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
### ❌ Anti-Pattern 2: God Interface
|
|
357
|
+
|
|
358
|
+
```typescript
|
|
359
|
+
// ❌ BAD: One interface for everything
|
|
360
|
+
interface IChatProvider {
|
|
361
|
+
streamChat(): void
|
|
362
|
+
saveMessage(): void
|
|
363
|
+
loadMessages(): void
|
|
364
|
+
analyzeImage(): void
|
|
365
|
+
getTools(): void
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// ✅ GOOD: Separate interfaces per concern
|
|
369
|
+
interface IAIProvider { streamChat(): void }
|
|
370
|
+
interface IStorageProvider { saveMessage(): void; loadMessages(): void }
|
|
371
|
+
interface IVisionProvider { analyzeImage(): void }
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
---
|
|
375
|
+
|
|
376
|
+
## Related Patterns
|
|
377
|
+
|
|
378
|
+
- **[Durable Objects WebSocket](./tanstack-cloudflare.durable-objects-websocket.md)**: DOs inject providers into engines
|
|
379
|
+
- **[Library Services Pattern](./tanstack-cloudflare.library-services.md)**: Services can implement storage interfaces
|
|
380
|
+
- **[Zod Schema Validation](./tanstack-cloudflare.zod-schema-validation.md)**: Schemas define data shapes passed through interfaces
|
|
381
|
+
|
|
382
|
+
---
|
|
383
|
+
|
|
384
|
+
## Checklist for Implementation
|
|
385
|
+
|
|
386
|
+
- [ ] Interfaces defined in `interfaces/` directory
|
|
387
|
+
- [ ] Each interface covers one capability (single responsibility)
|
|
388
|
+
- [ ] Methods use typed parameter objects
|
|
389
|
+
- [ ] Optional providers marked with `?` in config
|
|
390
|
+
- [ ] Engine constructor accepts config object with all providers
|
|
391
|
+
- [ ] Implementations in separate `providers/` or `chat-providers/` directory
|
|
392
|
+
- [ ] Assembly (wiring) happens in Durable Object or server entry point
|
|
393
|
+
- [ ] Tests mock all providers via interface
|
|
394
|
+
- [ ] No direct implementation imports in engine code
|
|
395
|
+
|
|
396
|
+
---
|
|
397
|
+
|
|
398
|
+
**Status**: Stable - Core architectural pattern for pluggable systems
|
|
399
|
+
**Recommendation**: Use when building features with external dependencies that may change
|
|
400
|
+
**Last Updated**: 2026-02-28
|
|
401
|
+
**Contributors**: Patrick Michaelsen
|